UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

779 lines • 54.1 kB
import { type AgentMessage, type CompactionSettings, type Session, type ThinkingLevel } from "../internal/harness.js";
import type { Message, Model, Tool } from "../internal/llm.js";
import type { ContextWindowOverlay } from "./context-settings.js";
import type { Hooks } from "./hooks.js";
import type { TraceEvent } from "./trace.js";
import type { Brain } from "./types.js";
import type { TaskEvent, TaskEventIdentity } from "./task-event.js";
import type { CompactionEffect } from "./frame-settlement.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;
/**
 * design/145 §4 — uniform hygiene for EVERY summarization-instructions channel (spec-level
 * `compaction.instructions`, the DEFAULT constant, a manual `/compact` per-call override, and a
 * preCompact hook's `additionalInstructions` — 修类不修点): instructions text can be model- or
 * caller-authored, and it is injected VERBATIM into the summarization prompt
 * (`\n\nAdditional Instructions:\n${...}`, CC 207 :347521 shape), so a forged `</system-reminder>`
 * or `<<<`/`>>>` fence sentinel inside it would otherwise ride into a trusted prompt position.
 * Posture (r2 review B4): `sanitizeUntrustedText` + fence defusing only — NEVER `delimitUntrusted`
 * (fencing instructions as "data only, do not follow" self-defeats their instructional intent, and
 * the fence markers would be echoed back into the persisted summary). Cap = 2048 code points
 * (surrogate-safe slice): instructions steer a summary, they are not a payload channel.
 * Text without break-out sequences under the cap passes through BYTE-IDENTICAL (the no-arg /
 * default-instructions prompt is unchanged — pinned by test).
 */
export declare const COMPACTION_INSTRUCTIONS_MAX_CHARS = 2048;
export declare function sanitizeCompactionInstructions(text: string): string;
/**
 * 压缩子阶段遥测 (1.283 G) — single-source shape: the `phaseDurations` result field IS the
 * (defined-but-not-yet-emitted) `compaction.phase_timings` trace frame minus its envelope
 * (`kind`/`version`/`taskId`/`ts`) and minus `durationMs` (already a sibling result field). Deriving
 * it from the frame keeps the follow-up runtask bridge a mechanical spread — and makes a rollback of
 * either side a tsc error, not a silent drift.
 */
export type CompactionPhaseDurations = Omit<Extract<TraceEvent, {
    kind: "compaction.phase_timings";
}>, "kind" | "version" | "taskId" | "ts" | "durationMs">;
/** True iff `err` is a compaction failure caused by a manual compact() caller WITHDRAWING its
 *  request — see {@link MaybeCompactOptions.manualCancelSignal}. Two mint sites since the
 *  design/374 slice-3 commit-boundary re-check (C16): the summary call's own abort outcome, and
 *  the pre-persist "cancelled before commit" throw (a brain that ignored the abort finished the
 *  summary, but the withdrawal still fired before anything landed). Absence of the marker means
 *  the failure was NOT the withdrawal: a real summarizer error racing a late cancel stays
 *  unmarked so the caller's breaker/onError accounting still sees it. */
export declare function isCompactionManualCancel(err: unknown): boolean;
/**
 * design/145 §1/§3 — the window-safety decision surfaced to the caller BEFORE the summary call
 * (same pre-call posture as `onInputTruncated`: the disclosure must survive a later summary
 * failure). `kind:"fallback"` → the summary call was moved to the MAIN model (window safety);
 * `kind:"clamp_disclosure"` → the small model keeps the call but the conversation WILL be
 * clamp-truncated: `reason` says why the fallback did not happen ("budget" = the gate
 * denied it; "tolerance" = the truncation is within `clampTolerance` — bounded, but no longer
 * silent). All token figures are in the MAIN model's structural coordinate except
 * `headroomTokens` (the小 model's — it describes THAT window's capacity).
 */
export interface CompactionWindowSafetyInfo {
    kind: "fallback" | "clamp_disclosure";
    truncationRatio: number;
    /** Present on "clamp_disclosure": why the small model kept the call despite the truncation. */
    reason?: "budget" | "tolerance";
    /** Serialized conversation size incl. any re-sent previousSummary (chars ÷ main-model charsPerToken). */
    contentTokens: number;
    /** ≈ what fits in the compaction model's window (kept chars ÷ its charsPerToken); estimate. */
    headroomTokens?: number;
    /** 门A estimate: main-model input price × contentTokens, integer micro-USD (absent when pricing
     *  was unavailable — the gate then never blocked on budget). */
    estCostMicroUsd?: number;
}
/**
 * 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;
export declare function sanitizeCompactionSettings(settings: CompactionSettings, contextWindow: number | undefined): CompactionSettings;
/**
 * design/169-A — the CC-form fork context: the main lane's latest REAL brain request (same
 * `systemPrompt` bytes, same message-list bytes, same tools). The summary request built from it is
 * that request plus ONE appended user-role instruction message, so the provider's prompt cache
 * re-serves the already-paid main prefix — the economics that make "summarize on the main model"
 * cheap. Structural twin of the engine-side `CompactionForkContext` (engine/compaction/compaction.ts
 * — the engine layer cannot import core, so the shape is declared on both faces and must stay
 * assignable; `compact()`'s parameter type is the checked seam).
 */
export interface CompactionForkContext {
    systemPrompt?: string;
    /** The physical cache-span block layout of the main request, when it had one (Context.systemBlocks)
     *  — the fork must reproduce it or a multi-block pack's cache boundaries are silently lost. */
    systemBlocks?: Array<{
        text: string;
        cacheControlBoundary: boolean;
    }>;
    messages: Message[];
    tools?: Tool[];
    /** The model id that served the recorded request — part of the provider cache key; the fork
     *  eligibility gate refuses a snapshot from a different model (see the engine-side twin's doc). */
    modelId?: string;
}
export interface MaybeCompactOptions {
    /** Disclosure channel for CONTAINED host-callback failures in this scope (the RB-463
     *  isolation primitive's bounded first-failure per site). Absent ⇒ failures are counted only. */
    onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
    /** The session to compact. Typed on the INTERFACE face (typecheck 车 2026-07-24): every member this
     *  module touches lives on `Session` (the optional `getPromptEpoch` degrades honestly via `?.()`). */
    session: Session;
    /** This run's provider-declared prompt sections (the epoch declaration axis):
     *  the compaction-boundary epoch restatement/advance must hash the SAME artifact identity as the
     *  prepare-time pin, or every compaction under a declaration-carrying run would flap the pin. */
    epochDeclaredSections?: import("../prompt-assembly/epoch.js").EpochDeclaredSections;
    /** A compaction-boundary CENTER CANDIDATE ADOPTION, pre-assembled by the
     *  caller (prepare's seam): the descriptor advances with the candidate's declarations + provenance
     *  in the SAME CAS append as the new baseline, and `apply()` (the live prefix/fingerprint swap)
     *  runs ONLY after that append succeeded — a failed/blocked compaction must swap nothing. */
    centerAdoption?: {
        /** Absent together with `clear` ⇒ never happens; adoption carries the new digest, ROLLBACK
         *  (explicit center disable) carries `clear:true` and no digest. */
        centerArtifactDigest?: string;
        sourceRevision?: string;
        clear?: true;
        declaredSections: import("../prompt-assembly/epoch.js").EpochDeclaredSections;
        /** the post-swap prompt overhead in the trigger estimate's coordinate: used for THIS
         *  compaction's post-measurement so PTL recovery/anti-thrash see the swapped prefix size. */
        overheadTokensAfter: number;
        apply: (committedArtifactDigest?: string) => void;
    };
    /** Model whose context window bounds the budget (the model that runs the next turn). */
    model: Model;
    /** The deployment's LIVE per-model window overlay for THIS pass ({@link RunnerDeps.contextSettings}
     *  keyed by the leg's catalog word). Read by the caller at the moment of the pass, never snapshotted:
     *  every window number below — the trigger, the sanitize clamp, the disclosed `contextUsage` — is
     *  measured through it, so an operator who lowers a model's window reaches the legs already running.
     *  Absent ⇒ the model card's own declarations, unchanged. */
    contextOverlay?: ContextWindowOverlay;
    /** Optional cheaper model used to produce the summary. Falls back to `model`.
     *  design/145 §1 (semantics精确化): used only while WINDOW-SAFE and the budget gate allows — when a
     *  dry-run clamp shows the conversation would lose more than `settings.clampTolerance` of its
     *  chars to fit this model's window, the summary call falls back to the MAIN model (`model`),
     *  which is always window-safe (the content grew inside its window; CC 207 :348496 compresses
     *  with the main-loop model unconditionally — the fallback IS the CC posture, the independent
     *  compaction model is sema's superset). Subagent note (design/145 §5): delegated children never
     *  inherit this option (subagent.ts buildChildSpec carries no compactionModel), so a child always
     *  self-compacts on its own main model — the judgment is structurally never triggered there. */
    compactionModel?: Model;
    /**
     * design/169-A — accessor for the main lane's latest real request context (see
     * {@link CompactionForkContext}). Consulted per pass, at call time (the ref advances with every
     * main request). Used ONLY when the summary call runs on the MAIN model (no `compactionModel` in
     * play — an independent small model has no main prefix to share); `undefined` (or an accessor
     * returning `undefined`: no main request issued yet, or a library caller driving maybeCompact
     * directly) keeps the independent request form byte-for-byte. A fork-form prompt-too-long falls
     * back ONCE to the independent form inside `compact()` (the fork form must not clamp — a clamp
     * rewrites the cached prefix, defeating the form's purpose).
     */
    forkContext?: () => CompactionForkContext | undefined;
    brain: Brain;
    getApiKeyAndHeaders?: (model: Model) => Promise<{
        apiKey: string;
        headers?: Record<string, string>;
    } | undefined>;
    thinking?: ThinkingLevel;
    settings?: CompactionSettings;
    /** Appended to the summarization prompt to steer what must be preserved. Sanitized inside
     *  maybeCompact via {@link sanitizeCompactionInstructions} (design/145 §4 — every channel,
     *  including the preCompact hook's additions, gets the same treatment). */
    customInstructions?: string;
    /**
     * The user's / project's own INSTRUCTION-FILE content (the CLAUDE.md family this
     * deployment loads into an ordinary session), for the INDEPENDENT summary form.
     *
     * Why it needs a seat at all: the summarization prompt carries CC's closing invitation to follow
     * "additional summarization instructions provided in the included context", and the independent
     * form swaps in its own system prompt — so the loader-mounted files never reached that
     * summarizer and the invitation named nothing this form delivers. Supplying this makes it true:
     * the content is rendered as the request's first section, ahead of the conversation, with
     * instruction standing for the summarization task only. The FORK form carries it too — in the ONE
     * message that form appends, so the cached main prefix is untouched: it preserves whatever system
     * prompt the main request had, and nothing establishes that this prompt contains the files, so a
     * conforming fork response would otherwise succeed having never seen them. Budget for the seat's
     * bytes on BOTH forms.
     *
     * TRUST TIER — host supply, same as `systemPrompt`: this string is handed to the model as
     * instructions. Never fill it from tool output, model text, or any other in-band channel. It is
     * NOT sanitized or length-capped here (unlike `customInstructions`, which is per-pass steering
     * text of unknown provenance) precisely because it is the deployment's own prompt material.
     *
     * ABSENT (default) ⇒ every summarization request byte is what it was before this option existed.
     * That invariant is over the summarization REQUEST; the one deliberate effect outside it — the
     * new `<instruction-files>` authority envelope being defused on the fenced/shelled lanes for every
     * deployment — is stated at `CompactionPreparation.contextInstructionFiles`.
     */
    contextInstructionFiles?: string;
    signal?: AbortSignal;
    /**
     * Manual compact() withdrawal signal — the composed cancel signals of the manual callers THIS
     * pass claimed (a strict SUBSET of `signal`, which additionally carries the task's own abort).
     * Used only for failure ATTRIBUTION: when the summary call's failure is its abort outcome and
     * this signal has fired, the thrown error is stamped so {@link isCompactionManualCancel} holds —
     * the caller then treats the pass as withdrawn ("mooted") instead of summarizer evidence. It
     * never aborts anything by itself (the same signals already ride inside `signal`).
     */
    manualCancelSignal?: AbortSignal;
    /**
     * design/145 门A — budget interaction for the window-safety fallback: a MAIN-model re-compaction
     * of a near-window conversation is one giant prefill whose cost lands in the SAME ledger the
     * budget gate enforces (`recordCompactionUsage` → `stats.costMicroUsd` → `overBudget()`), so an
     * un-gated fallback under a cost cap is the "钱花了任务照死" double loss. When present AND
     * `mainInputPer1M` is known, the fallback is denied if `spentMicroUsd() + contentTokens ×
     * mainInputPer1M > capMicroUsd` — the pass then keeps the small model and DISCLOSES the clamp
     * (`clamp_disclosure`, reason "budget"). Absent field / absent pricing = the gate never blocks
     * (design: "pricing/cap 不可得=门不拦").
     */
    fallbackBudget?: {
        /** Live cumulative spend reader (the caller's `stats.costMicroUsd` at decision time). */
        spentMicroUsd: () => number;
        /** The run's cost ceiling, integer micro-USD. */
        capMicroUsd: number;
        /** MAIN-model input price, absolute USD per 1M tokens (`ModelPricing.inputPer1M`) — numerically
         *  equal to micro-USD per token, so `est = contentTokens × mainInputPer1M` needs no unit dance. */
        mainInputPer1M?: number;
    };
    /**
     * design/145 §3 — pre-call observer for the window-safety decision (fallback / disclosed clamp).
     * Fired BEFORE the summary call (survives a later failure, `onInputTruncated` posture); throws
     * are swallowed (observe-only). The caller bridges it to the `compaction.model_fallback` /
     * `compaction.clamp_disclosure` trace frames.
     */
    onWindowSafety?: (info: CompactionWindowSafetyInfo) => void;
    /**
     * 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<string | null | {
            withheld: {
                pattern: string;
            };
        }>;
        /** Max modified files to attach. Default 3. */
        maxFiles?: number;
        /** Per-file char cap (≈4 chars/token). Default 16_000 (~4k tokens). */
        maxCharsPerFile?: number;
        /**
         * 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[];
        /**
         * RB-197② (CC `compact.ts:1415-1463` `shouldExcludeFromPostCompactRestore` parity): paths already
         * re-seeded through a DIFFERENT channel every turn (the deployment's declared instruction files —
         * `ProjectMemoryLoad.instructionSources`, e.g. a project/user CLAUDE.md) are excluded from the
         * `recentlyReadFiles`/fallback candidate set — re-attaching them here would just duplicate content
         * the system-prompt lane already carries. A plain path list, not a hardcoded filename: core still
         * never decides what counts as "an instruction file" on its own (types.ts's own seam rule) — it
         * only trusts what the deployment already declared.
         */
        instructionSourcePaths?: ReadonlyArray<string>;
        /**
         * `recentlyReadFiles` yields CANONICAL keys (resolveKey
         * output — the same coordinate `ReadFileState` is keyed in), but `instructionSourcePaths` and the
         * kept-tail scan inside `maybeCompact` (which reads raw tool-call arguments via
         * `extractFileOpsFromMessage`) do NOT share that coordinate — a model-typed relative path and its
         * canonical form are different strings, so a plain `Set.has()` between them silently never matches
         * even for the identical file. Optional callback (present exactly when there's a containment root
         * to resolve against — i.e. hands enabled) that canonicalizes a raw path the SAME way the hands
         * toolkit does, so `maybeCompact` can normalize every side of the comparison before excluding.
         * Best-effort: a throw/rejection is caught by the caller and treated as "leave this one path
         * unnormalized" (a safe direction — worst case is a missed exclusion, not a wrong one). Absent
         * when there is no containment root (hands disabled) — in that regime every path source is
         * ALREADY in the same raw coordinate, so the fallback exact-string comparison is already correct.
         */
        normalizePath?: (raw: string) => Promise<string>;
        /**
         * A kept-tail Read tool-CALL's mere presence does not prove the
         * file's real content is visible there: its toolRESULT may be a dedup stub ("unchanged since you
         * last read it…") whose ORIGINAL full transmission sits in the summarized prefix THIS compaction is
         * about to cut away — preserving read-state for such a path would leave both the kept tail and the
         * next read pointing at content that is nowhere in context (RB-197①'s failure, reintroduced one hop
         * removed). Optional predicate (present exactly when the caller's Read tool actually emits a
         * recognizable stub marker) so `maybeCompact` can require at least one SUBSTANTIVE kept-tail Read
         * result before trusting a path as genuinely visible; absent ⇒ every non-error kept-tail Read is
         * trusted (the pre-this-fix behavior — still safe when the caller has no stub concept at all).
         */
        isDedupStubResult?: (resultText: string) => boolean;
    };
    /**
     * Fires ONCE per LANDED compaction (after `appendCompaction` committed the new baseline), with the
     * files that were attached WHOLE (untruncated) into the summary. The Runner wires this to the read-state
     * invalidation (`applyContextShrinkToReadFileState`): the summary just replaced the history the read-dedup
     * stubs point at, so stale entries must drop and the re-attached content re-registers. Kept OUTSIDE
     * `workingFileAttachments` deliberately — the invalidation is owed even when attachments are disabled
     * (an empty array then). Best-effort: a throwing callback must not fail the landed compaction.
     *
     * `preserveReadState`:
     * canonical-coordinate paths (same shape as `normalizePath`'s output) whose read-state entry must
     * survive the compaction's clear even though they are NOT in `attachedComplete` — every file
     * `maybeCompact` deliberately skipped re-attaching, for EITHER exclusion reason (kept-tail-visible,
     * or matches a declared instruction source). Without this the read-state clear (see
     * `applyContextShrinkToReadFileState`) would wipe them anyway, defeating both the read-before-edit gate
     * and the next read's dedup for exactly the files this optimization was supposed to leave untouched.
     * (The instruction-source half of this was missing in an earlier revision: `instructionSources` and
     * `seededFiles` are independent `ProjectMemoryLoad` fields — a path can be declared in the former
     * without ever being seeded via the latter, so an instruction-excluded file the model explicitly Read
     * has a live, non-seeded entry that needs the SAME preservation as a kept-tail one.)
     */
    onApplied?: (attachedComplete: ReadonlyArray<{
        path: string;
        content: string;
    }>, preserveReadState?: ReadonlyArray<string>) => void;
    /**
     * #1036 — THE COMMIT-BEAT EFFECT SINK: what this compaction did to the transcript, reported
     * once, right after the `appendCompaction` that landed it. `summarizedEntryIds` = every entry id
     * that fell into the summarized prefix (before `firstKeptEntryId`); `"unknown"` when the kept-tail
     * anchor is not in the branch — the conservative reading (the same one
     * {@link countSummarizedCrossSessionMessages} already takes) is that the whole branch went into
     * the summary.
     *
     * Why HERE and not at the three call sites: a fact about a landed compaction has exactly one
     * birthplace, and an effect minted per lane would be a rule written three times (and, the two
     * times someone forgets, a lane where a summarized frame is silently never repaired). The three
     * lanes reach it through the options assembly they already share.
     *
     * Observe-only, like every post-landing side effect here: a throwing sink must not fail a landed
     * compaction.
     */
    onCommitted?: (effect: CompactionEffect) => void;
    /**
     * env-tail migration — the git-status lane's compaction interaction (two-phase receipt). The git
     * frame lives on the MESSAGE plane, does not enter the summary, and the projection strips it from
     * serialized history — so a landed compaction would silently lose the model's git view while the
     * announced hash keeps suppressing every re-send. When present:
     *   - `pending` is restated on the compaction entry's `details.gitAnnouncement` with
     *     `pending: true` in the SAME CAS as the new baseline (the promptEpoch/announcedListings
     *     restatement posture) — any reader between the landing and the re-assert receipt sees an
     *     OWED announcement, never a false "announced";
     *   - `land()` is invoked immediately after the landed baseline, BEFORE any context rebuild or
     *     retry (all three routes — turn-boundary auto, prompt-too-long recovery, end-of-task finish
     *     — funnel through this one append): it re-appends the current frame and writes the
     *     announced mirror once the append receipt is in hand. `land` contains its own failures (a
     *     failed re-assert leaves the pending restatement authoritative — the next boundary/leg
     *     re-announces); a throw here must never fail a LANDED compaction, so the call is belted.
     */
    gitRestate?: {
        pending: {
            kind: "full" | "degraded" | "unavailable" | "non-repo";
            hash: string;
        };
        land: () => Promise<void>;
    };
    /** The approval-posture directive's post-compaction restatement (the runner's `compactionRestateOptions`):
     *  runs right after `gitRestate.land`, under the same belt and for the same reason — the compacted
     *  context may have lost an engine frame the run still needs, and every reader of the new baseline
     *  (the next model request first) must see it restated. */
    postureRestate?: () => Promise<void>;
    /**
     * 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<string | null | undefined>;
    /**
     * 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`, a
     * SEMA choice — the earlier "(CC parity)" label was unsupported and is withdrawn (anchoring
     * re-check 2026-08-23: CC 2.1.223 has no external summary provider and no reuse concept at all;
     * its three 3s are the PTL retry cap `b$d`, the consecutive-compaction-failure breaker `C$d` and
     * the rapid-refill trip `Q3u`, none of them this). 3 is picked for the same reason those are: a
     * small bound on how long a degraded path may keep answering before the real one is forced.
     * `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).
     * ABSENT when the post measurement failed — never a stand-in ("fully effective" is not true of a
     * landing that leaves a summary behind). Every consumer reads absence as "no evidence": the floor
     * stays where it is and the backoff does not flip.
     */
    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 修② — 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;
    /**
     * The design/36 ToolSearch activation set LIVE at this pass (the
     * runner threads `[...prepared.activeTools]` on all three lanes: boundary, PTL recovery, finish).
     * Persisted into the landed compaction entry's details (`CompactionDetails.activeTools`) so the
     * NEXT task boundary's re-derivation — whose transcript source (ToolSearch toolCall parts) this
     * compaction just replaced with prose — re-seeds from a structured carrier instead (the
     * checkpoint seed's `resume.seed.activeTools` sibling; prepare-task consumes it). Absent or
     * empty ⇒ no key is written, byte-identical to pre-RB-402 entries.
     */
    activeTools?: ReadonlyArray<string>;
}
/**
 * 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 修①: 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;
    /** Set (with `compacted:false`) when the pass PASSED the trigger gate
     *  (natural or forced) but found nothing to summarize — no valid cut point / empty history. Was
     *  previously indistinguishable from the quiet under-threshold return, so a forced pass with
     *  nothing to do looked identical to one that never wanted to run; the caller's outcome event
     *  (`compaction_outcome{outcome:"noop"}`) reads this flag. */
    noop?: true;
    tokensBefore?: number;
    /** The §25.2 STRUCTURAL pre/post delta in ONE coordinate — the only subtractable quantity here.
     *  Absent ⇒ the post measurement failed and no delta was taken. */
    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;
    /** 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: 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;
    /** 压缩子阶段遥测 (1.283 批 G 观察项 / design/145 §6 正交后续批 — TB 观测到整段 ~55s 停顿,需要
     *  子阶段读数支撑归因): wall-time split of THIS landed pass, in the same Date.now() coordinate as
     *  `durationMs`. `prepareMs` = context build → trigger math → cut-point selection → hooks/provider
     *  consult → clamp dry run + 门A/门B gating (everything before the summary call). `summaryMs` = the
     *  summary LLM leg (`compact()`: serialization → clamp → completion, incl. any PTL retries) — ABSENT
     *  on a provider-reuse pass (no LLM call happened; its provider consult time sits in `prepareMs`).
     *  `persistMs` = working-file attachments → session rewrite (`appendCompaction`) → post measurement →
     *  observers. The phases tile the pass exactly: prepareMs + (summaryMs ?? 0) + persistMs ===
     *  durationMs. `ptlRetries` = reactive prompt-too-long retries burned inside this pass's summary
     *  call(s) (both split-turn legs accumulate into the one counter; 0 on provider reuse). Set only when
     *  `compacted`. Exposure path note: runtask deliberately NOT touched this batch — the bridge to the
     *  `compaction.phase_timings` trace frame (trace.ts, defined-not-yet-emitted) is a follow-up wiring
     *  batch that adds an additive passthrough next to runtask's existing
     *  `...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {})` spread. */
    phaseDurations?: CompactionPhaseDurations;
    /** 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;
    };
    /**
     * Set (with `compacted:false`) when the trigger predicate could not be EVALUATED at all: the model
     * declares no usable autocompact window (non-finite or <= 0), so `shouldCompact` refuses on the
     * sentinel and `contextUsage` is honestly absent.
     *
     * This is a different state from "under the threshold", and it used to be indistinguishable from it:
     * both returned a bare `{compacted:false}` with no `contextUsage`, no trace and no error, so a
     * deployment whose model config lost its window had auto-compaction permanently disabled and the
     * only notification was that nothing ever happened. The context still grows, the provider still
     * eventually refuses the request, and the operator has nothing to read. The caller bridges this to
     * a once-per-task host disclosure — silence is the defect here, so a noisy line is the fix.
     *
     * `estTokens` is the size this pass DID manage to estimate (structural + prompt overhead when no
     * usage anchor is available), so the disclosure can say how big the context already is even though
     * it cannot say how big the window is.
     */
    unevaluableWindow?: {
        estTokens: number;
        /** WHICH of the three window fields the `??` precedence actually selected. A defined-but-unusable
         *  higher-precedence field (`autoCompactTokens: 0`) shadows a healthy lower one, so "no window is
         *  declared" is the wrong story on that path — the caller reports THIS field, not all three. */
        windowField: "autoCompactTokens" | "contextTokens" | "contextWindow";
        /** The unusable value that field carried, when it carried one at all (absent = nothing declared). */
        windowValue?: number;
        modelId?: string;
    };
    /**
     * The trigger inputs THIS pass used, reported on
     * EVERY return path — including the ordinary "nothing to do" one. A consumer rendering "how full is the
     * context" was otherwise forced to re-derive the numerator itself, and a re-derivation that drifts from
     * the engine's own is the projection-antipattern this repo keeps paying for: the displayed percentage
     * would disagree with when compaction actually fires.
     *
     * `usedTokens` is the exact first argument to `shouldCompact` (usage-anchored when the anchor is fresh,
     * structural + fixed prompt overhead otherwise — see the derivation above). `windowTokens` is the
     * AUTOCOMPACT window (`autoCompactTokens` when the model declares one, else its context window), NOT
     * necessarily the physical request window. `compactAtTokens` is the threshold those two are compared
     * against, so `usedTokens > compactAtTokens` is precisely the engine's own trigger predicate.
     */
    contextUsage?: {
        usedTokens: number;
        windowTokens: number;
        compactAtTokens: number;
    };
    /** design/145 §3: this pass's summary call FELL BACK from the independent compaction model to the
     *  main model (window safety). Rides the `compacted` wire event as an additive field. */
    modelFallback?: true;
    /** design/145 §3: why the fallback happened — "window" is the only reason today. */
    fallbackReason?: "window";
    /** design/145 §3: the dry-run truncation ratio of a pass whose summary input WAS (or would be)
     *  clamp-truncated on the small model — set when a gate denied the fallback OR the ratio was
     *  within tolerance but non-zero. */
    clampedRatio?: number;
    /** design/145 §3: "budget" = the gate denied the fallback; "tolerance" = within-tolerance
     *  ordinary truncation (disclosed, not silent). */
    clampReason?: "budget" | "tolerance";
}>;
/** What a landed compaction hands the wire frame. Structural, so both the full `maybeCompact` result
 *  and the settle lane's narrower `EndOfTaskCompaction` satisfy it without either importing the other. */
export interface CompactedFrameSource {
    tokensBefore?: number;
    freedTokens?: number;
    postTriggerTokens?: number;
    triggerTokens?: number;
    durationMs?: number;
    phaseDurations?: CompactionPhaseDurations;
    firstKeptEntryId?: string;
    attachedFiles?: Array<{
        path: string;
        chars: number;
        truncated: boolean;
    }>;
    modelFallback?: true;
    fallbackReason?: "window";
    clampedRatio?: number;
    clampReason?: "budget" | "tolerance";
    /** Cross-session message frames that sat in the summarized prefix (present iff ≥ 1). */
    summarizedCrossSessionMessages?: number;
}
/**
 * THE mint of the `compacted` wire frame — one function, three lanes (the turn boundary, the
 * prompt-too-long recovery, the end-of-task settle). The three used to carry a verbatim copy of the
 * same fourteen-line literal, which is how a field reaches the wire on two lanes out of three: a key
 * added to one copy is invisible in the others' diffs, and the only guard against it was a test that
 * grepped the SOURCE TEXT of all three for a matching line. A behavior can be pinned; a copy can only
 * be spelled.
 *
 * What differs per lane is exactly `trigger` and the identity envelope, so those are the parameters;
 * everything else is derived here, once. Absence semantics live on the event type
 * ({@link import("./task-event.js").TaskEvent}) and are the same whichever lane fires.
 */
export declare function mintCompactedEvent(comp: CompactedFrameSource, trigger: "auto" | "manual" | "forced", identity: TaskEventIdentity): Extract<TaskEvent, {
    type: "compacted";
}>;
/**
 * How many cross-session message frames the summarized prefix held: the engine-note user messages
 * whose text is the drain's `<cross-session-message …>` envelope, before the kept tail begins. The
 * frames were delivered (acked from the box) and are now only whatever the summary kept of them — the
 * count is what lets a consumer say so; `0` is reported as absence.
 */
export declare function countSummarizedCrossSessionMessages(branch: ReadonlyArray<{
    type: string;
    id: string;
    message?: {
        role: string;
        provenance?: string;
        content: unknown;
    };
}>, firstKeptEntryId: string): 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 — but the ORIGINAL justification for that label was wrong
 *      on both of its premises, and the corrected pair still supports it. (a) "CC's full compact
 *      keeps tail 0 and monotonically shrinks" describes the `GNo` branch, which a LOCAL threshold
 *      compaction never reaches (CC 2.1.223 `y9s` :432704 routes local passes to the reactive
 *      pipeline, which preserves the last group verbatim — `YMo` :399971, `s = 1` :399982); CC's
 *      default posture therefore has a keep-tail, exactly as ours does. (b) "CC has no
 *      request-layer trim" holds only for the MAIN conversation request: CC does own a
 *      keep-the-newest-groups-that-fit primitive (`UPb` :645223), whose only caller is the
 *      prompt-hook evaluator's own transcript window, not a main-lane request. What actually makes
 *      this regime ours is the third factor — the trim seam feeding a deceived usage anchor, and
 *      attachment mass re-entering after the fold — not the absence of a keep-tail. 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;