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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 11 additions & 2 deletions src/vercel/UIMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` &&
Expand Down
76 changes: 76 additions & 0 deletions src/vercel/toUIMessages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Loading