Study Notes
Notefoundation

Multi-Agent Topologies

Why the god-agent becomes an architectural liability — context dilution, quadratic tool confusion, three production topologies, formal handoffs, scoped state and tools, reducers for parallel writes, and subgraph boundaries that teams can test independently.

40 min read·verified Content review 2026·reviewed 2026-09-01

After this you should be able to

  • Choose an appropriate multi-agent topology for a workflow
  • Define handoff, state ownership, reducer, and tool-scope boundaries
TopologiesHandoffsReducers

Applied AI · Multi-Agent Systems

How to take one overloaded “god agent” and split it into specialized sub-agents that hand off cleanly — and why every serious production system is built this way.

Session 9.1  ·  Updated Sep 2026  ·  Read ~22 min


// the big idea

A single LLM told to do everything — classify, sanitize, call tools, enforce security, format replies — fails stochastically: its attention spreads too thin and it starts breaking rules at random. The fix is architectural, not a better prompt. Separate the concerns into small agents, give each only the tools it needs, and move state between them through a typed contract. The absence of a capability is the only guarantee it can’t be misused.


1

Notebook Summary#

Hand-copy this. Telegraphic by design — every line is expanded in Section 2.

1 · Why decompose (monolith = anti-pattern)#

  • God agent → one LLM + one giant prompt doing all jobs → “junk drawer” → does everything, reliably nothing.
  • Context dilution → attention ∝ 1/k² (token at position k). Long prompt ⇒ edge instructions get near-zero weight. Model doesn’t forget → stops attending.
  • Tool confusion → n tools ⇒ n-way choice every call. Risk grows quadratically (similar tools interfere). 50 tools ⇒ 50C2 = 1,225 confusable pairs.
  • Deterministic vs stochastic bug → bad Python fires every time; bad monolith agent fires with probability p, and p rises with scale/load/time.
  • Cost → 10k-token prompt sent every turn. 10k req/day ≈ 100M tokens/day on prompt overhead alone.
  • ✎ doodle: one fat circle labeled “GOD” with ~10 arrows fanning out to classify / mask / refund / CRM / summarize / security…

2 · Principles#

  • Separation of Concerns (Dijkstra, ~1974) → split system into distinct sections, one concern each.
  • Single Responsibility (SOLID / R. Martin) → an agent has one reason to exist / one reason to change.
  • Blast radius → if it breaks, how much breaks? Monolith = whole system. Scoped agent = its job only.
  • Least privilege (tools) → bind exactly the tools needed, zero more.
  • Least privilege (state) → an agent reads/writes only the state fields it owns.

3 · Three failure modes (memorize)#

  • ① Router that answers instead of routing (retrieval tool → treated as answer tool).
  • ② Summarizer that calls CRM tools (task/tool descriptions semantically overlap).
  • ③ Security agent that generates reports (worst — long adversarial input pushes security rules to the edge).

4 · Topologies#

TypeShapeUse
Swarmpeer-to-peer, no bossbrainstorm / research only; enterprise = catastrophe
Hierarchicalsupervisor → workersgold standard for enterprise
PipelineA → B → C, linearfixed sequential transforms
memory: Swarm = group chat, no moderator (non-deterministic, unbounded, unauditable). Hierarchical = hospital: physician dispatches labs, no lab calls another. Pipeline = airflow/assembly line.
  • ✎ doodle: swarm = tangled mesh of dots; hierarchical = 1 box on top, 3 below; pipeline = 3 boxes in a row with → →.
  • 5 · Handoff = formal state transition#

    • Not a string, not a message → transfer of custody of a typed state object under a schema contract both agents hold.
    • Receiver must never infer / recompute → sender writes explicit fields (ticket_type, pii_detected, sanitized_input).
    • No schema = KeyError waiting to happen (Python TypedDict not enforced at runtime → add Pydantic in prod).

    6 · Reducers (parallel writes)#

    • No reducer ⇒ last write wins ⇒ silent data loss under parallelism.
    • add_messages built-in for messages; define custom reducers for every other field before testing.

    7 · The two agents#

    AgentToolsOwns
    Triage (immune system)ZEROraw/sanitized input, pii_detected, injection_detected, ticket_type, is_safe
    Tech Support (specialist)CRM lookup, KB searchtool_results, final_response, summary
    ⭐ TRICK A model can’t misuse a tool it was never shown. Tool absence is the only reliable guarantee — not a prompt rule. ⚠ PITFALL Refund tool copy-pasted into triage → prompt said “don’t refund” → still fired on ~3% of billing complaints → ~300 unauthorized refunds/day. Fix = delete the binding (30s).

    8 · Subgraphs vs unified graph#

    • Unified → all nodes share one StateGraph. Grows into spaghetti fast (paths ~ n!; merge conflicts across teams).
    • Subgraph → compiled StateGraph embedded as one node = black box to parent. Enforces ownership → team A owns triage, team B owns tech support, team C owns master.
    • Parent + subgraph must share ≥1 overlapping state key. Master state = superset of all subgraph states.
    ⚠ PITFALL Forgot .compile() → parent gets an uncompiled blueprint, not a runnable graph. compile() validates structure + resolves edges. Non-negotiable.

    9 · Data flow (final architecture)#

    • request → master graph → triage subgraph (ingress → classify → handoff) → is_safe?
    • if unsafe → deterministic block response, 0 LLM tokens. if safe → route by ticket_type → tech-support subgraph (summarize? → agent ⇄ tools → respond → egress) / fraud handler / general handler.
    💡 TIP Interview line: “What’s the blast radius?” is a standard pre-deploy question. If you can’t answer it, you can’t ship the agent.

    2

    Detailed Study Notes#

    A support agent that worked in the demo and quietly rotted in production — and the architecture that fixes it.

    Picture the agent from the last session. One language model, one long system prompt. It masks personal data, spots prompt-injection attempts, classifies the ticket, calls the CRM, formats a clean reply. In the demo it looked great. Then the knowledge base grew, three more tools got added, and one day it started issuing refunds when a customer merely asked about a bill. Nobody changed the refund logic. The prompt still said “do not issue refunds during classification.” It broke anyway — on maybe three requests in a hundred. That failure is the whole reason this lesson exists.

    2.1 · The monolith and why it’s an anti-pattern#

    A monolithic agent is a single LLM call, carrying one system prompt, made responsible for every job in the workflow. Teams reach it naturally: build a proof-of-concept with one model and a long prompt, then keep bolting on responsibilities. Need billing? Add billing instructions. Need inventory? Add inventory instructions. Need security? Add security rules. The prompt accretes duties until it becomes what practitioners call a god agent.

    Analogy · the junk drawer

    A god agent is the kitchen junk drawer. You keep dropping in miscellaneous things “just for now,” and eventually it contains everything and is useful for nothing — you can never find the one thing you need. It’s also the fresh-startup hire handed five job descriptions (receptionist, accountant, engineer, HR, security) and told to pick the right one per situation. Not a talent failure — a structural one.

    GOD AGENT classify intent mask PII detect injection look up CRM search knowledge base issue refund · enforce security …
    fig 2.1 — one call, every concern. Each new arrow dilutes attention on all the others.

    The core distinction: a monolithic Python module that has a bug is deterministically wrong — the bug fires every time, so you find it and fix it. A monolithic LLM agent is stochastically wrong — it fires the same bug with some probability on every call, and that probability climbs with scale, load, and time. You can pass every test and still ship a system that misbehaves 3% of the time in the wild.

    2.2 · Context dilution — why long prompts stop working#

    Context dilution is the failure where a model’s attention is spread so thinly across a long prompt that critical instructions receive almost no weight. LLMs are attention-based: they can attend to any piece of the context window, but that attention is a finite budget divided among everything present. A tight 500-token prompt gives the model sharp resolution on every rule. Stretch the same rules into a 10,000-token prompt and the instructions near the edges become, in practice, invisible.

    // the shape of it

    As the instructor framed it: an instruction at token position k receives attention roughly proportional to 1/k² relative to an instruction at position 1 — instruction-following accuracy falling off with (roughly) the square of the distance from the most-attended region. So in a 10,000-token prompt the last stretch of instructions can sit at a small fraction of the weight the opening tokens get. The model doesn’t forget those rules — it simply stops attending to them reliably.

    [!] Treat the exact 1/k² law as an illustrative heuristic for the intuition, not a precise universal constant — real attention profiles depend on the model. The direction is what matters: longer prompt ⇒ edge instructions lose weight.

    token 1 token 10,000 weight effectively invisible sharp resolution here
    fig 2.2 — attention weight vs. position. Rules pushed to the tail sit in the dead zone.

    The real-world symptoms are exactly what you’d predict from attention collapse: an agent that issues refunds on a billing question (the refund tool sat near the classification rules, which drifted out of the attention window); an agent that calls a CRM tool for a plain knowledge-base query; an agent that spits raw JSON when asked a natural-language question. None of these are “the model is dumb” — they’re the geometry of a bloated prompt.

    2.3 · Tool confusion grows quadratically#

    Every tool you bind forces the model to solve an n-way classification (“which of these do I call?”) on every single turn. This isn’t linear. Similar tools create semantic interference in the model’s selection process, so the risk of a wrong pick scales with the number of pairs the model could confuse — and pairs grow quadratically.

    Monolith

    50 tools, one agent#

    Confusable pairs = 50C2 = (50×49)/2 = 1,225. Every extra tool crowds the decision space and raises the odds of a mis-route or a wrong parameter.

    Decomposed

    3 + 5 + 4 tools, three agents#

    Each agent chooses within its own small set, so confusion points add instead of multiplying: 3 + 5 + 4 = 12 local decisions. The quadratic term never forms.

    Same total capability, radically different failure surface. Splitting the toolbox is the single cheapest way to buy reliability.

    2.4 · The principles that justify splitting#

    Separation of Concerns (SOC) is the plain idea that a system should be divided into distinct sections, each addressing one concern — one piece of information that affects behavior. It was articulated by Edsger W. Dijkstra around 1974. [!] The transcript guessed he was “from France” and was unsure — Dijkstra was Dutch. The principle and date stand. SOC doesn’t tell you how to separate; only that you must.

    In ordinary software, violating SOC gives you code that’s hard to read and maintain. In LLM systems it gives you something worse: a system that is probabilistically incorrect. That’s the reason the principle takes on new urgency here.

    Single Responsibility Principle (SRP) — from Robert C. Martin’s SOLID set — sharpens SOC into: a class (here, an agent) should have exactly one reason to change. So the triage agent exists to sanitize and classify; the tech-support agent exists to resolve issues; a billing agent exists to process billing. When new PII patterns appear, only triage changes. When a new CRM tool lands, only tech support changes. The others are untouched.

    💡 TIP · blast radius

    Before deploying any agent, answer one question: if this component is corrupted or misbehaves, what is the scope of the damage? In a monolith the blast radius is the entire system. In a properly scoped design it’s bounded to that one agent’s job — a triage malfunction can only hurt classification, never resolution. This calculation is a standard, expected step in AI architecture review.

    There’s also a hard cost argument. A 10,000-token system prompt is re-sent on every agentic turn — every tool call, every routing decision, every response. At 10,000 requests a day that’s on the order of 100 million tokens/day spent on prompt overhead alone. Split those instructions across a 500-token triage prompt and a ~1,000-token tech-support prompt and the average cost per request drops by roughly an order of magnitude.

    Analogy · microservices, again

    Early web apps were monoliths: front-end, business logic, DB access, auth, caching in one process. Change the auth logic and you redeploy the whole server. The industry moved to independently deployable microservices with clean, versioned interfaces for exactly the reasons above. Multi-agent decomposition is that same migration, one layer up.

    2.5 · Three production failure modes#

    These aren’t hypothetical — they’re documented patterns. Each is a role bleeding past its mandate because attention on the “stay in your lane” instruction has decayed.

    ① Router that answers#

    A classify-and-route agent, given retrieval tools “to route better,” starts answering directly. Its attention began treating information-retrieval tools as response-generation tools.

    ② Summarizer that calls CRM#

    A summarization agent with CRM access “for context” starts firing those tools at the wrong times — “look up customer” and “summarize conversation” share too much semantic overlap.

    ③ Security agent that reports#

    Worst case. A security filter that also has reporting tools drifts into general assistant behavior under long adversarial input that shoves the security rules to the context edge.

    ⚠ PITFALL · the statistics are inevitable

    If an agent with n tools has a per-call base error rate, the probability of at least one mis-route across N daily requests approaches certainty as N grows. With a monolith it’s not a question of whether one of these modes fires in production — only when. That’s why the answer is structural, not “write a firmer prompt.”

    2.6 · The three topologies#

    A topology is the communication pattern between your agents — who is allowed to talk to whom. Choosing it is as consequential as choosing a data model for a database: pick wrong and the system is structurally incapable of meeting enterprise needs (determinism, auditability, cost bounds).

    Swarm — peer-to-peer, no boss#

    Any agent can invoke any other agent; behavior is emergent from local decisions, and the group converges by something like majority voting. Genuinely useful for creative brainstorming, exploratory research, or simulation — where non-prescriptive, organic exploration is the point.

    Analogy · group chat with no moderator

    Everyone speaks when they feel like it. The topic wanders, decisions get contradicted, and the group eventually reaches some conclusion — but the path there is different every time. For an enterprise system that must produce identical outputs for identical inputs and be audited, that’s unacceptable.

    ⚠ PITFALL · “who owns the state?”

    With no central authority, no one owns state consistency. Multiple agents mutate shared state at once (race conditions), and A→B→C→A circular routing isn’t just possible — without explicit loop detection it’s structurally guaranteed. There’s no natural termination, so calls and cost are unbounded, and incidents can’t be reproduced. If someone describes “collaborative agents” and the owner of state is “everyone,” it’s failing in production.

    Hierarchical — supervisor / worker#

    A single supervisor holds exclusive routing authority. It dispatches work to specialized workers based on the current state, each worker executes only its function and returns results only to the supervisor — never to another worker. The supervisor integrates results, updates shared state, and makes the next routing decision. This is the gold standard for enterprise systems and the topology this course builds.

    Analogy · the attending physician

    The physician (supervisor) evaluates the patient and dispatches orders: bloodwork to the lab, an X-ray to radiology, a prescription to pharmacy. The lab never calls radiology; pharmacy never orders bloodwork. Each specialist does its task and reports back. The physician integrates and decides the next step. Single source of truth, deterministic path, clean audit trail.

    Pipeline — linear sequence#

    The simplest topology: a fixed sequence where each agent’s output is the next agent’s input. A processes raw input into enriched state, B applies its transform, and so on — like an Airflow DAG or an assembly line. No head, but also no branching intelligence.

    Swarm no boss · emergent Hierarchical super w1w2w3 one authority · deterministic Pipeline ABC fixed sequence
    fig 2.6 — the three topologies. Only the middle one is safe for enterprise workflows.

    2.7 · The handoff — a formal state transition#

    A handoff is not a function call or a message — it’s the transfer of custody of a structured state object from one agent to the next, governed by a schema contract both agents agree to. Get it wrong and you get the most common class of production failure in multi-agent systems, almost always because someone treated the handoff as an informal string instead of a typed protocol.

    The rule that kills ambiguity: the receiving agent must never infer, assume, or recompute anything the sender should have handed it. If triage classified the ticket as technical, tech support receives ticket_type = "technical" as an explicit field — not as something it must re-derive from the conversation text. If PII was masked, it receives pii_detected = True and sanitized_input as explicit fields.

    state contract — the shared TypedDict
    from typing import TypedDict, Literal
    
    # The handoff contract. Both agents agree to these fields and types.
    class SupportState(TypedDict):
        raw_input: str
        sanitized_input: str          # written by triage
        pii_detected: bool            # written by triage
        injection_detected: bool      # written by triage
        ticket_type: Literal["technical", "billing", "fraud", "general"]
        is_safe: bool                 # triage's final safety verdict
        # ...downstream fields owned by tech support live here too
    ⚠ PITFALL · the KeyError waiting to happen

    Python TypedDict is not enforced at runtime. If the sending agent forgets to write an expected field, the receiver blows up with a KeyError — the single most common bug in early multi-agent code. The contract principle: agent A must leave state in a condition agent B can operate on with zero ambiguity. In production, wrap the TypedDict with Pydantic validation so required fields and value constraints are actually checked, not just hinted. [+]

    Triage writes fields SupportState ticket_type is_safe · pii… Tech Support reads fields custody transfer under a schema contract
    fig 2.7 — the state object is the interface. Explicit fields, no re-derivation.

    2.8 · State ownership, scope, and reducers#

    State ownership is the multi-agent version of variable scope. Just as a well-designed function reads and writes only its own locals and declared parameters, a well-designed agent touches only the state fields in its scope. Triage owns sanitized_input, pii_detected, injection_detected, ticket_type, is_safe. Tech support owns tool_results, final_response, summary. Neither writes the other’s fields.

    // least privilege is a security boundary

    This isn’t just tidiness. If tech support could write is_safe or pii_detected, an adversarially crafted tool result could corrupt the security state and make the master graph mis-route future requests. Field ownership is a security boundary, not a style preference.

    A reducer is a function that decides how concurrent writes to a state field are merged. When agents run in parallel, a field with no reducer defaults to plain overwrite — last writer wins — which silently loses data depending on who happened to finish last. LangGraph ships add_messages as the built-in reducer for the messages field; for every other field you must define reducers explicitly, and you must do it before you test, not after you’re debugging a data-loss incident.

    ⚠ PITFALL · the silent overwrite bug

    Say both subgraphs write to a shared tool_results field. Triage writes {"pii_scan": "clean", "injection": "none"}; later tech support writes {"balance": 998, ...}. With no reducer, the second write replaces the first — triage’s security findings vanish, and nothing errors. The fix is a reducer that appends instead of overwrites. This is exercise 3.1 below.

    2.9 · Tool scoping & the zero-trust agent#

    Tool scoping means binding an agent exactly the tools it needs and no others — the AI translation of the principle of least privilege from information security: a process gets exactly the permissions it needs, not one more. Applied to LLMs, this goes beyond access control; it directly changes correctness and predictability.

    ⭐ TRICK · absence is the only guarantee

    A language model cannot misuse a tool it was never shown. Tool hallucination — an LLM generating a call for a purpose the tool wasn’t meant for — is structurally impossible if the tool isn’t bound. That’s why the triage agent is given exactly zero tools. Not few, not “harmless” ones — zero.

    Engineers keep trying to substitute a strong prompt rule (“do not use the refund tool for classification”) for actual tool absence. It doesn’t hold. LLMs are probabilistic: they will violate any natural-language instruction with some non-zero probability, and across thousands of calls that violation becomes statistically guaranteed. The only reliable mitigation is architectural: if triage should never refund, don’t bind the refund tool to triage.

    ⚠ PITFALL · the 300-refunds-a-day incident

    A real failure: a refund tool was copy-pasted into the triage agent’s tool list by accident. The prompt clearly said “you are a classification agent, do not initiate refunds.” It behaved for 97% of requests — and on the ~3% involving billing complaints, it issued refunds unprompted. At 10,000 requests/day that’s ~300 unauthorized refunds/day. The fix took 30 seconds (delete the binding); the incident report took much longer. Every tool you add “just in case” expands the attack surface, the prompt, the attention dilution, and the cost. If you can’t name which node calls a tool and exactly why, it shouldn’t be in the system.

    2.10 · The two agents in this build#

    Triage — the immune system#

    Processes everything from the outside world before any other agent touches it. Zero tools by design.

    1. read raw input from state
    2. run PII analyzer / anonymizer
    3. run prompt-injection detection on the sanitized input
    4. call the classifier LLM at temperature = 0 (deterministic ticket type)
    5. write is_safe from the combined results
    6. package the handoff for the next node

    Tech Support — the specialist#

    Receives a clean, pre-validated, pre-classified ticket and focuses only on resolving it. It doesn’t know how intake worked — that ignorance is intentional.

    Reads: sanitized_input, ticket_type, summary.
    Owns: tool_results, final_response, summary.
    Tools: get_customer_details (CRM), search_knowledge_base.

    The prompts follow the same logic as the tools. Triage’s system prompt is surgical: classification instructions for the four ticket types, PII-handling rules, and nothing else — no CRM instructions, no tool talk. Tech support’s prompt is purely technical: how to use its tools, interpret KB results, format resolutions, handle tool failures — and zero security or PII content, because triage already handled that. Short prompts mean high attention on what matters.

    2.11 · Subgraphs vs. unified graphs#

    A subgraph is a fully compiled StateGraph embedded into a parent graph as a single node. LangGraph gives you two options: one unified graph where every node shares a single state definition, or subgraphs where each agent is its own compiled graph plugged into a master graph.

    The unified graph rots into a spaghetti problem faster than people expect. Eight nodes with three transitions each is already 24 conditional edges; the number of paths grows roughly as n!. Five developers editing one graph file means constant merge conflicts and no single person who can reason about the whole thing — the exact organizational failure that pushed the industry from monoliths to microservices.

    Subgraphs fix this by making ownership enforceable in code. To the parent, a subgraph is a black box: it receives a state object, returns a modified one, and its internal nodes/edges are entirely opaque. Team A owns triage, team B owns tech support, team C owns the master orchestration and the contracts between them.

    building & compiling a subgraph
    from langgraph.graph import StateGraph, START, END
    
    # --- triage subgraph ---
    triage = StateGraph(SupportState)
    triage.add_node("ingress", ingress_node)        # PII + injection scan
    triage.add_node("classify", classify_node)      # LLM @ temp=0
    triage.add_edge(START, "ingress")
    triage.add_conditional_edges("ingress", route_after_ingress)  # blocked? -> END
    triage.add_edge("classify", END)
    triage_graph = triage.compile()                 # NON-NEGOTIABLE
    
    # --- embed it in the master graph as ONE node ---
    master = StateGraph(SupportState)
    master.add_node("triage", triage_graph)         # subgraph = a node
    master.add_node("tech_support", tech_graph)
    master.add_conditional_edges("triage", route_after_triage)  # by ticket_type
    master_app = master.compile(checkpointer=checkpointer)
    ⚠ PITFALL · forgetting .compile()

    If you hand the parent an uncompiled StateGraph, it receives a blueprint, not a runnable graph — and execution fails. .compile() validates the graph structure and resolves edge conditions; it must run on every subgraph before embedding. Also: parent and subgraph state must share at least one overlapping key for data to flow. In practice the subgraph state is a subset and the master state is the superset of all fields.

    2.12 · The assembled system#

    Put together, it’s a supervisor/worker hierarchy. The master graph holds the full shared state and two subgraphs. Everything from the outside enters triage; only safe, classified requests reach a worker.

    user request→ master graph→ triage: ingress→ classify→ is_safe?
    safe→ route by ticket_type→ tech support: agent ⇆ tools → respond /fraud handler /general handler
    MASTER GRAPH · shared state (superset) request TRIAGE (0 tools) ingress · PII + injection classify (temp=0) is_safe? unsafe → block · 0 tokens TECH SUPPORT (2 tools) agent tools respond → egress fraud handler general handler route by ticket_type
    fig 2.12 — supervisor/worker: triage gates the door, the master routes, workers resolve.

    2.13 · Interview & practice Q&A#

    Q1 · Why is a monolithic agent’s bug “worse” than a monolithic Python module’s bug?

    A Python bug is deterministic — it fires every time on the same input, so tests catch it and a fix is permanent. A monolith LLM agent is stochastic: the same “bug” (a mis-route, a wrong tool call) fires with some probability on every call, and that probability rises with scale, load, and time. You can pass all your tests and still misbehave a few percent of the time in production, where it’s far harder to reproduce and diagnose.

    Q2 · Explain context dilution in one breath.

    Attention is a finite budget spread across the whole prompt. Lengthen the prompt and each instruction gets a smaller share — roughly proportional to 1/k² of its position — so rules near the edges receive near-zero weight and stop being followed reliably. The model doesn’t forget them; it stops attending to them. Shorter, focused prompts per agent restore that attention.

    Q3 · Why does tool confusion scale quadratically, not linearly?

    The risk isn’t about the count of tools but the number of pairs the model could confuse, since similar tools interfere semantically. n tools give roughly nC2 confusable pairs — 50 tools is 1,225. Splitting the toolbox across agents means each agent chooses within a small set, so confusion points add (3+5+4=12) instead of forming the quadratic term.

    Q4 · Why can’t a strong prompt rule replace removing a tool?

    LLMs are probabilistic and will violate any natural-language instruction with some non-zero probability; across thousands of calls that violation is statistically guaranteed. A prompt saying “never use the refund tool” still leaves the tool callable. The only reliable mitigation is architectural — don’t bind the tool. A model cannot misuse a capability it was never given.

    Q5 · Why is the swarm topology unacceptable for enterprise systems?

    No central authority means no owner of state consistency, so parallel writes race, and circular routing (A→B→C→A) is structurally guaranteed without explicit loop detection. There’s no natural termination, making calls and cost unbounded, and the non-deterministic call sequence makes incidents impossible to reproduce or audit. Enterprise systems need identical outputs for identical inputs — swarms can’t promise that. They’re fine for brainstorming and research.

    Q6 · What makes a handoff “formal,” and what breaks without one?

    A formal handoff transfers a typed state object under a schema contract, with every field the receiver needs written explicitly by the sender — no inference or re-derivation. Without it, agents pass informal strings, the receiver assumes a field exists, and you get a runtime KeyError — the most common early multi-agent bug, because Python TypedDict isn’t enforced at runtime. Production adds Pydantic to actually validate the contract.

    Q7 · When do you need a custom reducer, and what happens if you skip it?

    Any time more than one agent can write the same state field under parallel execution. Without a reducer the field defaults to overwrite — last writer wins — so an earlier agent’s data is silently lost depending on completion order. LangGraph provides add_messages for the messages field; define custom reducers (e.g. append) for every other shared field before testing, not after a data-loss incident.

    Q8 · Why must a subgraph be compiled, and how does its state relate to the parent’s?

    .compile() validates the graph structure and resolves edge conditions, producing a runnable object; hand the parent an uncompiled StateGraph and it gets a blueprint that fails at execution. The parent and subgraph must share at least one overlapping state key so data can flow; typically the subgraph state is a subset of fields and the master state is the superset of all subgraphs’ fields.

    Q9 · Someone hands you an agent to deploy. What’s the first question you ask?

    “What’s the blast radius?” — if this component is corrupted or misbehaves, what’s the scope of the damage? In a monolith it’s the whole system; in a scoped design it’s bounded to that agent’s single responsibility. It’s a standard pre-deploy check, and being unable to answer it is a sign the system isn’t decomposed enough to ship.

    2.14 · Key takeaways#

    • Monolith agents fail stochastically from context dilution (attention ~1/k²) and quadratic tool confusion — the fix is architecture, not a firmer prompt.
    • Separation of Concerns + Single Responsibility give each agent one reason to exist and bound its blast radius.
    • Hierarchical (supervisor/worker) is the enterprise gold standard; swarm is for exploration only; pipeline is for fixed linear transforms.
    • Handoffs are typed state transitions under a schema contract — explicit fields, no re-derivation, validated with Pydantic in prod.
    • Least privilege for tools and state: bind exactly what’s needed; tool absence is the only real guarantee; triage runs zero tools.
    • Subgraphs enforce ownership and must be compiled; define reducers before parallel writes lose data silently.
    god agent context dilution 1/k² attention quadratic tool confusion SOC · SRP blast radius swarm / hierarchical / pipeline typed handoff schema contract least privilege zero-trust agent reducer subgraph · .compile()

    3

    Microexercises#

    Reading proves you recognize the idea. These prove you can use it. Attempt each before opening the walkthrough.

    3.1 · Build it — a reducer that stops silent data loss#

    // your task

    Both subgraphs write findings into a shared findings state field. Triage writes its security scan; tech support later writes its tool results. With the default overwrite, triage’s findings vanish. Make both survive.

    Requirements

    • Write a reducer merge_findings(old, new) that appends rather than replaces.
    • Declare the findings field on the state so LangGraph uses your reducer.
    • Given the writes below, the final findings must contain both entries, triage first.
    starter — the two writes that must both survive
    # triage node returns:
    {"findings": [{"pii_scan": "clean", "injection": "none"}]}
    
    # tech-support node later returns:
    {"findings": [{"customer": "C10002", "balance": 998, "status": "past_due"}]}
    
    # EXPECTED final state["findings"]  ->  a list of length 2, triage entry first
    Show the full walkthrough

    Step 1 — see why the default loses data. A field with no reducer uses “last write wins.” The tech-support return value replaces the triage list wholesale, and nothing raises — the security findings are just gone. That silence is what makes it dangerous.

    Step 2 — write an append reducer. A reducer takes the current value and the incoming value and returns the merged result. For lists, concatenate. Guard against None on the first write.

    Step 3 — attach it with Annotated. LangGraph reads the reducer from the field’s type annotation. Without Annotated[..., merge_findings] it silently falls back to overwrite — the annotation is the wiring.

    from typing import Annotated, TypedDict, Any
    
    def merge_findings(old: list | None, new: list | None) -> list:
        return (old or []) + (new or [])   # append, never replace
    
    class SupportState(TypedDict):
        findings: Annotated[list[dict[str, Any]], merge_findings]
    
    # after both writes:
    # state["findings"] == [
    #   {"pii_scan": "clean", "injection": "none"},          # triage
    #   {"customer": "C10002", "balance": 998, "status": "past_due"},
    # ]   -> length 2, triage first  (correct)

    Why it matters: the reducer is the merge policy for shared state. Define it before you test parallel paths — discovering you needed one while debugging a production data-loss incident is the expensive way to learn this.

    3.2 · Diagnose it — the agent that refunds when it shouldn’t#

    // your task

    Below is a triage agent that only classifies tickets. It works in every test. In production, on a small fraction of billing complaints, it issues refunds no one asked for. Predict the exact failure, name the root cause, and fix it in one line. Assume ~10,000 requests/day, ~3% billing complaints.

    buggy — triage_agent.py
    SYSTEM_PROMPT = """You are a CLASSIFICATION agent.
    Return one of: technical | billing | fraud | general.
    Do NOT initiate refunds. Do NOT call the refund tool."""
    
    tools = [classify_ticket, search_kb, issue_refund]   # copy-pasted list
    triage_llm = llm.bind_tools(tools)                    # temp defaults
    
    def triage_node(state: SupportState) -> dict:
        msg = triage_llm.invoke([SYSTEM_PROMPT, state["raw_input"]])
        return {"ticket_type": msg.content, "is_safe": True}
    Show the full walkthrough

    Predict the failure first. No exception is raised — that’s the trap. On roughly 3% of requests (the billing complaints, where the refund tool is semantically closest to the input), the model emits a real issue_refund tool call. At 10,000 req/day × 3% ≈ ~300 unauthorized refunds per day, silently, while every unit test passes.

    Root cause — name it precisely. The refund tool is bound to an agent that should never call it. The prompt rule (“do not initiate refunds”) is a natural-language instruction, and an LLM violates any such instruction with non-zero probability; across thousands of calls that violation becomes statistically guaranteed. The prompt is not a security boundary. Secondary smell: no temperature=0, so classification isn’t even deterministic.

    Fix — remove the capability, don’t argue with it.

    tools = [classify_ticket]                 # zero-trust: bind ONLY what triage needs
    triage_llm = llm.bind_tools(tools)
    # (bonus) make classification deterministic:
    triage_llm = llm.bind_tools(tools).with_config({"temperature": 0})

    Why it matters: a model cannot misuse a tool it was never shown. Tool absence — not a firmer prompt — is the only reliable guarantee. If you can’t name the node that calls a tool and exactly why, unbind it.

    Annotated[..., reducer] append > overwrite reducers before test bind only needed tools prompt ≠ security boundary predict the silent failure temp=0 for classify