Skip to content

RemoteGraph and subgraph nesting: using a compiled graph as a node

源码版本1.2.9

Responsibilities

LangGraph lets you treat a compiled graph — either a local Pregel instance or a remote RemoteGraph — as a node inside another graph. RemoteGraph (RemoteGraph:118) is the client implementation of PregelProtocol, calling remotely-deployed graphs through the LangGraph Server API. The local StateGraph.compile() product is itself a Pregel (also a PregelProtocol), so you can directly parent_graph.add_node(child_graph) to nest it.

This layer is responsible for abstracting away "nested graph": when the parent graph schedules this node, whether the backing instance is a local Pregel or a remote HTTP API, it goes through the same invoke / stream interface. PregelNode uses find_subgraph_pregel (find_subgraph_pregel:47) to recursively locate the Pregel instance inside a node's runnable and attaches it to node.subgraphs, so the parent's get_subgraphs can enumerate the subgraph and stream(subgraphs=True) can see subgraph-internal events.

Design motivation

  • Distributed deployment: Large graphs are often split into multiple services (conversation management, retrieval, tool calling), each a LangGraph deployment. The parent graph strings them into a workflow via RemoteGraph. Each service can scale and upgrade independently without redeploying the parent.
  • Independent nested checkpoints: After add_node(compiled_graph), the subgraph can be configured with its own checkpointer (compile(checkpointer=...)) or inherit from the parent. The subgraph's thread_id is auto-prefixed with a namespace by the parent via CONFIG_KEY_CHECKPOINT_NS (parent_node:<task_id>|child_node:<task_id>), so parent and child checkpoints never collide.
  • PregelProtocol unified interface: Both local Pregel and RemoteGraph implement PregelProtocolinvoke / stream / astream / get_state / aget_state / get_subgraphs share the same signatures. The parent does not need to treat them differently.
  • Stream pass-through: With subgraphs=True enabled, the parent's _output tags events with a namespace prefix on dequeue (get_subgraphs:1076); the caller gets (ns, mode, payload) triples and can tell which node at which nesting level an event came from.
  • Command.PARENT across levels: A subgraph node can return Command(graph=Command.PARENT, update=...) to write updates to the parent's state — passed via pending writes between parent and subgraph, and processed by the parent's _first at finalization.

Key files

  • RemoteGraph:118RemoteGraph(PregelProtocol) client; holds assistant_id / client / sync_client / name.
  • RemoteGraph.__init__:132 — Accepts url / api_key / headers / client / sync_client; defaults to creating via get_client / get_sync_client.
  • RemoteGraph.stream:757 — Sync streaming entry; forwards the request to sync_client.run_stream.
  • RemoteGraph.astream:912 — Async streaming entry; forwards to the client.run_stream async iterator.
  • RemoteGraph.invoke:1133 — Sync collapsing entry; internally goes through stream to take the last value.
  • find_subgraph_pregel:47 — Recursively searches a node's bound runnable for PregelProtocol instances to identify subgraphs.
  • PregelNode:97 — The PregelNode class; holds a subgraphs: Sequence[PregelProtocol] field.
  • PregelNode subgraphs init:180PregelNode.__init__ calls find_subgraph_pregel(self.bound) to auto-identify subgraphs.
  • get_subgraphs:1076 — Parent enumerates direct subgraphs; recurses downward when recurse=True.
  • attach_node:1431 — At compile time, turns the add_node-registered action into PregelNode(bound=node.runnable, ...); subgraph identification happens here.
  • coerce_to_runnable:529 — Any Runnable instance (including Pregel / RemoteGraph) is returned directly — this is the entry point for "compiled graph as node".

Data flow

add_node(compiled_child_graph) goes through coerce_to_runnable (coerce_to_runnable:529):

python
def coerce_to_runnable(
    thing: RunnableLike, *, name: str | None, trace: bool
) -> Runnable:
    """Coerce a runnable-like object into a Runnable."""
    if isinstance(thing, Runnable):
        return thing
    elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
        return RunnableLambda(thing, name=name)
    elif callable(thing):
        # ...
    elif isinstance(thing, dict):
        return RunnableParallel(thing)
    else:
        raise TypeError(...)

Both Pregel and RemoteGraph inherit from Runnable, so they hit the first branch and return directly. PregelNode.__init__ calls find_subgraph_pregel(self.bound) during the attach_node phase (find_subgraph_pregel:47):

python
def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
    from langgraph.pregel import Pregel

    candidates: list[Runnable] = [candidate]

    for c in candidates:
        if (
            isinstance(c, PregelProtocol)
            # subgraphs that disabled checkpointing are not considered
            and (not isinstance(c, Pregel) or c.checkpointer is not False)
        ):
            return c
        elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
            candidates.extend(c.steps)
        elif isinstance(c, RunnableLambda):
            candidates.extend(c.deps)
        elif isinstance(c, RunnableCallable):
            if c.func is not None:
                candidates.extend(
                    nl.__self__ if hasattr(nl, "__self__") else nl
                    for nl in get_function_nonlocals(c.func)
                )
            elif c.afunc is not None:
                candidates.extend(
                    nl.__self__ if hasattr(nl, "__self__") else nl
                    for nl in get_function_nonlocals(c.afunc)
                )

    return None

It recurses through RunnableSequence / RunnableLambda / RunnableCallable — even a node wrapped in a RunnableLambda that calls Pregel is recognized. The identified subgraph is attached to self.subgraphs = [subgraph], so the parent's get_subgraphs() (get_subgraphs:1076) can enumerate it.

At execution time, the parent's PregelRunner.tick calls the subgraph Pregel's stream / astream (or the corresponding RemoteGraph methods). The subgraph runs its own Pregel loop internally, and the events it produces are passed through by the parent's stream(subgraphs=True) — the parent prefixes CONFIG_KEY_CHECKPOINT_NS with the node name when calling the subgraph, the subgraph's events carry that namespace when written back to the parent's stream queue, and the parent's _output merges them with local events on dequeue.

Boundaries and failure modes

  • Subgraphs with checkpointing disabled are not recognized: find_subgraph_pregel explicitly skips Pregels with c.checkpointer is False (skip no-checkpointer:54). Such "stateless subgraphs" are not treated as subgraphs — the parent's get_subgraphs does not see them, and their events do not carry a namespace.
  • RemoteGraph requires at least url or client: In __init__, when both client and sync_client are None and no url was passed, neither client gets created, and a subsequent _validate_client() call raises ValueError (validate client:181).
  • RemoteGraph does not support every stream_mode: _reject_v3_unsupported rejects certain parameter combinations; the remote API's stream protocol has more restrictions than local Pregel (reject v3:195).
  • Nested namespaces use NS_SEP: The namespace format is parent_node:<task_id> + NS_SEP + child_node:<task_id>, recursing downward. add_node rejects node names containing NS_SEP (reject NS_SEP:794) to avoid namespace ambiguity.
  • Subgraph interrupt() propagates along the namespace: When a subgraph node's interrupt() raises GraphInterrupt, the parent's tick detects the subgraph's pending interrupt, and the parent also enters an interrupt state. On client resume, Command(resume=...) is routed to the corresponding subgraph by namespace via CONFIG_KEY_RESUME_MAP (Command resume mapping:904).
  • Command.PARENT boundary: When a subgraph node does return Command(graph=Command.PARENT, ...), the subgraph's map_command directly raises InvalidUpdateError("There is no parent graph") (PARENT guard:58). This is actually a fallback — the normal path is that the subgraph's _get_updates mapper at attach_node time skips Command.PARENT and writes it verbatim into the subgraph's pending writes; the parent recognizes and routes it inside _first.

Summary

RemoteGraph lets LangGraph support cross-service, cross-process graph nesting, while local Pregel nesting is auto-identified via find_subgraph_pregel — both go through PregelProtocol. The parent's stream(subgraphs=True) passes subgraph events through via namespace. For subgraph event-stream details see /stream/run-stream; for subgraph node scheduling see /pregel/pregel.

See official docs: LangGraph docs · README.