Archived
fix: show model response errors in chat
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Show model response errors in the chat transcript instead of leaving the conversation blank.
|
||||
@@ -29,6 +29,19 @@ describe("chat message normalization", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows assistant model errors as system chat messages", () => {
|
||||
expect(normalizeMessage({ role: "assistant", content: [], stopReason: "error", errorMessage: "429 rate limit", timestamp: "2026-05-09T12:00:00.000Z", provider: "openai", model: "gpt-4.1" })).toEqual([
|
||||
{ role: "system", parts: [{ type: "text", text: "Model response failed: 429 rate limit" }], meta: { timestamp: "2026-05-09T12:00:00.000Z", model: { provider: "openai", id: "gpt-4.1" } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps partial assistant content and adds a visible error line", () => {
|
||||
expect(normalizeMessage({ role: "assistant", content: [{ type: "text", text: "partial answer" }], stopReason: "error", errorMessage: "connection lost" })).toEqual([
|
||||
textMessage("assistant", "partial answer"),
|
||||
textMessage("system", "Model response failed: connection lost"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("extracts skill invocation blocks into dedicated skill and user messages", () => {
|
||||
expect(normalizeMessage({ role: "user", content: "<skill name=\"playwright\" location=\"/skills/playwright\">\nUse browser\n</skill>\n\nNow test the UI" })).toEqual([
|
||||
{ role: "user", parts: [{ type: "skillInvocation", name: "playwright", location: "/skills/playwright", content: "Use browser" }] },
|
||||
|
||||
@@ -53,7 +53,16 @@ export function normalizeMessage(message: unknown): ChatLine[] {
|
||||
|
||||
const visible = parts.filter((part) => part.type !== "empty");
|
||||
const displayRole = role === "assistant" && visible.length > 0 && visible.every((part) => part.type === "skillRead") ? "skill" : role;
|
||||
return visible.length > 0 ? [withMessageMeta({ role: displayRole, parts: visible, ...(source === undefined ? {} : { source }) }, message)] : [];
|
||||
const lines = visible.length > 0 ? [withMessageMeta({ role: displayRole, parts: visible, ...(source === undefined ? {} : { source }) }, message)] : [];
|
||||
const errorLine = assistantErrorLine(message);
|
||||
return errorLine === undefined ? lines : [...lines, withMessageMeta(errorLine, message)];
|
||||
}
|
||||
|
||||
function assistantErrorLine(message: unknown): ChatLine | undefined {
|
||||
if (getString(message, "role") !== "assistant" || getString(message, "stopReason") !== "error") return undefined;
|
||||
const errorMessage = getString(message, "errorMessage")?.trim();
|
||||
const detail = errorMessage === undefined || errorMessage === "" ? "The model returned an error." : errorMessage;
|
||||
return textMessage("system", `Model response failed: ${detail}`);
|
||||
}
|
||||
|
||||
function isChatLine(message: unknown): message is ChatLine {
|
||||
|
||||
@@ -61,6 +61,46 @@ describe("applyTranscriptEvent", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("appends finalized assistant errors that have no displayable content", () => {
|
||||
expect(applyTranscriptEvent([textMessage("user", "question")], {
|
||||
type: "message.end",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
stopReason: "error",
|
||||
errorMessage: "provider returned 500",
|
||||
timestamp: "2026-05-09T12:00:00.000Z",
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet",
|
||||
},
|
||||
})).toEqual([
|
||||
textMessage("user", "question"),
|
||||
{ role: "system", parts: [{ type: "text", text: "Model response failed: provider returned 500" }], meta: { timestamp: "2026-05-09T12:00:00.000Z", model: { provider: "anthropic", id: "claude-sonnet" } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces streamed assistant text and keeps the finalized error line", () => {
|
||||
const streamed: ChatLine[] = [
|
||||
textMessage("user", "question"),
|
||||
textMessage("assistant", "partial"),
|
||||
];
|
||||
|
||||
expect(applyTranscriptEvent(streamed, {
|
||||
type: "message.end",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "partial answer" }],
|
||||
stopReason: "error",
|
||||
errorMessage: "connection lost",
|
||||
timestamp: "2026-05-09T12:00:00.000Z",
|
||||
},
|
||||
})).toEqual([
|
||||
textMessage("user", "question"),
|
||||
{ ...textMessage("assistant", "partial answer"), meta: { timestamp: "2026-05-09T12:00:00.000Z" } },
|
||||
{ role: "system", parts: [{ type: "text", text: "Model response failed: connection lost" }], meta: { timestamp: "2026-05-09T12:00:00.000Z" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces streamed skill reads when the finalized assistant message includes thinking", () => {
|
||||
const streamed: ChatLine[] = [
|
||||
{ role: "skill", parts: [{ type: "skillRead", name: "playwright", path: "/skills/playwright/SKILL.md" }] },
|
||||
|
||||
@@ -25,10 +25,16 @@ function applyFinalMessage(messages: ChatLine[], rawMessage: unknown): ChatLine[
|
||||
return finalizeToolExecution(messages, rawToolResult.toolCallId, rawToolResult.toolName, summarizeArgs(rawToolResult.content), rawToolResult.text, rawToolResult.isError, rawToolResult.content, rawToolResult.details);
|
||||
}
|
||||
|
||||
const ended = normalizeMessage(rawMessage)[0];
|
||||
if (ended === undefined) return undefined;
|
||||
const displayEnded = ended.role === "assistant" ? withoutToolCalls(ended) : ended;
|
||||
if (displayEnded.parts.length === 0) return messages;
|
||||
const ended = normalizeMessage(rawMessage);
|
||||
if (ended.length === 0) return undefined;
|
||||
const displayEnded = ended
|
||||
.map((line) => line.role === "assistant" ? withoutToolCalls(line) : line)
|
||||
.filter((line) => line.parts.length > 0);
|
||||
if (displayEnded.length === 0) return messages;
|
||||
return displayEnded.reduce((next, line) => applyFinalLine(next, line), messages);
|
||||
}
|
||||
|
||||
function applyFinalLine(messages: ChatLine[], displayEnded: ChatLine): ChatLine[] {
|
||||
const skillReadIndex = findMatchingSkillRead(messages, displayEnded);
|
||||
if (skillReadIndex >= 0) return [...messages.slice(0, skillReadIndex), displayEnded, ...messages.slice(skillReadIndex + 1)];
|
||||
const last = messages.at(-1);
|
||||
|
||||
Reference in New Issue
Block a user