The round trip: four messages and one correlation id
Lesson 1 of 5 in Function Calling Deep Dive: The Wire Under Tool Use.
You already know the tool contract: name, description, schema. This module goes one layer down, to the wire. Because underneath every framework — LangGraph, the Claude Agent SDK, Strands, Pydantic AI, whatever you are using next year — the same four-message dance is happening, and when your agent breaks at 2 a.m. you will be reading that, not the framework’s abstractions.
Here is the universal contract, and it really is universal:
- You declare the tools available for this turn, each as a name, a description, and a parameters JSON Schema.
- The model may return a structured tool call instead of prose: a tool name, its arguments as JSON, and a call id.
- You execute it — the model cannot. Your runtime parses the arguments, validates them, decides whether this call is even allowed, and runs the code.
- You return a result message keyed to that call id, append it to the conversation, and call the model again. It continues with the result in context.
Nothing in that list is a vendor feature. It is the shape of the problem: a text-generating model needs a way to request effects it cannot perform, and you need a way to tell it what happened.
One tool-call round trip, message by message
- Request 1: messages + tool declarations + tool_choice
Every request carries the whole conversation so far plus the tool list. The API is stateless: nothing is remembered for you between calls.
- Model: prose, or a tool call?
The stop/finish reason on the response tells you which happened. This branch is the model-directed control flow from foundations, expressed as a field on a JSON payload.
- Assistant message: name + arguments (JSON) + call id
The id exists for one reason: to correlate this request with the result you will send back. With parallel calls, it is the only thing that tells them apart.
- Your runtime: parse → validate → authorize
Three separate jobs. Parsing can fail, validation can fail, and authorization can refuse a perfectly valid call. None of them are the model’s responsibility.
- Execute the tool
Your code, your credentials, your timeouts, your rate limits. The model has no idea what any of that is.
- Result message tagged with the same call id
Errors go here too — as a result the model can read, not as an exception that kills the loop. That is what lets it retry.
- Request 2: same messages + call + result
You resend the entire thread, now two messages longer. The model reads the result and picks the next move.
- Model answers, or calls again
Two properties of that diagram surprise people the first time they see the raw payloads.
The API is stateless; the transcript is the state. There is no server-side memory of your agent’s turn. Every request re-sends the full message list — system prompt, user message, the assistant’s tool call, your tool result, the next tool call — which is why long loops get expensive: you are paying for a transcript that grows on every iteration. The conversation array is the agent’s working memory.
The call id is load-bearing. It looks like plumbing, and then you send back a result with a mismatched or missing id and the request fails outright, or the model reasons over a result it thinks belongs to a different call. When the model emits three calls at once, the ids are the only way anyone — API or model — can tell which answer belongs to which question.
Key terms: function calling, tool call, tool-use id, tool result, JSON Schema, context window
Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.