From decc3490ab75362fd20e60f50b5def2e5a50625f Mon Sep 17 00:00:00 2001 From: Robel Estifanos Date: Fri, 30 Oct 2026 16:20:03 -0400 Subject: [PATCH] fix(ui): a tool result with an error output renders as output-error --- CHANGELOG.md | 2 + src/vercel/UIMessages.ts | 13 +++++- src/vercel/toUIMessages.test.ts | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a5f6e8d..535ff284c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Keep the whole order being continued in context, past the recent window (#433, #436) - `useStreamingText` decodes UTF-8 characters split across chunks (#440) +- Render a tool result with an `error-text` or `error-json` output as + `output-error` (#162, #441) ## 0.7.9 diff --git a/src/vercel/UIMessages.ts b/src/vercel/UIMessages.ts index 527885ada..78c9204a2 100644 --- a/src/vercel/UIMessages.ts +++ b/src/vercel/UIMessages.ts @@ -588,10 +588,19 @@ function createAssistantUIMessage< : typedPart.output; // Check for error at both the content part level (isError) and message level // isError may exist on stored tool results but isn't in ToolResultPart type + const outputType = typedPart.output?.type; const hasError = - (contentPart as { isError?: boolean }).isError || message.error; + (contentPart as { isError?: boolean }).isError || + message.error || + outputType === "error-text" || + outputType === "error-json"; const errorText = - message.error || (hasError ? String(output) : undefined); + message.error || + (hasError + ? outputType === "error-json" + ? JSON.stringify(output) + : String(output) + : undefined); const call = allParts.find( (part) => part.type === `tool-${contentPart.toolName}` && diff --git a/src/vercel/toUIMessages.test.ts b/src/vercel/toUIMessages.test.ts index f01fa1dd0..14bc73e1e 100644 --- a/src/vercel/toUIMessages.test.ts +++ b/src/vercel/toUIMessages.test.ts @@ -886,6 +886,82 @@ describe("toUIMessages", () => { expect(toolPart.state).toBe("output-error"); }); + describe("tool result with an error output (issue #162)", () => { + const toolCall = baseMessageDoc({ + _id: "msg1", + order: 1, + stepOrder: 1, + tool: true, + message: { + role: "assistant", + content: [ + { + type: "tool-call", + toolName: "explode", + toolCallId: "call1", + input: {}, + args: {}, + }, + ], + }, + }); + const toolResult = ( + output: + | { type: "error-text"; value: string } + | { type: "error-json"; value: { code: number } }, + ) => + baseMessageDoc({ + _id: "msg2", + order: 1, + stepOrder: 2, + tool: true, + message: { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call1", + toolName: "explode", + output, + }, + ], + }, + }); + const toolPart = (messages: MessageDoc[]) => { + const parts = toUIMessages(messages)[0].parts.filter( + (p) => p.type === "tool-explode", + ); + expect(parts).toHaveLength(1); + return parts[0] as any; + }; + + it("renders error-text with its call as output-error", () => { + const part = toolPart([ + toolCall, + toolResult({ type: "error-text", value: "Error: kaboom" }), + ]); + expect(part.state).toBe("output-error"); + expect(part.errorText).toBe("Error: kaboom"); + }); + + it("renders an orphan error-text result as output-error", () => { + const part = toolPart([ + toolResult({ type: "error-text", value: "Error: kaboom" }), + ]); + expect(part.state).toBe("output-error"); + expect(part.errorText).toBe("Error: kaboom"); + }); + + it("renders error-json as output-error with the JSON as errorText", () => { + const part = toolPart([ + toolCall, + toolResult({ type: "error-json", value: { code: 42 } }), + ]); + expect(part.state).toBe("output-error"); + expect(part.errorText).toBe('{"code":42}'); + }); + }); + describe("userId preservation", () => { it("preserves userId in user messages", () => { const messages = [