Lesson 2: The Full Round-Trip of a Tool Call
Learning goals:
- Name the key fields the request and the response each carry in one tool-call round-trip
- Tell whether a piece of tool_use / tool_result code is matched up correctly
- Spot a data dependency between calls in the same parallel batch, and know when to split the calls into two rounds
- Explain why the phrase "the model calls a tool" is itself inaccurate
Prerequisites: You've read Lesson 1 and know why agents need tools | Prev: Lesson 1 << | Next: Lesson 3 >>
Start With Three Chunks of JSON
You're building a support bot. A user asks, "Can you check where my order ORD-2026-8842 is?" Your code sends that message to the model along with a tool definition:
Notice the new tools field. It isn't a message; it's a manifest that tells the model what tools it has on hand, what each one looks like, and what parameters each one needs.1 You have to send this manifest with every request — the model doesn't "remember" it, so your code has to include it each time.
The model reads the manifest and, instead of answering the order status directly, returns something like this:
Two new things show up here: stop_reason has become "tool_use", and the content array has a new block with type: "tool_use". The model hasn't looked up any order information — it doesn't even know where the order system lives. It's just saying, "I need you to call get_order_status for me with these parameters, then tell me the result."
Your code takes over from here, actually queries the order system, gets a result, and packs that result into the next request to send back:
Notice what got added: the model's full reply from the previous round is dropped back into messages verbatim, followed by a new user message. That message doesn't hold text the user typed — it holds a type: "tool_result" block whose tool_use_id matches exactly the id the model just handed you.
Only after seeing this request does the model finally say something like, "Your order is on its way out of the Shanghai transit center, with delivery expected on August 27." Three chunks of JSON, three role switches: the model makes a request, your code runs it, the result gets fed back. That's the whole of one tool-call round-trip.
stop_reason Is a Signal, Not an Execution Record
Here's the thing beginners get wrong most often: they assume stop_reason: "tool_use" means the tool has already been called. It hasn't. It's just the model's reason for why it stopped when it finished this message, the same kind of field as "end_turn" (it's done talking) or "max_tokens" (it ran out of room), only with a different value.2
The model never touches a database, fires an HTTP request, or runs a shell command on its own. All it can do is emit a structured request; the rest of the work falls to your code or Anthropic's servers.3 That's why tools split into "client tools" (the host application runs them) and "server tools" (Anthropic runs them on your behalf) — the difference is only about who runs this step, not about whether the model can run it itself.2
The Three Fields in a tool_use Block, None Optional
Look back at that tool_use block. Only three of its fields are required:4
id: the unique identifier for this call, in the form toolu_01XYZ.... It has exactly one job — matching things up when you send the result back later.
name: the tool the model picked, which has to match the name of one of the tools in your tools manifest exactly.
input: an object holding the parameters for this call, shaped to satisfy the rules you defined in input_schema.
Put those three fields together and you have everything the model can express: "I want to call the name tool with this id, and here's the input." It won't tack on logic like "retry three times" — you write that yourself in the host code. How to design a tool interface so the model makes fewer parameter mistakes is Lesson 3's territory; this lesson only cares about how these three fields get packed in and read back out.
tool_result Matches Up by tool_use_id
A single reply from the model can hold more than one tool_use block. Say the user asks, "Can you check where my order ORD-2026-8842 is, and also check whether ORD-2026-9001 has shipped?" The model puts two tool_use blocks in the same content array, and stop_reason is still "tool_use".
Your code has to look up both orders, then, in the same user message, drop both results into the content array together, with each tool_result claiming its call by its own tool_use_id:
If you take a shortcut and send one round with just the first tool_use_id on its own, the model refuses to continue the conversation, because "the previous round had a tool_use block that never got its tool_result" — both blocks have to be claimed together in the next user message; you can't split them across two requests and send them back in batches.5 The tool_result block also has an optional is_error field: set it to true when the tool fails, and the model knows this call ran into a problem.4
Calls in the Same Batch Can't See Each Other's Results
Now that the batched-return rule is settled, there's a deeper trap: the data dependency between tool_use blocks in the same batch.
Switch scenarios. A money-transfer agent is set up with two tools: read_balance(account_id) reads the balance, and withdraw(account_id, amount) moves money out. The user says, "Move $100 out of A001, if there's enough." In a single response, the model gives back two tool_use blocks: read_balance({"account_id": "A001"}) and withdraw({"account_id": "A001", "amount": 100}).
Look at withdraw's amount: 100, copied straight from the number in the user's sentence, with no relationship to whether the balance is enough. This isn't the model being lazy; it has no choice. At the moment it generates this response, read_balance is still just "something it plans to do" — its return value doesn't even exist yet, so withdraw can't read it. Within one batch of tool_use blocks, no call can see the results of the others in that batch, because those results haven't been executed or sent back at that point.
So here's a line you have to hold yourself: if a write operation's parameter should, in theory, equal the return value of a read operation in the same batch, those two calls shouldn't appear in the same response. The genuinely safe approach is to split them into two rounds: run only read_balance first, send the real balance back as a tool_result, and once the model sees "the balance is only 60," let it decide whether to call withdraw and for how much.
Three tactics that actually work:
- Write the precondition into the tool description. Add a line to
withdraw's description: "only call after you've seen the latest balance returned by read_balance." The tool description is itself part of the prompt the model can read, which is far more reliable than hoping the model figures out the dependency on its own.6
- Turn off parallelism with disable_parallel_tool_use. Set
{"type": "auto", "disable_parallel_tool_use": true} in the request's tool_choice, and the model calls at most one tool per response.5 Tighten the behavior to one-at-a-time first, get the dependencies between steps clear in your head, and only then consider loosening it.
- Backstop it in the execution layer. Have the code that runs
withdraw re-check the latest balance itself, refuse to run if the condition isn't met, and write the reason into the tool_result error info for the model to see, rather than pretending it succeeded. Even if the model bundles the two calls together again this time, this check catches the risk.
Draw It as a Diagram
Draw the round-trip above and it looks like this:
The step most often gotten wrong on this diagram is the "append" arrow: sending just the tool_result on its own and forgetting to drop the model's full tool_use reply from that round back into messages. The model then receives a tool result that appears out of nowhere, with no record in its context of the request it made — a non sequitur or an outright error becomes likely. The right move is to store every round's response into history verbatim; messages only ever grows longer and is never trimmed.3
One Task May Take More Than One Round-Trip
The example above ended after a single tool call. In real scenarios, the model often has to go back and forth several times before it can finish. Picture a deployment bot. The user says, "Restart the service for me, and tell me if there are any errors in the logs":
- The model returns
tool_use on the first round, calling restart_service; you run it and send the result back
- The model returns
tool_use again on the second round, calling read_logs to check for errors; you run it and send the logs back
- On the third round the model finally returns
stop_reason: "end_turn", with a summary in text
The code logic on the host side is essentially a loop: as long as stop_reason is still "tool_use", keep running tools, packing the results back in, and sending another round; once it turns into "end_turn", hand the final text to the user.3
This loop has no fixed cap on iterations — for one user request, the model might call a tool just once, or five or six times before it has gathered enough. Lesson 3 covers how tool-interface design can cut the number of round-trips; for this lesson, just remember: multiple round-trips are the norm, not the exception.
Swap the Host, the Field Names Change, the Structure Doesn't
If you're on an OpenAI-compatible API, the same mechanism comes in different wrapping: the call request shows up in the choices[0].message.tool_calls array, the finishing signal isn't called stop_reason but finish_reason, and its value is "tool_calls" rather than "tool_use".7 OpenAI's official docs describe the process as "a multi-step conversation between your application and a model via the OpenAI API. When the model calls a function, you must execute it and return the result" — the model issues a call request, the application runs it and sends the result back, exactly like Claude.8
Field names change with the API, but the skeleton — "the model only sends requests, the host handles execution, results come back carrying an identifier, and it may loop several rounds" — is universal.
Recap
- The model never runs anything directly. It only emits
stop_reason: "tool_use" plus one or more tool_use blocks; execution stays with the host application
- A
tool_use block has only three required fields: id (for matching), name (the tool picked), and input (the parameters)
- Results go back as
tool_result blocks, and tool_use_id has to match the id of the corresponding tool_use block exactly
- One response can have multiple
tool_use blocks; the matching tool_result blocks have to be packed into the same user message, not split across multiple requests
tool_use blocks in the same batch can't see each other's execution results: if a write operation's parameter depends on a read operation's return value in the same batch, split them into two rounds, or force one-at-a-time with disable_parallel_tool_use
- One task may take several round-trips: the host-side implementation is essentially a loop — keep executing and sending back while
stop_reason is still "tool_use", and it's only done once it turns into "end_turn"
>> Lesson 3: Five Common Tool Types: Read, Write, Execute, Search, Call