mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-31 04:09:49 +02:00
Fixes incorrect `Command.PARENT` bubbling when checkpoint namespaces include numeric task-disambiguation segments like `|1`. In some nested-invoke/fanout scenarios, the runtime inserts a purely-numeric namespace segment between `name:task_id` segments (e.g. `parent_first:<id>|1|node:<id>`). The previous ParentCommand rewrite logic only handled numeric segments at the end of the namespace, which could produce a malformed parent graph identifier (e.g. `parent_first:<id>|1`) and prevent the command from routing to the intended parent node. This change normalizes checkpoint namespaces by dropping numeric segments before computing the parent namespace in both sync and async retry paths. Added a minimal regression test that exercises the nested-invoke case and asserts that `Command(graph=Command.PARENT, goto=...)` reliably routes to the parent graph, regardless of whether the jump comes from the first or second nested invocation.
58 lines
1.9 KiB
Python
58 lines
1.9 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from langchain_core.runnables import RunnableConfig
|
|
from typing_extensions import TypedDict
|
|
|
|
from langgraph.graph import END, START, StateGraph
|
|
from langgraph.types import Command
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
|
|
async def test_parent_command_from_nested_subgraph() -> None:
|
|
class ParentState(TypedDict):
|
|
jump_from_idx: int
|
|
|
|
class ChildState(TypedDict):
|
|
jump: bool
|
|
|
|
child_builder: StateGraph[ChildState] = StateGraph(ChildState)
|
|
|
|
async def child_node(state: ChildState) -> Command | ChildState:
|
|
if state["jump"]:
|
|
return Command(graph=Command.PARENT, goto="parent_second")
|
|
return state
|
|
|
|
child_builder.add_node("node", child_node)
|
|
child_builder.add_edge(START, "node")
|
|
|
|
child_0 = child_builder.compile()
|
|
child_1 = child_builder.compile()
|
|
|
|
parent_builder: StateGraph[ParentState] = StateGraph(ParentState)
|
|
|
|
async def parent_first(state: ParentState, config: RunnableConfig) -> ParentState:
|
|
await child_0.ainvoke({"jump": state["jump_from_idx"] == 1}, config)
|
|
if state["jump_from_idx"] == 1:
|
|
raise AssertionError("Shouldn't be here")
|
|
|
|
await child_1.ainvoke({"jump": state["jump_from_idx"] == 2}, config)
|
|
if state["jump_from_idx"] == 2:
|
|
raise AssertionError("Shouldn't be here")
|
|
|
|
return state
|
|
|
|
async def parent_second(state: ParentState) -> ParentState:
|
|
return state
|
|
|
|
parent_builder.add_node("parent_first", parent_first)
|
|
parent_builder.add_node("parent_second", parent_second)
|
|
parent_builder.add_edge(START, "parent_first")
|
|
parent_builder.add_edge("parent_second", END)
|
|
|
|
graph = parent_builder.compile().with_config(recursion_limit=10)
|
|
|
|
assert await graph.ainvoke({"jump_from_idx": 1}) == {"jump_from_idx": 1}
|
|
assert await graph.ainvoke({"jump_from_idx": 2}) == {"jump_from_idx": 2}
|