Part of the Build Your Own Coding Agent tutorial. One issue = one chapter (chapters/06-tool-chains.md) plus its examples/06-tool-chains/ samples.
Goal (1 sentence): Build the core agent engine - a loop that drives client.messages.create until stop_reason === "end_turn" - handling sequential chains and parallel tool calls.
After this chapter you can
- Drive a multi-step agent loop that branches on
stop_reason, accumulates conversation history correctly, never breaks on the first response, and terminates cleanly.
- Handle both sequential tool chains (one result feeds the next call) and parallel
tool_use blocks executed (optionally concurrently) via Promise.all, collecting one tool_result per block.
- Reason about dependencies and ordering: return independent results in one batch; let the model naturally serialize dependent calls across turns.
- Steer the model with
tool_choice and extract a reusable runner with a handler registry, maxIterations guard, and is_error recovery.
What to cover (ONE paragraph, not a list)
Chapter 5 returned a single tool result; here you build the loop that keeps going. Start with the control-flow skeleton: branch on stop_reason (tool_use -> keep going, end_turn -> done, max_tokens/pause_turn -> handle) and never break on the first response - always append the full response.content before the matching tool_result blocks so every tool_use_id is answered exactly once. Then show sequential chaining - a tool whose output the model reads and feeds into a second call on the next iteration (for example, look up an ID, then fetch details for that ID) - followed by parallel tool use, where a single turn yields several tool_use blocks that you filter with b.type === "tool_use" and resolve optionally concurrently with Promise.all, collecting one tool_result per block. Cover ordering and dependencies: when results are independent return them in one batch; when one call depends on another the model naturally serializes across turns. Next, cover tool_choice modes (auto, any, tool + name, none) and disable_parallel_tool_use: true, noting that forcing a tool changes when end_turn arrives. Close by extracting a generic runner: a Map of tool name -> handler, the Anthropic.Tool[] definitions, a maxIterations cap, and is_error: true on failures; then wire the same runner to client.messages.stream + finalMessage(), and briefly note where betaZodTool + client.beta.messages.toolRunner fits and why writing the loop by hand first makes its behavior legible.
Going deeper (optional asides - keep OFF the main line)
betaZodTool + client.beta.messages.toolRunner: once you have written the loop by hand, the SDK helper is a thin wrapper - show the mapping in a callout so readers can choose either path.
Out of scope (defer - do NOT preview)
- Persisting conversation state across process restarts (later chapter on memory).
- Plugging the runner into the live Telegram surface from Chapter 3 (integration shown there, not here).
Code samples - examples/06-tool-chains/
Must-keep for a beginner (floor - never cut for brevity)
- The run command for
agent-loop.ts (the first sample).
- "Never hardcode your key; it comes from the environment" (once, in prose).
- That the full
response.content must be appended before any tool_result blocks - a beginner cannot infer this ordering from the type signatures.
- The one genuinely non-obvious gotcha: forcing a specific tool with
tool_choice means the model may never emit end_turn on that turn, so the loop condition must account for it.
Friendliness floor (never cut - terse is not friendly)
- The chapter addresses the reader as "you", never "the user" or "one".
- The intro AND at least one section open with a warm, second-person sentence.
Key APIs (flat list, reference only)
client.messages.create, client.messages.stream, finalMessage(), stop_reason (tool_use/end_turn/max_tokens/pause_turn), response.content, tool_use, tool_result, tool_use_id, is_error, tool_choice, Anthropic.MessageParam, client.beta.messages.toolRunner
Prerequisites
Chapter 5 (direct foundation); Chapter 2 helps for the streaming runner.
Definition of done
Part of the Build Your Own Coding Agent tutorial. One issue = one chapter (
chapters/06-tool-chains.md) plus itsexamples/06-tool-chains/samples.Goal (1 sentence): Build the core agent engine - a loop that drives
client.messages.createuntilstop_reason === "end_turn"- handling sequential chains and parallel tool calls.After this chapter you can
stop_reason, accumulates conversation history correctly, never breaks on the first response, and terminates cleanly.tool_useblocks executed (optionally concurrently) viaPromise.all, collecting onetool_resultper block.tool_choiceand extract a reusable runner with a handler registry,maxIterationsguard, andis_errorrecovery.What to cover (ONE paragraph, not a list)
Chapter 5 returned a single tool result; here you build the loop that keeps going. Start with the control-flow skeleton: branch on
stop_reason(tool_use-> keep going,end_turn-> done,max_tokens/pause_turn-> handle) and never break on the first response - always append the fullresponse.contentbefore the matchingtool_resultblocks so everytool_use_idis answered exactly once. Then show sequential chaining - a tool whose output the model reads and feeds into a second call on the next iteration (for example, look up an ID, then fetch details for that ID) - followed by parallel tool use, where a single turn yields severaltool_useblocks that you filter withb.type === "tool_use"and resolve optionally concurrently withPromise.all, collecting onetool_resultper block. Cover ordering and dependencies: when results are independent return them in one batch; when one call depends on another the model naturally serializes across turns. Next, covertool_choicemodes (auto,any,tool+ name,none) anddisable_parallel_tool_use: true, noting that forcing a tool changes whenend_turnarrives. Close by extracting a generic runner: aMapof tool name -> handler, theAnthropic.Tool[]definitions, amaxIterationscap, andis_error: trueon failures; then wire the same runner toclient.messages.stream+finalMessage(), and briefly note wherebetaZodTool+client.beta.messages.toolRunnerfits and why writing the loop by hand first makes its behavior legible.Going deeper (optional asides - keep OFF the main line)
betaZodTool+client.beta.messages.toolRunner: once you have written the loop by hand, the SDK helper is a thin wrapper - show the mapping in a callout so readers can choose either path.Out of scope (defer - do NOT preview)
Code samples - examples/06-tool-chains/
agent-loop.ts- minimal hand-written loop that runs untilend_turn.sequential-chain.ts- two tools where one result feeds the next call.parallel-tools.ts- multipletool_useblocks executed viaPromise.all.tool-choice.ts-auto/any/ forced tool /disable_parallel_tool_use.agent-runner.ts- reusable runner with handler registry,maxIterations, andis_error.runner-with-stream.ts- runner wired toclient.messages.stream+finalMessage().Must-keep for a beginner (floor - never cut for brevity)
agent-loop.ts(the first sample).response.contentmust be appended before anytool_resultblocks - a beginner cannot infer this ordering from the type signatures.tool_choicemeans the model may never emitend_turnon that turn, so the loop condition must account for it.Friendliness floor (never cut - terse is not friendly)
Key APIs (flat list, reference only)
client.messages.create,client.messages.stream,finalMessage(),stop_reason(tool_use/end_turn/max_tokens/pause_turn),response.content,tool_use,tool_result,tool_use_id,is_error,tool_choice,Anthropic.MessageParam,client.beta.messages.toolRunnerPrerequisites
Chapter 5 (direct foundation); Chapter 2 helps for the streaming runner.
Definition of done
chapters/06-tool-chains.md, <=120 lines, <=4 main-line H2s plus an optional "What's next" closer (pastewc -lANDgrep -c '^## 'in the PR).bun run, imported via<<< @/examples/06-tool-chains/file.ts, <=35 lines, comment:code <=0.30.@anthropic-ai/sdksurface; ASCII punctuation only.README.mdand the.vitepress/config.tssidebar;bun x vitepress buildpasses.