Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions integrations/langgraph/python/ag_ui_langgraph/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
Expand Down
16 changes: 16 additions & 0 deletions integrations/langgraph/python/tests/test_message_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
5 changes: 5 additions & 0 deletions integrations/langgraph/typescript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down