Handoff protocol with conditional edges
A StateGraph is just a directed graph of nodes. The supervisor needs to decide, at runtime, which subagent runs next. LangGraph calls that a conditional edge: the edge's destination is chosen by a small function that reads state and returns a string.
Supervisor-routed graph topology
Every message enters through supervisor, routes to exactly one specialist, and terminates.
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
def _route_selector(state: ClaimState) -> str:
return state.get('route', 'policy')
def build_graph():
"""Compile the LangGraph StateGraph with an in-memory checkpointer."""
graph = StateGraph(ClaimState)
graph.add_node('supervisor', supervisor_node)
graph.add_node('policy_agent', policy_agent_node)
graph.add_node('billing_agent', billing_agent_node)
graph.add_node('claims_agent', claims_agent_node)
graph.add_node('escalation_agent', escalation_agent_node)
graph.add_edge(START, 'supervisor')
graph.add_conditional_edges(
'supervisor',
_route_selector,
{
'policy': 'policy_agent',
'billing': 'billing_agent',
'claims': 'claims_agent',
'escalation': 'escalation_agent',
},
)
for leaf in ('policy_agent', 'billing_agent', 'claims_agent', 'escalation_agent'):
graph.add_edge(leaf, END)
return graph.compile(checkpointer=MemorySaver())add_conditional_edges takes the source node, a selector function that reads state and returns a route name, and a dict that maps route names to destination nodes. The selector is dumb on purpose: it just reads the route field the supervisor already set. All the reasoning happens inside supervisor_node.
_compiled = None
def get_graph():
"""Return the compiled graph, compiling on first access.
Lazy compile means importing this module does not require API keys
or a running provider. Tests can import freely.
"""
global _compiled
if _compiled is None:
_compiled = build_graph()
return _compiledLazy compile matters. Without it, importing agent_graph in any test, lint step, or CI job would spin up the LLM provider and the checkpointer. With it, import is free and the graph only materializes when something actually calls get_graph().
Ordering exercise: Order the lifecycle of a single chat request through the graph
Loading practice…
Checkpoint: Supervisor routing checkpoint
Loading practice…