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.
The request
Section titled “The request”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 reply that asks for a tool
Section titled “The reply that asks for a tool”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.
The first reply
Section titled “The first reply”Run the first step, and compare what you see with the output below.
python3 wire.py first_reply{
"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].
Map the fake reply onto the real one
Section titled “Map the fake reply onto the real one”The lesson compares a fake model that returns {'tool': 'get_weather', 'args': {'city': 'Lisbon'}} or {'answer': '...'} with a real API reply, which has a stop_reason field and a content list of typed blocks, one of which may be a tool_use block with id, name and input.
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.
Which field of the fake reply did the loop test to know whether to run a tool, and which field did it pass to the function?
The result goes back as a user message
Section titled “The result goes back as a user message”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}], }The tool result message
Section titled “The tool result message”Run the second step, and compare what you see with the output below.
python3 wire.py tool_result{
"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].
Where does the tool result go?
Section titled “Where does the tool result go?”The lesson shows the message a loop sends back to the Claude API after running a tool the model asked for.
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?
Which roles does the Claude API accept in a message list, and what links a result to the request it answers?
The second request repeats everything
Section titled “The second request repeats everything”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.
The final reply
Section titled “The final reply”Run the last step, and compare what you see with the output below.
python3 wire.py final_replymessages 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.
How does the loop know it is done?
Section titled “How does the loop know it is done?”The lesson shows two API replies: one with stop_reason tool_use and a tool_use block, and one with stop_reason end_turn and only a text block.
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?
What did the fake loop test, and which real field carries that decision?
Which rules does the second request follow?
Section titled “Which rules does the second request follow?”The Claude API answers a tool request with a tool_use block (id, name, input) and expects the result back as a tool_result block in a user message.
The loop has run the function and builds the next request. Which of these does the vendor page require?
Which value does the loop copy from the reply into the message it sends back, and where in the list does that message have to sit?
What reaches the model?
Section titled “What reaches the model?”A tool definition sent to the Claude API has a name, a description, and an input_schema. The function runs in the developer's own code.
You define get_weather for a real model API. Which part of the tool is sent in the request?
Which parts of the fake model's TOOLS dict were text, and which part was code?
Put the round trip in order
Section titled “Put the round trip in order”The lesson follows one tool call through the Claude API: the request with the tool definitions, the model's reply, the result the loop sends back, and the model's answer.
- Send the messages with the tool definitions
- The model replies with a `tool_use` block and `stop_reason` `tool_use`
- Your loop runs the function with the block's input
- Send a `user` message with a `tool_result` that quotes the id
- The model replies with text and `stop_reason` `end_turn`
What does the model need before it can ask, and what does your reply have to quote?
From one tool to many systems
Section titled “From one tool to many systems”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
- A tool definition sends the model a name, a description, and an input schema, and the function stays in your code [3].
- A model asks for a tool with a
tool_useblock that has an id, a name and an input, and astop_reasonoftool_use. Your loop sends the result back in ausermessage as atool_resultblock that quotes the id, and the whole history goes with it, because the model keeps no state [1]. end_turnis where the fake model’sanswerkey went, and thestop_reasonfield is what a real loop branches on [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
References
Section titled “References”- Anthropic. Handle tool calls. Claude Platform documentation. Reference.
Claude docs handle-tool-calls - Anthropic. Messages. Claude Platform documentation. Reference.
Claude docs messages - Anthropic. Define tools. Claude Platform documentation. Reference.
Claude docs define-tools - Anthropic. Stop reasons and fallback. Claude Platform documentation. Reference.
Claude docs handling-stop-reasons - Anthropic. Building with the Claude API. Claude Academy. Course.
Academy building-with-the-claude-api - Anthropic. Introduction to Model Context Protocol. Claude Academy. Course.
Academy introduction-to-model-context-protocol - 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