Skip to content

[Bug]: LangGraph adapters drop ToolMessage status on the LangChain → AG-UI direction, so a tool failure is not durable across MESSAGES_SNAPSHOT #2305

Description

@contextablemark

Describe the Bug

Follow-up to #2226, split out of the review of #2263.

#2263 fixes the AG-UI → LangChain direction: an incoming ToolMessage.error now becomes status="error" on the LangChain ToolMessage, so a client-reported tool failure reaches the model as a failure. The reverse direction is still lossy — neither adapter maps status == "error" back onto AG-UI's ToolMessage.error:

  • TypeScript — integrations/langgraph/typescript/src/utils.ts:306, the tool branch of langchainMessagesToAgui:
case "tool":
  out.push({
    id: message.id!,
    role: "tool",
    content: stringifyIfNeeded(resolveMessageContent(message.content)),
    toolCallId: message.tool_call_id,
  });
  break;
  • Python — integrations/langgraph/python/ag_ui_langgraph/utils.py:248, the ToolMessage branch of langchain_messages_to_agui:
elif isinstance(message, ToolMessage):
    agui_messages.append(AGUIToolMessage(
        id=str(message.id),
        role="tool",
        content=stringify_if_needed(resolve_message_content(message.content)),
        tool_call_id=message.tool_call_id,
    ))

message.status is never read in either, so every tool message emitted to the client comes back with error unset.

Steps to Reproduce

  1. Send an AG-UI ToolMessage with error set to a LangGraph agent (the useFrontendTool / HITL path, or call agui_messages_to_langchain directly).
  2. With fix(langgraph): carry ToolMessage.error onto the LangChain status flag #2263 applied, the LangChain ToolMessage correctly carries status="error" and the model sees the failure. So far so good.
  3. Let the adapter emit a MESSAGES_SNAPSHOT (or otherwise convert graph state back to AG-UI messages) and inspect the tool message the client receives: error is None/undefined.

Direct exercise of the converter, no graph needed:

from langchain_core.messages import ToolMessage
from ag_ui_langgraph.utils import langchain_messages_to_agui

out = langchain_messages_to_agui([
    ToolMessage(id="t-1", content="Tool failed: invalid id", tool_call_id="tc-1", status="error")
])[0]
print(out.error)  # None -- expected "..." / some non-empty signal

Expected Behavior

status == "error" on the LangChain side should produce an AG-UI ToolMessage whose error field is set, so the failure survives a full round trip.

Why it matters

The graph checkpoint keeps status server-side, so within a thread the model keeps seeing the failure. The loss shows up on the client's copy of the conversation:

  • A client that renders ToolMessage.error cannot distinguish a failed frontend tool call from a successful one after a snapshot overwrites its local messages.
  • Any flow where the client's message list is the source of truth for the next run (a fresh thread, a stateless replay, a snapshot-then-resend) converts back through agui_messages_to_langchain with error unset, and fix(langgraph): carry ToolMessage.error onto the LangChain status flag #2263's fix silently produces status="success" again.

Open design question

error is typed Optional[str] — a message, not a boolean — and LangChain's status carries no text, so the reverse mapping has to invent one (a fixed sentinel like "error", or the tool content, or whatever the forward direction chose to stash). This is the mirror image of the "should the error text be preserved?" question in #2226, and the two are best decided together: whatever slot the forward direction writes the text into (additional_kwargs, or folded into content) is what the reverse direction should read back out.

Precedent worth looking at: the claude-managed-agents adapters already fold the text and the flag together in the forward direction — integrations/claude-managed-agents/typescript/src/agent.ts:30-32 and integrations/claude-managed-agents/python/ag_ui_claude_managed_agents/agent.py:521-532 newline-join content + error into the tool result text and set the provider's error flag from bool(error).

Environment

integrations/langgraph (TypeScript + Python), as of bb1c2afd
Related: #2226, #2263

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions