Concepts

CompiledGraph

Why langgraph-hierarchies wraps CompiledStateGraph — hooks, isolation, tool answers, context, and tracing.

compile_graph() does not return LangGraph’s CompiledStateGraph directly. It returns a CompiledGraph wrapper that owns the inner compiled graph and adds the behaviors required for production hierarchies.

Why not CompiledStateGraph alone?

LangGraph’s CompiledStateGraph is a runnable state machine. For decomposable agent hierarchies you also need:

  1. A hook pipeline that survives embedding as a subgraph node
  2. State isolation at subagent boundaries via SubagentPolicy
  3. Tool-answer emission so subgraph results appear as ToolMessage responses
  4. Context propagation so runtime.context.model flows through the tree
  5. Trace-safe embedding so nested graphs remain visible in LangGraph traces

CompiledGraph implements all five.

Hook pipeline

_build_runnable() assembles an explicit Runnable chain:

# Execution order (outermost first):
# aentry -> entry -> after_entry -> compiled -> aexit -> exit -> after_exit
sequenceDiagram
    participant Caller
    participant Aentry as aentry_hook
    participant Entry as entry_hook
    participant AfterEntry as after_entry_hook
    participant Inner as CompiledStateGraph
    participant Aexit as aexit_hook
    participant Exit as exit_hook
    participant AfterExit as after_exit_hook

    Caller->>Aentry: invoke(state)
    Aentry->>Entry: pass-through or async entry
    Entry->>AfterEntry: snapshot + policy + factory hook
    AfterEntry->>Inner: root defaults, reset iteration
    Inner->>Inner: graph nodes run
    Inner->>Aexit: final state
    Aexit->>Exit: pass-through or async exit
    Exit->>AfterExit: restore parent + factory hook
    AfterExit->>Caller: ToolMessage if as_tool
HookDefined onRole
aentry_hookCompiledGraph / factory aentry_hookAsync entry shim
entry_hookCompiledGraph + factory entry_hookSnapshot parent state, apply SubagentPolicy entry rules, call factory hook
after_entry_hookCompiledGraphApply _root_state_defaults, reset iteration_number
(inner graph)LangGraphActual agent execution
aexit_hookCompiledGraph / factory aexit_hookAsync exit shim
exit_hookCompiledGraph + factory exit_hookPop subagent stack, merge fields, call factory hook
after_exit_hookCompiledGraphEmit answering ToolMessage when invoked as a tool

Building hooks as a Runnable sequence (rather than calling them manually) is critical: when CompiledGraph is registered as a graph node, LangGraph flattens the chain via steps and every hook — especially after_exit_hook — still executes.

State isolation

When subagent_policy is set on the child factory, entry_hook and exit_hook manage the __subagent_stack__:

Entry: _snapshot_parent_state deep-copies the parent frame and pushes it onto the stack. _apply_entry_policy clears messages (and other fields per policy).

Exit: _apply_exit_policy pops the frame, restores the parent snapshot, copies merge_fields from the child, and drops discard_fields.

Factory-level entry_hook / exit_hook on your graph class run inside this envelope — use them for agent-specific setup, not for reimplementing isolation.

See SubagentPolicy for field-level control.

Tool-answer emission

When a subgraph is invoked as a tool (as_tool=True) and current_tool_call is set, after_exit_hook synthesizes the response the parent LLM expects:

tool_message = ToolMessage(
    content=state.get("current_agent_report", ""),
    name=tool_call["name"],
    tool_call_id=tool_call["id"],
)
state["messages"] = state.get("messages", []) + [tool_message]

Without this wrapper step, the parent reasoning node would see an unresolved tool call. The report content comes from empty_back or forced_exit inside the child ReactGraph.

Context propagation

Top-level invocation passes dependencies via context=:

result = root.invoke(
    create_base_state_defaults(),
    context=BaseContext(model=my_model),
)

CompiledGraph._prepare_config merges a LangGraph Runtime(context=...) into the config. Child subgraphs inherit the merged runtime, so runtime.context.model in ReactGraph.reasoning resolves without per-level wiring.

Trace-safe embedding

Each CompiledGraph gets a unique node_label (name + random suffix) when embedded in a parent. The steps property exposes the inner hook pipeline so trace utilities can locate the embedded Pregel without flattening the wrapper into disconnected steps.

node_label and as_tool

PropertyPurpose
node_labelInternal graph node name (unique per compile)
namePublic tool name the parent LLM calls
as_toolWhen True, child is exposed as an LLM tool and emits ToolMessage on exit
runnableFull hook pipeline — use for invoke / stream
_compiledRaw CompiledStateGraph — used when Pregel-specific kwargs are needed

Invocation surfaces

CompiledGraph implements invoke, ainvoke, stream, astream, and state APIs (get_state, update_state, …). Most callers use invoke / stream on the root CompiledGraph returned by compile_as_root().

Next

Subagents

How subagents are attached, invoked, and return results to the parent.