import type { CompactionConfig } from '../config/runtime.js'; import type { Message } from '../types/message/index.js'; import { type WorkingSetPlan } from './salience/working-set.js'; /** * The compaction decision, with no turn attached. * * The whole algorithm — the leading-system floor scan, the tool-result * pre-pass, the boundary search, the guards — lived inside * `runCompactionCheck` and read everything off the iteration context — the * live message array, the logger, the event emitter, the working-state * manager. Nothing outside a live iteration could run it, so the pass was * testable only through a full run harness and unreachable from any * host-callable entry point. * * This file therefore holds NO reference to that context, and a test greps * for one: reintroducing a single field read would quietly re-couple the * arithmetic to a turn and nothing else would fail. * * What stayed behind is everything with an effect: the model call, the * working-memory re-pin, the array install, the logging, every * `emitEvent`. What moved here is the arithmetic. The split is the * question "what should happen" separated from "make it happen", and only * the first half can be asked without a turn. */ /** Why a pass decided to do nothing. */ export type CompactionSkipReason = /** Fewer messages than the recent window plus a floor — nothing to move. */ 'too_few_messages' /** An in-run pass has no leading `system` message to use as its permanent floor. */ | 'no_system_floor' /** Every candidate boundary would split a tool-call pair. */ | 'no_safe_cut' /** A safe cut exists but leaves nothing older worth summarising. */ | 'too_few_older'; export type CompactionPlan = { readonly kind: 'skip'; readonly reason: CompactionSkipReason; } | { readonly kind: 'cleared'; readonly messages: readonly Message[]; readonly clearedCount: number; /** Assistant narrations cut to their first sentence by the salience pass. */ readonly stubbedCount?: number; readonly charsReclaimed: number; readonly reclaimedTokens: number; readonly reliefWasEnough: boolean; } | { readonly kind: 'plan'; readonly systemMessages: readonly Message[]; readonly olderMessages: readonly Message[]; readonly recentMessages: readonly Message[]; readonly keepStart: number; }; export interface CompactionPlanInput { readonly messages: readonly Message[]; readonly config: CompactionConfig; readonly contextWindowTokens: number; readonly estimatedTokens: number; readonly force?: boolean; /** * Let a host-created pass establish its own retained summary floor. * * This is deliberately separate from `force`: a provider overflow can * force the threshold decision without changing the live turn's prompt * invariant, while a host may own a durable user/assistant-only history * that has no system floor yet. */ readonly allowNoSystemFloor?: boolean; /** * Skip the tool-result pre-pass and go straight to the boundary search. * * The two steps are sequential in a real pass: ask once for a cleared * candidate and, if that was not enough relief, ask again for a cut over * that candidate. The caller need not install the first answer before the * second question. In fact the live runtime deliberately stages an * insufficient clear until summary verification succeeds, then publishes * the combined edit atomically. Returning both from one call would mean * nesting a union inside a union and computing a boundary the caller may * never use; asking twice says what is happening without prescribing when * an effect becomes visible. */ readonly skipToolResultClear?: boolean; } /** * The last index whose tail fits in `budgetTokens`, walking backwards. * * Replaces the naive count boundary and nothing else — the caller runs the * existing `findSafeTrimIndex` search downward from whatever this returns, * so the `tool_use` ↔ `tool_result` pairing guarantee is untouched by * construction rather than by care. * * Floored at one message. A single final message larger than the whole * budget still has to be kept: it is the live turn, and dropping it to * satisfy a size preference would delete the thing the turn is answering. * * Exported for its own tests. There used to be a second copy of this in the * phase file with a `__forTests` export beside it; two implementations of * one boundary calculation is the failure this extraction exists to end, so * the phase's copy is gone and its tests point here. */ export declare function naiveKeepStartByTokens(messages: readonly Message[], budgetTokens: number): number; /** * The salience pass: score every message and evict the least salient * until the context is under `softTarget`. Returned as a `cleared` plan * so the phase commits it the way it commits the stale-result pass; the * summary path runs after it only when the trigger threshold is still * exceeded. */ /** Where the salience pass holds the context, as a fraction of the window. */ export declare const DEFAULT_SOFT_TARGET = 0.5; export declare function planSalienceWorkingSet(input: { readonly messages: readonly Message[]; readonly config: CompactionConfig; readonly contextWindowTokens: number; readonly estimatedTokens: number; readonly openTasks?: readonly string[]; }): Extract & { readonly working: WorkingSetPlan; }; export declare function planCompaction(input: CompactionPlanInput): CompactionPlan; /** * Below this, a pass would summarise almost nothing and pay a model call to * do it. */ export declare const MIN_OLDER_MESSAGES_TO_COMPACT = 1; //# sourceMappingURL=plan.d.ts.map