diff --git a/langgraph-go/tests/test_primitives_test.go b/langgraph-go/tests/test_primitives_test.go index 238f3e475..1aa6f4486 100644 --- a/langgraph-go/tests/test_primitives_test.go +++ b/langgraph-go/tests/test_primitives_test.go @@ -73,3 +73,52 @@ func TestInputAndStatePrimitivesCompatible(t *testing.T) { t.Fatalf("unexpected logs: %#v", result.Logs) } } + +func (w *primitiveWorkflow) startNoFinishNode(ctx *ag.Context, _ any, state primitiveState) (ag.Command, error) { + state.Logs = append(state.Logs, "start") + return ag.Command{ + Update: state, + Goto: []ag.Send{ + {Node: w.middleNoFinishNode, NodeInput: "from_start"}, + }, + }, nil +} + +func (w *primitiveWorkflow) middleNoFinishNode(ctx *ag.Context, input string, state primitiveState) (ag.Command, error) { + state.Logs = append(state.Logs, "middle:"+input) + state.Count += 1 + state.Done = "stopped" + // No goto and no finish node configured: run should end automatically. + return ag.Command{Update: state}, nil +} + +func TestRunEndsWithoutFinishNode(t *testing.T) { + workflow := &primitiveWorkflow{} + graph := ag.NewAdvancedStateGraph[primitiveState]() + + graph.AddEntryNode(workflow.startNoFinishNode) + graph.AddNode(workflow.middleNoFinishNode) + + handler, err := graph.Compile().Start(nil, primitiveState{ + Count: 7, + Logs: []string{}, + Done: "", + }) + if err != nil { + t.Fatalf("start failed: %v", err) + } + + result, err := handler.WaitForResult() + if err != nil { + t.Fatalf("result failed: %v", err) + } + if result.Done != "stopped" { + t.Fatalf("unexpected done: %v", result.Done) + } + if result.Count != 8 { + t.Fatalf("unexpected count: %v", result.Count) + } + if len(result.Logs) != 2 || result.Logs[0] != "start" || result.Logs[1] != "middle:from_start" { + t.Fatalf("unexpected logs: %#v", result.Logs) + } +} diff --git a/libs/langgraph/langgraph/advanced_graph/state.py b/libs/langgraph/langgraph/advanced_graph/state.py index 4d5a2198e..98a1a0027 100644 --- a/libs/langgraph/langgraph/advanced_graph/state.py +++ b/libs/langgraph/langgraph/advanced_graph/state.py @@ -130,11 +130,9 @@ class AdvancedStateGraph(Generic[StateT]): def compile(self) -> CompiledGraphEngine[StateT]: if self._entry_point is None: raise ValueError("Entry point is not set") - if self._finish_point is None: - raise ValueError("Finish point is not set") if self._entry_point not in self._nodes: raise ValueError(f"Entry point node `{self._entry_point}` does not exist") - if self._finish_point not in self._nodes: + if self._finish_point is not None and self._finish_point not in self._nodes: raise ValueError(f"Finish point node `{self._finish_point}` does not exist") return CompiledGraphEngine( nodes=dict(self._nodes), @@ -153,7 +151,7 @@ class CompiledGraphEngine(Generic[StateT]): nodes: dict[str, Callable[..., Any]], async_channels: dict[str, _ChannelSpec], entry_point: str, - finish_point: str, + finish_point: str | None, ) -> None: self._nodes = nodes self._async_channels = async_channels @@ -220,7 +218,7 @@ class _GraphEngineRun: nodes: dict[str, Callable[..., Any]], async_channel_specs: dict[str, _ChannelSpec], entry_point: str, - finish_point: str, + finish_point: str | None, ) -> None: self._nodes = nodes self._entry_point = entry_point @@ -235,12 +233,13 @@ class _GraphEngineRun: self.context = Context(self) async def run(self, initial_state: StateT) -> StateT: + finish_point = self._finish_point or "" loop = asyncio.get_running_loop() result_obj = await loop.run_in_executor( _advanced_graph_executor(), self._rust_engine.run_graph_py, self._entry_point, - self._finish_point, + finish_point, initial_state, self._execute_node_for_rust, ) diff --git a/libs/langgraph/tests/advanced-graph/test_primitives.py b/libs/langgraph/tests/advanced-graph/test_primitives.py index ddc41af59..2248a3102 100644 --- a/libs/langgraph/tests/advanced-graph/test_primitives.py +++ b/libs/langgraph/tests/advanced-graph/test_primitives.py @@ -44,3 +44,23 @@ async def test_input_and_state_primitives_are_compatible() -> None: "middle:input=from_start", "finish:input=from_middle", ] + + +async def test_run_ends_without_finish_node() -> None: + graph = AdvancedStateGraph(PrimitiveState) + + async def start_node(state: PrimitiveState) -> Command: + state["logs"].append("start") + return Command(update=state, goto=Send("middle_node", "from_start")) + + async def middle_node(input: str, state: PrimitiveState) -> dict[str, object]: + state["logs"].append(f"middle:{input}") + return {"counter": state["counter"] + 1, "logs": state["logs"], "done": "stopped"} + + graph.add_entry_node(start_node) + graph.add_node(middle_node) + + result = await graph.compile().ainvoke({"counter": 7, "logs": [], "done": None}) + assert result["counter"] == 8 + assert result["done"] == "stopped" + assert result["logs"] == ["start", "middle:from_start"]