@aparte/plugin-compaction 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/budget.ts","../src/selector.ts","../src/transcript.ts","../src/compaction.ts"],"sourcesContent":["/**\n * budget.ts — the context-window budget.\n *\n * One question, answered without a tokenizer: how much of the model's window is left\n * for the conversation once the fixed costs are paid — the system prompt, the tools,\n * the room reserved for the reply and for thinking, a buffer, a margin. The answer is\n * what `createCompactionSelector` (selector.ts) walks the history against.\n *\n * Budget = contextWindow − systemPrompt − tools − reservedThinking\n * − reservedGeneration − autocompactBuffer − safetyMargin\n *\n * The budget is then split in two: a slot for the running summary and the sliding\n * window of verbatim turns.\n *\n * It lived in `@aparte/engine` until 0.16.0. Nothing in the loop read it — the loop\n * reports usage and lets the caller decide — so it moved here, next to the one thing\n * that does: the compaction this plugin performs. Zero deps, a char-count heuristic.\n */\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CompactionConfig {\n /** Total context window of the active model, in tokens. */\n contextWindow: number;\n /** Reserved budget for the model's thinking/reasoning block, in tokens. */\n reservedThinking: number;\n /** Reserved budget for the assistant response (max_new_tokens cap). */\n reservedGeneration: number;\n /** Fraction of context_window kept as autocompact buffer (0..1). */\n autocompactBufferPct: number;\n /** Hard safety margin in tokens. */\n safetyMargin: number;\n /** Floor for history budget — never compact below this. */\n minHistoryBudget: number;\n /**\n * Share of the history budget held back for the running summary, so the verbatim\n * window leaves room for it (0..1).\n */\n summaryRatio: number;\n /**\n * Ceiling on that reservation, in tokens. It sizes the WINDOW — it does not bound\n * the summary your `summarize` returns, and nothing here truncates one.\n */\n summaryMaxTokens: number;\n}\n\nexport interface BudgetBreakdown {\n contextWindow: number;\n systemPrompt: number;\n tools: number;\n reservedThinking: number;\n reservedGeneration: number;\n autocompactBuffer: number;\n safetyMargin: number;\n historyAvailable: number;\n}\n\nexport interface BudgetResult {\n historyBudget: number;\n breakdown: BudgetBreakdown;\n config: CompactionConfig;\n}\n\n/**\n * The history budget, split: the room held back for the running summary, and the\n * verbatim window that is what remains.\n *\n * `summary` is a RESERVATION, read only to shrink `window`. Nothing measures the\n * summary against it, and nothing clips one that comes back longer — a summariser that\n * overruns simply costs the turn more than the split assumed.\n */\nexport interface SplitBudget {\n summary: number;\n window: number;\n}\n\n// ─── Constants ──────────────────────────────────────────────────────────────\n\nexport const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {\n // Conservative model-agnostic defaults — the consuming app overrides\n // `contextWindow` / `reservedThinking` with its model's real values.\n contextWindow: 8192,\n reservedThinking: 0,\n reservedGeneration: 2000,\n autocompactBufferPct: 0.10, // ~3300 tok\n safetyMargin: 500,\n minHistoryBudget: 1000,\n\n summaryRatio: 0.10,\n summaryMaxTokens: 400,\n};\n\n// ─── Token estimation ──────────────────────────────────────────────────────\n\n/**\n * Token heuristic (FR ~3.5 chars/tok, EN ~4 chars/tok).\n * Accurate to ±10% — enough for budgeting, and it avoids `tokenizer.encode()`,\n * which costs ~5ms per message × N (too slow to run on every turn).\n */\nexport function estimateTokens(text: string | null | undefined): number {\n if (!text) return 0;\n return Math.ceil(text.length / 3.8);\n}\n\n/**\n * Estimate tokens for a JSON-serializable structure (e.g. tools array).\n */\nexport function estimateTokensJson(obj: unknown): number {\n if (!obj) return 0;\n try {\n return estimateTokens(JSON.stringify(obj));\n } catch {\n return 0;\n }\n}\n\n// ─── Budget computation ────────────────────────────────────────────────────\n\n/**\n * Compute the available history budget after subtracting fixed costs.\n */\nexport function computeHistoryBudget(input: {\n systemPrompt: string;\n toolsArray?: unknown;\n config?: Partial<CompactionConfig>;\n}): BudgetResult {\n const cfg: CompactionConfig = { ...DEFAULT_COMPACTION_CONFIG, ...(input.config ?? {}) };\n\n const systemTokens = estimateTokens(input.systemPrompt);\n const toolsTokens = estimateTokensJson(input.toolsArray);\n const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);\n\n const fixed = systemTokens + toolsTokens\n + cfg.reservedThinking + cfg.reservedGeneration\n + autocompactBuffer + cfg.safetyMargin;\n\n const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);\n\n return {\n historyBudget,\n breakdown: {\n contextWindow: cfg.contextWindow,\n systemPrompt: systemTokens,\n tools: toolsTokens,\n reservedThinking: cfg.reservedThinking,\n reservedGeneration: cfg.reservedGeneration,\n autocompactBuffer,\n safetyMargin: cfg.safetyMargin,\n historyAvailable: historyBudget,\n },\n config: cfg,\n };\n}\n\n/**\n * Split the history budget: the running summary's slot (capped), and the rest for\n * the verbatim window.\n */\nexport function splitHistoryBudget(\n historyBudget: number,\n cfg: CompactionConfig = DEFAULT_COMPACTION_CONFIG,\n): SplitBudget {\n const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));\n return { summary, window: historyBudget - summary };\n}\n","/**\n * selector.ts — the budget-aware selector: which messages are summarised, which stay.\n *\n * A compaction decides what to summarise through one function:\n * `(messages) => { keep, drop }`. This is the default one: the newest turns that still\n * fit the history budget stay verbatim, the older ones are dropped for summarising.\n *\n * The budget is `budget.ts`'s (`computeHistoryBudget` + `splitHistoryBudget`), so the\n * gauge a page shows, the selection the compaction uses and the window the model\n * declares all speak the same numbers. The window is read through a getter at each\n * call — a model change is picked up on the next compaction, never guessed.\n */\n\nimport { computeHistoryBudget, estimateTokens, splitHistoryBudget, type CompactionConfig } from './budget.js';\n\n/**\n * The least a message must carry to be costed: its text, or segments with text.\n * Structural on purpose, so a host's own message type fits without a cast — core's\n * `AparteMessage` satisfies it.\n */\nexport interface CompactableMessage {\n content?: string;\n /**\n * `unknown` elements, read defensively below: core's segment union includes a\n * segment with no `content` at all, and TypeScript refuses a type with no property\n * in common with the all-optional `{ content?: unknown }` (a \"weak type\"), so that\n * tighter shape made `AparteMessage` unassignable here.\n */\n segments?: ReadonlyArray<unknown>;\n}\n\n/** What a compaction asks for: the messages kept verbatim and the ones to summarise. */\nexport interface CompactionSelection<M> {\n keep: M[];\n drop: M[];\n}\n\n/**\n * The selector itself — generic in the MESSAGE type. The generic sits on the returned\n * function, not on the factory: a factory-level `<M>` has no inference site at\n * `createCompactionSelector({...})` and falls back to the bare `CompactableMessage`.\n */\nexport type CompactionSelector = <M extends CompactableMessage>(messages: M[]) => CompactionSelection<M>;\n\nexport interface CompactionSelectorOptions {\n /**\n * The active model's context window, in tokens — a number, or a getter read at\n * each call so a model change is picked up (`() => aparteGlobalConfig.getCurrentModel()?.contextWindow`).\n * Unknown (`undefined`) means nothing is dropped: without a window there is no\n * budget to be over.\n */\n contextWindow: number | (() => number | undefined);\n /** The system prompt the request carries, for the budget. A string or a getter. Default none. */\n systemPrompt?: string | (() => string | null | undefined);\n /** The tools declared to the model, for the budget. A value or a getter. Default none. */\n tools?: unknown | (() => unknown);\n /** Partial override of the budget's config — reserves, ratios, floors. */\n config?: Partial<Omit<CompactionConfig, 'contextWindow'>>;\n /** Never summarise fewer than this many of the newest messages. Default 2 — the last exchange. */\n minKeep?: number;\n}\n\nconst read = <T>(value: T | (() => T)): T => (typeof value === 'function' ? (value as () => T)() : value);\n\n/**\n * What a message costs: its text, or the text of its segments when it has no `content`.\n * A tool call has no `content` but is not free: its arguments and its result go to the\n * model (core sends a reply's calls and their answers on later turns), so it costs what\n * that text does.\n */\nconst textOf = (message: CompactableMessage): string => {\n if (typeof message.content === 'string' && message.content.length > 0) return message.content;\n return (message.segments ?? [])\n .map((segment) => {\n if ((segment as { type?: unknown }).type === 'tool_call') {\n const { toolCall, result } = segment as { toolCall?: { input?: unknown }; result?: unknown };\n return `${JSON.stringify(toolCall?.input ?? {})}\\n${typeof result === 'string' ? result : ''}`;\n }\n const content = (segment as { content?: unknown }).content;\n return typeof content === 'string' ? content : '';\n })\n .join('\\n');\n};\n\n/**\n * Build the selector. `setupCompaction` builds this one itself over the current model;\n * build your own to close over a budget only the app knows:\n *\n * ```ts\n * setupCompaction({\n * selector: createCompactionSelector({\n * contextWindow: 32_000, // yours, not the model's\n * systemPrompt: () => aparteGlobalConfig.resolveSystemPrompt(),\n * minKeep: 6, // never summarise the last three exchanges\n * }),\n * });\n * ```\n *\n * Walks the history from the newest message: what fits the sliding window's budget\n * is kept verbatim, the rest is dropped for summarising. When everything fits,\n * `drop` is empty and the compaction reports `{ skipped: true }` — nothing to do.\n */\nexport function createCompactionSelector(options: CompactionSelectorOptions): CompactionSelector {\n const minKeep = Math.max(0, options.minKeep ?? 2);\n return <M extends CompactableMessage>(messages: M[]): CompactionSelection<M> => {\n const contextWindow = read(options.contextWindow);\n if (!contextWindow || contextWindow <= 0) return { keep: messages, drop: [] };\n\n const budget = computeHistoryBudget({\n systemPrompt: read(options.systemPrompt) ?? '',\n toolsArray: read(options.tools),\n config: { ...options.config, contextWindow },\n });\n const windowBudget = splitHistoryBudget(budget.historyBudget, budget.config).window;\n\n let used = 0;\n let cut = messages.length;\n for (let i = messages.length - 1; i >= 0; i--) {\n const cost = estimateTokens(textOf(messages[i]!));\n const kept = messages.length - 1 - i;\n if (used + cost > windowBudget && kept >= minKeep) break;\n used += cost;\n cut = i;\n }\n if (cut === 0) return { keep: messages, drop: [] };\n return { keep: messages.slice(cut), drop: messages.slice(0, cut) };\n };\n}\n","/**\n * transcript.ts — what the summariser reads.\n *\n * A message as the model should read it before summarising: its text, then one line\n * per tool call — the name, the input, the result or the status — and one per error.\n * The history the loop sends leaves tool calls out on purpose (it already carries them\n * as a call and a result); a summary is where they would otherwise be lost, and a long\n * session of tool work used to compact into a summary that had never seen a tool run.\n */\n\nimport type { AparteMessage } from '@aparte/core';\n\n/**\n * The summariser's instruction. English, because it is addressed to the model and not\n * to the user; a host that wants another language or emphasis passes `prompt`.\n *\n * The last sentence is the one that is easy to leave out and expensive to omit. The\n * instruction rides the final user turn, which is also where a reply to the\n * conversation would go, so a model reading a transcript that ends in a question has\n * two plausible things to do: summarise, or answer. Every implementation that puts the\n * instruction in the user turn carries a clause like this one, and one goes further and\n * inserts a fake assistant turn to get it — a host replacing `prompt` should keep it.\n */\nexport const DEFAULT_COMPACTION_PROMPT =\n 'You are compacting a conversation between a user and an assistant so that it can continue ' +\n 'with less context. Write a summary the assistant can pick the work up from: what the user ' +\n 'wants and why, the decisions taken and their reasons, the tasks still open, the facts and ' +\n 'tool results that still matter (file names, values, errors, outcomes), and anything the ' +\n 'assistant would otherwise have to ask again. Lines marked [tool …] are tool calls with their ' +\n 'result. Write in the third person, factually, as compact as completeness allows. No preamble. ' +\n 'Do not continue the conversation, do not answer a question it contains and do not call a ' +\n 'tool: reply with the summary and nothing else.';\n\nconst clip = (text: string, max: number): string => (text.length > max ? `${text.slice(0, max)}…` : text);\nconst safeJson = (value: unknown): string => {\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return String(value);\n }\n};\n\n/**\n * A message's text — the rule core's history serializer follows, so the summariser\n * reads what the model would have read: streamed replies keep their text in\n * `segments` (fences and the language tag kept, a type this plugin does not know by\n * its `content` else its `fallback`), and `content` is the fallback for a\n * non-streaming reply that wrote no segments at all.\n */\nexport function messageText(message: AparteMessage): string {\n const parts: string[] = [];\n for (const segment of message.segments ?? []) {\n const content = (segment as { content?: unknown }).content;\n if (segment.type === 'text') {\n if (typeof content === 'string' && content) parts.push(content);\n } else if (segment.type === 'code') {\n const lang = (segment as { language?: string }).language ?? '';\n parts.push(`\\`\\`\\`${lang}\\n${typeof content === 'string' ? content : ''}\\n\\`\\`\\``);\n } else if (segment.type === 'thinking' || segment.type === 'tool_call' || segment.type === 'error') {\n continue;\n } else if (typeof content === 'string' && content) {\n parts.push(content);\n } else {\n const fallback = (segment as { fallback?: unknown }).fallback;\n if (typeof fallback === 'string' && fallback) parts.push(fallback);\n }\n }\n const rendered = parts.join('\\n').trim();\n if (rendered) return rendered;\n return typeof message.content === 'string' ? message.content : '';\n}\n\n/**\n * The transcript line(s) of one message for the summariser: the text, then\n * `[tool name] input → result` per tool call (the input clipped at 300 characters, the\n * result at 600) and `[error] …` per error segment.\n */\nexport function transcriptForSummary(message: AparteMessage): string {\n const lines: string[] = [];\n const text = messageText(message);\n if (text) lines.push(text);\n for (const segment of message.segments ?? []) {\n if (segment.type === 'tool_call') {\n const call = segment.toolCall;\n const input = clip(safeJson(call.input), 300);\n const outcome = segment.result !== undefined\n ? `→ ${clip(segment.result, 600)}`\n : `(${segment.status})`;\n lines.push(`[tool ${call.name}] ${input} ${outcome}`);\n } else if (segment.type === 'error') {\n const content = (segment as { content?: string }).content;\n if (content) lines.push(`[error] ${clip(content, 300)}`);\n }\n }\n return lines.join('\\n');\n}\n","/**\n * compaction.ts — `setupCompaction`: the controller that answers `aparte-compact`.\n *\n * One compaction is: resolve the chat, select what to summarise (the budget-aware\n * selector over the current model by default), summarise it through the config's\n * transport — or a summariser of the host's — then replace the transcript with the\n * summary as a notice followed by the kept turns verbatim. Every step that can fail\n * reports on `window` (`aparte-compact-start` / `-done` / `-error`, each naming the\n * chat) AND in the returned outcome, so a host that called `compact()` itself never\n * has to listen for the answer to its own call.\n *\n * What this file is careful about, each learned on the client's version of it:\n * - one compaction at a time per setup — a second request while one runs is\n * reported skipped, not started (two paid summaries, both replacing the transcript);\n * - a transcript with a turn in flight is left alone — summarising under a streaming\n * reply would drop it;\n * - the summarisation has its OWN abort controller, reached by `abort()` and by an\n * `aparte-abort` addressed to the chat, so a summary the user cancelled stops there;\n * - and a summary nobody cancelled is refused too when the transcript it was written\n * from is gone: if not one selected turn is left on the target when the model\n * answers, the conversation was switched (or reset) underneath, and the summary\n * would land over a transcript it never read;\n * - what arrived while the summary was being written is kept: the replacement is\n * summary, then the kept turns, then anything newer than the selection;\n * - a chat is addressed by id through the same rule the gauge and the client use, and\n * `scopeToTargetId` makes a setup answer one chat on a page that has several.\n */\n\nimport {\n aparteGlobalConfig, contentToText, resolveConfig, revokeAttachmentUrls, uuid,\n type AparteConfig, type AparteMessage, type AparteChatMessage, type AparteChatRequest, type AparteStreamEvent,\n type AparteAIProvider, type AparteCompactDoneEventDetail, type AparteCompactErrorEventDetail,\n type AparteCompactStartEventDetail,\n} from '@aparte/core';\nimport { createCompactionSelector, type CompactionSelection } from './selector.js';\nimport { DEFAULT_COMPACTION_PROMPT, transcriptForSummary } from './transcript.js';\n\n/**\n * The three things a chat must expose to be compacted: read the active path, empty\n * it, append to it. `<aparte-chat-viewport>` does; an `<aparte-chat>` shell hands over\n * its viewport. A host whose transcript lives elsewhere (a framework store) gives\n * `resolveTarget` an object of its own.\n */\nexport interface CompactionTarget {\n getMessages(): AparteMessage[];\n /**\n * Empty the transcript. A compaction passes `{ revokeAttachments: false }`,\n * because it puts the kept turns straight back: `<aparte-chat-viewport>` releases\n * every message's object URLs on the way out, which killed the images and files of\n * the turns the compaction was keeping. It revokes the summarised-away ones itself\n * afterwards. A target that ignores the argument keeps working — it just leaks the\n * URLs of what it drops, which is what it did before this existed.\n */\n clearAll(options?: { revokeAttachments?: boolean }): void;\n appendMessage(message: AparteMessage): void;\n}\n\n/** Which messages are summarised (`drop`) and which stay verbatim (`keep`). Pure — no model call. */\nexport type CompactionMessageSelector = (messages: AparteMessage[]) => CompactionSelection<AparteMessage>;\n\n/** The key (or record of settings) for a provider — the same shape `AparteClientOptions.keyResolver` takes. */\nexport type CompactionKeyResolver = (\n providerId: string,\n) => string | Record<string, string> | undefined | null | Promise<string | Record<string, string> | undefined | null>;\n\n/** Replaces the model call: given the summarisation request, return the summary text. */\nexport type CompactionSummarizer = (request: AparteChatRequest, signal: AbortSignal) => Promise<string>;\n\n/** Why a compaction did nothing. */\nexport type CompactionSkipReason =\n /** The transcript is empty. */\n | 'empty'\n /** The selector kept everything — within budget, or nothing older than what is kept. */\n | 'nothing-to-drop'\n /** A compaction is already running for this setup. */\n | 'running'\n /** A turn is in flight in the transcript; compacting under it would drop it. */\n | 'streaming';\n\nexport type CompactionOutcome =\n | { ok: true; skipped: true; reason: CompactionSkipReason; targetId?: string }\n | { ok: true; skipped: false; summary: string; kept: number; dropped: number; targetId?: string }\n | { ok: false; error: string; targetId?: string };\n\nexport interface CompactionSetupOptions {\n /**\n * Which messages are summarised away and which stay verbatim.\n *\n * Default: `createCompactionSelector` over the current model — its `contextWindow`,\n * the resolved system prompt and the registered tools set the budget, and the\n * newest turns that fit the window stay. When the current model declares no\n * window there is no budget to walk, so the last `keepWithoutWindow` messages\n * stay and the rest is summarised.\n */\n selector?: CompactionMessageSelector;\n /** How many of the newest messages stay when the model declares no window. Default 4 — the last two exchanges. */\n keepWithoutWindow?: number;\n /**\n * The summariser's instruction — carried by the ask itself, not by a `system`\n * message, so a provider that imposes its own system prompt cannot drop it.\n * English by default — it is addressed to the model, not to the user — asking for\n * the decisions, the open tasks and the tool results that still matter. Replace it\n * to steer the summary (a language, a domain, a length).\n */\n prompt?: string;\n /**\n * The key for the provider, when it is not on the config (`config.setKeyProvider`)\n * — the resolver an `AparteClient` was given can be passed here as is. Consulted\n * first; `config.getKey(providerId)` is the fallback.\n */\n keyResolver?: CompactionKeyResolver;\n /**\n * Replace the model call entirely. The request carries the transcript of the\n * dropped turns, then a final `user` message holding the summariser's instruction\n * and the ask, and `_meta: { compaction: true }`; return the summary text. For a\n * host with an endpoint of its own, or a cheaper model for summaries. With one, no\n * provider needs to be configured at all.\n */\n summarize?: CompactionSummarizer;\n /**\n * Resolve the chat to compact. Default: the element whose id is the `targetId`\n * (or, unnamed, the first `<aparte-chat>` / `<aparte-chat-viewport>` /\n * `[data-aparte-chat]` on the page), taking its viewport when the element itself\n * cannot render. Return your own object for a transcript that lives in a store.\n */\n resolveTarget?: (targetId?: string) => CompactionTarget | null;\n /**\n * Answer only the `aparte-compact` and `aparte-abort` events that name this chat\n * id, and compact it when `compact()` is called without one. For a page with\n * several chats and one setup per chat.\n */\n scopeToTargetId?: string;\n /** Listen for `aparte-compact` and `aparte-abort` on `window`. Default `true`; `false` for a host that calls `compact()` itself. */\n listen?: boolean;\n}\n\nexport interface CompactionController {\n /**\n * Compact one chat — the one `targetId` names, else the scoped one, else the\n * first on the page. Never rejects: the outcome says what happened, and the\n * same information goes out as `aparte-compact-done` / `aparte-compact-error`.\n */\n compact(targetId?: string): Promise<CompactionOutcome>;\n /** Abort the summarisation in flight, if any. The transcript is left untouched. */\n abort(): void;\n /** `true` while a summarisation is in flight. */\n readonly running: boolean;\n /** Remove the listeners and forget this setup; an in-flight summarisation is aborted. */\n dispose(): void;\n}\n\nconst ABORTED = 'Compaction aborted';\n\n/**\n * The model call, settled by the signal as well as by itself: an abort resolves the\n * compaction NOW, whether or not the transport (or a host's `summarize`) honours the\n * signal — a late result is discarded. Without this, a transport that ignored the\n * signal kept `running` true for as long as it pleased.\n */\nconst withAbort = <T>(work: Promise<T>, signal: AbortSignal): Promise<T> =>\n new Promise<T>((resolve, reject) => {\n const onAbort = (): void => reject(new Error(ABORTED));\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n work.then(\n (value) => { signal.removeEventListener('abort', onAbort); resolve(value); },\n (error: unknown) => { signal.removeEventListener('abort', onAbort); reject(error); },\n );\n });\n\n/** The setup per config, so a hot-reloading host does not stack listeners. */\nconst controllers = new WeakMap<AparteConfig, CompactionController>();\n\n/**\n * The configs whose setup answers the window's requests for any chat on them: listening,\n * and not scoped to one chat. Only those make another setup step aside — a setup with\n * `listen: false`, or one scoped to another chat, answers nothing for the rest. In the order\n * they were set up: a chat whose config has none is answered by the first.\n */\nconst answering = new Set<AparteConfig>();\n\n/**\n * The chats a listening setup is scoped to (`scopeToTargetId`), with how many setups name each:\n * a chat named by a scoped setup is that setup's, and the others step aside for it.\n */\nconst scopedChats = new Map<string, number>();\n\n/**\n * The `resolveTarget` of every listening, unscoped setup that brought its own, with how many\n * setups pass each: a chat that is not on the page (a transcript in a store) is answered by the\n * setup whose resolver knows it, and by none of the others. A scoped setup's resolver is not\n * here — the chat it answers is in `scopedChats`, and it answers no other.\n */\nconst ownResolvers = new Map<(targetId?: string) => CompactionTarget | null, number>();\n\nconst asTarget = (candidate: unknown, depth = 0): CompactionTarget | null => {\n if (!candidate || typeof candidate !== 'object' || depth > 1) return null;\n const el = candidate as Partial<CompactionTarget> & { viewport?: unknown };\n if (typeof el.getMessages === 'function' && typeof el.clearAll === 'function' && typeof el.appendMessage === 'function') {\n return el as CompactionTarget;\n }\n // The `<aparte-chat>` shell matches the selectors but renders through its viewport.\n return asTarget(el.viewport, depth + 1);\n};\n\n/** The default target: by id, else the first chat host on the page that can render. */\nconst resolveDomTarget = (targetId?: string): CompactionTarget | null => {\n if (typeof document === 'undefined') return null;\n if (targetId) return asTarget(document.getElementById(targetId));\n for (const el of document.querySelectorAll('aparte-chat, aparte-chat-viewport, [data-aparte-chat]')) {\n const target = asTarget(el);\n if (target) return target;\n }\n return null;\n};\n\n/** A host without a document (Node, a test of the selection alone) gets the outcome and no events. */\nconst hasWindow = typeof window !== 'undefined';\n\nconst inFlight = (message: AparteMessage): boolean => message.status === 'streaming' || message.status === 'pending';\n\n/** The default selector: the engine budget over the current model, read at each call. */\nfunction defaultSelector(config: AparteConfig, keepWithoutWindow: number): CompactionMessageSelector {\n const budgeted = createCompactionSelector({\n contextWindow: () => config.getCurrentModel()?.contextWindow,\n systemPrompt: () => config.resolveSystemPrompt(),\n tools: () => config.getTools(),\n });\n return (messages) => {\n if (config.getCurrentModel()?.contextWindow) return budgeted(messages);\n const cut = Math.max(0, messages.length - keepWithoutWindow);\n return { keep: messages.slice(cut), drop: messages.slice(0, cut) };\n };\n}\n\n/**\n * The summarisation request: the instruction, the dropped turns as a transcript, the ask.\n *\n * Every turn being deleted is summarised, minus the ones still in flight — the same\n * `inFlight` predicate `compact()` guards the whole transcript with, so the two cannot\n * disagree. The clause here used to be hand-written and demanded `status: 'completed'`\n * on an assistant turn, which meant a host appending its own messages (no `status` at\n * all — the shape the docs teach) had its replies deleted without ever reaching the\n * summariser. A turn that ended in an error is summarised too: the user read it, and it\n * is about to be deleted.\n */\nfunction buildRequest(drop: AparteMessage[], prompt: string, modelId: string): AparteChatRequest {\n const history: AparteChatMessage[] = drop\n .filter((m) => !inFlight(m))\n .map((m) => ({ role: m.role, content: transcriptForSummary(m) }))\n .filter((m) => contentToText(m.content).length > 0);\n return {\n messages: [\n ...history,\n // The instruction rides the USER turn, not a `system` message.\n //\n // A system message is a persona, and a provider is entitled to own that: one\n // serving a local model under a fixed training contract replaces the request's\n // system message with its own, by design. When it does, the summarisation\n // instruction simply never arrives — and nothing errors, because the request is\n // still well formed. The model reads the bare \"Please summarize this\n // conversation.\" after somebody else's persona and answers something plausible:\n // measured on a consumer's local model, one reply refused for want of internet\n // access, one said \"noted, I'll do it\", and one invented figures for a client\n // that appears nowhere in the transcript — which then landed in the transcript\n // as the summary notice and became the premise of every following turn.\n //\n // The instruction is not a persona: it is the task of this one request, and the\n // task belongs where every provider must look. Reported as #45 by a consumer who\n // only found it by reading the output closely.\n { role: 'user', content: `${prompt}\n\nPlease summarize the conversation above.` },\n ],\n modelId,\n stream: false,\n // Named as what it is, so a backend can route it to a cheaper model.\n _meta: { compaction: true },\n };\n}\n\n/** The model call through the config's transport, non-streaming, draining a stream if one comes back anyway. */\nasync function summarizeThroughTransport(\n config: AparteConfig,\n provider: AparteAIProvider,\n request: AparteChatRequest,\n signal: AbortSignal,\n keyResolver: CompactionKeyResolver | undefined,\n): Promise<string> {\n const resolved = keyResolver ? await keyResolver(provider.id) : undefined;\n const auth = resolved || (await config.getKey(provider.id)) || undefined;\n // The key was resolved asynchronously; an abort may have landed meanwhile.\n if (signal.aborted) throw new Error(ABORTED);\n const response = await config.getTransport().chat(provider, request, auth, { providerId: provider.id, signal });\n if (typeof response === 'string') return response;\n const reader = (response as ReadableStream<AparteStreamEvent>).getReader();\n const chunks: string[] = [];\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value.type === 'text') chunks.push(value.delta);\n }\n } finally {\n reader.releaseLock();\n }\n return chunks.join('');\n}\n\n/**\n * Install compaction on a config. Returns the controller; the same call on the same\n * config replaces the previous setup (its listeners are removed first).\n *\n * ```ts\n * import { setupCompaction } from '@aparte/plugin-compaction';\n *\n * const compaction = setupCompaction(); // the global config, the current model's budget\n * // `<aparte-context auto-compact>` now asks for a compaction on reaching 90 %;\n * // or ask yourself:\n * await compaction.compact();\n * ```\n */\nexport function setupCompaction(options: CompactionSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): CompactionController {\n controllers.get(config)?.dispose();\n\n const keepWithoutWindow = Math.max(0, options.keepWithoutWindow ?? 4);\n const select = options.selector ?? defaultSelector(config, keepWithoutWindow);\n const resolveTarget = options.resolveTarget ?? resolveDomTarget;\n const scope = options.scopeToTargetId;\n\n let running: { targetId: string | undefined; abort: AbortController } | null = null;\n let disposed = false;\n\n /**\n * Is a window event for this setup? Scoped: only the chat it names. Otherwise a chat\n * is answered by the setup on ITS OWN config (the one its boundary, `attachConfig`,\n * resolves), and only when that config has none by another: the global setup answers\n * every chat (the single-chat page, which must need no wiring), a setup on a config of\n * its own answers a chat on the global config. Never both: a page with a global setup\n * and an instance one answered a global chat twice — two summaries, one from the\n * wrong model.\n */\n const addressed = (e: Event): boolean => {\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n if (scope) return targetId === scope;\n // A chat a scoped setup names is that setup's alone.\n if (targetId && scopedChats.has(targetId)) return false;\n if (typeof document === 'undefined') return true;\n const el = targetId\n ? document.getElementById(targetId)\n : document.querySelector<HTMLElement>('aparte-chat, aparte-chat-viewport, [data-aparte-chat]');\n if (!el && targetId) {\n // Not on the page: the setup whose own resolver knows it answers it, and only that\n // one. When no resolver knows it, the routing below picks a setup as for any other\n // chat, and that one says it found nothing: a request is never dropped in silence.\n if (options.resolveTarget && resolveTarget(targetId)) return true;\n for (const other of ownResolvers.keys()) if (other(targetId)) return false;\n }\n const owner = el ? resolveConfig(el) : aparteGlobalConfig;\n if (owner === config) return true;\n if (answering.has(owner)) return false;\n if (config === aparteGlobalConfig) return true;\n // A chat on the global config, which has no setup: one setup answers it, the first that\n // answers for a whole config. Each of them did, and the chat was summarised — or\n // reported missing — once per setup on the page.\n return owner === aparteGlobalConfig && answering.values().next().value === config;\n };\n const onCompact = (e: Event): void => {\n if (!addressed(e)) return;\n void controller.compact((e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId);\n };\n const onAbort = (e: Event): void => {\n if (!running) return;\n // An unnamed abort stops whatever runs. One that names a chat stops ours when it is\n // the chat we compact — or, compacting an unnamed one, when it is a chat we answer.\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n if (targetId && (running.targetId ? targetId !== running.targetId : !addressed(e))) return;\n controller.abort();\n };\n const listening = options.listen !== false && typeof window !== 'undefined';\n if (listening) {\n window.addEventListener('aparte-compact', onCompact);\n window.addEventListener('aparte-abort', onAbort);\n }\n if (listening && !scope) answering.add(config);\n if (listening && scope) scopedChats.set(scope, (scopedChats.get(scope) ?? 0) + 1);\n const ownResolver = listening && !scope ? options.resolveTarget : undefined;\n if (ownResolver) ownResolvers.set(ownResolver, (ownResolvers.get(ownResolver) ?? 0) + 1);\n\n const controller: CompactionController = {\n get running() {\n return running !== null;\n },\n\n abort() {\n running?.abort.abort();\n },\n\n dispose() {\n if (disposed) return;\n disposed = true;\n controller.abort();\n if (listening) {\n window.removeEventListener('aparte-compact', onCompact);\n window.removeEventListener('aparte-abort', onAbort);\n if (scope) {\n const left = (scopedChats.get(scope) ?? 1) - 1;\n if (left > 0) scopedChats.set(scope, left);\n else scopedChats.delete(scope);\n }\n }\n if (ownResolver) {\n const left = (ownResolvers.get(ownResolver) ?? 1) - 1;\n if (left > 0) ownResolvers.set(ownResolver, left);\n else ownResolvers.delete(ownResolver);\n }\n if (controllers.get(config) === controller) {\n controllers.delete(config);\n answering.delete(config);\n }\n },\n\n async compact(requested?: string): Promise<CompactionOutcome> {\n const targetId = requested ?? scope;\n // The events are written out literally, one per site: the docs' events\n // reference reads the dispatch itself to say where an event goes out from.\n const fail = (error: string): CompactionOutcome => {\n // A failure is also said on the console: the documented way to ask is a\n // fire-and-forget `aparte-compact`, and a page with no listener on the\n // error event would otherwise fail in total silence.\n console.warn(`[aparte/plugin-compaction] compaction failed: ${error}`);\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactErrorEventDetail>('aparte-compact-error', { detail: { error, targetId } }));\n return { ok: false, error, targetId };\n };\n const skip = (reason: CompactionSkipReason): CompactionOutcome => {\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactDoneEventDetail>('aparte-compact-done', { detail: { skipped: true, reason, targetId } }));\n return { ok: true, skipped: true, reason, targetId };\n };\n\n if (disposed) return fail('This compaction setup was disposed');\n if (running) return skip('running');\n\n const target = resolveTarget(targetId);\n if (!target) return fail(targetId ? `No chat with id \"${targetId}\" to compact` : 'No chat found to compact');\n\n const messages = target.getMessages();\n if (messages.length === 0) return skip('empty');\n if (messages.some(inFlight)) return skip('streaming');\n\n const { keep, drop } = select(messages);\n if (drop.length === 0) return skip('nothing-to-drop');\n\n const modelConfig = config.getModelConfig();\n const providerId = modelConfig.defaultProvider;\n let provider: AparteAIProvider | undefined;\n if (!options.summarize) {\n if (!providerId) return fail('No provider configured');\n provider = config.getAIProvider(providerId);\n if (!provider) return fail(`Provider '${providerId}' not found`);\n }\n\n const request = buildRequest(drop, options.prompt ?? DEFAULT_COMPACTION_PROMPT, modelConfig.defaultModel || '');\n // The ask alone, with nothing above it: the turns being deleted say nothing a\n // summary could carry. Paying a model to summarise an empty transcript and then\n // replacing the conversation with the answer is worse than doing nothing. (One,\n // not two: the instruction rides the ask now — see `buildRequest`.)\n if (request.messages.length <= 1) return fail('Nothing summarisable in the dropped turns');\n const abort = new AbortController();\n running = { targetId, abort };\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactStartEventDetail>('aparte-compact-start', { detail: { targetId } }));\n\n try {\n const raw = await withAbort(\n options.summarize\n ? options.summarize(request, abort.signal)\n : summarizeThroughTransport(config, provider!, request, abort.signal, options.keyResolver),\n abort.signal,\n );\n const summary = raw.trim();\n if (!summary) throw new Error('Empty summary returned by model');\n\n // Replace, in one pass over the live transcript: the summary as a notice\n // (`compaction: true` — the viewport draws it centred without avatar or\n // actions, the history sends it under a preamble saying what it is; its role\n // is `user` because it is context handed to the model, and a `system`\n // message mid-conversation is refused by some providers), then the kept\n // turns AS THEY ARE NOW, then whatever arrived while the summary was written.\n // By id, not by object: the repository replaces a message's object on every\n // update, so a kept turn touched meanwhile would have read as \"arrived\" and\n // gone in twice.\n const selectedIds = new Set(messages.map((m) => m.id));\n const keptIds = new Set(keep.map((m) => m.id));\n const live = target.getMessages();\n // Nothing selected survived: this is not the transcript that was summarised.\n // A conversation switch (or a reset) replaces the whole active path, and the\n // summary of A would otherwise be written over B — and persisted with it.\n if (!live.some((m) => selectedIds.has(m.id))) return fail('The transcript changed while the summary was being written');\n const kept = live.filter((m) => keptIds.has(m.id));\n const arrived = live.filter((m) => !selectedIds.has(m.id));\n // Not a plain `clearAll()`: the viewport revokes the object URLs of\n // everything it drops, and the kept turns are going straight back in.\n target.clearAll({ revokeAttachments: false });\n target.appendMessage({\n id: uuid(),\n role: 'user',\n compaction: true,\n content: `**${config.t('compactionSummaryTitle')}**\\n\\n${summary}`,\n timestamp: Date.now(),\n status: 'completed',\n });\n for (const message of kept) target.appendMessage(message);\n for (const message of arrived) target.appendMessage(message);\n // Now the real casualties: a summarised-away turn is off the screen for\n // good, so its `blob:` URLs are released here rather than leaked.\n const survivors = new Set([...kept, ...arrived].map((m) => m.id));\n for (const message of drop) if (!survivors.has(message.id)) revokeAttachmentUrls(message.attachments);\n\n const outcome: CompactionOutcome = { ok: true, skipped: false, summary, kept: kept.length, dropped: drop.length, targetId };\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactDoneEventDetail>('aparte-compact-done', { detail: { summary, kept: kept.length, dropped: drop.length, targetId } }));\n return outcome;\n } catch (err: unknown) {\n const aborted = abort.signal.aborted || (err instanceof Error && err.name === 'AbortError');\n return fail(aborted ? ABORTED : err instanceof Error ? err.message : String(err));\n } finally {\n running = null;\n }\n },\n };\n\n controllers.set(config, controller);\n return controller;\n}\n"],"names":[],"mappings":";AA8EO,MAAM,4BAA8C;AAAA;AAAA;AAAA,EAGvD,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,sBAAsB;AAAA;AAAA,EACtB,cAAc;AAAA,EACd,kBAAkB;AAAA,EAElB,cAAc;AAAA,EACd,kBAAkB;AACtB;AASO,SAAS,eAAe,MAAyC;AACpE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,KAAK,KAAK,SAAS,GAAG;AACtC;AAKO,SAAS,mBAAmB,KAAsB;AACrD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACA,WAAO,eAAe,KAAK,UAAU,GAAG,CAAC;AAAA,EAC7C,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAOO,SAAS,qBAAqB,OAIpB;AACb,QAAM,MAAwB,EAAE,GAAG,2BAA2B,GAAI,MAAM,UAAU,GAAC;AAEnF,QAAM,eAAe,eAAe,MAAM,YAAY;AACtD,QAAM,cAAc,mBAAmB,MAAM,UAAU;AACvD,QAAM,oBAAoB,KAAK,MAAM,IAAI,gBAAgB,IAAI,oBAAoB;AAEjF,QAAM,QAAQ,eAAe,cACvB,IAAI,mBAAmB,IAAI,qBAC3B,oBAAoB,IAAI;AAE9B,QAAM,gBAAgB,KAAK,IAAI,IAAI,kBAAkB,IAAI,gBAAgB,KAAK;AAE9E,SAAO;AAAA,IACH;AAAA,IACA,WAAW;AAAA,MACP,eAAe,IAAI;AAAA,MACnB,cAAc;AAAA,MACd,OAAO;AAAA,MACP,kBAAkB,IAAI;AAAA,MACtB,oBAAoB,IAAI;AAAA,MACxB;AAAA,MACA,cAAc,IAAI;AAAA,MAClB,kBAAkB;AAAA,IAAA;AAAA,IAEtB,QAAQ;AAAA,EAAA;AAEhB;AAMO,SAAS,mBACZ,eACA,MAAwB,2BACb;AACX,QAAM,UAAU,KAAK,IAAI,IAAI,kBAAkB,KAAK,MAAM,gBAAgB,IAAI,YAAY,CAAC;AAC3F,SAAO,EAAE,SAAS,QAAQ,gBAAgB,QAAA;AAC9C;ACtGA,MAAM,OAAO,CAAI,UAA6B,OAAO,UAAU,aAAc,UAAsB;AAQnG,MAAM,SAAS,CAAC,YAAwC;AACpD,MAAI,OAAO,QAAQ,YAAY,YAAY,QAAQ,QAAQ,SAAS,EAAG,QAAO,QAAQ;AACtF,UAAQ,QAAQ,YAAY,CAAA,GACvB,IAAI,CAAC,YAAY;AACd,QAAK,QAA+B,SAAS,aAAa;AACtD,YAAM,EAAE,UAAU,OAAA,IAAW;AAC7B,aAAO,GAAG,KAAK,UAAU,UAAU,SAAS,CAAA,CAAE,CAAC;AAAA,EAAK,OAAO,WAAW,WAAW,SAAS,EAAE;AAAA,IAChG;AACA,UAAM,UAAW,QAAkC;AACnD,WAAO,OAAO,YAAY,WAAW,UAAU;AAAA,EACnD,CAAC,EACA,KAAK,IAAI;AAClB;AAoBO,SAAS,yBAAyB,SAAwD;AAC7F,QAAM,UAAU,KAAK,IAAI,GAAG,QAAQ,WAAW,CAAC;AAChD,SAAO,CAA+B,aAA0C;AAC5E,UAAM,gBAAgB,KAAK,QAAQ,aAAa;AAChD,QAAI,CAAC,iBAAiB,iBAAiB,EAAG,QAAO,EAAE,MAAM,UAAU,MAAM,GAAC;AAE1E,UAAM,SAAS,qBAAqB;AAAA,MAChC,cAAc,KAAK,QAAQ,YAAY,KAAK;AAAA,MAC5C,YAAY,KAAK,QAAQ,KAAK;AAAA,MAC9B,QAAQ,EAAE,GAAG,QAAQ,QAAQ,cAAA;AAAA,IAAc,CAC9C;AACD,UAAM,eAAe,mBAAmB,OAAO,eAAe,OAAO,MAAM,EAAE;AAE7E,QAAI,OAAO;AACX,QAAI,MAAM,SAAS;AACnB,aAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,YAAM,OAAO,eAAe,OAAO,SAAS,CAAC,CAAE,CAAC;AAChD,YAAM,OAAO,SAAS,SAAS,IAAI;AACnC,UAAI,OAAO,OAAO,gBAAgB,QAAQ,QAAS;AACnD,cAAQ;AACR,YAAM;AAAA,IACV;AACA,QAAI,QAAQ,EAAG,QAAO,EAAE,MAAM,UAAU,MAAM,GAAC;AAC/C,WAAO,EAAE,MAAM,SAAS,MAAM,GAAG,GAAG,MAAM,SAAS,MAAM,GAAG,GAAG,EAAA;AAAA,EACnE;AACJ;ACxGO,MAAM,4BACT;AASJ,MAAM,OAAO,CAAC,MAAc,QAAyB,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,MAAM;AACpG,MAAM,WAAW,CAAC,UAA2B;AACzC,MAAI;AACA,WAAO,KAAK,UAAU,KAAK,KAAK;AAAA,EACpC,QAAQ;AACJ,WAAO,OAAO,KAAK;AAAA,EACvB;AACJ;AASO,SAAS,YAAY,SAAgC;AACxD,QAAM,QAAkB,CAAA;AACxB,aAAW,WAAW,QAAQ,YAAY,CAAA,GAAI;AAC1C,UAAM,UAAW,QAAkC;AACnD,QAAI,QAAQ,SAAS,QAAQ;AACzB,UAAI,OAAO,YAAY,YAAY,QAAS,OAAM,KAAK,OAAO;AAAA,IAClE,WAAW,QAAQ,SAAS,QAAQ;AAChC,YAAM,OAAQ,QAAkC,YAAY;AAC5D,YAAM,KAAK,SAAS,IAAI;AAAA,EAAK,OAAO,YAAY,WAAW,UAAU,EAAE;AAAA,OAAU;AAAA,IACrF,WAAW,QAAQ,SAAS,cAAc,QAAQ,SAAS,eAAe,QAAQ,SAAS,SAAS;AAChG;AAAA,IACJ,WAAW,OAAO,YAAY,YAAY,SAAS;AAC/C,YAAM,KAAK,OAAO;AAAA,IACtB,OAAO;AACH,YAAM,WAAY,QAAmC;AACrD,UAAI,OAAO,aAAa,YAAY,SAAU,OAAM,KAAK,QAAQ;AAAA,IACrE;AAAA,EACJ;AACA,QAAM,WAAW,MAAM,KAAK,IAAI,EAAE,KAAA;AAClC,MAAI,SAAU,QAAO;AACrB,SAAO,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU;AACnE;AAOO,SAAS,qBAAqB,SAAgC;AACjE,QAAM,QAAkB,CAAA;AACxB,QAAM,OAAO,YAAY,OAAO;AAChC,MAAI,KAAM,OAAM,KAAK,IAAI;AACzB,aAAW,WAAW,QAAQ,YAAY,CAAA,GAAI;AAC1C,QAAI,QAAQ,SAAS,aAAa;AAC9B,YAAM,OAAO,QAAQ;AACrB,YAAM,QAAQ,KAAK,SAAS,KAAK,KAAK,GAAG,GAAG;AAC5C,YAAM,UAAU,QAAQ,WAAW,SAC7B,KAAK,KAAK,QAAQ,QAAQ,GAAG,CAAC,KAC9B,IAAI,QAAQ,MAAM;AACxB,YAAM,KAAK,SAAS,KAAK,IAAI,KAAK,KAAK,IAAI,OAAO,EAAE;AAAA,IACxD,WAAW,QAAQ,SAAS,SAAS;AACjC,YAAM,UAAW,QAAiC;AAClD,UAAI,eAAe,KAAK,WAAW,KAAK,SAAS,GAAG,CAAC,EAAE;AAAA,IAC3D;AAAA,EACJ;AACA,SAAO,MAAM,KAAK,IAAI;AAC1B;ACwDA,MAAM,UAAU;AAQhB,MAAM,YAAY,CAAI,MAAkB,WACpC,IAAI,QAAW,CAAC,SAAS,WAAW;AAChC,QAAM,UAAU,MAAY,OAAO,IAAI,MAAM,OAAO,CAAC;AACrD,MAAI,OAAO,SAAS;AAChB,YAAA;AACA;AAAA,EACJ;AACA,SAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM;AACxD,OAAK;AAAA,IACD,CAAC,UAAU;AAAE,aAAO,oBAAoB,SAAS,OAAO;AAAG,cAAQ,KAAK;AAAA,IAAG;AAAA,IAC3E,CAAC,UAAmB;AAAE,aAAO,oBAAoB,SAAS,OAAO;AAAG,aAAO,KAAK;AAAA,IAAG;AAAA,EAAA;AAE3F,CAAC;AAGL,MAAM,kCAAkB,QAAA;AAQxB,MAAM,gCAAgB,IAAA;AAMtB,MAAM,kCAAkB,IAAA;AAQxB,MAAM,mCAAmB,IAAA;AAEzB,MAAM,WAAW,CAAC,WAAoB,QAAQ,MAA+B;AACzE,MAAI,CAAC,aAAa,OAAO,cAAc,YAAY,QAAQ,EAAG,QAAO;AACrE,QAAM,KAAK;AACX,MAAI,OAAO,GAAG,gBAAgB,cAAc,OAAO,GAAG,aAAa,cAAc,OAAO,GAAG,kBAAkB,YAAY;AACrH,WAAO;AAAA,EACX;AAEA,SAAO,SAAS,GAAG,UAAU,QAAQ,CAAC;AAC1C;AAGA,MAAM,mBAAmB,CAAC,aAA+C;AACrE,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,MAAI,SAAU,QAAO,SAAS,SAAS,eAAe,QAAQ,CAAC;AAC/D,aAAW,MAAM,SAAS,iBAAiB,uDAAuD,GAAG;AACjG,UAAM,SAAS,SAAS,EAAE;AAC1B,QAAI,OAAQ,QAAO;AAAA,EACvB;AACA,SAAO;AACX;AAGA,MAAM,YAAY,OAAO,WAAW;AAEpC,MAAM,WAAW,CAAC,YAAoC,QAAQ,WAAW,eAAe,QAAQ,WAAW;AAG3G,SAAS,gBAAgB,QAAsB,mBAAsD;AACjG,QAAM,WAAW,yBAAyB;AAAA,IACtC,eAAe,MAAM,OAAO,gBAAA,GAAmB;AAAA,IAC/C,cAAc,MAAM,OAAO,oBAAA;AAAA,IAC3B,OAAO,MAAM,OAAO,SAAA;AAAA,EAAS,CAChC;AACD,SAAO,CAAC,aAAa;AACjB,QAAI,OAAO,gBAAA,GAAmB,cAAe,QAAO,SAAS,QAAQ;AACrE,UAAM,MAAM,KAAK,IAAI,GAAG,SAAS,SAAS,iBAAiB;AAC3D,WAAO,EAAE,MAAM,SAAS,MAAM,GAAG,GAAG,MAAM,SAAS,MAAM,GAAG,GAAG,EAAA;AAAA,EACnE;AACJ;AAaA,SAAS,aAAa,MAAuB,QAAgB,SAAoC;AAC7F,QAAM,UAA+B,KAChC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAC1B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,qBAAqB,CAAC,EAAA,EAAI,EAC/D,OAAO,CAAC,MAAM,cAAc,EAAE,OAAO,EAAE,SAAS,CAAC;AACtD,SAAO;AAAA,IACH,UAAU;AAAA,MACN,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAiBH,EAAE,MAAM,QAAQ,SAAS,GAAG,MAAM;AAAA;AAAA,0CAAA;AAAA,IAEJ;AAAA,IAElC;AAAA,IACA,QAAQ;AAAA;AAAA,IAER,OAAO,EAAE,YAAY,KAAA;AAAA,EAAK;AAElC;AAGA,eAAe,0BACX,QACA,UACA,SACA,QACA,aACe;AACf,QAAM,WAAW,cAAc,MAAM,YAAY,SAAS,EAAE,IAAI;AAChE,QAAM,OAAO,YAAa,MAAM,OAAO,OAAO,SAAS,EAAE,KAAM;AAE/D,MAAI,OAAO,QAAS,OAAM,IAAI,MAAM,OAAO;AAC3C,QAAM,WAAW,MAAM,OAAO,aAAA,EAAe,KAAK,UAAU,SAAS,MAAM,EAAE,YAAY,SAAS,IAAI,QAAQ;AAC9G,MAAI,OAAO,aAAa,SAAU,QAAO;AACzC,QAAM,SAAU,SAA+C,UAAA;AAC/D,QAAM,SAAmB,CAAA;AACzB,MAAI;AACA,WAAO,MAAM;AACT,YAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,UAAI,KAAM;AACV,UAAI,MAAM,SAAS,OAAQ,QAAO,KAAK,MAAM,KAAK;AAAA,IACtD;AAAA,EACJ,UAAA;AACI,WAAO,YAAA;AAAA,EACX;AACA,SAAO,OAAO,KAAK,EAAE;AACzB;AAeO,SAAS,gBAAgB,UAAkC,IAAI,SAAuB,oBAA0C;AACnI,cAAY,IAAI,MAAM,GAAG,QAAA;AAEzB,QAAM,oBAAoB,KAAK,IAAI,GAAG,QAAQ,qBAAqB,CAAC;AACpE,QAAM,SAAS,QAAQ,YAAY,gBAAgB,QAAQ,iBAAiB;AAC5E,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,QAAQ,QAAQ;AAEtB,MAAI,UAA2E;AAC/E,MAAI,WAAW;AAWf,QAAM,YAAY,CAAC,MAAsB;AACrC,UAAM,WAAY,EAAqD,QAAQ;AAC/E,QAAI,cAAc,aAAa;AAE/B,QAAI,YAAY,YAAY,IAAI,QAAQ,EAAG,QAAO;AAClD,QAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,UAAM,KAAK,WACL,SAAS,eAAe,QAAQ,IAChC,SAAS,cAA2B,uDAAuD;AACjG,QAAI,CAAC,MAAM,UAAU;AAIjB,UAAI,QAAQ,iBAAiB,cAAc,QAAQ,EAAG,QAAO;AAC7D,iBAAW,SAAS,aAAa,KAAA,OAAY,MAAM,QAAQ,EAAG,QAAO;AAAA,IACzE;AACA,UAAM,QAAQ,KAAK,cAAc,EAAE,IAAI;AACvC,QAAI,UAAU,OAAQ,QAAO;AAC7B,QAAI,UAAU,IAAI,KAAK,EAAG,QAAO;AACjC,QAAI,WAAW,mBAAoB,QAAO;AAI1C,WAAO,UAAU,sBAAsB,UAAU,SAAS,KAAA,EAAO,UAAU;AAAA,EAC/E;AACA,QAAM,YAAY,CAAC,MAAmB;AAClC,QAAI,CAAC,UAAU,CAAC,EAAG;AACnB,SAAK,WAAW,QAAS,EAAqD,QAAQ,QAAQ;AAAA,EAClG;AACA,QAAM,UAAU,CAAC,MAAmB;AAChC,QAAI,CAAC,QAAS;AAGd,UAAM,WAAY,EAAqD,QAAQ;AAC/E,QAAI,aAAa,QAAQ,WAAW,aAAa,QAAQ,WAAW,CAAC,UAAU,CAAC,GAAI;AACpF,eAAW,MAAA;AAAA,EACf;AACA,QAAM,YAAY,QAAQ,WAAW,SAAS,OAAO,WAAW;AAChE,MAAI,WAAW;AACX,WAAO,iBAAiB,kBAAkB,SAAS;AACnD,WAAO,iBAAiB,gBAAgB,OAAO;AAAA,EACnD;AACA,MAAI,aAAa,CAAC,MAAO,WAAU,IAAI,MAAM;AAC7C,MAAI,aAAa,MAAO,aAAY,IAAI,QAAQ,YAAY,IAAI,KAAK,KAAK,KAAK,CAAC;AAChF,QAAM,cAAc,aAAa,CAAC,QAAQ,QAAQ,gBAAgB;AAClE,MAAI,0BAA0B,IAAI,cAAc,aAAa,IAAI,WAAW,KAAK,KAAK,CAAC;AAEvF,QAAM,aAAmC;AAAA,IACrC,IAAI,UAAU;AACV,aAAO,YAAY;AAAA,IACvB;AAAA,IAEA,QAAQ;AACJ,eAAS,MAAM,MAAA;AAAA,IACnB;AAAA,IAEA,UAAU;AACN,UAAI,SAAU;AACd,iBAAW;AACX,iBAAW,MAAA;AACX,UAAI,WAAW;AACX,eAAO,oBAAoB,kBAAkB,SAAS;AACtD,eAAO,oBAAoB,gBAAgB,OAAO;AAClD,YAAI,OAAO;AACP,gBAAM,QAAQ,YAAY,IAAI,KAAK,KAAK,KAAK;AAC7C,cAAI,OAAO,EAAG,aAAY,IAAI,OAAO,IAAI;AAAA,cACpC,aAAY,OAAO,KAAK;AAAA,QACjC;AAAA,MACJ;AACA,UAAI,aAAa;AACb,cAAM,QAAQ,aAAa,IAAI,WAAW,KAAK,KAAK;AACpD,YAAI,OAAO,EAAG,cAAa,IAAI,aAAa,IAAI;AAAA,YAC3C,cAAa,OAAO,WAAW;AAAA,MACxC;AACA,UAAI,YAAY,IAAI,MAAM,MAAM,YAAY;AACxC,oBAAY,OAAO,MAAM;AACzB,kBAAU,OAAO,MAAM;AAAA,MAC3B;AAAA,IACJ;AAAA,IAEA,MAAM,QAAQ,WAAgD;AAC1D,YAAM,WAAW,aAAa;AAG9B,YAAM,OAAO,CAAC,UAAqC;AAI/C,gBAAQ,KAAK,iDAAiD,KAAK,EAAE;AACrE,YAAI,UAAW,QAAO,cAAc,IAAI,YAA2C,wBAAwB,EAAE,QAAQ,EAAE,OAAO,SAAA,EAAS,CAAG,CAAC;AAC3I,eAAO,EAAE,IAAI,OAAO,OAAO,SAAA;AAAA,MAC/B;AACA,YAAM,OAAO,CAAC,WAAoD;AAC9D,YAAI,UAAW,QAAO,cAAc,IAAI,YAA0C,uBAAuB,EAAE,QAAQ,EAAE,SAAS,MAAM,QAAQ,SAAA,EAAS,CAAG,CAAC;AACzJ,eAAO,EAAE,IAAI,MAAM,SAAS,MAAM,QAAQ,SAAA;AAAA,MAC9C;AAEA,UAAI,SAAU,QAAO,KAAK,oCAAoC;AAC9D,UAAI,QAAS,QAAO,KAAK,SAAS;AAElC,YAAM,SAAS,cAAc,QAAQ;AACrC,UAAI,CAAC,OAAQ,QAAO,KAAK,WAAW,oBAAoB,QAAQ,iBAAiB,0BAA0B;AAE3G,YAAM,WAAW,OAAO,YAAA;AACxB,UAAI,SAAS,WAAW,EAAG,QAAO,KAAK,OAAO;AAC9C,UAAI,SAAS,KAAK,QAAQ,EAAG,QAAO,KAAK,WAAW;AAEpD,YAAM,EAAE,MAAM,SAAS,OAAO,QAAQ;AACtC,UAAI,KAAK,WAAW,EAAG,QAAO,KAAK,iBAAiB;AAEpD,YAAM,cAAc,OAAO,eAAA;AAC3B,YAAM,aAAa,YAAY;AAC/B,UAAI;AACJ,UAAI,CAAC,QAAQ,WAAW;AACpB,YAAI,CAAC,WAAY,QAAO,KAAK,wBAAwB;AACrD,mBAAW,OAAO,cAAc,UAAU;AAC1C,YAAI,CAAC,SAAU,QAAO,KAAK,aAAa,UAAU,aAAa;AAAA,MACnE;AAEA,YAAM,UAAU,aAAa,MAAM,QAAQ,UAAU,2BAA2B,YAAY,gBAAgB,EAAE;AAK9G,UAAI,QAAQ,SAAS,UAAU,EAAG,QAAO,KAAK,2CAA2C;AACzF,YAAM,QAAQ,IAAI,gBAAA;AAClB,gBAAU,EAAE,UAAU,MAAA;AACtB,UAAI,UAAW,QAAO,cAAc,IAAI,YAA2C,wBAAwB,EAAE,QAAQ,EAAE,SAAA,EAAS,CAAG,CAAC;AAEpI,UAAI;AACA,cAAM,MAAM,MAAM;AAAA,UACd,QAAQ,YACF,QAAQ,UAAU,SAAS,MAAM,MAAM,IACvC,0BAA0B,QAAQ,UAAW,SAAS,MAAM,QAAQ,QAAQ,WAAW;AAAA,UAC7F,MAAM;AAAA,QAAA;AAEV,cAAM,UAAU,IAAI,KAAA;AACpB,YAAI,CAAC,QAAS,OAAM,IAAI,MAAM,iCAAiC;AAW/D,cAAM,cAAc,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AACrD,cAAM,UAAU,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC7C,cAAM,OAAO,OAAO,YAAA;AAIpB,YAAI,CAAC,KAAK,KAAK,CAAC,MAAM,YAAY,IAAI,EAAE,EAAE,CAAC,EAAG,QAAO,KAAK,4DAA4D;AACtH,cAAM,OAAO,KAAK,OAAO,CAAC,MAAM,QAAQ,IAAI,EAAE,EAAE,CAAC;AACjD,cAAM,UAAU,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC;AAGzD,eAAO,SAAS,EAAE,mBAAmB,MAAA,CAAO;AAC5C,eAAO,cAAc;AAAA,UACjB,IAAI,KAAA;AAAA,UACJ,MAAM;AAAA,UACN,YAAY;AAAA,UACZ,SAAS,KAAK,OAAO,EAAE,wBAAwB,CAAC;AAAA;AAAA,EAAS,OAAO;AAAA,UAChE,WAAW,KAAK,IAAA;AAAA,UAChB,QAAQ;AAAA,QAAA,CACX;AACD,mBAAW,WAAW,KAAM,QAAO,cAAc,OAAO;AACxD,mBAAW,WAAW,QAAS,QAAO,cAAc,OAAO;AAG3D,cAAM,YAAY,IAAI,IAAI,CAAC,GAAG,MAAM,GAAG,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAChE,mBAAW,WAAW,KAAM,KAAI,CAAC,UAAU,IAAI,QAAQ,EAAE,EAAG,sBAAqB,QAAQ,WAAW;AAEpG,cAAM,UAA6B,EAAE,IAAI,MAAM,SAAS,OAAO,SAAS,MAAM,KAAK,QAAQ,SAAS,KAAK,QAAQ,SAAA;AACjH,YAAI,UAAW,QAAO,cAAc,IAAI,YAA0C,uBAAuB,EAAE,QAAQ,EAAE,SAAS,MAAM,KAAK,QAAQ,SAAS,KAAK,QAAQ,SAAA,EAAS,CAAG,CAAC;AACpL,eAAO;AAAA,MACX,SAAS,KAAc;AACnB,cAAM,UAAU,MAAM,OAAO,WAAY,eAAe,SAAS,IAAI,SAAS;AAC9E,eAAO,KAAK,UAAU,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACpF,UAAA;AACI,kBAAU;AAAA,MACd;AAAA,IACJ;AAAA,EAAA;AAGJ,cAAY,IAAI,QAAQ,UAAU;AAClC,SAAO;AACX;"}
1
+ {"version":3,"file":"index.js","sources":["../src/budget.ts","../src/cut.ts","../src/selector.ts","../src/transcript.ts","../src/compaction.ts"],"sourcesContent":["/**\n * budget.ts — the context-window budget.\n *\n * One question, answered without a tokenizer: how much of the model's window is left\n * for the conversation once the fixed costs are paid — the system prompt, the tools,\n * the room reserved for the reply and for thinking, a buffer, a margin. The answer is\n * what `createCompactionSelector` (selector.ts) walks the history against.\n *\n * Budget = contextWindow − systemPrompt − tools − reservedThinking\n * − reservedGeneration − autocompactBuffer − safetyMargin\n *\n * The budget is then split in two: a slot for the running summary and the sliding\n * window of verbatim turns.\n *\n * It lived in `@aparte/engine` until 0.16.0. Nothing in the loop read it — the loop\n * reports usage and lets the caller decide — so it moved here, next to the one thing\n * that does: the compaction this plugin performs. Zero deps, a char-count heuristic.\n */\n\nimport { estimateTokens } from '@aparte/core';\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CompactionConfig {\n /** Total context window of the active model, in tokens. */\n contextWindow: number;\n /** Reserved budget for the model's thinking/reasoning block, in tokens. */\n reservedThinking: number;\n /** Reserved budget for the assistant response (max_new_tokens cap). */\n reservedGeneration: number;\n /** Fraction of context_window kept as autocompact buffer (0..1). */\n autocompactBufferPct: number;\n /** Hard safety margin in tokens. */\n safetyMargin: number;\n /** Floor for history budget — never compact below this. */\n minHistoryBudget: number;\n /**\n * Share of the history budget held back for the running summary, so the verbatim\n * window leaves room for it (0..1).\n */\n summaryRatio: number;\n /**\n * Ceiling on that reservation, in tokens. It sizes the WINDOW: the verbatim turns\n * leave this much room for the summary. The summary request's own `maxTokens` is a\n * looser ceiling that only keeps the answer inside the window (see `setupCompaction`),\n * and a summary no shorter than the turns it replaces is refused.\n */\n summaryMaxTokens: number;\n}\n\nexport interface BudgetBreakdown {\n contextWindow: number;\n systemPrompt: number;\n tools: number;\n reservedThinking: number;\n reservedGeneration: number;\n autocompactBuffer: number;\n safetyMargin: number;\n historyAvailable: number;\n}\n\nexport interface BudgetResult {\n historyBudget: number;\n breakdown: BudgetBreakdown;\n config: CompactionConfig;\n}\n\n/**\n * The history budget, split: the room held back for the running summary, and the\n * verbatim window that is what remains.\n *\n * `summary` is a RESERVATION, read to shrink `window`. The summary request carries a\n * looser `maxTokens` of its own (it keeps the answer inside the window), and a summary\n * that is cut off at that limit, or no shorter than the turns it replaces, is refused.\n */\nexport interface SplitBudget {\n summary: number;\n window: number;\n}\n\n// ─── Constants ──────────────────────────────────────────────────────────────\n\nexport const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {\n // Conservative model-agnostic defaults — the consuming app overrides\n // `contextWindow` / `reservedThinking` with its model's real values.\n contextWindow: 8192,\n reservedThinking: 0,\n reservedGeneration: 2000,\n autocompactBufferPct: 0.10, // ~3300 tok\n safetyMargin: 500,\n minHistoryBudget: 1000,\n\n summaryRatio: 0.10,\n summaryMaxTokens: 400,\n};\n\n// ─── Token estimation ──────────────────────────────────────────────────────\n\n/**\n * Token heuristic — core's, re-exported under the name this module always had.\n *\n * It lives in `@aparte/core` (`estimateTokens`) because the context gauge and the count\n * the client records on each reply read the SAME estimate: a plugin that kept a private copy\n * would decide on figures the page does not show.\n */\nexport { estimateTokens };\n\n/**\n * Estimate tokens for a JSON-serializable structure (e.g. tools array).\n */\nexport function estimateTokensJson(obj: unknown): number {\n if (!obj) return 0;\n try {\n return estimateTokens(JSON.stringify(obj));\n } catch {\n return 0;\n }\n}\n\n// ─── Budget computation ────────────────────────────────────────────────────\n\n/**\n * Compute the available history budget after subtracting fixed costs.\n */\nexport function computeHistoryBudget(input: {\n systemPrompt: string;\n toolsArray?: unknown;\n config?: Partial<CompactionConfig>;\n}): BudgetResult {\n const cfg: CompactionConfig = { ...DEFAULT_COMPACTION_CONFIG, ...(input.config ?? {}) };\n\n const systemTokens = estimateTokens(input.systemPrompt);\n const toolsTokens = estimateTokensJson(input.toolsArray);\n const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);\n\n const fixed = systemTokens + toolsTokens\n + cfg.reservedThinking + cfg.reservedGeneration\n + autocompactBuffer + cfg.safetyMargin;\n\n const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);\n\n return {\n historyBudget,\n breakdown: {\n contextWindow: cfg.contextWindow,\n systemPrompt: systemTokens,\n tools: toolsTokens,\n reservedThinking: cfg.reservedThinking,\n reservedGeneration: cfg.reservedGeneration,\n autocompactBuffer,\n safetyMargin: cfg.safetyMargin,\n historyAvailable: historyBudget,\n },\n config: cfg,\n };\n}\n\n/**\n * Split the history budget: the running summary's slot (capped), and the rest for\n * the verbatim window.\n */\nexport function splitHistoryBudget(\n historyBudget: number,\n cfg: CompactionConfig = DEFAULT_COMPACTION_CONFIG,\n): SplitBudget {\n const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));\n return { summary, window: historyBudget - summary };\n}\n","/**\n * cut.ts — where a checkpoint sits, and the one thing a summary must not lose.\n *\n * Two small pure pieces `compact()` leans on, kept out of it so each can be read (and\n * tested) without a model call in sight.\n */\n\n/**\n * The index of the first message that stays verbatim, moved so it is a USER message.\n *\n * A checkpoint is read by the model as \"the summary, then this message and everything\n * after it\", and every provider wants the first turn after a preamble to be the user's:\n * a history that resumes on an assistant reply is refused by some templates and\n * misread by the rest. So the boundary the selector chose (`boundary`, the count of\n * leading messages it dropped) is moved toward the OLDER end until it lands on a user\n * message, which keeps more verbatim and summarises less than the selector asked for.\n * When that would summarise nothing at all, it moves toward the NEWER end instead, which\n * summarises a little more. A kept message has to exist to carry the checkpoint, so the\n * boundary never goes past the last message.\n *\n * `null` when no user message qualifies — there is nothing a checkpoint could sit on.\n */\nexport function cutAtUser(messages: ReadonlyArray<{ role: string }>, boundary: number): number | null {\n const last = messages.length - 1;\n if (last < 1) return null;\n const start = Math.min(boundary, last);\n for (let i = start; i > 0; i--) if (messages[i]!.role === 'user') return i;\n for (let i = start + 1; i <= last; i++) if (messages[i]!.role === 'user') return i;\n return null;\n}\n\n/**\n * The line that introduces the conversation's first request inside a stored summary.\n * English and fixed, because the summary is addressed to the model and because an\n * update of the summary has to find the block again (see {@link splitFirstRequest}).\n */\nexport const FIRST_REQUEST_LINE = \"The conversation's first request, verbatim:\";\n\n/** The most of the first request a summary carries. */\nexport const FIRST_REQUEST_MAX = 1500;\n\n/**\n * The first request, appended to the model's summary under {@link FIRST_REQUEST_LINE}.\n *\n * It is not written by the model: what the user first asked is the one piece of a\n * conversation every later turn is judged against, and a summariser paraphrases it away\n * as readily as anything else. Clipped, so a pasted document does not become the summary.\n */\nexport function firstRequestBlock(text: string): string {\n const trimmed = text.trim();\n const clipped = trimmed.length > FIRST_REQUEST_MAX ? `${trimmed.slice(0, FIRST_REQUEST_MAX)}…` : trimmed;\n return `\\n\\n${FIRST_REQUEST_LINE}\\n${clipped}`;\n}\n\n/**\n * A stored summary taken apart: what the model wrote, and the first-request block that\n * was appended to it (empty when there is none).\n *\n * An update of a summary hands the model the first part only and puts the second back\n * unchanged afterwards. Handing it the whole thing would have it copy the block into\n * its own answer, and the block would be there twice.\n */\nexport function splitFirstRequest(summary: string): { base: string; block: string } {\n const at = summary.indexOf(`\\n\\n${FIRST_REQUEST_LINE}\\n`);\n return at < 0 ? { base: summary, block: '' } : { base: summary.slice(0, at), block: summary.slice(at) };\n}\n","/**\n * selector.ts — the budget-aware selector: which messages are summarised, which stay.\n *\n * A compaction decides what to summarise through one function:\n * `(messages) => { keep, drop }`. This is the default one: the newest turns that still\n * fit the history budget stay verbatim, the older ones are dropped for summarising.\n *\n * The budget is `budget.ts`'s (`computeHistoryBudget` + `splitHistoryBudget`), so the\n * gauge a page shows, the selection the compaction uses and the window the model\n * declares all speak the same numbers. The window is read through a getter at each\n * call — a model change is picked up on the next compaction, never guessed.\n */\n\nimport { computeHistoryBudget, estimateTokens, splitHistoryBudget, type CompactionConfig } from './budget.js';\n\n/**\n * The least a message must carry to be costed: its text, or segments with text.\n * Structural on purpose, so a host's own message type fits without a cast — core's\n * `AparteMessage` satisfies it.\n */\nexport interface CompactableMessage {\n content?: string;\n /**\n * `unknown` elements, read defensively below: core's segment union includes a\n * segment with no `content` at all, and TypeScript refuses a type with no property\n * in common with the all-optional `{ content?: unknown }` (a \"weak type\"), so that\n * tighter shape made `AparteMessage` unassignable here.\n */\n segments?: ReadonlyArray<unknown>;\n}\n\n/** What a compaction asks for: the messages kept verbatim and the ones to summarise. */\nexport interface CompactionSelection<M> {\n keep: M[];\n drop: M[];\n}\n\n/**\n * The selector itself — generic in the MESSAGE type. The generic sits on the returned\n * function, not on the factory: a factory-level `<M>` has no inference site at\n * `createCompactionSelector({...})` and falls back to the bare `CompactableMessage`.\n */\nexport type CompactionSelector = <M extends CompactableMessage>(messages: M[], limits?: CompactionLimits) => CompactionSelection<M>;\n\n/**\n * What one call can tighten beyond the budget the selector was built with. The plugin's own\n * default selector is called with these, per compaction; a host that passes a selector of\n * its own as `setupCompaction`'s `selector` is called with the messages alone, and one that\n * calls this selector directly may leave them out. All of it is in REAL tokens, the ones the\n * model counts, so a heuristic that undercounts (`scale`) is accounted for on both sides.\n */\nexport interface CompactionLimits {\n /**\n * How far the character heuristic undercounts: real tokens per estimated token, the way\n * the context measure calibrates it. The messages AND the system prompt and tools are\n * priced at `scale` times their estimate. Default `1`.\n */\n scale?: number;\n /**\n * Tokens the history must leave room for: the turn being prepared, which is not in the\n * messages handed over but will be in the request. A long new message is made room for,\n * down to `minKeep` messages. Default `0`.\n */\n reserve?: number;\n /**\n * Aim below the budget: the share of the window (0..1) the request should land at once\n * the history is trimmed, the fixed costs (system prompt, tools, the reply's room, the\n * summary's slot) counted. Never above the budget; without it the selector keeps as much\n * as the budget allows, which on a large window is within a few percent of full.\n */\n landAt?: number;\n /** The most the kept history and `reserve` may take together, when something known to have failed bounds it. */\n ceiling?: number;\n}\n\nexport interface CompactionSelectorOptions {\n /**\n * The active model's context window, in tokens — a number, or a getter read at\n * each call so a model change is picked up (`() => aparteGlobalConfig.getCurrentModel()?.contextWindow`).\n * Unknown (`undefined`) means nothing is dropped: without a window there is no\n * budget to be over.\n */\n contextWindow: number | (() => number | undefined);\n /** The system prompt the request carries, for the budget. A string or a getter. Default none. */\n systemPrompt?: string | (() => string | null | undefined);\n /** The tools declared to the model, for the budget. A value or a getter. Default none. */\n tools?: unknown | (() => unknown);\n /** Partial override of the budget's config — reserves, ratios, floors. */\n config?: Partial<Omit<CompactionConfig, 'contextWindow'>>;\n /** Never summarise fewer than this many of the newest messages. Default 2 — the last exchange. */\n minKeep?: number;\n /**\n * What one message costs, in tokens. Default: the character estimate of its text\n * (`content`, else its segments' text and its tool calls' input and result).\n *\n * Pass core's `estimateWireTokens` for one message to measure what the wire actually\n * sends — a thinking block, which the model is never shown, then costs nothing —\n * which is what `setupCompaction`'s own default selector does. A direct caller keeps\n * the default, so nothing it already relies on moves.\n */\n estimate?: (message: CompactableMessage) => number;\n}\n\nconst read = <T>(value: T | (() => T)): T => (typeof value === 'function' ? (value as () => T)() : value);\n\n/**\n * What a message costs: its text, or the text of its segments when it has no `content`.\n * A tool call has no `content` but is not free: its arguments and its result go to the\n * model (core sends a reply's calls and their answers on later turns), so it costs what\n * that text does.\n */\nconst textOf = (message: CompactableMessage): string => {\n if (typeof message.content === 'string' && message.content.length > 0) return message.content;\n return (message.segments ?? [])\n .map((segment) => {\n if ((segment as { type?: unknown }).type === 'tool_call') {\n const { toolCall, result } = segment as { toolCall?: { input?: unknown }; result?: unknown };\n return `${JSON.stringify(toolCall?.input ?? {})}\\n${typeof result === 'string' ? result : ''}`;\n }\n const content = (segment as { content?: unknown }).content;\n return typeof content === 'string' ? content : '';\n })\n .join('\\n');\n};\n\n/**\n * Build the selector. `setupCompaction` builds this one itself over the current model;\n * build your own to close over a budget only the app knows:\n *\n * ```ts\n * setupCompaction({\n * selector: createCompactionSelector({\n * contextWindow: 32_000, // yours, not the model's\n * systemPrompt: () => aparteGlobalConfig.resolveSystemPrompt(),\n * minKeep: 6, // never summarise the last three exchanges\n * }),\n * });\n * ```\n *\n * Walks the history from the newest message: what fits the sliding window's budget\n * is kept verbatim, the rest is dropped for summarising. When everything fits,\n * `drop` is empty and the compaction reports `{ skipped: true }` — nothing to do.\n */\nexport function createCompactionSelector(options: CompactionSelectorOptions): CompactionSelector {\n const minKeep = Math.max(0, options.minKeep ?? 2);\n const costOf = options.estimate ?? ((message: CompactableMessage) => estimateTokens(textOf(message)));\n return <M extends CompactableMessage>(messages: M[], limits?: CompactionLimits): CompactionSelection<M> => {\n const contextWindow = read(options.contextWindow);\n if (!contextWindow || contextWindow <= 0) return { keep: messages, drop: [] };\n\n const budget = computeHistoryBudget({\n systemPrompt: read(options.systemPrompt) ?? '',\n toolsArray: read(options.tools),\n config: { ...options.config, contextWindow },\n });\n const split = splitHistoryBudget(budget.historyBudget, budget.config);\n\n // The room for the verbatim history, in real tokens. The budget above is made of\n // estimates: priced at `scale`, the system prompt and the tools take (scale − 1) times\n // their estimate more than it counted, and that comes out of the room.\n const scale = limits?.scale !== undefined && limits.scale > 0 ? limits.scale : 1;\n const fixed = budget.breakdown.systemPrompt + budget.breakdown.tools;\n let windowBudget = split.window - (scale - 1) * fixed;\n if (limits?.landAt !== undefined) {\n const { reservedThinking, reservedGeneration, minHistoryBudget } = budget.config;\n // Not below the floor the budget itself keeps: on a small window the fixed costs\n // alone are past the aim, and a history that small is not worth a summary.\n windowBudget = Math.min(windowBudget, Math.max(\n minHistoryBudget,\n limits.landAt * contextWindow - scale * fixed - reservedThinking - reservedGeneration - split.summary,\n ));\n }\n if (limits?.ceiling !== undefined) windowBudget = Math.min(windowBudget, limits.ceiling);\n windowBudget -= limits?.reserve ?? 0;\n\n let used = 0;\n let cut = messages.length;\n for (let i = messages.length - 1; i >= 0; i--) {\n const cost = scale * costOf(messages[i]!);\n const kept = messages.length - 1 - i;\n if (used + cost > windowBudget && kept >= minKeep) break;\n used += cost;\n cut = i;\n }\n if (cut === 0) return { keep: messages, drop: [] };\n return { keep: messages.slice(cut), drop: messages.slice(0, cut) };\n };\n}\n","/**\n * transcript.ts — what the summariser reads.\n *\n * A message as the model should read it before summarising: its text, then one line\n * per tool call — the name, the input, the result or the status — and one per error.\n * The history the loop sends leaves tool calls out on purpose (it already carries them\n * as a call and a result); a summary is where they would otherwise be lost, and a long\n * session of tool work used to compact into a summary that had never seen a tool run.\n */\n\nimport type { AparteMessage } from '@aparte/core';\n\n/**\n * The summariser's instruction. English, because it is addressed to the model and not\n * to the user; a host that wants another language or emphasis passes `prompt`.\n *\n * It fixes the SECTIONS of a summary rather than leaving them to the model's taste: a\n * summary is read by a model that has nothing else, and one that is free to choose its\n * shape drops the section it finds least interesting, which is usually the open tasks.\n * It asks for the conversation's own language because a summary in another one is\n * answered in it. The same summary is also handed back to be UPDATED at the next\n * compaction (the request then opens with \"Summary so far:\"), so the sections are what\n * keeps a summary of a summary from drifting into prose.\n *\n * The last sentence is the one that is easy to leave out and expensive to omit. The\n * instruction rides the final user turn, which is also where a reply to the\n * conversation would go, so a model reading a transcript that ends in a question has\n * two plausible things to do: summarise, or answer. Every implementation that puts the\n * instruction in the user turn carries a clause like this one, and one goes further and\n * inserts a fake assistant turn to get it — a host replacing `prompt` should keep it.\n */\nexport const DEFAULT_COMPACTION_PROMPT =\n 'You are compacting a conversation between a user and an assistant so that it can continue ' +\n 'with less context. Write a summary the assistant can pick the work up from, in these ' +\n 'sections, each one short and factual: ' +\n \"1. The user's goal and constraints. \" +\n '2. Decisions taken, and why. ' +\n '3. Tasks still open. ' +\n '4. Facts and tool results still needed: names, values, errors, outcomes. ' +\n '5. The next step. ' +\n 'Lines marked [tool …] are tool calls with their result. Write in the language of the ' +\n 'conversation, in the third person, as compact as completeness allows. No preamble. ' +\n 'Do not continue the conversation, do not answer a question it contains and do not call a ' +\n 'tool: reply with the summary and nothing else.';\n\nconst clip = (text: string, max: number): string => (text.length > max ? `${text.slice(0, max)}…` : text);\nconst safeJson = (value: unknown): string => {\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return String(value);\n }\n};\n\n/**\n * A message's text — the rule core's history serializer follows, so the summariser\n * reads what the model would have read: streamed replies keep their text in\n * `segments` (fences and the language tag kept, a type this plugin does not know by\n * its `content` else its `fallback`), and `content` is the fallback for a\n * non-streaming reply that wrote no segments at all.\n */\nexport function messageText(message: AparteMessage): string {\n const parts: string[] = [];\n for (const segment of message.segments ?? []) {\n const content = (segment as { content?: unknown }).content;\n if (segment.type === 'text') {\n if (typeof content === 'string' && content) parts.push(content);\n } else if (segment.type === 'code') {\n const lang = (segment as { language?: string }).language ?? '';\n parts.push(`\\`\\`\\`${lang}\\n${typeof content === 'string' ? content : ''}\\n\\`\\`\\``);\n } else if (segment.type === 'thinking' || segment.type === 'tool_call' || segment.type === 'error') {\n continue;\n } else if (typeof content === 'string' && content) {\n parts.push(content);\n } else {\n const fallback = (segment as { fallback?: unknown }).fallback;\n if (typeof fallback === 'string' && fallback) parts.push(fallback);\n }\n }\n const rendered = parts.join('\\n').trim();\n if (rendered) return rendered;\n return typeof message.content === 'string' ? message.content : '';\n}\n\n/**\n * The transcript line(s) of one message for the summariser: the text, then\n * `[tool name] input → result` per tool call (the input clipped at 300 characters, the\n * result at 600) and `[error] …` per error segment.\n */\nexport function transcriptForSummary(message: AparteMessage): string {\n const lines: string[] = [];\n const text = messageText(message);\n if (text) lines.push(text);\n for (const segment of message.segments ?? []) {\n if (segment.type === 'tool_call') {\n const call = segment.toolCall;\n const input = clip(safeJson(call.input), 300);\n const outcome = segment.result !== undefined\n ? `→ ${clip(segment.result, 600)}`\n : `(${segment.status})`;\n lines.push(`[tool ${call.name}] ${input} ${outcome}`);\n } else if (segment.type === 'error') {\n const content = (segment as { content?: string }).content;\n if (content) lines.push(`[error] ${clip(content, 300)}`);\n }\n }\n return lines.join('\\n');\n}\n","/**\n * compaction.ts — `setupCompaction`: the controller that answers `aparte-compact`.\n *\n * One compaction is: resolve the chat, select what to summarise (the budget-aware\n * selector over the model's view of the conversation by default), summarise it through\n * the config's transport — or a summariser of the host's — then write the summary as a\n * CHECKPOINT on the first message that stays verbatim. Nothing is deleted: the messages\n * above the checkpoint stay on the screen and in storage, and the history the client\n * builds resumes from the checkpoint, so the model reads the summary in their place.\n * Every step that can fail reports on `window` (`aparte-compact-start` / `-done` /\n * `-error`, each naming the chat) AND in the returned outcome, so a host that called\n * `compact()` itself never has to listen for the answer to its own call.\n *\n * What this file is careful about, each learned on the client's version of it:\n * - one compaction at a time per CHAT — a second request for a chat whose summary is\n * being written is reported skipped, not started (two paid summaries, both written),\n * while another chat on the same setup compacts on its own;\n * - a transcript with a turn in flight is left alone — except the turn being PREPARED\n * (a send or retry the client is about to make), which the caller names in\n * `exclude`: it is neither summarised nor counted as in flight;\n * - the summarisation has its OWN abort controller, reached by `abort()`, by an\n * `aparte-abort` addressed to the chat and by the abort signal of the request that\n * asked for it (the client's Stop), so a summary the user cancelled stops there, and a\n * Restore of the same chat cancels it too;\n * - and a summary nobody cancelled is refused too when the message it was to sit on\n * is no longer on the chat's path, or the checkpoint it updates was taken off: the\n * conversation was switched (or reset) or restored underneath, and a success the host\n * dropped would be reported as one;\n * - the checkpoint is the model's own update of the one before it: a second compaction\n * sends \"Summary so far:\" and only the messages since, not the whole history again;\n * - a chat is addressed by id through the same rule the gauge and the client use, and\n * `scopeToTargetId` makes a setup answer one chat on a page that has several.\n */\n\nimport {\n aparteGlobalConfig, contentToText, estimateWireTokens, historyFromCheckpoint, measureContext, resolveConfig,\n type AparteConfig, type AparteMessage, type AparteChatMessage, type AparteChatRequest, type AparteStreamEvent,\n type AparteAIProvider, type AparteCompactDoneEventDetail, type AparteCompactErrorEventDetail,\n type AparteCompactEventDetail, type AparteCompactStartEventDetail, type AparteCompactor,\n type AparteCompactorOutcome, type AparteCompactorReason, type AparteCompactorRequest,\n} from '@aparte/core';\nimport {\n computeHistoryBudget, DEFAULT_COMPACTION_CONFIG, estimateTokens, splitHistoryBudget,\n} from './budget.js';\nimport { cutAtUser, firstRequestBlock, splitFirstRequest } from './cut.js';\nimport {\n createCompactionSelector, type CompactableMessage, type CompactionLimits, type CompactionSelection, type CompactionSelector,\n} from './selector.js';\nimport { DEFAULT_COMPACTION_PROMPT, messageText, transcriptForSummary } from './transcript.js';\n\n/**\n * What a chat must expose to be compacted: read the active path and update one message\n * of it. `<aparte-chat-viewport>` does; an `<aparte-chat>` shell hands over its viewport.\n * A host whose transcript lives elsewhere (a framework store) gives `resolveTarget` an\n * object of its own.\n */\nexport interface CompactionTarget {\n getMessages(): AparteMessage[];\n /**\n * Merge `updates` into one message of the path — the one write a compaction makes: the\n * checkpoint (`contextSummary`) on the first message that stays. A target without it\n * cannot hold a checkpoint, and the compaction fails with an error saying so rather\n * than falling back to rewriting the transcript.\n */\n updateMessage?(messageId: string, updates: Partial<AparteMessage>): void;\n /**\n * Not called any more. A compaction used to empty the transcript and put the kept turns\n * back; it now writes one checkpoint and removes nothing. Still accepted, so a target\n * written for the earlier contract keeps type-checking.\n */\n clearAll?(options?: { revokeAttachments?: boolean }): void;\n /** Not called any more; see `clearAll`. */\n appendMessage?(message: AparteMessage): void;\n}\n\n/** Which messages are summarised (`drop`) and which stay verbatim (`keep`). Pure — no model call. */\nexport type CompactionMessageSelector = (messages: AparteMessage[]) => CompactionSelection<AparteMessage>;\n\n/** The key (or record of settings) for a provider — the same shape `AparteClientOptions.keyResolver` takes. */\nexport type CompactionKeyResolver = (\n providerId: string,\n) => string | Record<string, string> | undefined | null | Promise<string | Record<string, string> | undefined | null>;\n\n/**\n * Replaces the model call: given the summarisation request, return the summary text.\n * The request asks for a stream (`stream: true`) and carries `maxTokens`, a ceiling on the\n * answer that keeps it inside the window — a summariser that talks to a model should pass both on.\n */\nexport type CompactionSummarizer = (request: AparteChatRequest, signal: AbortSignal) => Promise<string>;\n\n/** Why a compaction did nothing. */\nexport type CompactionSkipReason =\n /** The transcript is empty. */\n | 'empty'\n /**\n * The selector kept everything — within budget, or nothing older than what is kept —\n * or no user message could carry the checkpoint (the first message kept must be the\n * user's). Also what an automatic request is told when the back-off, a Restore or the\n * hysteresis says not to try now, and what an automatic or overflow request is told\n * when `auto: false` (or `listen: false`) keeps the setup manual.\n */\n | 'nothing-to-drop'\n /** A compaction is already running for this chat. */\n | 'running'\n /** A turn is in flight in the transcript; compacting under it would summarise a half-written reply. */\n | 'streaming'\n /**\n * The current model declares no `contextWindow` and nothing of the host's sizes the\n * request (no `selector`, no `summarize`, no `keepWithoutWindow`). Without a window the\n * default would send everything but the last few messages in one request, which a small\n * model refuses as over its context. Declare the window, or pass one of the three.\n */\n | 'no-window';\n\n/** How a compaction ended — core's outcome, the same type the compactor seam returns. */\nexport type CompactionOutcome = AparteCompactorOutcome;\n\nexport interface CompactionSetupOptions {\n /**\n * Which messages are summarised and which stay verbatim, over what the model reads:\n * the messages from the latest checkpoint on, minus the turn being prepared.\n *\n * Default: `createCompactionSelector` over the current model — its `contextWindow`,\n * the resolved system prompt and the registered tools set the budget, and the\n * newest turns that fit stay. Each compaction sizes it: to land below the `threshold`\n * (about `threshold − 0.2` of the window once the fixed costs are paid, so one\n * compaction buys several turns), with room for the turn being prepared, and in the\n * model's own tokens (the heuristic's undercount, as the context gauge calibrates it).\n * A `selector` of your own is called with the messages alone and keeps what it keeps.\n * Whatever it chooses, the cut is moved so the\n * first message kept is a user message (the checkpoint sits on it). When the current\n * model declares no window there is no budget to walk, and the compaction skips\n * (`'no-window'`) unless the host sizes the request itself: a `selector`, a\n * `summarize` or `keepWithoutWindow`.\n */\n selector?: CompactionMessageSelector;\n /**\n * How many of the newest messages stay when the model declares no window; everything\n * older goes to the summariser in ONE request, so the number is the host's bet that it\n * fits the model. Passing it is the opt-in: left out, a model without a window makes the\n * default compaction skip with `'no-window'`, since nothing says the request would fit.\n * Without a `selector` of your own, `4` (the last two exchanges) is what it takes when\n * you opt in through `summarize` alone. Ignored when the model declares a window.\n */\n keepWithoutWindow?: number;\n /**\n * The summariser's instruction — carried by the ask itself, not by a `system`\n * message, so a provider that imposes its own system prompt cannot drop it.\n * English by default — it is addressed to the model, not to the user — asking for\n * fixed sections (the goal, the decisions, the open tasks, the facts and tool results\n * still needed, the next step) in the language of the conversation. Replace it\n * to steer the summary (a language, a domain, a length).\n */\n prompt?: string;\n /**\n * The key for the provider, when it is not on the config (`config.setKeyProvider`)\n * — the resolver an `AparteClient` was given can be passed here as is. Consulted\n * first; `config.getKey(providerId)` is the fallback.\n */\n keyResolver?: CompactionKeyResolver;\n /**\n * Replace the model call entirely. The request carries the transcript of the\n * turns being summarised, then a final `user` message holding the summariser's\n * instruction and the ask, `stream: true`, `maxTokens` (a ceiling on the answer that\n * keeps it inside the window) and `_meta: { compaction: true }`; return the summary\n * text. When an earlier checkpoint exists the request opens with \"Summary so far:\" and\n * that summary, and asks for an updated one. Your own call\n * is not trimmed to the window — the sizing is yours — but a summary that is no\n * shorter than the turns it replaces is still refused. For a\n * host with an endpoint of its own, or a cheaper model for summaries. With one, no\n * provider needs to be configured at all.\n */\n summarize?: CompactionSummarizer;\n /**\n * Resolve the chat to compact. Default: the chat element the request carries (the\n * compactor seam passes it, so a chat inside a shadow root is found), else the element\n * whose id is the `targetId` (or, unnamed, the first `<aparte-chat>` /\n * `<aparte-chat-viewport>` / `[data-aparte-chat]` on the page), taking its viewport\n * when the element itself cannot render. Return your own object for a transcript that\n * lives in a store.\n */\n resolveTarget?: (targetId?: string) => CompactionTarget | null;\n /**\n * Answer only the `aparte-compact` and `aparte-abort` events that name this chat\n * id, and compact it when `compact()` is called without one. A request for another chat\n * is refused. For a page with several chats and one setup per chat — and note that the\n * config has ONE compactor: the setup made last on a config is the one the client and\n * the gauge call.\n */\n scopeToTargetId?: string;\n /**\n * Listen for `aparte-compact` and `aparte-abort` on `window`. Default `true`; `false` for\n * a host that calls `compact()` itself.\n *\n * `false` also keeps compaction manual, whatever `auto` says (see `auto`): a setup that\n * hears nothing decides nothing. A Stop still cancels a summary: the client hands the\n * turn's abort signal to the compactor, so it does not depend on this listener.\n */\n listen?: boolean;\n /**\n * Compact by itself. Default `true`: once set up, the plugin summarises a chat when a\n * turn ends with the context at `threshold` of its window, before a send whose request\n * would not fit — the client asks first (`beforeSend`) — and when the model refuses a\n * request as over its window (the client asks again, with `reason: 'overflow'`, and\n * sends the turn once more).\n *\n * `false` means MANUAL ONLY: only a person's request compacts (the gauge's Compact now,\n * a separator's Retry, `compact()`, `aparte-compact`). None of the three automatic\n * attempts is made, the overflow included — `compact()` answers an `'auto'` or\n * `'overflow'` request with a skip, so the model's error stands exactly as it did before\n * the plugin. Forced `false` with `listen: false` (a setup that hears nothing decides\n * nothing).\n *\n * It acts only when a summariser can run: a provider registered for the current model,\n * or a `summarize` of the host's. It never acts on a Restore, a conversation switch or a\n * load, and never again after two failures in a row until something succeeds or a person\n * asks (see the back-off in the docs).\n */\n auto?: boolean;\n /**\n * The share of the window, from 0 to 1, at which a turn's end triggers a compaction.\n * Default `0.8`. The gauge's own levels are independent of it.\n *\n * A compaction aims lower than the threshold: with the default selector it leaves the\n * history the model reads at about `threshold − 0.2` of the window, less the fixed costs\n * (the system prompt, the tools, the room for the reply, the summary's slot), so one\n * compaction buys several turns and the next is not asked for after this one. That holds\n * for a person's request and an overflow retry too; a `selector` of your own is left\n * alone. If the context is still at the threshold afterwards (a long recent tail the\n * selector must keep), the after-turn trigger waits until a turn ends below it; a request\n * that would not fit is still compacted before it is sent, and an overflow still retried.\n */\n threshold?: number;\n /**\n * The window to measure against when the current model declares none — a number, or a\n * getter read at each use (`() => settings.contextWindow`). A local server has no\n * window the page can read, and without one nothing is automatic and the default\n * selector skips. The model's own `contextWindow` wins when it declares one. For one\n * more release, the `window` a gauge sends with its `aparte-compact` request is read\n * as a last resort.\n */\n contextWindow?: number | (() => number | undefined);\n}\n\n/**\n * The controller `setupCompaction` returns — and the compactor it registers on the\n * config (`AparteConfig.setCompactor`), so the gauge and the client call the same object\n * a host does.\n */\nexport interface CompactionController extends\n Omit<AparteCompactor, 'compact' | 'beforeSend' | 'preview'>,\n Required<Pick<AparteCompactor, 'beforeSend' | 'preview'>> {\n /**\n * Compact one chat. Takes the compactor seam's request (the chat element, its id, the\n * ids of a turn being prepared, why) or, as it always did, a chat id, or nothing:\n * the scoped chat, else the first on the page. A string or nothing is a person's\n * request (`reason: 'manual'`). Never rejects: the outcome says what happened, and the\n * same information goes out as `aparte-compact-done` / `aparte-compact-error`.\n */\n compact(request?: string | AparteCompactorRequest): Promise<CompactionOutcome>;\n /** Abort the summarisations in flight, if any. The transcripts are left untouched. */\n abort(): void;\n /** `true` while a summarisation is in flight, for any chat of this setup. */\n readonly running: boolean;\n /** Remove the listeners and forget this setup; an in-flight summarisation is aborted and the compactor unregistered. */\n dispose(): void;\n}\n\n/** The share of the window at which a turn's end triggers a compaction. */\nconst DEFAULT_THRESHOLD = 0.8;\n/** Failures in a row, for one chat, after which the automatic attempts stop. */\nconst MAX_FAILURES = 2;\n/**\n * How far below the threshold a compaction aims to leave the context, as a share of the\n * window. A compaction that landed AT the threshold would buy one turn and be asked again\n * after it; landing a fifth of the window below buys several.\n */\nconst LANDING_MARGIN = 0.2;\n/**\n * After an overflow, the share of what failed that the kept history and the new turn may\n * still take. The model refused that request: one a token smaller is no answer.\n */\nconst OVERFLOW_SHRINK = 0.75;\n\n/**\n * What the policy remembers about one chat. All of it is forgotten on a conversation\n * switch (`aparte-reset-done`): none of it is about the new conversation.\n */\ninterface ChatPolicy {\n /** Consecutive failed attempts; two stop the automatic ones. */\n failures: number;\n /** A Restore was pressed: the person wants the originals sent, so nothing automatic until they ask again. */\n suspended: boolean;\n /** The last compaction left the context at or over the threshold: no automatic attempt until another turn completes. */\n cooling: boolean;\n /** The range the last successful compaction summarised — an automatic attempt never takes it twice. */\n lastRange?: string;\n}\n\nconst ABORTED = 'Compaction aborted';\nconst CHANGED ='The conversation changed while the summary was being written';\n\n/**\n * The model call, settled by the signal as well as by itself: an abort resolves the\n * compaction NOW, whether or not the transport (or a host's `summarize`) honours the\n * signal — a late result is discarded. Without this, a transport that ignored the\n * signal kept `running` true for as long as it pleased.\n */\nconst withAbort = <T>(work: Promise<T>, signal: AbortSignal): Promise<T> =>\n new Promise<T>((resolve, reject) => {\n const onAbort = (): void => reject(new Error(ABORTED));\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n work.then(\n (value) => { signal.removeEventListener('abort', onAbort); resolve(value); },\n (error: unknown) => { signal.removeEventListener('abort', onAbort); reject(error); },\n );\n });\n\n/** The setup per config, so a hot-reloading host does not stack listeners. */\nconst controllers = new WeakMap<AparteConfig, CompactionController>();\n\n/**\n * The configs whose setup answers the window's requests for any chat on them: listening,\n * and not scoped to one chat. Only those make another setup step aside — a setup with\n * `listen: false`, or one scoped to another chat, answers nothing for the rest. In the order\n * they were set up: a chat whose config has none is answered by the first.\n */\nconst answering = new Set<AparteConfig>();\n\n/**\n * The chats a listening setup is scoped to (`scopeToTargetId`), with how many setups name each:\n * a chat named by a scoped setup is that setup's, and the others step aside for it.\n */\nconst scopedChats = new Map<string, number>();\n\n/**\n * The `resolveTarget` of every listening, unscoped setup that brought its own, with how many\n * setups pass each: a chat that is not on the page (a transcript in a store) is answered by the\n * setup whose resolver knows it, and by none of the others. A scoped setup's resolver is not\n * here — the chat it answers is in `scopedChats`, and it answers no other.\n */\nconst ownResolvers = new Map<(targetId?: string) => CompactionTarget | null, number>();\n\nconst asTarget = (candidate: unknown, depth = 0): CompactionTarget | null => {\n if (!candidate || typeof candidate !== 'object' || depth > 1) return null;\n const el = candidate as Partial<CompactionTarget> & { viewport?: unknown };\n const writes = typeof el.updateMessage === 'function'\n || (typeof el.clearAll === 'function' && typeof el.appendMessage === 'function');\n if (typeof el.getMessages === 'function' && writes) return el as CompactionTarget;\n // The `<aparte-chat>` shell matches the selectors but renders through its viewport.\n return asTarget(el.viewport, depth + 1);\n};\n\n/** The default target: by id, else the first chat host on the page that can render. */\nconst resolveDomTarget = (targetId?: string): CompactionTarget | null => {\n if (typeof document === 'undefined') return null;\n if (targetId) return asTarget(document.getElementById(targetId));\n for (const el of document.querySelectorAll('aparte-chat, aparte-chat-viewport, [data-aparte-chat]')) {\n const target = asTarget(el);\n if (target) return target;\n }\n return null;\n};\n\n/** A host without a document (Node, a test of the selection alone) gets the outcome and no events. */\nconst hasWindow = typeof window !== 'undefined';\n\nconst inFlight = (message: AparteMessage): boolean => message.status === 'streaming' || message.status === 'pending';\n\n/**\n * The default selector: the engine budget over the current model, read at each call, measuring\n * what the WIRE sends for a message (core's estimate) rather than its text alone, so the\n * selection and the gauge price a reply's tool calls and its hidden thinking the same way.\n */\nfunction defaultSelector(\n config: AparteConfig, keepWithoutWindow: number, windowOf: () => number | undefined,\n): (messages: AparteMessage[], limits?: CompactionLimits) => CompactionSelection<AparteMessage> {\n const budgeted: CompactionSelector = createCompactionSelector({\n contextWindow: windowOf,\n systemPrompt: () => config.resolveSystemPrompt(),\n tools: () => config.getTools(),\n estimate: (message: CompactableMessage) => estimateWireTokens([message as AparteMessage]),\n });\n return (messages, limits) => {\n if (windowOf()) return budgeted(messages, limits);\n const cut = Math.max(0, messages.length - keepWithoutWindow);\n return { keep: messages.slice(cut), drop: messages.slice(0, cut) };\n };\n}\n\n/** The turns that reach the summariser: the dropped ones as transcript lines, minus what is in flight or says nothing. */\nconst summarisable = (drop: AparteMessage[]): AparteChatMessage[] =>\n drop\n .filter((m) => !inFlight(m))\n .map((m) => ({ role: m.role, content: transcriptForSummary(m) }))\n .filter((m) => contentToText(m.content).length > 0);\n\n/**\n * Adjacent turns of one role become one turn, joined by a blank line, the way core's wire\n * history joins them. Turns that say nothing are dropped before this (a reply stopped before\n * its first token), which can leave two of the user's side by side, and a chat template that\n * insists on alternation (a local model's: Mistral, Gemma) refuses them. The summary request\n * goes straight to the transport, so it does not pass through the merge a send does.\n */\nconst alternating = (turns: AparteChatMessage[]): AparteChatMessage[] => {\n const merged: AparteChatMessage[] = [];\n for (const turn of turns) {\n const last = merged[merged.length - 1];\n if (last && last.role === turn.role) {\n merged[merged.length - 1] = { role: last.role, content: `${contentToText(last.content)}\\n\\n${contentToText(turn.content)}` };\n } else {\n merged.push(turn);\n }\n }\n return merged;\n};\n\n/**\n * What a summary is made of once a reasoning model's inline thinking is taken out. A model\n * that reasons in its text (llama.cpp or vLLM without a reasoning parser) opens `<think>`,\n * and the chat path turns that block into a thinking segment; the summary never goes through\n * that parser, so the block would be stored as the summary, shown under \"Show summary\" and\n * sent on every later turn. A block left open (the answer ran out inside it) is dropped to the\n * end, and so is the reasoning of a template that opens the block in the prompt, whose answer\n * carries only the closing tag.\n */\nconst withoutThinking = (text: string): string =>\n text\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, '')\n .replace(/<think>[\\s\\S]*$/i, '')\n .replace(/^[\\s\\S]*<\\/think>/i, '')\n .trim();\n\n/**\n * The ask that closes the request. One place, because the input bound has to count what it\n * sends. An update asks for the old summary with the new turns folded in, not for a\n * summary of the new turns alone.\n */\nconst askFor = (prompt: string, updating: boolean): string => `${prompt}\n\n${updating\n ? 'Please write the updated summary: the summary so far, with the conversation above folded in.'\n : 'Please summarize the conversation above.'}`;\n\n/**\n * \"Summary so far:\" and the summary, ahead of the turns since. Merged into the first turn\n * when that is the user's, so the request never opens with two user messages in a row: a\n * chat template that insists on alternation (a local model's) refuses one.\n */\nconst withSummarySoFar = (turns: AparteChatMessage[], summary: string): AparteChatMessage[] => {\n const note = `Summary so far:\\n${summary}\\n\\nThe conversation since then:`;\n const [first, ...rest] = turns;\n if (first?.role === 'user') return [{ role: 'user', content: `${note}\\n\\n${contentToText(first.content)}` }, ...rest];\n return [{ role: 'user', content: note }, ...turns];\n};\n\n/**\n * The summarisation request: the instruction, the turns as a transcript, the ask — and,\n * when an earlier checkpoint exists, its summary first.\n *\n * Every turn being summarised is included, minus the ones still in flight — the same\n * `inFlight` predicate `compact()` guards the transcript with, so the two cannot\n * disagree. The clause here used to be hand-written and demanded `status: 'completed'`\n * on an assistant turn, which meant a host appending its own messages (no `status` at\n * all — the shape the docs teach) had its replies kept out of the summary without ever\n * reaching the summariser. A turn that ended in an error is summarised too: the user\n * read it, and the model should know it happened.\n */\nfunction buildRequest(\n turns: AparteChatMessage[], summarySoFar: string | undefined, prompt: string, modelId: string, maxTokens: number,\n): AparteChatRequest {\n const body = summarySoFar === undefined ? turns : withSummarySoFar(turns, summarySoFar);\n const last = body[body.length - 1];\n const ask = askFor(prompt, summarySoFar !== undefined);\n return {\n messages: [\n // The ask joins a trailing user turn rather than following it: two user turns in a\n // row are what `alternating` took out of the transcript.\n ...(last?.role === 'user' ? body.slice(0, -1) : body),\n // The instruction rides the USER turn, not a `system` message.\n //\n // A system message is a persona, and a provider is entitled to own that: one\n // serving a local model under a fixed training contract replaces the request's\n // system message with its own, by design. When it does, the summarisation\n // instruction simply never arrives — and nothing errors, because the request is\n // still well formed. The model reads the bare \"Please summarize this\n // conversation.\" after somebody else's persona and answers something plausible:\n // measured on a consumer's local model, one reply refused for want of internet\n // access, one said \"noted, I'll do it\", and one invented figures for a client\n // that appears nowhere in the transcript — which then landed in the transcript\n // as the summary notice and became the premise of every following turn.\n //\n // The instruction is not a persona: it is the task of this one request, and the\n // task belongs where every provider must look. Reported as #45 by a consumer who\n // only found it by reading the output closely.\n { role: 'user', content: last?.role === 'user' ? `${contentToText(last.content)}\\n\\n${ask}` : ask },\n ],\n modelId,\n // Streamed, so an `error` event and a `done` that says the model ran out of room\n // reach the drain below; a non-streamed answer cut at the limit is just a string.\n stream: true,\n // A ceiling on the answer, so a server cannot let it run until the context is\n // full (see `summaryAnswerCap`).\n maxTokens,\n // Named as what it is, so a backend can route it to a cheaper model.\n _meta: { compaction: true },\n };\n}\n\n/**\n * The model call through the config's transport, streamed, drained strictly: an `error`\n * event throws (the text before it is a fragment, not a summary), and so does a `done`\n * that reports the length limit (a summary cut off mid-sentence would stand in for the\n * conversation it was meant to carry). A transport that answers with a plain string\n * is taken as the whole summary.\n */\nasync function summarizeThroughTransport(\n config: AparteConfig,\n provider: AparteAIProvider,\n request: AparteChatRequest,\n signal: AbortSignal,\n keyResolver: CompactionKeyResolver | undefined,\n): Promise<string> {\n const resolved = keyResolver ? await keyResolver(provider.id) : undefined;\n const auth = resolved || (await config.getKey(provider.id)) || undefined;\n // The key was resolved asynchronously; an abort may have landed meanwhile.\n if (signal.aborted) throw new Error(ABORTED);\n const response = await config.getTransport().chat(provider, request, auth, { providerId: provider.id, signal });\n if (typeof response === 'string') return response;\n const reader = (response as ReadableStream<AparteStreamEvent>).getReader();\n const chunks: string[] = [];\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value.type === 'text') chunks.push(value.delta);\n else if (value.type === 'error') throw new Error(value.message);\n else if (value.type === 'done' && value.finishReason === 'length') {\n throw new Error(\"The summary was cut off at the model's length limit\");\n }\n }\n } catch (err: unknown) {\n // Stop the producer: nobody reads the rest of a refused answer.\n void reader.cancel().catch(() => undefined);\n throw err;\n } finally {\n reader.releaseLock();\n }\n return chunks.join('');\n}\n\n/**\n * The ceiling on the summary's answer, sent as the request's `maxTokens`.\n *\n * It protects the window, not the summary's length: without one, a server lets the answer\n * run until the context is full. A quarter of the window, at most the budget's\n * `reservedGeneration` (the room every request already keeps for a reply), and never below\n * the room the window walk holds back for the summary (`summaryRatio` of the history\n * budget, capped by `summaryMaxTokens`). It is deliberately looser than that reservation:\n * on a 4 096-token model the reservation is about 118 tokens, and a reasoning model spends\n * more than that thinking before it writes a word, so a cap that tight would cut every\n * summary off and every compaction would be refused. With no window, `reservedGeneration`.\n */\nfunction summaryAnswerCap(config: AparteConfig, contextWindow: number | undefined): number {\n const { reservedGeneration } = DEFAULT_COMPACTION_CONFIG;\n if (!contextWindow || contextWindow <= 0) return reservedGeneration;\n const budget = computeHistoryBudget({\n systemPrompt: config.resolveSystemPrompt() ?? '',\n toolsArray: config.getTools(),\n config: { contextWindow },\n });\n const reservation = splitHistoryBudget(budget.historyBudget, budget.config).summary;\n return Math.max(1, reservation, Math.min(reservedGeneration, Math.floor(contextWindow / 4)));\n}\n\n/**\n * How many of the turns to summarise, oldest first, the summarisation request can carry:\n * the instruction, the summary so far (`baseTokens`) and their transcript must fit the\n * window less the answer's room, the reserved thinking and the safety margin (the\n * budget's own defaults), all in the model's tokens: the estimates are scaled by `scale`.\n * The turns past that point are not summarised now — they stay\n * verbatim, and the next compaction takes them.\n */\nfunction fitOldest(\n drop: AparteMessage[], prompt: string, contextWindow: number, maxTokens: number, baseTokens: number, scale: number,\n): number {\n const room = contextWindow - maxTokens - DEFAULT_COMPACTION_CONFIG.reservedThinking - DEFAULT_COMPACTION_CONFIG.safetyMargin;\n // `scale` is how far the heuristic undercounts this conversation, so the request is priced\n // in the tokens the model will count, the way the selection is.\n let used = scale * (estimateTokens(askFor(prompt, baseTokens > 0)) + baseTokens);\n for (let i = 0; i < drop.length; i++) {\n used += scale * summarisable([drop[i]!]).reduce((sum, m) => sum + estimateTokens(contentToText(m.content)), 0);\n if (used > room) return i;\n }\n return drop.length;\n}\n\n/** What `compact()` settled on before it paid for a summary: where the cut falls and what goes to the model. */\ninterface Plan {\n target: CompactionTarget;\n /** Summarised by this compaction, oldest first. */\n drop: AparteMessage[];\n /** The first message kept — a user message, the one the checkpoint will sit on. */\n holder: AparteMessage;\n /** The summary the previous checkpoint holds, and how many messages it stands for. */\n previous?: { summary: string; count: number };\n /** The message that carries that earlier checkpoint, to see before writing that it still does. */\n previousHolderId?: string;\n /** The first request, ready to append to the new summary, or `''`. */\n firstRequest: string;\n maxTokens: number;\n prompt: string;\n}\n\ntype Planned =\n | { kind: 'plan'; plan: Plan }\n | { kind: 'skip'; reason: CompactionSkipReason }\n | { kind: 'fail'; error: string; messageId?: string };\n\n/**\n * Install compaction on a config. Returns the controller, which is also registered as the\n * config's compactor; the same call on the same config replaces the previous setup (its\n * listeners are removed first).\n *\n * ```ts\n * import { setupCompaction } from '@aparte/plugin-compaction';\n *\n * const compaction = setupCompaction(); // the global config, the current model's budget\n * // `<aparte-context>` now offers \"Compact now\", and the separator's Retry works;\n * // or ask yourself:\n * await compaction.compact();\n * ```\n */\nexport function setupCompaction(options: CompactionSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): CompactionController {\n controllers.get(config)?.dispose();\n\n const keepWithoutWindow = Math.max(0, options.keepWithoutWindow ?? 4);\n const scope = options.scopeToTargetId;\n const threshold = options.threshold !== undefined && options.threshold > 0 ? Math.min(options.threshold, 1) : DEFAULT_THRESHOLD;\n\n /** The window the gauge's request named, kept as the last resort for one release. */\n let windowHint: number | undefined;\n /**\n * The window everything here measures against: the current model's, else the host's\n * `contextWindow`, else the gauge's. Read at each use — a model change is picked up on\n * the next decision, never guessed.\n */\n const windowFor = (): number | undefined => {\n const declared = config.getCurrentModel()?.contextWindow;\n if (declared && declared > 0) return declared;\n const own = typeof options.contextWindow === 'function' ? options.contextWindow() : options.contextWindow;\n return own && own > 0 ? own : windowHint;\n };\n const selectDefault = defaultSelector(config, keepWithoutWindow, windowFor);\n\n /** The chat to compact: the host's own resolver, else the chat element the request names, else the page. */\n const resolveFor = (element: Element | null | undefined, targetId?: string): CompactionTarget | null => {\n if (options.resolveTarget) {\n const own = options.resolveTarget(targetId);\n if (own) return own;\n }\n const named = element ? asTarget(element) : null;\n if (named) return named;\n return options.resolveTarget ? null : resolveDomTarget(targetId);\n };\n\n /** The summaries being written, by chat: two chats on one setup compact independently. */\n const running = new Map<string, { abort: AbortController; done: Promise<void> }>();\n let disposed = false;\n\n /**\n * Is a window event for this setup? Scoped: only the chat it names. Otherwise a chat\n * is answered by the setup on ITS OWN config (the one its boundary, `attachConfig`,\n * resolves), and only when that config has none by another: the global setup answers\n * every chat (the single-chat page, which must need no wiring), a setup on a config of\n * its own answers a chat on the global config. Never both: a page with a global setup\n * and an instance one answered a global chat twice — two summaries, one from the\n * wrong model.\n */\n const addressed = (e: Event): boolean => {\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n if (scope) return targetId === scope;\n // A chat a scoped setup names is that setup's alone.\n if (targetId && scopedChats.has(targetId)) return false;\n if (typeof document === 'undefined') return true;\n const el = targetId\n ? document.getElementById(targetId)\n : document.querySelector<HTMLElement>('aparte-chat, aparte-chat-viewport, [data-aparte-chat]');\n if (!el && targetId) {\n // Not on the page: the setup whose own resolver knows it answers it, and only that\n // one. When no resolver knows it, the routing below picks a setup as for any other\n // chat, and that one says it found nothing: a request is never dropped in silence.\n if (options.resolveTarget && options.resolveTarget(targetId)) return true;\n for (const other of ownResolvers.keys()) if (other(targetId)) return false;\n }\n const owner = el ? resolveConfig(el) : aparteGlobalConfig;\n if (owner === config) return true;\n if (answering.has(owner)) return false;\n if (config === aparteGlobalConfig) return true;\n // A chat on the global config, which has no setup: one setup answers it, the first that\n // answers for a whole config. Each of them did, and the chat was summarised — or\n // reported missing — once per setup on the page.\n return owner === aparteGlobalConfig && answering.values().next().value === config;\n };\n const onCompact = (e: Event): void => {\n if (!addressed(e)) return;\n const detail = (e as CustomEvent<AparteCompactEventDetail | undefined>).detail;\n if (typeof detail?.window === 'number' && detail.window > 0) windowHint = detail.window;\n // The gauge's deprecated `auto-compact` says its request is automatic: the back-off\n // counts it. A button's request, which says nothing, is a person's.\n void run({\n target: null,\n reason: detail?.auto ? 'auto' : 'manual',\n ...(detail?.targetId !== undefined ? { targetId: detail.targetId } : {}),\n });\n };\n const onAbort = (e: Event): void => {\n // An unnamed abort stops whatever runs. One that names a chat stops ours when it is\n // the chat we compact — or, compacting an unnamed one, when it is a chat we answer.\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n for (const [key, entry] of running) {\n if (targetId && (key ? targetId !== key : !addressed(e))) continue;\n entry.abort.abort();\n }\n };\n const listening = options.listen !== false && typeof window !== 'undefined';\n\n // ── The policy: when to compact without being asked ─────────────────────────────────\n\n /**\n * Automatic compaction is on, and a setup that hears nothing decides nothing: `listen:\n * false` is a host that drives `compact()` itself, and a host that does has not given\n * the plugin the say over when.\n */\n const autoOn = options.auto !== false && listening;\n const policies = new Map<string, ChatPolicy>();\n const policyOf = (key: string): ChatPolicy => {\n let policy = policies.get(key);\n if (!policy) {\n policy = { failures: 0, suspended: false, cooling: false };\n policies.set(key, policy);\n }\n return policy;\n };\n /** A summariser can run: the host's own, or a provider registered for the current model. */\n const canSummarise = (): boolean => {\n if (options.summarize) return true;\n const providerId = config.getModelConfig().defaultProvider;\n return Boolean(providerId && config.getAIProvider(providerId));\n };\n /**\n * How far the character heuristic undercounts this conversation: real tokens per estimated\n * token, the factor the context measure calibrates the same way (the last reply's real\n * input over the estimate recorded when it was sent, clamped, and 1 when it has none).\n * Read off the measure itself, forced to an estimate, so the selection, the pre-send\n * comparison and the gauge price a history with one number and cannot drift apart.\n */\n const scaleOf = (messages: AparteMessage[], size: number | undefined): number => {\n const measure = measureContext({\n messages, config, viewChanged: true, ...(size !== undefined ? { windowOverride: size } : {}),\n });\n const estimate = measure.parts.system + measure.parts.tools + measure.parts.summary + measure.parts.messages;\n return estimate > 0 && measure.used > 0 ? measure.used / estimate : 1;\n };\n /**\n * May the plugin start a compaction of this chat on its own, now? `cooling` holds back only\n * the after-turn trigger — a context that a compaction could not bring under the threshold\n * would otherwise pay for a summary every turn. Before a send it does not apply: a request\n * that does not fit the window is reason enough, and waiting for a turn to end below the\n * threshold would mean waiting forever on a server that truncates instead of refusing.\n */\n const mayAct = (policy: ChatPolicy, beforeSend = false): boolean =>\n autoOn && canSummarise() && policy.failures < MAX_FAILURES && !policy.suspended && (beforeSend || !policy.cooling);\n\n /**\n * A turn ended: measure what the model would read next, and compact when it is at the\n * threshold. `aparte-message-done` is dispatched once the reply is completed and its\n * usage recorded, so the measure reads the real figure the provider reported.\n *\n * Lifts the hysteresis when this measure is below the threshold, and only then: a turn\n * that completes with the context still at the threshold is not a reason to try again,\n * and the same summary was being paid for after every turn of a conversation that sat\n * near its window.\n */\n const onTurnDone = (e: Event): void => {\n if (!addressed(e) || !autoOn) return;\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n const policy = policyOf(targetId ?? '');\n const element = e.target instanceof Element ? e.target : null;\n const target = resolveFor(element, targetId);\n const size = windowFor();\n if (!target || !size) return;\n if (measureContext({ messages: target.getMessages(), config, windowOverride: size }).used < threshold * size) {\n policy.cooling = false;\n return;\n }\n if (!mayAct(policy)) return;\n void run({ target: element, reason: 'auto', ...(targetId !== undefined ? { targetId } : {}) });\n };\n /**\n * A Restore: the person asked for the original messages to go to the model again, so the\n * policy stops proposing the summary until they ask for it themselves.\n */\n const onRestored = (e: Event): void => {\n if (!(e as CustomEvent<AparteCompactDoneEventDetail | undefined>).detail?.restored || !addressed(e)) return;\n const key = (e as CustomEvent<AparteCompactDoneEventDetail>).detail.targetId ?? '';\n const policy = policyOf(key);\n policy.suspended = true;\n policy.cooling = false;\n delete policy.lastRange;\n // A summary still being written for this chat was built on a checkpoint the person has\n // just taken off: finishing it would put that checkpoint back, seconds after they\n // confirmed the Restore. The run turns the abort into a failed outcome and writes nothing.\n running.get(key)?.abort.abort();\n };\n /** A conversation switch (or a reset): nothing remembered was about this one. */\n const onSwitch = (): void => policies.clear();\n\n if (listening) {\n window.addEventListener('aparte-compact', onCompact);\n window.addEventListener('aparte-abort', onAbort);\n window.addEventListener('aparte-message-done', onTurnDone);\n window.addEventListener('aparte-compact-done', onRestored);\n window.addEventListener('aparte-reset-done', onSwitch);\n }\n if (listening && !scope) answering.add(config);\n if (listening && scope) scopedChats.set(scope, (scopedChats.get(scope) ?? 0) + 1);\n const ownResolver = listening && !scope ? options.resolveTarget : undefined;\n if (ownResolver) ownResolvers.set(ownResolver, (ownResolvers.get(ownResolver) ?? 0) + 1);\n\n /**\n * Where the cut falls, over the model's view of the conversation: from the latest\n * checkpoint on, less the turn being prepared. Pure — no model call — so `compact()`\n * and a preview read the same plan.\n */\n const plan = (\n element: Element | null | undefined, targetId: string | undefined, exclude: ReadonlySet<string>, reason: AparteCompactorReason,\n ): Planned => {\n const target = resolveFor(element, targetId);\n if (!target) return { kind: 'fail', error: targetId ? `No chat with id \"${targetId}\" to compact` : 'No chat found to compact' };\n\n const path = target.getMessages();\n if (path.length === 0) return { kind: 'skip', reason: 'empty' };\n // The turn being prepared is not \"in flight\": the client names it, and it is the reason\n // the compaction was asked for. Any OTHER message still streaming is.\n if (path.some((m) => !exclude.has(m.id) && inFlight(m))) return { kind: 'skip', reason: 'streaming' };\n\n const contextWindow = windowFor();\n // The default selector's fallback without a window sends all but the last few turns\n // in one request, sized by nothing: a small model answers 400, context exceeded. A host\n // that sized it itself (its selector, its summariser, a `keepWithoutWindow`) owns that.\n const hostSizes = options.selector || options.summarize || options.keepWithoutWindow !== undefined;\n if (!contextWindow && !hostSizes) return { kind: 'skip', reason: 'no-window' };\n\n // What the model reads from now on: the path from its latest checkpoint, whose first\n // message is the one that carries it. Everything above is already inside that summary.\n const checkpointed = historyFromCheckpoint(path);\n const holderOfPrevious = checkpointed.summary !== undefined ? checkpointed.messages[0] : undefined;\n const previous = holderOfPrevious?.contextSummary\n ? { summary: holderOfPrevious.contextSummary.summary, count: holderOfPrevious.contextSummary.count }\n : undefined;\n const view = checkpointed.messages.filter((m) => !exclude.has(m.id));\n\n // How far the heuristic undercounts this conversation: the selection, the summary\n // request and the room for the turn being prepared are all priced with it, in the\n // tokens the model counts rather than the ones the heuristic does.\n const scale = contextWindow ? scaleOf(path, contextWindow) : 1;\n let selected: CompactionSelection<AparteMessage>;\n if (options.selector) {\n // A host's own selector is called with the messages alone: what it keeps is its call.\n selected = options.selector(view);\n } else {\n // The default one is sized to where the compaction should LEAVE the context, not to\n // the budget's near-full edge (about 88 % of a 128K window, a threshold below which\n // nothing would ever be dropped, and one a compaction lands on only to be asked\n // again after the next turn), and with room for the turn being prepared, which is\n // not in `view` but will be in the request.\n const reserve = Math.round(scale * estimateWireTokens(path.filter((m) => exclude.has(m.id))));\n const limits: CompactionLimits = { scale, reserve, landAt: Math.max(0, threshold - LANDING_MARGIN) };\n // An overflow says the request that was built is too big, whatever the estimate\n // thought of it: cut well below it.\n if (reason === 'overflow') limits.ceiling = Math.floor(OVERFLOW_SHRINK * (scale * estimateWireTokens(view) + reserve));\n selected = selectDefault(view, limits);\n }\n if (selected.drop.length === 0) return { kind: 'skip', reason: 'nothing-to-drop' };\n // The selector speaks of two sets; a checkpoint is one boundary. Take the run of\n // dropped messages at the front — a selector that picks a hole in the middle is not\n // asking for a summary the model could read in order.\n const droppedIds = new Set(selected.drop.map((m) => m.id));\n let boundary = 0;\n while (boundary < view.length && droppedIds.has(view[boundary]!.id)) boundary++;\n if (boundary === 0) return { kind: 'skip', reason: 'nothing-to-drop' };\n\n const prompt = options.prompt ?? DEFAULT_COMPACTION_PROMPT;\n const maxTokens = summaryAnswerCap(config, contextWindow);\n const base = previous ? splitFirstRequest(previous.summary) : undefined;\n const baseTokens = base ? estimateTokens(base.base) : 0;\n // Your own `summarize` is not trimmed to the window — the sizing is yours.\n const fit = (n: number): number =>\n contextWindow && !options.summarize ? fitOldest(view.slice(0, n), prompt, contextWindow, maxTokens, baseTokens, scale) : n;\n\n // The turns that do not fit the summary request are not summarised now, not lost: they\n // stay verbatim after the checkpoint, and the next compaction takes them.\n const fits = fit(boundary);\n if (fits === 0) return { kind: 'fail', error: \"The oldest turn to summarise does not fit the model's context window\" };\n const cut = cutAtUser(view, fits);\n if (cut === null) return { kind: 'skip', reason: 'nothing-to-drop' };\n // Moved toward the newer end, the cut summarises more than the request was sized for.\n if (cut > fits && fit(cut) < cut) return { kind: 'skip', reason: 'nothing-to-drop' };\n\n const drop = view.slice(0, cut);\n const holder = view[cut]!;\n\n // The first request survives every compaction word for word: a summariser paraphrases\n // away what the user first asked, and every later turn is judged against it. Found in\n // the first summary, carried through each update; never asked of the model.\n let firstRequest = base?.block ?? '';\n if (!previous) {\n const first = path.find((m) => m.role === 'user' && !m.compaction);\n const text = first && drop.some((m) => m.id === first.id) ? messageText(first).trim() : '';\n if (text) firstRequest = firstRequestBlock(text);\n }\n return {\n kind: 'plan',\n plan: {\n target, drop, holder, ...(previous ? { previous } : {}),\n ...(holderOfPrevious ? { previousHolderId: holderOfPrevious.id } : {}),\n firstRequest, maxTokens, prompt,\n },\n };\n };\n\n const run = async (request: AparteCompactorRequest & { beforeSend?: true }): Promise<CompactionOutcome> => {\n const targetId = request.targetId ?? scope ?? (request.target?.id || undefined);\n const reason = request.reason ?? 'manual';\n const key = targetId ?? '';\n const policy = policyOf(key);\n let messageId: string | undefined;\n // The events are written out literally, one per site: the docs' events\n // reference reads the dispatch itself to say where an event goes out from.\n const fail = (error: string): CompactionOutcome => {\n // A stop the person asked for is not the summary failing.\n if (error !== ABORTED) policy.failures++;\n // A failure is also said on the console: the documented way to ask is a\n // fire-and-forget `aparte-compact`, and a page with no listener on the\n // error event would otherwise fail in total silence.\n console.warn(`[aparte/plugin-compaction] compaction failed: ${error}`);\n if (hasWindow) {\n window.dispatchEvent(new CustomEvent<AparteCompactErrorEventDetail>('aparte-compact-error', {\n detail: { error, targetId, ...(messageId ? { messageId } : {}), ...(error === ABORTED ? { aborted: true as const } : {}) },\n }));\n }\n return { ok: false, error, targetId, ...(messageId ? { messageId } : {}) };\n };\n const skip = (reason: CompactionSkipReason): CompactionOutcome => {\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactDoneEventDetail>('aparte-compact-done', { detail: { skipped: true, reason, targetId } }));\n return { ok: true, skipped: true, reason, targetId };\n };\n\n if (disposed) return fail('This compaction setup was disposed');\n // A scoped setup compacts its own chat. Refused quietly: another setup may own the\n // one asked for, and an error event here would tell the page it failed.\n if (scope && request.targetId !== undefined && request.targetId !== scope) {\n return { ok: false, error: `This compaction setup only compacts the chat \"${scope}\"`, targetId: request.targetId };\n }\n // `auto: false` means manual only: nobody asked for an automatic attempt or an overflow\n // retry, so neither is made — reported as a skip, the one answer every caller already\n // handles, and the client's error stands exactly as it did before there was a plugin.\n if ((reason === 'auto' || reason === 'overflow') && !autoOn) return skip('nothing-to-drop');\n // Somebody asked, so the policy may try again: the back-off and a Restore both wait for\n // exactly this. (An overflow is neither: the model refused a request, nobody chose it.)\n if (reason === 'manual' || reason === 'retry') {\n policy.failures = 0;\n policy.suspended = false;\n }\n // An overflow waits for the summary already being written for its chat — the retry is\n // measured against the history that summary leaves — where anything else is told to wait.\n if (reason === 'overflow') for (let busy = running.get(key); busy; busy = running.get(key)) await busy.done;\n // The turn that asked was stopped: no summary is paid for it.\n if (request.signal?.aborted) return fail(ABORTED);\n if (running.has(key)) return skip('running');\n // Nobody asked, and the policy has said not now. Reported as nothing to do — a skip is\n // the one answer every caller already handles — and `force` overrides it.\n if (reason === 'auto' && !request.force && (policy.failures >= MAX_FAILURES || policy.suspended || (policy.cooling && !request.beforeSend))) {\n return skip('nothing-to-drop');\n }\n\n const planned = plan(request.target, targetId, new Set(request.exclude ?? []), reason);\n if (planned.kind === 'skip') return skip(planned.reason);\n if (planned.kind === 'fail') {\n if (planned.messageId) messageId = planned.messageId;\n return fail(planned.error);\n }\n const { target, drop, holder, previous, previousHolderId, firstRequest, maxTokens, prompt } = planned.plan;\n messageId = holder.id;\n // The same range twice is the loop the hysteresis exists to prevent.\n const range = `${drop[0]!.id}..${drop[drop.length - 1]!.id}`;\n if (reason === 'auto' && !request.force && policy.lastRange === range) return skip('nothing-to-drop');\n // Checked before the model is paid: there is no replace fallback, and a summary that\n // cannot be written is a summary nobody should wait for.\n if (!target.updateMessage) return fail('This chat cannot hold a checkpoint: its transcript has no updateMessage');\n\n const modelConfig = config.getModelConfig();\n const providerId = modelConfig.defaultProvider;\n let provider: AparteAIProvider | undefined;\n if (!options.summarize) {\n if (!providerId) return fail('No provider configured');\n provider = config.getAIProvider(providerId);\n if (!provider) return fail(`Provider '${providerId}' not found`);\n }\n\n // What the model is shown of the earlier checkpoint is the summary it wrote, without\n // the first request we appended to it (see above): it would copy the block into its\n // own answer, and the block would be there twice.\n const summarySoFar = previous ? splitFirstRequest(previous.summary).base : undefined;\n const turns = alternating(summarisable(drop));\n // Nothing to read: the turns being summarised say nothing a summary could carry.\n // Paying a model to summarise an empty transcript and then standing the answer in\n // for the conversation is worse than doing nothing.\n if (turns.length === 0) return fail('Nothing summarisable in the dropped turns');\n const summariseRequest = buildRequest(turns, summarySoFar, prompt, modelConfig.defaultModel || '', maxTokens);\n\n const abort = new AbortController();\n // The turn's own Stop reaches the summary whether or not this setup listens on `window`.\n const onSignal = (): void => abort.abort();\n request.signal?.addEventListener('abort', onSignal, { once: true });\n let finished!: () => void;\n running.set(key, { abort, done: new Promise<void>((resolve) => { finished = resolve; }) });\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactStartEventDetail>('aparte-compact-start', { detail: { targetId, messageId } }));\n\n try {\n const raw = await withAbort(\n options.summarize\n ? options.summarize(summariseRequest, abort.signal)\n : summarizeThroughTransport(config, provider!, summariseRequest, abort.signal, options.keyResolver),\n abort.signal,\n );\n const modelSummary = withoutThinking(raw);\n if (!modelSummary) throw new Error('Empty summary returned by model');\n // A summary that is not shorter than what it replaces frees nothing: refuse it, the\n // conversation is left as it is. What it replaces is the summary it updates and\n // the turns it folds in — the appended first request is on both sides of that\n // account, so it is on neither.\n const replaced = estimateTokens(summarySoFar) + turns.reduce((sum, m) => sum + estimateTokens(contentToText(m.content)), 0);\n if (estimateTokens(modelSummary) >= replaced) throw new Error('The summary is no shorter than the turns it replaces');\n\n // One write: the checkpoint, on the first message that stays. Not a replacement of\n // the transcript — the messages above it are untouched, which is also what makes\n // a Restore possible. By id, and only if that message is still on the path: a\n // conversation switch (or a reset) replaces the whole path while the summary is\n // written, and a success the host dropped would be reported as one.\n const live = target.getMessages();\n const at = live.findIndex((m) => m.id === holder.id);\n if (at < 0) throw new Error(CHANGED);\n // A summary that updates an earlier checkpoint is refused when that checkpoint is\n // gone: a Restore took it off while this was being written, and the update would\n // bring back what the person had just sent away.\n if (previousHolderId && !live.find((m) => m.id === previousHolderId)?.contextSummary) throw new Error(CHANGED);\n const summary = `${modelSummary}${firstRequest}`;\n const count = drop.length + (previous?.count ?? 0);\n target.updateMessage!(holder.id, { contextSummary: { summary, count, createdAt: Date.now() } });\n\n // What the policy learns from it: a success clears the back-off, remembers the range,\n // and, when what the model reads next is still over the threshold (a summary that\n // saved little, a long kept tail), waits for another turn before trying again.\n policy.failures = 0;\n policy.lastRange = range;\n const size = windowFor();\n const after = measureContext({ messages: target.getMessages(), config, ...(size !== undefined ? { windowOverride: size } : {}) });\n policy.cooling = after.window !== undefined && after.used >= threshold * after.window;\n\n const kept = live.length - at;\n const outcome: CompactionOutcome = { ok: true, skipped: false, summary, kept, dropped: drop.length, messageId: holder.id, count, targetId };\n if (hasWindow) window.dispatchEvent(new CustomEvent<AparteCompactDoneEventDetail>('aparte-compact-done', { detail: { summary, kept, dropped: drop.length, targetId, messageId: holder.id, count } }));\n return outcome;\n } catch (err: unknown) {\n const aborted = abort.signal.aborted || (err instanceof Error && err.name === 'AbortError');\n return fail(aborted ? ABORTED : err instanceof Error ? err.message : String(err));\n } finally {\n request.signal?.removeEventListener('abort', onSignal);\n running.delete(key);\n finished();\n }\n };\n\n const controller: CompactionController = {\n get running() {\n return running.size > 0;\n },\n\n abort() {\n for (const entry of running.values()) entry.abort.abort();\n },\n\n dispose() {\n if (disposed) return;\n disposed = true;\n controller.abort();\n if (listening) {\n window.removeEventListener('aparte-compact', onCompact);\n window.removeEventListener('aparte-abort', onAbort);\n window.removeEventListener('aparte-message-done', onTurnDone);\n window.removeEventListener('aparte-compact-done', onRestored);\n window.removeEventListener('aparte-reset-done', onSwitch);\n if (scope) {\n const left = (scopedChats.get(scope) ?? 1) - 1;\n if (left > 0) scopedChats.set(scope, left);\n else scopedChats.delete(scope);\n }\n }\n if (ownResolver) {\n const left = (ownResolvers.get(ownResolver) ?? 1) - 1;\n if (left > 0) ownResolvers.set(ownResolver, left);\n else ownResolvers.delete(ownResolver);\n }\n // Only if it is still the registered one: a later setup replaced it, and took the seam.\n if (config.getCompactor() === controller) config.setCompactor(null);\n if (controllers.get(config) === controller) {\n controllers.delete(config);\n answering.delete(config);\n }\n },\n\n compact(request) {\n return run(typeof request === 'object' ? request : { target: null, ...(request !== undefined ? { targetId: request } : {}) });\n },\n\n async settled(targetId) {\n // The chat's own summary — and an unnamed chat's, which may be the same one. Loops: a\n // compaction that ends may be followed by one queued behind it.\n const pending = (): Promise<void>[] => [...running]\n .filter(([key]) => targetId === undefined || key === '' || key === targetId)\n .map(([, entry]) => entry.done);\n for (let waiting = pending(); waiting.length > 0; waiting = pending()) await Promise.all(waiting);\n },\n\n async beforeSend({ target, targetId, exclude, requestTokens, signal }) {\n // A send never races a summary: whatever is running for its chat finishes first, and\n // the request is measured against the conversation it left behind.\n await controller.settled(targetId);\n const id = targetId ?? scope ?? (target?.id || undefined);\n const policy = policyOf(id ?? '');\n const size = windowFor();\n if (!size || !mayAct(policy, true)) return false;\n // The request's estimate is the raw heuristic's; the model counts in its own tokens,\n // and the measure has calibrated how far apart the two are for this conversation.\n const chat = resolveFor(target, id);\n const scale = chat ? scaleOf(chat.getMessages(), size) : 1;\n // The room the model keeps for its reply, as the budget everywhere else reads it.\n if (scale * requestTokens + DEFAULT_COMPACTION_CONFIG.reservedGeneration <= size) return false;\n const outcome = await run({\n target, reason: 'auto', beforeSend: true, exclude, ...(id !== undefined ? { targetId: id } : {}), ...(signal ? { signal } : {}),\n });\n // `true` only when a checkpoint was written: the caller builds the request again.\n return outcome.ok && !outcome.skipped;\n },\n\n preview(request) {\n if (scope && request.targetId !== undefined && request.targetId !== scope) return null;\n const id = request.targetId ?? scope ?? (request.target?.id || undefined);\n if (disposed || running.has(id ?? '') || !canSummarise()) return null;\n // What a person asking now would get: a manual request, no turn being prepared.\n const planned = plan(request.target, id, new Set(), 'manual');\n if (planned.kind !== 'plan') return null;\n return { messages: planned.plan.drop.length, tokens: estimateWireTokens(planned.plan.drop) };\n },\n };\n\n controllers.set(config, controller);\n config.setCompactor(controller);\n return controller;\n}\n"],"names":["reason"],"mappings":"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"}
@@ -36,7 +36,37 @@ export interface CompactionSelection<M> {
36
36
  * function, not on the factory: a factory-level `<M>` has no inference site at
37
37
  * `createCompactionSelector({...})` and falls back to the bare `CompactableMessage`.
38
38
  */
39
- export type CompactionSelector = <M extends CompactableMessage>(messages: M[]) => CompactionSelection<M>;
39
+ export type CompactionSelector = <M extends CompactableMessage>(messages: M[], limits?: CompactionLimits) => CompactionSelection<M>;
40
+ /**
41
+ * What one call can tighten beyond the budget the selector was built with. The plugin's own
42
+ * default selector is called with these, per compaction; a host that passes a selector of
43
+ * its own as `setupCompaction`'s `selector` is called with the messages alone, and one that
44
+ * calls this selector directly may leave them out. All of it is in REAL tokens, the ones the
45
+ * model counts, so a heuristic that undercounts (`scale`) is accounted for on both sides.
46
+ */
47
+ export interface CompactionLimits {
48
+ /**
49
+ * How far the character heuristic undercounts: real tokens per estimated token, the way
50
+ * the context measure calibrates it. The messages AND the system prompt and tools are
51
+ * priced at `scale` times their estimate. Default `1`.
52
+ */
53
+ scale?: number;
54
+ /**
55
+ * Tokens the history must leave room for: the turn being prepared, which is not in the
56
+ * messages handed over but will be in the request. A long new message is made room for,
57
+ * down to `minKeep` messages. Default `0`.
58
+ */
59
+ reserve?: number;
60
+ /**
61
+ * Aim below the budget: the share of the window (0..1) the request should land at once
62
+ * the history is trimmed, the fixed costs (system prompt, tools, the reply's room, the
63
+ * summary's slot) counted. Never above the budget; without it the selector keeps as much
64
+ * as the budget allows, which on a large window is within a few percent of full.
65
+ */
66
+ landAt?: number;
67
+ /** The most the kept history and `reserve` may take together, when something known to have failed bounds it. */
68
+ ceiling?: number;
69
+ }
40
70
  export interface CompactionSelectorOptions {
41
71
  /**
42
72
  * The active model's context window, in tokens — a number, or a getter read at
@@ -53,6 +83,16 @@ export interface CompactionSelectorOptions {
53
83
  config?: Partial<Omit<CompactionConfig, 'contextWindow'>>;
54
84
  /** Never summarise fewer than this many of the newest messages. Default 2 — the last exchange. */
55
85
  minKeep?: number;
86
+ /**
87
+ * What one message costs, in tokens. Default: the character estimate of its text
88
+ * (`content`, else its segments' text and its tool calls' input and result).
89
+ *
90
+ * Pass core's `estimateWireTokens` for one message to measure what the wire actually
91
+ * sends — a thinking block, which the model is never shown, then costs nothing —
92
+ * which is what `setupCompaction`'s own default selector does. A direct caller keeps
93
+ * the default, so nothing it already relies on moves.
94
+ */
95
+ estimate?: (message: CompactableMessage) => number;
56
96
  }
57
97
  /**
58
98
  * Build the selector. `setupCompaction` builds this one itself over the current model;
@@ -1 +1 @@
1
- {"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../src/selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA4D,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9G;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACrC;AAED,wFAAwF;AACxF,MAAM,WAAW,mBAAmB,CAAC,CAAC;IAClC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,EAAE,CAAC,EAAE,CAAC;CACb;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,SAAS,kBAAkB,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAEzG,MAAM,WAAW,yBAAyB;IACtC;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD,iGAAiG;IACjG,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC1D,0FAA0F;IAC1F,KAAK,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IAClC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC,CAAC;IAC1D,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAwBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAyB/F"}
1
+ {"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../src/selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA4D,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9G;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACrC;AAED,wFAAwF;AACxF,MAAM,WAAW,mBAAmB,CAAC,CAAC;IAClC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,EAAE,CAAC,EAAE,CAAC;CACb;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,SAAS,kBAAkB,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,gBAAgB,KAAK,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAEpI;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gHAAgH;IAChH,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACtC;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD,iGAAiG;IACjG,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC1D,0FAA0F;IAC1F,KAAK,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IAClC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC,CAAC;IAC1D,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC;CACtD;AAwBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CA4C/F"}
@@ -12,6 +12,14 @@ import type { AparteMessage } from '@aparte/core';
12
12
  * The summariser's instruction. English, because it is addressed to the model and not
13
13
  * to the user; a host that wants another language or emphasis passes `prompt`.
14
14
  *
15
+ * It fixes the SECTIONS of a summary rather than leaving them to the model's taste: a
16
+ * summary is read by a model that has nothing else, and one that is free to choose its
17
+ * shape drops the section it finds least interesting, which is usually the open tasks.
18
+ * It asks for the conversation's own language because a summary in another one is
19
+ * answered in it. The same summary is also handed back to be UPDATED at the next
20
+ * compaction (the request then opens with "Summary so far:"), so the sections are what
21
+ * keeps a summary of a summary from drifting into prose.
22
+ *
15
23
  * The last sentence is the one that is easy to leave out and expensive to omit. The
16
24
  * instruction rides the final user turn, which is also where a reply to the
17
25
  * conversation would go, so a model reading a transcript that ends in a question has
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,QAQc,CAAC;AAWrD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqB1D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAkBnE"}
1
+ {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,QAYc,CAAC;AAWrD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqB1D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAkBnE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/plugin-compaction",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "homepage": "https://apartejs.dev/plugins/compaction/",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -28,12 +28,12 @@
28
28
  "node": ">=18"
29
29
  },
30
30
  "peerDependencies": {
31
- "@aparte/core": ">=0.24.0 <1.0.0"
31
+ "@aparte/core": ">=0.26.0 <1.0.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "^5.4.0",
35
35
  "vite": "^6.0.0",
36
- "@aparte/core": "0.24.0"
36
+ "@aparte/core": "0.26.0"
37
37
  },
38
38
  "keywords": [
39
39
  "aparte",