Agents

Tool calling is a round trip

Tool calling is the round trip where a model writes a structured request for a function, the application runs it, and the result goes back into the model's context before it answers. The model never executes anything itself.

Follow one customer question through a real tool call: the model writes a request and runs nothing, your code looks the order up, and only when the result goes back does the model answer. Four recorded exchanges show the round trip, a lost result, an error, and a refused cancellation.

By Film published 1:41 watch

Recorded exchange
Loading film

Models cannot actA language model can only write text, so when a customer asks where order A-1042 is and when it will arrive, the model has no way to look it up by itself.

Narrated with James Phoenix's AI voice.

get_order_status
Tool the model asked for
A-1042
Argument
Your code
Run by
Yes
Result sent back
True to the order table
Answer

Round trip: The result came back, so the reply is true to the order table.

A language model reads text and writes text. It cannot look up an order, charge a card or send an email. Tool calling is how an application lets it ask for those things: the request lists the tools the application offers, each with a name, a description and a JSON schema for its arguments, and the model may answer with a request to call one instead of with prose.

Every exchange in the film is a real recording. The questions were sent to `google/gemini-3.8-flash` through OpenRouter at temperature 0 on 11 September 2026, with a system prompt for an online shop and two tools, `get_order_status` and `cancel_order`. The handler in the film's code editor is the real code that ran: a regular expression that checks the order id, a one-row order table, and a guard that refuses to cancel anything that is not still being processed.

The traced request is the whole mechanism. The customer asks where order A-1042 is. The model's first turn contains no text at all, only a tool call naming `get_order_status` with the argument `{"order_id": "A-1042"}`. Your code checks the id, reads the table, and returns the row: shipped with DPD on 9 September, arriving 14 September. That result is sent back to the model as a tool message, in a second request that repeats the conversation, and only then does the model write the answer the customer sees.

The part people miss is where the model's role ends. It writes a request, and nothing more. Everything that happens to that request happens in your code: whether the arguments parse, whether the id is well formed, whether this customer may touch this order, and whether the action runs at all. When the model asked to cancel A-1042, the order had already shipped, so the application refused and said why, and the model passed the refusal on. Validation and permissions belong on your side, because the model cannot enforce them.

The failure mode is forgetting that the trip has two legs. Stop after the first turn and the customer receives an empty message, because the text of a tool-calling turn is usually empty. Worse, send the result back empty, as a bug in the plumbing might, and the model does not say that something went wrong. In three runs of three it told the customer it could not find order A-1042, an order that had shipped two days earlier. A missing result reads to the model like a negative result.

Errors should travel the same road as data. When the customer typed A-1402, the lookup failed, and the application returned a structured error rather than nothing. The model turned it into a polite request to double-check the order number, which is the right answer, because the error said exactly what happened.

The takeaway is a division of labour. The model proposes a call and phrases the reply. Your code decides whether the call is valid and allowed, runs it, and always sends a result back, including when the result is an error.

The maths

One tool call is two model calls
reply1=M(system, q, tools)={ call(f,a), text=∅ },reply2=M(system, q, call, r)\text{reply}_1 = M(\text{system},\, q,\, \text{tools}) = \{\, \text{call}(f, a),\ \text{text} = \varnothing \,\}, \qquad \text{reply}_2 = M(\text{system},\, q,\, \text{call},\, r)

The first call returns a tool call and no text: in all four recordings the model sent an empty reply alongside the request. The answer only exists after a second call that carries the call and its result r back in the messages.

Your code sits in the middle
r=fapp(a)only ifvalid(a)∧allowed(f,a)r = f_{\text{app}}(a) \quad \text{only if} \quad \text{valid}(a) \wedge \text{allowed}(f, a)

The model picks f and writes the arguments a as JSON. It never runs f. Your application parses a, checks it, decides whether the call is allowed, runs the function, and chooses what r says. The cancel request here was refused by one line of application code.

An empty result is still an answer
r=""  ⇒  reply2≈"not found"r = \text{""} \;\Rightarrow\; \text{reply}_2 \approx \text{"not found"}

When the tool message came back empty, the model told the customer it could not find an order that had shipped two days earlier, in three runs of three. It reads a missing result as a negative result, so a bug in the plumbing becomes a confident wrong answer.

Errors are data
r={ error:order_not_found }  ⇒  reply2="please double-check the order id"r = \{\, \text{error}: \text{order\_not\_found} \,\} \;\Rightarrow\; \text{reply}_2 = \text{"please double-check the order id"}

Returning a structured error, rather than throwing or returning nothing, lets the model explain the failure in terms the customer understands. The typo exchange is exactly that: A-1402 does not exist, your code said so, and the reply asks the customer to check the number.

Transcript

The narration, chapter by chapter. A timestamp opens the film at that moment.

0:00Models cannot act
A language model can only write text, so when a customer asks where order A-1042 is and when it will arrive, the model has no way to look it up by itself.
0:12The request
So instead of answering, it writes a tool call that fits the schema you gave it: a small card naming get_order_status with the order id A-1042, and its reply text is empty.
0:26Your code runs it
The card crosses to your application, and it is your code, not the model, that parses the arguments string, checks the id, reads the order table, and gets back shipped with DPD, arriving 14 September.
0:39Back to the model
Only when that result goes back into the conversation as a tool message with the same call id does the model write the answer: your order A-1042 has shipped via DPD and should arrive on 14 September.
0:53Stopped halfway
Here is the first failure mode: stop after the first turn, and the customer gets an empty reply, because that turn was only a tool call.
1:03Result lost
Worse, send the result back empty, and the model does not report an error: it tells the customer it could not find an order that shipped two days ago.
1:14Errors travel too
Errors travel the same road: a customer types A-1402, your code returns order not found, and the model turns that into a request to double-check the number, which is exactly right.
1:26Who decides
Cancelling shows who is in charge: the model asks to cancel A-1042, your code refuses because it has shipped, so validate every call on your side and always send the result back.

Also available as captions (WebVTT), the video file and a Markdown copy of this page.

Related terms

More visualisations

Embed this film

Paste this into a post, a course page or a newsletter. The film plays in place, with the same controls as here, and credits the source.

Building with language models?

These explainers come out of the work. If you want the same thinking applied to your own system, that is what I do.

See how I can help