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.

agent_graph.py
python
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.

agent_graph.py
python
_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 _compiled

Lazy 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…