Messages, Roles, and the Wire Format
Underneath every SDK is one HTTP request. It carries a list of messages, each with a role and a content field, alongside the list of tools the model is allowed to ask for. The response is one more message. That is the whole interface, and every agent library in this ecosystem is a wrapper around that one exchange.
The array is rebuilt from scratch on every turn and sent in full, because nothing accumulates on the provider's side. Learning to read that array rather than the SDK's typed objects is the difference between debugging an agent and guessing at it — when the agent does something inexplicable, the raw request is the evidence, and it usually contains the answer in plain sight.
The Four Roles
Four roles carry everything. The system role holds standing instructions: who the agent is, what it may not do, that refunds above $150 need a human. The user role holds what the person said. The assistant role holds what the model said, including any request to call a tool. The tool role holds results your code produced after running what the model asked for and appended to the array itself.
Every major API expresses those four, with different spellings for the same idea. Some carry standing instructions in a separate field outside the array rather than as a message; some name the fourth role something other than tool. The concepts do not move, and neither should your mental model. What does matter, and what you should never flatten, is the boundary between the roles: models are trained to weight system content as instruction and user content as material, and the caching machinery in Chapter 5 keys off exactly where those boundaries fall.
Content Is a List, Not a String
A message's content is a list of blocks, not a string. One assistant message can legitimately hold a text block narrating what the model is about to do, then a tool-request block naming search_orders with its arguments, and on some models a reasoning block ahead of both. Three blocks, one message, one turn. The same is true in the other direction: a single message from your code can carry two tool results at once.
This is where the most common wire-format bug lives. Code that reads the assistant's content as a string and hands it to the reply pipeline silently drops the tool request: the agent tells the buyer "Let me look that order up" and stops, having asked for a lookup nobody executed. Iterate the blocks, dispatch on each block's type, and never assume there is exactly one. Chapter 7 revisits the same failure as a control-flow problem; here it is simply a misreading of the format.
The Array Is Rebuilt Every Turn
There is no server-side conversation. On turn one you send two messages. On turn seven you send the system instructions, the ticket, and every assistant message and tool result from turns one through six, in order, as a fresh request. Your code owns that array completely: what goes in, what order it is in, and what gets dropped when it grows past what you are willing to pay for.
Two consequences follow at once. The first is the bill — every token in the array is charged again on every turn, which is the arithmetic Topic 08 works through with real Sundry numbers. The second is that history is editable, and editing it is a decision with teeth. Compacting turns three through eight into one summary message is a legitimate technique that Chapter 5 covers properly. Quietly rewriting an earlier turn in place so the model "sees the right thing" is how you make every trace you read afterwards a work of fiction.
Tool Definitions Travel With Every Request
The tool list is part of the request, not a registration step. There is no endpoint where you declare issue_refund once and refer to it by name from then on. Every schema — the name, the description, the parameter types, the prose that tells the model when this tool is the right one — is re-serialized and re-sent on turn twelve exactly as it was on turn one.
Sundry's nine tools come to roughly 680 tokens of schema, and that is paid on every model call of every ticket, forever. A team that grows the surface from nine tools to fourteen without touching a word of the prompt will watch baseline cost per ticket climb by about an eighth and spend a week looking for the cause in the wrong file. Tool definitions are context. Count them as context, keep the list stable so the prefix can cache, and treat adding a tool as a cost decision rather than a convenience.
What the SDK Adds
An SDK gives you retries with backoff, streaming assembly that turns a stream of fragments back into whole blocks, typed objects, and constructors that spare you writing JSON by hand. All of that is worth having, and none of it is the API. It is convenience layered over a wire format that is documented and stable, and knowing which is which is what lets you tell a provider's behaviour apart from a client library's opinion when the two disagree at three in the morning.
The practical test is blunt: if you cannot say what your SDK puts on the wire for a given call, you cannot debug that call. Serialize the request once per feature in development and read it. Half the surprises in an agent's first month are visible in that dump — a tool whose description never got attached, a system prompt duplicated into a user message by a helper, history in the wrong order after a retry.
Reading a Real Request
Below is one turn of the Sundry agent as it goes over the wire, trimmed to its structure. The buyer's ticket has arrived, the model has already asked for an order lookup, your code has run it and appended the result, and this is the request that asks the model what to do next.
{
"model": "the version you pinned",
"tools": [ /* nine schemas, ~680 tokens, identical every turn */ ],
"messages": [
{"role": "system", "content": [
{"type": "text", "text": "You are Sundry's support agent. Refunds above $150 need a human."}
]},
{"role": "user", "content": [
{"type": "text", "text": "Hi - the shelving unit turned up Tuesday with a cracked side panel..."}
]},
{"role": "assistant", "content": [
{"type": "text", "text": "Let me look that order up."},
{"type": "tool_call", "id": "c1",
"name": "search_orders", "arguments": {"email": "b.oduya@example.com"}}
]},
{"role": "tool", "content": [
{"type": "tool_result", "call_id": "c1",
"text": "{ order_id: SU-88421, seller: Ashcombe Furniture, delivered: 2026-03-10, ... }"}
]}
]
}
Read it top to bottom. The tool list is the nine schemas, unchanged since the first call and unchanged for the rest of the ticket. The system message is standing policy, including the ceiling. The user message is the buyer's prose, verbatim, because nobody summarized it. The assistant message holds two blocks — a sentence of narration and a tool request carrying an id — and the tool message carries the answer back linked to that same id, because the model needs to know which of several outstanding requests this result belongs to.
Everything in that array goes out again next turn, plus whatever the model says next and whatever the next tool returns. Nothing is dropped on your behalf. The parts that repeat unchanged — the tool list and the system message, about 1,100 tokens together — are exactly the parts a cache can pay for, and the parts that grow are the tool results, so Chapter 5 spends most of its attention on them rather than on prompt wording.
- Building the array by string concatenation into one long user message — role boundaries are load-bearing for both instruction adherence and caching, and flattening them costs you both while looking tidier in the code.
- Putting standing instructions in a user message because it seemed simpler — the model weights the two differently, and the stable prefix stops being cacheable the moment policy sits after the customer's text instead of before it.
- Editing earlier turns in place mid-run to correct what the model saw — the stored transcript no longer matches what was actually sent, and every trace you read after that incident is fiction (Chapter 13).
- Forgetting that tool definitions are re-sent — a team grows the surface from nine tools to fourteen, watches cost per ticket rise by an eighth with no behaviour change, and audits the system prompt for a week.
- Trusting the SDK's object model to be the API's model — the wire format is the contract and the objects are one vendor's ergonomics, so a block the client flattens into a string is still a separate block in what the provider received.
- Dump the raw request array in development at least once per feature, and read it top to bottom before that feature ships.
- Keep a single function that builds the array, so ordering, role assignment and pruning have exactly one implementation to audit and change.
- Count the tool list as part of the context budget and put its token cost on the same dashboard as the history (Chapter 5).
- Write the loop against the wire format and keep vendor SDK usage thin enough to swap in an afternoon — the last topic in this chapter is the only place vendor spellings belong.
Knowledge Check
Sundry's refund ceiling and escalation rules are moved out of the system message and into the first user message, ahead of the buyer's text. Nothing else changes. What is the consequence?
- Instruction adherence weakens and the stable prefix stops caching, because policy now lives inside variable text
- Nothing measurable changes, because the model reads the whole array as one document regardless of the role labels it carries
- The provider rejects the request, because policy instructions are only accepted in the system role
- Cost per turn falls, because tokens carried in a user message are billed at a lower rate than tokens in the system role
The agent replies "Let me look that order up" and the loop exits with that as the resolution. The provider's raw response shows the model also asked for search_orders. What went wrong?
- The code read the assistant message's content as one string and never saw the tool-request block
- The tool schema was missing from the request, so the model described the lookup instead of asking for it
- Temperature was set too high, so the model narrated a plan in place of emitting a structured call
- The provider ran
search_orderson its side and returned only the narration without the result
On turn nine of a Sundry ticket, what does your code actually send to the provider?
- The system message, the ticket, and every assistant and tool message from the first eight turns, in order
- Only the newest tool result, because the provider keeps the earlier turns attached to a conversation id
- A hash of the earlier turns plus the new message, which the provider expands from its own server cache
- The last three turns only, since providers truncate anything older than that from the request for you
Sundry grows from nine tools to fourteen. The system prompt is untouched and behaviour looks unchanged, yet cost per ticket rises. Why?
- Every tool schema rides along with each request, so five more schemas are billed on every single turn
- A longer tool list slows selection down, and providers bill for the extra time the model spends choosing
- Registering new tools with the provider carries a one-off fee that is spread across the month's tickets
- Each added tool triggers a separate schema-validation call before the main request is allowed through
You got correct