diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 18e1f153..783c332e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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)) diff --git a/packages/coding-agent/src/core/compaction/utils.ts b/packages/coding-agent/src/core/compaction/utils.ts index 9c8f46bc..38a68d5f 100644 --- a/packages/coding-agent/src/core/compaction/utils.ts +++ b/packages/coding-agent/src/core/compaction/utils.ts @@ -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)}`); } } } diff --git a/packages/coding-agent/test/compaction-serialization.test.ts b/packages/coding-agent/test/compaction-serialization.test.ts new file mode 100644 index 00000000..eada4df1 --- /dev/null +++ b/packages/coding-agent/test/compaction-serialization.test.ts @@ -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); + }); +});