Agents

Tool calling is a round trip

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.

Recorded exchange
Loading 3D view

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 application on the right of the counter 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 of the counter, 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.

Related terms

More visualisations

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