Chapter 1: Foundations: The Agent Loop and the Tool-Use Primitive

Learning Objectives

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

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
ResponsibilityOwned by the modelOwned by your application
Understand the user's requestYesNo
Choose which tool to callYesNo
Fill in the tool's argumentsYesNo
Actually run the tool (I/O, network, DB)NoYes
Handle auth, secrets, permissionsNoYes
Format the result and return itNoYes
Decide whether the task is finishedShared (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.

DimensionSingle-shotWorkflowAgent
Number of model callsOneFixed, known in advanceDynamic, unknown until done
Who decides the next stepN/AYour codeThe model
Tools involvedNoneOptional, orchestrated by youCentral; model selects them
LoopNoneNone (linear)Yes, keyed on stop_reason
Best forDeterministic transformsPredictable pipelinesOpen-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

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_reasonMeaningWhat your code does
end_turnClaude finished naturallyExit the loop; return the answer
tool_useClaude wants to call one or more toolsExecute them; continue the loop
max_tokensHit the limit; output truncatedIncrease the limit or stream
stop_sequenceHit a custom stop sequenceHandle per your design
pause_turnA server-side tool loop pausedResend the conversation to resume
refusalClaude declined for safetyHandle 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 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

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_choiceBehaviorDefault when
{"type": "auto"}Claude decides whether and which tool to callTools 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 toolNo 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 modeWhat it looks likeTypical remedy
Hallucinated tool"Calls" a tool never definedRely on stop_reason/schema, not prose; strengthen prompt
Wrong argumentsMalformed or type-mismatched inputTighten schema; strict: true + additionalProperties: false; add input_examples
Over-callingUses a tool when the answer was in context"Use your judgment..."; sharpen the "when not to use" text
Under-callingAnswers from memory when it should fetch"Use the tools to investigate..."; or force with any
Lost parallelismOne tool_use block when several warrantedReturn all results in one user message; add parallel-efficiency line
Batched dependent callsParallel calls that actually depend on each otherAdd "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
tool_choice is set to none; switch it to auto

Your Progress

Answer Explanations