cleveractors.langgraph provides no StateGraph bridge — host applications must walk CompiledActor nodes manually #8

Open
opened 2026-05-22 06:58:31 +00:00 by hurui200320 · 0 comments
Member

Summary

cleveractors.langgraph exposes NodeConfig, Edge, GraphState, and a single-node Node executor, but no function that assembles a compiled actor into a native LangGraph StateGraph. Every host application that wants to run a CompiledActor must re-implement the same node-walking loop by hand.

langgraph>=0.2.0 is already a declared runtime dependency of this library (see pyproject.toml) yet is never imported anywhere in the source tree. The dependency is deadweight until this bridge exists.

Metadata

  • Commit message: feat(langgraph): add build_state_graph() to assemble CompiledActor into a native StateGraph
  • Branch: feat/langgraph-state-graph-bridge

Details

What the library currently provides

compile_actor() produces a CompiledActor — a typed, validated data bundle:

class CompiledActor(BaseModel):
    name: str
    nodes: dict[str, NodeConfig]   # library's own data class
    edges: list[Edge]              # library's own data class
    entry_point: str
    metadata: CompilationMetadata  # includes exit_nodes

Node.execute(state) runs a single NodeConfig against a GraphState and returns a partial-update dict. That is the full extent of the execution surface. There is no function that wires these pieces into a running StateGraph.

What hosts have to do instead

Every consumer must hand-roll the same loop:

current: str | None = actor.entry_point
exit_set = set(actor.metadata.exit_nodes)
while current is not None:
    nc = actor.nodes[current]
    result = await Node(config=nc, provider_registry=registry).execute(state)
    state.update(result)
    if current in exit_set:
        break
    next_edge = next((e for e in actor.edges if e.source == current), None)
    current = next_edge.target if next_edge else None

This loop silently ignores conditional routing, does not compose with LangGraph's checkpointing, streaming, or interrupt APIs, and has to be duplicated in every host that uses the library.

Impact

  • Every host application carries an ad-hoc node-walking loop that belongs in the library.
  • langgraph>=0.2.0 is a declared runtime dependency that is never exercised — it provides no value until this bridge is added.
  • The name cleveractors.langgraph implies LangGraph integration but stops at data types; the name becomes accurate only once the bridge exists.
  • NodeType.CONDITIONAL nodes cannot be wired to add_conditional_edges() in the manual loop, so conditional branches are silently dropped.

Proposed API

A single new public function in src/cleveractors/langgraph/builder.py:

from langgraph.graph.state import CompiledStateGraph

def build_state_graph(
    actor: CompiledActor,
    provider_registry: ProviderRegistryPort,
) -> CompiledStateGraph:
    """Assemble a CompiledActor into a runnable LangGraph StateGraph.

    Args:
        actor: A compiled actor bundle produced by compile_actor().
        provider_registry: The host's provider registry, used to resolve
            (provider, model) pairs into Agent instances at execution time.

    Returns:
        A compiled LangGraph StateGraph ready for ainvoke() / astream().
        The state schema is ActorState; hosts that need advanced control
        (checkpointing, custom reducers, interrupt) may inspect or extend
        the returned graph before calling .compile() themselves.
    """

A new public ActorState TypedDict is exported from cleveractors.langgraph so hosts can type-annotate their own code against the state schema:

# cleveractors/langgraph/__init__.py
from .builder import ActorState, build_state_graph

Usage after the bridge exists:

from cleveractors.langgraph import build_state_graph

runnable = build_state_graph(restored, registry)
result = await runnable.ainvoke({
    "messages": [{"role": "user", "content": "Who are you"}]
})

Implementation notes

Four mapping problems must be solved:

1. State schema — LangGraph requires a TypedDict with reducer annotations. GraphState is a Pydantic model. A public ActorState TypedDict adapter is defined in builder.py and exported:

import operator
from typing import Annotated, TypedDict

class ActorState(TypedDict):
    messages: Annotated[list[dict], operator.add]
    metadata: dict[str, Any]
    current_node: str | None

2. Node wrapper — Each LangGraph node function receives ActorState and must return a partial-update dict. Node.execute() expects a GraphState, so each wrapper converts between the two:

def _make_node_fn(nc: NodeConfig, registry: ProviderRegistryPort):
    node = Node(config=nc, provider_registry=registry)
    async def _fn(state: ActorState) -> dict[str, Any]:
        gs = GraphState(
            messages=state["messages"],
            metadata=state.get("metadata", {}),
            current_node=state.get("current_node"),
        )
        return await node.execute(gs)
    return _fn

3. Conditional edgesNodeType.CONDITIONAL nodes produce a condition_result: bool update. They are wired via add_conditional_edges() using a routing function that reads condition_result from state:

graph.add_conditional_edges(
    node_id,
    lambda state: true_target if state.get("condition_result") else false_target,
)

Full expression-based routing (multi-branch conditions, field comparisons) is out of scope for this ticket and will be addressed in a follow-up if required.

4. Exit nodes → ENDactor.metadata.exit_nodes must each receive an explicit add_edge(exit_id, END) call; LangGraph has no implicit termination.

New files

  • src/cleveractors/langgraph/builder.pyActorState and build_state_graph()
  • docs/adr/ADR-006-langgraph-stategraph-execution-bridge.md — records the decision to expose a first-class LangGraph execution bridge, the ActorState TypedDict contract, the build_state_graph public API shape, and the rationale for scoping conditional routing to boolean condition_result only

Definition of Done

  • ActorState TypedDict defined in src/cleveractors/langgraph/builder.py with Annotated[list[dict], operator.add] reducer on messages
  • build_state_graph(actor, registry) -> CompiledStateGraph implemented in src/cleveractors/langgraph/builder.py
  • Each CompiledActor.nodes entry wrapped as an async LangGraph node function that converts ActorState ↔ GraphState
  • Plain edges wired via graph.add_edge()
  • NodeType.CONDITIONAL nodes wired via graph.add_conditional_edges() using boolean condition_result routing; complex expression routing explicitly deferred to a follow-up ticket
  • Each exit node wired to END
  • ActorState and build_state_graph added to cleveractors/langgraph/__init__.py and its __all__
  • BDD scenario: linear two-node graph executes to completion via build_state_graph
  • BDD scenario: conditional branch routes to the correct target based on condition_result
  • BDD scenario: exit node terminates the graph (no further nodes executed)
  • docs/adr/ADR-006-langgraph-stategraph-execution-bridge.md written, covering API contract, ActorState schema, conditional routing scope decision, and rationale
  • docs/reference/actor_compiler.md updated with a "Running a compiled actor" section documenting build_state_graph and ActorState
  • docs/specification.md updated to list ActorState and build_state_graph in the public surface table
# Summary `cleveractors.langgraph` exposes `NodeConfig`, `Edge`, `GraphState`, and a single-node `Node` executor, but no function that assembles a compiled actor into a native LangGraph `StateGraph`. Every host application that wants to run a `CompiledActor` must re-implement the same node-walking loop by hand. `langgraph>=0.2.0` is already a declared **runtime** dependency of this library (see `pyproject.toml`) yet is never imported anywhere in the source tree. The dependency is deadweight until this bridge exists. # Metadata - Commit message: `feat(langgraph): add build_state_graph() to assemble CompiledActor into a native StateGraph` - Branch: `feat/langgraph-state-graph-bridge` # Details ## What the library currently provides `compile_actor()` produces a `CompiledActor` — a typed, validated data bundle: ```python class CompiledActor(BaseModel): name: str nodes: dict[str, NodeConfig] # library's own data class edges: list[Edge] # library's own data class entry_point: str metadata: CompilationMetadata # includes exit_nodes ``` `Node.execute(state)` runs a **single** `NodeConfig` against a `GraphState` and returns a partial-update dict. That is the full extent of the execution surface. There is no function that wires these pieces into a running `StateGraph`. ## What hosts have to do instead Every consumer must hand-roll the same loop: ```python current: str | None = actor.entry_point exit_set = set(actor.metadata.exit_nodes) while current is not None: nc = actor.nodes[current] result = await Node(config=nc, provider_registry=registry).execute(state) state.update(result) if current in exit_set: break next_edge = next((e for e in actor.edges if e.source == current), None) current = next_edge.target if next_edge else None ``` This loop silently ignores conditional routing, does not compose with LangGraph's checkpointing, streaming, or interrupt APIs, and has to be duplicated in every host that uses the library. ## Impact - Every host application carries an ad-hoc node-walking loop that belongs in the library. - `langgraph>=0.2.0` is a declared runtime dependency that is never exercised — it provides no value until this bridge is added. - The name `cleveractors.langgraph` implies LangGraph integration but stops at data types; the name becomes accurate only once the bridge exists. - `NodeType.CONDITIONAL` nodes cannot be wired to `add_conditional_edges()` in the manual loop, so conditional branches are silently dropped. ## Proposed API A single new public function in `src/cleveractors/langgraph/builder.py`: ```python from langgraph.graph.state import CompiledStateGraph def build_state_graph( actor: CompiledActor, provider_registry: ProviderRegistryPort, ) -> CompiledStateGraph: """Assemble a CompiledActor into a runnable LangGraph StateGraph. Args: actor: A compiled actor bundle produced by compile_actor(). provider_registry: The host's provider registry, used to resolve (provider, model) pairs into Agent instances at execution time. Returns: A compiled LangGraph StateGraph ready for ainvoke() / astream(). The state schema is ActorState; hosts that need advanced control (checkpointing, custom reducers, interrupt) may inspect or extend the returned graph before calling .compile() themselves. """ ``` A new public `ActorState` TypedDict is exported from `cleveractors.langgraph` so hosts can type-annotate their own code against the state schema: ```python # cleveractors/langgraph/__init__.py from .builder import ActorState, build_state_graph ``` Usage after the bridge exists: ```python from cleveractors.langgraph import build_state_graph runnable = build_state_graph(restored, registry) result = await runnable.ainvoke({ "messages": [{"role": "user", "content": "Who are you"}] }) ``` ## Implementation notes Four mapping problems must be solved: **1. State schema** — LangGraph requires a TypedDict with reducer annotations. `GraphState` is a Pydantic model. A public `ActorState` TypedDict adapter is defined in `builder.py` and exported: ```python import operator from typing import Annotated, TypedDict class ActorState(TypedDict): messages: Annotated[list[dict], operator.add] metadata: dict[str, Any] current_node: str | None ``` **2. Node wrapper** — Each LangGraph node function receives `ActorState` and must return a partial-update dict. `Node.execute()` expects a `GraphState`, so each wrapper converts between the two: ```python def _make_node_fn(nc: NodeConfig, registry: ProviderRegistryPort): node = Node(config=nc, provider_registry=registry) async def _fn(state: ActorState) -> dict[str, Any]: gs = GraphState( messages=state["messages"], metadata=state.get("metadata", {}), current_node=state.get("current_node"), ) return await node.execute(gs) return _fn ``` **3. Conditional edges** — `NodeType.CONDITIONAL` nodes produce a `condition_result: bool` update. They are wired via `add_conditional_edges()` using a routing function that reads `condition_result` from state: ```python graph.add_conditional_edges( node_id, lambda state: true_target if state.get("condition_result") else false_target, ) ``` Full expression-based routing (multi-branch conditions, field comparisons) is **out of scope** for this ticket and will be addressed in a follow-up if required. **4. Exit nodes → `END`** — `actor.metadata.exit_nodes` must each receive an explicit `add_edge(exit_id, END)` call; LangGraph has no implicit termination. ## New files - `src/cleveractors/langgraph/builder.py` — `ActorState` and `build_state_graph()` - `docs/adr/ADR-006-langgraph-stategraph-execution-bridge.md` — records the decision to expose a first-class LangGraph execution bridge, the `ActorState` TypedDict contract, the `build_state_graph` public API shape, and the rationale for scoping conditional routing to boolean `condition_result` only # Definition of Done - [ ] `ActorState` TypedDict defined in `src/cleveractors/langgraph/builder.py` with `Annotated[list[dict], operator.add]` reducer on `messages` - [ ] `build_state_graph(actor, registry) -> CompiledStateGraph` implemented in `src/cleveractors/langgraph/builder.py` - [ ] Each `CompiledActor.nodes` entry wrapped as an async LangGraph node function that converts `ActorState ↔ GraphState` - [ ] Plain edges wired via `graph.add_edge()` - [ ] `NodeType.CONDITIONAL` nodes wired via `graph.add_conditional_edges()` using boolean `condition_result` routing; complex expression routing explicitly deferred to a follow-up ticket - [ ] Each exit node wired to `END` - [ ] `ActorState` and `build_state_graph` added to `cleveractors/langgraph/__init__.py` and its `__all__` - [ ] BDD scenario: linear two-node graph executes to completion via `build_state_graph` - [ ] BDD scenario: conditional branch routes to the correct target based on `condition_result` - [ ] BDD scenario: exit node terminates the graph (no further nodes executed) - [ ] `docs/adr/ADR-006-langgraph-stategraph-execution-bridge.md` written, covering API contract, `ActorState` schema, conditional routing scope decision, and rationale - [ ] `docs/reference/actor_compiler.md` updated with a "Running a compiled actor" section documenting `build_state_graph` and `ActorState` - [ ] `docs/specification.md` updated to list `ActorState` and `build_state_graph` in the public surface table
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveractors-core#8
No description provided.