The Agent Loop and Acting
Tool use and function calling
Schemas, tool calls, error recovery
The loop from the last lesson gave your agent a heartbeat. This lesson gives it hands: the exact protocol by which a model - which can only produce text - reaches out and touches the real world. Master this round trip and you can wire a model to any API, database, or piece of code you own.
The model has no hands
Start from an uncomfortable fact: a large language model cannot fetch a webpage, query your database, or send an email. It only produces tokens. (the same thing vendors call function calling) is the protocol that closes that gap. Simon Willison's minimal definition: "you tell the model that there are tools it can use, and have it output special syntax ... requesting a tool action, then stop." Your harness parses that request, runs the tool, and starts a new turn with the result appended.[1]
The one idea everything else hangs on
get_weather with {location: "Paris"}" - and stops. Your application executes the function and feeds the result back. That single seam is the whole game: it is why tool use is controllable, auditable, and safe to gate - and why "the model did X" is always shorthand for "the model asked my code to do X."The "special syntax" is not magic. Vendors fine-tuned models to emit calls in a specific serialization and surface it through the API as structured fields.[1] Under the hood the model is still just generating a JSON blob - which is why tool use and structured outputs (the next lesson) are two faces of the same coin.
Tap a node to see what it does.
The four-beat round trip
Every tool call is the same short dance between three players: the model, your app (the harness), and the tool (the outside world). The model talks only to your app; your app is the only thing that ever executes.
- Request plus tools. A user message plus the
tools[]schema goes to the model. - stop_reason: tool_use. The model stops and emits a call, for example
get_weather(location), instead of a finished answer. - Your app executes. It parses the arguments and runs the real function,
get_weather("SF"). - The tool returns. A value like
"15C, sunny"comes back to your app. - Append and re-invoke. Your app appends the result as a
tool_resultand calls the model again. - stop_reason: end_turn. The model reads the result and answers in plain text. Chain these and you have an agent.
Declaring a tool: the schema is the interface
You hand the model a list of tool definitions. Across vendors the shape is nearly identical - a name, a natural-language description, and a JSON Schema for the parameters. The model sees only these three things. It never sees your function body, so the description and schema are the entire contract.
{
"name": "get_weather",
"description": "Get the current weather in a given location. Use this
whenever the user asks about weather, temperature, or conditions for a
place. Returns a short text description and the temperature.",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state/country, e.g. 'San Francisco, CA'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit; defaults to the locale convention"
}
},
"required": ["location"]
}
}{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given location. Use this
whenever the user asks about weather, temperature, or conditions.",
"parameters": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City and state/country" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["location", "unit"],
"additionalProperties": false
},
"strict": true
}
}Same tool, two dialects. Anthropic nests the schema under input_schema; OpenAI nests it under function.parameters and, in strict mode, requires additionalProperties: false and every property listed in required. Cosmetic differences, identical intent.
The description is a prompt, so treat it like one
mode is express then address is required" - that gap is exactly what tool examples fill.[4]A real round trip, in JSON
Here is the full get_weather exchange, Anthropic-style, so you can see exactly what crosses the wire. Turn 1, your request ships the tools plus the conversation:
{
"model": "claude-...",
"max_tokens": 1024,
"tools": [ { /* get_weather definition from above */ } ],
"messages": [
{ "role": "user", "content": "What's the weather in San Francisco?" }
]
}The model decides it needs the tool. It stops with stop_reason: "tool_use" and puts a tool_use block in its content - note the id, you will need it:[2]
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "Let me check that for you." },
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "San Francisco, CA", "unit": "celsius" }
}
]
}Your code runs it - get_weather("San Francisco, CA", "celsius") returns "15C, sunny" - and you send Turn 2. Echo the model's own turn back into history, then append a user message carrying a tool_result block that references the same tool_use_id:
{
"messages": [
{ "role": "user", "content": "What's the weather in San Francisco?" },
{ "role": "assistant", "content": [ /* the text + tool_use block, echoed verbatim */ ] },
{ "role": "user", "content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "15 degrees celsius, sunny"
}
]}
]
}The model reads the result and finishes with stop_reason: "end_turn" and plain text - "It's currently 15C and sunny in San Francisco." The loop ends. When the tool fails, return the error in-band so the model can recover instead of your harness silently dying:[2]
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"is_error": true,
"content": "WeatherAPI returned 503 Service Unavailable"
}Now the model can retry, try a different city spelling, or tell the user it could not fetch the data - its choice.
OpenAI: same control flow, two gotchas
tool_calls array where function.arguments is a JSON-encoded string, not an object - you must JSON.parse it (a classic bug). You reply with a role: "tool" message whose tool_call_id matches the call's id.[3] Different field names, but the recipe is universal: detect call, execute, append result with matching id, re-invoke.Chaining calls is the agent
One round trip is a parlor trick. Chaining them is the agent. You just keep looping until the model stops asking for tools (end_turn or a normal stop):
def run(user_msg, tools):
messages = [system, user_msg]
while True:
resp = model(messages, tools)
if resp.wants_tool_calls:
messages.append(resp.assistant_turn) # keep the model's call in history
for call in resp.tool_calls:
result = execute(call.name, call.args) # YOUR code runs the tool
messages.append(tool_result(call.id, result))
continue
return resp.text # no tool requested -> final answerWillison notes such a chain "could potentially execute dozens of responses on the way to a final answer." His Datasette trace is a clean example of self-correction with no human in the loop: the model runs a SQL query, gets an error back as a tool result, calls schema() to learn the table structure, then issues a corrected query on the second attempt.[1] We built this skeleton in the agent-loop lesson; tool use is what makes each turn act.
Always cap the loop
Steering the model: tool_choice and parallelism
tool_choice controls whether and what the model may call this turn:
| You want | OpenAI | Anthropic |
|---|---|---|
| Model decides (default) | "auto" | {"type":"auto"} |
| Must call some tool | "required" | {"type":"any"} |
| Must call this tool | {"type":"function","name":"..."} | {"type":"tool","name":"..."} |
| No tools this turn | "none" | {"type":"none"} |
Forcing a call (any / required / named) guarantees a structured action - handy for a mandatory extraction - but it removes the model's option to ask a clarifying question or answer directly. Prefer auto and steer with the system prompt: a nudge like "use the tools to investigate before responding" raises tool use, while "use your judgment about whether to call a tool" keeps it conservative.[2]
Parallel tool calls. When actions are independent - "weather in Paris and Tokyo" - the model can emit several in one turn (multiple tool_use blocks for Anthropic; multiple entries in tool_calls for OpenAI, toggled by parallel_tool_calls, default true).[3]
"tool_calls": [
{ "id": "call_a", "function": { "name": "get_weather", "arguments": "{\"location\":\"Paris\"}" } },
{ "id": "call_b", "function": { "name": "get_weather", "arguments": "{\"location\":\"Tokyo\"}" } }
]Run both concurrently in your executor and wall-clock cost approaches the slowest single call instead of the sum (five 200 ms fetches: about 1 s sequential vs about 200 ms parallel). The one rule, the coupling test: only parallelize genuinely independent calls. Parallelize calls that depend on each other's output and you get racy, wrong results; serialize those.
Designing the interface: the ACI
You already know prompt engineering. Tool design is its twin - Anthropic calls the tool surface the . The model is a programmer whose only documentation is your names, descriptions, and schemas. Invest there as heavily as in the system prompt.
Well-designed tools
enum over free strings); few required fields over many optional ones; unambiguous, non-overlapping names; a strict schema so arguments cannot be malformed.Tools that mis-fire
tool_choice when the answer needed a question.When to add a tool (and when not to)
Two closing ACI notes. Schema conformance is not correctness: a strict weather tool can still return a confidently wrong temperature - the shape is guaranteed, the value is not. And let the model reason before you force it into a schema: hard format constraints measurably dent reasoning,[6] so put a free-text field first or split reasoning and formatting into two steps - the subject of the next lesson.
Scaling to hundreds of tools
"Can a model call one tool?" is solved. The 2025 to 2026 frontier is "how do agents wield hundreds of tools cheaply and reliably?" Anthropic's advanced tool use answers with three moves:[4]
| Move | What it does | Payoff |
|---|---|---|
| Tool Search | Mark rarely-used tools defer_loading: true; the model discovers and loads only what it needs at runtime instead of paying for every schema upfront. | Tool-def context 77K to 8.7K tokens (-85%); selection accuracy 79.5% to 88.1%. |
| Programmatic calling | The model writes Python that calls tools in a sandbox, processes their outputs in code, and returns only what matters, instead of one API round trip per call.[5] | 43.6K to 27.3K tokens (-37%); about 19+ inference passes eliminated. |
| Tool Use Examples | Attach concrete example calls to teach formatting, nested structures, and optional-parameter correlations that JSON Schema cannot express. | Parameter-handling accuracy 72% to 90%. |
Deferred tool loading collapses the context spent on tool schemas by about 85% - from roughly 77K tokens with all tools loaded to about 8.7K with Tool Search on - freeing the window for real work while raising selection accuracy.[4]
The open standard that makes tools portable across models is the Model Context Protocol (MCP), covered later. And the moment your agent can act, security matters: prompt injection rides in on tool results (a fetched page, an email, a DB row). Never blindly trust tool output, and gate side-effecting tools behind confirmation - the "lethal trifecta" of private data plus untrusted content plus exfiltration is exactly what tool-using agents combine.[1] A later lesson is dedicated to it.
Check yourself
Match each part of the tool protocol to what it does.
constrains the shape of the arguments
tells the model when to call it
matches a result back to its request
feeds the outcome back into context
A model returns a tool_use block for get_weather. What happens next?
In the OpenAI round trip, why is function.arguments a classic source of bugs, and what must you do with it?
What type does it actually arrive as? Try to state it, then check.
Lock it in
- A tool is declared as JSON - name plus description plus schema - and that is all the model sees; the description is a prompt.
- The model only requests a call and stops; your app executes and returns a
tool_result. Detect, execute, append (matching the id), re-invoke. - Return errors in-band (
is_error) so the model self-corrects; always cap the loop by iterations or budget. tool_choicesteers whether and what to call; parallelize only independent calls to cut latency.- Design the ACI like a prompt, use
strictschemas, and at scale reach for tool search plus programmatic calling.
Primary source
Anthropic, "Tool use with Claude"The single cleanest reference for the whole mechanic: stop_reason: "tool_use", the tool_use and tool_result blocks, tool_choice, strict tools, parallel calls, and error handling, with runnable examples.
Sources