@aparte/plugin-compaction 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -35
- package/dist/budget.d.ts +14 -9
- package/dist/budget.d.ts.map +1 -1
- package/dist/compaction.d.ts +174 -78
- package/dist/compaction.d.ts.map +1 -1
- package/dist/cut.d.ts +53 -0
- package/dist/cut.d.ts.map +1 -0
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +415 -104
- package/dist/index.js.map +1 -1
- package/dist/selector.d.ts +41 -1
- package/dist/selector.d.ts.map +1 -1
- package/dist/transcript.d.ts +8 -0
- package/dist/transcript.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @aparte/plugin-compaction
|
|
2
2
|
|
|
3
|
-
Conversation **compaction** for [aparté](https://github.com/apartejs/aparte):
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
SDK ships it as an opt-in module — so this is where it lives.
|
|
3
|
+
Conversation **compaction** for [aparté](https://github.com/apartejs/aparte): when a conversation
|
|
4
|
+
outgrows the model's window, summarise the older turns for the model and keep the recent ones
|
|
5
|
+
verbatim — without deleting anything. Nothing in core compacts by itself — no UI kit does, and
|
|
6
|
+
every agent SDK ships it as an opt-in module — so this is where it lives.
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
npm install @aparte/plugin-compaction @aparte/core
|
|
@@ -12,47 +12,66 @@ npm install @aparte/plugin-compaction @aparte/core
|
|
|
12
12
|
```ts
|
|
13
13
|
import { setupCompaction } from '@aparte/plugin-compaction';
|
|
14
14
|
|
|
15
|
-
const compaction = setupCompaction(); // the global config, the current model's
|
|
15
|
+
const compaction = setupCompaction(); // the global config, the current model's window
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
That is the whole wiring. The plugin registers itself as the config's compactor and decides on
|
|
19
|
+
its own: after a turn that leaves the conversation past 80 % of the window (`threshold`), and
|
|
20
|
+
before a send whose request would not fit (the turn waits, Stop cancels both), and when the
|
|
21
|
+
model refuses a request as over its window (it compacts and sends the turn once more).
|
|
22
|
+
|
|
23
|
+
A compaction aims **below** the threshold, not at it: the history the model reads afterwards
|
|
24
|
+
lands at about `threshold − 0.2` of the window, less the fixed costs (system prompt, tools, the
|
|
25
|
+
room for the reply, the summary's slot), so one summary buys several turns instead of being asked
|
|
26
|
+
for again after the next. The selection is made in the model's own tokens (the character
|
|
27
|
+
heuristic's undercount is calibrated from the last reply, as the context gauge does) and leaves
|
|
28
|
+
room for the turn being prepared, so a long paste is made room for. A `selector` of your own is
|
|
29
|
+
called with the messages alone and is left as it is. Each chat compacts on its own: a summary
|
|
30
|
+
being written for one does not hold up another. A Restore cancels a summary still being written
|
|
31
|
+
for that chat (with `listen: false`, which hears no window event, the summary is refused when it
|
|
32
|
+
comes back instead), and Stop cancels it in every case.
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
the
|
|
29
|
-
|
|
30
|
-
puts back the summary as a **notice** (`compaction: true` — centred, no avatar, no actions;
|
|
31
|
-
sent to the model under a preamble saying what it is) followed by the kept turns verbatim.
|
|
34
|
+
A compaction **writes a checkpoint, it deletes nothing**. The summary goes on the first turn it
|
|
35
|
+
keeps — `contextSummary: { summary, count, createdAt }`, saved with the conversation — and the
|
|
36
|
+
history the model receives starts there, the summary first. Every message stays on screen and
|
|
37
|
+
in storage; the bubble draws a separator above that turn ("12 messages summarised for the
|
|
38
|
+
model", Show summary, Restore), and Restore sends the original messages to the model again.
|
|
32
39
|
|
|
33
40
|
```ts
|
|
34
|
-
await compaction.compact();
|
|
35
|
-
compaction.abort();
|
|
36
|
-
compaction.running;
|
|
37
|
-
compaction.dispose();
|
|
41
|
+
await compaction.compact('main-chat'); // on demand; returns the outcome, never throws
|
|
42
|
+
compaction.abort(); // the summary in flight; the conversation is untouched
|
|
43
|
+
compaction.running; // true meanwhile
|
|
44
|
+
compaction.dispose(); // remove the listeners and the compactor
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
**The summary** is asked of the current model through the config's transport: streamed, with a
|
|
48
|
+
`maxTokens` that keeps the answer inside the window and `_meta: { compaction: true }`, so a
|
|
49
|
+
backend can route it to a cheaper model. The default prompt asks for fixed sections in the
|
|
50
|
+
conversation's language; a second compaction updates the summary rather than summarising a
|
|
51
|
+
summary; the first request is kept word for word. A summary cut off at the length limit, ending
|
|
52
|
+
in an error, or no shorter than what it replaces is refused and the conversation stays as it
|
|
53
|
+
was. With **no** window on the model (and no `contextWindow` option) nothing says what fits, so
|
|
54
|
+
the default skips with `'no-window'`.
|
|
55
|
+
|
|
56
|
+
**Options** — `auto` (default `true`; `false` is **manual only**: only a person's request compacts —
|
|
57
|
+
Compact now, Retry, `compact()`, `aparte-compact` — and neither the after-turn, the before-send nor
|
|
58
|
+
the overflow attempt is made, so a model's overflow error stands as it did before the plugin; so
|
|
59
|
+
does `listen: false`), `threshold` (default `0.8`), `contextWindow` (the window when the model declares none),
|
|
60
|
+
`selector` (your own `(messages) => { keep, drop }`; `createCompactionSelector` builds the
|
|
61
|
+
budget-aware one), `prompt`, `keyResolver`, `summarize` (replace the model call: your endpoint, a
|
|
62
|
+
cheaper model), `resolveTarget` (a transcript in a store: `{ getMessages, updateMessage }`),
|
|
63
|
+
`scopeToTargetId` (one setup per chat on a multi-chat page), `keepWithoutWindow`, `listen`. The
|
|
64
|
+
config comes last, like every `setup*`: `setupCompaction(options, config)`.
|
|
47
65
|
|
|
48
|
-
**Events**, on `window`, each naming the chat
|
|
49
|
-
(`{ summary, kept, dropped }`,
|
|
50
|
-
`running`, `streaming
|
|
66
|
+
**Events**, on `window`, each naming the chat and the message the checkpoint sits on:
|
|
67
|
+
`aparte-compact-start`, `aparte-compact-done` (`{ summary, kept, dropped, count }`, `{ skipped:
|
|
68
|
+
true, reason }` — `empty`, `nothing-to-drop`, `running`, `streaming`, `no-window` — or `{ restored:
|
|
69
|
+
true }`), `aparte-compact-error` (`{ error }`).
|
|
51
70
|
|
|
52
71
|
**The budget**, exported for a host that wants the numbers: `computeHistoryBudget`,
|
|
53
|
-
`splitHistoryBudget`, `estimateTokens`, `estimateTokensJson`, `DEFAULT_COMPACTION_CONFIG`;
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
`splitHistoryBudget`, `estimateTokens`, `estimateTokensJson`, `DEFAULT_COMPACTION_CONFIG`; and
|
|
73
|
+
`transcriptForSummary` / `messageText` / `DEFAULT_COMPACTION_PROMPT` for a `summarize` of your
|
|
74
|
+
own that wants the same transcript.
|
|
56
75
|
|
|
57
76
|
`@aparte/core` is the only **peer dependency**. No element, no DOM at import: the same entry
|
|
58
77
|
serves the browser and Node.
|
package/dist/budget.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* reports usage and lets the caller decide — so it moved here, next to the one thing
|
|
17
17
|
* that does: the compaction this plugin performs. Zero deps, a char-count heuristic.
|
|
18
18
|
*/
|
|
19
|
+
import { estimateTokens } from '@aparte/core';
|
|
19
20
|
export interface CompactionConfig {
|
|
20
21
|
/** Total context window of the active model, in tokens. */
|
|
21
22
|
contextWindow: number;
|
|
@@ -35,8 +36,10 @@ export interface CompactionConfig {
|
|
|
35
36
|
*/
|
|
36
37
|
summaryRatio: number;
|
|
37
38
|
/**
|
|
38
|
-
* Ceiling on that reservation, in tokens. It sizes the WINDOW
|
|
39
|
-
* the summary
|
|
39
|
+
* Ceiling on that reservation, in tokens. It sizes the WINDOW: the verbatim turns
|
|
40
|
+
* leave this much room for the summary. The summary request's own `maxTokens` is a
|
|
41
|
+
* looser ceiling that only keeps the answer inside the window (see `setupCompaction`),
|
|
42
|
+
* and a summary no shorter than the turns it replaces is refused.
|
|
40
43
|
*/
|
|
41
44
|
summaryMaxTokens: number;
|
|
42
45
|
}
|
|
@@ -59,9 +62,9 @@ export interface BudgetResult {
|
|
|
59
62
|
* The history budget, split: the room held back for the running summary, and the
|
|
60
63
|
* verbatim window that is what remains.
|
|
61
64
|
*
|
|
62
|
-
* `summary` is a RESERVATION, read
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
+
* `summary` is a RESERVATION, read to shrink `window`. The summary request carries a
|
|
66
|
+
* looser `maxTokens` of its own (it keeps the answer inside the window), and a summary
|
|
67
|
+
* that is cut off at that limit, or no shorter than the turns it replaces, is refused.
|
|
65
68
|
*/
|
|
66
69
|
export interface SplitBudget {
|
|
67
70
|
summary: number;
|
|
@@ -69,11 +72,13 @@ export interface SplitBudget {
|
|
|
69
72
|
}
|
|
70
73
|
export declare const DEFAULT_COMPACTION_CONFIG: CompactionConfig;
|
|
71
74
|
/**
|
|
72
|
-
* Token heuristic
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
+
* Token heuristic — core's, re-exported under the name this module always had.
|
|
76
|
+
*
|
|
77
|
+
* It lives in `@aparte/core` (`estimateTokens`) because the context gauge and the count
|
|
78
|
+
* the client records on each reply read the SAME estimate: a plugin that kept a private copy
|
|
79
|
+
* would decide on figures the page does not show.
|
|
75
80
|
*/
|
|
76
|
-
export
|
|
81
|
+
export { estimateTokens };
|
|
77
82
|
/**
|
|
78
83
|
* Estimate tokens for a JSON-serializable structure (e.g. tools array).
|
|
79
84
|
*/
|
package/dist/budget.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAI9C,MAAM,WAAW,gBAAgB;IAC7B,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oCAAoC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,eAAe,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAClB;AAID,eAAO,MAAM,yBAAyB,EAAE,gBAYvC,CAAC;AAIF;;;;;;GAMG;AACH,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAOvD;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IACxC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACtC,GAAG,YAAY,CA2Bf;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAC9B,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,gBAA4C,GAClD,WAAW,CAGb"}
|
package/dist/compaction.d.ts
CHANGED
|
@@ -2,103 +2,132 @@
|
|
|
2
2
|
* compaction.ts — `setupCompaction`: the controller that answers `aparte-compact`.
|
|
3
3
|
*
|
|
4
4
|
* One compaction is: resolve the chat, select what to summarise (the budget-aware
|
|
5
|
-
* selector over the
|
|
6
|
-
* transport — or a summariser of the host's — then
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* selector over the model's view of the conversation by default), summarise it through
|
|
6
|
+
* the config's transport — or a summariser of the host's — then write the summary as a
|
|
7
|
+
* CHECKPOINT on the first message that stays verbatim. Nothing is deleted: the messages
|
|
8
|
+
* above the checkpoint stay on the screen and in storage, and the history the client
|
|
9
|
+
* builds resumes from the checkpoint, so the model reads the summary in their place.
|
|
10
|
+
* Every step that can fail reports on `window` (`aparte-compact-start` / `-done` /
|
|
11
|
+
* `-error`, each naming the chat) AND in the returned outcome, so a host that called
|
|
12
|
+
* `compact()` itself never has to listen for the answer to its own call.
|
|
11
13
|
*
|
|
12
14
|
* What this file is careful about, each learned on the client's version of it:
|
|
13
|
-
* - one compaction at a time per
|
|
14
|
-
* reported skipped, not started (two paid summaries, both
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
15
|
+
* - one compaction at a time per CHAT — a second request for a chat whose summary is
|
|
16
|
+
* being written is reported skipped, not started (two paid summaries, both written),
|
|
17
|
+
* while another chat on the same setup compacts on its own;
|
|
18
|
+
* - a transcript with a turn in flight is left alone — except the turn being PREPARED
|
|
19
|
+
* (a send or retry the client is about to make), which the caller names in
|
|
20
|
+
* `exclude`: it is neither summarised nor counted as in flight;
|
|
21
|
+
* - the summarisation has its OWN abort controller, reached by `abort()`, by an
|
|
22
|
+
* `aparte-abort` addressed to the chat and by the abort signal of the request that
|
|
23
|
+
* asked for it (the client's Stop), so a summary the user cancelled stops there, and a
|
|
24
|
+
* Restore of the same chat cancels it too;
|
|
25
|
+
* - and a summary nobody cancelled is refused too when the message it was to sit on
|
|
26
|
+
* is no longer on the chat's path, or the checkpoint it updates was taken off: the
|
|
27
|
+
* conversation was switched (or reset) or restored underneath, and a success the host
|
|
28
|
+
* dropped would be reported as one;
|
|
29
|
+
* - the checkpoint is the model's own update of the one before it: a second compaction
|
|
30
|
+
* sends "Summary so far:" and only the messages since, not the whole history again;
|
|
25
31
|
* - a chat is addressed by id through the same rule the gauge and the client use, and
|
|
26
32
|
* `scopeToTargetId` makes a setup answer one chat on a page that has several.
|
|
27
33
|
*/
|
|
28
|
-
import { type AparteConfig, type AparteMessage, type AparteChatRequest } from '@aparte/core';
|
|
34
|
+
import { type AparteConfig, type AparteMessage, type AparteChatRequest, type AparteCompactor, type AparteCompactorOutcome, type AparteCompactorRequest } from '@aparte/core';
|
|
29
35
|
import { type CompactionSelection } from './selector.js';
|
|
30
36
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
37
|
+
* What a chat must expose to be compacted: read the active path and update one message
|
|
38
|
+
* of it. `<aparte-chat-viewport>` does; an `<aparte-chat>` shell hands over its viewport.
|
|
39
|
+
* A host whose transcript lives elsewhere (a framework store) gives `resolveTarget` an
|
|
40
|
+
* object of its own.
|
|
35
41
|
*/
|
|
36
42
|
export interface CompactionTarget {
|
|
37
43
|
getMessages(): AparteMessage[];
|
|
38
44
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* afterwards. A target that ignores the argument keeps working — it just leaks the
|
|
44
|
-
* URLs of what it drops, which is what it did before this existed.
|
|
45
|
+
* Merge `updates` into one message of the path — the one write a compaction makes: the
|
|
46
|
+
* checkpoint (`contextSummary`) on the first message that stays. A target without it
|
|
47
|
+
* cannot hold a checkpoint, and the compaction fails with an error saying so rather
|
|
48
|
+
* than falling back to rewriting the transcript.
|
|
45
49
|
*/
|
|
46
|
-
|
|
50
|
+
updateMessage?(messageId: string, updates: Partial<AparteMessage>): void;
|
|
51
|
+
/**
|
|
52
|
+
* Not called any more. A compaction used to empty the transcript and put the kept turns
|
|
53
|
+
* back; it now writes one checkpoint and removes nothing. Still accepted, so a target
|
|
54
|
+
* written for the earlier contract keeps type-checking.
|
|
55
|
+
*/
|
|
56
|
+
clearAll?(options?: {
|
|
47
57
|
revokeAttachments?: boolean;
|
|
48
58
|
}): void;
|
|
49
|
-
|
|
59
|
+
/** Not called any more; see `clearAll`. */
|
|
60
|
+
appendMessage?(message: AparteMessage): void;
|
|
50
61
|
}
|
|
51
62
|
/** Which messages are summarised (`drop`) and which stay verbatim (`keep`). Pure — no model call. */
|
|
52
63
|
export type CompactionMessageSelector = (messages: AparteMessage[]) => CompactionSelection<AparteMessage>;
|
|
53
64
|
/** The key (or record of settings) for a provider — the same shape `AparteClientOptions.keyResolver` takes. */
|
|
54
65
|
export type CompactionKeyResolver = (providerId: string) => string | Record<string, string> | undefined | null | Promise<string | Record<string, string> | undefined | null>;
|
|
55
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Replaces the model call: given the summarisation request, return the summary text.
|
|
68
|
+
* The request asks for a stream (`stream: true`) and carries `maxTokens`, a ceiling on the
|
|
69
|
+
* answer that keeps it inside the window — a summariser that talks to a model should pass both on.
|
|
70
|
+
*/
|
|
56
71
|
export type CompactionSummarizer = (request: AparteChatRequest, signal: AbortSignal) => Promise<string>;
|
|
57
72
|
/** Why a compaction did nothing. */
|
|
58
73
|
export type CompactionSkipReason =
|
|
59
74
|
/** The transcript is empty. */
|
|
60
75
|
'empty'
|
|
61
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* The selector kept everything — within budget, or nothing older than what is kept —
|
|
78
|
+
* or no user message could carry the checkpoint (the first message kept must be the
|
|
79
|
+
* user's). Also what an automatic request is told when the back-off, a Restore or the
|
|
80
|
+
* hysteresis says not to try now, and what an automatic or overflow request is told
|
|
81
|
+
* when `auto: false` (or `listen: false`) keeps the setup manual.
|
|
82
|
+
*/
|
|
62
83
|
| 'nothing-to-drop'
|
|
63
|
-
/** A compaction is already running for this
|
|
84
|
+
/** A compaction is already running for this chat. */
|
|
64
85
|
| 'running'
|
|
65
|
-
/** A turn is in flight in the transcript; compacting under it would
|
|
66
|
-
| 'streaming'
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
kept: number;
|
|
77
|
-
dropped: number;
|
|
78
|
-
targetId?: string;
|
|
79
|
-
} | {
|
|
80
|
-
ok: false;
|
|
81
|
-
error: string;
|
|
82
|
-
targetId?: string;
|
|
83
|
-
};
|
|
86
|
+
/** A turn is in flight in the transcript; compacting under it would summarise a half-written reply. */
|
|
87
|
+
| 'streaming'
|
|
88
|
+
/**
|
|
89
|
+
* The current model declares no `contextWindow` and nothing of the host's sizes the
|
|
90
|
+
* request (no `selector`, no `summarize`, no `keepWithoutWindow`). Without a window the
|
|
91
|
+
* default would send everything but the last few messages in one request, which a small
|
|
92
|
+
* model refuses as over its context. Declare the window, or pass one of the three.
|
|
93
|
+
*/
|
|
94
|
+
| 'no-window';
|
|
95
|
+
/** How a compaction ended — core's outcome, the same type the compactor seam returns. */
|
|
96
|
+
export type CompactionOutcome = AparteCompactorOutcome;
|
|
84
97
|
export interface CompactionSetupOptions {
|
|
85
98
|
/**
|
|
86
|
-
* Which messages are summarised
|
|
99
|
+
* Which messages are summarised and which stay verbatim, over what the model reads:
|
|
100
|
+
* the messages from the latest checkpoint on, minus the turn being prepared.
|
|
87
101
|
*
|
|
88
102
|
* Default: `createCompactionSelector` over the current model — its `contextWindow`,
|
|
89
103
|
* the resolved system prompt and the registered tools set the budget, and the
|
|
90
|
-
* newest turns that fit
|
|
91
|
-
*
|
|
92
|
-
*
|
|
104
|
+
* newest turns that fit stay. Each compaction sizes it: to land below the `threshold`
|
|
105
|
+
* (about `threshold − 0.2` of the window once the fixed costs are paid, so one
|
|
106
|
+
* compaction buys several turns), with room for the turn being prepared, and in the
|
|
107
|
+
* model's own tokens (the heuristic's undercount, as the context gauge calibrates it).
|
|
108
|
+
* A `selector` of your own is called with the messages alone and keeps what it keeps.
|
|
109
|
+
* Whatever it chooses, the cut is moved so the
|
|
110
|
+
* first message kept is a user message (the checkpoint sits on it). When the current
|
|
111
|
+
* model declares no window there is no budget to walk, and the compaction skips
|
|
112
|
+
* (`'no-window'`) unless the host sizes the request itself: a `selector`, a
|
|
113
|
+
* `summarize` or `keepWithoutWindow`.
|
|
93
114
|
*/
|
|
94
115
|
selector?: CompactionMessageSelector;
|
|
95
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* How many of the newest messages stay when the model declares no window; everything
|
|
118
|
+
* older goes to the summariser in ONE request, so the number is the host's bet that it
|
|
119
|
+
* fits the model. Passing it is the opt-in: left out, a model without a window makes the
|
|
120
|
+
* default compaction skip with `'no-window'`, since nothing says the request would fit.
|
|
121
|
+
* Without a `selector` of your own, `4` (the last two exchanges) is what it takes when
|
|
122
|
+
* you opt in through `summarize` alone. Ignored when the model declares a window.
|
|
123
|
+
*/
|
|
96
124
|
keepWithoutWindow?: number;
|
|
97
125
|
/**
|
|
98
126
|
* The summariser's instruction — carried by the ask itself, not by a `system`
|
|
99
127
|
* message, so a provider that imposes its own system prompt cannot drop it.
|
|
100
128
|
* English by default — it is addressed to the model, not to the user — asking for
|
|
101
|
-
* the decisions, the open tasks and
|
|
129
|
+
* fixed sections (the goal, the decisions, the open tasks, the facts and tool results
|
|
130
|
+
* still needed, the next step) in the language of the conversation. Replace it
|
|
102
131
|
* to steer the summary (a language, a domain, a length).
|
|
103
132
|
*/
|
|
104
133
|
prompt?: string;
|
|
@@ -110,51 +139,118 @@ export interface CompactionSetupOptions {
|
|
|
110
139
|
keyResolver?: CompactionKeyResolver;
|
|
111
140
|
/**
|
|
112
141
|
* Replace the model call entirely. The request carries the transcript of the
|
|
113
|
-
*
|
|
114
|
-
* and the ask,
|
|
142
|
+
* turns being summarised, then a final `user` message holding the summariser's
|
|
143
|
+
* instruction and the ask, `stream: true`, `maxTokens` (a ceiling on the answer that
|
|
144
|
+
* keeps it inside the window) and `_meta: { compaction: true }`; return the summary
|
|
145
|
+
* text. When an earlier checkpoint exists the request opens with "Summary so far:" and
|
|
146
|
+
* that summary, and asks for an updated one. Your own call
|
|
147
|
+
* is not trimmed to the window — the sizing is yours — but a summary that is no
|
|
148
|
+
* shorter than the turns it replaces is still refused. For a
|
|
115
149
|
* host with an endpoint of its own, or a cheaper model for summaries. With one, no
|
|
116
150
|
* provider needs to be configured at all.
|
|
117
151
|
*/
|
|
118
152
|
summarize?: CompactionSummarizer;
|
|
119
153
|
/**
|
|
120
|
-
* Resolve the chat to compact. Default: the element
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
154
|
+
* Resolve the chat to compact. Default: the chat element the request carries (the
|
|
155
|
+
* compactor seam passes it, so a chat inside a shadow root is found), else the element
|
|
156
|
+
* whose id is the `targetId` (or, unnamed, the first `<aparte-chat>` /
|
|
157
|
+
* `<aparte-chat-viewport>` / `[data-aparte-chat]` on the page), taking its viewport
|
|
158
|
+
* when the element itself cannot render. Return your own object for a transcript that
|
|
159
|
+
* lives in a store.
|
|
124
160
|
*/
|
|
125
161
|
resolveTarget?: (targetId?: string) => CompactionTarget | null;
|
|
126
162
|
/**
|
|
127
163
|
* Answer only the `aparte-compact` and `aparte-abort` events that name this chat
|
|
128
|
-
* id, and compact it when `compact()` is called without one.
|
|
129
|
-
* several chats and one setup per chat
|
|
164
|
+
* id, and compact it when `compact()` is called without one. A request for another chat
|
|
165
|
+
* is refused. For a page with several chats and one setup per chat — and note that the
|
|
166
|
+
* config has ONE compactor: the setup made last on a config is the one the client and
|
|
167
|
+
* the gauge call.
|
|
130
168
|
*/
|
|
131
169
|
scopeToTargetId?: string;
|
|
132
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Listen for `aparte-compact` and `aparte-abort` on `window`. Default `true`; `false` for
|
|
172
|
+
* a host that calls `compact()` itself.
|
|
173
|
+
*
|
|
174
|
+
* `false` also keeps compaction manual, whatever `auto` says (see `auto`): a setup that
|
|
175
|
+
* hears nothing decides nothing. A Stop still cancels a summary: the client hands the
|
|
176
|
+
* turn's abort signal to the compactor, so it does not depend on this listener.
|
|
177
|
+
*/
|
|
133
178
|
listen?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Compact by itself. Default `true`: once set up, the plugin summarises a chat when a
|
|
181
|
+
* turn ends with the context at `threshold` of its window, before a send whose request
|
|
182
|
+
* would not fit — the client asks first (`beforeSend`) — and when the model refuses a
|
|
183
|
+
* request as over its window (the client asks again, with `reason: 'overflow'`, and
|
|
184
|
+
* sends the turn once more).
|
|
185
|
+
*
|
|
186
|
+
* `false` means MANUAL ONLY: only a person's request compacts (the gauge's Compact now,
|
|
187
|
+
* a separator's Retry, `compact()`, `aparte-compact`). None of the three automatic
|
|
188
|
+
* attempts is made, the overflow included — `compact()` answers an `'auto'` or
|
|
189
|
+
* `'overflow'` request with a skip, so the model's error stands exactly as it did before
|
|
190
|
+
* the plugin. Forced `false` with `listen: false` (a setup that hears nothing decides
|
|
191
|
+
* nothing).
|
|
192
|
+
*
|
|
193
|
+
* It acts only when a summariser can run: a provider registered for the current model,
|
|
194
|
+
* or a `summarize` of the host's. It never acts on a Restore, a conversation switch or a
|
|
195
|
+
* load, and never again after two failures in a row until something succeeds or a person
|
|
196
|
+
* asks (see the back-off in the docs).
|
|
197
|
+
*/
|
|
198
|
+
auto?: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* The share of the window, from 0 to 1, at which a turn's end triggers a compaction.
|
|
201
|
+
* Default `0.8`. The gauge's own levels are independent of it.
|
|
202
|
+
*
|
|
203
|
+
* A compaction aims lower than the threshold: with the default selector it leaves the
|
|
204
|
+
* history the model reads at about `threshold − 0.2` of the window, less the fixed costs
|
|
205
|
+
* (the system prompt, the tools, the room for the reply, the summary's slot), so one
|
|
206
|
+
* compaction buys several turns and the next is not asked for after this one. That holds
|
|
207
|
+
* for a person's request and an overflow retry too; a `selector` of your own is left
|
|
208
|
+
* alone. If the context is still at the threshold afterwards (a long recent tail the
|
|
209
|
+
* selector must keep), the after-turn trigger waits until a turn ends below it; a request
|
|
210
|
+
* that would not fit is still compacted before it is sent, and an overflow still retried.
|
|
211
|
+
*/
|
|
212
|
+
threshold?: number;
|
|
213
|
+
/**
|
|
214
|
+
* The window to measure against when the current model declares none — a number, or a
|
|
215
|
+
* getter read at each use (`() => settings.contextWindow`). A local server has no
|
|
216
|
+
* window the page can read, and without one nothing is automatic and the default
|
|
217
|
+
* selector skips. The model's own `contextWindow` wins when it declares one. For one
|
|
218
|
+
* more release, the `window` a gauge sends with its `aparte-compact` request is read
|
|
219
|
+
* as a last resort.
|
|
220
|
+
*/
|
|
221
|
+
contextWindow?: number | (() => number | undefined);
|
|
134
222
|
}
|
|
135
|
-
|
|
223
|
+
/**
|
|
224
|
+
* The controller `setupCompaction` returns — and the compactor it registers on the
|
|
225
|
+
* config (`AparteConfig.setCompactor`), so the gauge and the client call the same object
|
|
226
|
+
* a host does.
|
|
227
|
+
*/
|
|
228
|
+
export interface CompactionController extends Omit<AparteCompactor, 'compact' | 'beforeSend' | 'preview'>, Required<Pick<AparteCompactor, 'beforeSend' | 'preview'>> {
|
|
136
229
|
/**
|
|
137
|
-
* Compact one chat
|
|
138
|
-
*
|
|
230
|
+
* Compact one chat. Takes the compactor seam's request (the chat element, its id, the
|
|
231
|
+
* ids of a turn being prepared, why) or, as it always did, a chat id, or nothing:
|
|
232
|
+
* the scoped chat, else the first on the page. A string or nothing is a person's
|
|
233
|
+
* request (`reason: 'manual'`). Never rejects: the outcome says what happened, and the
|
|
139
234
|
* same information goes out as `aparte-compact-done` / `aparte-compact-error`.
|
|
140
235
|
*/
|
|
141
|
-
compact(
|
|
142
|
-
/** Abort the
|
|
236
|
+
compact(request?: string | AparteCompactorRequest): Promise<CompactionOutcome>;
|
|
237
|
+
/** Abort the summarisations in flight, if any. The transcripts are left untouched. */
|
|
143
238
|
abort(): void;
|
|
144
|
-
/** `true` while a summarisation is in flight. */
|
|
239
|
+
/** `true` while a summarisation is in flight, for any chat of this setup. */
|
|
145
240
|
readonly running: boolean;
|
|
146
|
-
/** Remove the listeners and forget this setup; an in-flight summarisation is aborted. */
|
|
241
|
+
/** Remove the listeners and forget this setup; an in-flight summarisation is aborted and the compactor unregistered. */
|
|
147
242
|
dispose(): void;
|
|
148
243
|
}
|
|
149
244
|
/**
|
|
150
|
-
* Install compaction on a config. Returns the controller
|
|
151
|
-
* config replaces the previous setup (its
|
|
245
|
+
* Install compaction on a config. Returns the controller, which is also registered as the
|
|
246
|
+
* config's compactor; the same call on the same config replaces the previous setup (its
|
|
247
|
+
* listeners are removed first).
|
|
152
248
|
*
|
|
153
249
|
* ```ts
|
|
154
250
|
* import { setupCompaction } from '@aparte/plugin-compaction';
|
|
155
251
|
*
|
|
156
252
|
* const compaction = setupCompaction(); // the global config, the current model's budget
|
|
157
|
-
* // `<aparte-context
|
|
253
|
+
* // `<aparte-context>` now offers "Compact now", and the separator's Retry works;
|
|
158
254
|
* // or ask yourself:
|
|
159
255
|
* await compaction.compact();
|
|
160
256
|
* ```
|
package/dist/compaction.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../src/compaction.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../src/compaction.ts"],"names":[],"mappings":"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"}
|
package/dist/cut.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cut.ts — where a checkpoint sits, and the one thing a summary must not lose.
|
|
3
|
+
*
|
|
4
|
+
* Two small pure pieces `compact()` leans on, kept out of it so each can be read (and
|
|
5
|
+
* tested) without a model call in sight.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The index of the first message that stays verbatim, moved so it is a USER message.
|
|
9
|
+
*
|
|
10
|
+
* A checkpoint is read by the model as "the summary, then this message and everything
|
|
11
|
+
* after it", and every provider wants the first turn after a preamble to be the user's:
|
|
12
|
+
* a history that resumes on an assistant reply is refused by some templates and
|
|
13
|
+
* misread by the rest. So the boundary the selector chose (`boundary`, the count of
|
|
14
|
+
* leading messages it dropped) is moved toward the OLDER end until it lands on a user
|
|
15
|
+
* message, which keeps more verbatim and summarises less than the selector asked for.
|
|
16
|
+
* When that would summarise nothing at all, it moves toward the NEWER end instead, which
|
|
17
|
+
* summarises a little more. A kept message has to exist to carry the checkpoint, so the
|
|
18
|
+
* boundary never goes past the last message.
|
|
19
|
+
*
|
|
20
|
+
* `null` when no user message qualifies — there is nothing a checkpoint could sit on.
|
|
21
|
+
*/
|
|
22
|
+
export declare function cutAtUser(messages: ReadonlyArray<{
|
|
23
|
+
role: string;
|
|
24
|
+
}>, boundary: number): number | null;
|
|
25
|
+
/**
|
|
26
|
+
* The line that introduces the conversation's first request inside a stored summary.
|
|
27
|
+
* English and fixed, because the summary is addressed to the model and because an
|
|
28
|
+
* update of the summary has to find the block again (see {@link splitFirstRequest}).
|
|
29
|
+
*/
|
|
30
|
+
export declare const FIRST_REQUEST_LINE = "The conversation's first request, verbatim:";
|
|
31
|
+
/** The most of the first request a summary carries. */
|
|
32
|
+
export declare const FIRST_REQUEST_MAX = 1500;
|
|
33
|
+
/**
|
|
34
|
+
* The first request, appended to the model's summary under {@link FIRST_REQUEST_LINE}.
|
|
35
|
+
*
|
|
36
|
+
* It is not written by the model: what the user first asked is the one piece of a
|
|
37
|
+
* conversation every later turn is judged against, and a summariser paraphrases it away
|
|
38
|
+
* as readily as anything else. Clipped, so a pasted document does not become the summary.
|
|
39
|
+
*/
|
|
40
|
+
export declare function firstRequestBlock(text: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* A stored summary taken apart: what the model wrote, and the first-request block that
|
|
43
|
+
* was appended to it (empty when there is none).
|
|
44
|
+
*
|
|
45
|
+
* An update of a summary hands the model the first part only and puts the second back
|
|
46
|
+
* unchanged afterwards. Handing it the whole thing would have it copy the block into
|
|
47
|
+
* its own answer, and the block would be there twice.
|
|
48
|
+
*/
|
|
49
|
+
export declare function splitFirstRequest(summary: string): {
|
|
50
|
+
base: string;
|
|
51
|
+
block: string;
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=cut.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cut.d.ts","sourceRoot":"","sources":["../src/cut.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpG;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gDAAgD,CAAC;AAEhF,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAItD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAGlF"}
|