@sema-agent/core
Version:
Stateless, task-oriented AI agent core
494 lines (493 loc) • 37 kB
TypeScript
import type { Context, Message, Model, StreamFn, Tool, Usage } from "../llm/index.js";
import { type AgentCoreCompletionRuntimeDeps } from "../loop/runtime-deps.js";
import type { AgentMessage, ThinkingLevel } from "../loop/types.js";
import { CompactionError, type Result, type SessionTreeEntry } from "../harness/types.js";
import { type FileOperations, type InvokedSkillRetention } from "./utils.js";
/** File-operation details stored on generated compaction entries. */
export interface CompactionDetails {
/** Files read in the compacted history. */
readFiles: string[];
/** Files modified in the compacted history (sorted, for stable display). */
modifiedFiles: string[];
/**
* The same modified set ordered most-recently-touched FIRST. Top-N consumers (working-file
* attachments) pick from here so an alphabetical sort cannot push the file the model actually
* needs out of the window (the LONGRUN-2b miss). Optional: absent on details persisted before
* this field existed.
*/
modifiedFilesByRecency?: string[];
/**
* Invoked-skill retention area (CC 2.1.207 parity): the skills invoked in the COMPACTED range
* (merged over the previous compaction's retained set), most recent invocation first, already
* budgeted (per-skill truncation marker + total-area skip semantics — see compaction/utils.ts).
* Rendered onto the compaction summary message by `buildSessionContext`; kept OUT of the summary
* string so the summarizer LLM never re-chews skill bodies. Optional: absent on details persisted
* before this field existed, and omitted when no skill was ever invoked.
*/
invokedSkills?: InvokedSkillRetention[];
/**
* RB-398-b①: CUMULATIVE count of transcript messages this compaction's summary stands in
* for (this range's folded messages + the previous compaction's carried count). WRITE-side
* accounting on purpose: deriving the count at render time from the branch walk broke the F3
* "bounded wake equals full wake" invariant (a floored storage cannot see below the floor, so the
* two sides disagreed — caught by test/session-floor.test.ts). Drives the summary wrapper's scale
* disclosure (`buildSessionContext` → `CompactionSummaryMessage.elidedMessages`). Optional-absent
* on pre-field entries (⇒ no disclosure, byte-identical legacy wrapper).
*/
elidedMessages?: number;
/**
* PTL-shortfall accounting: CUMULATIVE count of folded messages the summary does NOT cover — the ones a reactive
* prompt-too-long retry shed from the summarization input (`summarizeWithPtlRetry`'s group
* rollback) after `prepareCompaction` had already committed the cut. Those messages leave the
* transcript with everyone else (so {@link elidedMessages} stays the honest FOLD count) but
* nothing in the summary stands in for them, and the wrapper used to claim otherwise.
*
* Deliberately a SECOND number rather than a correction of the first: lowering `elidedMessages`
* to the covered count would under-report the fold scale, which is the exact judgment that field
* exists to inform ("was a whole investigation folded, or three turns?"). Same cumulative +
* write-side + optional-absent discipline as its sibling; a lossless pass omits it entirely, so
* details written by the common path stay byte-identical. Counted in the summarization-input
* coordinate — a LOWER BOUND on uncovered transcript messages (see
* {@link SummarizationInputTruncation.droppedMessages}).
*/
unsummarizedMessages?: number;
/**
* RB-398-c: `<persisted-output ref="…">` handles found in the COMPACTED range
* (merged over the previous compaction's retained set, oldest-first, capped at
* {@link PERSISTED_OUTPUT_REFS_MAX_ENTRIES} newest). The offloaded blobs outlive the preview that
* carried their only address — without this channel the handle survived only if the summarizer
* model happened to copy the string. Rendered as a deterministic summary footer by `compact()`
* (`formatPersistedOutputRefs`), the same channel `readFiles`/`modifiedFiles` already have.
* Optional-absent when the range held no offloaded results.
*/
persistedOutputRefs?: string[];
/**
* RB-402: the activated deferred-tool names LIVE at the
* moment this compaction landed — the design/36 activation state whose transcript carriers
* (the ToolSearch toolCall parts, and since RB-431 the successful direct calls, that
* `extractDiscoveredToolNames` replays at every task boundary)
* this very compaction just replaced with summary prose. Same field shape as the checkpoint
* seed's `CheckpointState.activeTools` (design/45 §4.bis — the sibling seam that already
* survives a suspend/resume). Written by the runtask compaction lanes
* (`MaybeCompactOptions.activeTools` → `maybeCompact`'s appendCompaction, NOT by
* `prepareCompaction` — the engine layer has no view of the runner's activation Set); consumed
* by prepare-task's boundary re-derivation via `readCompactionActiveTools` and intersected with
* the NEXT leg's own deferred registry (a name no longer deferred there is inert).
* Optional-absent on pre-RB-402 entries and on legs with no activations.
*/
activeTools?: string[];
}
/** Generated compaction data ready to be persisted as a compaction entry. */
export interface CompactionResult<T = unknown> {
/** Summary text that replaces compacted history in future context. */
summary: string;
/** Entry id where retained history starts. */
firstKeptEntryId: string;
/** Estimated context tokens before compaction. */
tokensBefore: number;
/** Optional implementation-specific details stored with the compaction entry. */
details?: T;
}
/** Compaction thresholds and retention settings. */
export interface CompactionSettings {
/** Enable automatic compaction decisions. */
enabled: boolean;
/** Tokens reserved below the context window for the TRIGGER threshold (threshold = window −
* reserve). Historically this single knob ALSO seeded the summary legs' output budget
* (0.8×/0.5×); that second role is now owned by {@link summaryOutputBudgetTokens} (with this
* field as its default base), so trigger-geometry repairs (`sanitizeCompactionSettings`) can
* never silently move the summary budget. */
reserveTokens: number;
/**
* Explicit base for the summary OUTPUT budget (the 0.8× history-leg / 0.5× turn-prefix-leg
* max_tokens derivation), decoupled from `reserveTokens`' trigger role. Absent ⇒ `reserveTokens`
* is the base (historical behavior). Either way the effective base is capped by the SUMMARY
* model's own max output (see {@link summaryOutputBudgetTokens}) — budgeting output the model
* cannot emit only starves the pre-send input clamp. `sanitizeCompactionSettings` passes this
* field through untouched: it is not trigger geometry.
*/
summaryOutputBudgetTokens?: number;
/** Approximate recent-context tokens to keep after compaction. */
keepRecentTokens: number;
/** design/145 §1 — window-safety clamp tolerance for an independent (smaller-window) compaction
* model: when the summarization input would be clamp-truncated by MORE than this ratio (the
* {@link dryRunSummarizationClamp} judgment), the summary call becomes a candidate to fall back
* to the MAIN model, which is always window-safe (the content grew inside its window). 0 = any
* truncation is a fallback candidate; 1 = never fall back (the pre-145 clamp-only behavior, the
* escape hatch). Default {@link DEFAULT_CLAMP_TOLERANCE}. */
clampTolerance?: number;
}
/** design/145 §1 — default `clampTolerance`: head+tail clamping keeps ≤10% mid-section loss BOUNDED
* AND DISCLOSED (`compaction.clamp_disclosure`), not "lossless" — the honest wording is in the
* design doc. Above this, a cheap-model summary is judged too lossy and the main model takes over. */
export declare const DEFAULT_CLAMP_TOLERANCE = 0.1;
/**
* Default compaction settings used by the harness.
*
* `keepRecentTokens: 0` is a ZERO BUDGET, not "keep nothing" — {@link findCutPoint} has a
* structural floor underneath the budget (see its doc), so the cut still lands on the last valid
* cut point and the summary is followed by the last exchange VERBATIM. Measured on the real
* Runner: the first request after an auto-compaction is `[summary wrapper][last assistant turn]
* [new user]`, and a within-turn boundary keeps `[emitting assistant][its tool results]` because
* a trailing tool-result batch pulls the floor back to its call site.
*
* That is the CC posture, arrived at through a different knob. CC 2.1.223's LOCAL threshold
* compaction always routes to the reactive pipeline (`y9s` :432704 — the `GNo` full-compaction
* branch with `messagesToKeep: []` :431788 is reached only for a remote/in-process runner, and a
* `source:"auto"` window makes the local threshold not fire at all, `hky` :432687 / `aYe` :242816).
* Reactive splits the session into GROUPS (`Ubr` :399459 — a new assistant `message.id` starts a
* group and its tool results ride with it) and preserves the last `s = 1` of them verbatim
* (`YMo` :399971 / :399982), zeroing the preserved assistants' usage on the way out (`ryt` :401285
* — our `buildSessionContext` kept-tail `stripAssistantUsage`, design/123 D1, is the same act).
* `s` only grows on ITS retry path, so one group is the default posture on both sides.
*
* design/169-C moved this default from 20000 to 0 and anchored the change on `messagesToKeep: []`;
* the anchor names the minority path, but the VALUE lands on the majority path's behavior, and
* raising it again would keep MORE than CC does (measured: a 2000-token budget keeps two whole
* exchanges). A positive `keepRecentTokens` remains the explicit knob for callers that want a
* wider verbatim tail than either engine's default.
*
* Known residual (recorded, not repaired): when the last entry is a USER message the floor takes
* it alone, where CC's group would also carry the assistant before it. The runner's lanes evaluate
* compaction at turn boundaries and at finish — after the assistant/tool results, before the next
* prompt — so that shape belongs to direct `maybeCompact` callers.
*/
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
/**
* design/123 D2 — default structural coefficient (chars per token) for every structural token
* estimate. Threaded as an explicit parameter through the estimate chain so caller-side coordinates
* never split (estimate vs cut-point vs defense); per-model override via `Model.charsPerToken`.
*
* 4 → 3 (design/374 slice 4, ruled 2026-08-28) — this is a RE-ANCHORING, not a divergence.
* CC's coefficient is a two-branch function, not a constant: `sf(model)` (cli250.js, `Z6` table) is
* `Z6.has(family) ? 4 : 3`
* where `Z6` lists exactly the OLD families (claude-3.x, opus-4-0/4-1/4-5/4-6, sonnet-4-0/4-5/4-6,
* haiku-4-5). **3 is CC's DEFAULT branch; 4 is the whitelist hit** — and CC's own current generation
* (opus-5 / sonnet-5) is not in the table, so upstream estimates its own newest models at 3. This
* engine is BYOM: its models are always outside that table, so 4 was the branch CC would never have
* taken for them. We had it backwards, and said so in the parity register as an open item
* (`compaction-chars-per-token-default-4-vs-cc-out-of-table-3`) until this ruling.
*
* Direction of the change: a token estimate is chars/coefficient, so a SMALLER coefficient estimates
* MORE tokens — every structural trigger (compaction threshold, the clearStale frontier, the guard,
* the cut point, the prompt-overhead term) fires EARLIER. That is the conservative direction and the
* measured one: design/123's own live calibration puts real code near 2.18 chars/token and CJK at
* 1–1.6, i.e. the truth is on 3's side of the choice, and the failure mode 4 produced was the
* defense self-certifying an over-window request as fitting.
*
* NOT affected, deliberately: the microCompact ≥20k minimum-savings estimate
* (`context-edit.ts` `structuralClearSavings`) hard-codes 4. That is CC-exact and stays — CC's own
* `tQt` calls its estimator with NO coefficient argument, taking that function's `t = 4` default,
* while the CONTEXT estimator is called WITH the per-model coefficient. The asymmetry is upstream's,
* and flipping this constant must not move that gate (pinned).
*/
export declare const DEFAULT_CHARS_PER_TOKEN = 3;
/** Calculate total context tokens from provider usage. */
export declare function calculateContextTokens(usage: Usage): number;
/**
* Return usage from the last successful assistant message in session entries.
*
* design/123 D1 stale-anchor note (fable-m2, same-family point): this scans RAW session entries, so
* unlike `buildSessionContext` (which shallow-strips the usage of kept-tail assistants replayed
* across a compaction boundary — the anchor "naturally disappears", CC 198 parity) it CAN still see
* a pre-compaction anchor. Any consumer using this across a compaction boundary must treat such an
* anchor as STALE: it reflects the pre-compaction request and overestimates the post-compaction
* context. (Currently exported with no in-repo production consumer.)
*/
export declare function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | undefined;
/** Estimated context-token usage for a message list. */
export interface ContextUsageEstimate {
/** Estimated total context tokens. */
tokens: number;
/** Tokens reported by the most recent assistant usage block. */
usageTokens: number;
/** Estimated tokens after the most recent assistant usage block. */
trailingTokens: number;
/** Index of the message that provided usage, or null when none exists. */
lastUsageIndex: number | null;
}
/** Estimate context tokens for messages using provider usage when available.
* `charsPerToken` (design/123 D2): structural coefficient for the anchor-less fallback and the
* trailing (post-anchor) increment — pass `Model.charsPerToken` so the estimate matches the model. */
export declare function estimateContextTokens(messages: AgentMessage[], charsPerToken?: number): ContextUsageEstimate;
/** Return whether context usage exceeds the configured compaction threshold. */
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
/** Estimate token count for one message using a conservative character heuristic.
* `charsPerToken` (design/123 D2): per-model structural coefficient (default 3 since design/374
* slice 4; the historical value was 4 = byte-compatible
* legacy heuristic). Images contribute a FIXED token weight independent of the coefficient. */
export declare function estimateTokens(message: AgentMessage, charsPerToken?: number): number;
/** Find the user-visible message that starts the turn containing an entry. */
export declare function findTurnStartIndex(entries: SessionTreeEntry[], entryIndex: number, startIndex: number): number;
/** Cut point selected for compaction. */
export interface CutPointResult {
/** Index of the first entry retained after compaction. */
firstKeptEntryIndex: number;
/** Index of the turn-start entry when the cut splits a turn, otherwise -1. */
turnStartIndex: number;
/** Whether the selected cut point splits an in-progress turn. */
isSplitTurn: boolean;
}
/** Find the compaction cut point that keeps approximately the requested recent-token budget.
* `charsPerToken` (design/123 D2): the keep-recent accumulation must live in the SAME structural
* coordinate as the trigger estimate, or the kept tail is systematically over/under-sized.
*
* STRUCTURAL FLOOR — the budget can raise the kept tail, never empty it. The backward scan breaks
* at the FIRST message it visits when the budget is already satisfied (a budget of 0 is satisfied
* immediately), and then lands on a valid cut point, so the kept tail is never smaller than
* "the last cut point onward". Two shapes follow, and both are load-bearing:
* - the tail ends on an assistant/user message ⇒ that message is itself a cut point and is kept;
* - the tail ends inside a tool-result batch ⇒ tool results are not cut points, so the `!found`
* fallback below pulls the floor back to the LARGEST cut point ≤ i, i.e. the emitting
* assistant, and the whole batch is kept with it.
* Reading `keepRecentTokens: 0` as "no verbatim tail" is therefore wrong at both the name and the
* behavior (see {@link DEFAULT_COMPACTION_SETTINGS} for the measured shape and the CC coordinates
* it matches). The pins live in test/compaction-keep-tail-floor.test.ts. */
export declare function findCutPoint(entries: SessionTreeEntry[], startIndex: number, endIndex: number, keepRecentTokens: number, charsPerToken?: number): CutPointResult;
export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
/**
* Render the host-supplied instruction files as the FIRST section of an independent-form
* summarization request, or `""` when the seat is absent (see
* `CompactionPreparation.contextInstructionFiles`).
*
* TRUST TIER — read before wiring a host: this content is DEPLOYMENT supply, the same tier as the
* system prompt and `EnvironmentFacts`, and it is handed to the model with instruction standing for
* the summarization task. It is NOT a model-writable position and must never be filled from tool
* output, model text, or any other in-band channel; the prompt's own provenance sentence keeps
* those classified as data, and this seat must not become the way around it. Hosts pass the same
* instruction-file content their prompt assembly mounts (already fence-sanitized upstream by
* `composeMemoryBlock` on the runner path).
*
* SINGLE SOURCE for the request bytes AND the budget charge: `generateSummary`,
* `generateTurnPrefixSummary` and `dryRunSummarizationClamp` all render through this function and
* charge `.length` of what it returns, so the window-safety judgment cannot under-count the section
* the real call sends (the failure mode the dry run was built to make impossible).
*/
export declare function contextInstructionFilesSection(contextInstructionFiles?: string): string;
/**
* design/169-A (CC 220 :516490-516516 / :387325-387336) — the CC-form FORK context for a summary
* request: the main lane's latest REAL request. When present (and the summary model IS the main
* model), the summary request is the main conversation itself — same `systemPrompt` bytes, same
* message-list byte PREFIX, same tools — plus ONE appended user-role instruction message. The
* provider's prompt cache then re-serves the already-paid main prefix, which is the economics that
* make "summarize on the main model" cheap (an independent request re-prefills the whole
* conversation at full price every boundary).
*
* Mirrored structurally (not imported — core cannot be imported from the engine layer) by
* `CompactionForkContext` in `src/core/auto-compaction.ts`; the two must stay assignable.
*/
export interface CompactionForkContext {
/** The main request's system prompt, byte-identical (absent when the main request had none). */
systemPrompt?: string;
/** The main request's PHYSICAL system-block layout (Context.systemBlocks), when present — the
* cache-span boundaries are part of the prefix identity a fork must reproduce. */
systemBlocks?: Context["systemBlocks"];
/** The main request's message list — reused as the fork's byte-identical prefix. */
messages: Message[];
/** The main request's tool declarations (part of the provider's cached prefix; optional — the
* instruction forbids tool calls either way). */
tools?: Tool[];
/** The model id that SERVED the recorded request. The model is part of the provider's cache key:
* a fork whose summary call runs on a different model (mid-run budget degradation swapped models
* after the snapshot) pays a full prefill — the eligibility gate compares this against the
* summary model and degrades to the independent form on mismatch. Optional so a direct caller
* that manages its own model constancy can omit it (absent = caller vouches). */
modelId?: string;
}
/** Extract the fork-form summary from its envelope; `undefined` = non-conforming response.
*
* Grammar (falsification-style round 3, both arms pinned): after stripping the permitted `<analysis>`
* scratch blocks, the ENTIRE remaining response must be one envelope — text outside it is a
* rejection, not noise ("<summary>placeholder</summary> I refuse" must not pass as "placeholder").
* The inner match is GREEDY with the closing tag anchored at the END, because the summary quotes
* user messages VERBATIM — a quoted literal `</summary>` inside the body must not truncate an
* otherwise conforming summary into an accepted fragment (the first-match form silently dropped
* every section after the quote). */
export declare function extractForkSummaryEnvelope(text: string): string | undefined;
/** design/169-A — the single user-role instruction message appended to the forked main prefix.
* Semantics = SUMMARIZATION_SYSTEM_PROMPT's text-only contract + SUMMARIZATION_PROMPT's nine-section
* body ("The messages above…" reads exactly right in the fork form: the conversation IS the
* messages above, un-serialized) + the fork-only instruction-file authority clause (see
* {@link FORK_CONTEXT_INSTRUCTION_AUTHORITY}) + the fork-only summary envelope (see
* {@link FORK_SUMMARY_ENVELOPE_DEMAND}; the independent form keeps its envelope-free contract).
* Instructions inject in the same CC-verbatim shape as the independent form so every channel keeps
* one injection grammar. */
export declare function forkSummarizationInstruction(customInstructions?: string, contextInstructionFiles?: string): string;
/**
* Trigger-reserve / summary-budget decoupling (blocker fix, 触发点对标批复审追补): the base for the
* summary legs' OUTPUT budget (history leg = 0.8× this, turn-prefix leg = 0.5× this).
*
* Two semantics used to ride one knob: `reserveTokens` is the TRIGGER deduction (threshold =
* window − reserve, repaired by `sanitizeCompactionSettings` against the trigger frontier), and the
* same — possibly repaired — value seeded the summary output budget. That coupling made the summary
* budget a hostage of trigger geometry: when the frontier moved (0.7W → W−33000), a 200k-window
* default's budget silently shrank 48000 → 26400 with zero summary-semantics justification.
*
* Decoupled shape:
* base = settings.summaryOutputBudgetTokens ?? settings.reserveTokens (explicit knob wins)
* effective = min(base, summary model's max output) (never budget output
* the model can't emit)
* CC contrast (2.1.207): CC's summary output cap is purely model-derived (its summary call runs at
* the model's max output) and fully separate from the trigger deduction — the `min(model.maxTokens,…)`
* leg IS that posture. The base deliberately stays reserve-derived rather than jumping to the raw
* model max: sema (unlike CC) clamps the summarization INPUT pre-send, and the clamp budgets
* `window − max(0.8×base, reasoning floor)` — a model-max base (e.g. 64k) would eat half a small
* summarizer's window in output reservation and starve the input side. Callers wanting the pure CC
* posture set `summaryOutputBudgetTokens = model.maxTokens` explicitly.
*
* SINGLE SOURCE: consumed by `compact()` (both real legs), `dryRunSummarizationClamp` (the 1.286
* same-params dry-run judgment), and the prepare-time `compaction.window_config_warning` estimate —
* so the dry run and the real clamp cannot drift.
*/
export declare function summaryOutputBudgetTokens(model: Model, settings: CompactionSettings): number;
/** Fidelity-disclosure payload for a truncated summarization input (bridged by the caller to the
* `compaction.input_truncated` trace frame). `keptChars` = surviving conversation chars (marker excluded). */
export interface SummarizationInputTruncation {
label: "history" | "turn_prefix";
droppedChars: number;
keptChars: number;
/**
* How many WHOLE messages this drop removed from the summarization input, when the drop was the
* PTL retry's group rollback (`summarizeWithPtlRetry`) rather than the proactive head+tail clamp.
* The clamp cuts a mid-section of TEXT and leaves every message present, so it reports chars only
* and omits this field; the rollback shifts entire assistant-turn groups off the front, and those
* messages are folded out of the transcript by the same pass WITHOUT ever reaching the summarizer
* — the fact the wrapper's scale disclosure has to state (see
* {@link CompactionDetails.unsummarizedMessages}). Counted in the summarization-INPUT coordinate
* (messages actually handed to the summarizer, i.e. post-`convertToLlm`), so it is a LOWER BOUND
* on the transcript messages the summary does not cover.
*/
droppedMessages?: number;
}
/** design/145 §1 — result of a window-safety clamp DRY RUN (see {@link dryRunSummarizationClamp}). */
export interface SummarizationClampDryRun {
/** Conversation chars the clamp would drop across BOTH summary legs (history + split-turn prefix). */
droppedChars: number;
/** Conversation chars that would survive (marker excluded), across both legs. */
keptChars: number;
/** `droppedChars / max(1, droppedChars + keptChars)` — always ∈ [0,1]; 0 for empty content
* (nothing to drop) and <1 even at negative headroom (the clamp keeps a MIN_KEEP floor). */
truncationRatio: number;
/** Total serialized conversation chars (pre-clamp), across both legs — the honest "content size"
* coordinate for the fallback cost gate (chars ÷ the consumer's charsPerToken). */
conversationChars: number;
/** #500 — instruction-file SECTION chars this pass would send, SUMMED over the legs that run (a
* split turn sends it twice, and each leg's copy is bounded by its own geometry, so this is not
* `section.length` and cannot be re-derived by a caller). `0` when the seat is unfilled.
*
* Exists for the design/145 门A fallback cost gate, which estimates what a MAIN-model re-run would
* prefill: the section is real input the fallback pays for, so a gate that counted only the
* conversation could pass a fallback that then exceeded the cap — the "钱花了任务照死" double loss
* the gate exists to prevent, reintroduced through the one input it did not know about. */
instructionFilesChars: number;
}
/**
* design/145 §1 — the SINGLE-SOURCE window-safety judgment: dry-run `clampSummarizationConversation`
* with the SAME parameters the real summary call(s) would use (same serialization, same fixed-prompt
* charge, same output reservation), WITHOUT sending anything. Consumed by `maybeCompact` to decide
* whether an independent (smaller-window) compaction model would truncate the conversation past
* `CompactionSettings.clampTolerance` — the r2 review killed the r1 token-domain twin formula
* precisely because two formulas drift; this function IS the clamp, so the judgment cannot.
*
* Split-turn boundaries aggregate both legs (history + turn prefix): the real path clamps each leg
* against its own budget (0.8× vs 0.5× reserve), so the dry run mirrors both and reports the summed
* ratio. Multi-round compactions are covered by construction: the preparation's `previousSummary`
* charges the fixed-prompt budget exactly as `generateSummary` does, so round 2's judgment is over
* "prior summary + new increment", not the original history.
*/
export declare function dryRunSummarizationClamp(preparation: CompactionPreparation, model: Model, customInstructions?: string, charsPerToken?: number): SummarizationClampDryRun;
/** CC 198 jNl — verbatim marker inserted where the oldest conversation groups were dropped. */
export declare const COMPACTION_PTL_RETRY_MARKER = "[earlier conversation truncated for compaction retry]";
/** Generate or update a conversation summary for compaction.
* `summaryBudgetTokens` — the OUTPUT budget base (history leg = 0.8× it). Callers going through
* `compact()` get the decoupled, model-capped value from {@link summaryOutputBudgetTokens}; this
* parameter was historically the raw `settings.reserveTokens` (trigger reserve). */
export declare function generateSummary(currentMessages: AgentMessage[], model: Model, summaryBudgetTokens: number, apiKey: string | undefined, headers?: Record<string, string>, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, runtime?: AgentCoreCompletionRuntimeDeps, charsPerToken?: number, onInputTruncated?: (info: SummarizationInputTruncation) => void, onPtlRetry?: () => void, contextInstructionFiles?: string): Promise<Result<string, CompactionError>>;
/** Prepared inputs for a compaction run. */
export interface CompactionPreparation {
/** Entry id where retained history starts. */
firstKeptEntryId: string;
/** Messages summarized into the history summary. */
messagesToSummarize: AgentMessage[];
/** Prefix messages summarized separately when compaction splits a turn. */
turnPrefixMessages: AgentMessage[];
/** Whether compaction splits a turn. */
isSplitTurn: boolean;
/** Estimated context tokens before compaction. */
tokensBefore: number;
/** Previous compaction summary used for iterative updates. */
previousSummary?: string;
/** File operations extracted from summarized history. */
fileOps: FileOperations;
/** Budgeted invoked-skill retention for the DROPPED range (merged over the previous compaction's
* retained set), most-recent-first — ready to persist as `CompactionDetails.invokedSkills`. */
invokedSkills: InvokedSkillRetention[];
/** RB-398-c: offload handles from the DROPPED range (merged over the previous compaction's
* retained set, oldest-first, capped) — ready to persist as
* `CompactionDetails.persistedOutputRefs` and render as the summary footer. Optional so
* hand-built preparations (tests, external callers) stay valid; absent = none. */
persistedOutputRefs?: string[];
/** RB-398-b①: cumulative folded-message count (this range + the previous carrier's count) —
* ready to persist as `CompactionDetails.elidedMessages`. Optional (same rule as above). */
elidedMessages?: number;
/** PTL-shortfall accounting: the previous carrier's uncovered-message count, CARRIED FORWARD only — this pass's own
* uncovered count is not knowable at prepare time (it is produced by the summary leg's PTL
* rollback), so `compact()` adds its own drops on top before persisting
* `CompactionDetails.unsummarizedMessages`. Absent = nothing carried. */
carriedUnsummarizedMessages?: number;
/** #500 — the user's / project's instruction-file content, supplied by the DEPLOYMENT, for the
* summarization request — BOTH forms: as the independent request's first section, and inside the
* one message the fork form appends (below the cached prefix, so the fork's economics are
* untouched; the fork's own system prompt usually carries these files already, but nothing
* establishes that, so a populated seat is delivered rather than assumed). Rendered by
* {@link contextInstructionFilesSection} as
* the request's first section, and charged to the fixed-prompt budget by both summary legs and
* {@link dryRunSummarizationClamp} off this one value. Optional, and ABSENT ⇒ every summarization
* request byte is what it was before this field existed. Read
* {@link contextInstructionFilesSection}'s trust-tier note before filling it: host supply only.
*
* SCOPE OF THAT INVARIANT, stated because the change has one deliberate effect OUTSIDE it
* (raised in review as a contradiction; it is not one, but it was unstated). Byte-identity is
* over the SUMMARIZATION REQUEST ASSEMBLY, and there it is exact. Registering
* `<instruction-files>` in the authority census additionally puts it in the derived
* fenced/shelled-lane sets, so external bytes that literally spell the tag are now defused with a
* ZWSP on those lanes — for every deployment, seat or no seat. That is not a compatibility break
* smuggled in: those lanes rewrite bytes BY CONTRACT (that is what makes them fences), it is what
* registering any new authority envelope means in this tree, and the alternative — a
* model-facing authority tag that the fences built to defuse forgeries do not defuse — is
* strictly worse. Disclosed rather than hidden behind a flag. */
contextInstructionFiles?: string;
/** Settings used to prepare compaction. */
settings: CompactionSettings;
}
/** Prepare session entries for compaction, or return undefined when compaction is not applicable.
* `charsPerToken` (design/123 D2, signature threading): keeps `tokensBefore` and the cut-point
* accounting in the same per-model structural coordinate as the caller's trigger estimate.
* `windowTokens` (window-aware retention clamp): the trigger window of the model that will consume
* the post-compaction context. When provided, the invoked-skills retained area is clamped to the
* headroom that window leaves after the existing geometry (`window − reserveTokens −
* keepRecentTokens`, in chars) — the CC-shaped 20k/100k retention caps are UPPER BOUNDS tuned for
* large windows, and on a small-window model an unclamped area would push every post-compaction
* request straight back over the window the compaction just freed. Absent ⇒ the plain CC-shaped
* budget (legacy behavior).
* `contextInstructionFiles` (#500): the deployment's instruction-file content for the INDEPENDENT
* summary form — carried straight through onto {@link CompactionPreparation}, whose field doc and
* {@link contextInstructionFilesSection}'s trust-tier note are the contract. Absent/blank ⇒ the
* field is omitted and nothing downstream changes. */
export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number, contextInstructionFiles?: string): Result<CompactionPreparation | undefined, CompactionError>;
export { computeFileLists, serializeConversation } from "./utils.js";
/** Generate compaction summary data from prepared session history.
* `charsPerToken` (design/123 D2, signature threading): coefficient for the summarization-input
* window guard. Defaults to the SUMMARY model's own `charsPerToken` (the estimated input is sent to
* THAT model), then 4. */
export declare function compact(preparation: CompactionPreparation, model: Model, apiKey: string | undefined, headers?: Record<string, string>, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, runtime?: AgentCoreCompletionRuntimeDeps, charsPerToken?: number, onInputTruncated?: (info: SummarizationInputTruncation) => void, onPtlRetry?: () => void,
/** design/169-A — when present, the summary is attempted in the CC FORK form first (main prefix +
* one instruction message; see {@link CompactionForkContext}). A prompt-too-long on the fork form
* falls back ONCE to the independent form below (which can clamp; the fork form must not — a
* clamp would rewrite the cached prefix). The caller only passes this when the summary model IS
* the main model; an independent compaction model has no main prefix to share. */
forkContext?: CompactionForkContext): Promise<Result<CompactionResult, CompactionError>>;
/** The prefix leg's base prompt, with the caller's instructions injected in the SAME shape the
* history leg uses (`\n\nAdditional Instructions:\n${…}`, CC 198 verbatim). Single source so the
* dry-run clamp charges exactly what the real call sends. */
export declare function turnPrefixSummarizationPrompt(customInstructions?: string): string;