@animalabs/context-manager 0.3.0 → 0.4.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 (50) hide show
  1. package/dist/src/context-manager.d.ts +69 -2
  2. package/dist/src/context-manager.d.ts.map +1 -1
  3. package/dist/src/context-manager.js +128 -10
  4. package/dist/src/context-manager.js.map +1 -1
  5. package/dist/src/strategies/autobiographical.d.ts +205 -3
  6. package/dist/src/strategies/autobiographical.d.ts.map +1 -1
  7. package/dist/src/strategies/autobiographical.js +767 -59
  8. package/dist/src/strategies/autobiographical.js.map +1 -1
  9. package/dist/src/types/index.d.ts +2 -2
  10. package/dist/src/types/index.d.ts.map +1 -1
  11. package/dist/src/types/index.js +1 -1
  12. package/dist/src/types/index.js.map +1 -1
  13. package/dist/src/types/strategy.d.ts +208 -0
  14. package/dist/src/types/strategy.d.ts.map +1 -1
  15. package/dist/src/types/strategy.js +21 -0
  16. package/dist/src/types/strategy.js.map +1 -1
  17. package/dist/test/non-blocking-compile.test.d.ts +22 -0
  18. package/dist/test/non-blocking-compile.test.d.ts.map +1 -0
  19. package/dist/test/non-blocking-compile.test.js +114 -0
  20. package/dist/test/non-blocking-compile.test.js.map +1 -0
  21. package/dist/test/pins.test.d.ts +10 -0
  22. package/dist/test/pins.test.d.ts.map +1 -0
  23. package/dist/test/pins.test.js +164 -0
  24. package/dist/test/pins.test.js.map +1 -0
  25. package/dist/test/recall-positioning.test.d.ts +16 -0
  26. package/dist/test/recall-positioning.test.d.ts.map +1 -0
  27. package/dist/test/recall-positioning.test.js +181 -0
  28. package/dist/test/recall-positioning.test.js.map +1 -0
  29. package/dist/test/search.test.d.ts +10 -0
  30. package/dist/test/search.test.d.ts.map +1 -0
  31. package/dist/test/search.test.js +141 -0
  32. package/dist/test/search.test.js.map +1 -0
  33. package/dist/test/speculation-cap.test.d.ts +17 -0
  34. package/dist/test/speculation-cap.test.d.ts.map +1 -0
  35. package/dist/test/speculation-cap.test.js +157 -0
  36. package/dist/test/speculation-cap.test.js.map +1 -0
  37. package/dist/test/strategy-persistence.test.d.ts +15 -0
  38. package/dist/test/strategy-persistence.test.d.ts.map +1 -0
  39. package/dist/test/strategy-persistence.test.js +244 -0
  40. package/dist/test/strategy-persistence.test.js.map +1 -0
  41. package/dist/test/tool-pruning.test.d.ts +10 -0
  42. package/dist/test/tool-pruning.test.d.ts.map +1 -0
  43. package/dist/test/tool-pruning.test.js +140 -0
  44. package/dist/test/tool-pruning.test.js.map +1 -0
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +1 -1
  47. package/src/context-manager.ts +142 -10
  48. package/src/strategies/autobiographical.ts +815 -61
  49. package/src/types/index.ts +14 -1
  50. package/src/types/strategy.ts +223 -0
@@ -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';
@@ -1,3 +1,4 @@
1
+ import type { JsStore } from '@animalabs/chronicle';
1
2
  import type { Membrane } from '@animalabs/membrane';
2
3
  import type { StoredMessage, MessageId, Sequence } from './message.js';
3
4
  import type { ContextEntry, TokenBudget, PendingWork } from './context.js';
@@ -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
  /**
@@ -128,6 +145,109 @@ export function isResettableStrategy(s: ContextStrategy): s is ResettableStrateg
128
145
  return 'resetHeadWindow' in s && typeof (s as ResettableStrategy).resetHeadWindow === 'function';
129
146
  }
130
147
 
148
+ /**
149
+ * Strategy that supports protected ranges (pins + documents).
150
+ * Pinned ranges are excluded from compression and render raw at their
151
+ * original chronological position. Implemented by AutobiographicalStrategy.
152
+ */
153
+ export interface PinnableStrategy extends ContextStrategy {
154
+ pinRange(firstMessageId: string, lastMessageId: string, opts?: { name?: string }): string;
155
+ markDocument(messageId: string, opts?: { name?: string }): string;
156
+ unpin(pinId: string): boolean;
157
+ listPins(): ReadonlyArray<ProtectedRange>;
158
+ }
159
+
160
+ /** Type guard for strategies that support pins / documents. */
161
+ export function isPinnableStrategy(s: ContextStrategy): s is PinnableStrategy {
162
+ return (
163
+ 'pinRange' in s &&
164
+ typeof (s as PinnableStrategy).pinRange === 'function' &&
165
+ 'unpin' in s &&
166
+ typeof (s as PinnableStrategy).unpin === 'function'
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Query for `searchSummaries`. At least one of `text` or `regex` should be
172
+ * provided to constrain results; otherwise all summaries pass.
173
+ */
174
+ export interface SearchQuery {
175
+ /** Case-insensitive substring match against summary content. */
176
+ text?: string;
177
+ /** Regex match against summary content (overrides `text` if both set). */
178
+ regex?: RegExp;
179
+ /** Filter by summary level(s). Default: all levels. */
180
+ levels?: SummaryLevel[];
181
+ /** Maximum number of results to return. Default: 50. */
182
+ limit?: number;
183
+ /**
184
+ * Include summaries that have been merged into a higher-level summary.
185
+ * Default: false (only "live" unmerged summaries are returned).
186
+ */
187
+ includeMerged?: boolean;
188
+ }
189
+
190
+ /** Result of a single search match. */
191
+ export interface SearchResult {
192
+ summary: SummaryEntry;
193
+ /** Number of times the query pattern matched in the summary content. */
194
+ matches: number;
195
+ }
196
+
197
+ /**
198
+ * Strategy that supports searching its summary archive. Implemented by
199
+ * AutobiographicalStrategy.
200
+ */
201
+ export interface SearchableStrategy extends ContextStrategy {
202
+ searchSummaries(query: SearchQuery): SearchResult[];
203
+ getSummary(id: string): SummaryEntry | null;
204
+ }
205
+
206
+ /** Type guard for strategies that support search. */
207
+ export function isSearchableStrategy(s: ContextStrategy): s is SearchableStrategy {
208
+ return (
209
+ 'searchSummaries' in s &&
210
+ typeof (s as SearchableStrategy).searchSummaries === 'function' &&
211
+ 'getSummary' in s &&
212
+ typeof (s as SearchableStrategy).getSummary === 'function'
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Per-render observability stats from a strategy. Counts and token sums for
218
+ * head / tail / summaries / pending work, suitable for TUIs and dashboards
219
+ * that want to display "how much of the context is folded vs raw" at a
220
+ * glance. Token sums use the strategy's own estimates so they line up with
221
+ * the numbers `select()` uses for budget math.
222
+ */
223
+ export interface RenderStats {
224
+ head: { messages: number; tokens: number };
225
+ tail: { messages: number; tokens: number };
226
+ summaries: {
227
+ l1: { count: number; tokens: number };
228
+ l2: { count: number; tokens: number };
229
+ l3: { count: number; tokens: number };
230
+ };
231
+ pending: { chunks: number; merges: number };
232
+ }
233
+
234
+ /**
235
+ * Strategy that can produce render-time observability stats. Implemented by
236
+ * AutobiographicalStrategy. Optional capability — strategies that don't
237
+ * implement it simply have `ContextManager.getRenderStats()` return `null`.
238
+ */
239
+ export interface RenderStatsCapableStrategy extends ContextStrategy {
240
+ getRenderStats(store: MessageStoreView): RenderStats;
241
+ }
242
+
243
+ /** Type guard for strategies that produce render stats. */
244
+ export function isRenderStatsCapable(s: ContextStrategy): s is RenderStatsCapableStrategy {
245
+ return (
246
+ 'getRenderStats' in s &&
247
+ typeof (s as RenderStatsCapableStrategy).getRenderStats === 'function'
248
+ );
249
+ }
250
+
131
251
  /**
132
252
  * Configuration for the Autobiographical strategy.
133
253
  */
@@ -181,6 +301,84 @@ export interface AutobiographicalConfig {
181
301
  l2BudgetTokens?: number;
182
302
  /** Token budget for L1 summaries in select() (default: 30000) */
183
303
  l1BudgetTokens?: number;
304
+
305
+ /**
306
+ * When true (default), each selected summary emits as its own positioned
307
+ * Q/A recall pair, sorted chronologically by source range. When false,
308
+ * all selected summaries are concatenated into one Q/A pair between head
309
+ * and tail (legacy behavior pre-2026-05-10).
310
+ *
311
+ * Per-region positioning is the spec-faithful behavior: it lets the agent
312
+ * see each memory in its temporal place rather than as a wall of unrelated
313
+ * recollections from another speaker. Without it, hierarchical compression
314
+ * is structurally similar to the dual-recall corruption pattern that
315
+ * caused Lena's context degradation on Hermes.
316
+ */
317
+ positionedRecallPairs?: boolean;
318
+
319
+ /**
320
+ * Template for the per-pair recall question header. Substitutions:
321
+ * {id} — summary id (e.g. "L1-3")
322
+ * {level} — summary level (1, 2, or 3)
323
+ * {first} — first source message id
324
+ * {last} — last source message id
325
+ * Default: '[Recall {id}]'.
326
+ *
327
+ * Only used when `positionedRecallPairs` is true.
328
+ */
329
+ recallHeaderTemplate?: string;
330
+
331
+ /**
332
+ * Per-tool retention limit: keep at most the last N `tool_result` blocks
333
+ * for each tool name. Older results get their content replaced with a
334
+ * brief marker referencing the tool name and how many newer results exist.
335
+ *
336
+ * Two shapes accepted:
337
+ * - `number`: applies as a global default across all tools.
338
+ * - `Record<toolName, number>`: per-tool limit; tools not listed are
339
+ * unlimited.
340
+ *
341
+ * Default: undefined (no pruning). Use a small number (1–5) for tools
342
+ * that produce verbose, mostly-stale output (e.g. file listings, http
343
+ * fetches, log queries).
344
+ */
345
+ toolResultMaxLastN?: number | Record<string, number>;
346
+
347
+ /**
348
+ * Truncate `tool_use` block inputs whose serialized JSON exceeds this
349
+ * many tokens. The truncated input becomes `{ "_truncated": true,
350
+ * "_originalTokens": N }` plus a head slice of the original input
351
+ * keys for context. Default: 0 (no truncation).
352
+ */
353
+ toolUseInputMaxTokens?: number;
354
+
355
+ /**
356
+ * Cap on the number of speculative L1 summaries the strategy will hold
357
+ * (queued + unmerged). When `count(unmerged L1s) + count(queued chunks)`
358
+ * exceeds this cap, `onNewMessage`'s auto-tick is held back. Chunks
359
+ * still form and queue, but compression is deferred until a manual
360
+ * `tick()` or `compile()` triggers it.
361
+ *
362
+ * Default: undefined (no cap; compression fires eagerly on every
363
+ * new message when `autoTickOnNewMessage` is true).
364
+ */
365
+ maxSpeculativeL1s?: number;
366
+
367
+ /**
368
+ * When false, the rendering pipeline emits the full ideal context (head
369
+ * window + all selected summaries + all recent messages) without
370
+ * truncating to fit `budget.maxTokens`. The caller's API will reject if
371
+ * the result exceeds the model's context window — the philosophy is
372
+ * "surface the overage rather than silently lose content."
373
+ *
374
+ * Default: true (legacy budget-aware truncation: stops emitting recall
375
+ * pairs and recent messages when the running total exceeds maxTokens).
376
+ *
377
+ * Recommended setting for long-lived agents on large-context models
378
+ * (e.g. opus-4-7 with 1M context): false. The window is generous enough
379
+ * that overflow is rare, and when it does happen you want to know.
380
+ */
381
+ enforceBudget?: boolean;
184
382
  }
185
383
 
186
384
  /**
@@ -217,6 +415,29 @@ export interface SummaryEntry {
217
415
  phaseType?: string;
218
416
  }
219
417
 
418
+ /**
419
+ * A range of messages protected from compression. Pins keep a span of raw
420
+ * messages visible at their original position in the rendered context.
421
+ *
422
+ * - `kind: 'pin'` — generic protected range (any first/last span).
423
+ * - `kind: 'document'` — typically a single message containing a body of
424
+ * information the agent wants to retain in full; semantically identical
425
+ * to a single-message pin, distinguished by metadata for tooling.
426
+ */
427
+ export interface ProtectedRange {
428
+ /** Stable id assigned at pin time. */
429
+ id: string;
430
+ /** First message id of the protected range (inclusive). */
431
+ firstMessageId: string;
432
+ /** Last message id of the protected range (inclusive). */
433
+ lastMessageId: string;
434
+ kind: 'pin' | 'document';
435
+ /** Optional human-readable label. */
436
+ name?: string;
437
+ /** Creation timestamp (ms since epoch). */
438
+ created: number;
439
+ }
440
+
220
441
  /**
221
442
  * Phase type for knowledge extraction workflows.
222
443
  */
@@ -279,4 +500,6 @@ Write naturally, as recollection of what you experienced.`,
279
500
  summaryContextLabel: 'What do you remember from earlier?',
280
501
  summaryParticipant: 'Claude',
281
502
  maxMessageTokens: 0,
503
+ positionedRecallPairs: true,
504
+ recallHeaderTemplate: '[Recall {id}]',
282
505
  };