mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-25 09:02:25 +02:00
## Summary Extends `ToolNode` so that a single tool invocation can return `list[Command | ToolMessage]` instead of only a single `Command` or `ToolMessage`. This brings `ToolNode`'s per-tool-call contract in line with the rest of LangGraph, where nodes can already return multiple Commands. Depends on langchain-ai/langchain#36963 which allows `list[ToolOutputMixin]` to pass through `BaseTool._format_output` unchanged. ## Changes ### `libs/prebuilt/langgraph/prebuilt/tool_node.py` **New list-return gate in `_execute_tool_sync` / `_execute_tool_async`** — After the existing `Command` and `ToolMessage` checks, a new branch accepts `list[Command | ToolMessage]` and routes it through `_validate_tool_command_list`. Lists with non-`Command`/`ToolMessage` elements raise `TypeError`. Both sync and async paths are updated symmetrically. **`_validate_tool_command_list`** — Enforces the terminating-ToolMessage rule: exactly one `ToolMessage` in the list must carry `tool_call_id == <outer_id>` (top-level or nested inside a `Command.update["messages"]`). Zero or multiple terminators raise `_MissingToolMessageError`. Individual Commands in the list are validated via the existing `_validate_tool_command`; when a Command lacks the terminator (which is allowed since the list-level check handles it), the `_MissingToolMessageError` is caught and the already-normalized command from the exception is used. **`_MissingToolMessageError`** — A `ValueError` subclass raised by `_validate_tool_command` (and `_validate_tool_command_list`) when no matching `ToolMessage` is found. Carries the already-normalized command so callers can recover without re-doing deepcopy/message-conversion work. Using a typed exception avoids brittle string-matching on error messages. **`_combine_tool_outputs`** — Flattens list entries at the top of the method so downstream combiner logic (parent-`goto` accumulation, ToolMessage wrapping) is unchanged. **Response processing moved inside try/except** — In both sync and async execute methods, the response validation (Command/ToolMessage/list checks) now runs inside the existing error-handling try block, so validation errors from the list path go through `_handle_tool_errors` like other tool errors. **Return type signatures** widened on `_execute_tool_sync`, `_execute_tool_async`, `_run_one`, `_arun_one` to include `list[Command | ToolMessage]`. ### `libs/prebuilt/tests/test_tool_node.py` New tests covering: valid list returns (top-level terminator, nested terminator, parent-goto + terminator), regression tests for single Command/ToolMessage returns, invalid cases (no terminator, multiple terminators), async parity, integration with mixed list/non-list tool calls, and `_handle_tool_errors` interaction. --------- Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>