From 50c5f7590449dd01145f0d30e7ace48b41307576 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:13:21 -0600 Subject: [PATCH] Compaction: sanitize token split accounting (#24058) * Compaction: sanitize token split accounting * Tests/Compaction: type sanitize token estimate callback --- src/agents/compaction.token-sanitize.test.ts | 52 ++++++++++++++++++++ src/agents/compaction.ts | 8 ++- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/agents/compaction.token-sanitize.test.ts diff --git a/src/agents/compaction.token-sanitize.test.ts b/src/agents/compaction.token-sanitize.test.ts new file mode 100644 index 000000000..f7fad927f --- /dev/null +++ b/src/agents/compaction.token-sanitize.test.ts @@ -0,0 +1,52 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import { describe, expect, it, vi } from "vitest"; + +const piCodingAgentMocks = vi.hoisted(() => ({ + estimateTokens: vi.fn((_message: unknown) => 1), + generateSummary: vi.fn(async () => "summary"), +})); + +vi.mock("@mariozechner/pi-coding-agent", async () => { + const actual = await vi.importActual( + "@mariozechner/pi-coding-agent", + ); + return { + ...actual, + estimateTokens: piCodingAgentMocks.estimateTokens, + generateSummary: piCodingAgentMocks.generateSummary, + }; +}); + +import { chunkMessagesByMaxTokens, splitMessagesByTokenShare } from "./compaction.js"; + +describe("compaction token accounting sanitization", () => { + it("does not pass toolResult.details into per-message token estimates", () => { + const messages: AgentMessage[] = [ + { + role: "toolResult", + toolCallId: "call_1", + toolName: "browser", + isError: false, + content: [{ type: "text", text: "ok" }], + details: { raw: "x".repeat(50_000) }, + timestamp: 1, + // oxlint-disable-next-line typescript/no-explicit-any + } as any, + { + role: "user", + content: "next", + timestamp: 2, + }, + ]; + + splitMessagesByTokenShare(messages, 2); + chunkMessagesByMaxTokens(messages, 16); + + const calledWithDetails = piCodingAgentMocks.estimateTokens.mock.calls.some((call) => { + const message = call[0] as { details?: unknown } | undefined; + return Boolean(message?.details); + }); + + expect(calledWithDetails).toBe(false); + }); +}); diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index ba9870afe..da83723c3 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -23,6 +23,10 @@ export function estimateMessagesTokens(messages: AgentMessage[]): number { return safe.reduce((sum, message) => sum + estimateTokens(message), 0); } +function estimateCompactionMessageTokens(message: AgentMessage): number { + return estimateMessagesTokens([message]); +} + function normalizeParts(parts: number, messageCount: number): number { if (!Number.isFinite(parts) || parts <= 1) { return 1; @@ -49,7 +53,7 @@ export function splitMessagesByTokenShare( let currentTokens = 0; for (const message of messages) { - const messageTokens = estimateTokens(message); + const messageTokens = estimateCompactionMessageTokens(message); if ( chunks.length < normalizedParts - 1 && current.length > 0 && @@ -93,7 +97,7 @@ export function chunkMessagesByMaxTokens( let currentTokens = 0; for (const message of messages) { - const messageTokens = estimateTokens(message); + const messageTokens = estimateCompactionMessageTokens(message); if (currentChunk.length > 0 && currentTokens + messageTokens > effectiveMax) { chunks.push(currentChunk); currentChunk = [];