Skip to content

What a tool call looks like on the wire

In the concepts course you named the turns of the agent loop: observe, think, act. This lesson shows what the think step looks like when it leaves the model as bytes. A model can’t run a function. What it can do is write a message that says “run this tool with these arguments”, in a format the vendor documents, and function calling is the name for that mechanism [1]. The next lesson, “Building your first agent”, hides the format behind a fake model that returns {"tool": "get_weather", "args": {"city": "Lisbon"}}. Here we look at the real messages behind that fake reply, so that when you swap the fake model for a real one you know which field to read.

Every example runs against a fixture: fake_api, a function that answers the way the Claude API is documented to answer, so the output is the same every time. The complete program is site/examples/building-agents/how-a-model-calls-a-tool/wire.py in the repository. Copy it to a folder of your own, and each step below is python3 wire.py <step> in that folder. The messages follow the Claude API.

A request to a stateless model API contains everything the model gets to know [2]. For a tool-using agent that’s a system prompt, a list of tool definitions, and the message list so far. A tool definition has a name, an input_schema, which is a JSON Schema object that names each parameter and its type, and a description. The first two are required, and the description is optional [2]. The description is the text the model reads to decide when to call the tool, and the vendor advises a long one. It explains the tool’s purpose, the situations that call for it and those that don’t, and the meaning of each parameter [3].

TOOLS = [
{
"name": "get_weather",
"description": "Current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, for example Lisbon."}
},
"required": ["city"],
},
},
]
def first_request(question):
return {
"model": "fake-model",
"max_tokens": 200,
"system": "You answer weather questions. Use the tool for live data.",
"tools": TOOLS,
"messages": [{"role": "user", "content": question}],
}

The function itself isn’t in the request. The model gets the name, the description and the schema, and the function stays on your side of the wire. The next lesson’s fake model keeps the function and a one-line description per tool and skips the schema. Here the schema tells the model that city is a required string, and the API rejects a definition without one.

The model reads the request and decides. When it wants the tool, the reply has a stop_reason of tool_use and a content list with a tool_use block in it [1]. The block has an id that’s unique to this call, the name of the tool, and an input object that fits the tool’s input_schema. A text block may come before it, and the model often writes one sentence about what it is about to do.

Example · run it

Run the first step, and compare what you see with the output below.

Terminal window
python3 wire.py first_reply
Output
{
  "id": "msg_01",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "text",
      "text": "I'll check the weather in Lisbon."
    },
    {
      "type": "tool_use",
      "id": "toolu_01",
      "name": "get_weather",
      "input": {
        "city": "Lisbon"
      }
    }
  ]
}

Output verified in CI from site/examples/building-agents/how-a-model-calls-a-tool/first_reply.py.

The fixture’s ids are short so you can read them. A real reply has a longer id, a toolu_ prefix followed by a run of letters and digits such as toolu_01Kx7mQp2rVdN8sLwe4Tzb9c [1]. Your loop never makes one up. It copies the id it was given.

Put the fake reply next to this one. "tool": "get_weather" is the name field of the tool_use block, and "args": {"city": "Lisbon"} is its input. The fake model had no id, because it only ever asked for one tool at a time. It had no stop reason either. The loop tested for the answer key instead. The real API tells you why it stopped in a field of its own, and the stop_reason value is what your loop branches on [4].

Checkpoint · match

Each row names a part of the fake model’s reply. Pick the field of the real API reply that plays the same role. One option isn’t used.

Your loop finds the tool_use block, runs the function it names with the input as arguments, and sends the result back. The Claude API has no tool role. The result goes in a message with role user, as a tool_result block whose tool_use_id quotes the id of the request it answers [1]. The content is the result as a string, or a list of content blocks when the result is more than text.

def tool_result_message(reply):
block = next(b for b in reply["content"] if b["type"] == "tool_use")
result = get_weather(**block["input"])
return {
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": block["id"], "content": result}],
}
Example · run it

Run the second step, and compare what you see with the output below.

Terminal window
python3 wire.py tool_result
Output
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01",
      "content": "27°C, sun"
    }
  ]
}

Output verified in CI from site/examples/building-agents/how-a-model-calls-a-tool/tool_result.py.

The next lesson’s fake loop stores the same result as {"role": "tool", "content": "27°C, sun"}. The real message says the same thing in three more fields, and the one that matters is tool_use_id. When the model asks for two tools in one reply, the ids are how it matches each result to the request it made. A tool that fails sends the error text in content and sets is_error to true, and the model reads the error and answers with it in mind [1].

Checkpoint · choice

The model asked for get_weather in a tool_use block with id toolu_01. Your loop ran the function and got 27°C, sun. How does the result travel back?

The model doesn’t keep state between calls [5] [2]. The second request repeats the system prompt, the tool definitions, and the whole message list: the user’s question, the assistant’s reply with its tool_use block, and the user message with the tool_result. The tool_result message must come directly after the assistant message that holds the tool_use block, with no message between them [1].

def second_request(request, reply):
messages = request["messages"] + [
{"role": "assistant", "content": reply["content"]},
tool_result_message(reply),
]
return dict(request, messages=messages)

The reply to that request has stop_reason end_turn and a text block with the answer [4]. This is the fake model’s {"answer": ...} reply, and end_turn is where the fake model’s answer key went.

Example · run it

Run the last step, and compare what you see with the output below.

Terminal window
python3 wire.py final_reply
Output
messages in the second request: 3
{
  "id": "msg_02",
  "role": "assistant",
  "stop_reason": "end_turn",
  "content": [
    {
      "type": "text",
      "text": "It is 27°C, sun there."
    }
  ]
}

Output verified in CI from site/examples/building-agents/how-a-model-calls-a-tool/final_reply.py.

One tool call took two requests and left three messages in the list. Each round of the loop sends the list again with two more messages on the end, and the model’s answer reads as if it remembered, because you sent it the memory.

Checkpoint · choice

Your loop has sent the second request and received a reply. Which check tells it to return what the model wrote to the user rather than go round again?

The messages above are the whole mechanism, and for one tool and one agent they’re enough. The cost shows up when you have many agents and many tools. Each agent has its own loop that turns a tool_use block into a function call, and each tool (a calendar, a ticket system, a database) has its own way of being called. With N agents and M tools you write N x M adapters, and a change to one tool means a change in all the agents that use it. This is the N x M integration problem [6]. A shared protocol between agents and tools turns N x M into N plus M: each agent speaks the protocol once, and each tool is wrapped once [7]. The last lesson in this course, on agent protocols, is about that protocol. For now, notice that the tool_use and tool_result blocks are already a small protocol between one model and your loop, and the rest of the course builds on them.

Exercise

Take the fake model’s reply {"tool": "get_weather", "args": {"city": "Lisbon"}} from the lesson “Building your first agent” and the tool’s result 27°C, sun. Without running the fixture, write down the three real API messages they become: the assistant reply with its tool_use block and an id you choose, the user message with the tool_result that quotes that id, and the final assistant reply. Then mark which field of the final reply plays the role of the fake model’s answer key.

A good result: the three messages have the roles assistant, user, assistant in that order, the tool_use_id in the second matches the id in the first, the input in the first is {"city": "Lisbon"}, and you marked stop_reason with the value end_turn as the field that says the model is done. Compare with the fixture’s output when you are finished. Reflect: which of the three messages did the fake loop write itself, and which did it receive?

Stretch: Change tool_result_message so the tool result reports an error with is_error set to true, and write the final reply the model would give. Check the field names against the vendor page on handling tool calls.

Recap

  1. A tool definition sends the model a name, a description, and an input schema, and the function stays in your code [3].
  2. A model asks for a tool with a tool_use block that has an id, a name and an input, and a stop_reason of tool_use. Your loop sends the result back in a user message as a tool_result block that quotes the id, and the whole history goes with it, because the model keeps no state [1].
  3. end_turn is where the fake model’s answer key went, and the stop_reason field is what a real loop branches on [4].
  4. Every agent that calls every tool its own way costs N x M adapters, which is the problem a shared protocol solves [6].

You can now

  • Defines a tool with a schema the model uses correctly

  1. Anthropic. Handle tool calls. Claude Platform documentation. Reference. Claude docs handle-tool-calls
  2. Anthropic. Messages. Claude Platform documentation. Reference. Claude docs messages
  3. Anthropic. Define tools. Claude Platform documentation. Reference. Claude docs define-tools
  4. Anthropic. Stop reasons and fallback. Claude Platform documentation. Reference. Claude docs handling-stop-reasons
  5. Anthropic. Building with the Claude API. Claude Academy. Course. Academy building-with-the-claude-api
  6. Anthropic. Introduction to Model Context Protocol. Claude Academy. Course. Academy introduction-to-model-context-protocol
  7. Addy Osmani, Ivar Soares Urdalen, Leo Simons. Tools, giving agents hands: function calling, schema design, the N x M problem. Agent Engineer Course. Course. AEC-03