Docs / How Agents Talk to Models / Tool calls
Tool calls
tools → model replies with a tool_use → your code runs it → you send a tool_result in a new request → repeat until the model answers. Each trip is a separate billed call, and every call re-reads the growing transcript and the whole tools list. One “fix the bug” becomes four to a dozen calls.An agent that can read files, run tests, or hit an API does it through tool calls, and this is where call counts explode. You send the model a list of tools (name + JSON schema) in the request. Instead of a final answer, the model can reply with a tool_use block: “call Read with path=orders.py.” Your code — not the model — runs the tool, then you send the result back as a tool_result in a new request. The model reads it and continues. That's the agent loop:
The four payloads, in order
Abstractions hide this, so here it is on the wire — one bugfix, request and response, four real bodies. Watch three things: the model never answers in ②, it asks; request ③ is a brand-new call that echoes the assistant's own tool_use back and appends your tool_result; and the tools array rides along on every request, billed as input each time.
// ① REQUEST — tools declared, the task as a user message
{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"tools": [
{ "name": "Read", "description": "Read a file",
"input_schema": { "type": "object",
"properties": { "path": { "type": "string" } }, "required": ["path"] } },
{ "name": "Edit", "description": "Replace text in a file", "input_schema": { "...": "..." } }
],
"messages": [
{ "role": "user", "content": "Make POST /orders reject a missing total, with a test." }
]
}
// ② RESPONSE — not an answer. stop_reason: tool_use → the model wants you to run a tool
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "Let me read the file first." },
{ "type": "tool_use", "id": "toolu_01A",
"name": "Read", "input": { "path": "orders.py" } }
],
"usage": { "input_tokens": 480, "output_tokens": 38 }
}
// ③ REQUEST — a NEW call. Turn 1 kept verbatim, the assistant's tool_use echoed
// back, then YOUR tool_result appended. The tools array is re-sent, re-billed.
{
"model": "claude-sonnet-5",
"tools": [ "...the same two tools, sent again..." ],
"messages": [
{ "role": "user", "content": "Make POST /orders reject a missing total, with a test." },
{ "role": "assistant", "content": [
{ "type": "tool_use", "id": "toolu_01A",
"name": "Read", "input": { "path": "orders.py" } } ] },
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "toolu_01A",
"content": "def create_order(req): total = req.body['total'] # no presence check ...60 lines..." } ] }
]
}
// ④ RESPONSE — the model read the file and emits the fix as ANOTHER tool_use
{
"stop_reason": "tool_use",
"content": [
{ "type": "tool_use", "id": "toolu_02B", "name": "Edit",
"input": { "path": "orders.py", "old": "total = req.body['total']", "new": "if 'total' not in req.body: abort(400)" } }
],
"usage": { "input_tokens": 690, "output_tokens": 52 } // ← input grew: transcript is longer now
}The loop runs once more past ④ — you apply the Edit, run pytest, and send the pass/fail back as one more tool_result. Only then does the model return a plain-text answer with stop_reason: end_turn. That's four model calls for one “fix the bug,” each re-reading the tools array and a transcript that was longer than the last.
The same loop on a time axis
The loop diagram shows who talks to whom. This sequence view shows when, and marks exactly which arrows cost money — the two model round-trips are billed, the local tool run between them is not:
Two costs hide in this loop. The obvious one: a single task is now many calls. The quiet one: the tools array is part of the input on every call — a fat toolbelt taxes each round-trip. A four-call bugfix where every call re-reads a ~2k-token transcript and a 3-tool schema, run entirely on the strong model, lands around the baseline the routing pages use:
stop_reason: tool_use as an error or a finished answer. It's neither — it's the model handing control back to you to run the named tool and call again with the result. If your client stops there, the agent simply halts mid-task; if it forgets to echo the assistant's tool_use block into the next request, the provider rejects the tool_result as unmatched.