Chapter 1: Foundations: The Agent Loop and the Tool-Use Primitive
Learning Objectives
Trace a complete tool_use/tool_result cycle through the Messages API, identifying every message role and content block involved.
Explain how stop_reason="tool_use" drives the agentic loop and where control returns to application code.
Design a minimal but correct agent loop in Python that handles multiple tool calls per turn.
Distinguish the model's responsibilities (deciding which tool, with what arguments) from the application's responsibilities (executing tools, returning results).
Pre-Quiz: What an Agent Actually Is
Why does tool use make Claude behave "less like a text generator and more like a function you call"?
Because Claude compiles the tool code and runs it inside the model
Because Claude emits a structured request that your code executes, giving text a way to trigger real actions
Because tools replace the model's language abilities with deterministic logic
Because the API switches to a separate, non-conversational endpoint for tools
In the blindfolded-coach analogy, what does it mean that "the coach never touches wood; the assistant never invents strategy"?
The model executes tools while the app decides which tool to use
The model reasons about which move to make while the application performs the physical action
Both sides share responsibility for executing and reasoning equally
The analogy shows the boundary is a bug to be engineered away
You need to extract structured fields from a document using a single, known tool and schema. Which approach fits best?
Build a full agent loop keyed on stop_reason
A workflow with several chained model calls decided at run time
Force a single tool call and read the result — no loop needed
A single-shot completion with no tools at all
What an Agent Actually Is
Key Points
An agent is just three things: a language model, a set of tools it may call, and a loop that runs until the work is done.
The tool-use primitive turns a text model into an actor — Claude describes what to do; your code does it.
Claude never executes anything itself: it emits a structured request, your code runs the operation, and the result flows back as a message.
The model owns reasoning (intent, tool choice, arguments); your application owns acting (I/O, auth, side effects, results).
Reach for an agent only when the sequence of steps is genuinely unpredictable — otherwise a single-shot completion or a fixed workflow is better.
Strip away the marketing and an "agent" is a small idea: a language model, a set of tools it is allowed to call, and a loop that keeps handing control back and forth until the work is done. Memory, planning, and autonomy are emergent behaviors of running that loop repeatedly. The tool-use primitive is what turns a text model into an actor — without tools, a model can only describe how to check the weather; it cannot check it.
This separation is the load-bearing wall of the whole book. The model owns reasoning: interpreting intent, selecting a tool, and composing well-formed arguments. Your code owns acting: executing the tool, handling the file system, network, authentication, and side effects, and returning a faithful result. Because execution lives entirely in your code, tool use is inherently sandboxable — the model can request a database delete, but your code can refuse. Security, rate limiting, and human-in-the-loop approval all live on your side of the line.
Figure 1.1: The reasoning/acting boundary — the model emits a request, the application executes and returns a result, control ping-pongs across a single dividing line.
flowchart LR
subgraph Model["Model (reasoning)"]
Intent["Interpret user intent"]
Select["Choose which tool"]
Args["Compose arguments"]
end
subgraph App["Application (acting)"]
Exec["Execute tool: I/O, network, DB"]
Auth["Handle auth and permissions"]
Result["Format and return result"]
end
Intent --> Select --> Args
Args -->|"tool_use request"| Exec
Exec --> Auth --> Result
Result -->|"tool_result"| Intent
Responsibility
Owned by the model
Owned by your application
Understand the user's request
Yes
No
Choose which tool to call
Yes
No
Fill in the tool's arguments
Yes
No
Actually run the tool (I/O, network, DB)
No
Yes
Handle auth, secrets, permissions
No
Yes
Format the result and return it
No
Yes
Decide whether the task is finished
Shared (via stop_reason)
Shared (reads stop_reason)
Not everything that calls a model is an agent. A single-shot completion is one request and one response — a classifier or summarizer. A workflow is a fixed, developer-authored sequence of model calls; the control flow is decided in advance by you. An agent is different in kind: the model itself decides the control flow at run time, and the loop continues until it signals it is done.
Dimension
Single-shot
Workflow
Agent
Number of model calls
One
Fixed, known in advance
Dynamic, unknown until done
Who decides the next step
N/A
Your code
The model
Tools involved
None
Optional, orchestrated by you
Central; model selects them
Loop
None
None (linear)
Yes, keyed on stop_reason
Best for
Deterministic transforms
Predictable pipelines
Open-ended tasks needing judgment
Visual animation — coming soon
Post-Quiz: What an Agent Actually Is
Why does tool use make Claude behave "less like a text generator and more like a function you call"?
Because Claude compiles the tool code and runs it inside the model
Because Claude emits a structured request that your code executes, giving text a way to trigger real actions
Because tools replace the model's language abilities with deterministic logic
Because the API switches to a separate, non-conversational endpoint for tools
In the blindfolded-coach analogy, what does it mean that "the coach never touches wood; the assistant never invents strategy"?
The model executes tools while the app decides which tool to use
The model reasons about which move to make while the application performs the physical action
Both sides share responsibility for executing and reasoning equally
The analogy shows the boundary is a bug to be engineered away
You need to extract structured fields from a document using a single, known tool and schema. Which approach fits best?
Build a full agent loop keyed on stop_reason
A workflow with several chained model calls decided at run time
Force a single tool call and read the result — no loop needed
A single-shot completion with no tools at all
Pre-Quiz: Anatomy of the tool_use / tool_result Cycle
Why is the description field called the single most important factor in tool performance?
It is the only field the API validates for length
It is the raw material from which the model decides whether and when to call the tool
It replaces the need for an input_schema entirely
It determines the tool_use_id the model generates
A newcomer is surprised that a tool_result block is placed inside a user-role message. What is the reason?
It is an API quirk with no conceptual meaning
Conversationally, your application is speaking on behalf of the tool back to the model
Tool results must always be assistant-role but the SDK relabels them
The user must manually type each tool result into the chat
Your loop reads a response and needs to decide whether to continue. What should it inspect?
The natural-language text in the assistant's content blocks
Whether the response contains the word "done"
The deterministic stop_reason field
The length of the content array
Anatomy of the tool_use / tool_result Cycle
Key Points
Everything flows through one endpoint — POST /v1/messages. Tools are just a tools parameter on an ordinary request, not a separate API.
Each tool has three core fields: name, a rich description, and an input_schema (JSON Schema).
The assistant turn carries a tool_use block with a unique id; the follow-up user turn carries a tool_result block whose tool_use_id must match exactly.
The Messages API is stateless — you resend the full conversation each turn, preserving raw content blocks so the tool_use_id linkage survives.
stop_reason — not the prose — is the authoritative signal: tool_use means continue, end_turn means stop, max_tokens means truncated.
Tools are simply a tools parameter attached to an ordinary message request — the entire agentic machinery is built on the same stateless request/response endpoint you already use for plain text. Each tool entry has a name (matching ^[a-zA-Z0-9_-]{1,64}$), a detailed description (aim for three to four or more sentences: what it does, when to use it and when not to, and what each parameter means), and an input_schema — a JSON Schema object declaring the arguments' type, properties, and which are required. Setting strict: true guarantees inputs conform exactly but requires additionalProperties: false.
When Claude calls a tool, its response content contains a tool_use block — often preceded by a natural-language text block — carrying type: "tool_use", a unique id (e.g. toolu_01A09...), the tool name, and an input object. Never depend on the leading text's exact wording; it varies and can be absent. After your code runs the tool, you return a tool_result block inside a user-role message — conversationally, you are speaking on behalf of the tool. Its tool_use_id must exactly match the originating id, or the API rejects the request.
Figure 1.2: The four-message cycle — user question, assistant tool_use, user tool_result, assistant final answer — with the tool_use_id as the thread stitching call to result. Think of it as a coat-check ticket: hand over your coat, get ticket toolu_01A09, present the same ticket to retrieve exactly the right coat.
sequenceDiagram
participant App as Application
participant Claude as Claude (Messages API)
App->>Claude: user message ("What's the weather?")
Claude-->>App: assistant turn: text + tool_use (id=toolu_01A09)
Note over App: Run the tool locally
App->>Claude: user message: tool_result (tool_use_id=toolu_01A09)
Claude-->>App: assistant turn: final answer (stop_reason=end_turn)
Every response carries a stop_reason — the authoritative, deterministic signal for what your loop should do next. Never parse natural-language text to decide whether to continue.
stop_reason
Meaning
What your code does
end_turn
Claude finished naturally
Exit the loop; return the answer
tool_use
Claude wants to call one or more tools
Execute them; continue the loop
max_tokens
Hit the limit; output truncated
Increase the limit or stream
stop_sequence
Hit a custom stop sequence
Handle per your design
pause_turn
A server-side tool loop paused
Resend the conversation to resume
refusal
Claude declined for safety
Handle gracefully
Figure 1.3: Branching on stop_reason — the deterministic signal that decides whether your loop continues, stops, or raises.
flowchart TD
Resp["Read response.stop_reason"]
Resp --> Check{"Which value?"}
Check -->|"tool_use"| Continue["Execute tools; continue loop"]
Check -->|"end_turn"| Stop["Return final answer; exit loop"]
Check -->|"max_tokens"| Trunc["Truncated: raise max_tokens or stream"]
Check -->|"stop_sequence"| Custom["Handle per your design"]
Check -->|"pause_turn"| Resume["Resend conversation to resume"]
Check -->|"refusal"| Refuse["Handle gracefully"]
Continue --> Resp
Visual animation — coming soon
Post-Quiz: Anatomy of the tool_use / tool_result Cycle
Why is the description field called the single most important factor in tool performance?
It is the only field the API validates for length
It is the raw material from which the model decides whether and when to call the tool
It replaces the need for an input_schema entirely
It determines the tool_use_id the model generates
A newcomer is surprised that a tool_result block is placed inside a user-role message. What is the reason?
It is an API quirk with no conceptual meaning
Conversationally, your application is speaking on behalf of the tool back to the model
Tool results must always be assistant-role but the SDK relabels them
The user must manually type each tool result into the chat
Your loop reads a response and needs to decide whether to continue. What should it inspect?
The natural-language text in the assistant's content blocks
Whether the response contains the word "done"
The deterministic stop_reason field
The length of the content array
Pre-Quiz: The Agent Loop in Code
Why must you append the assistant's raw content blocks to history rather than flattening them to text?
Raw blocks are smaller and save tokens
Flattening to text breaks the tool_use_id linkage and the next request fails
The API rejects any assistant message that contains text
Raw blocks let the model skip re-reading earlier turns
Claude returns two tool_use blocks in one assistant turn. What is the correct way to return their results?
One tool_result per following user message, sent as two separate messages
Both tool_result blocks together in a single following user message
Merge both results into one tool_result block with a shared id
Return them in an assistant-role message so the model can read them
A tool fails because a prerequisite call had not completed. What is the recommended handling?
Silently drop the tool_result so Claude does not see the failure
Crash the loop and restart the whole conversation
Return a tool_result with is_error: true and a descriptive message so Claude can recover
Retry the tool locally until it succeeds without telling the model
The Agent Loop in Code
Key Points
The loop is small: while stop_reason == "tool_use", execute the tools and continue the conversation.
Because Claude cannot run your code, every client-tool call is a round trip — your application, not the API, drives the loop.
Preserve raw assistant content blocks as you accumulate history; flattening to text breaks the tool_use_id linkage.
Obey the one-user-message rule: return exactly one tool_result per tool_use, all together in a single user message.
Never silently drop a result — surface failures as is_error: true so the model can acknowledge and recover.
The canonical five steps: (1) send a request with the tools array and user message; (2) Claude responds with stop_reason: "tool_use" and one or more tool_use blocks; (3) execute each tool and format outputs as tool_result blocks; (4) send a new request containing the prior messages, the assistant's response (with tool_use blocks preserved), and a user message of tool_result blocks; (5) repeat while stop_reason == "tool_use". The while loop is your code — the moment stop_reason comes back as tool_use, control has returned to your application; only the next create call hands it back to the model. This ping-pong is the loop.
Figure 1.4: The agent loop — call the model, branch on stop_reason, execute tools, append results, and repeat until end_turn.
flowchart TD
Start(["User message + tools"]) --> Call["client.messages.create(...)"]
Call --> Branch{"stop_reason == tool_use?"}
Branch -->|"No (end_turn)"| Done(["Return final answer"])
Branch -->|"Yes"| AppendA["Append raw assistant content (preserve tool_use blocks)"]
AppendA --> Run["Execute every tool_use block"]
Run --> Collect["Collect tool_result blocks (matched by tool_use_id)"]
Collect --> AppendU["Append one user message with all results"]
AppendU --> Call
Claude 4 models make parallel calls whenever a request benefits from multiple tools, so a single response can contain several tool_use blocks. Independent, read-only operations are typically run concurrently for lower latency; tools with side effects or ordering requirements are safer run sequentially. Whatever strategy you pick, obey the one-user-message rule — splitting results across multiple user messages "teaches" Claude to stop making parallel calls, the most common cause of lost parallelism. Also place every tool_result block before any text in that user message, or the API returns a 400.
Tools fail — networks time out, inputs are malformed, prerequisites are missing. The rule is to never silently drop a tool_result. Return is_error: true with an informative message; the error text is input to the model's next reasoning step, so make it descriptive. Claude typically retries with a different approach or reissues the call next turn. The SDK Tool Runner (beta) can drive the whole loop for you and auto-wraps raised exceptions as is_error results; reach for the manual loop when you need control over batching, ordering, custom error handling, or human-in-the-loop approval.
Visual animation — coming soon
Post-Quiz: The Agent Loop in Code
Why must you append the assistant's raw content blocks to history rather than flattening them to text?
Raw blocks are smaller and save tokens
Flattening to text breaks the tool_use_id linkage and the next request fails
The API rejects any assistant message that contains text
Raw blocks let the model skip re-reading earlier turns
Claude returns two tool_use blocks in one assistant turn. What is the correct way to return their results?
One tool_result per following user message, sent as two separate messages
Both tool_result blocks together in a single following user message
Merge both results into one tool_result block with a shared id
Return them in an assistant-role message so the model can read them
A tool fails because a prerequisite call had not completed. What is the recommended handling?
Silently drop the tool_result so Claude does not see the failure
Crash the loop and restart the whole conversation
Return a tool_result with is_error: true and a descriptive message so Claude can recover
Retry the tool locally until it succeeds without telling the model
Pre-Quiz: How Claude Decides to Call a Tool
You want to force Claude to return a specific JSON structure by calling one named tool and reading its input. Which tool_choice fits?
{"type": "auto"}
{"type": "none"}
{"type": "tool", "name": "..."}
{"type": "any"}
Why are any and tool incompatible with extended thinking?
They prefill the assistant turn to force a tool call, suppressing the reasoning preamble
Extended thinking requires exactly one tool in the array
They double the token cost beyond the thinking budget
They disable the input_schema validation thinking depends on
Claude keeps passing malformed, type-mismatched arguments to a tool. What is the most likely root cause and fix?
The model is broken; switch to a different model family
An under-constrained schema; tighten input_schema, set strict: true, add input_examples
The loop is not preserving raw content; re-append the blocks
tool_choice is set to none; switch it to auto
How Claude Decides to Call a Tool
Key Points
Selection is a reasoning problem — the model's raw material is the tool description and input_schema. You are writing the documentation from which the model decides.
By default Claude calls a tool when the request maps to a described capability and the answer is not already in context; the system prompt steers this up or down.
tool_choice gives deterministic control: auto (decide), any (must use some tool), tool (force a named tool), none (forbid tools).
Forcing one named tool is the key pattern for structured output — no multi-turn loop required.
Most "model" failures are specification failures: weak descriptions, wrong tool_choice, or missing schema constraints — all yours to fix.
The model's raw material for selection is the tool description and input_schema. A thin description like "gets data" forces the model to guess; a rich one lets it match user intent to capability confidently. In effect, you are not calling the tool — you are writing the documentation from which the model decides whether to call it. Under the default setting, Claude calls a tool when the request maps to a described capability and the answer is not already in context; "Use the tools to investigate before responding" pushes tool use up, while "Use your judgment..." keeps it conservative.
tool_choice
Behavior
Default when
{"type": "auto"}
Claude decides whether and which tool to call
Tools are provided
{"type": "any"}
Claude must use some tool (its choice)
—
{"type": "tool", "name": "..."}
Claude must use that specific tool
—
{"type": "none"}
Claude may not use any tool
No tools are provided
Figure 1.5: The tool_choice decision — how the four modes gate whether and which tool Claude may call.
flowchart TD
Need{"What control do you need?"}
Need -->|"Let Claude decide"| Auto["tool_choice: auto (default with tools)"]
Need -->|"Must call some tool"| Any["tool_choice: any"]
Need -->|"Must call one specific tool"| Tool["tool_choice: tool (name=...)"]
Need -->|"Forbid all tools"| None["tool_choice: none (default with no tools)"]
Auto --> AutoOut["Optional text preamble, then maybe tool_use"]
Any --> Prefill["Assistant turn prefilled: no text preamble"]
Tool --> Prefill
Tool --> Structured["Common pattern: force one tool for structured output"]
None --> Text["Text-only response"]
Two behaviors are easy to trip over. With any or tool, the API prefills the assistant turn to force a tool call, so Claude will not emit natural-language text before the tool_use block. And with extended thinking, only auto and none are supported — any and tool return an error, precisely because they suppress the reasoning preamble that thinking depends on. In auto mode Claude often emits a natural-language text block first — a lightweight chain of thought that improves tool selection and argument quality.
Failure mode
What it looks like
Typical remedy
Hallucinated tool
"Calls" a tool never defined
Rely on stop_reason/schema, not prose; strengthen prompt
"Use your judgment..."; sharpen the "when not to use" text
Under-calling
Answers from memory when it should fetch
"Use the tools to investigate..."; or force with any
Lost parallelism
One tool_use block when several warranted
Return all results in one user message; add parallel-efficiency line
Batched dependent calls
Parallel calls that actually depend on each other
Add "Only batch tool calls that are independent"
The unifying lesson: most "model" failures are specification failures. Wrong arguments usually mean an under-constrained schema; over- and under-calling usually mean an ambiguous description or the wrong tool_choice. Because you own the specification, you own most of the fix. Guard the one genuinely unrecoverable case — a hallucinated tool name — in code: your run_tool dispatcher should return a clear is_error result for any unknown name rather than crashing, letting the model self-correct next turn.
Visual animation — coming soon
Post-Quiz: How Claude Decides to Call a Tool
You want to force Claude to return a specific JSON structure by calling one named tool and reading its input. Which tool_choice fits?
{"type": "auto"}
{"type": "none"}
{"type": "tool", "name": "..."}
{"type": "any"}
Why are any and tool incompatible with extended thinking?
They prefill the assistant turn to force a tool call, suppressing the reasoning preamble
Extended thinking requires exactly one tool in the array
They double the token cost beyond the thinking budget
They disable the input_schema validation thinking depends on
Claude keeps passing malformed, type-mismatched arguments to a tool. What is the most likely root cause and fix?
The model is broken; switch to a different model family
An under-constrained schema; tighten input_schema, set strict: true, add input_examples
The loop is not preserving raw content; re-append the blocks