import { type AgentMessage, type CompactionSettings, type Session, type ThinkingLevel } from "../internal/harness.js"; import type { Model } from "../internal/llm.js"; import type { Hooks } from "./hooks.js"; import type { Brain } from "./types.js"; /** * Default summarization focus, applied when a task does not provide `compaction.instructions`. * Addresses the common fidelity failure where summaries drop specific facts. */ export declare const DEFAULT_COMPACTION_INSTRUCTIONS: string; /** * SEMA-ONLY (trim-seam defense, no CC analogue) — stale-anchor freshness margin: a usage anchor is * trusted only while the anchored estimate stays within `structural × margin + overhead` of the * current message list. 2 covers the worst measured structural under-count (charsPerToken=3 vs a * measured ~2.18 true ratio ⇒ ≤ ~1.4x) plus reasoning-block accounting slop, while still catching * the live deception band (anchored estimates 2.9x–18x over the real context — docs/TOKEN-ESTIMATE- * CALIBRATION-LIVE-2026-07-09.md §3/§4). */ export declare const STALE_ANCHOR_STRUCTURAL_MARGIN = 2; /** * Repair compaction settings that are PATHOLOGICAL for the model's context window — without touching * self-consistent explicit configuration (a deliberately large `reserveTokens` is a legitimate * "compact early" knob, e.g. for long-run testing). * * DEFAULT_COMPACTION_SETTINGS (reserve 16384 / keepRecent 20000) are tuned for ≥100K windows; on a * small window W they break in two compounding ways (found via LONGRUN-1's 12K declared window): * 1. threshold = W - reserve ≤ 0 → `shouldCompact` fires at EVERY turn boundary; * 2. keepRecent ≥ threshold → even a successful compaction leaves the kept tail at/over the * threshold; worse, `findCutPoint` with keepRecent ≥ the whole history falls back to the * EARLIEST valid cut point, summarizing a sliver of the head. Together: one real LLM summary * call per boundary, forever (thrash + cost explosion). * 3. (design/64 §26.4 fix 2 — the prefix-cache "death band") threshold ABOVE the clearStale point * (`EDIT_FRACTION` = 0.7W): between 0.7W and the threshold, every request advances the * tool-result clearing frontier → breaks the prefix cache at the frontier → ~0.7W re-prefilled * PER REQUEST (at a 262K window with the default reserve that band is ~40 turns × ~183K tokens * per compaction cycle). Verified on the real vLLM gateway (search [87]-[89]: post-compaction * collapse + steady-state block reuse + stable-stub frontier mechanics). * Repairs: (1) → reserve falls back to 15% of the window; (3) → threshold is clamped to ≤ 0.7W * (compaction always fires before clearStale can engage — summarize-then-never-clear beats * clear-then-summarize on both cache reuse AND fidelity); (2) → keepRecent drops to half the * (post-clamp) threshold. (3) applies to explicit settings too: `editAt` is not user-configurable, * so a threshold above it can never express consistent intent — compacting at 0.7W is strictly * better for that caller (content is summarized, not stub-cleared). Settings already satisfying all * three pass through unchanged (same object). */ export declare function sanitizeCompactionSettings(settings: CompactionSettings, contextWindow: number | undefined): CompactionSettings; export interface MaybeCompactOptions { session: Session; /** Model whose context window bounds the budget (the model that runs the next turn). */ model: Model; /** Optional cheaper model used to produce the summary. Falls back to `model`. */ compactionModel?: Model; brain: Brain; getApiKeyAndHeaders?: (model: Model) => Promise<{ apiKey: string; headers?: Record; } | undefined>; thinking?: ThinkingLevel; settings?: CompactionSettings; /** Appended to the summarization prompt to steer what must be preserved. */ customInstructions?: string; signal?: AbortSignal; /** * Anti-thrash re-trigger floor (design/64 §25.2): ALSO require the estimated context to exceed this * many tokens before compacting. The within-task caller sets it after each compaction (post-compaction * size × regrowth factor) so a compaction that could not free much headroom — small window, chunky * turns, large summary — does not re-fire a real summary LLM call at every subsequent boundary * (LONGRUN-1b measured 26/39 boundaries firing, +135% wall, without it). 0/undefined = no floor. */ minTokens?: number; /** * Force a compaction even when context is UNDER the auto-threshold (and ignore `minTokens`) — the * manual `/compact` path (TaskStream.compact, shell K-1c). It bypasses ONLY the threshold gate, not the * structural floor: if there is no valid cut point (too little history to summarize) the call still * returns `{compacted:false}` quietly. Default `false` = the auto-threshold policy. */ force?: boolean; /** * design/123 D4 (r5 final discriminant) — trim→compaction pressure propagation: force a compaction * whenever the NATURAL gate would not fire, i.e. `est < max(threshold, minTokens-floor)`. Trim * dropping real messages is external hard evidence that compaction is overdue, and it must override * BOTH gates that can wrongly hold it back: * - est < threshold — the deceived-estimate regime (request-only trim deflates the next usage * anchor while the session stays full-size; the 16k-live sawtooth), and * - threshold ≤ est < floor — the floor-blocked regime (post-compaction §25.2 floor above an * HONEST over-threshold estimate; 5th live run: 27 boundaries trim-dropping while the floor * held compaction off and input climbed 16k→27.4k). * `est ≥ max(threshold, floor)` fires naturally anyway (no force needed/applied — the result then * reports `naturalTrigger: true`). Spiral protection lives in the CALLER via * {@link nextTrimForceBackoff}: any landed compaction that still posts over the threshold * (incompressible transcript, the LONGRUN-1b shape) disables further trim-forces until a landed * compaction posts under the threshold again. Like `force`, this never bypasses the structural * floor (no valid cut point ⇒ `{compacted:false}`). */ forceUnderThreshold?: boolean; /** * Working-file attachments (LONGRUN-2, CC-compact parity review 2026-06-12): after a successful * summary, re-read up to `maxFiles` of the files the summarized region MODIFIED (from * CompactionDetails.modifiedFiles) and append their current on-disk contents to the summary text. * Why: LONGRUN-2 measured the model spending ≈2.3 extra read calls per compaction re-fetching * files it had just lost — each one a full request round-trip at post-compaction context size. * CC ships the same idea ("attachments", ≤5 files × ~5k tokens). Bounded: per-file char cap plus * a total cap that self-scales to ~15% of the model window, so small windows never drown in * attachments; the §25.2 freedTokens measurement naturally accounts for the added mass (the * anti-thrash floor stays truthful). Reader failures skip the file (best-effort, never fails the * compaction). Absent (default) = current behavior, paths-only. * Pick order (design/71 P2-① resolved the LONGRUN-2b limitation): `maxFiles` picks the most * RECENTLY touched files first (`CompactionDetails.modifiedFilesByRecency`, recency threaded * through the engine fileOps accumulator); alphabetical `modifiedFiles` remains the fallback for * details persisted before the field existed. */ workingFileAttachments?: { /** Read a task workspace file; null/throw = skip it. Wired by the Runner from the task's ExecutionEnv. */ readFile: (path: string) => Promise; /** Max modified files to attach. Default 3. */ maxFiles?: number; /** Per-file char cap (≈4 chars/token). Default 16_000 (~4k tokens). */ maxCharsPerFile?: number; /** * Blackboard 2026-07-03 (CC-parity selection set): the files the task has READ, most recent * first (CC 198's post-compact restore sorts readFileState by timestamp — the read set is wider * than the modified set and includes reference files the model consulted). When provided and * non-empty it WINS over `modifiedFilesByRecency`; absent/empty falls back to the modified set * (the pre-2026-07-03 behavior, and the right answer for callers with no read tracking). */ recentlyReadFiles?: () => string[]; }; /** * Fixed per-request prompt overhead (system prompt + tool schemas, in tokens), applied ONLY in the * anchor-less regime (design/64 §26.7, search [91] fourth candidate): when no assistant in the * visible history carries real usage (custom Brain implementations that don't report usage), the * chars/4 fallback counts MESSAGES only and is blind to this overhead — the threshold would compare * apples to oranges and under-trigger. When usage-anchored, the anchor IS the provider-billed input * (system+tools included), so the overhead is already in and this is not added. */ overheadTokens?: number; /** * design/84 Seam C — compact-boundary COST optimization (data-gated). Fidelity is already achieved by * the iterative-merge UPDATE_SUMMARY path (compaction.ts); the only remaining cost is one LLM call per * boundary. A `summaryProvider` lets a caller supply a summary WITHOUT an LLM call: when it returns a * NON-EMPTY string, that string is reused verbatim as the summary (the real `generateSummary` LLM call * is skipped, `reused:true`). Returning `null`/`undefined`/an empty-or-whitespace string falls back to * the existing `generateSummary` path — IDENTICAL to current behavior. **Absent (default) = no behavior * change whatsoever**: the LLM path runs byte-for-byte as before. * * THIS IS A THIN SEAM ONLY: no provider is implemented in core. The provider implementation is gated on * design/82 cost-per-truly-correct data (§3/§7-5). Reuse is guarded against degradation (see below). * * Inputs: * - `messagesToSummarize`: the message range that would be summarized this boundary. * - `previousSummary`: the prior compaction's summary (present on the UPDATE/iterative-merge path). * - `modifiedFiles`: files the summarized range modified, newest-first (same source the working-file * attachments use), so a provider can decide whether the reused summary is still representative. */ summaryProvider?: (input: { messagesToSummarize: AgentMessage[]; previousSummary?: string; modifiedFiles: string[]; }) => string | null | undefined | Promise; /** * design/84 Seam C reuse guard (c): after this many CONSECUTIVE provider-reuse compactions, the next * compaction is FORCED through the real `generateSummary` LLM path even if the provider would hit — this * bounds summary drift from indefinite reuse. The caller owns the counter via {@link onCompaction} * (`reused`) and feeds it back via this option; core treats `summaryProvider` as if absent for that one * boundary when `consecutiveProviderReuse >= maxConsecutiveProviderReuse`. Default `3` (CC parity). * `0`/undefined with no `consecutiveProviderReuse` = provider always consulted (no forced refresh). */ maxConsecutiveProviderReuse?: number; /** * design/84 Seam C reuse guard (c) — counter input: how many consecutive provider-reuse compactions have * happened so far (caller-tracked from {@link onCompaction}'s `reused`). When this reaches * `maxConsecutiveProviderReuse`, the provider is bypassed for this one boundary to force a real summary. */ consecutiveProviderReuse?: number; /** * design/84 Seam C — pure observer fired AFTER a successful compaction (never on a no-op/under-threshold * return). Does NOT alter control flow. `reused` distinguishes a provider-supplied summary from a real * LLM `generateSummary`. `freedTokens` is the §25.2 structural delta (may be ~0 when a reused summary * frees little — see reuse guard (b): such a compaction does NOT raise the caller's anti-thrash floor). */ onCompaction?: (info: { messagesCompacted: number; freedTokens: number; tokensBefore: number; reused: boolean; }) => void; /** * design/134 §3.2 — which trigger this pass runs under, threaded into the pre/postCompact callback * contexts and driving the block semantics: "auto" (threshold/boundary, the default) | "manual" * (/compact) | "forced" (promptTooLong recovery and trim-pressure propagation — the compaction is * not optional there, so a preCompact `block` is IGNORED on "forced"). */ trigger?: "auto" | "manual" | "forced"; /** * design/134 (CC PreCompact parity) — fired AFTER the trigger gate and a CONFIRMED valid cut point * (so every invocation corresponds to a compaction that would actually happen), BEFORE the * provider/summary call. `block` skips this compaction on auto/manual (`{compacted:false, * blocked:true}`); ignored on "forced". `additionalInstructions` is APPENDED to the summarization * instructions (never replaces them). A throw is swallowed on every path — treated as * no-block/no-instructions (the caller's wrapper owns tracing; an observer bug must never feed the * caller's breaker or kill PTL recovery). */ preCompact?: Hooks["preCompact"]; /** * design/134 (CC PostCompact parity) — observe-only, fired after the compaction LANDED * (appendCompaction done, post measurement taken). A throw is swallowed (same isolation contract * as {@link onCompaction}: the compaction already succeeded). */ postCompact?: Hooks["postCompact"]; /** * MF-18 修② ([496]③) — observe-only: fired when a summary call's serialized conversation INPUT was * truncated to fit the compaction model's context window (the ~300K-session shape that previously * guaranteed a provider prompt-too-long throw at every boundary). The caller bridges it to the * `compaction.input_truncated` trace frame (fidelity disclosure). Fired BEFORE the summary call, so * it reports truncation even when the attempt later fails. */ onInputTruncated?: (info: { label: "history" | "turn_prefix"; droppedChars: number; keptChars: number; }) => void; } /** * Compact the session in place when context usage exceeds the threshold. * * Two call sites, same policy: the within-task `turn_boundary` hook (design/64 §25 (A) — after the * session flush, before the next request's context rebuild) and `finish()` at task end. The caller * must ensure no session write is in flight (both sites are between model requests, post-flush). * * THROWS (council §26.6 #6 contract note): a summarization failure — including a summary that came * back EMPTY after the analysis-block strip (a real LLM call was burned, history left untouched) — * is thrown, not swallowed; callers are expected to catch, count it toward the §17.4 breaker, and * surface it via onError. A legitimate nothing-to-compact (under threshold/floor, no cut point) * returns `{compacted:false}` quietly. * Honors an optional cheaper `compactionModel` without mutating the harness's active model. * Zero-config: uses DEFAULT_COMPACTION_SETTINGS unless overridden; settings pathological for the * model's window are repaired via {@link sanitizeCompactionSettings}. */ export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{ compacted: boolean; /** design/134: set (with `compacted:false`) when a preCompact callback blocked this compaction * (auto/manual triggers only — never on "forced"). NOT a failure: the caller must not count it * toward the compaction breaker; the next boundary consults the callback again. */ blocked?: boolean; /** MF-18 修① ([496]③): set (with `compacted:false`) when compaction is DISABLED by settings * (`enabled:false`) — the request can never be honored this run. Lets the caller resolve a manual * /compact with the honest `"disabled"` outcome (and trace frame) instead of a structural noop. */ disabled?: boolean; tokensBefore?: number; freedTokens?: number; triggerTokens?: number; /** P-13: post-compaction context size in the SAME coordinate as `triggerTokens` (estimate + anchor-less * overhead). The caller's §25.2 regrowth floor = `postTriggerTokens × factor`, single-coordinate — never * `triggerTokens − freedTokens` (which mixed a usage-anchored trigger with a chars/4 freed delta). */ postTriggerTokens?: number; /** design/99 MF-18 `preserved_segment`: the session-tree entry id where the KEPT TAIL begins (the floor of * the surviving history after this compaction). Surfaced so the `compacted` wire event can carry it (CC * `SDKCompactBoundary.preserved_segment` parity); a consumer maps it to wire messages via its eventId↔entryId * map (the same one service keeps for resumeAt). Set only when `compacted` is true. */ firstKeptEntryId?: string; /** design/84 Seam C: true when this compaction reused a `summaryProvider` summary (no LLM call). */ reused?: boolean; /** Blackboard 2026-07-03 (ask ②): the working files re-read into the summary, in attachment order — * surfaced so the `compacted` wire event carries them and a shell renders CC's post-compact * `Read {path} (…)` cards. Set only when `compacted` is true and attachments were added. */ attachedFiles?: Array<{ path: string; chars: number; truncated: boolean; }>; /** design/123 D4: true when the NATURAL gate fired (est ≥ max(threshold, minTokens) — no force was * needed); false on a forced landing (`force`/`forceUnderThreshold`). Set only when `compacted`. */ naturalTrigger?: boolean; /** design/123 D4: whether the post-compaction size (`postTriggerTokens` coordinate) is STILL over the * sanitized auto threshold — the "couldn't compact it down" signal {@link nextTrimForceBackoff} * consumes. Set only when `compacted` and the post measurement succeeded. */ postOverThreshold?: boolean; /** TB telemetry B4 (service [397]): wall time of the whole compaction pass (context build → summary * call → history rewrite → post measurement). Set only when `compacted` — the `compacted` wire event * carries it so a consumer sees what a boundary pause actually cost. */ durationMs?: number; /** service [398] C3: set (with `compacted:false`) when the pass was over the auto threshold but the * anti-thrash floor (`minTokens`) suppressed it — the caller bridges this to `compaction.suppressed`. */ suppressedByFloor?: { estTokens: number; floor: number; }; }>; /** * design/123 D4 (r5) — trim-force BACKOFF transition (pure; the run-scoped bit lives in the caller). * Spiral protection replacing the §25.2 floor's role on the trim-force path: trim pressure re-arms * every request while the transcript is oversized, so an unconditional force would burn a summary * call at every boundary when summaries CANNOT shrink the context (the LONGRUN-1b incompressible * shape). The invariant: trim-force is permitted only while the LATEST landed compaction proved it * can bring the context back under the threshold (or no compaction has landed yet). Transitions, * evaluated after each maybeCompact result: * 1. no compaction landed → state unchanged (a structural no-op proves nothing either way); * 2. a landed compaction with `postOverThreshold` → "压不动": BACKOFF ON — compaction demonstrably * cannot reach under-threshold, so forcing more of them only burns summary calls; the §25.2 * floor policy takes back over (natural triggers are never gated by this bit); * 3. a landed compaction that posted UNDER the threshold → summaries compress again: BACKOFF OFF * (this is how the natural floor-crossing compaction of design/123 D4's reset clause re-enables * the trim-force — its healthy landing posts under threshold); * 4. post measurement unavailable → state unchanged (never flip on missing evidence); * 5. (COMPACTION-LIVE-AUDIT D-1, evaluated BEFORE rule 3) a TRIM-FORCED landing with * `freedTokens ≤ 0` → negative-yield: BACKOFF ON. The trim layer deceives the usage-anchored * estimate down into the sawtooth trough (live: est 18807 on a ~55k session), the forced pass * finds little compressible history, and summary + working-file attachments make the transcript * LARGER (live: 18807→44609, 12679→35214). `freedTokens` is the structural pre/post delta * (clamped ≥0, so ≤0 means "freed nothing") — orthogonal to rule 2/3's threshold test, which * misses this shape because the bloated post (44609) can still sit UNDER the threshold (44800), * so rule 3 alone would re-enable the force and repeat the negative-yield pass every boundary. * SEMA-ONLY DEFENSE, not a CC port: CC's full compact keeps tail 0 and monotonically shrinks * (capture §A4), and CC has no request-layer trim, so this regime cannot exist there — it is a * derived interaction of our trim seam × keep-tail × attachment mass. Scoped to trim-forced * passes only (`trimForced`), so Seam C reused-summary landings (freed≈0 by design) on the * natural/manual paths never trip it. Release path unchanged: backoff gates only the FORCE, a * later natural landing that posts under threshold (rule 3, non-trim-forced) turns it back off. */ export declare function nextTrimForceBackoff(prev: boolean, comp: { compacted: boolean; postOverThreshold?: boolean; freedTokens?: number; naturalTrigger?: boolean; }, trimForced?: boolean): boolean; /** design/119 S3 (CC 198 rapid_refill_breaker): mutable per-task refill tracking state. */ export interface RapidRefillState { lastCompactionTurn: number; count: number; } /** Fresh tracking state (no compaction seen yet). */ export declare function createRapidRefillState(): RapidRefillState; /** * design/119 S3 (CC 2.1.198 rapid_refill_breaker, :229717/:476338): record that a compaction just * happened at `turn` and report whether the thrash breaker should TRIP. A compaction landing within * `minGapTurns` of the previous one is a "rapid refill"; `tripAt` consecutive rapid refills = the * context is dominated by incompressible content and further compaction only burns summary calls. * CC terminates the task; sema's caller (runtask) deviates conservatively — it opens the existing * compaction breaker (no more compaction this task) and lets budget/PTL/turn limits end the run. */ export declare function recordCompactionAndCheckRapidRefill(state: RapidRefillState, turn: number, minGapTurns?: number, tripAt?: number): boolean; //# sourceMappingURL=auto-compaction.d.ts.map