# What a tool call looks like on the wire

> The request, the tool_use reply, the tool_result message and the final answer of one real API tool call, mapped onto the fake model of the first agent lesson.

Page: https://schubergphilis.github.io/ai-training/building-agents/how-a-model-calls-a-tool/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

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 (Handle tool calls, Claude Platform documentation). 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

A request to a stateless model API contains everything the model gets to
know (Messages, Claude Platform documentation). 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 (Messages, Claude Platform documentation). 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 (Define tools, Claude Platform documentation).

```python
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

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 (Handle tool calls, Claude Platform documentation). 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: The first reply

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

```sh
python3 wire.py first_reply
```

Output:

```text
{
  "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"
      }
    }
  ]
}
```

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` (Handle tool calls, Claude Platform documentation).
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
(Stop reasons and fallback, Claude Platform documentation).

#### Checkpoint: Map the fake reply onto the real one

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.

Statements:

- The `tool` key of the fake reply
- The `args` key of the fake reply
- The presence or absence of the `answer` key

Options:

- The `name` of the `tool_use` block
- The `input` of the `tool_use` block
- The `stop_reason` of the reply
- The `id` of the `tool_use` block

## 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 (Handle tool calls, Claude Platform documentation). The `content` is the result as a
string, or a list of content blocks when the result is more than text.

```python
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: The tool result message

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

```sh
python3 wire.py tool_result
```

Output:

```text
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01",
      "content": "27°C, sun"
    }
  ]
}
```

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
(Handle tool calls, Claude Platform documentation).

#### Checkpoint: Where does the tool result go?

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?

- In a message with role `tool`, with the result text as its content.
- In a message with role `user`, as a `tool_result` block that names the `tool_use_id`.
- In the `system` field of the next request, so the model sees it first.

## The second request repeats everything

The model doesn't keep state between calls
(Building with the Claude API, Claude Academy) (Messages, Claude Platform documentation).
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
(Handle tool calls, Claude Platform documentation).

```python
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 (Stop reasons and fallback, Claude Platform documentation). This is the fake
model's `{"answer": ...}` reply, and `end_turn` is where the fake model's
`answer` key went.

#### Example: The final reply

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

```sh
python3 wire.py final_reply
```

Output:

```text
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."
    }
  ]
}
```

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: How does the loop know it is done?

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 `content` list holds a `text` block with an answer in it.
- The message list has grown to three entries, so no tool round is left.
- The `stop_reason` of the reply is `end_turn`.

#### Pitfall: Dropping the assistant message from the history

A first loop often stores only the user's question and the tool result,
because those are the two things it wrote itself. The API then sees a
`tool_result` that doesn't follow the `tool_use` block it answers, and the
vendor page says a result block must come right after its request block,
so the request is rejected (Handle tool calls, Claude Platform documentation). Rule: append the assistant reply as it
came, `content` list and all, before you append the result. The history
must show the request that the result answers.

## 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**
(Introduction to Model Context Protocol, Claude Academy). 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 (Tools, giving agents hands: function calling, schema design, the N x M problem, Agent Engineer Course). 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 (Define tools, Claude Platform documentation).
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 (Handle tool calls, Claude Platform documentation).
3. `end_turn` is where the fake model's `answer` key went, and the
   `stop_reason` field is what a real loop branches on
   (Stop reasons and fallback, Claude Platform documentation).
4. Every agent that calls every tool its own way costs N x M adapters,
   which is the problem a shared protocol solves
   (Introduction to Model Context Protocol, Claude Academy).

## References

- Handle tool calls, Claude Platform documentation, https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls
- Messages, Claude Platform documentation, https://platform.claude.com/docs/en/api/messages
- Define tools, Claude Platform documentation, https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools
- Stop reasons and fallback, Claude Platform documentation, https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons
- Building with the Claude API, Claude Academy, https://academy.claude.com/courses/building-with-the-claude-api
- Introduction to Model Context Protocol, Claude Academy, https://academy.claude.com/courses/introduction-to-model-context-protocol
- Tools, giving agents hands: function calling, schema design, the N x M problem, Agent Engineer Course, https://lsimons.github.io/agent-engineer-course/03-tools-giving-agents-hands/
