/** * Fit `units` to `ctx.budget`, recency-anchored, preserving `pinned`/`atomic` invariants; a would-be- * dropped code/doc unit is recovered as its `compress()` signature before being evicted (COMPRESS tier). * @param {Unit[]} units the neutral transcript units, in conversation order (oldest → newest) * @param {AssembleCtx} [ctx] * @returns {Promise} * @category CE * @when Fit a transcript to a token budget — keep pinned + newest, drop the oldest, rescuing droppable code/doc units as signatures first. * @fails Never throws; with no budget it's identity (nothing dropped). Pinned units exceeding budget are kept best-effort, never a hard cap. * @signature assemble(units: Unit[], ctx?: AssembleCtx) => Promise * @example * import { assemble } from 'litectx' * const { units, dropped, tokens } = await assemble(transcript, { budget: 8000 }) */ export function assemble(units: Unit[], ctx?: AssembleCtx): Promise; /** * @typedef {Object} SummaryWindowCtx * @property {number} [budget] token budget for the assembled view (as {@link assemble}) * @property {(messages: {role: string, content: string}[]) => Promise} [summarize] * a provider-bound summarizer the HOST supplies (litectx never calls a * model itself). Absent → this is a plain {@link assemble}. * @property {number} [summaryKeep] N most-recent transcript turns kept VERBATIM (default 8); everything * older is rolled into one summary. litectx owns N. * @property {string} [summaryRole] role for the spliced summary unit (default "system") — role is the * consumer's grammar, so the host names it and its adapter places it * @property {string} [summaryId] id for the spliced summary unit (default derived from the folded range) * @property {string} [task] passed through to {@link assemble} (reserved) */ /** * summaryWindow (R-C6) — the rolling-summary read-path policy: keep the last-N transcript turns VERBATIM, * roll everything OLDER into one rolling summary, and budget-fit the result via {@link assemble}. litectx * owns trigger (engaged only under budget pressure) + N + the splice; the HOST owns the model (`ctx.summarize` — * litectx never calls one). The summary is a SYNTHETIC unit placed as the freshest content (a cache-stable * dynamic suffix; the verbatim prefix stays byte-identical for prefix caching) so the recency-anchored fit * keeps it; if even the summary can't fit it is dropped like any unit (never an overflow). The splice is * RESTORABLE: folded turns are reported in `dropped` (reason "summarized", recoverable by id) and listed on * the summary unit's `summarizes`. Falls back to a plain `assemble` when unwired, when everything already * fits (no pressure), or when there are < 2 older turns to fold — so it is never worse than FIT. * POC-gated: `poc/rc6-summarywindow-poc.mjs` — at equal budget, summaryWindow retained the dropped-turn * answers FIT-drop lost (discriminator 3/3 vs 0/3 on a live model). * * @param {Unit[]} units the neutral transcript units, in conversation order (oldest → newest) * @param {SummaryWindowCtx} [ctx] * @returns {Promise<{ units: Unit[], dropped: {id: string, reason: "budget"|"summarized"}[], tokens: number }>} * @category CE * @when Keep the last-N turns verbatim under budget pressure and fold older ones into one rolling summary (the host supplies the summarizer). * @fails Never throws; falls back to a plain `assemble` when unwired, when everything fits, or when there are < 2 older turns to fold — never worse than FIT. * @signature summaryWindow(units: Unit[], ctx?: SummaryWindowCtx) => Promise<{ units, dropped, tokens }> * @example * import { summaryWindow } from 'litectx' * const out = await summaryWindow(transcript, { budget: 8000, keepRecent: 6, summarize: async (t) => callModel(t) }) */ export function summaryWindow(units: Unit[], ctx?: SummaryWindowCtx): Promise<{ units: Unit[]; dropped: { id: string; reason: "budget" | "summarized"; }[]; tokens: number; }>; /** * @typedef {Object} TrimPolicy * @property {number} [maxTokens] SIZE policy — fit the running transcript to a token budget. Pure * delegation to {@link assemble}'s recency-anchored fit (incl. its * COMPRESS rescue tier); trim never reimplements that math. * @property {number} [keepLastN] COUNT policy — keep the N most-recent un-pinned ITEMS (an atomic * group counts as one item, kept/dropped whole). A turn-granular * heuristic a token budget cannot express when turn sizes vary. * Ignored when `maxTokens` is set (size takes precedence). */ /** * @typedef {Object} TrimResult * @property {Unit[]} units kept units, ORIGINAL order (cache-stable) * @property {{ id: string, reason: "size"|"count" }[]} dropped evicted turns, in original order * @property {Unit[]} harvest the dropped units WITH content — the * harvest-before-evict worklist: persist these (e.g. `remember`) * BEFORE discarding them from the canonical transcript. `harvest` * carries the same ids as `dropped`; both restore by id. */ /** * trim (R-C5) — the transcript-truncation seam: drop OLD turns by a recency/size heuristic and hand back * exactly what was dropped, content intact, so the caller can harvest-before-evict (RT-2 interlock). Unlike * {@link assemble} (a non-destructive per-step VIEW, canonical transcript preserved), trim's intent is * EVICTION — the caller permanently removes the dropped turns from its running transcript afterward; the * `harvest` worklist is what makes that safe (you cannot drop history you have not persisted). * * Two policies, one eviction contract. **SIZE** (`maxTokens`) delegates wholesale to assemble's fit — the * shipped, POC-proven recency/pinned/atomic mechanic, reused not rebuilt (POC C1). **COUNT** (`keepLastN`) * is the net-new knob: keep the N freshest un-pinned items, a turn-granular drop no budget reproduces when * sizes differ (POC C2a). Both never split an `atomic` group and never drop a `pinned` unit (an atomic * group with any pinned member is force-kept whole). Neither policy set → no-op (keep all). Async only to * share assemble's signature on the size path. POC: `poc/rc5-trim-poc.mjs`. * * @param {Unit[]} units the neutral transcript units, in conversation order (oldest → newest) * @param {TrimPolicy} [policy] * @returns {Promise} * @category CE * @when Evict old turns from a running transcript (by size or count) and get back the dropped units with content, so you can harvest-before-evict. * @fails Throws `TypeError` when `units` is not an array; with no policy set it's a no-op (keep all). Never splits an atomic group or drops a pinned unit. * @signature trim(units: Unit[], policy?: TrimPolicy) => Promise * @example * import { trim } from 'litectx' * const { units, harvest } = await trim(transcript, { keepLastN: 20 }) * // persist `harvest` (e.g. remember) BEFORE discarding the old turns */ export function trim(units: Unit[], policy?: TrimPolicy): Promise; export type SummaryWindowCtx = { /** * token budget for the assembled view (as {@link assemble}) */ budget?: number | undefined; /** * a provider-bound summarizer the HOST supplies (litectx never calls a * model itself). Absent → this is a plain {@link assemble}. */ summarize?: ((messages: { role: string; content: string; }[]) => Promise) | undefined; /** * N most-recent transcript turns kept VERBATIM (default 8); everything * older is rolled into one summary. litectx owns N. */ summaryKeep?: number | undefined; /** * role for the spliced summary unit (default "system") — role is the * consumer's grammar, so the host names it and its adapter places it */ summaryRole?: string | undefined; /** * id for the spliced summary unit (default derived from the folded range) */ summaryId?: string | undefined; /** * passed through to {@link assemble} (reserved) */ task?: string | undefined; }; export type TrimPolicy = { /** * SIZE policy — fit the running transcript to a token budget. Pure * delegation to {@link assemble}'s recency-anchored fit (incl. its * COMPRESS rescue tier); trim never reimplements that math. */ maxTokens?: number | undefined; /** * COUNT policy — keep the N most-recent un-pinned ITEMS (an atomic * group counts as one item, kept/dropped whole). A turn-granular * heuristic a token budget cannot express when turn sizes vary. * Ignored when `maxTokens` is set (size takes precedence). */ keepLastN?: number | undefined; }; export type TrimResult = { /** * kept units, ORIGINAL order (cache-stable) */ units: Unit[]; /** * evicted turns, in original order */ dropped: { id: string; reason: "size" | "count"; }[]; /** * the dropped units WITH content — the * harvest-before-evict worklist: persist these (e.g. `remember`) * BEFORE discarding them from the canonical transcript. `harvest` * carries the same ids as `dropped`; both restore by id. */ harvest: Unit[]; }; export type Unit = { /** * stable identifier (the restore handle into the canonical transcript) */ id: string; /** * conversational position ("user"|"assistant"|"tool"|"system") — the * consumer's grammar; opaque to litectx, never interpreted here */ role: string; /** * the unit's text */ content: string; /** * litectx node kind ("code"|"doc"|"fact"|"episode") for injected units; * null for pass-through transcript turns (role and kind are orthogonal) */ kind?: string | null | undefined; /** * "js"|"ts"|"py"|… — the parseable language of an injected code/doc node; * enables the COMPRESS signature tier (absent on transcript turns) */ format?: string | undefined; /** * the node's symbol name (used for the compressed marker when present) */ symbol?: string | undefined; /** * never dropped or reordered; budget is computed over the un-pinned rest */ pinned?: boolean | undefined; /** * group id — units sharing one are kept-or-dropped together, never split */ atomic?: string | null | undefined; /** * approximate token cost (the consumer's estimate; we fall back to chars/4) */ tokensApprox?: number | undefined; /** * set by assemble on a unit down-tiered to its signature to fit budget * (its `content` is the signature; full body recoverable by id, like a drop) */ compressed?: boolean | undefined; /** * set by summaryWindow on the SYNTHETIC unit it splices in — its `content` * is the rolling summary of the older turns it replaced */ summary?: boolean | undefined; /** * on a summary unit: the ids of the turns folded into it (each also * reported in `dropped` with reason "summarized"; restorable by id) */ summarizes?: string[] | undefined; }; export type AssembleCtx = { /** * token budget for the assembled view; omitted/Infinity → keep everything */ budget?: number | undefined; /** * recall intent — reserved for SELECT (recall-inject), unused by the fit */ task?: string | undefined; }; export type AssembleResult = { /** * the fitted view: kept units in ORIGINAL order * (cache-stable — pinned in place, no reordering) */ units: Unit[]; /** * units elided to fit budget, in original order; * restorable by `id` from the consumer's canonical transcript */ dropped: { id: string; reason: "budget"; }[]; /** * Σ `tokensApprox` of `units` (best-effort ≤ budget; * pinned that alone exceed budget are still kept — never a hard cap) */ tokens: number; };