From 636dd8a515b272d84fba10dddbea324864d05aab Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Thu, 3 Sep 2026 22:08:05 +0800 Subject: [PATCH] feat(agent-core-v2): remove the context budget reminders PR #3423 shipped two behaviors together: the context budget reminders (context_budget and compaction_ahead) and the compaction recovery pointer. Remove the reminders so the recovery pointer's effect can be measured on its own in an ablation run. Removed: the features/contextBudget feature, the budget-only CompactionStrategy.budget()/CompactionTriggerBudget/ IAgentFullCompactionService.budget() surface, the summarizer-input reminder filter, and the context_budget_reminder / compaction_ahead_reminder telemetry plus the ahead_* fields on compaction_finished. The recovery pointer (Context Recovery footer, wire journal line ranges, whole-line wire.jsonl reads, empty-history compaction failure) is unchanged. --- .changeset/compaction-context-budget.md | 2 +- .../agent/fullCompaction/fullCompaction.ts | 6 - .../fullCompaction/fullCompactionService.ts | 37 +--- .../src/agent/fullCompaction/strategy.ts | 30 --- .../agent-core-v2/src/app/telemetry/events.ts | 43 ----- .../contextBudget/compaction-ahead.md | 8 - .../features/contextBudget/context-budget.md | 5 - .../contextBudget/contextBudgetFeature.ts | 15 -- .../contextBudget/contextBudgetReminder.ts | 124 ------------ .../contextBudget/contextBudgetService.ts | 125 ------------ .../src/features/reminder/reminderService.ts | 6 +- packages/agent-core-v2/src/index.ts | 3 - .../fullCompaction/fullCompaction.test.ts | 90 --------- .../agent/fullCompaction/strategy.test.ts | 71 ------- .../contextBudget/contextBudget.test.ts | 178 ------------------ 15 files changed, 3 insertions(+), 740 deletions(-) delete mode 100644 packages/agent-core-v2/src/features/contextBudget/compaction-ahead.md delete mode 100644 packages/agent-core-v2/src/features/contextBudget/context-budget.md delete mode 100644 packages/agent-core-v2/src/features/contextBudget/contextBudgetFeature.ts delete mode 100644 packages/agent-core-v2/src/features/contextBudget/contextBudgetReminder.ts delete mode 100644 packages/agent-core-v2/src/features/contextBudget/contextBudgetService.ts delete mode 100644 packages/agent-core-v2/test/features/contextBudget/contextBudget.test.ts diff --git a/.changeset/compaction-context-budget.md b/.changeset/compaction-context-budget.md index 4217e6ff5a8..678ed941800 100644 --- a/.changeset/compaction-context-budget.md +++ b/.changeset/compaction-context-budget.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": minor --- -Remind the model of its context budget before automatic compaction, and after compaction point it at the session's event log for exact details. +After compaction, point the model at the session's event log so it can recover exact details of the compacted conversation. diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts index 758f98e4539..88c70d19440 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompaction.ts @@ -2,7 +2,6 @@ import type { CompactionResult, CompactionSource, } from './types'; -import type { CompactionTriggerBudget } from './strategy'; import { createDecorator } from "#/_base/di/instantiation"; import type { Event } from '#/_base/event'; import type { Hooks } from '#/hooks'; @@ -12,10 +11,6 @@ export interface FullCompactionInput { readonly instruction?: string; } -export interface CompactionBudget extends CompactionTriggerBudget { - readonly used: number; -} - export interface FullCompactionTask { readonly abortController: AbortController; readonly promise: Promise; @@ -30,7 +25,6 @@ export interface IAgentFullCompactionService { readonly compacting: FullCompactionTask | null; begin(input: FullCompactionInput): boolean; cancel(): void; - budget(): CompactionBudget; readonly hooks: Hooks<{ onWillCompact: FullCompactionTask; diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index b858f7dad30..7b0bf0ffaef 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -26,10 +26,6 @@ import { stripDynamicToolContext } from '#/agent/toolSelect/dynamicTools'; import { IAgentToolSelectService } from '#/agent/toolSelect/toolSelect'; import { IAgentTodoService } from '#/features/todo/todoService'; import { renderTodoList } from '#/features/todo/todoItem'; -import { - isContextBudgetReminder, - summarizeCompactionAheadFollowUp, -} from '#/features/contextBudget/contextBudgetReminder'; import { onUnexpectedError } from '#/_base/errors/unexpectedError'; import type { WireLineRange } from '#/wire/record'; import { IWireService } from '#/wire/wire'; @@ -52,7 +48,6 @@ import { renderCompactionInstruction } from './compactionInstruction'; import { renderContextRecoveryPointer } from './contextRecovery'; import { IAgentFullCompactionService, - type CompactionBudget, type FullCompactionInput, type FullCompactionTask, } from './fullCompaction'; @@ -248,10 +243,6 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom return this._compacting; } - budget(): CompactionBudget { - return { used: this.tokenCountWithPending(), ...this.strategy.budget() }; - } - cancel(): void { const active = this._compacting; if (active !== null) { @@ -652,9 +643,7 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom const delays = retryBackoffDelays(MAX_COMPACTION_RETRY_ATTEMPTS); let attempt: CompactionAttemptResult | undefined; - let historyForModel: readonly ContextMessage[] = stripDynamicToolContext(originalHistory).filter( - (message) => !isContextBudgetReminder(message), - ); + let historyForModel: readonly ContextMessage[] = stripDynamicToolContext(originalHistory); let droppedCount = 0; let overflowShrinkCount = 0; let emptyOrTruncatedShrinkCount = 0; @@ -781,7 +770,6 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom thinking_effort: thinkingEffort, trace_id: attempt.traceId, ...usageTelemetry(attempt.usage), - ...aheadReminderTelemetry(originalHistory), }; this.telemetry.track2('compaction_finished', properties); return result; @@ -845,29 +833,6 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom } } -type CompactionAheadTelemetryProperties = Pick< - CompactionFinishedEvent, - | 'ahead_reminder_delivered' - | 'ahead_steps_count' - | 'ahead_write_calls_count' - | 'ahead_bash_calls_count' - | 'ahead_todo_calls_count' ->; - -function aheadReminderTelemetry( - history: readonly ContextMessage[], -): CompactionAheadTelemetryProperties { - const followUp = summarizeCompactionAheadFollowUp(history); - if (followUp === undefined) return { ahead_reminder_delivered: false }; - return { - ahead_reminder_delivered: true, - ahead_steps_count: followUp.stepCount, - ahead_write_calls_count: followUp.writeCallCount, - ahead_bash_calls_count: followUp.bashCallCount, - ahead_todo_calls_count: followUp.todoCallCount, - }; -} - function findAPIStatusError(error: unknown): APIStatusError | undefined { let current: unknown = error; const seen = new Set(); diff --git a/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts b/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts index fd0ccdfbef1..2ca7120ed05 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts @@ -27,17 +27,9 @@ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = { minOverflowReductionRatio: 0.05, }; -export interface CompactionTriggerBudget { - readonly maxSize: number; - readonly triggerRatio: number; - readonly reservedContextSize: number; - readonly triggerTokens: number; -} - export interface CompactionStrategy { shouldCompact(usedSize: number): boolean; shouldBlock(usedSize: number): boolean; - budget(): CompactionTriggerBudget; computeCompactCount(messages: readonly Message[], source: CompactionSource): number; reduceCompactOnOverflow(messages: readonly Message[]): number; readonly checkAfterStep: boolean; @@ -59,10 +51,6 @@ export class RuntimeCompactionStrategy implements CompactionStrategy { return this.delegate().shouldBlock(usedSize); } - budget(): CompactionTriggerBudget { - return this.delegate().budget(); - } - computeCompactCount(messages: readonly Message[], source: CompactionSource): number { return this.windowDelegate().computeCompactCount(messages, source); } @@ -141,24 +129,6 @@ export class DefaultCompactionStrategy implements CompactionStrategy { ); } - budget(): CompactionTriggerBudget { - const maxSize = this.maxSize; - const reservedContextSize = this.config.reservedContextSize; - const reservedTrigger = - reservedContextSize > 0 && reservedContextSize < maxSize - ? maxSize - reservedContextSize - : Number.POSITIVE_INFINITY; - return { - maxSize, - triggerRatio: this.config.triggerRatio, - reservedContextSize, - triggerTokens: - maxSize <= 0 - ? Number.POSITIVE_INFINITY - : Math.min(Math.ceil(maxSize * this.config.triggerRatio), reservedTrigger), - }; - } - private shouldUseReservedContext(usedSize: number): boolean { const reservedSize = this.config.reservedContextSize; return reservedSize > 0 && reservedSize < this.maxSize && usedSize + reservedSize >= this.maxSize; diff --git a/packages/agent-core-v2/src/app/telemetry/events.ts b/packages/agent-core-v2/src/app/telemetry/events.ts index c0262faf233..465cce28e37 100644 --- a/packages/agent-core-v2/src/app/telemetry/events.ts +++ b/packages/agent-core-v2/src/app/telemetry/events.ts @@ -215,24 +215,6 @@ export interface CompactionFinishedEvent { input_cache_read?: number; input_cache_creation?: number; trace_id?: string; - ahead_reminder_delivered: boolean; - ahead_steps_count?: number; - ahead_write_calls_count?: number; - ahead_bash_calls_count?: number; - ahead_todo_calls_count?: number; -} - -export interface ContextBudgetReminderEvent { - bucket: 'half' | 'three_quarters'; - used_tokens: number; - trigger_tokens: number; - max_tokens: number; -} - -export interface CompactionAheadReminderEvent { - used_tokens: number; - trigger_tokens: number; - lead_tokens: number; } export interface CompactionFailedEvent { @@ -802,31 +784,6 @@ export const telemetryEventDefinitions = { input_cache_creation: 'Cache-creation input tokens', trace_id: 'Trace id of the final compaction request round; absent for non-Kimi protocols', - ahead_reminder_delivered: - 'Whether the compaction-ahead reminder had been delivered in the compacted window', - ahead_steps_count: 'Assistant steps taken between the compaction-ahead reminder and compaction', - ahead_write_calls_count: 'Write/Edit tool calls made after the compaction-ahead reminder', - ahead_bash_calls_count: 'Bash tool calls made after the compaction-ahead reminder', - ahead_todo_calls_count: 'Todo tool calls made after the compaction-ahead reminder', - }, - }), - context_budget_reminder: defineAgentTelemetryEvent({ - owner: 'kimi-code', - comment: 'The model is told how much of its context budget is used, once per bucket.', - properties: { - bucket: 'Share of the compaction trigger reached: half or three_quarters', - used_tokens: 'Context tokens in use when the reminder was injected', - trigger_tokens: 'Token count at which automatic compaction triggers', - max_tokens: 'Effective context window size in tokens', - }, - }), - compaction_ahead_reminder: defineAgentTelemetryEvent({ - owner: 'kimi-code', - comment: 'The model is warned once per window that automatic compaction is imminent.', - properties: { - used_tokens: 'Context tokens in use when the reminder was injected', - trigger_tokens: 'Token count at which automatic compaction triggers', - lead_tokens: 'Tokens between the reminder threshold and the compaction trigger', }, }), compaction_failed: defineAgentTelemetryEvent({ diff --git a/packages/agent-core-v2/src/features/contextBudget/compaction-ahead.md b/packages/agent-core-v2/src/features/contextBudget/compaction-ahead.md deleted file mode 100644 index 709aa6c6af8..00000000000 --- a/packages/agent-core-v2/src/features/contextBudget/compaction-ahead.md +++ /dev/null @@ -1,8 +0,0 @@ - -Context is at ~${used_pct}%; automatic compaction runs at ${trigger_pct}% (about ${remaining_k}k tokens from now). When it runs you will write a handoff note with text only — no tool calls. This is your last chance to act: -- persist unfinished intermediate results to files or the todo list — these survive verbatim and can be read back; -- verify with tools any claim you intend to carry forward (run the test; don't assume) — the note will be written with the result in view; -- bring the current sub-task to a hand-off-able boundary; don't start large new work now; -- if a long user input or constraint may be truncated (kept user messages are capped at ~${kept_k}k tokens), restate its essentials. -Then continue the task; do not stop. - diff --git a/packages/agent-core-v2/src/features/contextBudget/context-budget.md b/packages/agent-core-v2/src/features/contextBudget/context-budget.md deleted file mode 100644 index 897043ad4d1..00000000000 --- a/packages/agent-core-v2/src/features/contextBudget/context-budget.md +++ /dev/null @@ -1,5 +0,0 @@ - -Context: ~${used_pct}% of the ${max_k}k-token window is used; automatic compaction runs at ${trigger_k}k (${trigger_pct}%). Figures are as of the last check. -At compaction this window is replaced by a handoff note you write yourself (text only, no tools). Kept verbatim: your recent user messages (capped at ~${kept_k}k tokens; a long one keeps only its head) and the todo list. Cleared: assistant messages, tool calls and tool results — but the full record stays on disk and a recovery pointer will follow the note. -Do not wrap up or stop early because of budget. Prefer Grep or paged Read over whole-file reads when the payoff is small. - diff --git a/packages/agent-core-v2/src/features/contextBudget/contextBudgetFeature.ts b/packages/agent-core-v2/src/features/contextBudget/contextBudgetFeature.ts deleted file mode 100644 index 715ec1112ff..00000000000 --- a/packages/agent-core-v2/src/features/contextBudget/contextBudgetFeature.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Feature } from '#/features/feature'; -import { registerFeature } from '#/features/featureRegistry'; - -import { AgentContextBudgetService, IAgentContextBudgetService } from './contextBudgetService'; - -export class ContextBudgetFeature extends Feature { - static override readonly name = 'contextBudget'; - - constructor() { - super(); - this.contributeAgentService(IAgentContextBudgetService, AgentContextBudgetService); - } -} - -registerFeature(ContextBudgetFeature); diff --git a/packages/agent-core-v2/src/features/contextBudget/contextBudgetReminder.ts b/packages/agent-core-v2/src/features/contextBudget/contextBudgetReminder.ts deleted file mode 100644 index 1a2d5ad368f..00000000000 --- a/packages/agent-core-v2/src/features/contextBudget/contextBudgetReminder.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { renderPrompt } from '#/_base/utils/render-prompt'; -import { COMPACT_USER_MESSAGE_MAX_TOKENS } from '#/agent/contextMemory/compactionHandoff'; -import type { ContextMessage } from '#/agent/contextMemory/types'; -import type { CompactionBudget } from '#/agent/fullCompaction/fullCompaction'; - -import compactionAheadTemplate from './compaction-ahead.md?raw'; -import contextBudgetTemplate from './context-budget.md?raw'; - -export const CONTEXT_BUDGET_REMINDER_VARIANT = 'context_budget'; -export const COMPACTION_AHEAD_REMINDER_VARIANT = 'compaction_ahead'; - -export const COMPACTION_AHEAD_LEAD_RATIO = 0.1; - -export type ContextBudgetBucket = 'half' | 'three_quarters'; - -export interface ContextBudgetDisclosure { - readonly bucket: ContextBudgetBucket; -} - -const BUCKET_THRESHOLDS: readonly (readonly [ContextBudgetBucket, number])[] = [ - ['three_quarters', 0.75], - ['half', 0.5], -]; - -const WRITE_TOOL_NAMES = new Set(['Write', 'Edit']); -const BASH_TOOL_NAMES = new Set(['Bash']); -const TODO_TOOL_NAMES = new Set(['TodoList', 'SetTodoList']); - -export interface CompactionAheadFollowUp { - readonly stepCount: number; - readonly writeCallCount: number; - readonly bashCallCount: number; - readonly todoCallCount: number; -} - -export function contextBudgetBucket(budget: CompactionBudget): ContextBudgetBucket | undefined { - if (!Number.isFinite(budget.triggerTokens) || budget.triggerTokens <= 0) return undefined; - const share = budget.used / budget.triggerTokens; - for (const [bucket, threshold] of BUCKET_THRESHOLDS) { - if (share >= threshold) return bucket; - } - return undefined; -} - -export function compactionAheadLeadTokens(budget: CompactionBudget): number { - return Math.ceil(budget.maxSize * COMPACTION_AHEAD_LEAD_RATIO); -} - -export function shouldRemindCompactionAhead(budget: CompactionBudget): boolean { - if (!Number.isFinite(budget.triggerTokens) || budget.maxSize <= 0) return false; - if (budget.used >= budget.triggerTokens) return false; - return budget.triggerTokens - budget.used <= compactionAheadLeadTokens(budget); -} - -export function renderContextBudgetReminder(budget: CompactionBudget): string { - return renderPrompt(contextBudgetTemplate, { - used_pct: percent(budget.used, budget.maxSize), - max_k: thousands(budget.maxSize), - trigger_k: thousands(budget.triggerTokens), - trigger_pct: percent(budget.triggerTokens, budget.maxSize), - kept_k: thousands(COMPACT_USER_MESSAGE_MAX_TOKENS), - }).trimEnd(); -} - -export function renderCompactionAheadReminder(budget: CompactionBudget): string { - return renderPrompt(compactionAheadTemplate, { - used_pct: percent(budget.used, budget.maxSize), - trigger_pct: percent(budget.triggerTokens, budget.maxSize), - remaining_k: thousands(Math.max(0, budget.triggerTokens - budget.used)), - kept_k: thousands(COMPACT_USER_MESSAGE_MAX_TOKENS), - }).trimEnd(); -} - -export function isContextBudgetReminder(message: ContextMessage): boolean { - return ( - message.origin?.kind === 'injection' && - (message.origin.variant === CONTEXT_BUDGET_REMINDER_VARIANT || - message.origin.variant === COMPACTION_AHEAD_REMINDER_VARIANT) - ); -} - -export function isCompactionAheadReminder(message: ContextMessage): boolean { - return ( - message.origin?.kind === 'injection' && - message.origin.variant === COMPACTION_AHEAD_REMINDER_VARIANT - ); -} - -export function summarizeCompactionAheadFollowUp( - history: readonly ContextMessage[], -): CompactionAheadFollowUp | undefined { - let reminderIndex = -1; - for (let index = history.length - 1; index >= 0; index -= 1) { - if (isCompactionAheadReminder(history[index]!)) { - reminderIndex = index; - break; - } - } - if (reminderIndex < 0) return undefined; - - let stepCount = 0; - let writeCallCount = 0; - let bashCallCount = 0; - let todoCallCount = 0; - for (const message of history.slice(reminderIndex + 1)) { - if (message.role !== 'assistant') continue; - stepCount += 1; - for (const toolCall of message.toolCalls) { - if (WRITE_TOOL_NAMES.has(toolCall.name)) writeCallCount += 1; - else if (BASH_TOOL_NAMES.has(toolCall.name)) bashCallCount += 1; - else if (TODO_TOOL_NAMES.has(toolCall.name)) todoCallCount += 1; - } - } - return { stepCount, writeCallCount, bashCallCount, todoCallCount }; -} - -function percent(part: number, whole: number): number { - if (whole <= 0) return 0; - return Math.round((part / whole) * 100); -} - -function thousands(tokens: number): number { - return Math.round(tokens / 1000); -} diff --git a/packages/agent-core-v2/src/features/contextBudget/contextBudgetService.ts b/packages/agent-core-v2/src/features/contextBudget/contextBudgetService.ts deleted file mode 100644 index cdaef5d0d0b..00000000000 --- a/packages/agent-core-v2/src/features/contextBudget/contextBudgetService.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { fromCallback, setup } from 'xstate'; - -import { createDecorator, IInstantiationService } from '#/_base/di/instantiation'; -import { - AgentActorService, - type AgentActorContext, - type AgentActorRestoreEvent, -} from '#/agent/actorService/agentActorService'; -import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction'; -import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; -import { ITelemetryService } from '#/app/telemetry/telemetry'; -import { IAgentReminderService } from '#/features/reminder/reminderService'; -import type { ContextInjectionResult } from '#/features/reminder/types'; -import { IEventDispatcher } from '#/state/eventDispatcher'; - -import { - COMPACTION_AHEAD_REMINDER_VARIANT, - CONTEXT_BUDGET_REMINDER_VARIANT, - compactionAheadLeadTokens, - contextBudgetBucket, - renderCompactionAheadReminder, - renderContextBudgetReminder, - shouldRemindCompactionAhead, - type ContextBudgetDisclosure, -} from './contextBudgetReminder'; - -interface ContextBudgetActorContext { - readonly runtime: AgentActorContext; -} - -const contextBudgetReminders = fromCallback(({ - input, -}: { - input: { - readonly runtime: AgentActorContext; - }; -}) => { - const runtime = input.runtime; - const reminder = runtime.get(IAgentReminderService); - const compaction = runtime.get(IAgentFullCompactionService); - const telemetry = runtime.get(ITelemetryService); - - const budgetRegistration = reminder.register( - CONTEXT_BUDGET_REMINDER_VARIANT, - ({ lastDisclosure }): ContextInjectionResult | undefined => { - const budget = compaction.budget(); - const bucket = contextBudgetBucket(budget); - if (bucket === undefined || lastDisclosure?.bucket === bucket) return undefined; - telemetry.track2('context_budget_reminder', { - bucket, - used_tokens: budget.used, - trigger_tokens: budget.triggerTokens, - max_tokens: budget.maxSize, - }); - return { content: renderContextBudgetReminder(budget), disclosure: { bucket } }; - }, - ); - - const aheadRegistration = reminder.register( - COMPACTION_AHEAD_REMINDER_VARIANT, - ({ lastInjection }): string | undefined => { - if (lastInjection !== undefined) return undefined; - const budget = compaction.budget(); - if (!shouldRemindCompactionAhead(budget)) return undefined; - telemetry.track2('compaction_ahead_reminder', { - used_tokens: budget.used, - trigger_tokens: budget.triggerTokens, - lead_tokens: compactionAheadLeadTokens(budget), - }); - return renderCompactionAheadReminder(budget); - }, - ); - - return () => { - budgetRegistration.dispose(); - aheadRegistration.dispose(); - }; -}); - -const contextBudgetActorLogic = setup({ - types: {} as { - context: ContextBudgetActorContext; - input: AgentActorContext; - events: AgentActorRestoreEvent; - }, - actors: { contextBudgetReminders }, -}).createMachine({ - context: ({ input }) => ({ runtime: input }), - initial: 'beforeRestore', - states: { - beforeRestore: { - on: { 'runtime.restore': 'active' }, - }, - active: { - invoke: { - src: 'contextBudgetReminders', - input: ({ context }) => ({ runtime: context.runtime }), - }, - }, - }, -}); - -export interface IAgentContextBudgetService { - readonly _serviceBrand: undefined; -} - -export const IAgentContextBudgetService = createDecorator( - 'agentContextBudgetService', -); - -export class AgentContextBudgetService - extends AgentActorService - implements IAgentContextBudgetService -{ - declare readonly _serviceBrand: undefined; - - constructor( - @IEventDispatcher dispatcher: IEventDispatcher, - @IAgentScopeContext scopeContext: IAgentScopeContext, - @IInstantiationService instantiation: IInstantiationService, - ) { - super(dispatcher, scopeContext, instantiation); - this.attachActor(contextBudgetActorLogic, { id: 'contextBudget' }); - } -} diff --git a/packages/agent-core-v2/src/features/reminder/reminderService.ts b/packages/agent-core-v2/src/features/reminder/reminderService.ts index 5c3db3390de..f19319aeba7 100644 --- a/packages/agent-core-v2/src/features/reminder/reminderService.ts +++ b/packages/agent-core-v2/src/features/reminder/reminderService.ts @@ -33,11 +33,7 @@ interface ReminderEntry { readonly variant: string; } -const REMINDER_VARIANT_PRIORITY = new Map([ - ['date_change', -1], - ['context_budget', 1], - ['compaction_ahead', 2], -]); +const REMINDER_VARIANT_PRIORITY = new Map([['date_change', -1]]); interface ReminderActorContext { readonly entries: Set; diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 9879bd9103a..e18a259af67 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -647,9 +647,6 @@ export * from '#/agent/fullCompaction/compactionOps'; export * from '#/agent/fullCompaction/types'; export * from '#/agent/fullCompaction/contextRecovery'; export * from '#/agent/fullCompaction/compactionInstruction'; -export * from '#/features/contextBudget/contextBudgetReminder'; -export * from '#/features/contextBudget/contextBudgetService'; -import '#/features/contextBudget/contextBudgetFeature'; export * from '#/agent/llmRequester/llmRequester'; export * from '#/agent/llmRequester/llmRequesterService'; export * from '#/agent/llmRequester/llmRequestOps'; diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index e1587aa304a..8290c5b6bf6 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -29,7 +29,6 @@ import { IFileSystemStorageService } from '#/persistence/interface/storage'; import { InMemoryStorageService } from '#/persistence/backends/memory/inMemoryStorageService'; import { ISessionTokenCountingService } from '#/session/tokenCounting/sessionTokenCounting'; import { renderCompactionInstruction } from '#/agent/fullCompaction/compactionInstruction'; -import type { ContextMessage } from '#/agent/contextMemory/types'; import { IAgentToolSelectAnnouncementsService } from '#/agent/toolSelect/toolSelectAnnouncements'; import { IAgentFullCompactionService, @@ -3128,15 +3127,6 @@ describe('FullCompaction context recovery pointer', () => { }); } - function reminderMessage(variant: string, text: string): ContextMessage { - return { - role: 'user', - content: [{ type: 'text', text: `${text}` }], - toolCalls: [], - origin: { kind: 'injection', variant }, - }; - } - it('appends the journal location and window line ranges to the model-facing note', async () => { const ctx = recoveryAgent(appService(IFileSystemStorageService, locatedStorage(JOURNAL_HOME))); ctx.appendExchange(1, 'old user one', 'old assistant one', 20); @@ -3243,86 +3233,6 @@ describe('FullCompaction context recovery pointer', () => { ); }); - it('keeps context budget reminders out of the summarizer request', async () => { - const ctx = recoveryAgent(); - ctx.appendExchange(1, 'old user one', 'old assistant one', 20); - ctx.context.append( - reminderMessage('context_budget', 'BUDGET-REMINDER-TEXT'), - reminderMessage('compaction_ahead', 'AHEAD-REMINDER-TEXT'), - ); - ctx.appendExchange(2, 'recent user two', 'recent assistant two', 40); - - await compactOnce(ctx, 'Compacted summary.'); - - const request = JSON.stringify(ctx.lastLlmInput().input.history); - expect(request).toContain('old user one'); - expect(request).toContain('recent assistant two'); - expect(request).not.toContain('BUDGET-REMINDER-TEXT'); - expect(request).not.toContain('AHEAD-REMINDER-TEXT'); - }); - - it('reports what the agent did after the compaction-ahead reminder', async () => { - const records: TelemetryRecord[] = []; - const ctx = recoveryAgent({ telemetry: recordingTelemetry(records) }); - ctx.appendExchange(1, 'old user one', 'old assistant one', 20); - ctx.context.append( - reminderMessage('compaction_ahead', 'AHEAD-REMINDER-TEXT'), - { - role: 'assistant', - content: [{ type: 'text', text: 'persisting state' }], - toolCalls: [ - { type: 'function', id: 'call_write', name: 'Write', arguments: '{}' }, - { type: 'function', id: 'call_bash', name: 'Bash', arguments: '{}' }, - ], - }, - { role: 'tool', content: [{ type: 'text', text: 'ok' }], toolCalls: [], toolCallId: 'call_write' }, - { role: 'tool', content: [{ type: 'text', text: 'ok' }], toolCalls: [], toolCallId: 'call_bash' }, - ); - ctx.appendExchange(2, 'recent user two', 'recent assistant two', 40); - - await compactOnce(ctx, 'Compacted summary.'); - - expect(records).toContainEqual({ - event: 'compaction_finished', - properties: expect.objectContaining({ - ahead_reminder_delivered: true, - ahead_steps_count: 2, - ahead_write_calls_count: 1, - ahead_bash_calls_count: 1, - ahead_todo_calls_count: 0, - }), - }); - }); - - it('reports that no compaction-ahead reminder was delivered when none was', async () => { - const records: TelemetryRecord[] = []; - const ctx = recoveryAgent({ telemetry: recordingTelemetry(records) }); - ctx.appendExchange(1, 'old user one', 'old assistant one', 20); - ctx.appendExchange(2, 'recent user two', 'recent assistant two', 40); - - await compactOnce(ctx, 'Compacted summary.'); - - const finished = records.find((record) => record.event === 'compaction_finished'); - expect(finished?.properties).toMatchObject({ ahead_reminder_delivered: false }); - expect(finished?.properties).not.toHaveProperty('ahead_steps_count'); - }); - - it('exposes the live compaction budget from the numbers that drive auto compaction', () => { - const ctx = recoveryAgent(); - ctx.appendExchange(1, 'old user one', 'old assistant one', 1_000); - - const budget = ctx.get(IAgentFullCompactionService).budget(); - - expect(budget).toEqual({ - used: ctx.get(ISessionTokenCountingService).get(ctx.agentContext).size, - maxSize: 256_000, - triggerRatio: 0.85, - reservedContextSize: 50_000, - triggerTokens: 206_000, - }); - expect(budget.used).toBeGreaterThan(0); - }); - it('tells the summarizer a recovery pointer follows the note', () => { const withPointer = renderCompactionInstruction({}); const withCustom = renderCompactionInstruction({ customInstruction: ' keep the API facts ' }); diff --git a/packages/agent-core-v2/test/agent/fullCompaction/strategy.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/strategy.test.ts index dd37ab68843..ac826910c08 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/strategy.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/strategy.test.ts @@ -176,77 +176,6 @@ describe('DefaultCompactionStrategy', () => { expect(strategy.shouldCompact(28_000)).toBe(true); expect(strategy.shouldBlock(28_000)).toBe(true); }); - - it('describes a trigger budget that agrees with shouldCompact at the ratio threshold', () => { - const strategy = testCompactionStrategy(1_000_000); - - const budget = strategy.budget(); - - expect(budget).toEqual({ - maxSize: 1_000_000, - triggerRatio: 0.85, - reservedContextSize: 0, - triggerTokens: 850_000, - }); - expect(strategy.shouldCompact(budget.triggerTokens)).toBe(true); - expect(strategy.shouldCompact(budget.triggerTokens - 1)).toBe(false); - }); - - it('lets the reserved context lower the trigger budget', () => { - const strategy = new DefaultCompactionStrategy(() => 128_000, { - triggerRatio: 0.85, - blockRatio: 0.85, - reservedContextSize: 50_000, - maxCompactionPerTurn: 3, - maxOverflowCompactionAttempts: 3, - maxRecentMessages: 3, - maxRecentUserMessages: Infinity, - maxRecentSizeRatio: 0.2, - minOverflowReductionRatio: 0.05, - }); - - const budget = strategy.budget(); - - expect(budget.triggerTokens).toBe(78_000); - expect(strategy.shouldCompact(78_000)).toBe(true); - expect(strategy.shouldCompact(77_999)).toBe(false); - }); - - it('rounds a fractional ratio threshold up so the budget never fires early', () => { - const strategy = new DefaultCompactionStrategy(() => 100_001, { - triggerRatio: 0.85, - blockRatio: 0.85, - reservedContextSize: 0, - maxCompactionPerTurn: 3, - maxOverflowCompactionAttempts: 3, - maxRecentMessages: 3, - maxRecentUserMessages: Infinity, - maxRecentSizeRatio: 0.2, - minOverflowReductionRatio: 0.05, - }); - - const budget = strategy.budget(); - - expect(budget.triggerTokens).toBe(85_001); - expect(strategy.shouldCompact(85_001)).toBe(true); - expect(strategy.shouldCompact(85_000)).toBe(false); - }); - - it('ignores a reserve that is not smaller than the window in the trigger budget', () => { - const strategy = new DefaultCompactionStrategy(() => 32_000, { - triggerRatio: 0.85, - blockRatio: 0.85, - reservedContextSize: 50_000, - maxCompactionPerTurn: 3, - maxOverflowCompactionAttempts: 3, - maxRecentMessages: 3, - maxRecentUserMessages: Infinity, - maxRecentSizeRatio: 0.2, - minOverflowReductionRatio: 0.05, - }); - - expect(strategy.budget().triggerTokens).toBe(27_200); - }); }); function testCompactionStrategy(maxSize: number = 1_000): DefaultCompactionStrategy { diff --git a/packages/agent-core-v2/test/features/contextBudget/contextBudget.test.ts b/packages/agent-core-v2/test/features/contextBudget/contextBudget.test.ts deleted file mode 100644 index e631ca79389..00000000000 --- a/packages/agent-core-v2/test/features/contextBudget/contextBudget.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; - -import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; -import type { ContextMessage } from '#/agent/contextMemory/types'; -import { IAgentLoopService } from '#/agent/loop/loop'; -import type { CompactionBudget } from '#/agent/fullCompaction/fullCompaction'; -import { - contextBudgetBucket, - renderCompactionAheadReminder, - renderContextBudgetReminder, - shouldRemindCompactionAhead, -} from '#/features/contextBudget/contextBudgetReminder'; - -import { runWillBeginStepHooks, type StubLoop } from '../../agent/loop/stubs'; -import { recordingTelemetry, type TelemetryRecord } from '../../app/telemetry/stubs'; -import { testAgent, type TestAgentContext } from '../../harness'; - -const PROVIDER = { - type: 'kimi', - apiKey: 'test-key', - baseUrl: 'https://api.example/v1', - model: 'kimi-code', -} as const; -const CAPABILITIES = { - image_in: true, - video_in: true, - audio_in: false, - thinking: true, - tool_use: true, - max_context_tokens: 100_000, -} as const; - -function budget(used: number, overrides: Partial = {}): CompactionBudget { - return { - used, - maxSize: 100_000, - triggerRatio: 0.85, - reservedContextSize: 0, - triggerTokens: 85_000, - ...overrides, - }; -} - -function reminders(context: IAgentContextMemoryService, variant: string): ContextMessage[] { - return context - .get() - .filter((message) => message.origin?.kind === 'injection' && message.origin.variant === variant); -} - -function textOf(message: ContextMessage | undefined): string { - const part = message?.content[0]; - return part?.type === 'text' ? part.text : ''; -} - -describe('context budget reminder rules', () => { - it('buckets usage by its share of the compaction trigger', () => { - expect(contextBudgetBucket(budget(42_499))).toBeUndefined(); - expect(contextBudgetBucket(budget(42_500))).toBe('half'); - expect(contextBudgetBucket(budget(63_749))).toBe('half'); - expect(contextBudgetBucket(budget(63_750))).toBe('three_quarters'); - expect(contextBudgetBucket(budget(76_500))).toBe('three_quarters'); - expect(contextBudgetBucket(budget(84_999))).toBe('three_quarters'); - expect(contextBudgetBucket(budget(90_000))).toBe('three_quarters'); - }); - - it('never buckets when compaction can never trigger', () => { - expect(contextBudgetBucket(budget(50_000, { triggerTokens: Number.POSITIVE_INFINITY }))).toBeUndefined(); - expect(contextBudgetBucket(budget(50_000, { maxSize: 0, triggerTokens: Number.POSITIVE_INFINITY }))).toBeUndefined(); - }); - - it('warns about compaction only inside the lead window and before the trigger', () => { - expect(shouldRemindCompactionAhead(budget(74_999))).toBe(false); - expect(shouldRemindCompactionAhead(budget(75_000))).toBe(true); - expect(shouldRemindCompactionAhead(budget(84_999))).toBe(true); - expect(shouldRemindCompactionAhead(budget(85_000))).toBe(false); - expect(shouldRemindCompactionAhead(budget(50_000, { triggerTokens: Number.POSITIVE_INFINITY }))).toBe(false); - }); - - it('renders the budget reminder with the numbers that drive compaction', () => { - const text = renderContextBudgetReminder(budget(41_000)); - - expect(text.startsWith('')).toBe(true); - expect(text.endsWith('')).toBe(true); - expect(text).toContain('~41% of the 100k-token window is used; automatic compaction runs at 85k (85%)'); - expect(text).toContain('capped at ~20k tokens'); - expect(text).toContain('Do not wrap up or stop early because of budget'); - expect(text).not.toContain('${'); - }); - - it('renders the compaction-ahead reminder with the remaining lead', () => { - const text = renderCompactionAheadReminder(budget(76_000)); - - expect(text.startsWith('')).toBe(true); - expect(text).toContain('Context is at ~76%; automatic compaction runs at 85% (about 9k tokens from now)'); - expect(text).toContain('text only — no tool calls'); - expect(text).toContain('Then continue the task; do not stop.'); - expect(text).not.toContain('${'); - }); -}); - -describe('context budget reminders in the agent', () => { - let ctx: TestAgentContext; - let context: IAgentContextMemoryService; - let loop: StubLoop; - let telemetry: TelemetryRecord[]; - - beforeEach(async () => { - telemetry = []; - ctx = testAgent({ - telemetry: recordingTelemetry(telemetry), - initialConfig: { loopControl: { reservedContextSize: 0 } }, - }); - ctx.configure({ provider: PROVIDER, modelCapabilities: CAPABILITIES }); - context = ctx.get(IAgentContextMemoryService); - loop = ctx.get(IAgentLoopService) as StubLoop; - await ctx.restorePersisted(); - }); - - afterEach(async () => { - await ctx.dispose(); - }); - - it('tells the model its budget once per bucket as usage grows', async () => { - ctx.appendExchange(1, 'user one', 'assistant one', 30_000); - await runWillBeginStepHooks(loop); - expect(reminders(context, 'context_budget')).toHaveLength(0); - - ctx.appendExchange(2, 'user two', 'assistant two', 45_000); - await runWillBeginStepHooks(loop); - const [first] = reminders(context, 'context_budget'); - expect(textOf(first)).toContain(''); - expect(textOf(first)).toContain('automatic compaction runs at 85k (85%)'); - expect(first?.origin).toMatchObject({ kind: 'injection', variant: 'context_budget', disclosure: { bucket: 'half' } }); - - await runWillBeginStepHooks(loop); - expect(reminders(context, 'context_budget')).toHaveLength(1); - - ctx.appendExchange(3, 'user three', 'assistant three', 65_000); - await runWillBeginStepHooks(loop); - const budgetReminders = reminders(context, 'context_budget'); - expect(budgetReminders).toHaveLength(2); - expect(budgetReminders[1]?.origin).toMatchObject({ disclosure: { bucket: 'three_quarters' } }); - expect(telemetry.filter((record) => record.event === 'context_budget_reminder')).toHaveLength(2); - }); - - it('warns once that compaction is ahead when the trigger is within the lead window', async () => { - ctx.appendExchange(1, 'user one', 'assistant one', 76_000); - await runWillBeginStepHooks(loop); - - const [ahead] = reminders(context, 'compaction_ahead'); - expect(textOf(ahead)).toContain(''); - expect(textOf(ahead)).toContain('This is your last chance to act'); - expect(reminders(context, 'context_budget')).toHaveLength(1); - expect(context.get().at(-1)?.origin).toMatchObject({ variant: 'compaction_ahead' }); - - await runWillBeginStepHooks(loop); - expect(reminders(context, 'compaction_ahead')).toHaveLength(1); - expect(telemetry.filter((record) => record.event === 'compaction_ahead_reminder')).toHaveLength(1); - }); - - it('re-arms both reminders after compaction clears the window', async () => { - ctx.appendExchange(1, 'user one', 'assistant one', 76_000); - await runWillBeginStepHooks(loop); - expect(reminders(context, 'compaction_ahead')).toHaveLength(1); - - const completed = ctx.once('compaction.completed'); - ctx.mockNextResponse({ type: 'text', text: 'Compacted summary.' }); - await ctx.rpc.beginCompaction({}); - await completed; - expect(reminders(context, 'compaction_ahead')).toHaveLength(0); - expect(reminders(context, 'context_budget')).toHaveLength(0); - - ctx.appendExchange(2, 'user two', 'assistant two', 77_000); - await runWillBeginStepHooks(loop); - expect(reminders(context, 'compaction_ahead')).toHaveLength(1); - expect(reminders(context, 'context_budget')).toHaveLength(1); - }); -});