fix(coding-agent): truncate tool results in compaction summarization to prevent overflow, fixes #1796

This commit is contained in:
Mario Zechner
2026-03-06 16:56:47 +01:00
parent d8e3998d6e
commit c950c692a1
3 changed files with 97 additions and 1 deletions

View File

@@ -12,6 +12,7 @@
- Fixed custom editors having their `onEscape`/`onCtrlD` handlers unconditionally overwritten by app-level defaults, making vim-style escape handling impossible ([#1838](https://github.com/badlogic/pi-mono/issues/1838))
- Fixed auto-compaction retriggering on the first prompt after compaction due to stale pre-compaction assistant usage ([#1860](https://github.com/badlogic/pi-mono/issues/1860) by [@joelhooks](https://github.com/joelhooks))
- Fixed sessions never auto-compacting when hitting persistent API errors (e.g. 529 overloaded) by estimating context size from the last successful response ([#1834](https://github.com/badlogic/pi-mono/issues/1834))
- Fixed compaction summarization requests exceeding context limits by truncating tool results to 2k chars ([#1796](https://github.com/badlogic/pi-mono/issues/1796))
- Fixed `/new` leaving startup header content, including the changelog, visible after starting a fresh session ([#1880](https://github.com/badlogic/pi-mono/issues/1880))
- Fixed misleading docs and example implying that returning `{ isError: true }` from a tool's `execute` function marks the execution as failed; errors must be signaled by throwing ([#1881](https://github.com/badlogic/pi-mono/issues/1881))
- Fixed model switches through non-reasoning models to preserve the saved default thinking level instead of persisting a capability-forced `off` clamp ([#1864](https://github.com/badlogic/pi-mono/issues/1864))

View File

@@ -85,10 +85,26 @@ export function formatFileOperations(readFiles: string[], modifiedFiles: string[
// Message Serialization
// ============================================================================
/** Maximum characters for a tool result in serialized summaries. */
const TOOL_RESULT_MAX_CHARS = 2000;
/**
* Truncate text to a maximum character length for summarization.
* Keeps the beginning and appends a truncation marker.
*/
function truncateForSummary(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
const truncatedChars = text.length - maxChars;
return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
}
/**
* Serialize LLM messages to text for summarization.
* This prevents the model from treating it as a conversation to continue.
* Call convertToLlm() first to handle custom message types.
*
* Tool results are truncated to keep the summarization request within
* reasonable token budgets. Full content is not needed for summarization.
*/
export function serializeConversation(messages: Message[]): string {
const parts: string[] = [];
@@ -137,7 +153,7 @@ export function serializeConversation(messages: Message[]): string {
.map((c) => c.text)
.join("");
if (content) {
parts.push(`[Tool result]: ${content}`);
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
}
}
}

View File

@@ -0,0 +1,79 @@
import type { Message } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { serializeConversation } from "../src/core/compaction/utils.js";
describe("serializeConversation", () => {
it("should truncate long tool results", () => {
const longContent = "x".repeat(5000);
const messages: Message[] = [
{
role: "toolResult",
toolCallId: "tc1",
toolName: "read",
content: [{ type: "text", text: longContent }],
isError: false,
timestamp: Date.now(),
},
];
const result = serializeConversation(messages);
expect(result).toContain("[Tool result]:");
expect(result).toContain("[... 3000 more characters truncated]");
expect(result).not.toContain("x".repeat(3000));
// First 2000 chars should be present
expect(result).toContain("x".repeat(2000));
});
it("should not truncate short tool results", () => {
const shortContent = "x".repeat(1500);
const messages: Message[] = [
{
role: "toolResult",
toolCallId: "tc1",
toolName: "read",
content: [{ type: "text", text: shortContent }],
isError: false,
timestamp: Date.now(),
},
];
const result = serializeConversation(messages);
expect(result).toBe(`[Tool result]: ${shortContent}`);
expect(result).not.toContain("truncated");
});
it("should not truncate assistant or user messages", () => {
const longText = "y".repeat(5000);
const messages: Message[] = [
{
role: "user",
content: [{ type: "text", text: longText }],
timestamp: Date.now(),
},
{
role: "assistant",
content: [{ type: "text", text: longText }],
api: "anthropic",
provider: "anthropic",
model: "test",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
},
];
const result = serializeConversation(messages);
expect(result).not.toContain("truncated");
expect(result).toContain(longText);
});
});