no-finish-node

This commit is contained in:
Quanzheng Long
2026-03-16 15:55:35 -07:00
parent 76bee17ec4
commit 4f5b775819
3 changed files with 74 additions and 6 deletions
@@ -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)
}
}
@@ -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,
)
@@ -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"]