Agentic Architecture and Orchestration
How to make Claude act, observe, and keep acting until the work is actually finished, and how to scale that loop into systems you can trust.
Agentic architecture is not a bigger prompt. It is control flow you own, wrapped around a stateless model call, that lets Claude act in the world and learn from what happened before deciding what to do next. Everything in this domain rests on one fact: the loop lives in your code, not inside the model. You send the conversation, the model asks for tools or declares itself done, you execute and append results, and you go round again.
That single fact organises the rest. Orchestration is a decision about who runs which loop and what they may see. Hooks are the points where your code interposes on a loop the SDK is running. Decomposition is a decision about how much any one pass should hold. Session state is a decision about whether the observations already in a history still describe the world. Each task builds on the vocabulary of the one before it, so the chapters are ordered to be read in sequence.
One pattern recurs so often it is worth naming before you meet it seven times. Most wrong answers in this domain substitute something probabilistic for something deterministic: reading prose instead of a status field, a prompt instruction instead of a code gate, a bigger model instead of a second pass, a firmer request instead of a structural change. When you are choosing between options, ask which one still holds when the model behaves slightly differently than you expected.
Agentic Loops
The control flow you own around a stateless model call, and the one field that decides whether it keeps going.
A model call is a function. You pass text in, you get text out, and nothing persists. An agent is a loop wrapped around that function, and the loop lives in your code. Nothing inside Claude is looping. Nothing on Anthropic's side is remembering. Every mechanism in this domain, every orchestration pattern, every hook, every session decision, is built on top of a while loop that you wrote and that you are responsible for terminating correctly.
That sounds almost too simple to be worth a chapter. It is not, because the loop has exactly one correct exit condition and roughly a dozen plausible-looking wrong ones, and because the shape of the messages you accumulate inside the loop is governed by a contract that returns hard errors when you break it. Most production agent failures, and most exam scenarios, are one of those two things: a loop that exited on the wrong signal, or a conversation whose structure no longer says what the developer thinks it says.
So this chapter builds the loop once, carefully, and then spends its time on the two places it goes wrong: termination and message structure. Everything after this task assumes you can hold the loop in your head.
Four phases, one decision
The cycle has four phases. You send, you inspect, you execute, you append. Then you send again. Only the second phase involves a decision, and that decision is a switch on a single string.
- Send. Build the request from the full conversation so far, the system prompt, and the tool definitions. Post it to the Messages API.
- Inspect. Read
stop_reasonfrom the response. This is the model telling you why it stopped generating, and it is the only thing in the response that answers the question you actually have. - Execute. If the model asked for tools, run them. Every
tool_useblock in the response gets executed, in your process, by your code. Claude never runs anything. - Append. Push the assistant message exactly as it came back, then push a new user message carrying one
tool_resultpertool_use. Go back to phase one with the grown conversation.
Written out, the whole thing is short enough to read in one sitting, and short enough that the failure modes have nowhere to hide.
async function runAgent(userPrompt: string) {
const messages: Message[] = [{ role: "user", content: userPrompt }]
let turns = 0
while (turns < MAX_TURNS) {
turns += 1
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
system: SYSTEM_PROMPT,
tools: TOOL_DEFINITIONS,
messages,
})
// The whole decision lives in this one field.
switch (response.stop_reason) {
case "end_turn":
return finalText(response)
case "tool_use": {
messages.push({ role: "assistant", content: response.content })
const results = await executeAll(response.content)
messages.push({ role: "user", content: results })
continue
}
default:
// Not finished, finished badly, or paused. Never a success path.
return handleNonTerminal(response, messages)
}
}
return degradeGracefully(messages, "turn guard reached")
}Read the switch again. Two branches do real work and the default branch exists because there are five other values stop_reason can hold, none of which mean success. Most broken loops in the wild are broken because that default was written as return response on the theory that anything which is not tool_use must be a finished answer.
The loop is yours. The model contributes one field per turn to tell you what to do with it.
Why termination is the whole game
There is exactly one signal that means the model considers itself finished, and it is stop_reason equal to end_turn. That fact carries more weight than it first appears to, because the alternatives are genuinely tempting. Each one looks like it should work, and each one works in testing.
The most common alternative is to read the text. Claude narrates. It says things like "I have what I need, let me confirm the order status" and then, in the same response, issues the tool call. The narration and the request arrive together, in one content array, under one stop_reason of tool_use.
{
"id": "msg_02XYZ456",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "I have everything I need. Let me confirm the order status."
},
{
"type": "tool_use",
"id": "toolu_01ABC123",
"name": "lookup_order",
"input": { "order_id": "A-4471" }
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": { "input_tokens": 320, "output_tokens": 48 }
}A loop that inspects content[0].type, or scans the text for a completion phrase, reads that response as finished. It returns the narration to the user as the answer. The order was never looked up. Worse, this failure is silent and intermittent: it depends entirely on whether the model chose to narrate before calling, which varies with phrasing, temperature, and task.
Scanning for phrases like "task complete", "analysis finished", or a formatting marker such as FINAL: fails in both directions. The phrase appears mid-task when the model summarises a step, so you exit early. The phrase is absent when the model genuinely finishes but phrases it differently, so you run to the guard. Adding more phrases to the list widens both failure modes at once. There is one legitimate version of this idea: give the model a task_complete tool and branch on the tool name. That is a structured channel you control, which is a different mechanism from reading prose.
The second common alternative is a counter. Run at most eight iterations, then stop. This is not wrong so much as misassigned: a cap is a safety guard, not a stopping condition, and the two have opposite tuning pressures. Work length is data-dependent. A refund might resolve in three tool calls today and seven tomorrow. A trace investigation might need twenty. If you tune the cap to the common case you truncate the tail; if you tune it to the tail you burn turns on every short task and lose the protection you wanted. Meanwhile the model has been telling you the answer for free, once per turn, in a field designed for the purpose.
This is the single most common wrong fix in exam scenarios, and it is wrong because the cap was never the cause. Raising it moves the cutoff without curing it, and quietly increases cost on every run. If a loop is exiting before the work is done, the bug is in what you are branching on. Keep a high cap as a genuine last-resort guard that never fires in healthy operation, and log loudly when it does.
Both wrong answers share a shape worth naming, because you will see it again in every task in this domain: substituting something probabilistic for something deterministic. Text is probabilistic. Counters are blind. stop_reason is produced by the same decoding step that produced the content, and it is the model's own statement about whether it intends to act again.
Branch on stop_reason, display the text. Never the other way round.
The values you will actually see
The exam guide keys two values because two values are enough to explain the concept. A production loop meets seven, and five of them are traps if you treat them as completion. Each obliges you to do something different.
| stop_reason | What happened | What you must do |
|---|---|---|
end_turn | The model finished naturally. Text blocks, no tool_use. | Terminate. Return the text. This is the only unambiguous success. |
tool_use | The model wants one or more tools before continuing. | Execute every block, append every result, loop. |
max_tokens | Generation was cut off by the max_tokens you set. The response may end mid-sentence, or mid tool_use with a half-built input object. | Treat as incomplete. Raise the limit and retry, or append the partial content and ask for continuation. Never return it as an answer. |
stop_sequence | One of your configured stop_sequences was emitted. | Decide by intent. If you set the sequence as a boundary, honour it. If it fired accidentally, treat it as truncation. |
refusal | The model declined on policy grounds. stop_details carries a category and an optional explanation. | Surface it. Do not retry the same input, it will refuse again. |
pause_turn | A server-side tool such as web search hit its internal iteration budget mid-task. | Send the assistant content straight back so it continues. Not an error, not a completion. |
model_context_window_exceeded | Accumulated history filled the model's window during generation. Distinct from your own max_tokens cap. | Compact, drop, or fork. Then continue. |
Two of these deserve emphasis because they are the ones that produce plausible-looking output. max_tokens returns text that reads like an answer and is simply missing its end. If you return it, nobody notices until a customer does. refusal returns a polite paragraph explaining the decline, which a loop that treats non-tool_use as done will happily present as the result.
Write the default branch as suspicious rather than as success. An unrecognised value means the API told you something your code has not learned yet, which is not the same as finishing.
Statelessness, and what it costs you
The Messages API keeps nothing. There is no session, no server-side thread, no implicit history. Every request is a complete statement of everything the model should know. This is why the loop appends rather than sends: the twenty-first call carries all twenty prior assistant messages and all twenty prior result messages, because if it did not, the model would be reasoning from a world it has never seen.
The immediate consequence is that the model's only memory is the prompt you built. If you execute a tool and forget to append the result, the model's next request is conditioned on exactly the same context as the last one. Same context, same reasoning, same output: it asks for the same tool again. The classic symptom is an agent that keeps asking the customer to repeat an order number it already looked up three times.
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC123",
"content": "{\"status\":\"shipped\",\"carrier\":\"DHL\",\"eta\":\"2026-09-02\"}"
},
{
"type": "tool_result",
"tool_use_id": "toolu_04DEF789",
"content": "Inventory service timed out after 5000ms. Retryable.",
"is_error": true
},
{
"type": "text",
"text": "Optional note for the model. Must come after every tool_result block."
}
]
}Note the ordering inside that user message. Every tool_result block comes first; free text, if you want to add any, comes after all of them. Note also that the failed call still produced a block. A tool that timed out, threw, or was refused by a downstream service still owes the model an observation, marked with is_error. Silence is not a valid answer to a request you provoked.
A summary in your logs, a paragraph of assistant text describing what happened, or a fresh conversation containing only the result: none of these satisfy the contract. The model needs a tool_result block, carrying the exact tool_use_id, in the user message immediately following the request. A text description of a result is not a result. The tell is repetition: if an agent keeps re-requesting something, look for the missing block before you touch the prompt.
The second consequence is economic. Input tokens grow with every iteration because you retransmit everything. A loop producing modest output and modest tool results will be carrying tens of thousands of input tokens after a few dozen turns, and a loop that reads files or database rows gets there far sooner. The constant prefix, your system prompt and tool definitions, is cached and cheap. The accumulating middle is not.
Which means context management is not an optional refinement bolted on later; it is part of writing the loop. Track usage on every response. Set a token budget alongside your turn guard. When you approach it, compact deliberately: summarise older turns into a structured digest that keeps identifiers and decisions, drop stale results while keeping recent ones, or fork the work into a subagent whose transcript never enters this conversation. What you must not do is clear history on a timer and hope, because the observations you discard are the evidence the model is reasoning from. Domain 5 treats this properly. For now, know that the loop has a token cost curve and that you own it.
The prompt is the memory. Anything you do not append, the model never saw.
The pairing contract
Every tool_use block carries an id, prefixed toolu_. Every tool_result must carry that exact value as its tool_use_id. Correlation is by identifier and by nothing else, which is precisely why the design is safe: the model can call the same tool three times in one turn, and the results can come back in any order, and the mapping stays unambiguous.
Three substitutions look reasonable and all of them break. Matching by tool name fails the moment the same tool is called twice. Matching by position fails because the API permits results in any order, so your positional assumption is a coincidence rather than a contract. Generating a fresh identifier fails immediately and loudly, because it matches no outstanding request.
Around that identifier sit three structural rules the API enforces with a 400 rather than a warning. Results live in a message with role user, never assistant and never system. There is no tool role in this API, which is a real source of confusion for anyone arriving from a provider that has one. The result message must come immediately after the assistant message that requested it, with nothing inserted between. And inside that message, all tool_result blocks precede any text block.
The role rule is not bureaucratic. Roles carry meaning: assistant is what the model generated, user is what the world reported back. Filing an observation as assistant tells the model it said something it did not say, which is a much stranger claim than a schema violation.
Parallelism follows from the same contract. When one response contains three tool_use blocks, that was one decision made against one context, and the model is waiting on all three. Execute them concurrently if they are independent, then return the complete set in a single user message. Returning the first and dealing with the others next turn leaves requests outstanding and forces the model to reason from a partial picture of its own plan.
type ToolFailure = {
error_category: "transient" | "business_rule" | "permission" | "system"
is_retryable: boolean
message: string
partial_results?: unknown
}
async function executeOne(call: ToolUseBlock): Promise<ToolResultBlock> {
try {
// Backoff, attempt caps and rate limits live here, not in the prompt.
const value = await withBackoff(() => TOOLS[call.name](call.input))
return { type: "tool_result", tool_use_id: call.id, content: serialize(value) }
} catch (err) {
const failure = classify(err)
return {
type: "tool_result",
tool_use_id: call.id,
is_error: true,
content: JSON.stringify(failure),
}
}
}
// Every emitted tool_use gets exactly one tool_result, success or not.
async function executeAll(content: ContentBlock[]): Promise<ToolResultBlock[]> {
const calls = content.filter((b): b is ToolUseBlock => b.type === "tool_use")
const settled = await Promise.allSettled(calls.map(executeOne))
return settled.map((s, i) =>
s.status === "fulfilled"
? s.value
: {
type: "tool_result" as const,
tool_use_id: calls[i].id,
is_error: true,
content: "Executor crashed before the tool ran.",
},
)
}That executor draws the line this domain keeps returning to. Timing belongs to code: backoff, jitter, attempt caps, rate limits, concurrency. Strategy belongs to the model: given that this failed and why, try a different tool, adjust the arguments, or give up and escalate. A prompt cannot enforce a rate limit, and a model cannot know your infrastructure's retry budget.
Which is why the failure payload is structured rather than prose. Operation failed cannot distinguish a transient slip from a permanent business rule, so the model retries both, and the loop grinds against a wall until the guard fires. A category and an is_retryable flag turn that judgement into something your executor can act on before the model ever sees it.
What makes it agentic rather than scripted
You could write the same four phases with the tool order hard-coded, and for some workloads you should. What makes the loop agentic is that the model chooses the next call after seeing the last result. That distinction matters when the branching is open-ended: a thin search result that warrants a different query, an unexpected field that suggests a different tool, an alert type nobody enumerated.
A decision tree is complete only over the cases someone wrote down. On anything else it falls through. A model-driven loop composes the sequence at runtime and reasons about values rather than matching them. That is the whole argument for the pattern, and it is also the source of every guardrail in the rest of this domain, because a system that decides at runtime is a system that can decide wrongly.
The two are not in competition. Adaptive selection and deterministic gating coexist, and the correct production shape usually has both: the model picks the path, and code enforces the invariants that must hold on every path. Watchlist clearance before premium computation. Validation and an audit write before a balance adjustment. Those are not preferences to be expressed in a prompt; they are conditions to be enforced in code. Task 1.4 is entirely about that boundary.
One parameter deserves mention here because it can quietly destroy termination. tool_choice set to auto lets the model answer in text and emit end_turn when no tool is needed. Set to any, the model is forced to call a tool every single turn, which removes its ability to say it is finished. The loop then executes an unnecessary call, returns the result, and receives another forced call. Forcing has narrow legitimate uses on a single mandatory turn. As a loop-wide mode it is a trap with no exit.
It guarantees the opposite. end_turn becomes unreachable, so the only remaining exits are your turn guard and your budget. The agent runs to the cap on every task, however trivial, and the transcript fills with calls made because the parameter demanded one.
Who owns the loop: hand-rolled, SDK, managed
Everything above describes the hand-rolled case, where you own all four phases. Three other arrangements exist, and the tested question is almost never which is best; it is which requirement is in play.
| Concern | Hand-rolled loop | Agent SDK | Managed |
|---|---|---|---|
| History and compaction | You build and trim the messages array | Managed for you, with a compaction boundary event | Entirely hosted |
stop_reason inspection | You switch on it every turn | Internal, you consume a stream of events | Internal |
| Tool execution | Your executor, your process | Built-in tools run automatically, custom tools via handlers | Anthropic's sandbox |
| Safety bounds | Your counters, budgets, and timeouts | maxTurns and maxBudgetUsd options | Platform-enforced |
| Interception points | Wherever you put them | Hooks at defined lifecycle points | Limited |
Read that table as a gradient of control traded for convenience. If an auditor requires that your application code validate every operation before it happens, a client-executed loop is the answer and delegating execution to a remote connector fails the requirement, because the round trip no longer passes through your process. If the requirement is to run multi-hour investigations overnight without building orchestration, the managed harness is the answer and a hand-rolled loop fails on operational grounds.
The Batch API is asynchronous, which makes it sound like a fit for long-running agent work. It is a throughput and pricing mechanism for large numbers of independent single-shot requests. It does not run an interactive tool loop on your behalf, so nothing in a batch can call a tool, read the result, and decide what to do next. Recognise it as the near-miss it is.
What does not move, whichever surface you pick: tool descriptions, error handling strategy, safety bounds, and the context management approach. Those remain design decisions you own even when the loop mechanics do not.
Hardening: runaways, streaming, and the logical turn
A runaway is not an infinite loop in the usual sense. It is an agent making progress-shaped moves that do not progress: the same search with a slightly different phrasing, the same tool with the same arguments, three times running, because a vague error gave it nothing to change. A single guard does not catch this. Layer three.
- A hard ceiling on turns, budget, and wall-clock time, set high enough that healthy runs never touch it, and logged loudly when they do.
- Duplicate detection. The same tool with identical arguments twice in a row is a signal, not a coincidence. Force a response or escalate rather than letting it become five.
- Graceful degradation. When a budget is nearly spent or a plateau is detected, inject an instruction to summarise progress and hand off. Partial work that says what it covered beats a truncated transcript.
Streaming changes nothing about the contract and everything about the parsing. Deltas arrive for text and for tool_use alike. Text deltas go to the display. Tool deltas must be accumulated into complete calls and executed only once the stream ends and stop_reason is known, because an incomplete input object is not a request you can serve. A parser that treats every delta as displayable text renders JSON at the user and never executes anything.
Finally, a framing that clears up a persistent confusion. From the user's point of view, one question is one turn, even when the model generated four times and your executor ran six tools inside it. That logical turn spans assistant tool_use, your tool_result, and the model's continuation, and it is implemented as sequential appends to the same array. There is no chaining parameter to set. After a result, the model may simply emit another tool_use; that is the mechanism working, not a special mode. And tools are not post-processing applied to a finished answer. They run during generation, to gather what the answer needs.
Multi-Agent Orchestration Patterns
When one agent is not enough, which topology to reach for, and why most multi-agent failures are decomposition failures.
Once you can run a loop, the obvious next move is to run several. A coordinator takes a broad brief, splits it into subtasks, hands each to a specialist, and assembles the results. It is a satisfying architecture to draw and a deceptively easy one to get wrong, because almost every way it fails looks like a different problem than it is.
The two questions this task keeps asking are: should this be multi-agent at all, and if so, who is allowed to talk to whom. The first question has a real answer that is often no. The second has exactly one answer on the exam, and understanding why it is the answer explains most of the rest.
There is also a diagnostic skill buried in here that is worth naming up front. When a multi-agent system produces a confident, well-cited, internally consistent report that is missing an entire category of the topic, the specialists did nothing wrong. The coordinator asked the wrong questions. Recognising that pattern quickly is most of what this task tests.
First, does this need to be multi-agent
Fan-out is not free. Every subagent is a separate API call carrying its own system prompt, its own tool definitions, and its own context initialisation. The coordinator then accumulates every result, so the tokens are paid twice: once inside the specialist, once again in the hub. Add the decompose step, the collect step, and the synthesis step, and a query that a single well-prompted agent answers in six seconds can take forty and cost several times as much.
That trade is worth making when the work genuinely decomposes. It is not worth making when the subtasks are tightly interdependent and share heavy context, when the whole task fits comfortably in one context window, when there is no parallelism to exploit, or when one capability set covers everything. A frequently asked question feature answering from a small internal knowledge base does not need four agents. A cohesive summary of one document does not need three.
Quality tracks decomposition quality, not agent count. Six specialists working from a badly partitioned brief produce a worse answer than one agent with a good prompt, because the partitioning errors are now baked into the structure of the output. When a scenario describes a team that built a six-agent pipeline and got triple the latency with no quality gain, the answer is not to tune the agents.
The corollary is that a coordinator should not always run the full pipeline. It should look at the request and decide which specialists the request actually needs. A simple factual lookup might need one search. A document summary might need extraction and synthesis, and nothing else. Routing that decision through a fixed keyword table is brittle, because keywords overlap and novel phrasings fall through; the coordinator's own reasoning is the routing logic.
Multi-agent is justified by dependency structure and parallelism, not by task size.
Hub and spoke, and why nothing bypasses the hub
The topology is a star. The coordinator sits at the centre. Every piece of information that moves between specialists passes through it. The search specialist returns to the coordinator; the coordinator decides what, if anything, the analysis specialist should receive. No specialist calls, messages, or reads from another specialist. Ever.
This looks like an inefficiency, and in raw hop count it is. What it buys is three properties that are extremely hard to recover once lost. Observability: there is one place where the whole run is visible, so a transcript actually explains what happened. Consistent error handling: one component decides what a failure means and what to do about it. Controlled information flow: what each specialist sees was chosen deliberately rather than inherited by accident.
// Hub and spoke. Specialists never address each other; the coordinator
// owns every edge, which is what makes the run auditable.
async function coordinate(brief: string) {
const plan = await decompose(brief)
// Independent work goes out in one turn, not one at a time.
const findings = await Promise.all(
plan.independent.map((subtask) =>
runSubagent({
systemPrompt: specialistPrompt(subtask.role),
tools: toolsFor(subtask.role),
prompt: renderBrief(subtask),
}),
),
)
// Dependent work receives the findings it needs, explicitly passed.
const synthesis = await runSubagent({
systemPrompt: specialistPrompt("synthesist"),
tools: toolsFor("synthesist"),
prompt: renderBrief(plan.synthesis, { priorFindings: findings.flat() }),
})
return { findings, synthesis, coverage: checkCoverage(plan, findings) }
}Four alternatives keep appearing as tempting options, and each one trades away one of those properties. Direct specialist-to-specialist calls cut latency and destroy observability. A shared memory store lets agents coordinate without the hub, which is a real pattern, but it is a blackboard architecture rather than hub and spoke, and it removes the hub as routing authority. An event queue makes the subscriber autonomous, so the hub no longer decides. Chaining results from one specialist to the next describes a pipeline, where the hub cannot inject or filter between stages.
The same principle covers two situations that feel like exceptions. When a specialist finds something interesting but out of scope, it reports the finding and lets the coordinator decide whether to pursue it, rather than expanding its own brief. And when a specialist decides its sub-question really needs splitting further, it says so, and the coordinator restructures. Nested spawning is technically possible; the constraint is architectural. Either behaviour, done unilaterally, silently removes the coordinator's ability to manage coverage and duplication.
Two roles get conflated here worth separating. A hub decomposes and delegates: what to do, in what order. A supervisor governs: are the agents performing acceptably, are policies holding, when should a human be pulled in. Some architectures fuse them, others separate them, but the focus differs. Task execution is not quality governance.
Isolation is the default, and it surprises people
A spawned specialist starts with exactly what the coordinator put in its prompt. Not the coordinator's system prompt. Not the conversation so far. Not what a sibling specialist found. Not a shared scratchpad. Each invocation is a fresh call with its own context, and if the coordinator calls the same specialist twice, the second call knows nothing about the first.
This follows directly from statelessness in task 1.1, and yet it is the single most common wrong assumption in this domain. There is no inheritance flag and no shared-context parameter. The diagnostic signal is unmistakable once you know it: a specialist reports that it cannot proceed because no findings were provided, while the coordinator's transcript plainly shows those findings existing. The coordinator had them. It did not pass them.
There is no such history to read. Each specialist was a separate stateless call whose context evaporated when it returned. If synthesis needs the search results, the coordinator puts them in the synthesis prompt. Reading from a file works mechanically but takes the coordinator out of the information flow, which is the property the topology exists to protect.
Because isolation is total, everything the specialist needs must be restated: the subtask, the goal, relevant findings from earlier stages, the output format, and the behavioural constraints. If the coordinator's system prompt says every claim needs a citation, that requirement must appear in the synthesis prompt too, or synthesis will not know about it. Instructions do not propagate. The prompt boundary is the whole world.
How you pass findings matters as much as whether you pass them. Concatenating excerpts, URLs, and page numbers into one text blob invites the receiving agent to attach the wrong source to the wrong claim. Keep each excerpt bound to its metadata as structured data. Task 1.3 goes into the contract in detail; here it is enough to know that shape is load-bearing.
A specialist knows only what its prompt says. Nothing is inherited.
The failure that looks like everyone else's fault
Here is the pattern to learn cold. A research system produces a report. Every claim is cited. Nothing contradicts anything. The writing is thorough and confident. And the report covers solar and wind while never mentioning geothermal, tidal, biomass, or fusion.
Nobody downstream failed. The search specialists searched what they were told to search and did it well. Synthesis synthesised what it was given. The gap was introduced at the very first step, when the coordinator decided what the subtasks were. Everything after that faithfully executed a brief that was already incomplete.
Exam scenarios signal this deliberately. When a stem tells you that each subagent completed its assigned subtask flawlessly, it is clearing the downstream agents so that only one candidate remains. The tell is the combination of internal consistency with structural incompleteness: consistent because each part was done well, incomplete because whole categories were never assigned to anyone.
- Broadening the search queries does not help. More results within "solar energy" will never surface geothermal.
- Blaming synthesis does not help. Synthesis can only connect findings it received; there is no data about music or fusion to flag.
- Adding more specialists does not help by itself. If the decomposition is narrow, the new specialists get equally narrow briefs.
The fix is at the coordinator: partition the topic across its actual breadth, and then close the loop. After the first synthesis, evaluate the combined findings for coverage gaps and re-delegate targeted follow-ups for what is missing. That refinement loop is quality-driven rather than scheduled: it fires because a gap was detected, not because it is the third round. It runs until coverage is sufficient or an iteration ceiling is reached.
A subtler variant is worth its own paragraph, because it survives a perfectly broad decomposition. If you partition along one dimension only, anything that spans the partitions is structurally invisible. Split a biography by career period and no agent owns the lifelong arc of the subject's political philosophy. Split a supply chain by component and no agent traces how one fabrication plant closing ripples across memory, packaging, and policy at once. Every partition was covered. The theme that crosses them was not, because it belonged to nobody.
This is exactly backwards. Tighter boundaries make cross-cutting themes harder to catch, not easier, because they shrink the chance that any single agent stumbles across the connection. The problem is the breadth and dimensionality of the decomposition, not its precision. Either assign an agent to the cross-cutting theme explicitly, or run a coordinator integration pass over the combined findings that looks for interdependencies and re-delegates.
Five patterns, and the two that get confused
Anthropic's taxonomy names five arrangements. They are not a hierarchy, and the exam does not ask which is best; it describes a situation and asks which fits.
| Pattern | Shape | Fits when |
|---|---|---|
| Prompt chaining | Fixed sequential steps, each output feeding the next | The steps are always the same and always in the same order |
| Routing | Classify the input, send it down one specialised path | Inputs fall into distinct kinds needing different handling |
| Parallelisation | Concurrent calls: sectioning splits the work, voting repeats it | Subtasks are independent and known before the input arrives |
| Orchestrator-workers | A model decomposes at runtime, then dispatches | The subtasks vary per input and cannot be enumerated ahead |
| Evaluator-optimiser | Generate, score against criteria, loop until it passes | Quality is checkable and first drafts are reliably improvable |
The pair that gets confused is parallelisation with sectioning versus orchestrator-workers. Both fan work out to concurrent specialists, and drawn on a whiteboard they are the same picture. One question separates them: are the subtasks known before the input arrives?
An airline safety review that always examines the same four dimensions has fixed subtasks. Deterministic code can dispatch them, no model call needed to work out what they are. That is sectioning. A code migration across a repository where which files need changing depends entirely on what the repository contains has variable subtasks that only exist once you look. That is orchestrator-workers. Using an orchestrator for a fixed decomposition means paying a model call on every single request to rediscover the same four dimensions, with the added risk that it rediscovers them slightly differently each time.
Voting deserves a clarification because it looks like sectioning. Voting runs the same task several times with different framings and aggregates the verdicts, which is how you drive down false negatives on something like a security review of one diff. Sectioning runs different tasks. Same input, many perspectives, is voting. Different inputs, one perspective each, is sectioning.
One more shape sits alongside these. When stages genuinely depend on each other, collection feeding analysis feeding a report, that is pipeline orchestration and the stages cannot be concurrent. Attempting to parallelise them is not an optimisation, it is a correctness bug: analysis cannot begin before collection returns.
How to write a brief a specialist can actually use
There are two ways to instruct a specialist and they produce noticeably different agents. Procedural instructions enumerate steps: search for this phrase, filter by that field, return the top ten. Goal-based instructions state the objective and the criteria that make the output good: research this topic, and you are judged on coverage breadth, source diversity, and recency.
Procedural briefs turn a reasoning system into a script runner. When the prescribed query returns nothing, a script-running specialist reports insufficient results rather than trying a different phrasing. When a topic emerges that the prescribed queries were too specific to catch, it goes unnoticed. When a strict priority list excludes a tangential but relevant source, it stays excluded. Each of those failures looks like a separate bug, and teams patch them one at a time with fallback directives, which is treating symptoms.
Goal-based briefs let the specialist adapt: try alternatives, follow an emerging thread, surface something adjacent and flag it. But the swing can go too far. Bare goals with no criteria give the specialist nothing to evaluate itself against, so it stops too early or goes too deep and you cannot tell which. Goals plus explicit quality criteria is the shape that works.
Scope partitioning is the other half of writing good briefs. Assign distinct, non-overlapping territory: one specialist on academic sources, another on news, rather than both on the same topic. Overlap wastes retrieval cost and produces near-duplicate findings that synthesis then has to reconcile. Deduplicating afterwards treats the symptom, since the cost is already spent. Running the specialists sequentially so the second avoids the first's ground does eliminate the duplication, but it throws away the parallelism that motivated fan-out in the first place. A shared scratchpad for dynamic avoidance reintroduces exactly the inter-agent coupling the topology exists to prevent.
Parallelism itself has a structural requirement that catches people. Concurrency comes from emitting several delegation calls in a single coordinator response. Issue them across separate turns and the runtime has no choice but to run them one after another, because each turn's call is only visible after the previous turn resolved. No amount of prompt wording about doing things at the same time changes that: the emission shape determines the execution order. This is the same fact from task 1.1 about parallel tool blocks, wearing different clothes.
Concurrency is not a behaviour you can request. If the coordinator emits one delegation, waits for the result, then emits the next, the wall-clock cost is the sum of every specialist rather than the slowest one, regardless of what the system prompt asks for. Fix the emission pattern. Building an external orchestration layer that spawns parallel coordinator-specialist pairs also works, and duplicates the coordinator for no benefit.
Silent failures, conflicts, and partial results
The most dangerous thing a specialist can do is succeed emptily. A search times out, and the specialist returns a success status with an empty result list. The coordinator reads that as no data found on this topic, passes the gap forward, and synthesis writes a confident report whose confidence is entirely unearned. Nobody in the chain knows anything went wrong, because nothing said so.
The reason this is worse than a loud failure is that a coordinator can only make recovery decisions about failures it knows occurred. So the fix is typed error reporting: a status that distinguishes failure from emptiness, an error kind, the query that was attempted, any partial hits, whether it is retryable, and how long to wait. Now the coordinator can retry, annotate, escalate, or accept. What it must not do is have the specialist execute its own recommended follow-up, which quietly promotes the specialist to coordinator.
Empty can be legitimate. A topic that genuinely has no coverage returns nothing, and so does a search that fell over. Only the specialist knows which happened, so only the specialist can report it. Raising the timeout does not help either: the timeout still happens eventually and the result is still masked.
When specialists disagree, reconciliation belongs to the coordinator. Two agents return different dates for the same event, or a quantitative agent and a qualitative agent reach opposite conclusions. The coordinator weighs source credibility, cross-references, decides, or flags for human review. Specialists do not negotiate with each other, because they cannot see each other. And the resolution rules that feel efficient are all wrong: first response wins correlates timing with accuracy, which is nonsense; discarding conflicting findings destroys valid data to manufacture consensus; escalating every conflict to a person does not scale.
For systems where contradictions are expected rather than exceptional, insert an explicit reconciliation stage before synthesis. Compare the evidence, mark what remains unresolved, trigger targeted follow-up on material disagreements, and only then synthesise from reconciled input. Trusting synthesis to notice contradictions on its own does not work: it has no mechanism to know that two claims conflict unless someone flagged them. Asking each specialist for a numeric confidence score and averaging is worse, because model-generated confidence is poorly calibrated, and averaging turns unsupported certainty into a decision rule that looks quantitative.
Partial results deserve a middle path. If three of five source categories came back, do not block synthesis until everything succeeds, because some sources may be permanently unavailable and you would discard good work. Do not synthesise silently either. Produce output with explicit coverage annotations: this is well-supported, this area has a gap because those sources were unavailable. The coordinator then decides whether the gap is material enough to retry, and a retry can target the two that failed rather than re-running all five.
What breaks as the hub gets busy
The coordinator's strength is also its scaling limit. Everything flows through it, so under load it processes sequentially, and every specialist result lands in its context. After eight or ten substantial specialist reports the hub can be carrying well over a hundred thousand tokens, at which point the earlier reports are competing for attention with the later ones and synthesis quality visibly degrades. It is also a single point of failure: if it dies, the run dies.
The architectural responses are to keep the hub light, replicate it behind load balancing, and distribute governance hierarchically. Keeping it light is the one most often missed: a hub that performs deep analysis itself, rather than routing and curating, becomes the bottleneck by doing the work it was supposed to delegate.
For accumulation specifically, process each specialist report into a structured summary before it enters the hub's context, and for large sets, group findings into thematic clusters, synthesise each cluster, then synthesise the cluster summaries. Reducing the specialist count to limit accumulation trades away the breadth you built the system for. A bigger context window does not fix it either, because the problem is attention distribution rather than capacity, which is exactly the effect task 1.6 addresses head-on.
Handoff size follows the same logic. Small outputs, summaries, key findings, structured records, go straight into the next prompt. Large outputs, full extractions or tens of thousands of tokens of raw content, should be deposited somewhere and passed by reference, with the specialist summarising before returning. Passing everything by value breaks at scale; passing everything by reference adds a round trip to data that would have fitted comfortably in a prompt.
One economy is easy to miss. If the coordinator already holds the findings and the user asks for a summary, the coordinator should just write it. Spawning a synthesis specialist means initialising a context, transferring the findings, and paying a round trip to process data that was already in hand. Caching the spawn or pre-generating summaries optimises a step that should not exist.
Finally, if the system runs many briefs at once, each brief needs its own isolated state partition, enforced at the storage layer. Key-naming conventions carrying a task identifier are advisory, and one mistyped key mixes a renewable-energy brief with an electric-vehicle one. Cross-contamination between concurrent runs is close to undebuggable from the output.
Most multi-agent problems are coordinator problems: what it asked for, what it passed on, and how much it kept.
Subagent Invocation and Context Passing
The configuration gate that makes delegation possible, and the handoff contract that decides whether the result is usable.
Task 1.2 argued that the coordinator owns everything. This task is about the two places where that ownership is exercised concretely: the configuration that lets a coordinator spawn at all, and the payload it writes into each spawn.
Both have a failure mode that is invisible from the output. If the coordinator lacks the delegation tool, it does not error; it quietly does the work itself and returns something shallow. If the coordinator passes findings as prose instead of structured records, synthesis does not error either; it returns a report whose claims have lost their sources. In both cases the logs look calm and the answer looks finished.
There is also a trio of mechanisms here that get mixed up constantly: spawning a fresh subagent, forking, and resuming. They have different starting contexts and answer different questions, and the exam tests the distinction directly.
The gate: no tool, no delegation
A coordinator can only spawn subagents if the delegation tool appears in its own allowedTools. In the SDK the tool is Agent, renamed from Task, with Task retained as a working alias. This is a hard configuration gate, not a preference. When it is absent, the coordinator cannot emit a delegation call at all, no matter how carefully its system prompt describes the multi-agent architecture you intended.
What makes this worth its own section is the failure signature. The coordinator does not crash and does not complain. It reasons about delegation in text, sometimes describing the subagents it would like to use, and then it does the work itself with whatever tools it does have. The output is thin because a component designed to be an orchestrator is now a soloist. The tell in the logs is stark: zero delegation calls, ever.
A description tells the coordinator when a given subagent is the right one to invoke, so a vague description makes routing unreliable. It does not prevent invocation, because the coordinator can still call a subagent by name. If the logs show zero spawn attempts, the description is not the cause; the missing tool is. Conversely, when the tool is present but a definition is missing or malformed, you see attempted calls that fail. Different symptom, different fix.
One related decision belongs here. The coordinator's own tool set should lean towards orchestration rather than domain work. A coordinator holding search, file reading, and database access can always choose to do the job itself, and under pressure it often will, producing exactly the shallow single-agent output the architecture was meant to avoid. A little domain access for orchestration purposes, reading a config file to decide which specialists to spawn, is fine. Broad domain access invites the coordinator to bypass its own design.
Zero delegation attempts is a configuration bug. Failed delegation attempts are a definition bug.
What a subagent definition actually contains
Three fields do the work, and each has a different consumer. The description is read by the coordinator to decide when this specialist applies, so it is routing input. The system prompt is read by the specialist during execution, so it governs behaviour. The tool list is read by the runtime, so it sets a hard boundary on what the specialist can do.
That third field is worth dwelling on. A specialist can only invoke tools in its own list, and no instruction from the coordinator changes that. Telling a synthesis specialist to search the web when search is not in its list does not produce an error; it produces nothing, because the tool does not exist in that specialist's action space at all.
Scope the list to the role. Search specialists get search tools. Analysis specialists get file reading. Synthesis specialists get whatever they need to produce output. There are two independent reasons, and the second is the one people forget. The first is least privilege: a narrow list bounds what a specialist can do when it misbehaves. The second is reliability: tool selection degrades as the list grows, so a specialist choosing among three tools is measurably more accurate than one choosing among twenty.
Permissions flow outward rather than downward. A specialist's list is defined at spawn time and does not have to be a subset of the coordinator's, so a specialist can hold a database tool the coordinator lacks. What it cannot do is acquire something its own definition omits. There is one documented exception in the other direction: when the coordinator runs with permission bypassing enabled, that mode propagates to every specialist it spawns and cannot be re-tightened at the specialist level.
This looks like resilience and is actually role collapse. Synthesis now searches, which duplicates the search specialist's work, returns different sources, and produces citations that disagree with the ones already gathered. If synthesis genuinely needs to check one fact, give it a narrowly scoped verification tool rather than the general search capability.
Definitions do not have to be static. The SDK supports creating them at runtime, so a coordinator can shape its roster around the problem it has just analysed. That flexibility comes with lifecycle management you now own, and most systems do fine with a fixed roster.
The handoff contract
The prompt string is the only channel into a specialist. Not the coordinator's history, not a sibling's output, not a shared pool. So everything the specialist needs has to be written into it: the subtask, the goal, the relevant prior findings, the output shape, and any behavioural constraint that matters.
The classic version of getting this wrong is a coordinator that says find more on this topic, having discussed the topic at length in its own context. The specialist has no idea what topic. It says so, and from the outside it looks like the specialist failed.
The subtler and more damaging version is passing the right information in the wrong shape. A search specialist returns well-sourced findings with URLs and titles. A document specialist returns page-referenced analysis. Both are working correctly. The coordinator compresses both into narrative paragraphs and hands them to synthesis, and the final report contains confident claims with no attribution, because synthesis was never given anything to attribute them to.
// The contract that survives handoff. Drop a field here and no downstream
// prompt can recover it, because the subagent's context is already gone.
interface Finding {
claim: string
source_url: string
document_name?: string
page_number?: number
confidence: "high" | "medium" | "low"
retrieved_by: string
retrieved_at: string
}
interface SubagentReport {
subtask_id: string
status: "complete" | "partial" | "blocked"
findings: Finding[]
gaps: string[]
// Not the transcript. The coordinator pays for every token of this.
summary: string
}Keep each claim bound to its own metadata. A date matters more than it first appears: two specialists reporting different growth figures for the same market are contradicting each other only if they measured the same period. With dates attached, twenty-five percent in one year and forty percent two years later is a trend. With dates stripped, synthesis has to guess, and it guesses wrong in a way that is very hard to spot, attaching a recent figure to an older source.
An agent cannot cite what it never received. If the coordinator passed prose, there are no URLs, document names, or page numbers in the specialist's context, and the instruction is inert. The same applies to telling it to double-check dates, or adding a post-processing step to highlight them: you cannot verify or highlight metadata that was discarded upstream. Appending a generic sources consulted list is no better, because it maps nothing to anything: a reader still cannot trace one specific figure to one specific document.
There is an opposite error worth naming, because overcorrecting is common. Forwarding everything, raw pages and full reasoning chains, does preserve attribution and does overflow the receiving context. The target is selective passing: the material the downstream specialist needs, plus a structured index mapping claims to sources. Too little produces incomplete output; too much wastes budget and dilutes focus on what matters.
Design the return shape at dispatch, not afterwards. Ask for a compact structured report and you can collect from six specialists comfortably. Let each specialist choose its own format and you will occasionally receive a full transcript, at which point the coordinator's context is already blown and compressing after the fact is too late. For genuinely large payloads, have the specialist summarise before returning, or deposit the material in a shared store and pass a reference.
That shared-store option is more than a size workaround. Message passing records events; shared state records current truth. A knowledge bus, an MCP-backed memory server, a database, or a vector index lets each specialist index findings and lets downstream specialists retrieve only what is relevant, which keeps context bounded as pipeline stages accumulate and survives a crash mid-run. It also introduces write conflicts, so pick a resolution policy deliberately: last write wins for status flags, optimistic locking for records that must not be clobbered, append-only for finding lists and audit logs, coordinator-mediated for low throughput.
Structure survives handoff. Prose does not, and no downstream prompt can rebuild it.
Parallel, sequential, and what the coordinator gets back
Concurrency comes from emitting several delegation calls in one coordinator response. The runtime schedules them together, each in its own isolated context, and the batch costs the slowest specialist rather than the sum. Four specialists at forty-five seconds each cost forty-five seconds in parallel and three minutes sequentially, and the only difference is whether the calls left in one turn or four.
There is no parallel variant of the delegation tool, and no prompt phrasing that produces concurrency. If the calls go out across separate turns, they run in sequence, because each turn's call only exists after the previous turn resolved.
Sequential is correct when there is a real dependency. A writing specialist that needs research findings cannot start before the research specialist returns; spawn it in parallel and it produces generic content built on nothing. The test is mechanical: if the prompt refers to findings from the previous step, the dependency is real. If the prompt is self-contained, it is not.
The coordinator waits for the whole batch. Two specialists finishing in two seconds while two others take forty-five means the coordinator continues at forty-five, not at two. There is no partial-result streaming and no default timeout that cuts the wait short, so a hanging specialist hangs the batch unless you configured a timeout yourself.
What comes back is only the final message. The specialist's reasoning, its intermediate tool calls, and everything it discovered along the way are gone. This is a deliberate trade: it is what keeps the coordinator's context clean, and it is why debugging a bad specialist result means re-running rather than inspecting. It is also why a specialist must report failure as a typed error rather than as an empty success, since the final message is the coordinator's only evidence about what happened.
Two lifecycle hooks make the boundaries observable, with details that matter for correctness. The start hook fires once per invocation, not once per named specialist, so three spawns of the same definition give three events, which is what lets you correlate and time each one. The stop hook fires only on normal completion. A specialist that fails or is cancelled raises an error event instead, so a coordinator listening only for stop will wait for a result that is never coming.
Three documented ceilings sit around all of this. Concurrency defaults to twenty running specialists, past which a spawn is refused with a message saying the limit was reached. Nesting depth defaults to three layers below the main agent, and setting it to one prevents specialists from spawning at all. Spend is unbounded unless you set a budget, at which point exceeding it refuses new spawns, stops background ones, and ends the run with a budget error subtype.
That depth setting resolves a genuine confusion. Ordinary specialists can nest, up to the configured depth. But nesting is not free: each level is another fresh context and another handoff where information can be dropped, so a four-level chain is mostly an exercise in losing detail. Architecturally, a specialist that decides its sub-question needs splitting should report that to the coordinator, which restructures. Keep the tree flat by preference, not because the runtime forbids depth.
Fresh spawn, fork, resume
Three mechanisms, three starting contexts. Getting them straight is worth real marks, because the exam frames them as near-identical options and only the starting context distinguishes them.
| Fresh spawn | Fork | Resume | |
|---|---|---|---|
| Starting context | Only the prompt you wrote | The parent's full history, system prompt, tools, and model at the fork point | The saved transcript of one specific session |
| Relationship | Independent sibling | A branch that diverges after the fork point | Continuation of the same line |
| Sees siblings | No | No, once branched | Not applicable |
| Typical cue | "independent tasks, reduce latency" | "explore alternatives without losing context" | "continue the same investigation" |
A fork is the mechanism for divergent exploration from a shared baseline. You have analysed a codebase, discussed constraints, read the relevant files, and now you want to try two architectures without re-establishing any of it. Fork twice. Each branch has the whole analysis and develops its own conclusion, neither sees the other, and only the final result returns to the main conversation so the parent's context stays clean. The first request in a fork shares the parent's prompt cache, which makes it cheaper than a fresh spawn carrying equivalent context. Forks fan out flat: a fork cannot fork again.
Two clarifications that appear as distractors. Isolation for risky edits comes from running the fork in a separate worktree, not from forking itself, and there is no automatic merge; you compare the branches and apply what you want. And there is no standalone command-line flag that launches a fork as a separate top-level session, which is a phrasing worth recognising as wrong.
A tempting answer in migration scenarios: the coordinator established naming and conversion decisions early, several specialists must apply them consistently, so fork to give each one the context. It backfires, because forks are built to diverge. Each branch reasons independently from the same baseline and can reach a slightly different reading of the same decision, which is precisely the inconsistency you were trying to prevent. Consistency comes from writing the decisions explicitly into each specialist's prompt.
Resume is the linear counterpart. It re-enters one named session and continues appending to it. Two limits matter in practice. It restores conversation history from the saved transcript, not runtime state, so variables, in-memory objects, and knowledge of which writes already landed do not come back. And transcripts are stored under a path derived from the working directory, so resuming from a different directory silently fails to find the session and starts fresh. Task 1.7 works through the consequences.
Specialists can be resumed too. A completed specialist's result carries an identifier; capture it along with the session, pass the session on the next call, and reference the identifier in the prompt. The built-in exploration and planning agents are one-shot and return no such identifier.
When not to delegate
Delegation is a tool, not a default, and this task's material includes several scenarios where the right answer is for the coordinator to just do the thing.
The deciding question is context impact. If the work would flood the coordinator's context, a hundred thousand tokens of logs, a directory of files, delegate it: the specialist absorbs the bulk in its own context and returns a summary. If the work is small, sequential, and depends on state the coordinator already holds, do it directly. Spawning a specialist to summarise findings the coordinator is already holding pays initialisation, transfer, and a round trip to process data that was in hand, and no amount of caching that spawn makes an unnecessary step necessary.
Approval gates belong in the coordinator for the same reason. A specialist operating in an isolated context does not have the whole picture, so it cannot present a change for approval with the reasoning that makes the decision informed. The coordinator does.
Finally, when parallel results come back, assembly is the coordinator's job: deduplicate overlaps, reconcile conflicts, order the material, and synthesise. Specialists cannot deduplicate against each other because they cannot see each other. Only the hub holds every result at once, which is the same fact this whole domain keeps circling.
Workflow Enforcement and Handoff
Where a prompt stops being enough and code has to take over, and what a human needs to receive when the agent gives up.
Two halves, joined by one idea. The first half is enforcement: some steps must happen, in order, every time, and a system prompt cannot promise that. The second half is handoff: when the agent cannot or should not proceed, what it passes to a person determines whether the escalation is useful or just a shrug.
The joining idea is the one from task 1.1, applied to business rules rather than loop control. A prompt is probabilistic. Code is deterministic. Everything in the first half of this chapter is an application of that sentence, and the skill being tested is knowing when the difference matters enough to pay for.
It matters less often than an anxious engineer assumes. Turning every soft preference into a hook produces a system nobody can change. The chapter is as much about proportionality as about enforcement.
Ninety-five percent is a failing grade for some rules
Put a sentence in the system prompt telling the agent to verify a customer's identity before issuing any refund. The model reads it, understands it, and complies most of the time. Across many runs that lands somewhere in the low nineties, and the few percent that slip through are not random noise: they cluster in the unusual conversations, the persistent customer, the multi-turn dialogue that drifted, the phrasing nobody tested. Exactly the cases where the rule was protecting you.
Now put the same rule in code at the tool boundary. The refund tool consults session state, finds no successful verification for this session, and returns a structured error saying so. The refund never executes. No phrasing of the conversation, no persistence, no edge case reaches the executor, because the call is stopped before it gets there. The enforcement does not live in the model's reasoning; it lives outside the loop where the model decides anything.
// A gate is code that runs whether or not the model agrees with it.
async function preToolUse(call: ToolUseBlock, state: RunState) {
if (call.name === "compute_premium" && !state.watchlistCleared) {
return {
decision: "deny" as const,
reason: "Watchlist screening has not returned cleared for this applicant.",
}
}
if (call.name === "adjust_balance") {
const check = validateAdjustment(call.input)
if (!check.ok) return { decision: "deny" as const, reason: check.reason }
await auditLedger.append({
actor: state.runId,
operation: "adjust_balance",
input: call.input,
at: new Date().toISOString(),
})
}
return { decision: "allow" as const }
}The exam signals which side of the line you are on with fairly consistent phrasing. A rule that cannot be left to model discretion, leadership requiring zero tolerance, a single bypass constituting a security breach, financial or regulatory consequence: any of these means the answer is code. The decision does not depend on how good the current prompt is or how small the observed failure rate has become, because the argument was never about the average case.
The rule genuinely flips when stakes drop. Whether responses use bullets or numbered lists, whether tone is formal, whether sections appear in a preferred order: an occasional miss costs nothing, and a hook costs engineering, review, testing, and a new place for the system to break. Prompt guidance is the correct and proportionate choice there, and choosing a hook is over-engineering rather than rigour.
Must hold every time means code. Preferred with occasional deviation acceptable means prompt.
Four shapes of enforcement
Enforcement is not one mechanism. Which one you reach for depends on the shape of the requirement, and the exam tests that mapping precisely.
| Requirement | Mechanism | Why this one |
|---|---|---|
| B must not run until A succeeded | Prerequisite gate on B | Unconditional ordering. Read session state, block with a structured error naming the missing step. |
| Block only when an argument crosses a limit | Pre-execution hook reading the argument | The decision depends on a parameter value, so the check must see the value. |
| A side effect must follow every success | Post-execution hook | The effect must happen after the action succeeded, which a pre-execution hook cannot observe. |
| Action needs N prior occurrences of something | Stateful wrapper counting occurrences | The rule is about history, so something must hold a count across turns. |
A prerequisite gate is recoverable rather than terminal, which is easy to miss. Blocking a call is a redirect: the model receives the structured error, learns which step it skipped, calls that step, and retries successfully. So the error text is functional, not decorative. Verify identity first, calling get_customer, is actionable. Permission denied is not.
A gate is only as good as the state it reads, and this is a real failure mode rather than a hypothetical one. Verification succeeds early in the conversation, and six steps later the refund is refused because the flag lived in a transient object that reset. Persist the verified state durably, scoped to the session or customer so it neither vanishes mid-conversation nor leaks across users. A flag that never clears over-blocks, which is safer than the alternative for a financial rule and still a bug.
The strongest version of a prerequisite converts it into a type requirement. The verification tool emits a session-scoped token, and the sensitive tools take that token as a mandatory parameter. Only a successful verification produces one, so a call without it is rejected by the service layer rather than by a check you remembered to write. Scope and expiry matter, or the token becomes a stale pass reusable across sessions.
Post-execution hooks cover guaranteed side effects. An audit record after every successful refund belongs in a hook that fires on success, not in a separate audit tool the model is instructed to call, because the latter is back to depending on the model remembering. And a pre-execution hook cannot do this job at all: at that point the action has not succeeded and there may be nothing to record.
Multi-step business logic needs the counting variant. A retention offer that must be presented and declined twice before cancellation is permitted cannot be enforced by a script in the prompt; the model may skip an offer or accept the first refusal. Wrap the cancellation tool in a guard that tracks how many offers were made and declined, and return a structured error naming the next required step when the criteria are unmet.
For destructive actions, the same idea appears as an explicit confirmation gate: a dedicated confirmation tool takes the proposed action, surfaces it to the user, and returns a decision, and the destructive tool runs only on approval. A prompt saying always confirm before cancelling reads as sufficient and is not; there are documented cases of a model deleting a record without asking despite exactly that instruction.
When two independent rules exist, each gets its own enforcement point on the tool it guards. An anti-money-laundering prerequisite hooks the transfer tool; a refund cap hooks the refund tool. One hook could technically evaluate both, and separating them keeps each rule readable, testable, and independently changeable.
Soft signalling versus hard enforcement
This distinction is subtle, heavily tested, and easy to get wrong because both options involve code.
A tool that returns an error saying the amount exceeds the policy limit, please escalate, has communicated the policy. The model receives that string and decides what to do with it. It might escalate. It might also retry with a smaller amount, split the refund across two calls, pick a different tool, or reinterpret the message. The policy was expressed; it was not enforced.
A hook that intercepts the call, blocks it, and actively invokes the escalation workflow has enforced the policy. The difference is whether the violation remains reachable after the model has seen the response. A tool error is a fine second layer behind a hook. It is never the only control for something that must not happen.
Forcing a specific tool applies to that one API turn. It does not bind later turns and does not stop a different tool from being called first on a subsequent turn, so it cannot carry an ordering guarantee through to an irreversible action. It is a reasonable way to ensure a first action and no substitute for a prerequisite gate.
Two more mechanisms get proposed and neither does what it appears to. The order of entries in an allowed-tools list does not influence the order the model considers calling them; that behaviour does not exist. And a routing classifier operates at the request level, deciding which agent or pipeline handles an incoming request. It is the right answer when the problem is that the wrong agent took the case. It does nothing about the right agent skipping a step inside itself, which is what refund-ordering scenarios describe.
Decomposing the work into separate prompt-chained API calls, each holding only the tools for its stage, does enforce ordering, and it is disproportionate for a single ordering bug inside one agent. You inherit manual state passing, lost shared context, and more moving parts. If the task is genuinely a multi-stage pipeline with isolated context needs, chaining is reasonable on its own merits.
Two structural notes about capability. A generic tool such as one that executes arbitrary database queries is a scope and security problem regardless of what the prompt says about it: it reaches data far outside any single interaction and bypasses purpose-built validation. And when a specialist holds a tool it should never use, a status-lookup specialist that can also issue refunds, the fix is to remove the tool from its definition. Instructing it not to use a capability leaves the capability present and the compliance probabilistic.
Enforcement and adaptivity are orthogonal, which resolves a tension that scenarios like to construct. An insurance workflow where the needed lookup depends on what the file actually contains should let the model drive its own tool sequence. A fraud-watchlist clearance that must block premium computation should be a gate. Keep both. The wrong answers are to freeze the order and add examples for the exceptions, to grow a decision tree, or to remove the gate so the model can judge when screening is warranted.
One refinement for safety-critical gates. Proving that a prior tool ran is weaker than proving its result was valid. A collision-avoidance gate should require both a current authorisation and non-null risk evidence for every tracked contact, and deny when any contact returns missing data, naming what is unresolved. Missing data is not an implicit clear. Fail closed.
If the model can still cause the violation after reading your response, you signalled rather than enforced.
Three escalation triggers, and the proxies that look like them
The other half of this task. There are exactly three reliable reasons for an agent to hand a case to a human, and knowing the list matters because the alternatives are all plausible and all wrong.
- The customer explicitly asks for a human.
- The request needs a policy exception, or falls into a gap the policy does not address, so it exceeds the agent's authority.
- The agent cannot make meaningful progress: a genuine capability limit, an unresolvable data conflict, a tool that will not work.
Everything else is a proxy. Sentiment, turn count, self-reported confidence, punctuation, the case merely feeling complex or unusual: each is a measurable signal that correlates weakly with whether a person is needed, and each degrades the system in a different direction.
Explicit means explicit. Connect me to a real person now triggers immediate escalation, before investigation, however simple or clearly within policy the underlying issue is. The agent should pass its analysis along as context, and it must not stall, negotiate, or quietly keep investigating while the customer waits.
Frustration without a request for a human is not a trigger. This is ridiculous, nothing ever works, is rhetorical: acknowledge it, resolve the issue if it is within authority, and escalate only if the customer then asks for a person. Escalating on sentiment collapses first-contact resolution and, in the scenarios, is precisely what an agent stuck at fifty-five percent resolution against an eighty percent target is doing. The genuinely hard middle case is something like I just want to speak to someone who can actually help, which reads as frustration rather than a direct demand.
The policy trigger has two faces and the distinction is worth memorising. When the policy is silent, neither permitting nor prohibiting what was asked, the silence is itself the trigger: the agent has no authority to interpret or invent policy, so it escalates with a handoff that names the gap. It must not reason by analogy from a nearby clause, must not deny outright and foreclose a decision the business might want to make, and must not improvise a compromise.
When the policy enumerates its exceptions and the customer's reason matches none of them, deny cleanly and explain which conditions would have qualified. There is no ambiguity for a human to resolve, so escalating is wrong. Waivers only for documented medical emergency or military deployment is enumerated and exhaustive. Waivers for exceptional circumstances at manager discretion is not, and that one does escalate.
Failure classification belongs here too, because it decides between retry, explanation, and escalation. A timeout is transient: retry with backoff, in the background, without making the customer wait. A policy-limit breach is a business outcome: non-retryable, explain it. An authorisation gap is a permission problem: a different path, usually escalation. Treating all three the same way, apologise and retry, retries what cannot succeed and escalates what could have been answered.
Miscalibration is the interesting case, because it is the one place in this chapter where a prompt is the right fix. An agent that escalates routine cases and attempts the policy exceptions it should escalate has unclear decision boundaries, not an architectural defect. Write explicit criteria into the system prompt with contrasting examples, especially at the boundary. Confidence-score routing, sentiment analysis, and a separate ticket classifier all add machinery without supplying the missing criteria. This is judgement about when to escalate, which is a different thing from a compliance rule that must hold.
What the human actually receives
Assume the person receiving the escalation cannot see the conversation. That assumption is usually true, and it makes the payload the only information they get. So it has to be self-contained.
- The customer or account identifier, so they can pull the record without asking.
- A summary of what was asked and what the agent attempted.
- The agent's diagnosis of the root cause.
- The amount in dispute, where money is involved.
- A recommended action.
Every field is load-bearing. Drop the identifier and the human starts by hunting for the account. Drop the attempt summary and they repeat work the agent already did. Drop the diagnosis and the agent's investigation is thrown away. Any omission tends to end with the customer being contacted again to supply something the system already knew.
Four wrong shapes recur, each failing differently. A raw transcript moves the synthesis burden onto the human. The original message alone discards everything the agent learned. A bare flag saying escalated, please help, conveys nothing. A confidence score substitutes a number for the facts of the case. Writing the payload to a database and passing a reference identifier is good for audit and must not replace the direct payload, because now the human has a lookup to perform before they can start.
Two handling patterns round this out. When a bundled message raises several concerns at once, a return plus a billing dispute plus an address change, decompose it into distinct items, investigate them against the shared customer context so the lookups happen once rather than per concern, and answer with one unified resolution. Do not ask the customer to resubmit one concern per message, and do not fan the concerns out to separate agents, which fragments the context and adds latency for nothing. The deterministic version parses the message into an intent list up front, tracks resolution per intent, and gates the final response on all of them being resolved, which turns do address all parts from an instruction into a structural guarantee.
And when a lookup returns several matching records, ask the customer for one more identifier before taking any account-specific action. Every heuristic tiebreak is wrong: most recent order, highest lifetime value, most complete profile, alphabetical. Escalating every ambiguous match is also wrong, since it inflates escalation volume for something a single clarifying question resolves. Disambiguate first, escalate only if clarification fails.
Agent SDK Hooks
The lifecycle points where your code runs inside someone else's loop, and which direction each one can act in.
Task 1.4 argued that some rules belong in code. Hooks are where that code goes when the loop is not yours to edit. The SDK and Claude Code run the loop; hooks are the defined points at which your logic is invited in.
Almost everything here reduces to one question: has the tool run yet? A hook that fires before execution can prevent, pause, or rewrite the request. A hook that fires after execution can rewrite the result, log it, or react to it, and cannot make the effect not have happened. Get the direction right and most of these questions answer themselves; get it wrong and you have built detection where you needed prevention.
The rest is detail that genuinely matters in practice: which decision value to return, how matchers are compared, what an exit code means, and where a hook sits relative to the declarative permission rules around it.
Before or after, and why that decides everything
A pre-execution hook runs in the gap between the model emitting a tool call and the runtime invoking the tool. The decision has been made and the arguments exist, but nothing has happened yet. Your hook receives the tool name, the parsed input, and session metadata, and returns a decision. It can also rewrite the arguments before they execute, or redirect to a different workflow.
A post-execution hook runs after the tool returns and before the result is appended to the conversation. The effect has happened in the world. What you control is what the model is told about it: you can rewrite the payload, normalise it, redact it, filter it, or attach something to it, and you can trigger side effects such as an audit write or a formatter run.
The consequence is causal rather than stylistic. A refund has been issued. Funds have moved. A branch is gone. A pump rate has changed. A post-execution hook can flag the violation, enrich the result, or fire a compensating action, and the external system has already changed state. If the requirement is that something must not happen, only the pre-execution direction satisfies it, no matter how precise the detection logic on the other side is.
const hooks = {
// Runs before the tool. Can deny. This is the only place a hard
// invariant can be guaranteed.
PreToolUse: async ({ tool_name, tool_input }) => {
if (tool_name === "Write" && isProtected(tool_input.path)) {
return { decision: "deny", reason: "Path is release-protected." }
}
return { decision: "allow" }
},
// Runs after the tool. Cannot un-run it. Can rewrite what the model sees.
PostToolUse: async ({ tool_name, tool_output }) => {
if (tool_name === "Read") {
return { updatedToolOutput: redactSecrets(truncate(tool_output, 8000)) }
}
return {}
},
// Fires before compaction so you can pin facts that must not be summarised.
PreCompact: async ({ transcript }) => ({
preserve: extractInvariants(transcript),
}),
// Can block completion. Cannot rewrite the answer.
Stop: async ({ result }) =>
hasUncitedClaims(result)
? { decision: "block", reason: "Every claim needs a source before finishing." }
: { decision: "allow" },
}This is the single most common wrong answer in hook questions, and it is attractive because the detection is genuinely easier to write after the fact: the result is right there, fully formed. But the scenario asked you to prevent a production deletion, an over-limit transfer, or a write to a protected path. Detection after the fact is an incident report, not a control.
The ordering is fixed per call and registration order does not change it. Pre-execution hooks evaluate first; if any denies, the tool never runs, the reason goes back to the model, and the pipeline stops for that call. Because no result exists, the post-execution phase does not fire at all. If the call is allowed, the tool executes, and only then does the post-execution phase receive the result. Parallel hooks on the same event evaluate concurrently, but the phases stay in order.
Prevention runs before. Transformation and reaction run after. Nothing swaps those.
Four decisions, and the precedence between them
A pre-execution hook returns one of four values, and choosing between deny and ask is where the marks are.
| Decision | Effect | Use when |
|---|---|---|
deny | Hard block. Reason returned to the model, evaluation stops. | Policy says this must never proceed. |
ask | Pause and surface to a human. Resumes only on approval. | Policy says this may proceed after review. |
defer | No opinion. Judgement passes to the next control. | This hook does not apply to this call. |
allow | Permit the call through the rest of the pipeline. | This hook affirmatively approves. |
Using deny where the policy meant ask produces a dead end. A refund in an approval band is not forbidden; it needs a manager. Denying it tells the customer their refund cannot be processed, when the correct behaviour was to route it to someone who could approve it. Conversely, returning allow when you meant defer short-circuits the pipeline and skips downstream deny and ask rules that were supposed to run.
The band language in scenarios is the cue. Refunds between two figures needing approval, claims above a threshold needing human review, account deletion needing a manager: those are ask. Flagged orders, production deletions without a ticket reference: those are deny. Either way the response should carry structured context so the model knows what to do next, escalating with the amount and reason rather than receiving a generic failure.
In Claude Code, shell hooks express these decisions through exit codes, with a contract that catches people coming from git. Exit zero means no blocking decision and the flow continues. Exit two is the hard block: the call is denied and whatever the script wrote to standard error is surfaced to the model as the reason. Any other non-zero code, exit one most obviously, is treated as a hook error rather than a policy decision, so the call proceeds anyway. A script that means to block and exits one has silently disabled your enforcement while still looking like it ran.
Structured output follows the same discipline. Return schema-valid JSON with an explicit decision and, when blocking, a reason both the agent and an operator can read. Unstructured debug text where JSON was expected gets rejected. And decide deliberately what happens when the hook itself errors: for a mandatory safety gate, fail closed. Treating hook errors as approval to preserve throughput is a decision to bypass the gate under exactly the conditions where it is least healthy.
Where a hook sits in the permission chain
Hooks are not the only control. Claude Code evaluates a chain, and knowing the order tells you which mechanisms can actually guarantee something.
- Pre-execution hooks. Programmatic, they run before any declarative rule.
- Deny rules. A match blocks the call even under the most permissive mode.
- Ask rules. A match surfaces a human prompt.
- The active permission mode, such as interactive default, edit acceptance, or full bypass.
- Allow rules, evaluated last.
Two conclusions matter. An allow rule cannot guarantee anything, because it sits at the end and a more permissive mode has already had its say. And a guarantee that must hold in every mode, including full bypass, requires a hook or a deny rule, since those are the two controls evaluated before the mode is consulted. Organisation and managed settings outrank project, user, and local scopes, which is how a rule survives a developer's local configuration.
The two compose usefully as defence in depth, because they fail differently. A hook survives someone disabling or rewriting the deny configuration, since it is code running earlier. A deny rule survives someone reconfiguring or breaking the hook, and applies even under bypass. For an advisory invariant one layer is proportionate; for a legal or safety consequence, use both.
In the SDK, the runtime-callback equivalent gives you a per-call gate that can allow, deny, ask, or modify the input, with the advantage of running live code that can consult session state or query a service. Prefer it when the policy is per-session or per-customer. Prefer a deployed hook or a managed deny rule when the policy is fixed and universal, because that is more durable and more auditable than something supplied at every instantiation.
It looks like dynamic permissioning and is not. Editing the list never inspects the current call's arguments or any live state, so it cannot express a threshold or a prerequisite, and it races badly against a turn that emits several tool calls at once. Gate the call, do not shuffle the roster.
Matchers are literal, which bites
Every hook registration carries a matcher selecting which events it handles, and the comparison is exact rather than semantic. For tool hooks the matcher is a literal tool name. A matcher on the write tool never fires for an edit call, even though both modify files, because they are distinct tool names. A hook protecting a path that only matches the write tool is trivially bypassed by an agent that reaches for edit instead.
File-change matchers are literal paths, not globs. A pattern that looks like a source glob never matches, because the field is read as one literal filename. And omitting the matcher entirely fires the hook on every event of that type, which is intentional for a cheap audit log and a latency tax on every turn if the hook does real work.
Correct scoping is a narrow matcher plus an argument condition inside the handler. Match the shell tool, then discriminate the dangerous command shape in code. Test both directions: cases where the hook must fire, and cases where it must not. Matching a friendly display name rather than the actual tool identifier is a quiet way to build a rule that never fires.
Which raises allowlist versus denylist, because scoping and matching are the same problem at a smaller scale. A denylist scanning for dangerous substrings is defeated by aliases, flags, rewrapped commands, and indirection: keyword matching on destructive SQL misses the same statement embedded in a script. An allowlist inverts the default to deny, permitting only the exact approved shape. When the safe forms are few and the dangerous ones are many or hard to enumerate, allowlist. When the dangerous forms are few and well known, a denylist can be acceptable, and a hook that parses command structure beats one matching substrings either way.
Calibration deserves a note because overcorrection is common. A pattern matching any assignment to a variable named like a key will block a comment reminding the reader to load the key from the environment. Require the value to look like an actual secret, a long high-entropy string, rather than matching the name alone. And when a false positive happens, tighten the pattern; deleting the hook trades one nuisance for the entire coverage it provided.
Choosing the boundary and the hook type
A precondition must be checked where it needs to hold, which is usually the terminal action that commits state: the deploy, the merge, the release, the destroy. Check it at an earlier creation step and later steps can add the missing artifact or change the state before the committing call, so you get false negatives and false positives from the same gate. If the invariant must hold at intermediate steps too, add gates there as well, and keep the final one regardless.
Hook types split by the nature of the decision. Command hooks run deterministic shell code; HTTP hooks call a deterministic policy service. Prompt and agent hooks invoke model judgement to evaluate something semantically. Anything expressible as an equality, a comparison, or a pattern over known fields belongs in the deterministic kind, because it is cheap, precise, and reproducible. Using a model hook to check whether a path equals a protected literal pays for nondeterminism you did not need. Genuinely open-ended judgements, whether a change constitutes an architectural regression, need the model kind, along with explicit acceptance that the answer varies.
There is also a threshold-shape mistake worth calling out. A cap on an aggregate is not a cap on one request. If the rule is a net position limit, the gate must fetch the current holding at call time, project the holding plus this order, and compare that against the cap. Checking the order size against a per-order ceiling, or using a snapshot taken at session start, both miss the case where a series of small orders tips an already-loaded position over the line.
What the after-direction is genuinely good at
Framing post-execution hooks as the weaker direction undersells them. There is a class of work only they can do, because it needs a result to exist.
Normalisation is the headline case. Six partner APIs return dates as Unix seconds, ISO strings, day-first and month-first text, and prose. Statuses come back as numeric codes in one and labels in another. Currency arrives as floats, symbol-prefixed strings, and integer cents. A post-execution hook converts all of it into one canonical schema before the model ever reasons over it, using the output-replacement field. Do this in a prompt instead and you are asking the model to hold six format conventions in mind on every call, which it does unevenly and at token cost.
One field detail is tested: the general output-replacement field works for every tool, built-in and MCP alike, while the older MCP-specific field is deprecated and only reaches MCP tools. A shared normalisation hook using the deprecated field silently passes built-in tool results through unnormalised. Adjacent fields that add context or a system message do not replace the tool result the model reasons from.
Centralisation follows from the same reasoning. Per-tool wrappers grow linearly with partners, each needing its own branch and deployment. One schema-driven hook routing every result through shared conversion adds a new source at near-zero marginal cost. A central hook that enumerates each partner by name inside itself has reintroduced the linear cost while looking centralised. When you own few stable tools, fixing the shape at source is cleaner than either.
Reactive side effects are the other family: running a formatter or linter after a file is written, scanning for secrets after modification, writing an audit record after a refund succeeds, appending metadata after an analysis returns. All of these need the file to exist or the result to be available, which is precisely what post-execution timing provides. Running the formatter before the write has nothing to format; blocking an unlinted commit is preventive and belongs before.
Beyond tool calls: subagents, compaction, and completion
Two hooks bracket a delegated specialist rather than a tool call. The start hook fires at spawn, before anything inside the specialist runs, carrying spawn metadata. Use it for logging, for validating that the coordinator actually passed the required context, and for enforcing a spawn budget or rate limit. It fires once per invocation, not once per session, so three spawns of the same definition give three events, which is what makes per-child budgeting and timing possible.
The stop hook fires when the specialist completes and returns, so it can validate the output and block completion. What it cannot do is rewrite the answer. And it fires only on normal completion: failures and cancellations raise an error event instead, so a coordinator listening only for stop waits for a signal that will not arrive.
A compaction hook fires before history is summarised, which is the one chance to pin facts that must not be lost to a summary. Task 1.7 and Domain 5 both depend on this: the identifiers, decisions, and constraints that a summariser would treat as detail are often the load-bearing part.
A stop hook on the main run can block completion, which is how you enforce an output requirement such as every claim carrying a source. Like the subagent version, it gates rather than edits: it can refuse to finish and it cannot write the missing citation.
Where hooks are not enough
Hooks share the agent's tool surface, and some risks live below it. If the requirement is that an entire class of effect must be impossible under any prompting or tool choice, the control belongs in infrastructure.
Operating-system network isolation blocks outbound connections regardless of which tool name the agent uses or how a command is wrapped. A read-only database credential blocks writes at the connection layer even if every application check is bypassed. Neither can be talked past by reasoning or evaded by pattern variation, because neither is matching patterns.
Name matching only catches the names you thought of. A command-line HTTP client, a language runtime one-liner, a package manager reaching out mid-install: all of these leave the network without touching a tool called fetch. Removing the capability at the operating-system layer needs no enumeration.
Least privilege is the cheaper structural version. A specialist's tool list defines its surface by omission: leave out the editing tools and file modification is not discouraged, it does not exist. Shell access is all-or-nothing at that layer, so when a specialist needs shell but only for one safe invocation, remove what you can at the definition and constrain the rest with an allowlisting hook. Worktree isolation is orthogonal, controlling where edits land rather than whether they happen.
Last, treat hooks as control-plane software, because that is what they are. They are versioned, reviewed, and run with minimal authority, with sensitive values kept out of logs and outputs. Test the trigger conditions in both directions. Record decisions including denials and errors, not only approvals, since the denials are the evidence that the control works. Monitor latency, failure rate, and unexpected blocking. A one-line shell command is short, not low-risk, and a mandatory hook that starts failing must be fixed rather than quietly disabled.
Task Decomposition
Fixed versus adaptive splitting, and the structural reason a single thorough pass misses things a larger model cannot fix.
Two questions run through this task. How do you split work, and why does splitting it a particular way catch defects that no amount of extra capability catches?
The second question is the more interesting one, because the answer is structural rather than about model quality. Ask one pass to review forty files for security, performance, style, and cross-file consistency, and it will do all four shallowly. Not because it lacks the ability to do any one of them well, but because attention spread across forty files and four concerns is thin everywhere. The fix is architectural: more passes, each with less to hold. A bigger window and a stronger prompt do not fix it, and recognising that is most of what this task tests.
The first question, fixed or adaptive, is a decision you make once per workload and get wrong in both directions. Fixed structure applied to open-ended investigation cannot adapt. Adaptive machinery applied to a known checklist pays a rediscovery tax on every request.
Attention dilution is structural
The hallmark diagnostic: the review is not wrong, it is generic. Every finding is real and shallow. Nothing is fabricated and nothing is deep. Serious issues in individual files go unmentioned, and anything that only shows up between files, a caller that was not updated when a signature changed, a shared assumption that two modules now disagree about, is invisible.
Two distinct failures are in play and they need different fixes. Depth per item fails because attention is divided across too many items. Breadth across items fails because a pass looking at one file has no view of the others. Fixing only one leaves the other in place, which is why the answer is two layers rather than one better pass.
// One pass over forty files spreads attention too thin to catch anything
// deeply, and cannot see across files at all. Two layers fix both problems.
async function reviewChangeset(files: SourceFile[]) {
// Layer one: depth per item. Each pass sees one file and nothing else.
const perFile = await Promise.all(
files.map((file) =>
runPass({
systemPrompt: FILE_REVIEW_PROMPT,
prompt: renderFile(file),
}),
),
)
// Layer two: breadth across items. This pass never sees full file bodies,
// only the structured findings and the signatures they touch.
const integration = await runPass({
systemPrompt: INTEGRATION_REVIEW_PROMPT,
prompt: renderIntegrationInput({
findings: perFile.flat(),
signatures: files.map(publicSurface),
}),
})
return { perFile, integration }
}The first layer gives depth: each pass sees one item and nothing else, so its full attention is on that item. The second layer gives breadth: a pass that never sees full file bodies, only the structured findings and the public surfaces, which is exactly the view needed to notice that a signature moved and its callers did not.
The forty files already fitted. Capacity was never the constraint; attention distribution was. A larger window lets you put more in, and the same dilution reappears at the new scale. This is the same effect as the coordinator accumulation problem in task 1.2 and the positional degradation that Domain 5 treats directly, and in all three cases capacity is the wrong lever.
The pass was already trying to be thorough. Instructing it to try harder does not create attention that the structure spread thin, and in testing it changes the tone of the output rather than its depth. Prompt quality is a real lever for many problems and not for this one.
Batching is the near-miss worth understanding, because it is half right. Splitting forty files into batches of five does restore depth: each pass now has five files to think about. It does nothing for the cross-file dimension, because no pass ever sees across the batch boundaries. Batching without a dedicated integration pass fixes depth and leaves breadth broken.
Order matters too. The per-item passes come first, then the cross-item pass over their outputs. Folding the cross-item concern into each per-item pass, asking every file review to also consider consistency with the other thirty-nine, recreates the original dilution inside each pass. And the cross-item pass should consume structured findings and interfaces rather than full bodies, or it inherits the same problem it was created to solve.
Depth comes from fewer items per pass. Breadth comes from a separate pass that only sees the summaries.
The same shape in long-document work
Extraction over a long document fails the same way, with a distinctive signature: the beginning and end are handled well and the middle is thin. Facts that were present go unextracted, not because they were hard, but because of where they sat.
The fix is chunking with overlap. Each chunk is small enough for uniform attention, and overlapping the boundaries means nothing falls into a seam. For multi-document scope, layer it: extract per document, then group and reason over the extractions. That grouping layer is the same integration pass in different clothes.
Hierarchical summarisation is the sibling pattern for reasoning rather than extraction: segment, summarise each segment, then reason over the map of summaries. And when aggregating across sources whose shapes differ, normalise between extraction and merge. Merging raw extractions from divergent formats produces inconsistency that reads as contradiction, which is the same failure task 1.3 described at the handoff boundary.
Fixed pipeline or adaptive decomposition
One question decides this: are the steps and their order knowable before the input arrives?
Per-file review always does the same thing to each file, so the steps are known and deterministic code can dispatch them. A quality audit that always examines the same four dimensions is fixed. A repository migration where which files need changing, and how, depends entirely on what the repository turns out to contain is not knowable in advance, so the plan has to be built at runtime from what the agent finds.
Getting this backwards costs in both directions. Fixed structure on open-ended investigation cannot pivot when a finding changes what matters, so the agent completes a plan that stopped being the right plan several steps ago. Adaptive machinery on a known checklist pays a model call every request to rediscover the same four dimensions, with the added downside that it may enumerate them slightly differently each time, so your outputs stop being comparable.
Plan-then-execute and reactive stepping are the two adaptive shapes. Plan first, then execute the plan, gives you a reviewable artifact before anything happens and a clear structure to audit. Reasoning and acting in alternation, deciding each next step from the last observation, adapts more tightly and is harder to inspect. Complexity picks between them, and hybrids are common: plan the shape, step reactively inside each phase.
When a plan meets reality mid-execution, replan the affected portion. Preserve completed work and do not restart from scratch, which discards correct results, and do not push on with a plan you now know is wrong. This is also worth distinguishing from the agent loop of task 1.1: the loop is the machinery that runs each step, and the plan is what the steps are. Loop termination is still governed by the stop signal, not by whether the plan has been consumed.
A planner's output deserves validation before you execute it. Check the dependency graph for cycles, or two subtasks will wait on each other forever. Sort topologically to find what can genuinely run in parallel and what must be sequenced. And validate feasibility against domain knowledge, because a plan can be internally consistent and still contain a step that cannot be done.
Routing sits alongside both. Simple requests should skip the pipeline they do not need; complex ones get the full treatment. That is the same argument as task 1.2's case against always running every specialist, applied to the plan rather than the roster.
Confidence, review, and who checks the work
A per-item pass can emit a confidence score alongside its finding, which is useful for routing low-confidence items to closer review. It is only useful once calibrated: raw model confidence is unreliable in a specific and awkward way, since a confidently wrong answer looks exactly like a confidently right one from the outside. Calibrate the routing threshold against labelled examples rather than trusting the number as reported. Domain 5 treats this in full.
Self-review inside the same session has a structural limit that is easy to miss. A pass cannot reliably catch its own committed errors, because the reasoning that produced the error is still in context and still looks correct. Independent confirmation means a fresh instance with no prior reasoning: not the same agent asked to check itself, and not a second turn in the same conversation.
The specialisation argument is worth stating plainly. Several passes, each with a focused prompt and a scoped tool set, produce deeper analysis than one generalist prompt asked to cover the same ground, and the reason is the dilution argument again rather than any claim that specialised prompts are magic.
One behaviour resists prompting entirely. When a model is rewarded for tests passing and can edit the tests, telling it not to is weak. The control is review or visibility on test-file changes during implementation work, which is task 1.4's argument arriving in a decomposition context: an incentive misalignment needs a structural control, not an instruction.
Batch work, and what a batch cannot do
When a decomposition yields many independent items, batch submission becomes attractive for throughput and cost. It fits a specific shape only: single-turn, non-blocking, no tool calls. If nobody is waiting on the result and each item is one self-contained request, a batch is the right vehicle.
A batch cannot run a tool-calling loop, which means an item that fails cannot investigate and repair itself. That property is the reason batch submission is the wrong answer for agentic work however asynchronous the work sounds, the same near-miss flagged in task 1.1.
Partial failure is the normal case, so plan for it rather than treating it as exceptional.
- Identify failures by their per-item identifier, classify by cause, and resubmit only the affected subset. Resubmitting the whole batch pays again for everything that already worked.
- Give each item a retry budget and route exhausted items to a dead-letter queue, so one pathological input cannot consume the run's headroom.
- Isolate errors per item with log-and-continue rather than aborting the batch, and collect the failures for targeted retry.
- For long jobs, checkpoint and resume, which requires each item to be idempotent or to carry a deduplication key, or resuming double-processes whatever was in flight.
- Keep an append-only result manifest, so under partial failure you can still say exactly what was processed and what was not.
One granularity detail from the refinement case. When feeding defects back for correction, group defects that interact into one message and send independent ones separately. Interacting defects fixed in isolation produce fixes that conflict; independent defects bundled together dilute attention across them, which is this chapter's whole argument at the smallest scale.
Decomposing across agents rather than passes
Everything above works with passes inside one agent. When the work crosses agents, task 1.2's rules apply on top, and two are worth restating here because they are decomposition failures rather than topology failures.
First, a coordinator should do simple, well-scoped work itself and delegate only what benefits from isolation or specialisation. The test is context impact: delegate when the work would flood the coordinator, execute directly when it fits comfortably and depends on state already in hand.
Second, too-narrow partitions miss cross-cutting themes, and the answer is a dedicated cross-cutting scope or a coordinator integration pass over the merged findings that re-delegates the gaps. That refinement loop, inspect the merged output and re-delegate until coverage is sufficient, is the multi-agent form of the two-layer structure this whole chapter is built on: work the items, then work the relationships between them.
One correctness note when decomposed work performs parallel updates. Validate business-rule consistency after the updates land, and carry a compensating action for violations. Independent updates that are each individually valid can combine into a state that no single update would have produced, and a per-item check cannot see it. Which is, once again, why the integration pass exists.
The dependency location decides the structure. Work that depends on nothing else can be split; work that depends on the relationships needs a pass that can see them.
Session State and Resumption
Resume, fork, or start fresh, and why a session file is not durability.
An agent run ends, or is interrupted, and something has to happen next. Three mechanisms are available and the exam presents them as interchangeable options in scenarios where only one is right. The deciding question is never which is most powerful; it is whether the observations already in the history still describe the world.
That question has a sharp edge, because there is no automatic invalidation. A tool result recorded four hours ago sits in the restored conversation as a plain statement of fact, and the model weighs it as one. If the file changed, the dependency updated, or the ticket moved, the history is now a set of confident false premises that the model has no way to distinguish from the true ones.
The second theme is durability, which is a different thing from continuity. Resuming restores what was said. It does not restore what was running, and it does not tell you which side effects actually landed before the interruption. Systems that need to survive a crash need explicit external state, not a longer transcript.
Resume, fork, fresh
Task 1.3 introduced these as spawning mechanisms. Here they are continuation decisions, and the choice is driven by the state of the world rather than by the shape of the work.
// Three different questions, three different answers.
function chooseContinuation(session: Session, now: Date): Continuation {
// Nothing has moved underneath the run. Keep the thread.
if (!session.worldChangedSince(session.lastTurnAt)) {
return { mode: "resume", sessionId: session.id }
}
// The thread is still valid but you want two outcomes from one base.
if (session.wantsDivergentBranches) {
return { mode: "fork", sessionId: session.id }
}
// Observations in history describe a world that no longer exists.
return {
mode: "fresh",
handoff: {
issue: session.issueType,
actionsTaken: session.actions,
lastKnownStatus: session.status,
staleness: "Statuses below were observed earlier and must be re-checked.",
},
refetch: session.volatileFacts,
}
}Resume when the prior observations are still valid. The history replays, the accumulated reasoning is preserved, and you continue a coherent line of work at minimal cost. This is the right answer more often than the anxious answer suggests: if nothing has changed, rebuilding context is waste.
Fork when the baseline is good and you want two outcomes from it. You have analysed a codebase and want to try two migration strategies from the same understanding, compared fairly. Both branches carry the same prefix, diverge after the fork point, and never see each other.
Start fresh when the observations have gone stale. Not because fresh is safer in general, but because there is no way to remove a stale block from a restored history, so the only clean baseline is one you construct deliberately.
There is no change detection over tool results. Nothing compares the recorded file content against the file on disk, and nothing marks a stale block. The result sits in history at full weight. This is the single most common wrong answer in resumption scenarios, and it is attractive precisely because a sensible system would do this.
A fork copies the prefix, staleness included. You now have the same false premises in two branches rather than one. Forking is the right move from a baseline you have confirmed is accurate, and it is not a cleaning mechanism. Forking from just before the changed files were read sounds like a workaround and rarely survives contact with a real transcript, because the stale reasoning is not neatly confined to one segment.
Why you cannot talk your way out of stale context
The instinct on discovering stale context is to correct it: re-read the changed file, or tell the model to disregard the earlier results. Both append. Neither deletes. The old observation is still in the history, and now the model holds two contradictory records of the same file with no reliable rule for choosing between them.
Instructing it to prefer the more recent result reduces the error rate and does not eliminate it, and the residual failures cluster in cross-file reasoning, where a decision depends on several observations at once and one of them is the old one. In observed behaviour the agent keeps referencing the stale value even after being told not to.
There is a narrow band where resume plus targeted correction is genuinely correct, and the scenarios test that boundary deliberately. If only a few explicitly named files changed, the rest of the accumulated reasoning is sound, and the session is short enough to stay coherent, then resume, name the changed artifacts, and re-analyse exactly those. That is cheaper and more faithful than discarding a session's worth of valid understanding.
Scope the re-derivation deliberately. Re-reading everything to be safe wastes tokens and reintroduces variance for artifacts that were stable. Re-reading nothing carries the stale assumptions forward, which is at its worst in authentication and payment paths where a quiet wrong assumption is the whole risk. Full re-exploration earns its cost when the change was broad, when a refactor could have indirect effects beyond the named files, or when the session had already degraded.
When you do start fresh, what you inject matters as much as the decision to inject. The seed is a curated structured record: key findings, decisions already taken, open questions and next steps, what was examined, and verbatim values for anything load-bearing. Not the prior transcript, which reintroduces every stale payload and blows the budget. Not the final output alone, which loses the intermediate findings and the coverage tracking that made continuation possible.
Appending a correction leaves the error in place. A clean baseline has to be constructed, not requested.
Compaction and clearing solve different problems
Two adjacent commands get pulled into resumption questions as answers, and neither does what the question needs.
Compaction replaces the history with a summary and continues in the same session. It is a capacity tool: correct when a valid, coherent session has filled its window with verbose discovery output and needs room for a design phase. It cannot fix staleness, because summarisation has no way to tell a stale observation from a current one, and it makes specific recall worse by condensing exact names and figures into generic prose. It also does not produce a fresh baseline or a handoff artifact.
Clearing wipes the conversation. The choice between clearing and compacting turns on whether the next task is related. Pivoting from rendering to physics inside the same project keeps the rendering summary useful, so compact. Switching to an entirely unrelated feature where prior context would bleed in unhelpfully, clear. Using compaction to switch tasks leaves a summary of the old task in play; using clearing to continue the same investigation discards discoveries you still need.
The compaction hook from task 1.5 is the pressure valve here: it fires before the summary is written, which is your chance to pin the identifiers and decisions a summariser would otherwise flatten.
Long-session drift and the scratchpad
A distinct failure appears in long sessions with no staleness at all. Early in the run the agent identified exact class names and call edges. Two hundred turns later, buried under file listings and search output, those specifics have lost salience, and the agent starts substituting generic patterns for the specific names, contradicting its own earlier findings, or claiming the information is not available. It is in the history. It is just deep.
The countermeasure is to externalise confirmed findings to a file and read that file back when needed. A read injects the findings at the current turn at full freshness with little surrounding noise, so they compete at full strength instead of from a diluted position. It also decouples the knowledge from a conversation that will eventually be summarised or truncated.
Compaction makes this worse. The oldest findings, which are the ones already losing salience, are exactly what a summary compresses hardest, so the generic-pattern substitution becomes more likely rather than less. And raising the response length limit does nothing, since it governs how much the model writes rather than how far back it attends.
The stricter version applies when exact values must tie out. Financial figures, specific hosts, precise timestamps: extract them into a structured facts block held outside the conversation and re-injected at the top of every prompt, with an instruction to read quoted figures from that block. A system-prompt instruction to retain exact figures helps for the most recent few and not for the ones that already went through a summarisation pass.
Continuity is not durability
The context window lives in process memory. A crash, a restart, a container eviction clears it. The saved transcript records the conversation for a later explicit resume, and it does not restore process-local variables, write tracking, or an in-flight task map. Those need their own persistence.
The distinction that matters most in practice: the transcript records that a write was requested, not that it durably committed before the process died. Resuming and assuming the write landed is how a workflow double-charges or double-publishes.
So a workflow that must survive interruption persists progress to external storage: a correlation identifier, the last completed step, intermediate outputs, pending work, and often the agent version and configuration. On restart the orchestrator reads that state and continues from the next step rather than replaying the run.
- Checkpoint at meaningful boundaries: after a validated milestone, per batch, or at a phase transition. Checkpointing only at the end means any failure discards everything.
- Record verified outputs, not speculative intermediates. A checkpoint that persists uncertainty as fact corrupts every replay from it.
- Make replayable steps idempotent, or carry a correlation token so the downstream service returns a cached result rather than re-executing. Read-only analysis needs none of this; anything that charges, publishes, or mutates shared state needs all of it.
- Handle cancellation and timeout cooperatively: check for the signal, save partial state, exit, and let the orchestrator re-spawn from the checkpoint. Set timeouts per subtask rather than one global budget, so there is somewhere to checkpoint between them.
For multi-agent pipelines, each agent exports its progress to a known location and a coordinator loads a manifest that aggregates those exports, verifies coverage, and re-injects the relevant slice into each restarted agent. Letting each agent reload independently produces snapshots from different moments and therefore conflicting beliefs about what is done. Persisting the coordinator's conversation log instead is imprecise to parse and expensive to carry. When the guarantee is global coverage, twelve of twenty-eight documents processed, the manifest is what makes the claim checkable.
A limit stop is a checkpoint, not a failure. When a run stops because it hit a token or turn budget, the work up to that point is valid: summarise the progress, append a continuation, and resume. Retrying the same call with the same settings reproduces the same truncation, and marking the task failed throws away good work. The stop reason is what distinguishes capped by budget from failed by error, which is task 1.1's discipline arriving at the orchestration layer.
A transcript is a record of what was said. Durability is a record of what actually completed.
Where sessions do and do not exist
Several details decide whether a resume finds anything at all, and they are the kind of thing that looks like a bug when you first meet it.
Session transcripts are stored under a path derived from the working directory where the session started. Resume the correct identifier from a different directory and the runtime cannot locate the transcript, so the agent quietly starts fresh with no error to explain it.
Each SDK invocation without an explicit resume is an independent session. No history, no tool results, nothing carried forward, even from a call seconds earlier in the same script. Neither timing nor model size creates continuity; the lifecycle does, and only when you pass the identifier.
Across containers or hosts, local session files do not travel. A session that ran in one ephemeral worker cannot be resumed in the next by passing a path that does not exist there. The portable mechanism is to capture the earlier stage's results as explicit application state and inject them into the next, which is the same curated-summary pattern as recovering from staleness. Shipping the transcript or using a store adapter is possible, and fresh curated state is generally more reliable than replaying a large transcript that may itself be stale. A single long-lived session spanning a large bulk job is the other failure here: the window fills and long-lived workers are fragile.
Session identifiers are scoped handles, so treat them as such. Store the identifier alongside the workflow record that owns it, and resume it only for that logical task with matching authorisation. Reusing a recent session because it happens to be warm, or sharing one identifier across customers as a caching strategy, is a correctness and privacy failure rather than an optimisation.
Two cost notes on forking. A fork inherits the parent's full token load, so a late fork from a long verbose session pays that load again in every branch; forking early or from a summarised baseline is cheaper. And forks never merge automatically: you compare the branches and apply what you want by hand. Two terminals resuming the same session are not isolated branches, they are two views of one linear history.
Reading a scenario under time pressure
Exam scenarios in this domain are long, and most of the length is scene-setting. A few phrases do the actual work, and learning to find them is worth as much as knowing the material.
Start with what is being asked for. Prevent, guarantee, must never, zero tolerance, and single bypass all point at code enforcement. Prefer, should, and generally point at prompt guidance. Words about certainty are the load-bearing ones.
Then look for exonerations. When a stem tells you each specialist completed its assigned subtask flawlessly, or that both search and analysis were verified as working, it is clearing the downstream components so only the coordinator remains. That sentence is the answer key, written as background.
Then check what already happened. If the action has executed, no answer that prevents it is available, however precisely it detects the problem. If the action has not executed, an answer that only reports is not enforcement.
And check whether a proposed fix addresses the actual mechanism. Raising a cap does not repair a wrong termination signal. A larger context window does not repair diluted attention. Re-reading a file does not remove the stale observation. A stronger prompt does not create a guarantee. In each pair the second thing is what the scenario complained about and the first is what the tempting option changes.
Recognising a familiar mechanism and picking the option that names it. Several options usually name real mechanisms correctly; only one matches the requirement in the stem. A hook, a fork, a batch, and a classifier can all be genuine capabilities and still be wrong here because the question asked for prevention, consistency, an interactive loop, or a within-agent ordering fix.
If you have twenty seconds left on a question, ask the one question this whole domain is built around: which of these still holds when the model behaves slightly differently than the author of the scenario expected. That is usually the answer.