Skip to content

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

The model never runs the tool. It emits a request - "call 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.

You declare tools

Tap a node to see what it does.

Function calling is a protocol: the model asks, your code executes, the result returns to context.

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.

  1. Request plus tools. A user message plus the tools[] schema goes to the model.
  2. stop_reason: tool_use. The model stops and emits a call, for example get_weather(location), instead of a finished answer.
  3. Your app executes. It parses the arguments and runs the real function, get_weather("SF").
  4. The tool returns. A value like "15C, sunny" comes back to your app.
  5. Append and re-invoke. Your app appends the result as a tool_result and calls the model again.
  6. 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"]
  }
}
The same get_weather tool, Anthropic style, where the schema nests under input_schema.
{
  "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 for OpenAI, where strict mode requires additionalProperties false and every property listed in required.

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

Because the model only sees the name, description, and schema, the description is the highest-leverage lever on tool-call accuracy. Spell out when to reach for the tool, what each parameter means, the units, and edge cases. Vague descriptions cause missed or wrong calls. The JSON Schema constrains shape (types, enums, required fields) but cannot express relational rules like "if 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?" }
  ]
}
Turn 1 request (you to model): the tools plus the conversation ship together.

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" }
    }
  ]
}
Turn 1 response (model to you): stop_reason tool_use, with the id you must echo back.

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"
      }
    ]}
  ]
}
Turn 2 request (you to model): echo the assistant turn, then append the tool_result with the matching id.

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"
}
Turn 2 request, error variant (the API 503'd): is_error true so the model can retry or tell the user.

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

OpenAI returns a 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 answer
Chaining the round trip: loop until the model stops asking for tools. Your code is the only thing that executes.

Willison 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

An agent can ping-pong tool calls forever, or a mis-designed tool can trigger endless retries. Bound every loop by max iterations, max tool calls, or a token/dollar budget. A runaway loop is the most common way a tool-using agent burns money in production.

Steering the model: tool_choice and parallelism

tool_choice controls whether and what the model may call this turn:

You wantOpenAIAnthropic
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"}
tool_choice, in both dialects: let the model decide, force some tool, force one named tool, or forbid tools.

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\"}" } }
]
One turn, two independent calls (OpenAI): run them concurrently and wall-clock cost approaches the slowest single call.

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

Description written like a prompt (when to use it, what it returns, units, edge cases); minimal, typed parameters (prefer 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

Terse description ("gets weather") the model cannot situate; free-text params where an enum belonged; overlapping tools the model picks wrong between; a tool for stable knowledge or arithmetic the model already does; forced tool_choice when the answer needed a question.

When to add a tool (and when not to)

Add one when the answer needs fresh, private, or precise data the model cannot have memorized (weather, your DB, current prices, a user's files), when you need a side effect (send email, create ticket, run code), or to turn language into a typed action. Do not add one for stable knowledge, arithmetic the model can do, or creative writing - every extra tool costs tokens and adds a chance to mis-call.

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]

MoveWhat it doesPayoff
Tool SearchMark 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 callingThe 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 ExamplesAttach concrete example calls to teach formatting, nested structures, and optional-parameter correlations that JSON Schema cannot express.Parameter-handling accuracy 72% to 90%.
Anthropic's three moves for wielding hundreds of tools cheaply and reliably.

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.

drop here

constrains the shape of the arguments

drop here

tells the model when to call it

drop here

matches a result back to its request

drop here

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_choice steers whether and what to call; parallelize only independent calls to cut latency.
  • Design the ACI like a prompt, use strict schemas, 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

  1. 1.Simon Willison, Large Language Models can run tools (2025)
  2. 2.Anthropic, Tool use with Claude (overview)
  3. 3.OpenAI, Function calling guide
  4. 4.Anthropic, Advanced tool use
  5. 5.Anthropic, Programmatic tool calling
  6. 6.Tam et al., Let Me Speak Freely? Format restrictions and LLM performance (2024)