In course B, one line did the looping for you: stopWhen: isStepCount(3). This lesson takes that line apart. You'll write the loop by hand, about 30 lines, talking straight to Claude's Messages API, watch it run in a test, then delete it. After this, stopWhen will never be magic again.
Verified @anthropic-ai/sdk 1.4.0tsx 4.23.15wrangler 4.157.0
Start from c-01 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout c-01
1. Two tools for a script
The loop runs as a script on your machine, in Node, not in the Worker. So it needs two dev dependencies: Anthropic's SDK, and tsx, which runs a TypeScript file directly. From klippy/:
pnpm --filter agent add -D --save-exact @anthropic-ai/sdk@1.4.0 tsx@4.23.15Then three small edits in apps/agent:
package.json: add"type": "module",under"private": true. Without it,tsxrefuses theawaitat the top of the script (we hit exactly that error first).package.json: add a script,"loop": "tsx --env-file=.dev.vars scripts/loop.ts".--env-fileloads your.dev.vars, so the script can read your key.tsconfig.json: add"scripts/**/*.ts"toinclude, sopnpm typecheckchecks the script too.
This one script talks to Claude directly, with your own Anthropic key, not through the gateway. Add it to apps/agent/.dev.vars (never commit that file):
echo 'ANTHROPIC_API_KEY=sk-ant-...' >> .dev.vars2. One tool, the way the API wants it
Make apps/agent/scripts/loop.ts. First the tool. In course B, tool() and zod built this for you. Here you write what actually goes over the wire: a name, a description, and a JSON schema for the input.
get_time takes no input (properties: {}), so Claude only has to decide whether to call it.
3. The loop
Now the loop itself. Read it slowly: this is what every agent framework runs underneath.
messagesis the whole conversation. Each turn sends all of it: Claude remembers nothing between calls.maxTurnsis yourstopWhen. Without it, a model that keeps asking for tools would loop, and bill you, forever.stop_reasonsays why Claude stopped."tool_use"means "run a tool for me and tell me what happened". Anything else ("end_turn","max_tokens","refusal") means this turn is over: return the text.
4. Act, then observe
When Claude asks for a tool, two messages go back on the pile:
Claude's turn, whole. Its
contentholds the tool calls, each with anid. Send them back as they came, not just the text.Every result, in one user message, each matched to its call by
tool_use_id. That's the observe step: ground truth from your code, which Claude reads on the next turn.If the loop runs out of turns, it says so, instead of pretending.
The AI SDK's names map straight onto these: stop_reason: "tool_use" is its finishReason: "tool-calls", and maxTurns is stopWhen.
5. Run it
The end of the file runs the loop when you call the script:
In apps/agent:
pnpm loop "What time is it right now?"You should see turn 1: stop_reason = tool_use, then turn 2: stop_reason = end_turn, then Claude's answer with the time.
What we recorded, and what we didn't. We haven't recorded this run with Claude yet: the recording had no Anthropic key. With a deliberately wrong key, the call does reach the API, which proves the wiring:
If you see Top-level await is currently not supported with the "cjs"
output format, you skipped "type": "module" in step 1.
6. Prove it without a key
A test can stand in for Claude with a fake client that answers tool_use first and end_turn second. Copy test/loop.spec.ts from the repo, then run it:
The second test is the important one: a model that never stops asking gets cut off at maxTurns.
7. Now delete it
You've seen the loop. From here on, the AI SDK and Cloudflare's agent run it for you, with streaming, approvals and saved history on top. Next lesson starts by deleting scripts/loop.ts, its test, the loop script and the two dev dependencies.
Your code matches tag c-02 (changes from c-01).