diff --git a/integrations/langgraph/python/ag_ui_langgraph/utils.py b/integrations/langgraph/python/ag_ui_langgraph/utils.py index 30a0af7a96..e4e77e0047 100644 --- a/integrations/langgraph/python/ag_ui_langgraph/utils.py +++ b/integrations/langgraph/python/ag_ui_langgraph/utils.py @@ -251,6 +251,11 @@ def langchain_messages_to_agui(messages: List[BaseMessage]) -> List[AGUIMessage] role="tool", content=stringify_if_needed(resolve_message_content(message.content)), tool_call_id=message.tool_call_id, + # A LangChain tool result signals failure only through `status`, with + # no error text. Restore AG-UI's `error` so the failure survives the + # round trip; the value is a fixed sentinel (#2305) because the + # original text is not recoverable from the flag alone. + error="error" if message.status == "error" else None, )) else: raise TypeError(f"Unsupported message type: {type(message)}") diff --git a/integrations/langgraph/python/tests/test_message_conversion.py b/integrations/langgraph/python/tests/test_message_conversion.py index 92a9b29f73..e548c1f08b 100644 --- a/integrations/langgraph/python/tests/test_message_conversion.py +++ b/integrations/langgraph/python/tests/test_message_conversion.py @@ -231,6 +231,22 @@ def test_tool_message(self): assert result[0].role == "tool" assert result[0].content == "result" assert result[0].tool_call_id == "tc1" + # No error status carries no failure signal, so error stays unset. + assert result[0].error is None + + def test_tool_message_error_status_maps_to_error(self): + # The reverse of #2263: a LangChain tool result with status "error" must set + # AG-UI's error so the failure survives the round trip. The value is a fixed + # sentinel -- the original text is not recoverable from the flag alone (#2305). + msg = ToolMessage( + id="t1", + content="Tool failed: invalid id", + tool_call_id="tc1", + status="error", + ) + result = langchain_messages_to_agui([msg]) + assert result[0].role == "tool" + assert result[0].error == "error" def test_multimodal_human_message(self): msg = HumanMessage( diff --git a/integrations/langgraph/typescript/src/message-conversion.test.ts b/integrations/langgraph/typescript/src/message-conversion.test.ts index f040b2ff23..2ab22efb33 100644 --- a/integrations/langgraph/typescript/src/message-conversion.test.ts +++ b/integrations/langgraph/typescript/src/message-conversion.test.ts @@ -179,6 +179,24 @@ describe("Message Conversion - All Types", () => { const result: any[] = langchainMessagesToAgui([msg]); expect(result[0].role).toBe("tool"); expect(result[0].toolCallId).toBe("tc1"); + // No status / "success" carries no failure signal, so error stays unset. + expect(result[0].error).toBeUndefined(); + }); + + it("should map a tool message error status onto AG-UI's error field", () => { + // The reverse of #2263: a LangChain tool result with status "error" must set + // AG-UI's error so the failure survives the round trip. The value is a fixed + // sentinel — the original text is not recoverable from the flag alone (#2305). + const msg = { + id: "t1", + type: "tool", + content: "Tool failed: invalid id", + tool_call_id: "tc1", + status: "error", + } as any as LangGraphMessage; + const result: any[] = langchainMessagesToAgui([msg]); + expect(result[0].role).toBe("tool"); + expect(result[0].error).toBe("error"); }); it("should handle generic (ChatMessage) type as assistant", () => { diff --git a/integrations/langgraph/typescript/src/utils.ts b/integrations/langgraph/typescript/src/utils.ts index 3d05a481fc..9bba7d3e85 100644 --- a/integrations/langgraph/typescript/src/utils.ts +++ b/integrations/langgraph/typescript/src/utils.ts @@ -309,6 +309,11 @@ export function langchainMessagesToAgui(messages: LangGraphMessage[]): Message[] role: "tool", content: stringifyIfNeeded(resolveMessageContent(message.content)), toolCallId: message.tool_call_id, + // A LangChain tool result signals failure only through `status`, with no + // error text. Restore AG-UI's `error` so the failure survives the round + // trip; the value is a fixed sentinel (#2305) because the original text is + // not recoverable from the flag alone. + ...(message.status === "error" ? { error: "error" } : {}), }); break; default: