Skip to content

[Bug]: LangGraph adapters drop ToolMessage.error, so a client-reported tool failure reaches the model as a success #2226

Description

@GeauxEric

Description

ToolMessage.error is a first-class field of the AG-UI message schema, and the spec is explicit about its purpose — the messages concept page says verbatim error?: string // Optional error message if the tool execution failed and lists "Use error to indicate tool execution failures" in its key points.

Both first-party LangGraph adapters discard it when converting an incoming AG-UI tool message to a LangChain ToolMessage. LangChain has a field built for exactly this (status: Literal["success", "error"], defaulting to "success"), so the information has a destination — it is simply never carried across.

The result: a frontend tool that reports failure is delivered to the model as a successful tool result. The only thing distinguishing failure from success is whatever the client happened to put in content.

This is the one direction where the protocol can express failure. ToolCallResultEvent has no equivalent field (its documented properties are messageId, toolCallId, content, role), so agent→client failures are already untyped by design. Client→agent is the direction that has the field — and it is being dropped.

Where

Pythonintegrations/langgraph/python/ag_ui_langgraph/utils.py (agui_messages_to_langchain), as of ag-ui-langgraph 0.0.41 and unchanged in 0.0.42:

elif role == "tool":
    langchain_messages.append(ToolMessage(
        id=message.id,
        content=message.content,
        tool_call_id=message.tool_call_id,
    ))

message.error is never read. ag_ui/core/types.py defines it:

class ToolMessage(ConfiguredBaseModel):
    id: str
    role: Literal["tool"] = "tool"
    content: str
    tool_call_id: str
    error: Optional[str] = None          # <-- dropped
    encrypted_value: Optional[str] = None

TypeScriptintegrations/langgraph/typescript/src/agent.ts does not even model the field:

type ToolMessageFieldsWithToolCallId = {
  type?: string;
  tool_call_id: string;
  name?: string;
  content: unknown;
  id?: string;
};

There is no read of .error on an incoming tool message anywhere in that file, so the loss is not Python-specific.

Why it matters

LangChain providers already consume status. langchain_anthropic maps it onto the Anthropic tool-result flag:

"is_error": curr.status == "error",   # langchain_anthropic/chat_models.py

So the plumbing is complete end-to-end and broken by one omitted field in the adapter. On Anthropic, a client that correctly reports a failed frontend tool call still has that failure presented to the model as a success.

The impact is provider-dependent, which is probably why this has gone unnoticed: langchain_google_genai never reads status at all — a tool message becomes a function_response carrying content and name — so on Gemini the bug is invisible. It surfaces on providers that honor the flag.

Steps to reproduce

The converter can be exercised directly — no graph or model needed:

from ag_ui.core import ToolMessage as AguiToolMessage
from ag_ui_langgraph.utils import agui_messages_to_langchain

incoming = AguiToolMessage(
    id="t-1",
    role="tool",
    content="Tool failed: invalid id",
    tool_call_id="tc-1",
    error="invalid id",
)
out = agui_messages_to_langchain([incoming])[0]
print(repr(out.status), out.additional_kwargs)

Output on ag-ui-langgraph 0.0.41:

AG-UI in : {'id': 't-1', 'role': 'tool', 'content': 'Tool failed: invalid id',
            'tool_call_id': 'tc-1', 'error': 'invalid id', 'encrypted_value': None}
LangChain out: ToolMessage | status = 'success' | additional_kwargs = {}

Expected: status == "error".
Actual: status == "success" (the default), and error's text is not preserved
anywhere either — additional_kwargs is empty, so the failure is unrecoverable
downstream rather than merely untyped.

In a real run this is reached whenever a client answers a yielded frontend tool call with a
failure — the useFrontendTool / human-in-the-loop path — but the loss is entirely in the
converter, so the snippet above is sufficient.

Suggested fix

Map the presence of error onto LangChain's status in both adapters, e.g. in the Python converter:

elif role == "tool":
    langchain_messages.append(ToolMessage(
        id=message.id,
        content=message.content,
        tool_call_id=message.tool_call_id,
        status="error" if message.error else "success",
    ))

Two open questions for maintainers, since they affect the shape of the fix:

  • Should error's text be preserved as well as the flag? LangChain has no dedicated slot for it, so the options are to fold it into content (changes what the model reads) or to keep it in additional_kwargs (preserves round-tripping without altering the prompt). The flag alone is the minimum useful fix.
  • Should the reverse direction round-trip it? Since ToolCallResultEvent cannot express failure, a ToolMessage with status == "error" emitted back to the client currently loses the distinction, so error is not durable across a MESSAGES_SNAPSHOT. That may warrant a separate protocol discussion rather than being folded in here.

Documentation note

The spec is internally inconsistent about this field, which likely contributes to it being under-implemented: the messages concept page documents error and calls it out in its key points, while the tools concept page's "Tool Results" example omits it entirely. Aligning those would make the field harder to miss for other integrations.

Environment

  • ag-ui-protocol 0.1.19
  • ag-ui-langgraph 0.0.41 (verified unchanged in 0.0.42)
  • langchain-core 1.2.24

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions