@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
@@ -15,8 +15,13 @@ import type {
15
15
  MessageQueryResult,
16
16
  ContextInjection,
17
17
  CompileResult,
18
+ ProtectedRange,
19
+ SearchQuery,
20
+ SearchResult,
21
+ SummaryEntry,
18
22
  } from './types/index.js';
19
- import { isResettableStrategy } from './types/index.js';
23
+ import { isResettableStrategy, isPinnableStrategy, isSearchableStrategy, isRenderStatsCapable } from './types/index.js';
24
+ import type { RenderStats } from './types/index.js';
20
25
  import { MessageStore, MessageStoreEvent } from './message-store.js';
21
26
  import { ContextLog } from './context-log.js';
22
27
  import { PassthroughStrategy } from './strategies/passthrough.js';
@@ -93,6 +98,8 @@ export class ContextManager {
93
98
  /** Whether we own the store (created it) vs app owns it (passed in) */
94
99
  private ownsStore: boolean;
95
100
  private debugLogContext: boolean;
101
+ /** Namespace passed to strategies for scoping their persistent state slots. */
102
+ private strategyNamespace: string;
96
103
 
97
104
  private constructor(
98
105
  store: JsStore,
@@ -100,6 +107,7 @@ export class ContextManager {
100
107
  contextLog: ContextLog,
101
108
  strategy: ContextStrategy,
102
109
  ownsStore: boolean,
110
+ strategyNamespace: string,
103
111
  membrane?: Membrane,
104
112
  debugLogContext = false,
105
113
  ) {
@@ -108,6 +116,7 @@ export class ContextManager {
108
116
  this.contextLog = contextLog;
109
117
  this.strategy = strategy;
110
118
  this.ownsStore = ownsStore;
119
+ this.strategyNamespace = strategyNamespace;
111
120
  this.membrane = membrane;
112
121
  this.debugLogContext = debugLogContext;
113
122
 
@@ -173,12 +182,18 @@ export class ContextManager {
173
182
  });
174
183
  const strategy = config.strategy ?? new PassthroughStrategy();
175
184
 
185
+ // Namespace passed to strategies. Falls back to a stable per-store value
186
+ // so strategies always have something to scope state IDs by, even when
187
+ // the caller didn't supply a namespace.
188
+ const strategyNamespace = config.namespace ?? 'default';
189
+
176
190
  const manager = new ContextManager(
177
191
  store,
178
192
  messageStore,
179
193
  contextLog,
180
194
  strategy,
181
195
  ownsStore,
196
+ strategyNamespace,
182
197
  config.membrane,
183
198
  config.debugLogContext ?? false,
184
199
  );
@@ -294,6 +309,10 @@ export class ContextManager {
294
309
  /**
295
310
  * Create a branch from a specific message.
296
311
  * The new branch will have state as of that message's sequence (time-travel branching).
312
+ *
313
+ * Returns the new branch *name*, which is what `switchBranch` and `forkAt`
314
+ * expect. (Chronicle's branch APIs are name-keyed; the numeric `id` field
315
+ * on JsBranch is an internal identifier and isn't accepted by switchBranch.)
297
316
  */
298
317
  branchAt(messageId: MessageId, name?: string): string {
299
318
  const message = this.messageStore.get(messageId);
@@ -303,21 +322,50 @@ export class ContextManager {
303
322
 
304
323
  // Create branch name if not provided
305
324
  const branchName = name ?? `branch-${Date.now()}`;
306
-
325
+
307
326
  // Get current branch name to branch from
308
327
  const currentBranch = this.store.currentBranch();
309
-
328
+
310
329
  // Use createBranchAt to branch at the message's sequence (time-travel)
311
330
  const branch = this.store.createBranchAt(branchName, currentBranch.name, message.sequence);
312
331
 
313
- return branch.id;
332
+ return branch.name;
314
333
  }
315
334
 
316
335
  /**
317
336
  * Switch to a different branch.
337
+ *
338
+ * Re-initializes the strategy after switching so any branch-scoped state
339
+ * stored on Chronicle is reloaded. Strategies that hold derived in-memory
340
+ * caches (e.g. AutobiographicalStrategy.summaries) need this to avoid
341
+ * showing the previous branch's state on the new branch.
318
342
  */
319
- switchBranch(branchId: string): void {
343
+ async switchBranch(branchId: string): Promise<void> {
320
344
  this.store.switchBranch(branchId);
345
+ await this.initializeStrategy();
346
+ }
347
+
348
+ /**
349
+ * Fork from the current head: create a new branch at the current sequence
350
+ * and switch to it. The new branch starts with all current state (messages,
351
+ * context log, and strategy state) and diverges from there.
352
+ *
353
+ * Use this when an agent wants to explore an alternate timeline from
354
+ * "now" — e.g. trying a different response without committing.
355
+ *
356
+ * For time-travel branching at a specific historical message, use
357
+ * `branchAt(messageId, name?)` instead, then `switchBranch(name)`.
358
+ *
359
+ * Returns the new branch's name. The strategy is re-initialized on the
360
+ * new branch so it picks up the forked state.
361
+ */
362
+ async fork(name?: string): Promise<string> {
363
+ const branchName = name ?? `fork-${Date.now()}`;
364
+ const currentBranch = this.store.currentBranch();
365
+ const currentSeq = this.store.currentSequence();
366
+ const branch = this.store.createBranchAt(branchName, currentBranch.name, currentSeq);
367
+ await this.switchBranch(branch.name);
368
+ return branch.name;
321
369
  }
322
370
 
323
371
  /**
@@ -390,11 +438,15 @@ export class ContextManager {
390
438
  budget?: TokenBudget,
391
439
  injections?: ContextInjection[]
392
440
  ): Promise<CompileResult> {
393
- // Check readiness and wait if needed
394
- const readiness = this.strategy.checkReadiness();
395
- if (!readiness.ready && readiness.pendingWork) {
396
- await readiness.pendingWork;
397
- }
441
+ // Don't block the agent's turn on speculative compression — let it
442
+ // run in the background. The strategy renders whatever's available
443
+ // now; the next compile picks up the freshly-formed L1.
444
+ //
445
+ // Old behavior (await pendingWork to fold the latest chunk before
446
+ // the agent responds) added 30+ seconds of latency per turn whenever
447
+ // a chunk was forming, which is unacceptable UX for an agent that
448
+ // streams its responses. We accept "this turn doesn't have the very
449
+ // latest L1" in exchange for non-blocking compile.
398
450
 
399
451
  // Default budget
400
452
  const effectiveBudget: TokenBudget = budget ?? {
@@ -527,6 +579,84 @@ export class ContextManager {
527
579
  return this.strategy;
528
580
  }
529
581
 
582
+ // ==========================================================================
583
+ // Pins / documents (passthrough to the active strategy)
584
+ // ==========================================================================
585
+
586
+ /**
587
+ * Pin a range of messages so they aren't compressed and render raw at
588
+ * their original chronological position. Returns the new pin id.
589
+ *
590
+ * Throws if the active strategy doesn't support pins.
591
+ */
592
+ pinRange(firstMessageId: MessageId, lastMessageId: MessageId, opts?: { name?: string }): string {
593
+ if (!isPinnableStrategy(this.strategy)) {
594
+ throw new Error('Active strategy does not support pins');
595
+ }
596
+ return this.strategy.pinRange(firstMessageId, lastMessageId, opts);
597
+ }
598
+
599
+ /**
600
+ * Mark a single message as a "document" (semantically a body of
601
+ * information to retain in full). Same effect as a single-message pin
602
+ * with `kind: 'document'`. Returns the new pin id.
603
+ */
604
+ markDocument(messageId: MessageId, opts?: { name?: string }): string {
605
+ if (!isPinnableStrategy(this.strategy)) {
606
+ throw new Error('Active strategy does not support documents');
607
+ }
608
+ return this.strategy.markDocument(messageId, opts);
609
+ }
610
+
611
+ /** Remove a pin or document mark. Returns true if removed. */
612
+ unpin(pinId: string): boolean {
613
+ if (!isPinnableStrategy(this.strategy)) {
614
+ throw new Error('Active strategy does not support pins');
615
+ }
616
+ return this.strategy.unpin(pinId);
617
+ }
618
+
619
+ /** List all current pins. Returns empty array if strategy is not pinnable. */
620
+ listPins(): ReadonlyArray<ProtectedRange> {
621
+ if (!isPinnableStrategy(this.strategy)) return [];
622
+ return this.strategy.listPins();
623
+ }
624
+
625
+ // ==========================================================================
626
+ // Search (passthrough to the active strategy)
627
+ // ==========================================================================
628
+
629
+ /**
630
+ * Search the strategy's summary archive (substring or regex over content).
631
+ * Returns empty array if the strategy doesn't support search.
632
+ *
633
+ * Suitable for building memory-search agent tools at the framework layer
634
+ * — see e.g. agent-framework's MCPL host integration.
635
+ */
636
+ searchSummaries(query: SearchQuery): SearchResult[] {
637
+ if (!isSearchableStrategy(this.strategy)) return [];
638
+ return this.strategy.searchSummaries(query);
639
+ }
640
+
641
+ /** Look up a single summary by id. Returns null if not found / unsupported. */
642
+ getSummary(id: string): SummaryEntry | null {
643
+ if (!isSearchableStrategy(this.strategy)) return null;
644
+ return this.strategy.getSummary(id);
645
+ }
646
+
647
+ /**
648
+ * Per-render stats from the active strategy: head/tail message + token
649
+ * counts, plus per-level summary counts and total tokens. Returns null
650
+ * if the strategy doesn't implement `getRenderStats`.
651
+ *
652
+ * Designed for TUIs / dashboards that want to display "how much of the
653
+ * agent's context is folded vs raw" at a glance.
654
+ */
655
+ getRenderStats(): RenderStats | null {
656
+ if (!isRenderStatsCapable(this.strategy)) return null;
657
+ return this.strategy.getRenderStats(this.messageStore.createView());
658
+ }
659
+
530
660
  /**
531
661
  * Reset the head window to start from a new position.
532
662
  * Old head window messages become compressible.
@@ -583,6 +713,8 @@ export class ContextManager {
583
713
  contextLog: this.contextLog.createView(),
584
714
  membrane: this.membrane,
585
715
  currentSequence: this.store.currentSequence(),
716
+ store: this.store,
717
+ namespace: this.strategyNamespace,
586
718
  };
587
719
  }
588
720