langchain-ai/langgraph · error · ValueError

Tool returned a list with messages bound to tool_call_id '…

Error message

Tool {name} returned a list with {count} messages bound to tool_call_id '{id}'; expected exactly one terminating ToolMessage.

What it means

Terminator check in _validate_tool_command: the tool returned a list of updates containing zero or more-than-one terminating ToolMessage bound to the tool call's id. Graph semantics require exactly one final ToolMessage per tool call so the message history stays valid for the LLM.

Solutions

  1. Return exactly one ToolMessage with tool_call_id matching the executed call
  2. If returning a Command, include a single terminating ToolMessage for the call id in Command.update
  3. Drop duplicate ToolMessages for the same tool_call_id from the returned list
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at libs/prebuilt/langgraph/prebuilt/tool_node.py:1485 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of langchain-ai/langgraph@38031739e5 (2026-08-26). Data as JSON: /api/errors/c379ffa3576a739a. Report an issue: GitHub.

Appendix: source

Thrown at libs/prebuilt/langgraph/prebuilt/tool_node.py:1485

        expected_id = tool_call["id"]

        terminator_count = 0
        for item in response:
            if isinstance(item, ToolMessage):
                if item.tool_call_id == expected_id:
                    terminator_count += 1
            elif isinstance(item, Command) and isinstance(item.update, dict):
                for msg in item.update.get(self._messages_key, []):
                    if isinstance(msg, ToolMessage) and msg.tool_call_id == expected_id:
                        terminator_count += 1

        if terminator_count != 1:
            msg = (
                f"Tool {tool_call['name']} returned a list with "
                f"{terminator_count} messages bound to tool_call_id "
                f"{expected_id!r}; expected exactly one terminating ToolMessage."
            )
            raise ValueError(msg)

        # Per-Command normalization still runs, but the list-level count above
        # already guarantees exactly one terminator, so individual Commands may
        # lack one.
        validated: list[Command | ToolMessage] = []
        for item in response:
            if isinstance(item, Command):
                validated.append(
                    self._validate_tool_command(
                        item, tool_call, input_type, require_terminator=False
                    )
                )
            else:
                item.content = cast("str | list", msg_content_output(item.content))
                validated.append(item)
        return validated

    def _validate_tool_command(

View on GitHub (pinned to 38031739e5)