fix(coding-agent): truncate tool results in compaction summarization to prevent overflow, fixes #1796
This commit is contained in:
@@ -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 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 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 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 `/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 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))
|
- 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))
|
||||||
|
|||||||
@@ -85,10 +85,26 @@ export function formatFileOperations(readFiles: string[], modifiedFiles: string[
|
|||||||
// Message Serialization
|
// 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.
|
* Serialize LLM messages to text for summarization.
|
||||||
* This prevents the model from treating it as a conversation to continue.
|
* This prevents the model from treating it as a conversation to continue.
|
||||||
* Call convertToLlm() first to handle custom message types.
|
* 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 {
|
export function serializeConversation(messages: Message[]): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@@ -137,7 +153,7 @@ export function serializeConversation(messages: Message[]): string {
|
|||||||
.map((c) => c.text)
|
.map((c) => c.text)
|
||||||
.join("");
|
.join("");
|
||||||
if (content) {
|
if (content) {
|
||||||
parts.push(`[Tool result]: ${content}`);
|
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
packages/coding-agent/test/compaction-serialization.test.ts
Normal file
79
packages/coding-agent/test/compaction-serialization.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user