fix(coding-agent): show length stop errors

closes #4290
This commit is contained in:
Mario Zechner
2026-06-25 14:49:18 +02:00
parent e454f50b48
commit f14b3594c1
3 changed files with 37 additions and 10 deletions
+1
View File
@@ -4,6 +4,7 @@
### Fixed
- Fixed assistant messages stopped by output length to show a visible incomplete-response error ([#4290](https://github.com/earendil-works/pi/issues/4290)).
- Fixed `--no-session --session-id` so ephemeral CLI runs can use deterministic session IDs for provider cache affinity ([#6070](https://github.com/earendil-works/pi/issues/6070)).
- Fixed disk BMP image files to be detected, converted to PNG, and attached through `read` and CLI `@file` inputs ([#6047](https://github.com/earendil-works/pi/issues/6047)).
- Fixed auto-retry for provider stream errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)).
@@ -121,21 +121,30 @@ export class AssistantMessageComponent extends Container {
}
}
// Check if aborted - show after partial content
// But only if there are no tool calls (tool execution components will show the error)
// Check if incomplete/failed - show after partial content.
// For aborted/error tool calls, tool execution components show the error.
// Length stops can happen before a tool call is complete, so surface them here too.
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
this.hasToolCalls = hasToolCalls;
if (!hasToolCalls) {
if (message.stopReason === "length") {
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(
new Text(
theme.fg(
"error",
"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
),
1,
0,
),
);
} else if (!hasToolCalls) {
if (message.stopReason === "aborted") {
const abortMessage =
message.errorMessage && message.errorMessage !== "Request was aborted"
? message.errorMessage
: "Operation aborted";
if (hasVisibleContent) {
this.contentContainer.addChild(new Spacer(1));
} else {
this.contentContainer.addChild(new Spacer(1));
}
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
} else if (message.stopReason === "error") {
const errorMsg = message.errorMessage || "Unknown error";
@@ -7,7 +7,10 @@ const OSC133_ZONE_START = "\x1b]133;A\x07";
const OSC133_ZONE_END = "\x1b]133;B\x07";
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
function createAssistantMessage(content: AssistantMessage["content"]): AssistantMessage {
function createAssistantMessage(
content: AssistantMessage["content"],
overrides: Partial<Pick<AssistantMessage, "stopReason">> = {},
): AssistantMessage {
return {
role: "assistant",
content,
@@ -22,7 +25,7 @@ function createAssistantMessage(content: AssistantMessage["content"]): Assistant
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
stopReason: overrides.stopReason ?? "stop",
timestamp: Date.now(),
};
}
@@ -54,4 +57,18 @@ describe("AssistantMessageComponent", () => {
expect(rendered.includes(OSC133_ZONE_END)).toBe(false);
expect(rendered.includes(OSC133_ZONE_FINAL)).toBe(false);
});
test("renders length stops as visible errors", () => {
initTheme("dark");
const component = new AssistantMessageComponent(
createAssistantMessage([{ type: "thinking", thinking: "private reasoning" }], { stopReason: "length" }),
true,
);
const rendered = component.render(80).join("\n");
expect(rendered).toContain("Thinking...");
expect(rendered).toContain("maximum output token limit");
expect(rendered).toContain("response may be incomplete");
});
});