@aparte/plugin-compaction 0.16.0 → 0.16.1
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/compaction.d.ts +10 -8
- package/dist/compaction.d.ts.map +1 -1
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/transcript.d.ts +7 -0
- package/dist/transcript.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/compaction.d.ts
CHANGED
|
@@ -95,10 +95,11 @@ export interface CompactionSetupOptions {
|
|
|
95
95
|
/** How many of the newest messages stay when the model declares no window. Default 4 — the last two exchanges. */
|
|
96
96
|
keepWithoutWindow?: number;
|
|
97
97
|
/**
|
|
98
|
-
* The summariser's
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
98
|
+
* The summariser's instruction — carried by the ask itself, not by a `system`
|
|
99
|
+
* message, so a provider that imposes its own system prompt cannot drop it.
|
|
100
|
+
* English by default — it is addressed to the model, not to the user — asking for
|
|
101
|
+
* the decisions, the open tasks and the tool results that still matter. Replace it
|
|
102
|
+
* to steer the summary (a language, a domain, a length).
|
|
102
103
|
*/
|
|
103
104
|
prompt?: string;
|
|
104
105
|
/**
|
|
@@ -108,10 +109,11 @@ export interface CompactionSetupOptions {
|
|
|
108
109
|
*/
|
|
109
110
|
keyResolver?: CompactionKeyResolver;
|
|
110
111
|
/**
|
|
111
|
-
* Replace the model call entirely. The request carries the
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* model for summaries. With one, no
|
|
112
|
+
* Replace the model call entirely. The request carries the transcript of the
|
|
113
|
+
* dropped turns, then a final `user` message holding the summariser's instruction
|
|
114
|
+
* and the ask, and `_meta: { compaction: true }`; return the summary text. For a
|
|
115
|
+
* host with an endpoint of its own, or a cheaper model for summaries. With one, no
|
|
116
|
+
* provider needs to be configured at all.
|
|
115
117
|
*/
|
|
116
118
|
summarize?: CompactionSummarizer;
|
|
117
119
|
/**
|
package/dist/compaction.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../src/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAEH,KAAK,YAAY,EAAE,KAAK,aAAa,EAA0B,KAAK,iBAAiB,EAGxF,MAAM,cAAc,CAAC;AACtB,OAAO,EAA4B,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGnF;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC7B,WAAW,IAAI,aAAa,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;CAC/C;AAED,qGAAqG;AACrG,MAAM,MAAM,yBAAyB,GAAG,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,mBAAmB,CAAC,aAAa,CAAC,CAAC;AAE1G,+GAA+G;AAC/G,MAAM,MAAM,qBAAqB,GAAG,CAChC,UAAU,EAAE,MAAM,KACjB,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC;AAEtH,yFAAyF;AACzF,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAExG,oCAAoC;AACpC,MAAM,MAAM,oBAAoB;AAC5B,+BAA+B;AAC7B,OAAO;AACT,wFAAwF;GACtF,iBAAiB;AACnB,sDAAsD;GACpD,SAAS;AACX,gFAAgF;GAC9E,WAAW,CAAC;AAElB,MAAM,MAAM,iBAAiB,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/F;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,WAAW,sBAAsB;IACnC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,kHAAkH;IAClH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../src/compaction.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ function createCompactionSelector(options) {
|
|
|
82
82
|
return { keep: messages.slice(cut), drop: messages.slice(0, cut) };
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
const DEFAULT_COMPACTION_PROMPT = "You are compacting a conversation between a user and an assistant so that it can continue with less context. Write a summary the assistant can pick the work up from: what the user wants and why, the decisions taken and their reasons, the tasks still open, the facts and tool results that still matter (file names, values, errors, outcomes), and anything the assistant would otherwise have to ask again. Lines marked [tool …] are tool calls with their result. Write in the third person, factually, as compact as completeness allows. No preamble.";
|
|
85
|
+
const DEFAULT_COMPACTION_PROMPT = "You are compacting a conversation between a user and an assistant so that it can continue with less context. Write a summary the assistant can pick the work up from: what the user wants and why, the decisions taken and their reasons, the tasks still open, the facts and tool results that still matter (file names, values, errors, outcomes), and anything the assistant would otherwise have to ask again. Lines marked [tool …] are tool calls with their result. Write in the third person, factually, as compact as completeness allows. No preamble. Do not continue the conversation, do not answer a question it contains and do not call a tool: reply with the summary and nothing else.";
|
|
86
86
|
const clip = (text, max) => text.length > max ? `${text.slice(0, max)}…` : text;
|
|
87
87
|
const safeJson = (value) => {
|
|
88
88
|
try {
|
|
@@ -187,9 +187,26 @@ function buildRequest(drop, prompt, modelId) {
|
|
|
187
187
|
const history = drop.filter((m) => !inFlight(m)).map((m) => ({ role: m.role, content: transcriptForSummary(m) })).filter((m) => contentToText(m.content).length > 0);
|
|
188
188
|
return {
|
|
189
189
|
messages: [
|
|
190
|
-
{ role: "system", content: prompt },
|
|
191
190
|
...history,
|
|
192
|
-
|
|
191
|
+
// The instruction rides the USER turn, not a `system` message.
|
|
192
|
+
//
|
|
193
|
+
// A system message is a persona, and a provider is entitled to own that: one
|
|
194
|
+
// serving a local model under a fixed training contract replaces the request's
|
|
195
|
+
// system message with its own, by design. When it does, the summarisation
|
|
196
|
+
// instruction simply never arrives — and nothing errors, because the request is
|
|
197
|
+
// still well formed. The model reads the bare "Please summarize this
|
|
198
|
+
// conversation." after somebody else's persona and answers something plausible:
|
|
199
|
+
// measured on a consumer's local model, one reply refused for want of internet
|
|
200
|
+
// access, one said "noted, I'll do it", and one invented figures for a client
|
|
201
|
+
// that appears nowhere in the transcript — which then landed in the transcript
|
|
202
|
+
// as the summary notice and became the premise of every following turn.
|
|
203
|
+
//
|
|
204
|
+
// The instruction is not a persona: it is the task of this one request, and the
|
|
205
|
+
// task belongs where every provider must look. Reported as #45 by a consumer who
|
|
206
|
+
// only found it by reading the output closely.
|
|
207
|
+
{ role: "user", content: `${prompt}
|
|
208
|
+
|
|
209
|
+
Please summarize the conversation above.` }
|
|
193
210
|
],
|
|
194
211
|
modelId,
|
|
195
212
|
stream: false,
|
|
@@ -294,7 +311,7 @@ function setupCompaction(options = {}, config = aparteGlobalConfig) {
|
|
|
294
311
|
if (!provider) return fail(`Provider '${providerId}' not found`);
|
|
295
312
|
}
|
|
296
313
|
const request = buildRequest(drop, options.prompt ?? DEFAULT_COMPACTION_PROMPT, modelConfig.defaultModel || "");
|
|
297
|
-
if (request.messages.length <=
|
|
314
|
+
if (request.messages.length <= 1) return fail("Nothing summarisable in the dropped turns");
|
|
298
315
|
const abort = new AbortController();
|
|
299
316
|
running = { targetId, abort };
|
|
300
317
|
if (hasWindow) window.dispatchEvent(new CustomEvent("aparte-compact-start", { detail: { targetId } }));
|
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 /** Ratio of history budget allocated to the summary block. */\n summaryRatio: number;\n /** Hard cap for summary tokens. */\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/** The history budget, split: what the running summary may take, what the verbatim window gets. */\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/** What a message costs: its text, or the text of its segments when it has no `content`. */\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 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 */\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\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 system prompt. English by default — an instruction to the\n * model, not a string the user reads — asking for the decisions, the open tasks\n * and the tool results that still matter. Replace it to steer the summary (a\n * 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 summariser's system\n * prompt, the transcript of the dropped turns and `_meta: { compaction: true }`;\n * return the summary text. For a host with an endpoint of its own, or a cheaper\n * model for summaries. With one, no 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\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 { role: 'system', content: prompt },\n ...history,\n { role: 'user', content: 'Please summarize this conversation.' },\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. On the global\n * config: every event — the single-chat page, which must need no wiring. On a\n * config of its own, the client's rule: answer a chat unless it demonstrably\n * belongs to ANOTHER instance — one whose boundary (`attachConfig`) resolves a\n * different, non-global config. Without this, two setups on two configs both\n * answered every `aparte-compact`: two summaries, one of them from the 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 if (config === aparteGlobalConfig || 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) return true;\n const owner = resolveConfig(el);\n return owner === config || owner === aparteGlobalConfig;\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 (!addressed(e) || !running) return;\n // An abort that names another chat is not ours; an unnamed one stops whatever runs.\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n if (targetId && running.targetId && targetId !== running.targetId) 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\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 }\n if (controllers.get(config) === controller) controllers.delete(config);\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 instruction and the ask, and nothing between them: the turns being\n // deleted say nothing a summary could carry. Paying a model to summarise an\n // empty transcript and then replacing the conversation with the answer is\n // worse than doing nothing.\n if (request.messages.length <= 2) 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":";AAiEO,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;ACzFA,MAAM,OAAO,CAAI,UAA6B,OAAO,UAAU,aAAc,UAAsB;AAGnG,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,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;ACtGO,MAAM,4BACT;AAOJ,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;AC+DA,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;AAExB,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,EAAE,MAAM,UAAU,SAAS,OAAA;AAAA,MAC3B,GAAG;AAAA,MACH,EAAE,MAAM,QAAQ,SAAS,sCAAA;AAAA,IAAsC;AAAA,IAEnE;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;AAUf,QAAM,YAAY,CAAC,MAAsB;AACrC,UAAM,WAAY,EAAqD,QAAQ;AAC/E,QAAI,cAAc,aAAa;AAC/B,QAAI,WAAW,sBAAsB,OAAO,aAAa,YAAa,QAAO;AAC7E,UAAM,KAAK,WACL,SAAS,eAAe,QAAQ,IAChC,SAAS,cAA2B,uDAAuD;AACjG,QAAI,CAAC,GAAI,QAAO;AAChB,UAAM,QAAQ,cAAc,EAAE;AAC9B,WAAO,UAAU,UAAU,UAAU;AAAA,EACzC;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,UAAU,CAAC,KAAK,CAAC,QAAS;AAE/B,UAAM,WAAY,EAAqD,QAAQ;AAC/E,QAAI,YAAY,QAAQ,YAAY,aAAa,QAAQ,SAAU;AACnE,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;AAEA,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;AAAA,MACtD;AACA,UAAI,YAAY,IAAI,MAAM,MAAM,WAAY,aAAY,OAAO,MAAM;AAAA,IACzE;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/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 /** Ratio of history budget allocated to the summary block. */\n summaryRatio: number;\n /** Hard cap for summary tokens. */\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/** The history budget, split: what the running summary may take, what the verbatim window gets. */\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/** What a message costs: its text, or the text of its segments when it has no `content`. */\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 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\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. On the global\n * config: every event — the single-chat page, which must need no wiring. On a\n * config of its own, the client's rule: answer a chat unless it demonstrably\n * belongs to ANOTHER instance — one whose boundary (`attachConfig`) resolves a\n * different, non-global config. Without this, two setups on two configs both\n * answered every `aparte-compact`: two summaries, one of them from the 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 if (config === aparteGlobalConfig || 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) return true;\n const owner = resolveConfig(el);\n return owner === config || owner === aparteGlobalConfig;\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 (!addressed(e) || !running) return;\n // An abort that names another chat is not ours; an unnamed one stops whatever runs.\n const targetId = (e as CustomEvent<{ targetId?: string } | undefined>).detail?.targetId;\n if (targetId && running.targetId && targetId !== running.targetId) 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\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 }\n if (controllers.get(config) === controller) controllers.delete(config);\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":";AAiEO,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;ACzFA,MAAM,OAAO,CAAI,UAA6B,OAAO,UAAU,aAAc,UAAsB;AAGnG,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,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;AC/FO,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;AAExB,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;AAUf,QAAM,YAAY,CAAC,MAAsB;AACrC,UAAM,WAAY,EAAqD,QAAQ;AAC/E,QAAI,cAAc,aAAa;AAC/B,QAAI,WAAW,sBAAsB,OAAO,aAAa,YAAa,QAAO;AAC7E,UAAM,KAAK,WACL,SAAS,eAAe,QAAQ,IAChC,SAAS,cAA2B,uDAAuD;AACjG,QAAI,CAAC,GAAI,QAAO;AAChB,UAAM,QAAQ,cAAc,EAAE;AAC9B,WAAO,UAAU,UAAU,UAAU;AAAA,EACzC;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,UAAU,CAAC,KAAK,CAAC,QAAS;AAE/B,UAAM,WAAY,EAAqD,QAAQ;AAC/E,QAAI,YAAY,QAAQ,YAAY,aAAa,QAAQ,SAAU;AACnE,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;AAEA,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;AAAA,MACtD;AACA,UAAI,YAAY,IAAI,MAAM,MAAM,WAAY,aAAY,OAAO,MAAM;AAAA,IACzE;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;"}
|
package/dist/transcript.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ import type { AparteMessage } from '@aparte/core';
|
|
|
11
11
|
/**
|
|
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
|
+
*
|
|
15
|
+
* The last sentence is the one that is easy to leave out and expensive to omit. The
|
|
16
|
+
* instruction rides the final user turn, which is also where a reply to the
|
|
17
|
+
* conversation would go, so a model reading a transcript that ends in a question has
|
|
18
|
+
* two plausible things to do: summarise, or answer. Every implementation that puts the
|
|
19
|
+
* instruction in the user turn carries a clause like this one, and one goes further and
|
|
20
|
+
* inserts a fake assistant turn to get it — a host replacing `prompt` should keep it.
|
|
14
21
|
*/
|
|
15
22
|
export declare const DEFAULT_COMPACTION_PROMPT: string;
|
|
16
23
|
/**
|
package/dist/transcript.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/plugin-compaction",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"node": ">=18"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@aparte/core": ">=0.16.
|
|
30
|
+
"@aparte/core": ">=0.16.1 <1.0.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"typescript": "^5.4.0",
|
|
34
34
|
"vite": "^6.0.0",
|
|
35
|
-
"@aparte/core": "0.16.
|
|
35
|
+
"@aparte/core": "0.16.1"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"aparte",
|