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
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,71 @@ describe("replayHistoryIntoStrands", () => {
expect(calls[0]!.args).toBe("hello");
expect(calls[0]!.messages).toHaveLength(0);
});
it("carries a client-reported tool failure onto the toolResult status", async () => {
// AG-UI models the failure as ToolMessage.error; Bedrock models it as
// toolResult.status. Hardcoding "success" tells the model a failed
// frontend tool succeeded. Python parity: agent.py's _build_strands_history
// (#2317).
const { stub, calls } = recordingAgent();
const agent = makeAgent(stub);
await collect(
agent,
minimalRunInput({
messages: [
{ id: "u1", role: "user", content: "do something" },
{
id: "a1",
role: "assistant",
content: "",
toolCalls: [
{
id: "tc1",
type: "function",
function: { name: "render_chart", arguments: "{}" },
},
],
},
{
id: "t1",
role: "tool",
content: "tool failed: invalid id",
toolCallId: "tc1",
error: "invalid id",
},
],
}),
);
const history = calls[0]!.messages as Array<{ content: unknown[] }>;
const block = history[2]!.content[0] as { status: string };
expect(block.status).toBe("error");
});

it("keeps a successful tool result on the success status", async () => {
const { stub, calls } = recordingAgent();
const agent = makeAgent(stub);
await collect(
agent,
minimalRunInput({
messages: [
{ id: "u1", role: "user", content: "do something" },
{
id: "a1",
role: "assistant",
content: "",
toolCalls: [
{
id: "tc1",
type: "function",
function: { name: "render_chart", arguments: "{}" },
},
],
},
{ id: "t1", role: "tool", content: "ok", toolCallId: "tc1" },
],
}),
);
const history = calls[0]!.messages as Array<{ content: unknown[] }>;
const block = history[2]!.content[0] as { status: string };
expect(block.status).toBe("success");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,49 @@ describe("buildSnapshotMessages — standalone helper", () => {
expect((out[0] as { id: string }).id.length).toBeGreaterThan(0);
});

it("preserves error and encryptedValue on the tool echo", () => {
// This is an AG-UI -> AG-UI rebuild of the client's own message, so
// dropping its own fields loses the failure signal a MESSAGES_SNAPSHOT is
// supposed to make durable. Python parity: agent.py's
// _build_snapshot_messages (#2317).
const out = buildSnapshotMessages([
{
id: "t1",
role: "tool",
content: "tool failed: invalid id",
toolCallId: "tc1",
error: "invalid id",
encryptedValue: "enc",
} as never,
]);
expect(out).toHaveLength(1);
expect(out[0]).toEqual({
id: "t1",
role: "tool",
content: "tool failed: invalid id",
toolCallId: "tc1",
error: "invalid id",
encryptedValue: "enc",
});
});

it("omits error and encryptedValue when the client did not set them", () => {
const out = buildSnapshotMessages([
{
id: "t1",
role: "tool",
content: "ok",
toolCallId: "tc1",
} as never,
]);
expect(out[0]).toEqual({
id: "t1",
role: "tool",
content: "ok",
toolCallId: "tc1",
});
});

it("drops developer / system / reasoning / activity roles", () => {
const out = buildSnapshotMessages([
{ id: "s1", role: "system", content: "sys" } as never,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,60 @@ describe("convertMessagesForStrandsSeed", () => {
]);
});

it("carries a client-reported tool failure onto the seeded toolResult status", async () => {
// Same rule as _buildStrandsHistory: the flag comes from ToolMessage.error,
// and each result is stamped independently.
const seed = await convertMessagesForStrandsSeed([
{ id: "u", role: "user", content: "lookup" } as unknown as AguiMessage,
{
id: "a",
role: "assistant",
content: "",
toolCalls: [
{
id: "tc-1",
type: "function",
function: { name: "s1", arguments: "{}" },
},
{
id: "tc-2",
type: "function",
function: { name: "s2", arguments: "{}" },
},
],
} as unknown as AguiMessage,
{
id: "t1",
role: "tool",
toolCallId: "tc-1",
content: "ok",
} as unknown as AguiMessage,
{
id: "t2",
role: "tool",
toolCallId: "tc-2",
content: "tool failed: invalid id",
error: "invalid id",
} as unknown as AguiMessage,
]);
expect(seed[2].content).toEqual([
{
toolResult: {
toolUseId: "tc-1",
status: "success",
content: [{ text: "ok" }],
},
},
{
toolResult: {
toolUseId: "tc-2",
status: "error",
content: [{ text: "tool failed: invalid id" }],
},
},
]);
});

it("drops orphaned tool messages whose call id wasn't announced", async () => {
const seed = await convertMessagesForStrandsSeed([
{ id: "u", role: "user", content: "hi" } as unknown as AguiMessage,
Expand Down
25 changes: 21 additions & 4 deletions integrations/aws-strands/typescript/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,22 @@ export function buildSnapshotMessages(
out.push(assistant);
} else {
const toolCallId = (msg as { toolCallId?: string }).toolCallId ?? "";
out.push({
const { error, encryptedValue } = msg as {
error?: string;
encryptedValue?: string;
};
const tool = {
id: msgId,
role: "tool",
content: _coerceText(msg.content),
toolCallId,
} as AguiToolMessage);
} as AguiToolMessage;
// This is an AG-UI -> AG-UI rebuild of the client's own message, so
// preserve its error/encryptedValue on the snapshot echo instead of
// silently dropping the client's own fields.
if (error !== undefined) tool.error = error;
if (encryptedValue !== undefined) tool.encryptedValue = encryptedValue;
out.push(tool);
}
}
return out;
Expand Down Expand Up @@ -408,7 +418,12 @@ async function _buildStrandsHistory(
toolResult: {
toolUseId: toolCallId,
content: [_buildToolResultContent(msg.content)],
status: "success" as const,
// 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: (msg as { error?: string }).error
? ("error" as const)
: ("success" as const),
},
},
],
Expand Down Expand Up @@ -2728,7 +2743,9 @@ export async function convertMessagesForStrandsSeed(
pendingToolResults.push({
toolResult: {
toolUseId: toolCallId,
status: "success" as const,
status: (msg as { error?: string }).error
? ("error" as const)
: ("success" as const),
content: [{ text: textContent }],
},
});
Expand Down