SEC. 01The Problem
Agents feel like magic until you see the loop. A lot of training jumps straight to frameworks, and learners end up copying code they can't reason about when something goes wrong.
The idea underneath is small. The model never does anything itself. It asks for a tool, your code runs it, and the result goes back to the model as text. Then you do it again, until the model stops asking. I wanted a way to make that loop visible, and to give it a little tension, so people remember it.
SEC. 02The Design
The lab has three modes, and they run in the order I'd teach them:
- Watch. A worked example. A scripted agent crosses the lake while every message, the loop code and the token count update in step, with a caption explaining each move in plain language.
- You're the model. The roles flip. The lake turns to fog, and you get only what a model gets: a system prompt, five tool definitions and a growing list of messages. It's the fastest way I know to feel why tool descriptions and error messages matter.
- Live with Claude. The real thing. Paste an API key and the same loop calls Claude from your browser, with the real requests, thinking summaries and token usage on screen.
Five concepts light up along the bottom as you meet them: tool use, parallel calls, error recovery, growing context and stop reasons. They work like learning objectives you can watch fill in.
SEC. 03Design Decisions
- The safety rule lives in the tool. The system prompt says to measure before moving, but the
movetool refuses anyway. Learners see anis_errorresult come back and the agent recover, which is how real agents should be built. - The model has to reason, not peek. Each lake is generated from a seed, and the tools return plain JSON: surface hints that can lie, ice measurements, and a sonar that only says which way reads stronger.
- Watch mode is labeled for what it is. It runs a small rule-based agent written in JavaScript. It reads only the transcript and answers in the same message format the API uses. Live mode is the real Claude.
- Cost and memory are visible. A bar for every request shows the history being resent each turn. In Live mode the bars split into fresh and cached tokens, because the request turns on prompt caching.
SEC. 04How It's Built
It's a static page with no server behind it. The lake is a single plane in three.js, and one custom fragment shader draws nearly everything on it: the ice, snow, cracks, open water, drill holes, sonar pings and the fog over whatever the agent hasn't learned yet. On devices without WebGL, it switches to a lighter 2D view.
The agent loop is about forty lines of plain JavaScript, and it's the same loop in every mode. Only the function that produces the model's turn changes:
// loop.js, with the page's animation hooks taken out for (let turn = 1; turn <= maxTurns; turn++) { const response = await callModel({ system: SYSTEM_PROMPT, tools: TOOLS, messages }); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason !== "tool_use") return { stop: response.stop_reason, messages }; const results = []; for (const block of response.content) { if (block.type !== "tool_use") continue; const out = runTool(lake, block.name, block.input); results.push({ type: "tool_result", tool_use_id: block.id, content: out.text, ...(out.isError ? { is_error: true } : {}) }); } messages.push({ role: "user", content: results }); }
Live mode loads the Anthropic TypeScript SDK only when you use it. It calls Claude Opus 5.5 by default with adaptive thinking, low effort and prompt caching, and opts into Anthropic's server-side fallback in case a request is declined. The key stays in the tab's memory and goes only to api.anthropic.com. I built the whole thing with Claude Code.
SEC. 05The Code
Every source file is readable on this site.
The 4 inch rule is a game rule, loosely based on common ice safety guidance. Always check local conditions before you go out on real ice.