From 59a392bd78cc82ee0e8e596be4c7d26330576973 Mon Sep 17 00:00:00 2001 From: Robin Newhouse Date: Fri, 15 May 2026 09:53:57 -0700 Subject: [PATCH] Budget basic compaction projections --- .../extensions/context/basic-compaction.ts | 29 +++++++++++++++---- .../src/extensions/context/compaction.test.ts | 17 ++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/sdk/packages/core/src/extensions/context/basic-compaction.ts b/sdk/packages/core/src/extensions/context/basic-compaction.ts index 229c6518b3..cd8405c989 100644 --- a/sdk/packages/core/src/extensions/context/basic-compaction.ts +++ b/sdk/packages/core/src/extensions/context/basic-compaction.ts @@ -3,6 +3,7 @@ import type { CoreCompactionContext, CoreCompactionResult, } from "../../types/config"; +import { buildBudgetProjection } from "./budget-projection"; import { type EstimateMessageTokens, findFirstUserMessageIndex, @@ -390,7 +391,23 @@ export function runBasicCompaction(options: { ...candidates.map((candidate) => candidate.message), ...protectedTail, ]; - if (!haveMessagesChanged(options.context.messages, nextMessages)) { + const budgeted = buildBudgetProjection({ + messages: nextMessages, + targetTokens, + policyIntent: "basic_compaction_projection", + estimateMessageTokens: options.estimateMessageTokens, + }); + if (budgeted.status === "failed") { + options.logger?.debug("Basic compaction returned best-effort projection", { + budgetWarnings: budgeted.warnings.map((warning) => warning.code), + projectedTokens: budgeted.estimatedTokens, + targetTokens, + maxInputTokens: options.context.maxInputTokens, + }); + } + const resultMessages = budgeted.messages; + + if (!haveMessagesChanged(options.context.messages, resultMessages)) { return undefined; } @@ -402,18 +419,20 @@ export function runBasicCompaction(options: { options.estimateMessageTokens, ); const afterTokens = getTotalTokens( - nextMessages, + resultMessages, options.estimateMessageTokens, ); options.logger?.debug("Performed basic compaction", { messagesBefore: options.context.messages.length, - messagesAfter: nextMessages.length, - messagesRemoved: options.context.messages.length - nextMessages.length, + messagesAfter: resultMessages.length, + messagesRemoved: options.context.messages.length - resultMessages.length, tokensBefore: beforeTokens, tokensAfter: afterTokens, + budgetActions: budgeted.actions.length, + budgetWarnings: budgeted.warnings.map((warning) => warning.code), targetTokens, maxInputTokens: options.context.maxInputTokens, }); - return { messages: nextMessages }; + return { messages: resultMessages }; } diff --git a/sdk/packages/core/src/extensions/context/compaction.test.ts b/sdk/packages/core/src/extensions/context/compaction.test.ts index fec621d2e0..3a0c7da97d 100644 --- a/sdk/packages/core/src/extensions/context/compaction.test.ts +++ b/sdk/packages/core/src/extensions/context/compaction.test.ts @@ -365,13 +365,28 @@ describe("createContextCompactionPrepareTurn", () => { const compacted = runForcedBasicCompaction(messages, 1); expect(compacted).toEqual([ - { role: "user", content: "Old request" }, { role: "user", content: "Read the latest file" }, assistantToolUseMessage("tool-a"), toolResultMessage("tool-a", "latest result"), ]); }); + it("budgets the complete basic compaction output including the latest turn", () => { + const messages: LlmsProviders.Message[] = [ + { role: "user", content: "original task" }, + { role: "assistant", content: "old assistant " + "x".repeat(10_000) }, + { role: "user", content: "latest typed prompt" }, + assistantToolUseMessage("tool-live"), + toolResultMessage("tool-live", "live result " + "y".repeat(10_000)), + ]; + + const compacted = runForcedBasicCompaction(messages, 700); + + expect(totalJsonTokens(compacted)).toBeLessThanOrEqual(700); + expect(JSON.stringify(compacted)).toContain("latest typed prompt"); + expectNoOrphanedToolPairs(compacted); + }); + it("does not compact a single typed user message", () => { const messages: LlmsProviders.Message[] = [ { role: "user", content: "Only current request" },