UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

494 lines (493 loc) • 37 kB
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;