@animalabs/context-manager 0.3.0 → 0.5.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.
Files changed (161) hide show
  1. package/dist/src/adaptive/chunker.d.ts +70 -0
  2. package/dist/src/adaptive/chunker.d.ts.map +1 -0
  3. package/dist/src/adaptive/chunker.js +233 -0
  4. package/dist/src/adaptive/chunker.js.map +1 -0
  5. package/dist/src/adaptive/folding-strategy.d.ts +118 -0
  6. package/dist/src/adaptive/folding-strategy.d.ts.map +1 -0
  7. package/dist/src/adaptive/folding-strategy.js +12 -0
  8. package/dist/src/adaptive/folding-strategy.js.map +1 -0
  9. package/dist/src/adaptive/index.d.ts +17 -0
  10. package/dist/src/adaptive/index.d.ts.map +1 -0
  11. package/dist/src/adaptive/index.js +23 -0
  12. package/dist/src/adaptive/index.js.map +1 -0
  13. package/dist/src/adaptive/picker.d.ts +103 -0
  14. package/dist/src/adaptive/picker.d.ts.map +1 -0
  15. package/dist/src/adaptive/picker.js +357 -0
  16. package/dist/src/adaptive/picker.js.map +1 -0
  17. package/dist/src/adaptive/render.d.ts +39 -0
  18. package/dist/src/adaptive/render.d.ts.map +1 -0
  19. package/dist/src/adaptive/render.js +104 -0
  20. package/dist/src/adaptive/render.js.map +1 -0
  21. package/dist/src/adaptive/strategies/flat-profile.d.ts +19 -0
  22. package/dist/src/adaptive/strategies/flat-profile.d.ts.map +1 -0
  23. package/dist/src/adaptive/strategies/flat-profile.js +86 -0
  24. package/dist/src/adaptive/strategies/flat-profile.js.map +1 -0
  25. package/dist/src/adaptive/strategies/oldest-first.d.ts +19 -0
  26. package/dist/src/adaptive/strategies/oldest-first.d.ts.map +1 -0
  27. package/dist/src/adaptive/strategies/oldest-first.js +39 -0
  28. package/dist/src/adaptive/strategies/oldest-first.js.map +1 -0
  29. package/dist/src/context-manager.d.ts +74 -2
  30. package/dist/src/context-manager.d.ts.map +1 -1
  31. package/dist/src/context-manager.js +150 -10
  32. package/dist/src/context-manager.js.map +1 -1
  33. package/dist/src/index.d.ts +1 -1
  34. package/dist/src/index.d.ts.map +1 -1
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/message-store.d.ts +36 -1
  37. package/dist/src/message-store.d.ts.map +1 -1
  38. package/dist/src/message-store.js +134 -8
  39. package/dist/src/message-store.js.map +1 -1
  40. package/dist/src/strategies/autobiographical.d.ts +422 -5
  41. package/dist/src/strategies/autobiographical.d.ts.map +1 -1
  42. package/dist/src/strategies/autobiographical.js +2248 -98
  43. package/dist/src/strategies/autobiographical.js.map +1 -1
  44. package/dist/src/strategies/index.d.ts +1 -1
  45. package/dist/src/strategies/index.d.ts.map +1 -1
  46. package/dist/src/strategies/index.js.map +1 -1
  47. package/dist/src/types/index.d.ts +2 -2
  48. package/dist/src/types/index.d.ts.map +1 -1
  49. package/dist/src/types/index.js +1 -1
  50. package/dist/src/types/index.js.map +1 -1
  51. package/dist/src/types/message.d.ts +38 -0
  52. package/dist/src/types/message.d.ts.map +1 -1
  53. package/dist/src/types/strategy.d.ts +302 -10
  54. package/dist/src/types/strategy.d.ts.map +1 -1
  55. package/dist/src/types/strategy.js +28 -0
  56. package/dist/src/types/strategy.js.map +1 -1
  57. package/dist/test/adaptive/branching.test.d.ts +20 -0
  58. package/dist/test/adaptive/branching.test.d.ts.map +1 -0
  59. package/dist/test/adaptive/branching.test.js +176 -0
  60. package/dist/test/adaptive/branching.test.js.map +1 -0
  61. package/dist/test/adaptive/chunker.test.d.ts +2 -0
  62. package/dist/test/adaptive/chunker.test.d.ts.map +1 -0
  63. package/dist/test/adaptive/chunker.test.js +120 -0
  64. package/dist/test/adaptive/chunker.test.js.map +1 -0
  65. package/dist/test/adaptive/deep-levels.test.d.ts +10 -0
  66. package/dist/test/adaptive/deep-levels.test.d.ts.map +1 -0
  67. package/dist/test/adaptive/deep-levels.test.js +167 -0
  68. package/dist/test/adaptive/deep-levels.test.js.map +1 -0
  69. package/dist/test/adaptive/doc-plus-chat.test.d.ts +10 -0
  70. package/dist/test/adaptive/doc-plus-chat.test.d.ts.map +1 -0
  71. package/dist/test/adaptive/doc-plus-chat.test.js +194 -0
  72. package/dist/test/adaptive/doc-plus-chat.test.js.map +1 -0
  73. package/dist/test/adaptive/hard-fail.test.d.ts +9 -0
  74. package/dist/test/adaptive/hard-fail.test.d.ts.map +1 -0
  75. package/dist/test/adaptive/hard-fail.test.js +154 -0
  76. package/dist/test/adaptive/hard-fail.test.js.map +1 -0
  77. package/dist/test/adaptive/harness.d.ts +109 -0
  78. package/dist/test/adaptive/harness.d.ts.map +1 -0
  79. package/dist/test/adaptive/harness.js +263 -0
  80. package/dist/test/adaptive/harness.js.map +1 -0
  81. package/dist/test/adaptive/ingestion.test.d.ts +13 -0
  82. package/dist/test/adaptive/ingestion.test.d.ts.map +1 -0
  83. package/dist/test/adaptive/ingestion.test.js +306 -0
  84. package/dist/test/adaptive/ingestion.test.js.map +1 -0
  85. package/dist/test/adaptive/integration.test.d.ts +12 -0
  86. package/dist/test/adaptive/integration.test.d.ts.map +1 -0
  87. package/dist/test/adaptive/integration.test.js +247 -0
  88. package/dist/test/adaptive/integration.test.js.map +1 -0
  89. package/dist/test/adaptive/long-chronicle.test.d.ts +13 -0
  90. package/dist/test/adaptive/long-chronicle.test.d.ts.map +1 -0
  91. package/dist/test/adaptive/long-chronicle.test.js +186 -0
  92. package/dist/test/adaptive/long-chronicle.test.js.map +1 -0
  93. package/dist/test/adaptive/persistence.test.d.ts +11 -0
  94. package/dist/test/adaptive/persistence.test.d.ts.map +1 -0
  95. package/dist/test/adaptive/persistence.test.js +200 -0
  96. package/dist/test/adaptive/persistence.test.js.map +1 -0
  97. package/dist/test/adaptive/picker.test.d.ts +2 -0
  98. package/dist/test/adaptive/picker.test.d.ts.map +1 -0
  99. package/dist/test/adaptive/picker.test.js +333 -0
  100. package/dist/test/adaptive/picker.test.js.map +1 -0
  101. package/dist/test/adaptive/render.test.d.ts +2 -0
  102. package/dist/test/adaptive/render.test.d.ts.map +1 -0
  103. package/dist/test/adaptive/render.test.js +109 -0
  104. package/dist/test/adaptive/render.test.js.map +1 -0
  105. package/dist/test/adaptive/shard-immutability.test.d.ts +14 -0
  106. package/dist/test/adaptive/shard-immutability.test.d.ts.map +1 -0
  107. package/dist/test/adaptive/shard-immutability.test.js +184 -0
  108. package/dist/test/adaptive/shard-immutability.test.js.map +1 -0
  109. package/dist/test/adaptive/strategy-integration.test.d.ts +9 -0
  110. package/dist/test/adaptive/strategy-integration.test.d.ts.map +1 -0
  111. package/dist/test/adaptive/strategy-integration.test.js +293 -0
  112. package/dist/test/adaptive/strategy-integration.test.js.map +1 -0
  113. package/dist/test/message-store-sequence.test.d.ts +25 -0
  114. package/dist/test/message-store-sequence.test.d.ts.map +1 -0
  115. package/dist/test/message-store-sequence.test.js +157 -0
  116. package/dist/test/message-store-sequence.test.js.map +1 -0
  117. package/dist/test/non-blocking-compile.test.d.ts +22 -0
  118. package/dist/test/non-blocking-compile.test.d.ts.map +1 -0
  119. package/dist/test/non-blocking-compile.test.js +114 -0
  120. package/dist/test/non-blocking-compile.test.js.map +1 -0
  121. package/dist/test/pins.test.d.ts +10 -0
  122. package/dist/test/pins.test.d.ts.map +1 -0
  123. package/dist/test/pins.test.js +164 -0
  124. package/dist/test/pins.test.js.map +1 -0
  125. package/dist/test/recall-positioning.test.d.ts +16 -0
  126. package/dist/test/recall-positioning.test.d.ts.map +1 -0
  127. package/dist/test/recall-positioning.test.js +181 -0
  128. package/dist/test/recall-positioning.test.js.map +1 -0
  129. package/dist/test/search.test.d.ts +10 -0
  130. package/dist/test/search.test.d.ts.map +1 -0
  131. package/dist/test/search.test.js +141 -0
  132. package/dist/test/search.test.js.map +1 -0
  133. package/dist/test/speculation-cap.test.d.ts +17 -0
  134. package/dist/test/speculation-cap.test.d.ts.map +1 -0
  135. package/dist/test/speculation-cap.test.js +157 -0
  136. package/dist/test/speculation-cap.test.js.map +1 -0
  137. package/dist/test/strategy-persistence.test.d.ts +15 -0
  138. package/dist/test/strategy-persistence.test.d.ts.map +1 -0
  139. package/dist/test/strategy-persistence.test.js +244 -0
  140. package/dist/test/strategy-persistence.test.js.map +1 -0
  141. package/dist/test/tool-pruning.test.d.ts +10 -0
  142. package/dist/test/tool-pruning.test.d.ts.map +1 -0
  143. package/dist/test/tool-pruning.test.js +140 -0
  144. package/dist/test/tool-pruning.test.js.map +1 -0
  145. package/dist/tsconfig.tsbuildinfo +1 -1
  146. package/package.json +1 -1
  147. package/src/adaptive/chunker.ts +323 -0
  148. package/src/adaptive/folding-strategy.ts +129 -0
  149. package/src/adaptive/index.ts +51 -0
  150. package/src/adaptive/picker.ts +442 -0
  151. package/src/adaptive/render.ts +115 -0
  152. package/src/adaptive/strategies/flat-profile.ts +96 -0
  153. package/src/adaptive/strategies/oldest-first.ts +48 -0
  154. package/src/context-manager.ts +174 -10
  155. package/src/index.ts +1 -1
  156. package/src/message-store.ts +147 -8
  157. package/src/strategies/autobiographical.ts +2468 -166
  158. package/src/strategies/index.ts +1 -1
  159. package/src/types/index.ts +14 -1
  160. package/src/types/message.ts +43 -0
  161. package/src/types/strategy.ts +332 -10
@@ -1,2 +1,2 @@
1
1
  export { PassthroughStrategy } from './passthrough.js';
2
- export { AutobiographicalStrategy } from './autobiographical.js';
2
+ export { AutobiographicalStrategy, type AutobiographicalProgressSnapshot } from './autobiographical.js';
@@ -37,6 +37,19 @@ export type {
37
37
  PhaseType,
38
38
  KnowledgeConfig,
39
39
  ResettableStrategy,
40
+ ProtectedRange,
41
+ SearchQuery,
42
+ SearchResult,
43
+ SearchableStrategy,
44
+ PinnableStrategy,
45
+ RenderStats,
46
+ RenderStatsCapableStrategy,
40
47
  } from './strategy.js';
41
48
 
42
- export { DEFAULT_AUTOBIOGRAPHICAL_CONFIG, isResettableStrategy } from './strategy.js';
49
+ export {
50
+ DEFAULT_AUTOBIOGRAPHICAL_CONFIG,
51
+ isResettableStrategy,
52
+ isPinnableStrategy,
53
+ isSearchableStrategy,
54
+ isRenderStatsCapable,
55
+ } from './strategy.js';
@@ -47,6 +47,40 @@ export interface StoredMessage {
47
47
  timestamp: Date;
48
48
  /** IDs of messages that caused this one (from Chronicle causation) */
49
49
  causedBy?: MessageId[];
50
+
51
+ /**
52
+ * If this message is a shard of a larger logical message (chunked at
53
+ * ingestion because it exceeded `chunkThreshold`), this is the stable
54
+ * group id shared with sibling shards. Shards with the same id are
55
+ * concatenated into one API message at render time. Null/undefined
56
+ * for messages that fit in a single chunk. See
57
+ * `docs/adaptive-resolution-design.md` §3.6.
58
+ */
59
+ bodyGroupId?: string;
60
+
61
+ /**
62
+ * The shard's order within its bodyGroup, starting at 0. Only meaningful
63
+ * when `bodyGroupId` is set. Used to reassemble shards into byte-faithful
64
+ * order at render time.
65
+ */
66
+ shardIndex?: number;
67
+
68
+ /**
69
+ * Current display resolution for this chunk:
70
+ * - 0 = render raw content
71
+ * - k>0 = render the L_k summary that covers this chunk
72
+ *
73
+ * Set by the picker (or the agent in V2). Default 0. See
74
+ * `docs/adaptive-resolution-design.md` §3.3.
75
+ */
76
+ currentResolution?: number;
77
+
78
+ /**
79
+ * If true, the picker must not change `currentResolution` for this chunk.
80
+ * Set by `lockChunk()` (programmatic API) or, in V2, by the agent's
81
+ * `unfold` tool. Default false.
82
+ */
83
+ lockedByAgent?: boolean;
50
84
  }
51
85
 
52
86
  /**
@@ -85,6 +119,15 @@ export interface StoredMessageInternal {
85
119
  metadata?: MessageMetadata;
86
120
  timestamp: number; // Unix timestamp for storage
87
121
  causedBy?: MessageId[];
122
+
123
+ /** See StoredMessage.bodyGroupId */
124
+ bodyGroupId?: string;
125
+ /** See StoredMessage.shardIndex */
126
+ shardIndex?: number;
127
+ /** See StoredMessage.currentResolution */
128
+ currentResolution?: number;
129
+ /** See StoredMessage.lockedByAgent */
130
+ lockedByAgent?: boolean;
88
131
  }
89
132
 
90
133
  /**
@@ -1,4 +1,5 @@
1
- import type { Membrane } from '@animalabs/membrane';
1
+ import type { JsStore } from '@animalabs/chronicle';
2
+ import type { Membrane, ContentBlock } from '@animalabs/membrane';
2
3
  import type { StoredMessage, MessageId, Sequence } from './message.js';
3
4
  import type { ContextEntry, TokenBudget, PendingWork } from './context.js';
4
5
 
@@ -48,6 +49,22 @@ export interface StrategyContext {
48
49
  membrane?: Membrane;
49
50
  /** Current sequence number */
50
51
  currentSequence: Sequence;
52
+ /**
53
+ * Underlying Chronicle store. Strategies may register their own state slots
54
+ * (via `store.registerState`) for durable strategy state that needs to
55
+ * survive process restart and follow Chronicle branches.
56
+ *
57
+ * State IDs should be scoped under `namespace` to avoid collisions with
58
+ * other strategies or with the message/context-log states.
59
+ */
60
+ store: JsStore;
61
+ /**
62
+ * Namespace under which this strategy should scope its state IDs.
63
+ * Use as a prefix: e.g. `${namespace}/autobio:summaries`. Always defined;
64
+ * defaults to a stable per-manager value when no caller-supplied namespace
65
+ * exists, so strategies never need to handle the unscoped case.
66
+ */
67
+ namespace: string;
51
68
  }
52
69
 
53
70
  /**
@@ -108,6 +125,41 @@ export interface ContextStrategy {
108
125
  log: ContextLogView,
109
126
  budget: TokenBudget
110
127
  ): ContextEntry[];
128
+
129
+ /**
130
+ * Optional: at ingestion time, decide whether to split this incoming
131
+ * message into multiple shards (because it's too large to be a single
132
+ * fold unit). If returned, the framework stores each shard as a separate
133
+ * StoredMessage with the shared `bodyGroupId` and per-shard `shardIndex`,
134
+ * and the render path will reassemble them into one API message at
135
+ * compile time.
136
+ *
137
+ * Return null or undefined to skip chunking (message stored as one record).
138
+ *
139
+ * See `docs/adaptive-resolution-design.md` §3.6.
140
+ */
141
+ chunkIngressMessage?(
142
+ participant: string,
143
+ content: ContentBlock[]
144
+ ): IngressChunkResult | null;
145
+ }
146
+
147
+ /**
148
+ * Result of a strategy's ingestion-time chunking decision.
149
+ */
150
+ export interface IngressChunkResult {
151
+ /** Stable id shared by all shards of this message. */
152
+ bodyGroupId: string;
153
+ /**
154
+ * The shards in source order. Each becomes a separate StoredMessage.
155
+ * Concatenating the shards' text content must reproduce the original
156
+ * message body byte-for-byte.
157
+ */
158
+ shards: Array<{
159
+ content: ContentBlock[];
160
+ /** Order within the bodyGroup, starting at 0. */
161
+ shardIndex: number;
162
+ }>;
111
163
  }
112
164
 
113
165
  /**
@@ -128,6 +180,109 @@ export function isResettableStrategy(s: ContextStrategy): s is ResettableStrateg
128
180
  return 'resetHeadWindow' in s && typeof (s as ResettableStrategy).resetHeadWindow === 'function';
129
181
  }
130
182
 
183
+ /**
184
+ * Strategy that supports protected ranges (pins + documents).
185
+ * Pinned ranges are excluded from compression and render raw at their
186
+ * original chronological position. Implemented by AutobiographicalStrategy.
187
+ */
188
+ export interface PinnableStrategy extends ContextStrategy {
189
+ pinRange(firstMessageId: string, lastMessageId: string, opts?: { name?: string }): string;
190
+ markDocument(messageId: string, opts?: { name?: string }): string;
191
+ unpin(pinId: string): boolean;
192
+ listPins(): ReadonlyArray<ProtectedRange>;
193
+ }
194
+
195
+ /** Type guard for strategies that support pins / documents. */
196
+ export function isPinnableStrategy(s: ContextStrategy): s is PinnableStrategy {
197
+ return (
198
+ 'pinRange' in s &&
199
+ typeof (s as PinnableStrategy).pinRange === 'function' &&
200
+ 'unpin' in s &&
201
+ typeof (s as PinnableStrategy).unpin === 'function'
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Query for `searchSummaries`. At least one of `text` or `regex` should be
207
+ * provided to constrain results; otherwise all summaries pass.
208
+ */
209
+ export interface SearchQuery {
210
+ /** Case-insensitive substring match against summary content. */
211
+ text?: string;
212
+ /** Regex match against summary content (overrides `text` if both set). */
213
+ regex?: RegExp;
214
+ /** Filter by summary level(s). Default: all levels. */
215
+ levels?: SummaryLevel[];
216
+ /** Maximum number of results to return. Default: 50. */
217
+ limit?: number;
218
+ /**
219
+ * Include summaries that have been merged into a higher-level summary.
220
+ * Default: false (only "live" unmerged summaries are returned).
221
+ */
222
+ includeMerged?: boolean;
223
+ }
224
+
225
+ /** Result of a single search match. */
226
+ export interface SearchResult {
227
+ summary: SummaryEntry;
228
+ /** Number of times the query pattern matched in the summary content. */
229
+ matches: number;
230
+ }
231
+
232
+ /**
233
+ * Strategy that supports searching its summary archive. Implemented by
234
+ * AutobiographicalStrategy.
235
+ */
236
+ export interface SearchableStrategy extends ContextStrategy {
237
+ searchSummaries(query: SearchQuery): SearchResult[];
238
+ getSummary(id: string): SummaryEntry | null;
239
+ }
240
+
241
+ /** Type guard for strategies that support search. */
242
+ export function isSearchableStrategy(s: ContextStrategy): s is SearchableStrategy {
243
+ return (
244
+ 'searchSummaries' in s &&
245
+ typeof (s as SearchableStrategy).searchSummaries === 'function' &&
246
+ 'getSummary' in s &&
247
+ typeof (s as SearchableStrategy).getSummary === 'function'
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Per-render observability stats from a strategy. Counts and token sums for
253
+ * head / tail / summaries / pending work, suitable for TUIs and dashboards
254
+ * that want to display "how much of the context is folded vs raw" at a
255
+ * glance. Token sums use the strategy's own estimates so they line up with
256
+ * the numbers `select()` uses for budget math.
257
+ */
258
+ export interface RenderStats {
259
+ head: { messages: number; tokens: number };
260
+ tail: { messages: number; tokens: number };
261
+ summaries: {
262
+ l1: { count: number; tokens: number };
263
+ l2: { count: number; tokens: number };
264
+ l3: { count: number; tokens: number };
265
+ };
266
+ pending: { chunks: number; merges: number };
267
+ }
268
+
269
+ /**
270
+ * Strategy that can produce render-time observability stats. Implemented by
271
+ * AutobiographicalStrategy. Optional capability — strategies that don't
272
+ * implement it simply have `ContextManager.getRenderStats()` return `null`.
273
+ */
274
+ export interface RenderStatsCapableStrategy extends ContextStrategy {
275
+ getRenderStats(store: MessageStoreView): RenderStats;
276
+ }
277
+
278
+ /** Type guard for strategies that produce render stats. */
279
+ export function isRenderStatsCapable(s: ContextStrategy): s is RenderStatsCapableStrategy {
280
+ return (
281
+ 'getRenderStats' in s &&
282
+ typeof (s as RenderStatsCapableStrategy).getRenderStats === 'function'
283
+ );
284
+ }
285
+
131
286
  /**
132
287
  * Configuration for the Autobiographical strategy.
133
288
  */
@@ -181,35 +336,169 @@ export interface AutobiographicalConfig {
181
336
  l2BudgetTokens?: number;
182
337
  /** Token budget for L1 summaries in select() (default: 30000) */
183
338
  l1BudgetTokens?: number;
339
+
340
+ /**
341
+ * When true (default), each selected summary emits as its own positioned
342
+ * Q/A recall pair, sorted chronologically by source range. When false,
343
+ * all selected summaries are concatenated into one Q/A pair between head
344
+ * and tail (legacy behavior pre-2026-05-10).
345
+ *
346
+ * Per-region positioning is the spec-faithful behavior: it lets the agent
347
+ * see each memory in its temporal place rather than as a wall of unrelated
348
+ * recollections from another speaker. Without it, hierarchical compression
349
+ * is structurally similar to the dual-recall corruption pattern that
350
+ * caused Lena's context degradation on Hermes.
351
+ */
352
+ positionedRecallPairs?: boolean;
353
+
354
+ /**
355
+ * Template for the per-pair recall question header. Substitutions:
356
+ * {id} — summary id (e.g. "L1-3")
357
+ * {level} — summary level (1, 2, or 3)
358
+ * {first} — first source message id
359
+ * {last} — last source message id
360
+ * Default: '[Recall {id}]'.
361
+ *
362
+ * Only used when `positionedRecallPairs` is true.
363
+ */
364
+ recallHeaderTemplate?: string;
365
+
366
+ /**
367
+ * Per-tool retention limit: keep at most the last N `tool_result` blocks
368
+ * for each tool name. Older results get their content replaced with a
369
+ * brief marker referencing the tool name and how many newer results exist.
370
+ *
371
+ * Two shapes accepted:
372
+ * - `number`: applies as a global default across all tools.
373
+ * - `Record<toolName, number>`: per-tool limit; tools not listed are
374
+ * unlimited.
375
+ *
376
+ * Default: undefined (no pruning). Use a small number (1–5) for tools
377
+ * that produce verbose, mostly-stale output (e.g. file listings, http
378
+ * fetches, log queries).
379
+ */
380
+ toolResultMaxLastN?: number | Record<string, number>;
381
+
382
+ /**
383
+ * Truncate `tool_use` block inputs whose serialized JSON exceeds this
384
+ * many tokens. The truncated input becomes `{ "_truncated": true,
385
+ * "_originalTokens": N }` plus a head slice of the original input
386
+ * keys for context. Default: 0 (no truncation).
387
+ */
388
+ toolUseInputMaxTokens?: number;
389
+
390
+ /**
391
+ * Cap on the number of speculative L1 summaries the strategy will hold
392
+ * (queued + unmerged). When `count(unmerged L1s) + count(queued chunks)`
393
+ * exceeds this cap, `onNewMessage`'s auto-tick is held back. Chunks
394
+ * still form and queue, but compression is deferred until a manual
395
+ * `tick()` or `compile()` triggers it.
396
+ *
397
+ * Default: undefined (no cap; compression fires eagerly on every
398
+ * new message when `autoTickOnNewMessage` is true).
399
+ */
400
+ maxSpeculativeL1s?: number;
401
+
402
+ /**
403
+ * When false, the rendering pipeline emits the full ideal context (head
404
+ * window + all selected summaries + all recent messages) without
405
+ * truncating to fit `budget.maxTokens`. The caller's API will reject if
406
+ * the result exceeds the model's context window — the philosophy is
407
+ * "surface the overage rather than silently lose content."
408
+ *
409
+ * Default: true (legacy budget-aware truncation: stops emitting recall
410
+ * pairs and recent messages when the running total exceeds maxTokens).
411
+ *
412
+ * Recommended setting for long-lived agents on large-context models
413
+ * (e.g. opus-4-7 with 1M context): false. The window is generous enough
414
+ * that overflow is rare, and when it does happen you want to know.
415
+ */
416
+ enforceBudget?: boolean;
417
+
418
+ // --- Adaptive resolution (per docs/adaptive-resolution-design.md) ---
419
+
420
+ /**
421
+ * Enable picker-driven adaptive resolution. When true, `select()` uses
422
+ * the FoldingStrategy + Picker to choose per-message resolution under
423
+ * token-budget pressure rather than the threshold-driven `checkMergeThreshold`
424
+ * path. Default: false (existing hierarchical behavior preserved).
425
+ */
426
+ adaptiveResolution?: boolean;
427
+
428
+ /**
429
+ * Folding strategy name when adaptiveResolution is on. One of:
430
+ * 'flat-profile' (default) — level-equalizing
431
+ * 'oldest-first' — chronological
432
+ * Custom strategies can be plugged in by the host application.
433
+ */
434
+ foldingStrategy?: 'flat-profile' | 'oldest-first';
435
+
436
+ /**
437
+ * Slack ratio (hysteresis) for the picker. The picker folds until total
438
+ * tokens ≤ budget * (1 - slack), and stays quiet while between slack
439
+ * and budget. Default 0.1.
440
+ */
441
+ compressionSlackRatio?: number;
442
+
443
+ /**
444
+ * Enable bottom-up speculative pre-production of higher-level summaries.
445
+ * When a new L_k summary is produced, if N siblings exist that would share
446
+ * an L_{k+1} parent, the L_{k+1} is enqueued for production immediately
447
+ * (no picker request needed). Default true when adaptiveResolution is on.
448
+ */
449
+ speculativeProduction?: boolean;
184
450
  }
185
451
 
186
452
  /**
187
453
  * Compression level in the hierarchical pyramid.
454
+ *
455
+ * Historically constrained to 1 | 2 | 3. As of the adaptive-resolution design
456
+ * (`docs/adaptive-resolution-design.md`), levels are unbounded: the picker
457
+ * can recursively produce L4, L5, ... as needed. The narrower literal type
458
+ * is kept as `LegacySummaryLevel` for code that still assumes the old shape.
459
+ */
460
+ export type SummaryLevel = number;
461
+
462
+ /**
463
+ * The narrow level type used by pre-adaptive-resolution code paths.
464
+ * Prefer `SummaryLevel` for new code.
188
465
  */
189
- export type SummaryLevel = 1 | 2 | 3;
466
+ export type LegacySummaryLevel = 1 | 2 | 3;
190
467
 
191
468
  /**
192
469
  * A summary entry in the hierarchical memory pyramid.
193
470
  * L1: compressed from raw message chunks.
194
- * L2: merged from mergeThreshold L1s.
195
- * L3: merged from mergeThreshold L2s.
471
+ * L_{k>1}: merged from mergeThreshold L_{k-1}s.
196
472
  */
197
473
  export interface SummaryEntry {
198
474
  /** Unique ID (e.g., "L1-0", "L2-3") */
199
475
  id: string;
200
- /** Compression level */
476
+ /** Compression level (1, 2, 3, ... — unbounded in the adaptive-resolution design) */
201
477
  level: SummaryLevel;
202
478
  /** The summary text */
203
479
  content: string;
204
- /** Estimated token count (content.length / 4) */
480
+ /** Estimated token count (content.length / 4 or tokenizer-cached) */
205
481
  tokens: number;
206
- /** Level of the sources: 0 = raw messages, 1 = L1s, 2 = L2s */
207
- sourceLevel: 0 | 1 | 2;
208
- /** IDs of source items (message IDs for L1, summary IDs for L2/L3) */
482
+ /**
483
+ * Level of the sources: 0 = raw messages, k = L_k summaries.
484
+ * Pre-adaptive code uses 0 | 1 | 2; new code may produce higher values.
485
+ */
486
+ sourceLevel: number;
487
+ /** IDs of source items (message IDs for L1, summary IDs for L_{k>1}) */
209
488
  sourceIds: string[];
210
489
  /** Range of original message IDs covered */
211
490
  sourceRange: { first: string; last: string };
212
- /** If merged into a higher-level summary, that summary's ID */
491
+ /**
492
+ * The L_{level+1} summary this one is a source for, if produced.
493
+ * Pure archive metadata in the adaptive-resolution design — display
494
+ * decisions live on per-chunk `currentResolution`, not here.
495
+ */
496
+ parentId?: string;
497
+ /**
498
+ * @deprecated Renamed to `parentId` in the adaptive-resolution design.
499
+ * Kept for read compatibility with chronicles produced by the old
500
+ * threshold-driven path. New writes should set `parentId` only.
501
+ */
213
502
  mergedInto?: string;
214
503
  /** Creation timestamp */
215
504
  created: number;
@@ -217,6 +506,37 @@ export interface SummaryEntry {
217
506
  phaseType?: string;
218
507
  }
219
508
 
509
+ /**
510
+ * Helper: read the parent pointer from a summary, accepting either the
511
+ * new `parentId` field or the deprecated `mergedInto` alias.
512
+ */
513
+ export function getSummaryParentId(s: SummaryEntry): string | undefined {
514
+ return s.parentId ?? s.mergedInto;
515
+ }
516
+
517
+ /**
518
+ * A range of messages protected from compression. Pins keep a span of raw
519
+ * messages visible at their original position in the rendered context.
520
+ *
521
+ * - `kind: 'pin'` — generic protected range (any first/last span).
522
+ * - `kind: 'document'` — typically a single message containing a body of
523
+ * information the agent wants to retain in full; semantically identical
524
+ * to a single-message pin, distinguished by metadata for tooling.
525
+ */
526
+ export interface ProtectedRange {
527
+ /** Stable id assigned at pin time. */
528
+ id: string;
529
+ /** First message id of the protected range (inclusive). */
530
+ firstMessageId: string;
531
+ /** Last message id of the protected range (inclusive). */
532
+ lastMessageId: string;
533
+ kind: 'pin' | 'document';
534
+ /** Optional human-readable label. */
535
+ name?: string;
536
+ /** Creation timestamp (ms since epoch). */
537
+ created: number;
538
+ }
539
+
220
540
  /**
221
541
  * Phase type for knowledge extraction workflows.
222
542
  */
@@ -279,4 +599,6 @@ Write naturally, as recollection of what you experienced.`,
279
599
  summaryContextLabel: 'What do you remember from earlier?',
280
600
  summaryParticipant: 'Claude',
281
601
  maxMessageTokens: 0,
602
+ positionedRecallPairs: true,
603
+ recallHeaderTemplate: '[Recall {id}]',
282
604
  };