Chapter 5 — From Answering to Acting: Agents and the ReAct Loop
Every technique in the previous four chapters executes in a single round trip: you send a context, the model responds, the call ends. That works for questions, classification, extraction, summarization — tasks where the complete answer can be produced in one pass over a fixed context. But consider a more realistic task: "Review the last 50 pull requests in our repository, identify recurring patterns in code review feedback, check whether the same issues appear in the last 10 reviews, and draft an engineering blog post about what you found." A single call cannot do this. The model doesn't know what's in your repository. It doesn't know how many PRs there are. It can't decide what to check next based on what it found. It can't adapt its plan when PR #47 reveals a pattern it didn't anticipate at the start. This requires a loop. The model must be able to observe, decide, act, observe again, and repeat — as many times as the task requires. That loop is what defines an agent. This chapter builds it from scratch, explains why each piece of the loop exists, and then shows you when to dismantle the whole thing and use something simpler instead.
Part 1: The Agent Loop — Built From First Principles
Start with the simplest possible framing: an agent is a process that runs in a loop. Each iteration of the loop consists of the model generating a response, your code executing any actions the model requested, and the results of those actions being fed back into the model's context for the next iteration. The loop terminates when the model produces a final answer rather than requesting another action.
Let's build this in bare Python before any framework. No LangChain, no CrewAI, no abstraction layers. Just the loop.
def run_agent(initial_message: str, tools: list[Tool], max_iterations: int = 20) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": initial_message}
]
for iteration in range(max_iterations):
# Step 1: Ask the model what to do next
response = model.chat(messages=messages, tools=[t.schema for t in tools])
# Step 2: If the model produced a final answer, we're done
if response.stop_reason == "end_turn":
return response.content
# Step 3: If the model requested tool calls, execute them
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_call in response.tool_calls:
tool = find_tool(tools, tool_call.name)
result = tool.execute(tool_call.arguments)
tool_results.append({
"tool_use_id": tool_call.id,
"content": str(result)
})
messages.append({"role": "user", "content": tool_results})
continue
# Should not reach here
raise RuntimeError(f"Unexpected stop reason: {response.stop_reason}")
raise RuntimeError(f"Agent exceeded {max_iterations} iterations without completing")
This is the entire agent loop. Not as a simplification — this is genuinely the core loop that every agent framework is built on top of. The differences between frameworks are in what they add around this loop: richer state management, parallel tool execution, better error handling, visualization, multi-agent coordination. But the loop itself is this simple.
Notice what's happening mechanically. The messages list is the model's memory. It grows with every iteration: system prompt, initial request, model response with tool call, tool result, next model response, next tool call, result, and so on. The model's entire "knowledge" of what has happened is this message list. When you run an agent that makes 15 tool calls, the context window at iteration 15 contains the complete transcript of iterations 1 through 14. The model decides what to do next by reading the history of everything it has already done.
This has a direct consequence: agents consume context window faster than single calls. Every iteration adds tokens. An agent making 10 tool calls might consume 3x the tokens of a single call for the same task, because each tool's output must be read and re-read by the model in subsequent iterations. For Chapter 9's cost discussion, keep this in mind.
The max_iterations guard is not optional. A model can loop indefinitely — retrying a failed tool, checking the same condition repeatedly, getting confused and cycling. You need a hard stop. Set it based on your expected task complexity; 10-30 is typical. If your agent is regularly hitting the limit, either the task needs decomposition (Chapter 6) or the loop is broken and needs debugging.
Part 2: The ReAct Pattern — Why Interleaving Reasoning With Action Beats Separating Them
You built the loop. Now let's talk about what the model should be doing inside each iteration.
The naive approach: ask the model "here are your tools, what do you want to do?" The model picks a tool and generates arguments. Problem: the model has no scratch pad. It looks at the task, looks at the tool options, and immediately commits to an action. For simple tasks this works. For tasks that require reasoning — "I need to find all PRs where the reviewer flagged the same issue, which means I should first understand the review format, then query PRs, then cluster by issue type" — the model without reasoning tokens tends to make shallow, locally optimal decisions that globally underperform.
The ReAct pattern (Reasoning + Acting, from Yao et al. 2023) interleaves explicit reasoning tokens with action tokens. Before the model picks an action, it generates a "thought" — an explicit reasoning step about what it knows, what it needs to find out, and why it's choosing the next action. This is chain-of-thought applied to the agent loop.
Without ReAct:
User: Review the last 50 PRs and find recurring review feedback patterns.
Model: [tool_call: get_pull_requests(limit=50)]
[gets results]
Model: [tool_call: analyze_feedback(prs=...)] ← what does this even mean?
With ReAct:
User: Review the last 50 PRs and find recurring review feedback patterns.
Model: <thought>I need to understand the structure of PR reviews before I can
find patterns. I'll start by fetching a few recent PRs to understand
the review comment format, then design an analysis approach.</thought>
[tool_call: get_pull_requests(limit=5, fields=["review_comments"])]
[gets results — sees reviews are plain text comments]
Model: <thought>Reviews are freeform text. I can't do structured pattern matching
directly. I'll fetch all 50 PRs, then summarize the review themes
across them. Given the volume, I should batch the analysis.</thought>
[tool_call: get_pull_requests(limit=50)]
Why does the thought token improve decision quality? The same reason chain-of-thought improves answer quality in a single call: the reasoning tokens become part of the context that the action tokens are predicted from. A model that has explicitly articulated "I need to understand the structure before I can analyze it" will generate a more appropriate first tool call than a model that jumps straight to action. The reasoning constrains the action prediction.
The thought tokens don't have to be visible to the end user. You can strip them before presenting results. But they need to be present in the model's context during generation.
Structuring ReAct in a prompt:
System: You are a research agent with access to tools. For each step, you must:
1. Output a <thought> block explaining what you know, what you need, and why
you're choosing the next action
2. Then invoke the appropriate tool, OR
3. Output a <final_answer> block when you have enough information to answer
Be specific in thoughts. "I will search for X" is not specific.
"I will search for X because Y is the information gap I need to fill before I can Z" is specific.
Part 3: Plan-and-Execute vs ReAct — When Each Is Appropriate
ReAct is a reactive strategy: the model decides its next action after seeing the result of the previous one. This is powerful for exploratory tasks where you don't know in advance what you'll find. It's less efficient for tasks that could be planned upfront.
Plan-and-Execute is a two-phase approach: first, the model generates an explicit plan — a sequence of steps — before taking any action. Then a separate executor (which can be the same model or a different one) works through the plan step by step.
Plan-and-Execute structure:
Phase 1 — Planner:
User: Write a competitive analysis of our three main competitors.
Model: I will execute the following plan:
1. Fetch public pricing pages for Company A, B, C
2. Fetch feature comparison pages for each
3. Search for recent funding announcements
4. Summarize each company's positioning
5. Write comparative analysis
Phase 2 — Executor:
[Executes step 1 — calls web_fetch(company_a_pricing)]
[Executes step 2 — calls web_fetch...]
...
[Executes step 5 — calls model.generate(synthesis)]
Use ReAct when:
- The task is exploratory — you don't know what you'll find, and findings change what to look for next
- Steps are dependent on previous results in non-obvious ways
- The task is sufficiently short that replanning isn't needed (< ~10 steps)
- You want the model to handle unexpected discoveries adaptively
Use Plan-and-Execute when:
- The task structure is predictable and the plan can be fully specified upfront
- You want to show the user (or a human approver) the plan before executing it
- Steps are long-running and you want to parallelize independent ones
- The task is long (> 20 steps) and you want to batch the planning away from the execution
The hybrid (often best): Plan upfront, allow replanning mid-execution when unexpected results require pivoting. The planner produces an initial plan; the executor detects when a result diverges from expectations (using a check like "does this result change my plan?") and triggers a replan. This matches how a careful human engineer works on a complex investigation.
Part 4: The Observe-Think-Act Loop
A slight variant worth naming explicitly, as it clarifies the agent's epistemic state at each step:
- Observe: What is in my context? What do I know? What have I just learned from the last tool result?
- Think: What is the gap between what I know and what I need to answer the task? What action would best close that gap?
- Act: Execute the action (tool call or produce final answer)
The observe step is implicit in standard ReAct but worth making explicit in your system prompt for complex tasks. A model that explicitly articulates its current state of knowledge before deciding what to do next makes better decisions than one that jumps from tool result to tool call.
Observe: I have fetched PRs 1-20. The reviews show three recurring themes:
variable naming, missing error handling, and test coverage.
I haven't yet analyzed PRs 21-50 to check if these patterns persist.
Think: I need PRs 21-50 before I can confidently claim these are system-wide
patterns and not just artifacts of recent work. I should fetch them
before drawing conclusions.
Act: [tool_call: get_pull_requests(offset=20, limit=30)]
This explicit epistemic accounting — "here is what I know, here is what I don't know, here is my reasoning for the next action" — is particularly valuable for long-running agents where the task might take 30+ iterations. Without explicit observation steps, agents tend to "drift" — gradually losing track of the original goal as the context fills with tool results and the original task recedes.
Part 5: Filesystem as Context — External Memory During Long Runs
Here's a problem you'll encounter on any non-trivial agent task: the context window fills up. If each tool call returns 1,000 tokens of results, 30 iterations means 30,000 tokens of tool results in the context before the agent is done. For a 128k context window this is manageable; for longer tasks or larger tool outputs, you hit the limit.
The filesystem is a simple, effective external memory. Instead of keeping every tool result in the context, the agent writes results to files and reads them back selectively when needed.
# Agent uses filesystem to manage long-running state
tool: write_to_file(filename: str, content: str)
tool: read_from_file(filename: str) -> str
tool: list_files() -> list[str]
# In the agent loop, results accumulate on disk:
Model: <thought>I have 50 PR analyses to track. Rather than keeping all of them
in context, I'll write each one to a file as I process it and
summarize at the end.</thought>
[tool_call: write_to_file("pr_1_analysis.txt", analysis_of_pr_1)]
...
[after processing all 50 PRs]
Model: <thought>All PRs analyzed. Let me list the files and read them all
to write the final synthesis.</thought>
[tool_call: list_files()]
[tool_call: read_from_file("pr_1_analysis.txt")]
...
The key insight: the filesystem decouples working memory (what's currently in context) from persistent memory (what has been computed). This is the same principle behind write-ahead logs and materialized intermediate results in distributed systems — you're doing the same thing, just for a model agent.
The obvious risk: the agent needs reliable file management. Files created in one agent run should be scoped to that run (use a temporary directory or a run-specific prefix). You don't want agents from concurrent runs reading each other's files.
Part 6: Checkpoint and Resume — Why Long-Running Agents Must Be Interruptible
A 30-step agent task that takes 15 minutes is a non-trivial operation. During those 15 minutes, things fail: network calls time out, model API calls fail transiently, your application restarts, the user closes their browser. Without checkpointing, a failure at step 28 of 30 means starting over from scratch.
Checkpoint-and-resume means persisting the agent's state at regular intervals so it can restart from the last checkpoint rather than from scratch.
The agent's state is the message history. Everything the agent has done, observed, and decided is in the message list. If you serialize and store the message list at each iteration (or every N iterations), you can restore it on restart:
def run_agent_with_checkpoints(
task: str,
tools: list[Tool],
run_id: str,
checkpoint_store: CheckpointStore
) -> str:
# Try to restore from checkpoint
checkpoint = checkpoint_store.load(run_id)
if checkpoint:
messages = checkpoint.messages
start_iteration = checkpoint.iteration
else:
messages = [system_msg, user_msg(task)]
start_iteration = 0
for iteration in range(start_iteration, MAX_ITERATIONS):
response = model.chat(messages=messages, tools=[...])
if response.stop_reason == "end_turn":
checkpoint_store.delete(run_id) # Clean up on completion
return response.content
# Execute tool calls...
messages.extend(tool_results)
# Save checkpoint after each iteration
checkpoint_store.save(run_id, messages, iteration + 1)
raise RuntimeError("Max iterations exceeded")
For production agents handling tasks that span minutes or hours, checkpointing is not optional. The checkpoint store can be Redis (fast, ephemeral), a database (persistent across restarts), or a file system. The right choice depends on your recovery requirements.
A related concern: idempotency of tool calls. If the agent writes a file, then the process crashes, and the agent restarts from the checkpoint before the write, the file will be written again. Tool calls that are not idempotent (creates, sends, charges) need either deduplication (track which tool calls have been executed, skip on replay) or compensation logic (detect the duplicate and undo it). This is the same two-phase commit and exactly-once delivery problem from distributed systems, applied to agent tool calls.
Part 7: Human-in-the-Loop — Where and How to Insert Approval Gates
Not every agent action should execute automatically. Some actions are irreversible. Some have significant business consequences. Some involve ambiguity that the model might not handle correctly. For these, you want a human in the loop.
Human-in-the-loop is an interruption in the agent loop: the agent pauses, presents a proposed action or decision to a human, and waits for approval before proceeding.
Agent loop with approval gate:
Model: <thought>I have enough information to process the refund.</thought>
[tool_call: request_approval(
action="process_refund",
amount=247.50,
customer="John Smith",
reason="damaged product received"
)]
[Approval request sent to human operator]
[Human reviews and approves]
Model: Received approval. Proceeding.
[tool_call: process_refund(order_id="ORD-123", amount=247.50)]
The request_approval tool is not a special API construct — it's a regular tool that your application implements. When called, it pauses the agent loop (the application stops calling the model API), delivers the approval request to a human (via email, Slack, a dashboard), and waits for a response. When the human responds, the application resumes the agent loop with the approval decision injected as the tool result.
Design decisions for approval gates:
What requires approval? Actions that are irreversible (deletes, sends, charges), actions above a cost or impact threshold ("process refunds above $500"), actions in new domains the agent hasn't been tested on, and any action where model error could cause significant harm.
What approval timeout is acceptable? Some tasks can wait hours for human review; others need a response in seconds. Design your approval workflow for the latency your use case tolerates.
What happens if approval is denied? The model needs to handle rejection gracefully — either try an alternative approach, escalate to a higher-level agent, or inform the user that it can't complete the task without the denied action.
Async vs sync approval. For long-running tasks, async approval (the agent suspends, you get notified later) is better than sync (the agent blocks, your application holds a connection). Async approval requires the checkpoint-and-resume infrastructure from Part 6.
Part 8: When NOT to Use an Agent
This is the most important section in the chapter and the one most framework tutorials skip. Agents are the right tool for a narrow class of problems. Using them for everything is expensive, slow, unreliable, and unnecessarily complex.
Before you build an agent, ask: is this actually a dynamic, multi-step task that requires observing intermediate results and adapting? If the answer is no — if the steps are known in advance and don't depend on intermediate findings — a deterministic workflow is almost always better.
The Five Cases Where an Agent Is Wrong
1. Fixed pipelines. If your task is always: (a) extract entities from the user's message, (b) query the database, (c) format the response — you don't need an agent. You need three function calls and a bit of orchestration. A deterministic pipeline is faster, cheaper, easier to test, and has no failure modes from agent loop errors. Reserve agents for tasks where the number and type of steps are not known in advance.
2. Single-step tasks with retrieval. "Answer a question about our docs" sounds like an agent but is just RAG (Chapter 4) — one call to retrieve, one call to generate. The loop doesn't add anything because there's nothing to loop over.
3. Structured transformation. "Parse this JSON and reformat it" is a single prompt call with structured output. An agent adds latency, cost, and failure modes with no benefit.
4. High-frequency, low-complexity tasks. If you need to classify 100,000 support tickets, an agent loop adds latency per ticket that accumulates. Batch inference with a single, well-crafted prompt is dramatically more efficient.
5. Tasks requiring guaranteed accuracy. Agents are non-deterministic. Each decision in the loop can produce different results on different runs. For tasks where you need reproducible, auditable results — financial calculations, legal document analysis, safety-critical decisions — the non-determinism of an agent loop is a liability. A deterministic pipeline with model calls at specific decision points gives you auditable steps.
The Decision Framework
Is the number of steps known in advance? YES → Use a prompt chain or pipeline
NO ↓
Do intermediate steps depend on earlier NO → Use parallel prompt chains
results in unpredictable ways? YES ↓
Does the task require many steps (> 10)? NO → Use a simple ReAct agent
YES → Consider multi-agent (Chapter 6)
Does the task require human oversight? YES → Add approval gates
Is cost or latency critical? YES → Profile before shipping
The field has a tendency to reach for agents because they feel powerful. Senior AI engineers reach for agents last, after verifying that a simpler approach doesn't work. An agent that runs 20 LLM calls to do something a 3-call pipeline could handle is not impressive engineering — it's expensive engineering.
Part 9: LangGraph — What It Abstracts and Why It Exists
You now understand the agent loop from first principles. LangGraph (from LangChain) is a framework that provides a higher-level abstraction over this loop, using a directed graph model. Understanding it becomes easy once you've built the loop manually.
The Core Abstraction
LangGraph models agent execution as a directed graph where:
- Nodes are functions that take state and return updated state
- Edges are transitions between nodes — either fixed (always go from A to B) or conditional (go from A to B or C based on the current state)
- State is a typed dict that accumulates across the entire run — it's the equivalent of your
messageslist, but generalized to include any state your application needs
from langgraph.graph import StateGraph, END
from typing import TypedDict
class AgentState(TypedDict):
messages: list[dict]
tool_results: list[dict]
final_answer: str | None
def call_model(state: AgentState) -> AgentState:
response = model.chat(state["messages"], tools=TOOLS)
return {"messages": state["messages"] + [response.message]}
def execute_tools(state: AgentState) -> AgentState:
tool_results = []
for tool_call in extract_tool_calls(state["messages"][-1]):
result = dispatch_tool(tool_call)
tool_results.append(result)
return {"messages": state["messages"] + tool_results}
def should_continue(state: AgentState) -> str:
last_message = state["messages"][-1]
if has_tool_calls(last_message):
return "tools" # Continue to tool execution
return "end" # No tool calls → go to END
graph = StateGraph(AgentState)
graph.add_node("model", call_model)
graph.add_node("tools", execute_tools)
graph.set_entry_point("model")
graph.add_conditional_edges("model", should_continue, {"tools": "tools", "end": END})
graph.add_edge("tools", "model")
agent = graph.compile()
result = agent.invoke({"messages": [user_msg]})
The hand-built loop from Part 1 and this LangGraph graph do the same thing. The difference is what LangGraph makes easy: built-in state persistence (for checkpoint-and-resume), streaming (for progressive UI updates), human-in-the-loop interrupts, parallel node execution, and visualization of the graph structure.
Why the Graph Model Is Useful
The graph model earns its keep in two scenarios:
Complex branching. If your agent needs to behave differently based on many possible outcomes, a graph with named edges is easier to reason about and maintain than a cascade of if-else statements in a loop function.
Multi-agent orchestration. When you have multiple specialized agents (Chapter 6), LangGraph's graph model lets you define how work flows between them explicitly — which agent hands off to which, under what conditions, and with what state.
For simple ReAct agents, LangGraph is arguably overkill — your hand-built loop is shorter and easier to debug. For complex multi-step workflows with branching, parallelism, and human-in-the-loop gates, LangGraph's structure pays for itself.
Ecosystem Awareness
The agent framework landscape is fragmented. A brief orientation:
- LangChain/LangGraph: The most widely used Python framework. Strong ecosystem, extensive documentation, sometimes over-abstracted. Good for complex multi-agent graphs.
- CrewAI: Higher-level, role-based abstraction ("crew" of agents with defined roles and goals). Easier to get started, less flexible for non-standard patterns.
- AutoGen (Microsoft): Strong for multi-agent conversations, good for code generation scenarios, research-leaning.
- Anthropic Agent SDK: Official SDK for Claude-based agents. Clean API, stays close to the metal, integrates naturally with Claude's tool use.
- Vercel AI SDK: Excellent for frontend integration, streaming-first design. Better suited for product-facing agents than backend automation.
The framework choice matters less than understanding what the framework is doing. An engineer who has built the loop by hand can switch between frameworks in a day. An engineer who only knows a framework is stuck when the framework breaks.
Vocabulary Map
| Term | What it actually is (physical, not dictionary) |
|---|---|
| Agent | A process that runs in a loop: model generates a response or tool call → your code executes tool calls → results are injected back → model continues. The loop terminates when the model produces a final answer. |
| Agent loop | The core iteration: message history → model call → if tool_call: execute + inject result → repeat. Everything else is built on top of this. |
| ReAct (Reasoning + Acting) | A prompting pattern where the model explicitly generates reasoning tokens (thoughts) before action tokens. The reasoning constrains and improves the action decision — same mechanism as chain-of-thought. |
| Plan-and-Execute | A two-phase agent pattern: first generate a complete plan, then execute steps. Better for predictable, parallelizable tasks. Worse for exploratory tasks where findings change the plan. |
| Observe-Think-Act | An explicit epistemic loop variant: the model first articulates what it knows (observe), then reasons about what to do (think), then acts. Helps maintain task focus over long runs. |
| Context drift | The phenomenon where an agent loses track of its original goal over many iterations as the context window fills with tool results and the original task recedes in attention. |
| Checkpoint-and-resume | Persisting the agent's message history at each iteration so it can restart from the last checkpoint on failure, rather than from scratch. |
| Idempotency (agent context) | The property of a tool call that can be safely repeated without unintended side effects. Required for checkpoint-and-resume to work correctly on non-read-only tool calls. |
| Human-in-the-loop | An interruption in the agent loop where the agent pauses, presents a proposed action to a human, and waits for approval before proceeding. |
| Approval gate | A specific tool in the agent's tool set that triggers a human-in-the-loop interrupt. The agent calls it like any other tool; your application handles the pause-and-notify logic. |
| Deterministic pipeline | A multi-step workflow where steps are known in advance and don't depend on intermediate results in unpredictable ways. Preferred over an agent when the task structure is predictable. |
| LangGraph | A Python framework that models agent execution as a directed graph of state-transforming nodes. Adds value for complex branching, multi-agent coordination, and built-in checkpointing over a hand-built loop. |
| Filesystem as external memory | Using temporary file writes during a long agent run to keep context window usage bounded. The model writes intermediate results to disk and reads them selectively rather than keeping everything in the message history. |
| max_iterations guard | A hard stop on the agent loop to prevent infinite looping when the model gets stuck, retries a failing tool indefinitely, or enters a cycle. Always required. |
Practice Problems
Problem 1 — Failure Analysis
You deploy a ReAct agent to process customer refund requests. The agent has tools: get_order(order_id), verify_refund_eligibility(order_id), and process_refund(order_id, amount). In production, you observe that the agent occasionally processes refunds for orders it has already refunded — the refund is executed twice. Walk through two distinct root causes for this behavior (one in the agent loop design and one in the tool design), and propose a fix for each.
Problem 2 — Design from Scratch
You're building an agent that monitors your production infrastructure and automatically responds to incidents. It has tools: get_alerts(), get_metrics(service, window), get_logs(service, window, filter), restart_service(service), scale_up(service, instances), page_on_call(severity, message). Design the complete agent: which pattern (ReAct vs Plan-and-Execute), which actions require human-in-the-loop approval (and why), what the checkpoint/resume strategy is, what the max_iterations guard is set to, and what happens if the agent loop exceeds the limit during an active incident.
Problem 3 — Synthesis
Explain why these three design decisions are all consequences of the same fundamental property of the agent loop: (a) you must always set a max_iterations guard; (b) long-running agents should use the filesystem for intermediate results rather than keeping everything in the message history; (c) an agent that processes 100,000 records is usually the wrong tool — a batch pipeline with targeted model calls is better. What is the one property of the agent loop they all respond to?
Problem 4 — Go Deeper
You're tasked with building an agent that can modify its own system prompt based on feedback from its task performance — a "self-improving" agent. The agent evaluates whether its last response was good, and if not, proposes an update to its instructions. Analyze this design: what are the safety risks, what are the reliability risks, what would prevent this from becoming an unstable feedback loop, and how would you architect this to be useful without being dangerous? (This requires reasoning about agent design principles beyond what the chapter explicitly covers.)
Problem 5 — Connect to Previous Chapter
Chapter 4 built a RAG system as a standalone pipeline: query comes in, retrieval happens, answer goes out. Now, inside an agent loop, RAG becomes a tool. Describe specifically how your Chapter 4 RAG pipeline would be exposed as a tool in an agent: what does the tool schema look like, what does the tool return, and how does the agent use it in a ReAct loop to answer a multi-part question that requires multiple retrieval calls. Then: what is the difference between the agent's behavior when RAG is a tool vs. when RAG is pre-wired as a context-enrichment step before the model call? When is each approach better?
What's Coming Next
You can now build an agent that loops, reasons, acts, checkpoints, and handles human approvals. But it's still one agent — one context window, one perspective, one set of tools. A single agent has natural limits: the context window overflows on tasks that require tracking a large amount of state; a generalist agent underperforms a specialist when the task requires deep domain-specific behavior; and no single agent can check its own work. When work becomes complex enough to defeat one agent, you need multiple agents coordinating — and that introduces a new class of problems around memory, state sharing, and failure modes that can take down the whole system. Chapter 6 builds the multi-agent architecture, and the failure catalog that comes with it.