Skip to content

Commit 8c677f8

Browse files
sethconvexclaude
andcommitted
Fix tools not reporting proper errors (output-error state)
Two fixes: 1. normalizeToolResult in mapping.ts was stripping the isError flag. Now preserves isError when transforming tool result parts. 2. createAssistantUIMessage in UIMessages.ts now checks contentPart.isError in addition to message.error when determining tool state. This ensures tool results with isError: true show "output-error" state instead of incorrectly showing "output-available". Fixes #162 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 74cbf01 commit 8c677f8

File tree

3 files changed

+116
-6
lines changed

3 files changed

+116
-6
lines changed

src/UIMessages.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -463,16 +463,22 @@ function createAssistantUIMessage<
463463
typeof contentPart.output?.type === "string"
464464
? contentPart.output.value
465465
: contentPart.output;
466+
// Check for error at both the content part level (isError) and message level
467+
// isError may exist on stored tool results but isn't in ToolResultPart type
468+
const hasError =
469+
(contentPart as { isError?: boolean }).isError || message.error;
470+
const errorText =
471+
message.error || (hasError ? String(output) : undefined);
466472
const call = allParts.find(
467473
(part) =>
468474
part.type === `tool-${contentPart.toolName}` &&
469475
"toolCallId" in part &&
470476
part.toolCallId === contentPart.toolCallId,
471477
) as ToolUIPart | undefined;
472478
if (call) {
473-
if (message.error) {
479+
if (hasError) {
474480
call.state = "output-error";
475-
call.errorText = message.error;
481+
call.errorText = errorText ?? "Unknown error";
476482
call.output = output;
477483
} else {
478484
call.state = "output-available";
@@ -483,15 +489,15 @@ function createAssistantUIMessage<
483489
"Tool result without preceding tool call.. adding anyways",
484490
contentPart,
485491
);
486-
if (message.error) {
492+
if (hasError) {
487493
allParts.push({
488494
type: `tool-${contentPart.toolName}`,
489495
toolCallId: contentPart.toolCallId,
490496
state: "output-error",
491497
input: undefined,
492-
errorText: message.error,
498+
errorText: errorText ?? "Unknown error",
493499
callProviderMetadata: message.providerMetadata,
494-
} satisfies ToolUIPart<TOOLS>);
500+
} as ToolUIPart<TOOLS>);
495501
} else {
496502
allParts.push({
497503
type: `tool-${contentPart.toolName}`,
@@ -500,7 +506,7 @@ function createAssistantUIMessage<
500506
input: undefined,
501507
output,
502508
callProviderMetadata: message.providerMetadata,
503-
} satisfies ToolUIPart<TOOLS>);
509+
} as ToolUIPart<TOOLS>);
504510
}
505511
}
506512
break;

src/mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,8 @@ function normalizeToolResult(
549549
normalizeToolOutput("result" in part ? part.result : undefined),
550550
toolCallId: part.toolCallId,
551551
toolName: part.toolName,
552+
// Preserve isError flag for error reporting
553+
...("isError" in part && part.isError ? { isError: true } : {}),
552554
...metadata,
553555
} satisfies ToolResultPart;
554556
}

src/toUIMessages.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,4 +728,106 @@ describe("toUIMessages", () => {
728728
expect(textParts).toHaveLength(1);
729729
expect(textParts[0].text).toBe("The result is 42.");
730730
});
731+
732+
it("shows output-error state when tool result has isError: true (issue #162)", () => {
733+
const messages = [
734+
// Tool call
735+
baseMessageDoc({
736+
_id: "msg1",
737+
order: 1,
738+
stepOrder: 1,
739+
tool: true,
740+
message: {
741+
role: "assistant",
742+
content: [
743+
{
744+
type: "tool-call",
745+
toolName: "generateImage",
746+
toolCallId: "call1",
747+
args: { id: "invalid-id" },
748+
},
749+
],
750+
},
751+
}),
752+
// Tool result with error
753+
baseMessageDoc({
754+
_id: "msg2",
755+
order: 1,
756+
stepOrder: 2,
757+
tool: true,
758+
message: {
759+
role: "tool",
760+
content: [
761+
{
762+
type: "tool-result",
763+
toolCallId: "call1",
764+
toolName: "generateImage",
765+
output: {
766+
type: "text",
767+
value:
768+
'ArgumentValidationError: Value does not match validator.\nPath: .id\nValue: "invalid-id"\nValidator: v.id("images")',
769+
},
770+
isError: true,
771+
},
772+
],
773+
},
774+
}),
775+
];
776+
777+
const uiMessages = toUIMessages(messages);
778+
779+
expect(uiMessages).toHaveLength(1);
780+
expect(uiMessages[0].role).toBe("assistant");
781+
782+
const toolParts = uiMessages[0].parts.filter(
783+
(p) => p.type === "tool-generateImage",
784+
);
785+
expect(toolParts).toHaveLength(1);
786+
787+
const toolPart = toolParts[0] as any;
788+
expect(toolPart.toolCallId).toBe("call1");
789+
// Should show output-error, not output-available
790+
expect(toolPart.state).toBe("output-error");
791+
expect(toolPart.output).toContain("ArgumentValidationError");
792+
});
793+
794+
it("shows output-error when tool result has isError: true without tool call present (issue #162)", () => {
795+
// This simulates the case where the tool-call message wasn't saved
796+
const messages = [
797+
baseMessageDoc({
798+
_id: "msg1",
799+
order: 1,
800+
stepOrder: 1,
801+
tool: true,
802+
message: {
803+
role: "tool",
804+
content: [
805+
{
806+
type: "tool-result",
807+
toolCallId: "call1",
808+
toolName: "generateImage",
809+
output: {
810+
type: "text",
811+
value: "Error: Something went wrong",
812+
},
813+
isError: true,
814+
},
815+
],
816+
},
817+
}),
818+
];
819+
820+
const uiMessages = toUIMessages(messages);
821+
822+
expect(uiMessages).toHaveLength(1);
823+
expect(uiMessages[0].role).toBe("assistant");
824+
825+
const toolParts = uiMessages[0].parts.filter(
826+
(p) => p.type === "tool-generateImage",
827+
);
828+
expect(toolParts).toHaveLength(1);
829+
830+
const toolPart = toolParts[0] as any;
831+
expect(toolPart.state).toBe("output-error");
832+
});
731833
});

0 commit comments

Comments
 (0)