You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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:
fromlangchain_core.messagesimportToolMessagefromag_ui_langgraph.utilsimportlangchain_messages_to_aguiout=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
Describe the Bug
Follow-up to #2226, split out of the review of #2263.
#2263 fixes the AG-UI → LangChain direction: an incoming
ToolMessage.errornow becomesstatus="error"on the LangChainToolMessage, so a client-reported tool failure reaches the model as a failure. The reverse direction is still lossy — neither adapter mapsstatus == "error"back onto AG-UI'sToolMessage.error:integrations/langgraph/typescript/src/utils.ts:306, thetoolbranch oflangchainMessagesToAgui:integrations/langgraph/python/ag_ui_langgraph/utils.py:248, theToolMessagebranch oflangchain_messages_to_agui:message.statusis never read in either, so every tool message emitted to the client comes back witherrorunset.Steps to Reproduce
ToolMessagewitherrorset to a LangGraph agent (theuseFrontendTool/ HITL path, or callagui_messages_to_langchaindirectly).ToolMessagecorrectly carriesstatus="error"and the model sees the failure. So far so good.MESSAGES_SNAPSHOT(or otherwise convert graph state back to AG-UI messages) and inspect the tool message the client receives:errorisNone/undefined.Direct exercise of the converter, no graph needed:
Expected Behavior
status == "error"on the LangChain side should produce an AG-UIToolMessagewhoseerrorfield is set, so the failure survives a full round trip.Why it matters
The graph checkpoint keeps
statusserver-side, so within a thread the model keeps seeing the failure. The loss shows up on the client's copy of the conversation:ToolMessage.errorcannot distinguish a failed frontend tool call from a successful one after a snapshot overwrites its local messages.agui_messages_to_langchainwitherrorunset, and fix(langgraph): carry ToolMessage.error onto the LangChain status flag #2263's fix silently producesstatus="success"again.Open design question
erroris typedOptional[str]— a message, not a boolean — and LangChain'sstatuscarries 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 theerrortext 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 intocontent) is what the reverse direction should read back out.Precedent worth looking at: the
claude-managed-agentsadapters already fold the text and the flag together in the forward direction —integrations/claude-managed-agents/typescript/src/agent.ts:30-32andintegrations/claude-managed-agents/python/ag_ui_claude_managed_agents/agent.py:521-532newline-joincontent+errorinto the tool result text and set the provider's error flag frombool(error).Environment