fix(langgraph): map LangChain tool status back onto AG-UI ToolMessage.error - #2316
Conversation
….error The reverse conversion (langchainMessagesToAgui / langchain_messages_to_agui) dropped the LangChain tool result status, so a status='error' tool message came back to the client with error unset. After ag-ui-protocol#2263 fixed the forward direction, a client whose message list seeds the next run (fresh thread, stateless replay, snapshot-then-resend) re-converted with error unset and silently produced status='success' again, undoing the fix. Map status == 'error' back onto AG-UI's error in both adapters. The forward direction is flag-only, so the original text is not recoverable here; error carries a fixed sentinel so a client can tell a reported failure apart from a reported failure text. Flag durable, text best-effort, per the standard agreed on ag-ui-protocol#2306. A test is added on each side. Fixes ag-ui-protocol#2305
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1785903854' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785903854' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785903854' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1785903854' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785903854' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785903854' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1785903854
Commit: 183589c |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
BenTaylorDev
left a comment
There was a problem hiding this comment.
Approving. CI is green across all 42 checks on 58ee240d, and the change holds up under review.
What I verified beyond the diff:
- The design is the one that was asked for. The PR cites the standard agreed on #2306, so I read both threads rather than taking the framing. The rule there is "set the framework's error flag from
bool(error); fold the text in only where the target has no flag", and on #2305 the preference for the synthesized value was stated explicitly: a fixed sentinel over echoingcontent. This matches both. - Round trip against real
langchain_core(1.2.13), not mocks.status="error"→error='error'→ back tostatus='error'; the success path stayserror=None→status='success'. The flag is durable in both directions. - Types line up.
@langchain/langgraph-sdk@1.7.2declaresToolMessage.status?: "error" | "success", and AG-UI'sToolMessage.erroris optional string in both SDKs. - Merges clean with current
main(78 commits on, nothing touching either edited file), and the full Python suite passes at 385. - No missed sites. The other four
role: "tool"constructions in these adapters buildTOOL_CALL_RESULTevents, andToolCallResultEventhas noerrorfield in either SDK — nothing to carry there.
Two follow-ups worth tracking separately, neither blocking:
- That last point is a real limit on the fix's reach: the flag survives a
MESSAGES_SNAPSHOTbut the streamedTOOL_CALL_RESULTevent has nowhere to put it. So "the flag is durable" holds for snapshots, not for the streaming path. Might deserve its own issue against the protocol rather than the adapter. - The langgraph TypeScript package has no
check-typesscript and vitest is transpile-only, so a green suite here isn't type evidence. Not a problem with this PR — just worth knowing what the signal does and doesn't cover.
Thanks for the care on this one, particularly for raising the text-slot question on #2306 first and mirroring the answer in both directions instead of guessing.
Fixes #2305.
Problem
#2263 fixed the forward direction (AG-UI → LangChain): an incoming
ToolMessage.errorbecomesstatus="error". The reverse direction was still lossy —langchainMessagesToAgui/langchain_messages_to_aguinever readstatus, so a LangChain tool result withstatus="error"came back to the client witherrorunset.This undoes #2263's fix for any flow where the client's message list seeds the next run (a fresh thread, a stateless replay, a snapshot-then-resend): the client re-converts through the forward path with
errorunset, and silently getsstatus="success"again.Fix
Map
status == "error"back onto AG-UI'serrorin both adapters:integrations/langgraph/typescript/src/utils.ts, thetoolbranch oflangchainMessagesToAgui.integrations/langgraph/python/ag_ui_langgraph/utils.py, theToolMessagebranch oflangchain_messages_to_agui.The sentinel, and the honest contract
Per the standard agreed on #2306, the forward direction is flag-only, so the original error text is not recoverable here.
errortherefore carries a fixed sentinel ("error") rather than a synthesized-but-plausible string, so a client can tell "the agent reported failure" apart from "the agent reported this failure text". The flag is durable across a round trip; the text is best-effort. There is no portable free-text slot to preserve it (Bedrock'stoolResulthas none, AI SDK'sproviderOptionsis provider-namespaced).I picked a bare
"error"sentinel to be obviously synthetic — open to a different marker if you prefer.Tests
A test is added on each side, next to the existing reverse-direction tool-message tests.
message-conversion.test.ts— full package suite green, 268 tests.test_message_conversion.py—unittest discover testsgreen, 385 tests.