Firstpass v0.2.1
DocsGuaranteeBenchmarksCLIGitHub

Docs / How Agents Talk to Models / Tool calls

Part A · The core pathModule 4 of 13

Tool calls

A tool-using agent runs a loop: send tools → model replies with a tool_useyour 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 agent loop as a racetrack between two nodes. Left: the model, which reads the tools list and the growing transcript and is billed on every call. Right: your code, which runs the tool locally — not billed. Top track going right: the model replies with tool_use, asking your code to Read orders.py. Bottom track going left: your code sends a new request carrying the tool_result and the full transcript back to the model — this is a fresh billed call, and the transcript is larger than the last time. The loop repeats until the model returns a final answer instead of a tool_use. Each trip around the loop is a separate billed model call the model reads tools + transcript $ input billed 480 tok call 1 → 690 tok call 2 transcript grows each pass your code runs Read(orders.py) reads file from disk not billed no model call here model reply: tool_use · Read( path="orders.py" ) new request · tool_result + full transcript · $ billed again ↕ loop continues until the model stops calling tools or: final answer (end_turn) when the model has no more tool calls
The model never runs a tool itself — it asks, your code executes, you resend the result. So one “fix the bug” task becomes four, six, a dozen model calls, and every one of them re-reads the whole transcript and the tools list.

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.

json
// ① 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:

A sequence diagram of one tool-using task over time, with three lifelines: the client, the model, and the tool. Top to bottom: step one, the client sends a request carrying the tools list and the task to the model — billed as input. Step two, the model replies with a tool_use block — billed as output. Step three, the client runs the tool locally — no model call, not billed. Step four, the tool returns its output to the client. Step five, the client sends a new request carrying the tool_result plus the whole grown transcript — billed as input, and larger than step one. Step six, the model returns the final answer — billed as output. Only arrows that touch the model are billed, and the transcript grows every round trip. One task, over time — every arrow that touches the model is a billed call client model tool · your code ① request · tools + task $ input ② tool_use · Read(orders.py) $ output ③ run Read locally ④ file contents back ③④ never touch the model — no tokens billed ⑤ new request · tool_result + whole transcript $ input — bigger ⑥ final answer · or next tool_use $ output transcript at ① — the task alone transcript at ⑤ — task + tool_use + result, and it keeps growing every trip
The same loop on a time axis: two billed model round-trips (①② and ⑤⑥) with an unbilled local tool run (③④) between them. Step ⑤ is drawn fatter than ① on purpose — the transcript grew. A real bugfix stacks several of these before the final answer, which is why one task costs like four.

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:

4–12
model calls in one “fix the bug” task
×N
the tools list billed as input every call
$0.063
4-call bugfix, all on the strong model (illustrative)
Key idea The loop is where the meter and multi-turn compound. Every tool round-trip is a full billed call (the token meter) that re-reads a transcript which is longer than the last (multi-turn) plus the entire tools list. Call count × growing input is the multiplier that turns a one-cent question into a real bill — and the calls in a loop are wildly uneven in difficulty, which is the whole opening for Part B.
Common mistake Reading 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.
A tool-using task is a loop of billed round-trips — model asks, your code runs the tool, you resend the result — so one task costs like many, each re-reading a bigger transcript and the full tools list.