AI Systems · Multi-Agent Orchestration

The Supervisor Orchestrator

How a multi-agent system gets a brain: one permanent front door that reads the whole situation and decides who works next, without ever doing the work itself.

Updated September 2026 · Read ~18 min · Session 9.2

// the big idea

A supervisor is a single, permanent entry point that reads the full shared state and decides which specialist runs next — a hub every worker returns to. It routes; it never answers, calls tools, or does domain work. That one constraint is what makes the system testable, auditable, and safe to grow from 2 agents to 20 without touching the outside API.

1.0

Notebook Summary

Hand-copy target · fully derivable from the Detailed Notes below.

Why a supervisor

  • Problem → direct hand-offs (spoke→spoke) hard-code routing. Add an agent = edit pipeline, edit edges, retest, renotify every client.
  • Edge growth → super-linear (combinatorial explosion of conditional edges).
  • Fix → one supervisor = single permanent entry point. Open-Closed: open to new agents, closed to modification.
  • 💡Single Endpoint Req = non-negotiable. Client hits same URL on day 1 and after 20 agents added.

What a supervisor IS

  • Dedicated LLM node. Not a router. Not an agent. A coordinator with one job.
  • 1. Reads full state   2. Structured decision (Pydantic Literal)   3. Returns to hub   4. Zero execution.
  • doodle: [T][TS][FH] spokes -> big center circle SUP -> arrows back. END hangs off SUP.

Routing: two strategies

  Deterministic Semantic (LLM)
basis one state field full state
cost $0 1 LLM call
latency <1ms 200–500ms+
ambiguity no yes
reliability 100% probabilistic
  • ★Target 60–70% of routes deterministic (Python if/else). LLM only at raw entry point.
  • ⚠Making >2 LLM routes per request = broken state schema, not routing logic. Fix the schema.
  • Model = fast + cheap (Gemini 2.5 Flash, temp 0). Never a frontier model for routing.

Structured output

  • Pydantic Literal → next_worker ∈ {triage, tech_support, fraud_handler, FINISH}.
  • Enforced twice: (1) LLM structured-output API, (2) Pydantic validation on parse.
  • ⚠Free text ("I think billing...") → no matching node → KeyError 2am → graph crash.
  • Fallback → wrap call in try/except, default to triage (safest node).

Purity Principle

  • Supervisor does ONLY routing. Never tools, never customer responses, never domain analysis.
  • 💡Reason = testability. Pure supervisor = test by mocking state. Contaminated = need whole system.
  • aid: air-traffic controller never flies a plane. If it does, nobody coordinates.

FINISH + loop safety

  • FINISH = a routing decision the supervisor earns (all intents addressed? findings integrated? ready to ship?). Router maps FINISH → END.
  • delegation_count = circuit breaker. count ≥ MAX (e.g. 10) → force FINISH, zero extra cost.
  • Prompt soft-nudges toward FINISH as count rises [!] slide prompt uses >3 (soft); transcript uses 10 (hard code). Two layers, not a conflict.

Supervisor node — 7 stages

  • 1 extract fields → 2 check delegation limit → 3 build fast LLM (temp 0) → 4 prompt = system rule + state summary → 5 invoke, get validated object → 6 increment count → 7 return state update.
  • Stages 3–5 wrapped in try/except → default triage.

Cost (illustrative)

  • 5 needless LLM routes/req × 1k req/day × $0.001 ≈ $5/day ≈ $1,825/yr. At 10k/day ≈ $18,250/yr pure overhead.
  • Hybrid = static for ~80%, supervisor for ~20%. Use cheapest reliable mechanism.

2.0

Detailed Study Notes

A customer sends one message: "My API has been returning 500 errors since yesterday. Also, I just noticed a duplicate charge on my account. I need both fixed urgently." Your routing code reads the ticket, decides it is technical, and hands it to the tech-support agent. Tech support fixes the API and closes the ticket. The duplicate charge is never investigated. The customer escalates, angry, and now you are paying for a human agent to clean up what the machine skipped. Nothing crashed. The code did exactly what it was told. That is the failure this whole lesson is built to prevent.

2.1Why static routing breaks

Static routing is a lookup: read one field, return one path. It is fast, free, and perfectly reliable — for tickets that have exactly one intent.

The routing you have built so far (call it route_by_category) reads a single field like category and returns a single string. It was designed for clean, single-intent tickets: "Refund my payment." That covers maybe 70% of real traffic. The remaining traffic is where it quietly fails: multi-intent tickets (bill and API), ambiguity (is "account suspended" a billing problem or a fraud problem?), and conversations whose intent shifts halfway through.

Analogy · sign vs. cop

A traffic sign tells you where Manchester is — a fixed rule, painted once, that never looks at the road. A traffic cop watches what is actually happening at the junction and makes the call in real time. route_by_category is the sign. The supervisor is the cop.

⚠ PITFALL — the silent miss

Static routing does not error on a multi-intent ticket. It confidently picks one path and drops the rest. A silent miss costs far more than a crash, because no log tells you it happened — the customer's second problem simply vanishes.

2.2What a supervisor actually is

A supervisor is a dedicated LLM node that reads the shared state and decides which worker runs next. Not a router, not an agent — a coordinator with exactly one job.

The distinction matters. A router reads one field and returns one string. An agent does work — calls tools, writes responses. The supervisor sits above both: it reasons across the whole ticket history and every specialist's findings, then names the next step. Four properties define it:

1 · Reads full state

Reasons across ticket history, worker findings, and unresolved issues in shared state — not one field.

2 · Structured decision

Output is forced through with_structured_output() on a Pydantic model with a Literal.

3 · Returns to the hub

Every worker hands control back to the supervisor. Hub-and-spoke, no exceptions.

4 · Zero execution

No tools, no customer responses. Pure routing intelligence — the Purity Principle.

2.3The single-endpoint requirement

Customers must always talk to exactly one door, no matter how many specialists live behind it.

A retail bank with ten million customers exposes one chat window. Those customers must never know whether they are hitting a triage API, a billing API, or a fraud API. This is not a convenience — it is an architectural guarantee. When a direct hand-off wires triage straight to tech support with a hard-coded edge, the caller becomes coupled to the internals: add a specialist and you must edit the pipeline, rewire conditional edges, retest every path, and renotify every client. Edge count grows super-linearly — a combinatorial explosion.

Analogy · the hotel concierge

A guest never walks into the kitchen or the laundry. They speak to one person at the front desk, and the concierge delegates invisibly. The guest interface stays identical no matter how many departments exist or how they reorganize. The supervisor is that concierge: an abstraction layer over the internal topology.

Route everything through one supervisor and you get the Open-Closed Principle applied to a multi-agent system: open for extension (register a new subgraph in the master graph) and closed for modification (the external API contract, the client code, and the other subgraphs all stay untouched). You grow from 2 agents to 20 with zero client updates, zero contract changes, zero retesting of unchanged graphs.

SUPERVISOR the hub ↔ ↔ ↔ triage tech_support fraud_handler END every spoke connects ONLY to the hub — never to another spoke
fig 2.3 — hub-and-spoke: workers return to the supervisor; only the supervisor reaches END.
Why it matters

The single-endpoint rule is what makes the supervisor pattern meaningful. Without it, the supervisor is just one more LLM call bolted in front of your existing routing — cost with no architectural payoff. The invariant "every worker returns to the hub" is the whole point: a worker that routes to END on its own bypasses the supervisor's oversight entirely.

2.4Two routing strategies: deterministic vs. semantic

Deterministic routing is plain Python on a field you already trust; semantic routing is an LLM interpreting raw text. They cost wildly different amounts — choose by what the decision actually needs.

Deterministic (Python) routing uses if/else, match, or dict dispatch on a state field whose value is already validated and classified. Once triage has written ticket_type = "technical" to shared state, routing on that field costs $0, runs in under a millisecond, and is 100% reliable — not 99.7%, because if statements do not hallucinate. It is unit-testable with a plain assertion.

Semantic (LLM) routing is for raw, unstructured, ambiguous, or multi-intent input where no prior classification exists. This is the one place the LLM earns its keep — and it pays a latency tax: 200–500ms (up to ~2s) of wall-clock time per call, plus token cost, even for a fast model.

Dimension Deterministic Semantic (supervisor)
Decision basis one state field full state — all findings
LLM cost zero (pure Python) one LLM call per decision
Latency <1ms 200–500ms+ per step
Ambiguity no — picks one path yes — reasons across context
Multi-intent no — one route only yes — routes sequentially
Reliability 100%, unit-testable probabilistic, needs eval
★ TRICK — where the boundary sits

The expensive/cheap boundary is always at the same place: the system entry point, just before the first classifier. Raw text arrives there with no prior tag, so it needs the LLM. Every step after that operates on a typed, validated field — so it is pure Python. Find that one line and you know exactly where the latency tax is justified and where it is waste.

Watch the compounding: a request making five LLM routing decisions at ~1s each makes the user wait five seconds of dead time before seeing anything — five API calls paid for, zero user-visible value if it fails. The mechanical alternative would decide the same thing in under a millisecond at zero cost.

Fully-LLM routed Σ ~10s / ~$0.01 LLMLLMLLM LLMLLMLLM LLMLLMLLMLLM 1000ms · $0.001 each — every step pays the tax Hybrid (1 LLM entry + 9 Python) Σ ~1s / ~$0.001 LLM pypypy pypypy pypypy 0ms · $0 each — routes on already-typed fields Same 10-step job. ~10× less latency and cost by moving the boundary to the entry point.
fig 2.4 — the latency tax compounds when every step is an LLM route; the hybrid pays it once.
⚠ PITFALL — two LLM routes = wrong diagnosis

If you find yourself making more than two LLM routing calls per request in steady state, the fix is the state schema, not the routing logic. You are re-asking the LLM because a prior step failed to write enough classification into state. Add the field; route on it with Python.

💡 TIP — the model is a design choice

Use a fast, cheap model at temperature=0 (e.g. Gemini 2.5 Flash) for the supervisor. Intent classification needs no chain-of-thought or creativity — deploying a frontier model like Opus or Gemini 2.5 Pro for routing wastes capability budget on a task that doesn't need it. Aim for 60–70% of all routes deterministic; the production sweet spot is roughly static for 80%, supervisor for 20%.

2.5Structured output: Pydantic Literal

The supervisor must return a machine-checkable value, never free text. A Literal type pins the output to a closed set of valid worker names.

Picture the supervisor replying "I think billing should handle this." The graph's conditional edge tries to read next_worker, finds a sentence instead of a node name, and raises a KeyError — at 2am, in production, leaving the request permanently unserviced. The fix is to constrain the output space before the model ever speaks.

models.py — the routing contract
from typing import Literal
from pydantic import BaseModel

class SupervisorDecision(BaseModel):
    # next_worker can ONLY be one of these exact strings.
    next_worker: Literal["triage", "tech_support", "fraud_handler", "FINISH"]

Bind it with llm.with_structured_output(SupervisorDecision). LangChain converts the Pydantic schema to a JSON schema, hands it to the LLM's structured-output feature, and parses the response back into a typed object. The constraint is enforced at two points: first by the LLM API (it is steered to emit only approved values), then by Pydantic on deserialization (any stray value raises a validation error). The calling code receives a validated object with a known attribute type — not a raw string it must parse by hand.

⚠ PITFALL — prompts are probabilistic, not guarantees

Instructions in the prompt work with high probability, not certainty. At 10,000 requests/day, even a 0.3% miss is ~30 graph crashes daily. Literal + structured output drives that toward zero for well-formed responses — but still wrap the call in try/except and default to triage (the safest node, since triage can classify any input) for the malformed rest.

2.6The Supervisor Purity Principle

The supervisor has one job and must do only that job. The moment it starts doing business logic, it becomes untestable.

✓ The supervisor DOES

Read shared state and findings. Call the structured LLM. Return a SupervisorDecision. Domain-agnostic routing intelligence.

✗ The supervisor NEVER

Calls tools (KB, CRM). Generates customer responses. Performs domain analysis (PII rules, billing amounts). That lives in specialists.

Analogy · the air-traffic controller

An ATC does not fly planes; it issues routing instructions to qualified pilots. The moment the controller leaves the tower to fly one plane, the entire airspace loses its coordinator — every other plane is now flying without central intelligence. If the supervisor starts answering a user directly, the hub becomes just another spoke, and no node has global authority anymore.

💡 TIP — purity exists for testability

A pure supervisor is tested by mocking shared state and asserting the decision — no tools, no network, no domain fixtures. A contaminated one requires the entire system to be standing up before you can test a single routing choice. Purity is not aesthetic; it is what makes the most important node in the system verifiable. Treat its prompt as a security boundary, and keep a suite of ~50 representative tickets (including prompt-injection and multi-intent adversarial cases) that runs before every prompt change.

⚠ PITFALL — the helpful-agent leak (real example)

Amazon's Rufus is meant to answer only shopping questions, yet users get it to explain unrelated news events — burning tokens on work it was never scoped for. LLMs are completion machines trained to be helpful; without an explicit, specific prohibition they will drift into doing the work they shouldn't. State the ban in the prompt with maximum specificity: only route, never answer, under any circumstances.

2.7The FINISH condition

The supervisor decides when the job is done — not the workers. FINISH is a routing decision like any other, one the supervisor has to earn.

A worker that signals END on its own bypasses oversight: the supervisor never gets to check whether the billing dispute or fraud signal still sitting in messages was handled. Instead, every worker returns to the hub, and the supervisor runs three checks before it terminates:

1 · State synthesis

Read every finding in internal notes plus the final worker response.

2 · Validation check

Has every intent in the ticket been investigated? Is the answer ready to ship?

3 · Termination signal

Only when certain, return FINISH. The router intercepts it and points to END.

This is what prevents premature termination on complex, multi-intent tickets — the exact failure static routing produces silently. FINISH earned by synthesis is the difference between "API fixed, ticket closed" and "API fixed, billing addressed, then closed."

2.8Loop safety: delegation_count

A supervisor with no stop condition can loop forever. delegation_count is to the supervisor what an iteration limit is to an agent loop.

The loop is easy to fall into: the supervisor routes to tech, tech returns to the hub, the supervisor doesn't register progress and routes to tech again — token hemorrhage on circular logic. A counter is the circuit breaker. Each delegation increments it; once it hits the max, the supervisor forces FINISH at zero extra cost and delivers whatever response was last generated.

Why it matters — [!] two limits, not one

The sources use two different numbers, and both are correct because they are two layers. The prompt softly nudges the LLM toward FINISH as the count rises (the slide's rule uses > 3) — a probabilistic hint. The code enforces a hard ceiling (the walkthrough uses 10) that fires regardless of what the LLM wants. Soft limit persuades; hard limit guarantees.

2.9Designing the supervisor prompt

The prompt is the supervisor's only source of intelligence. It has three load-bearing parts: an explicit prohibition, an enumerated worker list, and a compact state summary.

The prohibition must be written with maximum specificity — "only," "never," "under any circumstances" — because a completion machine will otherwise try to be helpful. The approved list enumerates the exact valid strings for next_worker: it shrinks the output space, removes the need to invent a key from scratch, and gives the Pydantic Literal a target to validate against. The state summary passes a concise, structured digest instead of the full conversation — critical because the supervisor is called many times per request, so token savings multiply.

prompts.py — the supervisor system prompt
SUPERVISOR_PROMPT = """You are a support supervisor orchestrator.
Your ONLY function is to select the next worker from the approved list.
You must NEVER answer the user directly, NEVER call tools, and NEVER
generate content intended for the user, under any circumstances.

Approved workers: triage, tech_support, fraud_handler, FINISH

Decision rules:
  1. If the final response addresses every issue: return FINISH
  2. If a billing or technical issue is still unaddressed: return tech_support
  3. If suspicious activity is detected: return fraud_handler
  4. If delegation_count > 3: return FINISH
  Default to FINISH when in doubt.

Return only: {"next_worker": "<one approved value>"}"""
★ TRICK — the FINISH bias

"Default to FINISH when in doubt" is a cost-control lever, not sloppiness. Ambiguity that leans toward closure prevents extra routing calls on tickets that are effectively resolved. Keep the prompt clean of domain rules (no PII policy, no billing thresholds) — that would break the Purity Principle. The prompt persuades; delegation_count in code enforces.

2.10Building the node: seven stages

The supervisor node runs a fixed seven-stage cycle, with the LLM-touching stages wrapped so a failure degrades to a safe route instead of a crash.

1 · extract state fields 2 · check delegation limit 3 · build LLM (flash, T=0) 4 · prompt = rule + summary 5 · invoke → validated obj 6 · increment count 7 · return state update count ≥ MAX → force FINISH try / except → triage
fig 2.10 — the seven-stage cycle; stages 3–5 sit inside a try/except that falls back to triage.
supervisor.py — node + pure router
from langgraph.graph import END
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import SystemMessage, HumanMessage

MAX_DELEGATIONS = 10  # hard circuit breaker

def supervisor_node(state: SharedState) -> dict:
    # 1 - extract the fields the routing decision needs
    count = state.get("delegation_count", 0)

    # 2 - enforce the safety limit BEFORE any LLM call
    if count >= MAX_DELEGATIONS:
        return {"next_worker": "FINISH", "delegation_count": count}

    next_worker = "triage"  # safe default if the LLM path fails
    try:
        # 3 - fast, cheap, deterministic model
        llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)
        router = llm.with_structured_output(SupervisorDecision)

        # 4 - prompt = system prohibition + compact state summary
        messages = [
            SystemMessage(content=SUPERVISOR_PROMPT),
            HumanMessage(content=summarize_state(state)),
        ]

        # 5 - invoke; receive a VALIDATED SupervisorDecision object
        decision = router.invoke(messages)
        next_worker = decision.next_worker
    except Exception:
        next_worker = "triage"  # timeout / rate limit / bad schema

    # 6 + 7 - increment the count and return the state update
    return {"next_worker": next_worker, "delegation_count": count + 1}


def supervisor_router(state: SharedState) -> str:
    # PURE Python. No LLM. Reads the field the node already validated.
    worker = state["next_worker"]
    return END if worker == "FINISH" else worker
graph.py — hub-and-spoke wiring
builder.add_node("supervisor", supervisor_node)
builder.add_node("triage", triage_subgraph)
builder.add_node("tech_support", tech_support_subgraph)
builder.add_node("fraud_handler", fraud_subgraph)

builder.set_entry_point("supervisor")

# supervisor -> (worker | END), chosen by the pure router
builder.add_conditional_edges(
    "supervisor",
    supervisor_router,
    {
        "triage": "triage",
        "tech_support": "tech_support",
        "fraud_handler": "fraud_handler",
        END: END,
    },
)

# THE HUB INVARIANT: every worker returns to the supervisor
for worker in ("triage", "tech_support", "fraud_handler"):
    builder.add_edge(worker, "supervisor")
Why triage is the safe fallback

On any exception — API timeout, rate limit, schema validation failure — routing to triage is safest because the triage subgraph is built to accept any input and produce a classified state. The next supervisor pass can then route correctly. Falling back to FINISH would ship an unfinished answer; falling back to a specialist might feed it garbage.

2.11Auditability: one hub, total visibility

Because every transition passes through the supervisor, one node sees 100% of the execution flow. Instrument the hub and you get a complete, queryable audit trail for free.

Each decision records which worker was selected, which state was observed at selection time, the delegation count, and how long the LLM call took. That answers audit questions like "why did this complaint go to billing instead of tech?" down to the exact state. In a direct-handoff topology this is impossible — a mid-pipeline failure in one subgraph is invisible to every other node, and the user just gets no response. With a hub, that failure is visible: log it, increment an error counter, retry or escalate.

Eval frameworks

RAGAS (context/answer relevancy, precision, recall); DeepEval, which builds on G-Eval; Braintrust for systematic eval against representative inputs.

Tracing

LangSmith for agent evaluation and traces; OpenTelemetry for standardized, framework-agnostic observability across the whole app.

2.12Interview & practice Q&A

Why is a supervisor "closed for modification" but "open for extension"?

Adding a new agent means registering one subgraph in the master graph and updating the supervisor's approved-worker list — internal changes only. The external API contract, the client code, and the other subgraphs stay untouched. That is the Open-Closed Principle: the system extends without modifying the parts callers depend on.

When is semantic (LLM) routing actually justified?

Only when the input is raw and unclassified — typically the system entry point, where a brand-new, possibly multi-intent ticket arrives with no prior tag. Every downstream decision operates on a validated, typed field and should be pure Python. If you're paying the latency tax after the entry point, you're paying it for a decision that doesn't need language understanding.

Why enforce the routing key with a Literal instead of trusting the prompt?

Prompt instructions are probabilistic — they work with high probability, not certainty. At scale even a 0.3% failure produces dozens of graph crashes a day when the router can't match a free-text response to a node name. A Pydantic Literal plus structured output constrains the output at the API and validates it on parse, driving well-formed failures to near zero.

What breaks if the supervisor starts answering the user directly?

The hub collapses into a spoke. No node retains global coordination authority, so nothing guarantees every intent gets addressed or that findings are integrated before termination. It also destroys testability — a pure supervisor is tested by mocking state, while a contaminated one needs the whole system standing up.

Why must every worker return to the hub instead of routing to END?

A worker that signals END bypasses the supervisor's oversight — the supervisor never gets to check whether other intents (a billing dispute, a fraud signal) are still unaddressed in state. Returning to the hub lets the supervisor synthesize all findings and decide FINISH deliberately, which is what prevents premature closure on multi-intent tickets.

What is the difference between the prompt's delegation rule and the code's limit?

They are two layers. The prompt's rule (e.g. "if delegation_count > 3, return FINISH") is a soft, probabilistic nudge toward closure. The code's MAX_DELEGATIONS check (e.g. 10) is a hard circuit breaker that fires regardless of what the LLM decides, returning FINISH at zero extra cost. You need both: persuasion plus a guarantee against infinite loops.

Why fall back to triage on an LLM exception rather than FINISH?

Triage is designed to accept any input and produce a classified state, so a fallback there lets the next supervisor pass route correctly. Falling back to FINISH would ship an unfinished response to the customer; routing blindly to a specialist could feed it unclassified garbage. Triage is the only safe universal default.

How does the hub give you observability that direct hand-offs can't?

Every transition passes through one node, so instrumenting the supervisor captures 100% of the execution flow — which worker ran, the state at decision time, the delegation count, latency. In a spoke-to-spoke topology a mid-pipeline failure is local and invisible; the user simply gets no response and the audit trail fragments across nodes.

2.13Key takeaways

— The supervisor is a single, permanent entry point (the hub) that reads full state and decides the next worker; it routes, never executes.

— Static routing is right for ~70–80% of clean, single-intent tickets. The supervisor exists for the ambiguous, multi-intent, evolving rest.

— Deterministic Python routing on validated fields is $0, <1ms, 100% reliable. Reserve LLM routing for raw entry-point input; target 60–70% deterministic.

— Force output through a Pydantic Literal + structured output, and wrap the call in try/except defaulting to triage.

— Purity makes the supervisor testable and auditable; FINISH is an earned decision; delegation_count is the hard loop breaker.

— Every worker returns to the hub. No spoke-to-spoke. That single invariant buys oversight, integration, and a complete audit trail.

hub-and-spoke single endpoint open-closed deterministic vs semantic latency tax Pydantic Literal structured output Purity Principle FINISH earned delegation_count fallback = triage temp 0 / flash

3.0

Microexercises

Reading proves you recognize the pattern. These prove you can build it. Attempt each before opening the walkthrough.

3.1Build it — the decision model and pure router

// your task

Write the two pieces that turn an LLM's routing intent into a graph transition:

  • A Pydantic model SupervisorDecision whose next_worker can only be triage, tech_support, fraud_handler, or FINISH.
  • A pure function supervisor_router(state) that reads state["next_worker"] and returns the node name — except FINISH, which must return LangGraph's END. No LLM, no side effects.

Expected: supervisor_router({"next_worker": "tech_support"}) returns "tech_support"; supervisor_router({"next_worker": "FINISH"}) returns END.

Show the full walkthrough

Step 1 — constrain the output space. The router downstream will use the returned string as a dictionary key into the conditional-edge map. If the value isn't an exact node name, the graph raises KeyError. So the model's job is to make an invalid value impossible: a Literal lists every legal string, and Pydantic rejects anything else on construction.

Step 2 — keep the router pure. The routing decision was already made and validated in the node; the router only translates it into a transition. It reads one field and branches. The single special case is FINISH, which is not a node — it maps to the sentinel END.

solution
from typing import Literal
from pydantic import BaseModel
from langgraph.graph import END

class SupervisorDecision(BaseModel):
    next_worker: Literal["triage", "tech_support", "fraud_handler", "FINISH"]

def supervisor_router(state: dict) -> str:
    worker = state["next_worker"]
    return END if worker == "FINISH" else worker

# checks
assert supervisor_router({"next_worker": "tech_support"}) == "tech_support"
assert supervisor_router({"next_worker": "FINISH"}) == END

Why: the Literal enforces validity before the router runs, and the pure router stays trivially unit-testable — the two ideas that make the hub reliable.

3.2Diagnose it — the supervisor that never stops

// your task

This node runs, routes correctly, and returns to the hub — yet in production it eventually blows up on a hard multi-intent ticket. Name the root cause, predict the exact failure, and fix it.

buggy_supervisor.py
MAX_DELEGATIONS = 10

def supervisor_node(state: SharedState) -> dict:
    count = state.get("delegation_count", 0)
    if count >= MAX_DELEGATIONS:
        return {"next_worker": "FINISH", "delegation_count": count}

    llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)
    router = llm.with_structured_output(SupervisorDecision)
    decision = router.invoke([
        SystemMessage(content=SUPERVISOR_PROMPT),
        HumanMessage(content=summarize_state(state)),
    ])
    # returns the routing decision
    return {"next_worker": decision.next_worker}

Expected answer: the specific defect, the runtime symptom LangGraph will raise, and the one-line change that stops it.

Show the full walkthrough

Root cause: the node never increments delegation_count. It reads the count and checks the ceiling, but the returned state update omits delegation_count, so it stays at its initial value forever. The safety check at the top can therefore never trip.

Predict the failure: on a ticket the supervisor keeps re-routing (supervisor → tech → supervisor → tech …), the count is stuck, so the count >= MAX_DELEGATIONS guard never fires. The graph loops until LangGraph hits its own recursion ceiling and raises GraphRecursionError — after burning a full LLM call on every pointless lap. The circuit breaker was wired but never armed.

Step 1 — increment on every delegation. Step 2 — include the new count in the returned state so it actually persists.

fix
    # was: return {"next_worker": decision.next_worker}
    return {
        "next_worker": decision.next_worker,
        "delegation_count": count + 1,   # arm the circuit breaker
    }

(The original is also missing the try/except fallback to triage — a second production risk — but the loop is the defect that crashes here.)

Why: delegation_count is only a circuit breaker if it actually rises; a limit you never increment toward is decoration, not safety.

Literal blocks bad keys router stays pure FINISH → END increment every lap GraphRecursionError arm the breaker