agentfootprint 9.13.0 → 9.14.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/AGENTS.md +3 -2
- package/dist/core/agent/AgentBuilder.js +14 -5
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.d.ts +5 -2
- package/dist/esm/core/agent/AgentBuilder.js +14 -5
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/memory/define.d.ts +1 -1
- package/dist/esm/memory/define.js +113 -14
- package/dist/esm/memory/define.js.map +1 -1
- package/dist/esm/memory/define.types.d.ts +69 -4
- package/dist/esm/memory/define.types.js.map +1 -1
- package/dist/esm/memory/pipeline/default.d.ts +22 -0
- package/dist/esm/memory/pipeline/default.js +19 -3
- package/dist/esm/memory/pipeline/default.js.map +1 -1
- package/dist/esm/memory/stages/summarize.d.ts +193 -54
- package/dist/esm/memory/stages/summarize.js +370 -34
- package/dist/esm/memory/stages/summarize.js.map +1 -1
- package/dist/esm/memory/strategies.d.ts +7 -1
- package/dist/esm/memory/strategies.js +25 -11
- package/dist/esm/memory/strategies.js.map +1 -1
- package/dist/memory/define.js +113 -14
- package/dist/memory/define.js.map +1 -1
- package/dist/memory/define.types.js.map +1 -1
- package/dist/memory/pipeline/default.js +19 -3
- package/dist/memory/pipeline/default.js.map +1 -1
- package/dist/memory/stages/summarize.js +374 -35
- package/dist/memory/stages/summarize.js.map +1 -1
- package/dist/memory/strategies.js +25 -11
- package/dist/memory/strategies.js.map +1 -1
- package/dist/types/core/agent/AgentBuilder.d.ts +5 -2
- package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
- package/dist/types/memory/define.d.ts +1 -1
- package/dist/types/memory/define.d.ts.map +1 -1
- package/dist/types/memory/define.types.d.ts +69 -4
- package/dist/types/memory/define.types.d.ts.map +1 -1
- package/dist/types/memory/pipeline/default.d.ts +22 -0
- package/dist/types/memory/pipeline/default.d.ts.map +1 -1
- package/dist/types/memory/stages/summarize.d.ts +193 -54
- package/dist/types/memory/stages/summarize.d.ts.map +1 -1
- package/dist/types/memory/strategies.d.ts +7 -1
- package/dist/types/memory/strategies.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -145,16 +145,64 @@ export interface BudgetStrategy {
|
|
|
145
145
|
readonly maxEntries?: number;
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
-
* Summarize — when
|
|
149
|
-
*
|
|
150
|
-
* to "long conversations blow context.
|
|
148
|
+
* Summarize — when recall grows long, one LLM call compresses the older
|
|
149
|
+
* entries into a summary that is STORED, while the most recent `recent`
|
|
150
|
+
* entries stay raw. The standard answer to "long conversations blow context".
|
|
151
|
+
*
|
|
152
|
+
* Wired in 9.14.0. Through 9.13.0 this type existed, `llm` was required, and
|
|
153
|
+
* the pipeline `defineMemory` built loaded the last `recent` entries and
|
|
154
|
+
* stopped — a strategy that named a summarizer and never called it. What runs
|
|
155
|
+
* now:
|
|
156
|
+
*
|
|
157
|
+
* 1. load up to `size` entries (default 20);
|
|
158
|
+
* 2. keep the last `recent` verbatim, seam rounded outward to a whole turn;
|
|
159
|
+
* 3. fold everything older into ONE summary entry — one call, over the span;
|
|
160
|
+
* 4. WRITE that entry back to the same store under
|
|
161
|
+
* `msg-summary-{fromTurn}-{toTurn}`, so the next turn reads it instead of
|
|
162
|
+
* paying for it again;
|
|
163
|
+
* 5. recall is `[summary, ...recent verbatim]`.
|
|
164
|
+
*
|
|
165
|
+
* The folded originals are NOT deleted. They stay in the store and are
|
|
166
|
+
* excluded from recall by the summary's coverage metadata — delete the
|
|
167
|
+
* summary entry and recall is verbatim again.
|
|
151
168
|
*/
|
|
152
169
|
export interface SummarizeStrategy {
|
|
153
170
|
readonly kind: typeof MEMORY_STRATEGIES.SUMMARIZE;
|
|
154
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Keep this many most-recent entries uncompressed. The seam is rounded
|
|
173
|
+
* outward to a whole turn, so a question is never summarized while its
|
|
174
|
+
* answer stays raw.
|
|
175
|
+
*/
|
|
155
176
|
readonly recent: number;
|
|
177
|
+
/**
|
|
178
|
+
* How much history to load per turn — the pool the fold is taken from.
|
|
179
|
+
* Default 20 (`loadRecent`'s own default). Must be greater than `recent`:
|
|
180
|
+
* a window no larger than the verbatim tail never has anything older to
|
|
181
|
+
* compress, which is `window` with a summarizer bolted on.
|
|
182
|
+
*/
|
|
183
|
+
readonly size?: number;
|
|
156
184
|
/** LLM that does the compression — recommend a cheap model (haiku). */
|
|
157
185
|
readonly llm: LLMProvider;
|
|
186
|
+
/**
|
|
187
|
+
* Which model `llm` is called with. REQUIRED (9.14.0), and there is no
|
|
188
|
+
* fall back to the agent's own model — the same law `.compaction()` has
|
|
189
|
+
* enforced since 8.14.0. A default here had no correct case: the same
|
|
190
|
+
* provider family quietly bills your MAIN model for compression, and a
|
|
191
|
+
* different vendor is sent a model id it has never heard of.
|
|
192
|
+
*
|
|
193
|
+
* `Agent.memory()` additionally refuses a summarizer that is the agent's own
|
|
194
|
+
* provider INSTANCE at the agent's own model: those two calls look identical
|
|
195
|
+
* and are not — the agent's runs through reliability, decorators and the
|
|
196
|
+
* cache, and this one runs through none of them.
|
|
197
|
+
*/
|
|
198
|
+
readonly model: string;
|
|
199
|
+
/**
|
|
200
|
+
* Override the instruction the summarizer is given (the transcript
|
|
201
|
+
* delimiters and the authored label on the stored summary stay the
|
|
202
|
+
* library's). Use it for domain summaries: "preserve every refund-related
|
|
203
|
+
* number".
|
|
204
|
+
*/
|
|
205
|
+
readonly systemPrompt?: string;
|
|
158
206
|
}
|
|
159
207
|
/**
|
|
160
208
|
* Top-K — embed the user's query, retrieve top-K by cosine similarity.
|
|
@@ -333,6 +381,23 @@ export interface MemoryDefinition<T = unknown> {
|
|
|
333
381
|
readonly store?: MemoryStore;
|
|
334
382
|
/** When `read` runs. Default `TURN_START`. */
|
|
335
383
|
readonly timing: MemoryTiming;
|
|
384
|
+
/**
|
|
385
|
+
* What this memory's OWN LLM work bills to (9.14.0) — present only for a
|
|
386
|
+
* strategy that calls a model on the host's behalf, which today is
|
|
387
|
+
* `SUMMARIZE`.
|
|
388
|
+
*
|
|
389
|
+
* Carried in the open for one reason: `Agent.memory()` is where the agent's
|
|
390
|
+
* own provider and model are known, and it refuses a summarizer that is that
|
|
391
|
+
* exact instance at that exact model (the 8.14.0 rule `.compaction()` and
|
|
392
|
+
* `.window()` already enforce). `defineMemory` cannot make that check — it
|
|
393
|
+
* has never heard of an agent — so it declares the billing and the builder
|
|
394
|
+
* checks it. Same field name and same shape as `WindowStrategy.billing`, so
|
|
395
|
+
* one refusal serves both.
|
|
396
|
+
*/
|
|
397
|
+
readonly billing?: {
|
|
398
|
+
readonly provider: LLMProvider;
|
|
399
|
+
readonly model: string;
|
|
400
|
+
};
|
|
336
401
|
/** Reserved for a future release — patterns to redact before write. */
|
|
337
402
|
readonly redact?: MemoryRedactionPolicy;
|
|
338
403
|
/** Snapshot projection — only meaningful when `type === CAUSAL`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define.types.js","sourceRoot":"","sources":["../../../src/memory/define.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAQH,uEAAuE;AAEvE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ,EAAE,UAAU;IACpB,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,QAAQ;CACR,CAAC;AAGX;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,WAAW;IACtB,KAAK,EAAE,MAAM;IACb,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACR,CAAC;AAGX;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,eAAe,EAAE,iBAAiB;IAClC,UAAU,EAAE,YAAY;CAChB,CAAC;AAGX;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS,EAAE,WAAW;IACtB,OAAO,EAAE,SAAS;IAClB,SAAS,EAAE,WAAW;IACtB,IAAI,EAAE,MAAM;CACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"define.types.js","sourceRoot":"","sources":["../../../src/memory/define.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAQH,uEAAuE;AAEvE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ,EAAE,UAAU;IACpB,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,QAAQ;CACR,CAAC;AAGX;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,WAAW;IACtB,KAAK,EAAE,MAAM;IACb,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACR,CAAC;AAGX;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,eAAe,EAAE,iBAAiB;IAClC,UAAU,EAAE,YAAY;CAChB,CAAC;AAGX;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS,EAAE,WAAW;IACtB,OAAO,EAAE,SAAS;IAClB,SAAS,EAAE,WAAW;IACtB,IAAI,EAAE,MAAM;CACJ,CAAC;AAubX,uEAAuE;AAEvE,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAQ,MAAM,CAAC,MAAM,CAAC,YAAY,CAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAQ,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAQ,MAAM,CAAC,MAAM,CAAC,aAAa,CAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAQ,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,uEAAuE;AAEvE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,kBAA2B,CAAC;AAEvE,MAAM,UAAU,kBAAkB,CAAC,EAAU;IAC3C,OAAO,GAAG,2BAA2B,GAAG,EAAE,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO,GAAG,CAAC,UAAU,CAAC,2BAA2B,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,oBAA6B,CAAC;AAE3E,MAAM,UAAU,oBAAoB,CAAC,EAAU;IAC7C,OAAO,GAAG,6BAA6B,GAAG,EAAE,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,OAAO,GAAG,CAAC,UAAU,CAAC,6BAA6B,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FilterByDecayConfig } from '../stages/filterByDecay.js';
|
|
2
|
+
import { type SummarizeConfig } from '../stages/summarize.js';
|
|
2
3
|
import type { MemoryStore } from '../store/index.js';
|
|
3
4
|
import type { MemoryPipeline } from './types.js';
|
|
4
5
|
export interface DefaultPipelineConfig {
|
|
@@ -50,6 +51,27 @@ export interface DefaultPipelineConfig {
|
|
|
50
51
|
* pipeline level so it composes with `loadCount` / the budget knobs.
|
|
51
52
|
*/
|
|
52
53
|
readonly decay?: FilterByDecayConfig;
|
|
54
|
+
/**
|
|
55
|
+
* Compress what falls out of the verbatim tail (9.14.0). When present, a
|
|
56
|
+
* `Summarize` stage is composed directly after the load: the oldest loaded
|
|
57
|
+
* entries become ONE summary entry written back to this same store, and
|
|
58
|
+
* recall becomes `[summary, ...recent verbatim]`.
|
|
59
|
+
*
|
|
60
|
+
* The stage's `store` and `ttlMs` are filled from THIS config — the summary
|
|
61
|
+
* lands in the same namespace as the turns it stands for, and under the same
|
|
62
|
+
* retention window, because a summary that outlived the messages it
|
|
63
|
+
* compressed is the leak `writeTtlMs` exists to prevent.
|
|
64
|
+
*
|
|
65
|
+
* Absent — the historical behaviour — means no summarize stage is compiled
|
|
66
|
+
* at all, not a summarize stage that never fires. What is not in the chart
|
|
67
|
+
* cannot cost anything or appear in the narrative.
|
|
68
|
+
*
|
|
69
|
+
* `defineMemory({ strategy: { kind: MEMORY_STRATEGIES.SUMMARIZE, recent,
|
|
70
|
+
* llm, model } })` is the door most consumers use; this is the same thing,
|
|
71
|
+
* spelled at the pipeline level so it composes with `loadCount` and the
|
|
72
|
+
* budget knobs.
|
|
73
|
+
*/
|
|
74
|
+
readonly summarize?: Omit<SummarizeConfig, 'store' | 'ttlMs'>;
|
|
53
75
|
/**
|
|
54
76
|
* Override for the formatter's header text. Omit to use the default
|
|
55
77
|
* "Relevant context from prior conversations..." phrasing.
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* Composes Layer 2-3 stages into two flowchart subflows that the wire
|
|
5
5
|
* layer mounts inside the agent's main flowchart:
|
|
6
6
|
*
|
|
7
|
-
* READ : LoadRecent → PickByBudget →
|
|
7
|
+
* READ : LoadRecent → [Summarize] → [FilterByDecay] → PickByBudget →
|
|
8
|
+
* FormatDefault
|
|
8
9
|
* WRITE : WriteMessages
|
|
9
10
|
*
|
|
11
|
+
* The two bracketed stages are compiled only when their config is present
|
|
12
|
+
* (`summarize`, `decay`) — an absent option means an absent stage, never a
|
|
13
|
+
* stage that runs and does nothing.
|
|
14
|
+
*
|
|
10
15
|
* Why this particular composition?
|
|
11
16
|
* - Load-then-pick-then-format matches the cognitive sequence:
|
|
12
17
|
* retrieve candidates, choose what fits, present it.
|
|
@@ -50,6 +55,7 @@
|
|
|
50
55
|
import { flowChart } from 'footprintjs';
|
|
51
56
|
import { loadRecent } from '../stages/loadRecent.js';
|
|
52
57
|
import { filterByDecay } from '../stages/filterByDecay.js';
|
|
58
|
+
import { summarize } from '../stages/summarize.js';
|
|
53
59
|
import { pickByBudget } from '../stages/pickByBudget.js';
|
|
54
60
|
import { formatDefault } from '../stages/formatDefault.js';
|
|
55
61
|
import { writeMessages } from '../stages/writeMessages.js';
|
|
@@ -79,14 +85,24 @@ export function defaultPipeline(config) {
|
|
|
79
85
|
...(config.writeTier && { tier: config.writeTier }),
|
|
80
86
|
...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }),
|
|
81
87
|
};
|
|
82
|
-
// Compose: LoadRecent → [FilterByDecay] → [PickDecider →
|
|
83
|
-
// skip-no-budget | pick] → Format
|
|
88
|
+
// Compose: LoadRecent → [Summarize] → [FilterByDecay] → [PickDecider →
|
|
89
|
+
// skip-empty | skip-no-budget | pick] → Format
|
|
84
90
|
// pickByBudget is a builder-extension — it appends a decider + 3
|
|
85
91
|
// branches to the pipeline so "why did / didn't we inject memory?" is
|
|
86
92
|
// answerable via FlowRecorder.onDecision evidence, not just emit events.
|
|
87
93
|
let readBuilder = flowChart('LoadRecent', loadRecent(loadConfig), 'load-recent', {
|
|
88
94
|
description: 'Read N most-recent entries from storage into scope.loaded',
|
|
89
95
|
});
|
|
96
|
+
if (config.summarize !== undefined) {
|
|
97
|
+
// Directly after the load, and before every stage that trims: decay and
|
|
98
|
+
// the budget picker both decide against what recall CONTAINS, and what
|
|
99
|
+
// recall contains is not settled until the fold has happened.
|
|
100
|
+
readBuilder = readBuilder.addFunction('Summarize', summarize({
|
|
101
|
+
...config.summarize,
|
|
102
|
+
store: config.store,
|
|
103
|
+
...(config.writeTtlMs !== undefined && { ttlMs: config.writeTtlMs }),
|
|
104
|
+
}), 'summarize', 'Fold entries older than the verbatim tail into one stored summary entry');
|
|
105
|
+
}
|
|
90
106
|
if (config.decay !== undefined) {
|
|
91
107
|
// Before the picker: what has faded is not a budget question, and the
|
|
92
108
|
// budget is better spent on entries that are still worth something.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default.js","sourceRoot":"","sources":["../../../../src/memory/pipeline/default.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"default.js","sourceRoot":"","sources":["../../../../src/memory/pipeline/default.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,87 +1,226 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* summarize — stage that
|
|
3
|
-
* summary entry, preserving the most-recent N verbatim.
|
|
2
|
+
* summarize — the stage that makes the SUMMARIZE strategy summarize (9.14.0).
|
|
4
3
|
*
|
|
5
|
-
* Reads from scope: `loaded`
|
|
6
|
-
* Writes to scope: `loaded` (
|
|
7
|
-
*
|
|
4
|
+
* Reads from scope: `loaded`, `identity`
|
|
5
|
+
* Writes to scope: `loaded` (older entries replaced by ONE summary entry)
|
|
6
|
+
* Writes to store: the summary entry, under a deterministic id
|
|
7
|
+
* Emits: agentfootprint.memory.strategy_applied (every visit that
|
|
8
|
+
* changes recall or decides not to), carrying the reason,
|
|
9
|
+
* the model, and the token usage the call reported.
|
|
10
|
+
*
|
|
11
|
+
* NOT `cost.tick`, and that is a limit worth knowing: the
|
|
12
|
+
* USD channel needs a `pricingTable` and the run's
|
|
13
|
+
* cumulative counters, both of which live on the AGENT's
|
|
14
|
+
* scope, and a memory pipeline is a subflow with neither.
|
|
15
|
+
* Emitting a tick with `estimatedUsd: 0` would be a
|
|
16
|
+
* cheaper-looking lie than saying nothing, so the tokens
|
|
17
|
+
* ride the memory event instead and the fold is never
|
|
18
|
+
* silent.
|
|
8
19
|
*
|
|
9
20
|
* Where this fits in the pipeline:
|
|
10
21
|
*
|
|
11
|
-
* loadRecent → summarize → pickByBudget → formatDefault
|
|
22
|
+
* loadRecent → summarize → [filterByDecay] → pickByBudget → formatDefault
|
|
23
|
+
*
|
|
24
|
+
* ## What it does, and what that costs
|
|
25
|
+
*
|
|
26
|
+
* Recall arrives from `loadRecent` oldest-first. This stage keeps the last
|
|
27
|
+
* `preserveRecent` entries VERBATIM and folds everything older into a single
|
|
28
|
+
* summary entry — **one LLM call over the span**, not one per entry and not
|
|
29
|
+
* one per recall. The summary is then WRITTEN BACK to the store under the id
|
|
30
|
+
* `msg-summary-{fromTurn}-{toTurn}`, so the next turn LOADS it instead of
|
|
31
|
+
* paying for it again. That write-back is the whole cost model: a span is
|
|
32
|
+
* summarized once in the life of a conversation, by whichever turn first saw
|
|
33
|
+
* it fall out of the verbatim tail.
|
|
12
34
|
*
|
|
13
|
-
*
|
|
14
|
-
* which is a synthetic summary). The picker then selects from this
|
|
15
|
-
* reduced set using the standard budget logic.
|
|
35
|
+
* ## The originals are never deleted
|
|
16
36
|
*
|
|
17
|
-
*
|
|
37
|
+
* A summary is a CLAIM ABOUT the conversation, not the conversation. The
|
|
38
|
+
* entries it covers stay in the store byte-identical; they are excluded from
|
|
39
|
+
* recall by the summary's own coverage metadata ({@link SummaryCoverage}) and
|
|
40
|
+
* by nothing else. Delete the summary entry and the next recall is verbatim
|
|
41
|
+
* again. This is the same law `.compaction()` follows in the live window —
|
|
42
|
+
* the fold edits what is SENT, never what was said.
|
|
18
43
|
*
|
|
19
|
-
*
|
|
20
|
-
* MUST be the same each time for the same input. This requires:
|
|
44
|
+
* ## Two summarizer shapes
|
|
21
45
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
46
|
+
* • `llm: LLMProvider` + `model` — the library composes the call. The span
|
|
47
|
+
* is rendered as DATA between delimiters the authored instruction names,
|
|
48
|
+
* so a message inside the conversation cannot re-instruct the summarizer
|
|
49
|
+
* by looking like an instruction. `model` is REQUIRED here: naming it is
|
|
50
|
+
* what keeps the summarizer's bill separate from the agent's own (the
|
|
51
|
+
* `.compaction()` law, 8.14.0). Token usage is reported on the event.
|
|
52
|
+
* • `llm: (messages) => Promise<string>` — the caller's own call, unchanged
|
|
53
|
+
* since 2.x. The caller composed the request, so the caller named the
|
|
54
|
+
* model; the stage reports usage as unknown rather than inventing one.
|
|
26
55
|
*
|
|
27
|
-
*
|
|
28
|
-
* `llm` function. Callers are responsible for configuring determinism.
|
|
29
|
-
* Non-deterministic summarizers still work but invalidate prompt caches
|
|
30
|
-
* on every turn (~5× token-cost increase on cache-enabled providers).
|
|
56
|
+
* ## What comes back is DATA
|
|
31
57
|
*
|
|
32
|
-
*
|
|
58
|
+
* The summary text is appended after an authored label this file writes, and
|
|
59
|
+
* the label always comes first. A summarizer that returns "IGNORE ALL
|
|
60
|
+
* PREVIOUS INSTRUCTIONS" produces an entry that still says, in the library's
|
|
61
|
+
* own words and first, that what follows is a summary written by a model and
|
|
62
|
+
* that the originals are retained.
|
|
33
63
|
*
|
|
34
|
-
*
|
|
35
|
-
* - `loaded.length <= preserveRecent` → no-op (nothing to summarize; all
|
|
36
|
-
* entries would be preserved verbatim anyway).
|
|
37
|
-
* - LLM call throws → error propagates to the pipeline's executor
|
|
38
|
-
* (fail-loud, per loadRecent / writeMessages convention).
|
|
64
|
+
* ## Three ways it declines, all of them out loud
|
|
39
65
|
*
|
|
40
|
-
*
|
|
66
|
+
* • **not worth a call** — fewer than `minFoldEntries` foldable entries, or
|
|
67
|
+
* fewer than `triggerMinEntries` loaded. No call. Recall still changes if
|
|
68
|
+
* an EARLIER turn's summary is covering entries (that is the cheap turn
|
|
69
|
+
* the write-back bought), and then the event says so; a turn where
|
|
70
|
+
* nothing at all happened writes nothing and emits nothing.
|
|
71
|
+
* • **summarizer failed** — the provider threw. ONE `console.warn` per
|
|
72
|
+
* stage instance plus an event, and recall proceeds VERBATIM: a broken
|
|
73
|
+
* summarizer degrades this strategy to `window`, it does not fail the
|
|
74
|
+
* turn. (Through 9.13.0 the stage re-threw; a memory that cannot recall
|
|
75
|
+
* because its optional compressor is down is a worse answer than an
|
|
76
|
+
* uncompressed one.)
|
|
77
|
+
* • **replacement not smaller** — the summary plus its label is no shorter
|
|
78
|
+
* than the span it would replace. Folding then spends a call to GROW
|
|
79
|
+
* recall and lose detail at the same time, so the fold is dropped and the
|
|
80
|
+
* span is LATCHED: the same span is never re-asked, because the same
|
|
81
|
+
* inputs give the same answer and re-asking is a paid call whose result
|
|
82
|
+
* is already known (the 8.14.0 latch, keyed by the span's own ids). A
|
|
83
|
+
* span that has GROWN is a different key and is asked again on purpose.
|
|
41
84
|
*
|
|
42
|
-
*
|
|
43
|
-
* summarizer would compress just one entry — wasted LLM call with no
|
|
44
|
-
* real compression. Defaults (trigger 20, preserve 5) summarize 15
|
|
45
|
-
* entries when firing, which is a meaningful compression ratio.
|
|
85
|
+
* ## Determinism contract
|
|
46
86
|
*
|
|
47
|
-
*
|
|
87
|
+
* For prompt caching to stay stable, the same span should produce the same
|
|
88
|
+
* summary. Configure `temperature: 0` (and a seed where the provider has
|
|
89
|
+
* one) on the summarizer you pass. The stage cannot enforce it — but note
|
|
90
|
+
* that write-back makes it matter far less than it did: a span is summarized
|
|
91
|
+
* once and then read back from the store.
|
|
48
92
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* - `source.turn`: the LATEST turn that was summarized (for sorting)
|
|
53
|
-
* - `tier`: 'cold' (marks it as "condensed, may not be recent")
|
|
54
|
-
* - `source.identity`: carried over from the summarized range's first entry
|
|
93
|
+
* @see ../define.ts the `SUMMARIZE` strategy arm that wires this
|
|
94
|
+
* @see ../../core/agent/window/strategies/summarizeOldest.ts the same laws
|
|
95
|
+
* applied to the LIVE window instead of to recall
|
|
55
96
|
*/
|
|
56
97
|
import type { TypedScope } from 'footprintjs';
|
|
57
|
-
import type {
|
|
98
|
+
import type { MemoryEntry } from '../entry/index.js';
|
|
99
|
+
import type { LLMMessage as Message, LLMProvider } from '../../adapters/types.js';
|
|
100
|
+
import type { MemoryStore } from '../store/index.js';
|
|
58
101
|
import type { MemoryState } from './types.js';
|
|
102
|
+
/**
|
|
103
|
+
* The 2.x summarizer shape: the caller makes the call and returns the text.
|
|
104
|
+
* Kept because a hand-composed pipeline that already owns its provider does
|
|
105
|
+
* not need the library to own it too.
|
|
106
|
+
*/
|
|
107
|
+
export type SummarizeCallback = (messages: readonly Message[]) => Promise<string>;
|
|
59
108
|
export interface SummarizeConfig {
|
|
60
109
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
110
|
+
* Who writes the summary — a provider (preferred; pair with {@link model})
|
|
111
|
+
* or the legacy callback that makes its own call.
|
|
112
|
+
*
|
|
113
|
+
* Recommend a cheap model. The point of naming a summarizer at all is that
|
|
114
|
+
* compression is not worth your main model's price.
|
|
65
115
|
*/
|
|
66
|
-
readonly llm:
|
|
116
|
+
readonly llm: LLMProvider | SummarizeCallback;
|
|
67
117
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
118
|
+
* The model id the summarizer is called with. REQUIRED when `llm` is a
|
|
119
|
+
* provider, refused as meaningless when `llm` is a callback (the callback
|
|
120
|
+
* composed its own request and named its own model there).
|
|
121
|
+
*
|
|
122
|
+
* There is no `?? agentModel` fallback, by the same reasoning
|
|
123
|
+
* `.compaction()` uses: the default had no correct case — same family and
|
|
124
|
+
* it quietly bills your main model for compression, different vendor and it
|
|
125
|
+
* sends a model id nobody has heard of.
|
|
126
|
+
*/
|
|
127
|
+
readonly model?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Where the summary is written back to. Omit and the fold is computed for
|
|
130
|
+
* THIS recall only and thrown away — correct, and paid for again every
|
|
131
|
+
* turn. `defaultPipeline` passes its own store, so the factory path always
|
|
132
|
+
* writes back.
|
|
133
|
+
*/
|
|
134
|
+
readonly store?: MemoryStore;
|
|
135
|
+
/**
|
|
136
|
+
* Minimum `loaded.length` before a fold is considered. Below this, no-op —
|
|
137
|
+
* the conversation is short enough to keep whole. Default 20.
|
|
70
138
|
*/
|
|
71
139
|
readonly triggerMinEntries?: number;
|
|
72
140
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
141
|
+
* How many most-recent entries stay VERBATIM. The older ones become the
|
|
142
|
+
* summary. Default 5 — recent turns keep their exact phrasing so the agent
|
|
143
|
+
* can quote them.
|
|
144
|
+
*
|
|
145
|
+
* The seam is rounded OUTWARD to a whole turn: if the cut would land in the
|
|
146
|
+
* middle of turn 7, turn 7 goes to the verbatim side entirely. A question
|
|
147
|
+
* summarized while its answer stayed raw reads like an answer to nothing.
|
|
77
148
|
*/
|
|
78
149
|
readonly preserveRecent?: number;
|
|
79
150
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
151
|
+
* Do not spend a call to fold fewer than this many entries. Default 2 — one
|
|
152
|
+
* entry "compressed" into a summary plus a label is a paid call that makes
|
|
153
|
+
* recall longer. `defineMemory` raises this to the size of the verbatim
|
|
154
|
+
* tail, which is the cost policy: never fold less than you keep.
|
|
155
|
+
*/
|
|
156
|
+
readonly minFoldEntries?: number;
|
|
157
|
+
/**
|
|
158
|
+
* Override the authored instruction the summarizer is given. Domain
|
|
159
|
+
* summaries want this ("preserve every refund-related number"). Only the
|
|
160
|
+
* INSTRUCTION is yours — the transcript delimiters and the label written
|
|
161
|
+
* onto the summary entry are the library's and stay put.
|
|
84
162
|
*/
|
|
85
163
|
readonly systemPrompt?: string;
|
|
164
|
+
/**
|
|
165
|
+
* TTL in milliseconds applied to the summary entry, counted from the SPAN's
|
|
166
|
+
* clock (the newest entry it stands for), not from the fold.
|
|
167
|
+
* `defaultPipeline` passes its `writeTtlMs` here, so a compliance retention
|
|
168
|
+
* window expires the summary WITH the turns it compressed — a summary that
|
|
169
|
+
* outlived them is exactly the leak the retention window exists to prevent.
|
|
170
|
+
*/
|
|
171
|
+
readonly ttlMs?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Tier for the summary entry. Default `'cold'` — condensed, not recent.
|
|
174
|
+
*
|
|
175
|
+
* Worth pairing with the load's `tiers` filter if you set one: a filter that
|
|
176
|
+
* excludes this tier hides the summary from every later recall, and a
|
|
177
|
+
* summary that is never loaded is a span that is folded (and billed) again
|
|
178
|
+
* every turn. `defineMemory` sets no tier filter, so the factory path is
|
|
179
|
+
* safe by construction.
|
|
180
|
+
*/
|
|
181
|
+
readonly summaryTier?: 'hot' | 'warm' | 'cold';
|
|
182
|
+
/** `strategyId` on the emitted event. Default `'memory-summarize'`. */
|
|
183
|
+
readonly strategyId?: string;
|
|
86
184
|
}
|
|
185
|
+
/** Id prefix for every entry this stage writes. */
|
|
186
|
+
export declare const SUMMARY_ID_PREFIX = "msg-summary-";
|
|
187
|
+
/** Metadata key carrying {@link SummaryCoverage} on a summary entry. */
|
|
188
|
+
export declare const SUMMARY_COVERAGE_KEY = "summarizes";
|
|
189
|
+
/**
|
|
190
|
+
* What a summary entry stands for — the only thing that excludes an original
|
|
191
|
+
* from recall.
|
|
192
|
+
*
|
|
193
|
+
* `coveredIds` is the operative field and it is exact: recall drops an entry
|
|
194
|
+
* because a loaded summary NAMES it, never because a turn number falls inside
|
|
195
|
+
* a range. A turn that was half-folded (possible only for hand-composed
|
|
196
|
+
* configs; the stage rounds the seam outward) keeps the half nobody claimed.
|
|
197
|
+
*/
|
|
198
|
+
export interface SummaryCoverage {
|
|
199
|
+
/** Earliest turn represented. */
|
|
200
|
+
readonly fromTurn: number;
|
|
201
|
+
/** Latest turn represented. Also the entry's `source.turn`. */
|
|
202
|
+
readonly toTurn: number;
|
|
203
|
+
/** How many entries were folded. */
|
|
204
|
+
readonly entryCount: number;
|
|
205
|
+
/** The exact entries this summary stands for. */
|
|
206
|
+
readonly coveredIds: readonly string[];
|
|
207
|
+
/** Which model wrote it. Absent for the callback form — the caller knows. */
|
|
208
|
+
readonly model?: string;
|
|
209
|
+
/** When the fold happened (unix ms). */
|
|
210
|
+
readonly summarizedAtMs: number;
|
|
211
|
+
}
|
|
212
|
+
/** The id a fold over `[fromTurn, toTurn]` always gets. Deterministic. */
|
|
213
|
+
export declare function summaryEntryId(fromTurn: number, toTurn: number): string;
|
|
214
|
+
/**
|
|
215
|
+
* The coverage a summary entry carries, or `undefined` for an ordinary entry.
|
|
216
|
+
*
|
|
217
|
+
* Read through the METADATA, not the id: an id is a name and metadata is the
|
|
218
|
+
* claim. An entry named like a summary that carries no coverage stands for
|
|
219
|
+
* nothing and is treated as an ordinary entry.
|
|
220
|
+
*/
|
|
221
|
+
export declare function summaryCoverage(entry: MemoryEntry<unknown>): SummaryCoverage | undefined;
|
|
222
|
+
/** True when this entry is a summary written by this stage. */
|
|
223
|
+
export declare function isSummaryEntry(entry: MemoryEntry<unknown>): boolean;
|
|
224
|
+
/** Opening of the authored label. Stable — readers and tests match on it. */
|
|
225
|
+
export declare const SUMMARY_FRAME_PREFIX = "[summary of earlier turns";
|
|
87
226
|
export declare function summarize(config: SummarizeConfig): (scope: TypedScope<MemoryState>) => Promise<void>;
|