Skip to content
Merged
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
10 changes: 9 additions & 1 deletion integrations/aws-strands/python/src/ag_ui_strands/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ def _build_snapshot_messages(input_messages: List[Any]) -> List[Any]:
role="tool",
content=_coerce_text(msg.content),
tool_call_id=tool_call_id,
# This is an AG-UI -> AG-UI rebuild of the client's own message, so
# preserve its error/encrypted_value on the snapshot echo instead of
# silently dropping the client's own fields.
error=getattr(msg, "error", None),
encrypted_value=getattr(msg, "encrypted_value", None),
)
)
return out
Expand Down Expand Up @@ -237,7 +242,10 @@ def flush_tool_results() -> None:
"toolResult": {
"toolUseId": getattr(msg, "tool_call_id", "") or "",
"content": [{"text": _coerce_text(msg.content)}],
"status": "success",
# Carry the AG-UI failure signal onto Bedrock's toolResult status,
# so a client-reported tool failure is not asserted to the model as
# a success.
"status": "error" if getattr(msg, "error", None) else "success",
}
}
)
Expand Down
43 changes: 43 additions & 0 deletions integrations/aws-strands/python/tests/test_tool_error_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from ag_ui.core import ToolMessage

from ag_ui_strands.agent import _build_strands_history, _build_snapshot_messages


def _tool_message(**overrides):
fields = dict(
id="t1",
role="tool",
content="Tool failed: invalid id",
tool_call_id="tc1",
)
fields.update(overrides)
return ToolMessage(**fields)


class TestBedrockToolResultStatus:
def test_error_maps_onto_bedrock_status(self):
# A client-reported tool failure must reach the model as an error, not a
# silent success -- AG-UI's ToolMessage.error sets Bedrock's toolResult status.
history = _build_strands_history([_tool_message(error="invalid id")])
tool_result = history[0]["content"][0]["toolResult"]
assert tool_result["status"] == "error"

def test_defaults_to_success_without_error(self):
history = _build_strands_history([_tool_message(content="42")])
tool_result = history[0]["content"][0]["toolResult"]
assert tool_result["status"] == "success"


class TestSnapshotPreservesClientFields:
def test_preserves_error_and_encrypted_value(self):
# _build_snapshot_messages rebuilds the client's own message; it must not
# drop the client's error / encrypted_value on the snapshot echo.
snapshot = _build_snapshot_messages(
[_tool_message(error="invalid id", encrypted_value="enc-abc")]
)
assert snapshot[0].error == "invalid id"
assert snapshot[0].encrypted_value == "enc-abc"

def test_leaves_error_unset_when_absent(self):
snapshot = _build_snapshot_messages([_tool_message(content="42")])
assert snapshot[0].error is None
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, it, expect } from "vitest";
import type { Message } from "@ag-ui/client";
import { convertAGUIMessageToLangChain } from "../messages";

describe("convertAGUIMessageToLangChain — tool messages", () => {
it("maps a tool result with no error to status 'success'", () => {
const msg: Message = { id: "t1", role: "tool", content: "42", toolCallId: "tc1" };
const result = convertAGUIMessageToLangChain(msg) as any;
expect(result.tool_call_id).toBe("tc1");
// No error carries no failure signal, so status defaults to "success".
expect(result.status).toBe("success");
});

it("maps a tool error onto LangChain's status flag", () => {
// A client-reported tool failure must reach the model as an error, not a
// silent success — AG-UI's ToolMessage.error becomes status: "error".
const msg: Message = {
id: "t1",
role: "tool",
content: "Tool failed: invalid id",
toolCallId: "tc1",
error: "invalid id",
};
const result = convertAGUIMessageToLangChain(msg) as any;
expect(result.status).toBe("error");
});
});
3 changes: 3 additions & 0 deletions integrations/langchain/typescript/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export function convertAGUIMessageToLangChain(message: Message): BaseMessage {
return new ToolMessage({
content: message.content,
tool_call_id: message.toolCallId,
// Carry the AG-UI failure signal onto LangChain's tool-result status, so a
// client-reported tool failure is not delivered to the model as a success.
status: message.error ? "error" : "success",
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ describe("convertAGUIMessagesToMastra", () => {
toolCallId: "tc-1",
toolName: "get_weather",
result: "72°F",
isError: false,
},
],
});
Expand All @@ -542,10 +543,35 @@ describe("convertAGUIMessagesToMastra", () => {
toolCallId: "tc-orphan",
toolName: "unknown",
result: "some result",
isError: false,
},
],
});
});

it("carries a tool error onto the AI SDK isError flag", () => {
// A client-reported tool failure must reach the model as an error, not a
// silent success. AG-UI's ToolMessage.error sets the tool-result isError flag.
const messages: Message[] = [
{
id: "1",
role: "tool",
content: "Tool failed: invalid id",
toolCallId: "tc-1",
error: "invalid id",
},
];

const result = convertAGUIMessagesToMastra(messages);

expect((result[0] as any).content[0]).toEqual({
type: "tool-result",
toolCallId: "tc-1",
toolName: "unknown",
result: "Tool failed: invalid id",
isError: true,
});
});
});

describe("mixed conversations", () => {
Expand Down
3 changes: 3 additions & 0 deletions integrations/mastra/typescript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ export function convertAGUIMessagesToMastra(
toolCallId: message.toolCallId,
toolName: toolName,
result: message.content,
// Carry the AG-UI failure signal onto the AI SDK v4 tool-result flag, so a
// client-reported tool failure is not delivered to the model as a success.
isError: !!message.error,
},
],
} as CoreMessage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect } from "vitest";
import type { Message } from "@ag-ui/client";
import { convertMessagesToVercelAISDKMessages } from "../index";

describe("convertMessagesToVercelAISDKMessages — tool results", () => {
it("sets isError false when the tool result has no error", () => {
const messages: Message[] = [
{ id: "t1", role: "tool", content: "42", toolCallId: "tc1" },
];
const result = convertMessagesToVercelAISDKMessages(messages);
expect((result[0] as any).content[0]).toEqual({
type: "tool-result",
toolCallId: "tc1",
toolName: "unknown",
result: "42",
isError: false,
});
});

it("carries a tool error onto the AI SDK isError flag", () => {
// A client-reported tool failure must reach the model as an error, not a
// silent success. AG-UI's ToolMessage.error sets the tool-result isError flag.
const messages: Message[] = [
{
id: "t1",
role: "tool",
content: "Tool failed: invalid id",
toolCallId: "tc1",
error: "invalid id",
},
];
const result = convertMessagesToVercelAISDKMessages(messages);
expect((result[0] as any).content[0].isError).toBe(true);
});
});
3 changes: 3 additions & 0 deletions integrations/vercel-ai-sdk/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ export function convertMessagesToVercelAISDKMessages(
toolCallId: message.toolCallId,
toolName: toolName,
result: message.content,
// Carry the AG-UI failure signal onto the AI SDK v4 tool-result flag, so a
// client-reported tool failure is not delivered to the model as a success.
isError: !!message.error,
},
],
});
Expand Down
Loading