Chapter 8 · Production serving
AI Agents and Tool Calling
Chapter 5 gave a model tools to look things up. This chapter gives it tools that change things — and works through what that one difference does to reliability, memory, orchestration, and how much rope you can safely give it. Including the part most material skips: the loop is trivial, and almost all the engineering is in the harness around it.
[!] What this chapter assumes, and what it adds
Section 5.23 already introduced the agent loop, ReAct, Self-Ask and Toolformer, and section 5.23–5.24 already covered agentic RAG's topologies and its four guardrails against runaway retrieval loops. This chapter does not repeat that material — it builds on it.
The difference this chapter is built around: chapter 5's tools only ever read. A search that goes wrong wastes a few tokens. A tool that issues a refund, sends an email, or cancels an order that goes wrong costs real money and breaks real trust. Everything from section 8.5 onward exists because of that one change in what a wrong tool call can do.
8.1 A question retrieval can't answer
"What is your returns policy" is a lookup. "Cancel order 88421 and refund me" is not — it requires the model to change something outside itself, and that single distinction is what this whole chapter is about.
Chapter 5's agentic RAG gave a model a search tool and let it decide when to call it again. Every one of those tools shared a property worth naming explicitly:
[def] Read-only versus side-effecting
A read-only tool returns information and changes nothing in the world — a search, a lookup, a calculation. A side-effecting tool changes state somewhere else — issues a refund, sends an email, cancels an order, writes a row to a database. Calling a read-only tool twice by mistake wastes a little time. Calling issue_refund twice by mistake refunds the customer twice.
Nothing about how a model decides to call a tool changes based on this distinction. The model doesn't know or care whether search_products or cancel_order is "riskier" unless you tell it so. What changes is everything downstream of the call: how carefully you validate the arguments, whether a human needs to approve it first, how you handle a duplicate call, and how much damage a confused model can do before something notices.
[!] This is not a bigger version of chapter 5's problem
Chapter 5's four guardrails — iteration cap, latency budget, repeat-query detection, decision trace — protect against a loop that wastes money and time. They say nothing about a loop that acts. An agent stuck in a bad loop calling a search tool produces a slow, expensive, wrong answer. An agent stuck in a bad loop with a send_email tool sends the same email fifty times to a real customer. The mechanism is identical; the blast radius is not.
[+] What this chapter builds toward
Everything from here follows one thread: give a model the ability to act, and answer, concretely, at every step, what happens when it acts wrongly. Sections 8.2 and 8.3 cover the mechanics you'll need regardless of risk level. From 8.5 onward, every section is really answering some version of "what if this tool call was a mistake."
8.2 How tool calling actually works
Worth being precise about this, because "the model calls the tool" is a convenient simplification that hides an important fact: the model never calls anything. It only ever writes structured text describing what it would like called.
[def] Tool calling
You send the model a list of available tools, each described by a name, a natural-language description, and a JSON Schema for its arguments. Instead of a plain text reply, the model can emit a structured object naming a tool and filling in arguments matching that schema. Your code reads that object, actually executes the function, and sends the result back to the model as a new message. The model never touches your database, your email system, or anything else — it only ever produces the request to do so.
1. You send: [conversation so far] + [list of tool schemas]
2. Model replies: "call cancel_order with {order_id: '88421', reason: 'customer request'}"
(structured, not prose — this is the part your code parses)
3. Your code: actually calls cancel_order("88421", "customer request")
validates the model's arguments FIRST (8.4), executes, catches errors (8.7)
4. You send back: the tool's result, tagged so the model knows which call it answers
Model then either replies to the user, or requests another tool call — repeat.
The exact wire format differs slightly by provider — OpenAI's API calls this tools and returns a tool_calls array; Anthropic represents it as a tool_use content block and expects a matching tool_result block back. The shape underneath is the same exchange described above in every case, which is exactly the redundancy that motivates the standardisation in Part C of this chapter.
[!] The model can be wrong about the call itself
A tool call is a prediction, generated the same way every other token is generated. It can name a tool that doesn't exist, fill in an argument with the wrong type, or invent a plausible-looking order ID that isn't real. Nothing about the mechanism guarantees the call is valid — that guarantee, such as it is, comes entirely from validation your code performs in step 3, before anything with real consequences happens. Section 8.4 is about writing schemas that make invalid calls rarer; section 8.7 is about what to do when one gets through anyway.
[+] Why this matters more than it first appears
Keeping "the model requests, your code executes" sharply in mind is what prevents a whole category of mistake later in this chapter: treating a tool call as something that already happened. It hasn't. Every safety mechanism from 8.5 onward — validation, approval gates, spend caps — lives in the gap between step 2 and step 3, which exists precisely because the model's request and your code's execution are two separate things.
8.3 The agent loop, generalised
Section 5.23's loop — think, call a tool, observe the result, repeat — doesn't change shape when the tools have side effects. What changes is what has to be true before the loop is allowed to take a step.
while not done:
response = model.generate(conversation, tools=available_tools)
if response.wants_tool_call:
call = response.tool_call
if not passes_validation(call): # 8.4 / 8.7
conversation.append(error_result(call))
continue
if requires_approval(call): # 8.5 / 8.21
if not await_human_approval(call):
conversation.append(denied_result(call))
continue
result = execute(call) # the only side-effecting line
conversation.append(result)
else:
done = True
return response.text
Every line added to chapter 5's version of this loop is a checkpoint that exists specifically because execute(call) might not be reversible. None of them are needed for a read-only tool — a search that runs when it shouldn't have is a wasted step, not an incident.
[+] The loop is the easy part
Worth saying plainly, because this chapter is about to spend a lot of time on validation, approval, and budgets: the control flow above is a handful of lines and hasn't meaningfully changed since ReAct. Nearly everything that makes an agent with real tools hard to get right lives inside passes_validation, requires_approval and the spend accounting behind them — which is exactly where the rest of this chapter goes.
[retail] Same loop, two very different risk profiles
A customer service agent with search_orders, check_warranty and issue_refund runs the identical loop for all three. The first two never touch requires_approval; the third always does above some amount. Nothing in the loop's structure enforces that distinction — it has to be declared per tool, which is precisely the subject of 8.5.
8.4 Writing schemas a model can use
A tool's description is not documentation for a human developer. It is the only information the model has about when and how to use the tool, and a vague one produces wrong calls no amount of downstream validation fully catches.
[def] A description is a prompt, not a comment
The Model Context Protocol's own specification states this almost exactly: description is "a human-readable description of the tool" that "can be used by clients to improve the LLM's understanding of available tools," explicitly framed as "a hint to the model." Treat it with the same care as a system prompt — because that is functionally what it is.
| Weak | Usable | Why it matters |
|---|---|---|
| "get info" | "Look up an order's current status, items, and shipping address by order ID" | The model must decide whether to call this before it decides how. A vague description gets skipped for the wrong tool or called at the wrong time. |
| id: string | order_id: string, pattern "^[0-9]{5,8}$", description "the numeric order ID shown on the receipt, not the SKU" | Ambiguous parameter names get the wrong value filled in — especially when several tools share similarly named arguments. |
| No examples | One example call in the description for an unusual argument shape | Cheap, and it resolves format ambiguity that prose alone often doesn't — particularly for dates, enums, and nested objects. |
| One tool, ten optional parameters | Several narrow tools, each with a small number of required parameters | A model choosing between many optional fields on one tool makes more argument mistakes than one choosing between clearly separated tools. |
[!] Tool descriptions compete with each other
Every tool schema you register is sent to the model on every turn, alongside every other tool's schema. Two tools with overlapping descriptions — search_orders and get_order_by_id described almost identically — measurably increase how often the model picks the wrong one. Read your tool list the way you'd read a confusing pair of menu items: if a person skimming it could mix two of them up, a model calling thousands of times a day will.
[+] The test that actually matters
Chapter 5's advice about evaluating retrieval on your own queries applies here unchanged: build a small set of realistic requests, run them against your actual tool schemas, and check whether the model picks the right tool with the right arguments. A schema that looks clear to you and confuses the model in practice is common enough that this check should happen before a tool with side effects ever reaches production, not after.
8.5 Read-only versus side-effecting tools
Section 8.1 named the distinction. This section makes it a property you actually declare per tool, because every safety decision later in the chapter reads from it.
The Model Context Protocol formalises exactly this, and its vocabulary is worth adopting whether or not you use MCP itself. Each tool can carry annotations describing how it behaves:
- readOnlyHint
- The tool does not modify its environment. Default false — note the direction: unless you say otherwise, a tool is assumed to change something.
- destructiveHint
- The tool may perform destructive updates rather than only additive ones. Default true, and meaningful only when readOnlyHint is false. Again the cautious default: assume a writing tool can destroy something.
- idempotentHint
- Calling it repeatedly with the same arguments has no additional effect beyond the first call. Default false. This is the property that decides how dangerous a duplicate call is.
- openWorldHint
- Whether the tool interacts with an open-ended external world (a web search) or a closed one (a lookup in your own database).
[!] These are hints, and the spec says so explicitly
MCP's schema states outright that "all properties in ToolAnnotations are hints" and are "not guaranteed to provide a faithful description of tool behavior." Nothing enforces them. A tool annotated readOnlyHint: true can still write to your database if whoever wrote it was careless or malicious. Use annotations to drive your own policy decisions about approval and retries — never as a security boundary, and never as a substitute for the permissions on the credential the tool actually runs with.
Why idempotency is the property that matters most
Of the four, idempotentHint is the one that most directly determines what your code has to do. Agents retry — on a timeout, on a network blip, on a malformed response. If a retried call is idempotent, retrying is free. If it isn't, a retry is a second refund.
| Tool | Profile | What your code must do |
|---|---|---|
| get_order_status | Read-only | Retry freely. No approval, no dedupe, no audit trail beyond ordinary logging. |
| set_delivery_address | Writes, but idempotent | Retry safely — setting the same address twice is the same as once. Still log it; still consider approval if the value looks unusual. |
| issue_refund | Writes, not idempotent, destructive | Never blind-retry. Requires an idempotency key so a duplicate call is rejected by the payment system itself, plus approval above a threshold, plus a full audit record. |
[+] Make non-idempotent tools idempotent where you can
The strongest thing you can do for agent safety is often not in the agent at all. Have the agent generate a stable idempotency key per logical action — derived from the order ID and the reason, not randomly — and have the payment or fulfilment system reject a second call carrying a key it has already seen. That turns "the agent retried and refunded twice" from an incident into a no-op, and it works even when the agent misbehaves in a way you didn't anticipate. Defensive design in the tool beats careful prompting in the model every time.
8.6 Parallel calls and dependencies
A model can request several tool calls in one turn. Whether that's a latency win or a correctness bug depends entirely on whether the calls actually depend on each other — and the model is not reliably able to tell.
Most providers support emitting multiple tool calls in a single response. When the calls are genuinely independent, this is straightforwardly good: three lookups that would have taken three sequential round trips take one, and chapter 7's ray.wait() or an ordinary asyncio.gather executes them concurrently.
Safe to parallelise
get_order_status("88421"), get_order_status("88422"), check_stock("SKU-119") — three independent reads. Nothing one returns changes what another should be called with.
Must be sequential
get_order("88421") then issue_refund(amount=<the order total>) — the second call's arguments come from the first call's result. Run in parallel, the model has to guess the amount, and it will.
[!] The failure mode: invented arguments
When a model emits a parallel batch containing a call whose arguments it doesn't actually know yet, it doesn't stop — it fills them in with something plausible. A refund for £49.99 when the order was £499.90 is exactly the kind of error this produces, and it looks completely reasonable in the trace. If a tool's arguments can only come from another tool's output, either say so explicitly in the description, or don't expose both in a way that invites a single-turn batch.
[+] A simple, effective policy
Allow parallel calls freely among read-only tools, where a wrong call is cheap and correctness doesn't depend on ordering. Execute side-effecting tools one at a time, in the order requested, checking each result before the next runs. You give up a little latency on the rare parallel-write case and remove an entire category of ordering bug from the part of the system where those bugs are expensive.
8.7 Handling tool failures
Tools fail. The interesting question is not how to prevent that, but what the model sees afterwards — because a tool error is one of the few situations where the model can genuinely recover on its own if you let it.
There are three distinct failure kinds, and conflating them is what produces agents that spin uselessly:
- Invalid arguments — the model's mistake, and recoverable A malformed order ID, a missing required field, a string where a number belongs. Your validation catches this before execution. Return a specific error explaining what was wrong, and the model very often fixes it on the next turn.
- Legitimate business rejection — not an error at all "This order is outside the 30-day return window." Nothing malfunctioned; the answer is no. This should read as a normal tool result, not an exception, or the model treats a correct answer as something to retry around.
- Infrastructure failure — not the model's problem Timeout, connection refused, 500 from an upstream service. The model can do nothing useful with this. Retry it in your code with backoff, and only surface it to the model if retries are exhausted.
[+] Write errors for the model, not for your logs
ValidationError: field 'order_id' failed regex is written for you. "order_id must be the 5-8 digit number from the receipt; you provided 'SKU-119', which looks like a product code. Call get_order_by_customer first if you don't have the order ID." is written for the model — and the second one frequently produces a correct call on the very next turn where the first produces the same mistake again. This is prompt engineering wearing an error handler's clothes, and it's one of the highest-leverage things in this chapter.
[!] Feeding infrastructure errors back is how loops start
A model that receives "Error: connection timeout" has no way to fix a network problem, so it does the only thing it can: try again. And again. That's a runaway loop caused entirely by surfacing an error to a component that cannot act on it. Handle transient failures in your own retry logic where they belong, and when you do give up, tell the model something actionable — "The refund system is unavailable. Do not retry; tell the customer you've logged the request for manual follow-up."
[retail] The partial-failure case worth planning for
The nastiest real failure isn't a clean error — it's a call that timed out after the refund was actually issued. Your code sees a failure; the payment system sees a completed transaction. Without the idempotency key from 8.5, the obvious retry double-refunds. With one, the retry safely returns the original result. This is the concrete reason that section insisted the defence belongs in the tool rather than in the agent's reasoning.
8.8 The N×M integration problem
Section 8.2 mentioned that every provider expresses the same tool-calling exchange in a slightly different wire format. That inconsistency is annoying at one integration and becomes a genuine architectural problem at twenty.
Suppose your organisation has built tool integrations for orders, inventory, refunds, the warehouse system, and the CRM. Now suppose you have three applications that want to use them, each built against a different model provider. Without a shared standard, every application re-implements every integration in that provider's format:
[def] The N×M problem
N applications each needing M tool integrations produces N×M pieces of glue code, none of which are reusable across applications. Five tools and three apps is fifteen integrations to write, test and keep in sync as the underlying systems change. The integrations are almost identical in what they do and entirely different in how they're expressed.
[+] What a protocol turns that into
Define one standard interface between "things that expose tools" and "things that use tools", and N×M becomes N + M. Each tool system is implemented once as a server; each application implements the client side once and can then talk to every server. This is precisely the argument that produced ODBC for databases and the Language Server Protocol for editors, applied to LLM tool use.
[def] The Model Context Protocol
MCP is that standard. Its own description is a good one-line summary: it "lets you build servers that expose data and functionality to LLM applications in a secure, standardized way — think of it like a web API, but designed for LLM interactions." It is JSON-RPC based, and supports several transports: stdio for local servers, plus Streamable HTTP and SSE for remote ones.
[retail] Why this matters organisationally, not just technically
The real win is ownership. The team that owns the refund system writes and maintains one MCP server exposing refund tools, with the annotations from 8.5 set correctly and the idempotency handled properly — and every agent in the company consumes it without re-implementing that logic or re-deciding those safety properties. The alternative is each application team writing their own refund glue, each making their own decisions about retries and approval thresholds, with no consistency. At a single team's scale the protocol is overhead; across an organisation it's the difference between one careful implementation and a dozen careless ones.
8.9 MCP: tools, resources, prompts
An MCP server exposes three kinds of thing. The distinction between them is not bureaucratic — it's about who decides when each one gets used.
- Tools
- "Definition for a tool the client can call." Model-controlled: the model decides to invoke one, exactly as in 8.2. Each carries a name, a description, an inputSchema, and optionally the annotations from 8.5.
- Resources
- "A known resource that the server is capable of reading." Application-controlled: your app decides what to load and put in context — a file, a record, a document. The model doesn't call for these; they're supplied to it.
- Prompts
- "A prompt or prompt template that the server offers." User-controlled: typically surfaced as something a person explicitly picks, like a slash command or a menu action.
[+] The organising question: who initiates?
That's the cleanest way to keep the three straight. A tool is invoked because the model chose to. A resource is loaded because the application chose to. A prompt is used because the user chose to. The same underlying capability — say, fetching an order — could sensibly be exposed as any of the three depending on who should be making that decision in your product.
from mcp.server import MCPServer
mcp = MCPServer("Orders")
@mcp.tool()
def get_order_status(order_id: str) -> dict:
"""Look up an order's current status, items, and shipping address by order ID.
order_id is the 5-8 digit number from the receipt, not a product SKU.
"""
return orders_db.fetch(order_id)
The docstring becomes the tool description the model sees, and the type hints become the inputSchema. Everything section 8.4 said about writing descriptions carefully applies directly to that docstring — it is not a comment, it's the prompt the model reads when deciding whether to call this.
[!] A protocol standardises the interface, not the trust
Connecting an agent to a third-party MCP server means letting text you don't control describe tools to your model, and in many cases letting that server see the arguments your model sends. Everything section 5.36 said about indirect prompt injection applies with force here: a malicious or compromised server can describe its tools in ways designed to manipulate the model into calling them with sensitive data. Treat an external MCP server exactly as you'd treat any third-party API with access to your systems — scoped credentials, reviewed before adoption, and never trusted just because the protocol is standard.
8.10 Sampling and elicitation
Two newer MCP primitives that invert the usual direction: instead of the client asking the server for something, the server asks the client. Both exist to keep a person in the loop, which makes them directly relevant to everything in Part F.
[def] Elicitation
Method elicitation/create: "a request from the server to elicit additional information from the user via the client." The server has hit a point where it needs something only the person can supply — a confirmation, a missing field, a choice between options — and asks the client to obtain it, rather than guessing or failing.
[def] Sampling
Method sampling/createMessage: "a request from the server to sample an LLM via the client." The server needs model reasoning as part of doing its job, but doesn't hold model credentials itself — so it asks the client's model. The specification adds a line worth quoting: "the client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop)."
[+] Why the direction matters
Both primitives put the client — the thing the user actually trusts and interacts with — in the position of gatekeeper. The server can request information or model access, but cannot help itself to either. That's the same architectural instinct as 8.2's "the model requests, your code executes," applied one level up: the component with the credentials and the user relationship stays in control of what actually happens.
[retail] Elicitation as the approval gate
This is the natural place to implement the approval step sketched in 8.3's loop. A refund server that has decided a £400 refund is warranted can elicit an explicit confirmation rather than simply executing — and because the request goes through the client, the confirmation UI, the audit record and the identity of whoever approved it all live in the application that actually knows about users. Section 8.21 builds this out into a full approval policy.
8.11 Beyond ReAct
ReAct — reason, act, observe, repeat — is the default agent loop and section 5.23 covered it. It has a specific weakness that shows up as soon as tasks get longer, and two alternative shapes address it.
[!] ReAct's weakness: no commitment to a plan
ReAct decides the next action based on everything observed so far, one step at a time. That's exactly the right behaviour for a short task, and it drifts on a long one: by step eight, the accumulated observations have crowded the original goal out of the model's attention, and it starts pursuing whatever the last tool result suggested rather than what the user asked for. There is no artefact anywhere in the loop that represents "the thing we're trying to do."
| Shape | How it works | Use when |
|---|---|---|
| ReAct | Decide each action from the current state, one at a time. | Short tasks, two to five steps, where the path genuinely depends on what you find. The right default. |
| Plan-and-execute | Produce an explicit plan first, then execute its steps, re-planning only if a step fails. | Longer tasks with a knowable structure. The plan is a written artefact you can validate, show a human, and check progress against. |
| Reflection | After producing a result, the model critiques its own output against the goal and revises. | When correctness matters more than latency and there's an objective standard to check against. Costs at least double. |
[+] Why an explicit plan helps with side effects specifically
Plan-and-execute has a safety property that has nothing to do with accuracy: the plan exists before anything is executed. That means you can inspect it, check it against policy, count how many side-effecting steps it contains, and show it to a human for approval as a single coherent unit — rather than approving eight individual tool calls with no visibility into where the sequence is heading. For an agent that only reads, this is a nicety. For one that issues refunds, it's the difference between approving an action and approving a strategy.
[!] Reflection needs something real to check against
Asking a model "is that answer good?" with no external reference mostly produces agreement with itself — the same weakness section 5.33 flagged for LLM-as-judge. Reflection earns its cost when the critique step has an independent signal: test output, a schema validation, a retrieved document to check claims against, a calculation that can be redone. Self-critique against nothing but the model's own prior output is expensive theatre.
8.12 Short-term versus long-term memory
"Memory" gets used for two completely different things in agent systems. They have different mechanisms, different failure modes, and one of them is a solved problem while the other mostly isn't.
- Short-term (working) memory
- The conversation so far, held in the context window: user messages, model replies, tool calls and their results. It's simply the growing message list from 8.2. Bounded by the context length from chapter 6, and it disappears when the session ends.
- Long-term memory
- Information deliberately persisted across sessions and retrieved when relevant — a customer's stated preferences, past resolutions, facts learned weeks ago. Mechanically this is the retrieval stack from chapters 3 through 5, pointed at conversation history instead of documents.
[!] The short-term problem: tool results are enormous
An agent's context fills far faster than a chat's does, because every tool result goes into it. Five API calls returning JSON blobs can consume more context than the entire human conversation around them, and chapter 6 showed exactly what long contexts cost in KV cache and concurrency. Worse, section 5.17's lost-in-the-middle effect applies directly: the decisive tool result from step two is now buried mid-context by step nine, exactly where the model attends to it least.
| Technique | What it does | Cost |
|---|---|---|
| Truncate tool results | Return the 20 relevant fields, not the 200-field API response. | Nearly free, and usually the single biggest win. Do this first. |
| Summarise older turns | Replace early conversation with a compact summary once it exceeds a threshold. | An extra model call, and summaries lose detail the agent may later need. |
| Externalise to a scratchpad | Write intermediate findings to a store the agent can query, keeping context lean. | More moving parts; needs its own retrieval step. |
| Re-state the goal each turn | Repeat the original objective near the end of the context, where attention is strong. | A few tokens, and directly counters the drift described in 8.11. |
[+] Long-term memory is RAG, and should be built like it
There is a strong temptation to treat agent memory as a novel problem. It isn't: deciding what to store, embedding it, retrieving what's relevant to the current turn, and dealing with stale or contradictory entries are exactly the problems chapters 3 to 5 worked through. The genuinely agent-specific parts are narrower — deciding what is worth remembering at all, and expiring things that stop being true. Everything else, reuse.
[!] Memory makes wrong information persistent
A mistake in short-term memory lasts one session. A mistake written to long-term memory — "this customer prefers email contact," recorded from a misread message — affects every future interaction and is nearly invisible in a trace, because nothing in the current session shows where the wrong belief came from. Store facts with provenance and a timestamp, prefer explicit user statements over inferences, and give memory entries an expiry unless you have a reason to believe them permanently.
8.13 A scenario needing both
Planning and memory are easy to describe separately and easy to under-appreciate until a task needs both at once. Here is one that does.
[retail] The request
"My replacement laptop arrived damaged again. Just sort it out — same as last time, but I'm not paying return postage twice."
Nothing about this is answerable from the message alone. It requires knowing what "last time" was, planning a multi-step resolution, and executing side-effecting tools in an order that matters.
- Long-term memory supplies the missing referent "Same as last time" is meaningless without retrieving the prior interaction: a damaged-on-arrival replacement, resolved by refund plus a prepaid return label. Without persistent memory the agent must ask, which is precisely the experience the customer is signalling frustration about.
- An explicit plan makes the sequence inspectable Verify the order and damage claim, issue a refund for the item, generate a prepaid return label, waive the postage charge, confirm to the customer. Two of those five steps spend money. Producing the plan first means the whole strategy can be checked against policy before any of it executes — the safety property from 8.11.
- Ordering is load-bearing Generate the return label before confirming; confirm before the refund settles and the customer's expectation is set wrongly. Section 8.6's rule applies: these side-effecting steps run one at a time, each checked, not batched in parallel.
- Working memory has to survive the whole run The order total retrieved in step one is what step two refunds. Five tool results later, that number is mid-context where attention is weakest — so the plan carries it forward explicitly rather than trusting the model to find it again.
- The interaction is written back to long-term memory So that the next "same as last time" resolves correctly — with provenance and a timestamp, per 8.12's warning.
[+] What this scenario shows that the individual sections can't
Each mechanism covers a different failure. Without long-term memory the agent asks a question the customer already answered. Without a plan it drifts mid-sequence and forgets the postage waiver. Without careful ordering it confirms an outcome that hasn't happened. Without working-memory discipline it refunds the wrong amount. All four are plausible; none is prevented by getting the other three right.
8.14 Context engineering
Section 8.12 gave four tactics for keeping working memory under control. This section is about treating those tactics as a single engineering discipline with a name, a budget, and a policy — because on a long-running agent, what occupies the context window determines behaviour more than the prompt does.
[def] Context engineering
Context engineering is the deliberate management of what occupies the model's context window at each step: what goes in, in what order, what gets removed, and what is reconstructed later if needed. Prompt engineering asks what to say to the model. Context engineering asks what the model can see when it decides — and on an agent that has taken twenty steps, almost none of what it sees was written by you.
[!] The window is a budget, and tool output spends it fastest
A chat conversation grows by a few hundred tokens per turn. An agent grows by whatever the API returned, which might be an 8,000-token JSON document that mattered for one field. By step ten the context is mostly machine output that no longer informs any decision, and three separate costs are being paid for it: money per token, latency, and — the one that actually hurts — the lost-in-the-middle degradation from 5.17 pushing the important material into the region the model attends to least.
The eviction policy is the design
Once you accept the window is finite, the real question is not how to fit more in but what to drop first. That ordering is a design decision, and most agents make it by accident.
| Evict | Reasoning |
|---|---|
| 1. Superseded tool results | If stock was checked three times, only the latest matters. The earlier two are pure noise and can be dropped with zero information loss — the only free move on this list. |
| 2. Unused fields from results you kept | The 200-field response where you needed four. Filter at the tool boundary, before it ever enters context (8.12). |
| 3. Failed attempts, once recovered | Keep one line recording that the approach failed, so it isn't retried. Drop the stack trace and the retry chatter. |
| 4. Old reasoning text | The model's own deliberation from step three rarely informs step fifteen. Its conclusions might; its thinking-out-loud won't. |
| 5. Middle conversation, compacted | Last resort, because it is the only lossy step. Covered below. |
[def] Compaction
Compaction replaces a span of conversation with a model-generated summary of it, freeing the window while preserving the gist. It is the technique that makes genuinely long agent runs possible, and it is the one that quietly loses information. Everything else on the eviction list discards things you can prove are redundant; compaction discards things you have decided are unimportant, which is a judgement made by a model under a token limit.
[!] Repeated compaction compounds its own errors
Summarising a summary is lossy twice, and the second pass cannot recover what the first dropped. Run this five times across a long session and the early context has been through a game of telephone — details are not merely absent but subtly wrong, and nothing in the transcript indicates which parts are degraded. Compact from the original span each time rather than from the previous summary, if you still have it; keep the raw history in external storage even after evicting it from context, so compaction always reads from source.
[+] Protect the head and the tail
Compact the middle. The head holds the system prompt and the original goal; the tail holds the most recent tool results the next decision depends on. Both sit in high-attention positions and both are load-bearing. An agent that compacts indiscriminately will eventually summarise away the task it was given, then continue confidently on a paraphrase of it — which is 8.11's drift problem, now self-inflicted and much harder to spot.
[+] Offload rather than summarise, where you can
The best fix for a large tool result is often not to compress it but to store it and pass a reference. Write the 8,000-token document to a scratchpad keyed by ID, put a two-line description plus that ID in context, and give the agent a tool to read specific fields back. Nothing is lost, the window stays lean, and retrieval becomes explicit and traceable. This is the externalisation row from 8.12, and it is underused relative to summarisation because summarisation feels simpler — right up to the point where it silently drops the field you needed.
[!] Every eviction invalidates the prefix cache
Chapter 6's prefix caching only helps while the beginning of the context is unchanged between calls. Compaction rewrites the middle, which invalidates everything after it and forces a full re-prefill on the next step. This produces a genuinely counter-intuitive result: compacting too eagerly can cost more than the tokens it saves. Compact in infrequent large batches rather than trimming a little every turn, and keep the system prompt and tool definitions byte-identical across calls so at least that prefix survives.
[retail] A concrete policy for the support agent
ALWAYS RESIDENT ~4k system prompt + tool schemas (cacheable, never edited)
~1k the original request, restated verbatim near the tail
PROTECTED TAIL ~20k last 3 tool results in full
COMPACTED MIDDLE ~80k older turns; compacted from source at 70% full
HEADROOM ~23k next tool result + the model's reply
TRIGGER compact when total > 90k, not every turn (prefix cache)
NEVER compact the system prompt, the goal, or the last 3 results
The headroom line is the one teams forget. If you compact only when the window is full, the next tool result has nowhere to go and the call fails — so the trigger has to leave room for the largest plausible result plus the reply. Budget for the worst case you have actually observed, not the average.
8.15 Harness engineering
Section 8.3 said the loop is the easy part and that the difficulty lives in what surrounds it. That surrounding machinery has a name, and treating it as a designed artefact rather than glue code is what separates an agent that demos from one that works.
[def] The harness
The harness is everything around the model: the tool surface it can reach, how results are formatted before it sees them, how errors are worded, what the context contains (8.14), the stopping conditions, and the validation between deciding and acting. The model is a fixed component you mostly cannot change. The harness is the part you actually engineer — and on most teams it is where nearly all the quality gains come from.
[+] The reframe worth internalising
When an agent misbehaves, the instinct is to edit the system prompt. That is the weakest available lever, because it competes for attention with everything else in a long context and gets steadily more diluted as the run continues. The harness is stronger precisely because it is structural: a tool that cannot be called wrongly, a result the model cannot misread, and an error that states the fix are all enforced at every step regardless of how much context has accumulated. Prompt instructions decay over a long run. Harness constraints do not.
The tool surface is a user interface
Section 8.4 covered writing schemas the model can use. The harness view is one level up: not "is this tool well described" but "is this the right set of tools, at the right granularity."
| Decision | Guidance |
|---|---|
| How many tools | Fewer than feels natural. Past roughly fifteen to twenty, selection accuracy falls noticeably and descriptions start competing (8.4). If you need more, group them behind a router or split the agent (8.17). |
| Granularity | Match the task, not the API. Three calls the agent always makes together should be one tool — that removes two chances to get the sequence wrong and two tool results from the context. |
| Overlap | Avoid entirely. Two tools that could plausibly serve the same request produce inconsistent runs and are miserable to debug, because the failure is a coin flip. |
| Naming | Name by intent, not implementation. find_customer_orders beats query_orders_v2; the model has no idea what v2 changed. |
[+] Make illegal states unreachable through the schema
Anything you can express as a constraint the model cannot violate is worth more than any amount of instruction. An enum with four values cannot produce a fifth. A required field cannot be omitted. Splitting update_order(action) into cancel_order and refund_order makes "cancel when you meant refund" a different tool call rather than a wrong argument — visible in a trace, and blockable by policy. This is the same instinct as 15.16's tool authorisation, applied one layer earlier.
Observation formatting: what the model sees after acting
[!] Raw API responses are a poor observation format
The default is to serialise whatever the API returned and paste it in. That wastes context (8.14), buries the decisive field among irrelevant ones, and forces the model to do parsing work that costs attention it could spend on the actual decision. The harness should transform every result into the smallest, clearest form that supports the next step — and empty results deserve particular care: [] tells the model much less than "no orders found for customer 4471 in the last 90 days", which distinguishes "wrong customer" from "right customer, no orders" and prevents a pointless retry.
[+] Error messages are the highest-leverage text in the system
Section 8.7 established that errors should be written for the model. The harness view is stronger than that: an error message is a control signal, and it is read at exactly the moment the model is deciding what to do next — which makes it far more influential per token than anything in the system prompt.
WEAK {"error": "ValidationError", "code": 422}
-> model retries the identical call, fails again, loops
STRONG Error: 'date_from' must be ISO format (YYYY-MM-DD).
You sent '90 days ago'. To search recent orders,
compute the date first or omit date_from for all orders.
-> model corrects on the next step
The strong version names what was wrong, quotes what was sent, and states the recovery — including the option of not using the parameter at all. Most agent loops that spin are not reasoning failures; they are the model being told that something failed without being told what would work.
Verification inside the loop
[def] Verifiers
A verifier is a deterministic check the harness runs on the model's output before accepting it — a schema validation, a test suite, a policy rule, a recomputation. Unlike the reflection pattern in 8.11, a verifier is code, so it does not hallucinate agreement and costs no tokens. This is what 8.11 meant by reflection needing "an independent signal": the verifier is that signal, and where one exists it should always be preferred to asking the model to check itself.
[+] The generate-verify-repair loop
Where a verifier exists, the strongest pattern is: generate, verify, and on failure feed the specific verifier output back as the next observation. The model is no longer guessing what went wrong — it has been told, precisely, by something that cannot be wrong about it. Bound the repair attempts (two or three), because a verifier that keeps failing usually indicates the task is misspecified rather than the attempt being unlucky, and looping on it burns budget for nothing.
[retail] Harness changes that outperformed prompt changes
Four fixes for the support agent, none of which touched the system prompt: collapsing get_customer plus get_orders plus get_order_items into one get_customer_context call, removing two ordering mistakes and two context-filling results; returning "no orders found for this customer in the last 90 days" instead of an empty array; splitting a generic update_order into named, separately authorised tools; and validating refund amounts against the order total in the harness, so an over-refund is rejected with a message stating the maximum rather than reaching the approval queue at all. Each is a structural change that holds on every run — which is the property a prompt instruction never has.
8.16 Why agents fail at long horizons
An agent that handles a four-step task reliably will not necessarily handle a forty-step one at all. The failure is not linear, and the reasons are specific enough to design against — which is the point of naming them.
[!] Per-step reliability compounds, and the arithmetic is brutal
If each step succeeds independently with probability p, a run of n steps succeeds with pn. That is the single most important number in agent design and it is rarely computed:
per-step 5 steps 10 steps 20 steps 50 steps
99% 95.1% 90.4% 81.8% 60.5%
95% 77.4% 59.9% 35.8% 7.7%
90% 59.0% 34.9% 12.2% 0.5%
A 95% per-step success rate sounds respectable and produces an agent that fails roughly two runs in three at twenty steps. This is why the highest-value optimisation is usually removing steps, not improving the model — collapsing three tool calls into one (8.15) moves you along the row and up the column simultaneously.
[+] Independence is a simplification, in both directions
Real steps are correlated, which cuts both ways. A recoverable error makes the table pessimistic: a well-worded error message (8.15) turns a failed step into a retry rather than a failed run. But an unrecovered error makes it optimistic, because a wrong belief entering the context poisons every subsequent step — the failures are positively correlated in exactly the way that hurts. The table is a floor for reasoning about it, not a prediction.
The four failure modes
| Failure | What it looks like | Mitigation |
|---|---|---|
| Goal drift | By step fifteen the agent is pursuing something adjacent to the original request, having followed the gradient of its own recent observations. | Restate the goal near the tail every turn (8.12); an explicit plan to check against (8.11); never compact the goal (8.14). |
| Context poisoning | A wrong fact enters context early — a misread field, a hallucinated ID — and every later step reasons from it. The agent is confidently, consistently wrong. | Verifiers on tool outputs (8.15); provenance on facts; the ability to mark an observation as retracted rather than hoping it is ignored. |
| Looping | The same call repeated with the same arguments, or a two-step cycle between tools, forever. | Detect repeats structurally, not by asking the model. See below. |
| Silent partial completion | The agent reports success having done four of six things. The most dangerous mode, because nothing looks wrong. | Verify completion against the plan's steps, not against the model's own claim of being finished. |
[!] Detect loops in the harness, never by asking the model
A model that is stuck in a loop is, by construction, not noticing that it is stuck — so "check whether you are repeating yourself" is being asked of the faculty that has already failed. Loop detection belongs in the harness as plain code: hash each tool call with its arguments, and if the same hash appears three times, break out. Cheap, deterministic, and it works precisely when the model's judgement does not. The same applies to step and spend caps from 8.22 — bounds are enforced around the loop, not requested politely inside it.
[+] Failing loudly beats continuing hopefully
When a bound trips, the correct behaviour is to stop and report state: what was attempted, what succeeded, what did not, and what a human would need to do to finish. An agent that quietly gives up and produces a plausible summary of incomplete work is worse than one that errors, because the failure is invisible until someone notices the refund never arrived. Partial completion needs to be a reported outcome, not a silent one.
[retail] Shortening the horizon, and what actually makes it work
A support agent asked to "resolve this complaint" might take twenty-four steps. Decomposed into three bounded sub-tasks — diagnose, decide remedy, execute — that is three runs of eight steps each.
Here is the part worth being careful about, because it is easy to state wrongly: splitting alone changes nothing. At 95% per step, one 24-step run succeeds 29.2% of the time, and three chained 8-step runs succeed 29.2% of the time. It is the same twenty-four multiplications. Anyone claiming decomposition improves the arithmetic by itself is selling something.
What makes it work is the verified handoff between stages. Each stage has a checkable output — a diagnosis, a chosen remedy, a completed action — so a failed stage can be detected and retried without restarting the whole run. Allow one retry per verified stage and the same work goes from 29.2% to about 70%. The gain comes from the verifier and the retry boundary, not from drawing three boxes instead of one, and that is the real argument for the multi-agent split in 8.17.
8.17 When to split into agents
Section 5.23 already warned that multi-agent is usually premature, and gave the arithmetic: four agents at 90% reliability each is a system at 66%. That warning stands. This section is about the cases where splitting genuinely earns its cost.
The honest default remains one agent with a well-described toolbox. Most "we need multiple agents" instincts are really a single agent with confusing tool descriptions — which 8.4 fixes far more cheaply than an orchestration layer does. Three situations genuinely justify the split:
Irreconcilable instructions
A refunds agent needs cautious, policy-bound, verify-everything behaviour. A product recommendation agent needs to be expansive and suggestive. Those are opposing dispositions, and one system prompt trying to hold both does neither well.
Permission boundaries
The strongest reason, and specific to side-effecting tools. If only one narrow agent holds the refund credential, no amount of prompt injection against the general support agent can issue a refund — the capability simply isn't reachable from there.
Context that won't coexist
Two subtasks each needing large, unrelated context. Keeping both in one window makes each worse, per 8.12 — separate agents each get a clean, focused context.
[+] Permission separation is the argument that actually holds up
Notice that reason 2 is qualitatively different from the other two. Reasons 1 and 3 are about output quality, and you could often reach the same place with better prompting or tighter context management. Reason 2 is a structural guarantee: a tool that isn't registered to an agent cannot be called by that agent, regardless of what the model is persuaded to attempt. That's a security property, not a quality improvement, and it's the one that survives contact with an adversary.
[!] Every handoff is a place to lose information
When agent A hands to agent B, everything A learned that it didn't explicitly pass along is gone. In practice handoffs drop exactly the context that turns out to matter, and the failure looks like B being inexplicably stupid rather than A being insufficiently thorough. If you split, be deliberate about the handoff payload — it is an interface, and it deserves the same care as the tool schemas in 8.4.
8.18 Agents as Ray actors
Chapter 7 built exactly the machinery a multi-agent system needs, for a different reason. An agent that holds a conversation, remembers state, runs concurrently, and must be reachable by name is — precisely — a named async actor.
| Agent concern | Ray primitive | Why it fits |
|---|---|---|
| A conversation with accumulating state | Actor (7.4) | The message list, plan and scratchpad are exactly the "state that must be remembered between calls" test. |
| An orchestrator reachable from any request handler | Named, detached actor (7.5, 7.6) | Found via ray.get_actor(), survives the process that created it. |
| An agent that mostly waits on model and tool calls | Async actor (7.7) | Agent work is overwhelmingly I/O-bound — awaiting a vLLM replica or an HTTP tool. One actor can hold many conversations concurrently. |
| Many interchangeable workers for one specialism | ActorPool (7.8) | Ten identical "search specialist" agents sharing a work queue. |
| Scaling agent capacity with the vLLM fleet | Serve autoscaling + sync loop (7.14, 7.17) | Agent throughput is bounded by the model capacity behind it; both layers resize on the same signal. |
@ray.remote
class RefundAgent:
"""Holds the refund credential. No other agent does."""
def __init__(self, gateway_handle):
self.gateway = gateway_handle # the vLLM gateway from 7.7
self.tools = [issue_refund, get_order] # narrow, deliberate toolset
async def handle(self, task: dict) -> dict:
conversation = build_conversation(task)
for _ in range(MAX_STEPS): # the iteration cap from 5.23
reply = await self.gateway.generate.remote(conversation, tools=self.tools)
if not reply.wants_tool_call:
return {"result": reply.text}
conversation.append(await self.execute_checked(reply.tool_call))
return {"result": "escalated", "reason": "step limit reached"}
refund_agent = RefundAgent.options(
name="refund_agent", namespace="agents",
get_if_exists=True, lifetime="detached", max_concurrency=50,
).remote(gateway)
[+] The permission boundary becomes a process boundary
This is where 8.17's reason 2 stops being a diagram and becomes real. The refund credential lives inside RefundAgent's process and nowhere else. A general support agent can ask the refund agent to do something — a method call it can audit and rate-limit — but cannot call issue_refund itself, because that function isn't in its toolset and the credential isn't in its process. Prompt injection against the support agent has nothing to reach for.
[!] Don't let the actor boundary imply a trust boundary it doesn't have
The refund agent still has to validate what it's asked to do. A support agent that has been manipulated can pass along a manipulated request, and the refund agent receiving it through a tidy actor interface is no reason to trust its contents. Every check from Part B — argument validation, approval thresholds, idempotency keys — belongs inside the specialist, applied to requests from other agents exactly as they'd be applied to requests from a model.
8.19 A worked orchestration scenario
One request, traced through a supervisor and two specialists, showing where each chapter's machinery actually engages.
[retail] The request
"This blender stopped working after three weeks. Is it under warranty, and if so can you replace it? Also what else do you have around that price?"
Two genuinely different jobs in one message: a cautious warranty-and-replacement transaction, and an open-ended product recommendation. This is 8.17's reason 1 and reason 2 arriving together.
- The supervisor decomposes, and does not act A named async actor holding no side-effecting tools at all. It splits the message into a warranty task and a recommendation task. Holding no dangerous tools is deliberate — the component doing free-form interpretation of user text is the one most exposed to injection.
- The recommendation task dispatches immediately, in parallel It's read-only, so per 8.6 there's no reason to serialise it. The supervisor submits it to an ActorPool of catalogue-search agents and doesn't wait — chapter 7's fan-out pattern.
- The warranty task goes to the specialist, sequentially WarrantyAgent checks the purchase date and failure description, calls check_warranty (read-only), and determines the claim is valid.
- The replacement crosses a threshold and triggers approval create_replacement_order is side-effecting, non-idempotent, and above the auto-approve limit. Per 8.5 and 8.21 it requires confirmation — surfaced through elicitation (8.10), carrying an idempotency key so a retry can't duplicate the order.
- Results converge, and the supervisor composes one reply Recommendations came back while the warranty path was still waiting on approval. The supervisor merges both into a single coherent answer. Handoff payloads are explicit, per 8.17's warning.
| Piece | From |
|---|---|
| vLLM replicas generating every model turn | Chapter 6 |
| Named async actors, ActorPool, parallel fan-out | Chapter 7 |
| Catalogue search behind the recommendation agent | Chapters 3–5 |
| Tool schemas, annotations, approval gate, idempotency | This chapter |
[+] The shape worth copying
A supervisor that interprets and composes but holds no dangerous tools; specialists that hold narrow credentials and validate everything they're asked; read-only work fanned out in parallel and side-effecting work executed one step at a time behind an approval gate. That structure is not complicated, and nearly every production agent system that stays out of trouble looks roughly like it.
8.20 Why side effects raise the stakes
Section 5.23's four guardrails bound how much an agent can spend. Once tools act on the world, you also need to bound how much it can do — and those are different problems with different controls.
| Failure | Read-only agent (chapter 5) | Side-effecting agent (this chapter) |
|---|---|---|
| Loops without terminating | Wasted tokens, slow answer | Fifty identical emails to a real customer |
| Misreads an argument | Searches for the wrong thing, recovers next turn | Refunds £499.90 instead of £49.99, unrecoverable without intervention |
| Retries after a timeout | Harmless — the search just runs twice | Duplicate refund, unless idempotency was designed in (8.5) |
| Is manipulated by injected text | Returns a misleading answer | Performs an attacker-chosen action with your credentials |
[!] Chapter 5's guardrails are necessary and no longer sufficient
Keep all four — iteration cap, latency and token budget, repeat-detection, full decision trace. They remain the right first line, and repeat-detection in particular catches the duplicate-action case early. But an iteration cap of five doesn't help if the first action was a wrongly-sized refund, and a decision trace tells you what happened after it already has. Bounding cost and bounding consequence are separate jobs.
[+] Three additional controls, in order of value
- Capability restriction. The most effective by far: an agent cannot misuse a tool it was never given. Narrow toolsets per agent (8.17, 8.18) beat every runtime check.
- Approval gates on irreversible actions. A human confirms before the action, not after — section 8.21.
- Spend and rate limits per session. Bounds the damage from anything the first two missed — section 8.22.
[def] Design for reversibility
The single most useful design instinct for agents with side effects: prefer tools whose effects can be undone, and make the undo path as easy as the do path. An agent that proposes a refund into a queue a human clears is dramatically safer than one that issues refunds directly, and in many workflows it's equally useful. Ask of every side-effecting tool: if this fires wrongly a hundred times, what does it take to reverse it? If the answer is "a support ticket and an apology," the tool needs a gate.
8.21 Human-in-the-loop approval
The control that actually stops bad actions before they happen. The design question is never whether to have approval gates — it's where to put them, because a gate on everything is the same as no gate at all.
[!] Approval fatigue defeats the mechanism entirely
Ask a human to confirm every tool call and within a week they are clicking approve without reading, at which point you have all of the latency cost and none of the safety benefit — plus a worse outcome than no gate, because the audit log now records human approval for something nobody actually reviewed. A gate is only real if the person passing through it is genuinely deciding.
Deciding what needs a gate
| Action profile | Policy |
|---|---|
| Read-only (readOnlyHint) | Never gated. Log and move on. |
| Writes, idempotent, low value — update an address | Auto-approve, notify the customer afterwards. Reversible and cheap. |
| Writes, non-idempotent, below a value threshold — a £20 refund | Auto-approve within a per-session and per-day cap; sample a percentage for human review after the fact. |
| Above threshold, destructive, or affecting another party | Always gated, synchronously, before execution. |
| Anything already attempted and rejected once | Always gated. A retry after a denial is a signal something is wrong. |
[+] Approve the plan, not the step
This is where plan-and-execute from 8.11 pays for itself. Showing a reviewer "issue refund £400" in isolation asks them to judge an action with no context. Showing them the whole plan — verify damage, refund £400, issue prepaid label, waive postage, notify customer — lets them evaluate whether the strategy is right, which is a question a human can actually answer well. One meaningful approval beats five context-free ones.
[def] What the approval record must capture
Who approved, when, exactly what was shown to them, and the arguments as they stood at approval time. That last detail matters more than it sounds: if the agent can modify the arguments between approval and execution, the approval is meaningless. Freeze the call at the moment it's presented, and execute precisely what was approved — anything else, including a "minor correction," goes back through the gate.
[retail] Asynchronous approval keeps the agent useful
A gate doesn't have to block the conversation. The agent can tell the customer "I've submitted a £400 replacement for approval, you'll get confirmation within the hour," queue it, and carry on answering their other questions. The elicitation primitive from 8.10 supports the synchronous case, but for anything a supervisor rather than the customer must approve, a queue plus a notification is usually the better product decision — and it removes the pressure to set thresholds high just to keep the conversation flowing.
8.22 Cost and spend controls
Two entirely different budgets share the word "cost" in agent systems: tokens burned, and money moved. Both need caps, and confusing them leaves one of them unbounded.
- Inference cost
- Tokens spent on model calls. An agent multiplies this: every tool result re-enters the context on the next turn, so a ten-step run costs far more than ten single calls — it grows closer to quadratically than linearly as the conversation accumulates.
- Action cost
- Real-world money the agent's tools move — refunds issued, replacement orders created, credits applied. Chapter 5 had no equivalent, because retrieval spends nothing.
[!] Why agent inference cost surprises people
A five-step agent run is not five times a single call. Turn one sends the system prompt plus tool schemas; turn two sends all that plus the first tool call and its result; turn five sends everything that came before. With verbose tool results the input tokens grow every turn, and prefill dominates — exactly the regime chapter 6 showed is compute-bound. This is why 8.12's advice to truncate tool results is a cost control as much as a quality one, and why prefix caching from 6.12 matters so much for agents specifically: the stable prefix is re-sent on every single turn.
Where to put the limits
- Per-run token budget A hard ceiling on total tokens for one task, checked between steps. When exceeded, stop and escalate rather than continuing — chapter 5's guardrail, still correct.
- Per-session action budget Total real money one conversation may move. A support session that has already refunded £400 should require approval for the next pound regardless of thresholds.
- Per-user, per-day limits Catches the case a single session cannot see: the same customer opening eight conversations to obtain eight refunds. Session-scoped budgets are blind to this by construction.
- Global circuit breaker An aggregate rate across all sessions, with an automatic halt. If total refunds in an hour exceed several times the normal rate, stop the capability entirely and alert someone.
[+] The circuit breaker is the one people skip
Per-session limits feel sufficient because they bound the worst case for any one conversation. They do not bound the worst case for the system: a prompt injection that works once works on every session simultaneously, and a bad deploy misbehaves everywhere at once. Ten thousand sessions each staying under a £50 cap is a £500,000 incident in which every individual limit worked perfectly. The aggregate control is the only one that catches this, and it is almost always the last one teams add — usually right after they needed it.
[retail] Attribute spend to something you can act on
Record cost per run against the things you might actually change: which agent, which tool, which task type, which model. "Agents cost us £40,000 a month" prompts a panic; "the recommendation specialist accounts for 70% of token spend, mostly re-sending catalogue results that 8.12's truncation would shrink" prompts a fix. This is chapter 7's observability instinct applied to money.
8.23 Observability and evaluation
Agents fail in ways single model calls don't: the final answer looks fine and step four was wrong, or every step was individually reasonable and the sequence made no sense. You cannot debug that from outputs alone.
[def] The trace is the unit of debugging
Section 5.23 already asked for a full decision trace. For side-effecting agents the trace is additionally an audit record, and needs to answer questions nobody asked at the time: exactly what arguments were sent, what came back, what the model was shown before deciding, who approved what, and which idempotency key was used. A trace good enough for debugging is usually not detailed enough for an audit — build for the audit.
Metrics that mean something for agents
| Metric | Catches |
|---|---|
| Task success rate | The only metric that matters end-to-end — did the user's actual goal get met? Requires labelled outcomes, which is why teams avoid it and why it's worth the effort. |
| Steps per task | Rising step counts mean the agent is struggling before success rate visibly drops. An early warning signal. |
| Tool selection accuracy | How often the right tool was chosen for the situation. Directly tests the schema quality from 8.4. |
| Cap-hit rate | How often runs terminate at the iteration or token limit rather than completing. A cap doing its job occasionally is healthy; a cap firing on 15% of runs means something upstream is broken. |
| Approval override rate | How often humans reject what the agent proposed. Rising rejections mean the agent's judgement is drifting from policy — and a rate near zero may mean the reviewers have stopped reading (8.21). |
| Reversal rate | Actions later undone by a human. The clearest signal of real-world harm, and the one worth alerting on. |
[!] Final-output evaluation hides step-level failure
Judging only the last message misses the agent that reached a correct answer through three wrong tool calls and a lucky recovery — which will not stay lucky at scale. It equally misses the opposite: a perfectly reasoned run that ends with a bland reply scoring poorly. Evaluate the trajectory, not just the destination, and grade tool choice and argument correctness as their own metrics.
[+] Build the evaluation set from your own failures
Exactly as section 5.34 argued for RAG: the most valuable test cases are the real runs that went wrong. Every incident becomes a fixed case in a regression set, so the same failure cannot ship twice. For side-effecting agents, run that set against sandboxed tools that record what would have happened rather than doing it — which requires the tool layer to support a dry-run mode. Design that in from the start; retrofitting it to a refund system already in production is considerably less pleasant.
8.24 The framework landscape
Everything so far has been mechanism, deliberately framework-free, because the mechanism is what you're accountable for. This section is about which library packages that mechanism for you, and the honest answer to "which one" is narrower than the marketing suggests.
[!] Almost none of them can do something the others cannot
This is the question people most want answered, so take it head-on: for the core capability — run a loop, call tools, hold state, coordinate several agents — there is no meaningful capability gap. Every framework here can express every pattern in this chapter, and all of it is implementable in a few hundred lines without any framework at all. What differs is what comes packaged: persistence, evaluation harnesses, deployment targets, and how much control you retain when your needs stop matching the library's assumptions. Choose on those, not on capability.
| Framework | Version | Core abstraction | Reach for it when |
|---|---|---|---|
| No framework | — | Your own loop, per 8.3 | One agent, a handful of tools, and you want the control flow to be exactly what you wrote. A genuinely underrated default. |
| LangGraph | 1.2.11 | StateGraph — explicit nodes and edges over typed state | Control flow is the hard part: branching, cycles, resumability. The graph is inspectable and durable rather than implied by prompt text. |
| LangChain | 1.3.15 | Component abstractions over models, tools, retrievers | You want breadth of pre-built integrations. Increasingly used as a component library beneath LangGraph rather than an agent runtime itself. |
| Google ADK | 2.7.0 | Composable agents, plus a large evaluation and deployment surface | Evaluation and operational tooling matter as much as the agent, or you're deploying onto Google infrastructure. Detailed in 8.25. |
| CrewAI | 1.16.16 | Crews, roles, tasks, processes | The role-and-task metaphor genuinely matches your problem. Fast to a working prototype; the metaphor can fight you when it doesn't fit. |
| OpenAI Agents / Pydantic-AI | 0.23.1 / 2.31.0 | Thin, typed loops close to the provider API | You want typed tool signatures and very little indirection between your code and the model call. |
The differences that are actually real
Having said no capability gap exists, four differences genuinely affect what you can build without fighting the library:
Durable, resumable execution
LangGraph checkpoints graph state, with a durability mode of "sync", "async" or "exit", so a run can survive a process restart and resume. Rebuilding that yourself is real work.
Built-in evaluation
ADK ships roughly fifty evaluation modules, thirteen named metrics and a user simulator. This is the widest genuine gap in the table — most frameworks give you tracing and leave evaluation entirely to you.
Deployment targets
ADK has first-party deploy paths to Cloud Run, GKE and Agent Engine. That's an integration convenience, and it comes with a gravitational pull toward one cloud.
Interoperability
ADK ships adapters for LangChain and CrewAI tools, and can wrap a compiled LangGraph graph as an agent. Framework choice is less locked-in than it looks.
[+] How to actually choose
- Start with no framework if you have one agent and few tools. You will understand your own loop, and 8.3 showed how short it is.
- Add LangGraph when control flow becomes the difficulty — branching, retries, and especially runs that must survive a restart.
- Add ADK when you need the evaluation and operations surface, or you're on Google Cloud already.
- Choose on the parts you can't easily rebuild — durable state, evaluation harnesses, deployment — never on who has "agents" in the README.
[!] Check the churn before you commit
This space moves fast enough that today's idiomatic API is next year's deprecation. In ADK 2.7.0 specifically, SequentialAgent carries a deprecation notice in favour of a newer Workflow API, and the YAML agent config loader is deprecated too. That's not a criticism of ADK — every framework here is moving — but it is a reason to keep your business logic in plain functions the framework calls, rather than distributed across framework classes you'd have to rewrite. The tool implementations from Part B should not know which framework is orchestrating them.
8.25 Google ADK in detail
ADK is worth a closer look not because it can do something the others can't, but because the shape of what it ships tells you what Google thinks the hard parts of production agents are — and that assessment is largely correct.
Agent types: composition rather than prompting
ADK's central idea is that orchestration should be structural. Rather than instructing one model to "first do A, then B," you compose agents whose control flow is code:
- LlmAgent
- The workhorse — a model, an instruction, and a toolset. This is 8.3's loop.
- ParallelAgent
- Runs its sub-agents concurrently in isolated branches. The right shape for 8.6's independent read-only fan-out.
- LoopAgent
- Repeats sub-agents until one escalates or max_iterations is hit — the iteration cap from 5.23, enforced structurally rather than by prompt.
- Workflow
- The newer graph-based API, with nodes, joins, triggers, retry config and a dynamic scheduler. SequentialAgent is deprecated in its favour.
- RemoteA2aAgent
- A local handle to an agent running somewhere else, resolved from an agent card — covered below.
- LangGraphAgent
- Wraps a compiled LangGraph graph as an ADK agent. The frameworks are not mutually exclusive.
[+] Why structural orchestration matters for safety
A LoopAgent with max_iterations=5 cannot run six times, regardless of what the model decides. An instruction saying "try at most five times" is a suggestion a confused model can ignore. This is the same argument as 8.17's permission boundaries: constraints enforced by structure hold when the model misbehaves, and constraints expressed in prompts are exactly what stops holding at that moment.
The operational surface
| Area | What's there | Maps to |
|---|---|---|
| Sessions | In-memory, SQLite, general database, and managed cloud backends | Short-term memory (8.12), with a real persistence story rather than a dictionary |
| Memory | In-memory plus managed memory-bank and RAG-backed services | Long-term memory (8.12) — and note it is, as predicted, retrieval underneath |
| Code executors | Container, GKE, cloud sandbox — and an explicitly named unsafe_local variant | Sandboxing (8.23). The naming is a good sign: the dangerous option is labelled dangerous. |
| Plugins | Logging, tracing, context filtering, analytics, retry-on-reflection | Observability (8.23) as cross-cutting hooks rather than code sprinkled through your loop |
| Planners | A built-in planner and a ReAct-style planner | 8.11's planning strategies, selectable rather than hand-written |
| Tools | MCP client support, search, BigQuery, bash, computer use, agent-as-tool, plus LangChain and CrewAI adapters | Part B and Part C — including consuming the MCP servers from 8.9 |
A2A: agents calling agents across boundaries
[def] Agent-to-agent, and agent cards
Where MCP standardises how an agent reaches a tool, A2A standardises how an agent reaches another agent that it doesn't own and may not share a codebase, team or language with. The remote agent publishes an agent card — a description of what it can do and how to reach it — and ADK's RemoteA2aAgent resolves that card from an object, a URL, or a file, then handles message conversion and session state across the boundary.
[!] A2A is not ADK-exclusive
It would be easy to present this as ADK's unique feature. It isn't — CrewAI 1.16.16 ships its own A2A module too. A2A is an emerging cross-framework protocol rather than one vendor's differentiator, which is precisely what makes it worth paying attention to: the value of a protocol is that more than one implementation speaks it.
[retail] When A2A earns its complexity
Inside one codebase, A2A is pure overhead — call the agent directly, or make it a Ray actor as in 8.18. It earns its keep at organisational boundaries: the returns team owns a returns agent, deploys it on their own schedule, in their own language, with their own credentials, and your support agent consumes it without either team sharing code. That's the same N+M argument as 8.8, moved up a level from tools to agents — and the same 8.17 warning applies, since a remote agent is a handoff with a network boundary through it.
[+] The honest summary of ADK
Its strongest, least-matched offering is the evaluation subsystem in 8.26. Its agent composition is good and structurally enforced, but broadly matched by LangGraph. Its deployment integration is genuinely convenient and genuinely a pull toward one cloud. Adopt it for the evaluation and operational surface; keep your tool implementations and business logic in plain functions, so that surface is something you're using rather than something you're married to.
[def] Where this is continued
This section answers should I use ADK? Chapter 10 answers how do I build the thing? — hands-on, against a later release, and with every claim executed rather than described: grounded loops with a hallucination validator and entailment checking, the four control-flow shapes composed into one pipeline, skills, fully autonomous agents, and multi-agent flows run across a batch. It also demonstrates three failure modes that produce correct-looking output, including one that quietly multiplies your model spend.
8.26 Evaluating multi-agent systems
Section 8.23 argued for grading the trajectory rather than the destination. Multi-agent systems make that harder in a specific way: when a five-agent run fails, the failing agent and the responsible agent are frequently not the same one.
[!] The attribution problem
A supervisor mis-summarises a request; the specialist receives an accurate-looking but wrong task; it executes that task perfectly. End-to-end evaluation marks the specialist as the failure, because that's where the wrong action happened. Fix the specialist and nothing improves, because the fault was in the handoff two steps earlier — exactly the information loss 8.17 warned about. Any evaluation that only scores final outputs will consistently point you at the wrong component.
Four levels, and what each can and cannot see
| Level | Question it answers | Blind to |
|---|---|---|
| 1. Component | Given this exact input, does this one agent do the right thing? Run each agent in isolation against fixed inputs. | Everything about how agents combine. All components can pass while the system fails. |
| 2. Handoff | Did agent A pass everything agent B needed? Score the payload against what B actually required. | Whether the overall decomposition made sense in the first place. |
| 3. Trajectory | Was the sequence of agents and tool calls a sensible route to the goal? | Whether the final answer was any good. |
| 4. Outcome | Did the user's actual goal get met, and were the real-world actions correct? | Which component to fix — the attribution problem above. |
[+] You need level 4 and at least one of 1–3
Outcome evaluation alone tells you something is broken but not where. Component evaluation alone tells you everything is fine while users disagree. The practical combination is outcome plus trajectory: outcome tells you whether, trajectory tells you where, and you add handoff scoring when trajectory analysis keeps pointing at the seams between agents.
How ADK operationalises this
ADK's evaluation module is worth studying even if you never adopt it, because its metric taxonomy is a concrete answer to the levels above. Its named metrics include:
- tool_trajectory_avg_score
- Compares actual tool calls against expected ones. Crucially it offers three match modes: EXACT (no extra or missing calls), IN_ORDER (all expected calls present in order, others allowed in between), and ANY_ORDER (all present, order irrelevant). Each invocation scores 1.0 or 0.0 and the result is averaged.
- multi_turn_task_success_v1
- Outcome evaluation across a whole conversation rather than a single turn — level 4.
- multi_turn_trajectory_quality_v1 and multi_turn_tool_use_quality_v1
- Trajectory and tool-use quality assessed across multi-turn interactions — level 3.
- hallucinations_v1
- Groundedness of claims, the chapter 5 concern carried into agent outputs.
- safety_v1
- Safety scoring as a first-class metric alongside correctness.
- rubric_based_* variants
- Final-response quality, tool-use quality and multi-turn trajectory scored against explicit written rubrics rather than a single opaque number.
[+] The three match modes are the most portable idea here
Even with no intention of using ADK, steal this distinction. Most teams write trajectory tests as exact sequence matches, which then fail constantly for harmless reasons — the agent checked stock twice, or looked something up in a different order. EXACT is right for a rigid compliance sequence. IN_ORDER is right when relative order matters but extra steps are acceptable — refund must follow verification. ANY_ORDER is right when you only care that the necessary calls happened. Picking the loosest mode that still catches real failures is what makes a trajectory suite survive contact with a changing agent.
Simulated users, and the trap in them
[def] User simulation
Multi-turn agents cannot be evaluated from static test cases, because the second user message depends on what the agent said first. ADK's answer is an LLM-backed user simulator driven by composable behaviours — each with behaviour instructions and violation rubrics — so an eval case is a persona plus a conversation plan rather than a fixed script.
[!] A simulated user is another model that can be wrong
This is the trap, and ADK's own design acknowledges it by shipping a metric that scores the simulator's behaviour turn by turn. If your simulated user is unrealistically cooperative — always supplying the order number, never changing its mind, never frustrated — your agent will score beautifully and fail on contact with real people. Simulation extends your coverage; it does not replace real traffic, and a simulator nobody validates is a confident source of false reassurance.
[retail] What to actually build first
Start with twenty real conversations that went wrong, replayed against sandboxed tools in dry-run mode per 8.23, scored on outcome and on tool trajectory with IN_ORDER matching. That is a weekend of work, catches the majority of regressions, and doesn't depend on any framework. Add simulated users when you need coverage of conversations that haven't happened yet, and per-component scoring when trajectory failures keep pointing at one agent. Building the elaborate harness before you have the twenty real failures is the most common way teams spend a month on evaluation and learn nothing.
8.27 Key takeaways
The fifteen things worth remembering
- The model never calls anything. It emits a structured request; your code executes it. Every safety mechanism in this chapter lives in the gap between those two events.
- Read-only versus side-effecting is the distinction everything else hangs off. A wrong search wastes tokens; a wrong refund costs money and trust. The mechanism is identical, the blast radius is not.
- A tool description is a prompt, not a comment. MCP's own spec calls it a hint to the model. Overlapping descriptions across tools measurably increase wrong-tool selection.
- Idempotency is the property that decides how dangerous a retry is. Where you can, make side-effecting tools idempotent with a stable key — defensive design in the tool beats careful prompting in the model.
- Parallelise read-only calls freely; execute side-effecting calls one at a time. A model asked to batch a call whose arguments it doesn't know yet will invent them.
- Write tool errors for the model, not for your logs. A specific, actionable error frequently produces a correct call next turn. Never surface infrastructure errors the model can't act on — that's how runaway loops start.
- MCP turns N×M integrations into N+M through three primitives distinguished by who initiates: tools (model), resources (application), prompts (user). Standardising the interface does not make an external server trustworthy.
- Plan-and-execute buys a safety property, not just accuracy: the plan exists before anything executes, so it can be checked against policy and approved as a coherent strategy rather than as eight context-free steps.
- Long-term memory is RAG with an expiry policy. Reuse chapters 3–5 for the mechanism; the genuinely agent-specific problems are deciding what's worth storing and retiring what stops being true.
- Context engineering beats prompt engineering on a long run. By step twenty almost nothing the model can see was written by you. Evict superseded results and unused fields before compacting anything, because those are the only lossless moves.
- Compact the middle, never the head or the tail, and compact from the original span rather than from the previous summary — summarising a summary degrades through a game of telephone with no record of what was lost.
- Compacting too eagerly can cost more than it saves, because rewriting the context invalidates the prefix cache and forces a full re-prefill. Compact in infrequent large batches, and leave headroom for the largest tool result you have actually seen.
- The harness is the part you engineer; the model is fixed. Prompt instructions dilute as context grows. A tool that cannot be called wrongly, a result that cannot be misread, and an error that states the fix apply at every step regardless.
- Error messages are the highest-leverage text in an agent, read exactly when the model is deciding what to do next. Name what was wrong, quote what was sent, state the recovery. Most spinning loops are missing the third part.
- Prefer a verifier to self-reflection wherever one exists. A schema check or a test suite is code: it cannot hallucinate agreement and costs no tokens. Bound repair attempts, because repeated verifier failure means the task is misspecified.
- Per-step reliability compounds. 95% per step is 36% over twenty steps. The highest-value optimisation is usually removing steps, not upgrading the model.
- Decomposition alone does not improve that arithmetic — three chained eight-step runs multiply out identically to one twenty-four-step run. The gain comes from a verified handoff that lets a failed stage retry without restarting everything.
- Detect loops in the harness, never by asking the model, which is by construction the faculty that already failed to notice. Hash the call and its arguments; break on the third repeat.
- Split into multiple agents for permission boundaries above all. A tool that isn't registered to an agent cannot be reached from it — a structural guarantee that survives an adversary, unlike better prompting.
- An approval gate on everything is the same as no gate. Gate irreversible and above-threshold actions, freeze the arguments at approval time, and prefer approving a plan over approving a step.
- Per-session limits don't bound a systemic failure. Ten thousand sessions each under their cap is still an enormous incident with every individual limit working perfectly. The aggregate circuit breaker is the control teams skip and then wish they hadn't.
- No agent framework can do something the others fundamentally cannot. Choose on what you can't easily rebuild — durable resumable state, evaluation harnesses, deployment targets — not on capability claims.
- Structural constraints beat prompted ones. A loop agent capped at five iterations cannot run six; an instruction saying "try at most five times" is a suggestion a confused model can ignore.
- In multi-agent evaluation, the failing agent and the responsible agent are often different. Outcome scoring tells you whether something broke; trajectory and handoff scoring tell you where. You need both.
[def] The one-sentence version
Give the model tools described well enough to choose correctly, execute them yourself with validation and idempotency in between, restrict what each agent can reach at all, gate the irreversible actions on a human who is genuinely deciding, and bound the damage at the session, user, and system level — because the loop is the easy part, and everything hard is in what happens when a step is wrong.
8.28 Interview drills
Agent questions separate people who have shipped one from people who have read about them. The tell is almost always whether the answer engages with what happens when a tool call is wrong.
1. Walk me through what actually happens when a model "calls a tool."
The model doesn't call anything — that's the part worth being precise about. I send the conversation plus a list of tool schemas, each with a name, description and JSON Schema for its arguments. Rather than plain text, the model can emit a structured object naming a tool and filling in arguments. My code parses that, validates it, executes the actual function, and appends the result to the conversation as a new message. Then the model either replies to the user or requests another call.
Keeping that separation sharp matters because everything safety-related lives in the gap between the model requesting and my code executing — argument validation, approval gates, spend checks, idempotency keys. If you think of it as "the model called the tool," there's nowhere for any of that to go.
2. What changes when your agent's tools have side effects rather than only reading?
The loop doesn't change at all — the blast radius of every failure does. A read-only agent that loops wastes tokens and returns a slow answer. The same loop with a send_email tool sends fifty emails to a real customer. A misread argument in a search means searching for the wrong thing and recovering next turn; in a refund it means paying out ten times the right amount.
So the standard guardrails — iteration cap, token budget, repeat detection, decision trace — stay, but they only bound cost. I'd add three things that bound consequence: restricting which agent can reach which tool at all, approval gates on irreversible actions, and spend limits at session, user and global level. And I'd push as much defence as possible into the tool itself, particularly idempotency keys, because that protects against agent behaviour I didn't anticipate.
3. Your agent occasionally issues duplicate refunds. Diagnose it.
My first guess is a call that timed out after the refund actually succeeded. The agent sees a failure, retries, and the payment system happily processes a second refund because nothing tells it the two calls are the same logical action. That's not really a model problem — it's a missing idempotency key.
The fix is to have the agent generate a stable key per logical action, derived from something deterministic like the order ID and reason rather than randomly, and have the payment system reject a second call carrying a key it has already seen. Then a retry safely returns the original result. I'd also check whether the model is emitting the refund twice in one parallel batch, which is the other common cause, and enforce that side-effecting tools execute one at a time regardless.
What is being tested: whether you reach for a systems fix or try to prompt the problem away.
4. How do you decide which actions need human approval?
By reversibility and value, not by how important the action feels. Read-only calls are never gated. Idempotent low-value writes like updating an address get auto-approved with a notification. Non-idempotent actions below a value threshold auto-approve within a session cap, with a sample reviewed afterwards. Anything above threshold, destructive, or affecting a third party is gated synchronously before execution — as is any action the agent already had rejected once, since a retry after a denial is itself a warning sign.
The failure mode I'd actively design against is approval fatigue. Gate everything and reviewers start clicking approve without reading, which is worse than no gate because now the audit log claims human oversight that didn't happen. That's also why I'd rather show a reviewer a whole plan than an isolated step — judging whether a strategy makes sense is a question a person can actually answer, and one meaningful approval beats five context-free ones.
5. What problem does MCP solve, and what problem does it not solve?
It solves N×M. Without a standard, every application re-implements every tool integration in its own provider's format — five tools and three apps is fifteen pieces of glue that do nearly the same thing. With one protocol between tool servers and tool consumers, that becomes N+M: each tool system is implemented once as a server, each application implements the client side once. Organisationally that matters more than technically, because the team owning refunds writes the refund server once with the annotations and idempotency handled properly, instead of five teams each making their own safety decisions.
What it doesn't solve is trust. Connecting to a third-party MCP server means letting text you don't control describe tools to your model, and often letting that server see the arguments you send. A malicious server can write descriptions designed to manipulate the model into calling it with sensitive data. The protocol standardises the interface, not the trustworthiness of what's behind it, so I'd scope credentials tightly and review a server before adopting it. I'd also note the tool annotations are explicitly documented as hints, not guarantees — useful for driving my own policy, useless as a security boundary.
6. When is multi-agent actually worth it?
Less often than people think. Every handoff loses context, and a chain of four agents at 90% reliability each is a system at about 66%. Most "we need multi-agent" instincts are really one agent with confusing tool descriptions, which is far cheaper to fix.
The reason that genuinely holds up is permission separation. If only a narrow refund agent holds the refund credential, then no amount of prompt injection against the general support agent can issue a refund — the capability isn't reachable from there. That's a structural guarantee rather than a quality improvement, which is why it survives an adversary when better prompting wouldn't. Irreconcilable instructions and contexts too large to coexist are real secondary reasons, but I'd treat those as quality problems I might solve other ways first.
7. Why does agent inference cost grow faster than people expect?
Because a five-step run isn't five single calls. Every turn re-sends everything before it: the system prompt and tool schemas, then the first tool call and its result, then the second, and so on. Input tokens grow every turn, so cost is closer to quadratic than linear in the number of steps. Verbose tool results make it much worse — a few JSON blobs can dwarf the actual conversation.
Two things help most. Truncating tool results to the fields that matter is usually the single biggest win and improves quality too, since it fights the lost-in-the-middle effect. And prefix caching matters far more for agents than for chat, because that large stable prefix is re-sent on literally every turn — which is a prefill-dominated, compute-bound workload of exactly the kind worth tuning the serving layer for.
8. Your agent's final answers look good in evaluation but you're seeing real-world problems. What are you measuring wrong?
Almost certainly judging only the final output. That hides an agent reaching the right answer through three wrong tool calls and a lucky recovery — which won't stay lucky at scale — and it can't see anything about actions taken along the way, which is exactly where side-effecting agents cause harm. A run can produce a perfectly worded reply while having refunded the wrong amount.
I'd evaluate the trajectory: tool selection accuracy, argument correctness, steps per task as an early warning that the agent is struggling, and cap-hit rate. For real-world harm specifically, the two most informative metrics are approval override rate — how often humans reject what the agent proposed, which tracks judgement drift — and reversal rate, meaning actions a human later had to undo. I'd also build the regression set out of real incidents and run it against sandboxed tools with a dry-run mode, which needs designing in early rather than retrofitting onto a live refund system.
9. Which agent framework would you pick, and why?
I'd push back gently on the premise first, because for the core capability — run a loop, call tools, hold state, coordinate agents — there isn't a real gap between them. All of it is a few hundred lines without any framework. So I'd choose on the things I can't cheaply rebuild rather than on capability claims.
Concretely: no framework at all for a single agent with a few tools, because then the control flow is exactly what I wrote. LangGraph when control flow becomes the hard part — branching, cycles, and especially runs that must survive a process restart, since durable checkpointed state is genuinely tedious to build yourself. Google ADK when the evaluation and operational surface matters, which is its widest real advantage, or when we're on Google Cloud already. Either way I'd keep tool implementations and business logic in plain functions the framework calls, because this space deprecates fast — ADK 2.7 already deprecates its own SequentialAgent in favour of a newer workflow API.
What is being tested: whether you evaluate tools on substitutable value or repeat marketing claims.
10. How do you evaluate a multi-agent system, and why isn't end-to-end scoring enough?
End-to-end scoring can't attribute failure. If a supervisor mis-summarises a request and a specialist then executes that wrong task perfectly, outcome evaluation blames the specialist — so you fix the wrong component and nothing improves. The fault was in the handoff two steps earlier.
So I'd evaluate at several levels: components in isolation against fixed inputs, handoff payloads against what the receiving agent actually needed, the trajectory of agents and tool calls as a route to the goal, and the real outcome. Outcome tells me whether something broke, trajectory tells me where. Component-only evaluation gives you a system where every part passes and users still complain.
Two practical details. Don't write trajectory tests as exact sequence matches — the exact versus in-order versus any-order distinction is worth stealing from ADK regardless of framework, because exact matching fails constantly for harmless reasons like the agent checking stock twice. And if I used simulated users to cover conversations that haven't happened yet, I'd validate the simulator itself — an unrealistically cooperative simulated user that always supplies the order number and never gets frustrated produces excellent scores and an agent that fails on contact with real people.
Where this leaves you
You can now design an agent that acts rather than only answers: tool schemas a model can actually choose between, validation and idempotency in the gap between request and execution, a protocol layer that stops every application re-implementing the same integrations, planning and memory sized to the task, specialists whose credentials are genuinely out of reach of the components most exposed to manipulation, and budget controls at every level from a single call to the whole system.
The thread running through the chapter is one question asked repeatedly: what happens when this step is wrong? Chapter 5's agentic RAG could mostly shrug at that question, because a bad search is a wasted search. Once a tool moves money or sends something to a real person, the answer has to be concrete at every step — and the design work is almost entirely in making sure it is.
From here, chapter 9 steps back from the model layer entirely and covers the API design that fronts everything built so far: the concrete choices for exposing a React frontend to endpoints that include long-running LLM inference, why FastAPI became the default for that job, and the HTTP-level concepts — streaming, auth, versioning, idempotency — that this chapter's tool layer and chapter 6's serving layer both sit behind.