@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.
- package/dist/src/context-manager.d.ts +69 -2
- package/dist/src/context-manager.d.ts.map +1 -1
- package/dist/src/context-manager.js +128 -10
- package/dist/src/context-manager.js.map +1 -1
- package/dist/src/strategies/autobiographical.d.ts +205 -3
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +767 -59
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/types/index.d.ts +2 -2
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/strategy.d.ts +208 -0
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js +21 -0
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/non-blocking-compile.test.d.ts +22 -0
- package/dist/test/non-blocking-compile.test.d.ts.map +1 -0
- package/dist/test/non-blocking-compile.test.js +114 -0
- package/dist/test/non-blocking-compile.test.js.map +1 -0
- package/dist/test/pins.test.d.ts +10 -0
- package/dist/test/pins.test.d.ts.map +1 -0
- package/dist/test/pins.test.js +164 -0
- package/dist/test/pins.test.js.map +1 -0
- package/dist/test/recall-positioning.test.d.ts +16 -0
- package/dist/test/recall-positioning.test.d.ts.map +1 -0
- package/dist/test/recall-positioning.test.js +181 -0
- package/dist/test/recall-positioning.test.js.map +1 -0
- package/dist/test/search.test.d.ts +10 -0
- package/dist/test/search.test.d.ts.map +1 -0
- package/dist/test/search.test.js +141 -0
- package/dist/test/search.test.js.map +1 -0
- package/dist/test/speculation-cap.test.d.ts +17 -0
- package/dist/test/speculation-cap.test.d.ts.map +1 -0
- package/dist/test/speculation-cap.test.js +157 -0
- package/dist/test/speculation-cap.test.js.map +1 -0
- package/dist/test/strategy-persistence.test.d.ts +15 -0
- package/dist/test/strategy-persistence.test.d.ts.map +1 -0
- package/dist/test/strategy-persistence.test.js +244 -0
- package/dist/test/strategy-persistence.test.js.map +1 -0
- package/dist/test/tool-pruning.test.d.ts +10 -0
- package/dist/test/tool-pruning.test.d.ts.map +1 -0
- package/dist/test/tool-pruning.test.js +140 -0
- package/dist/test/tool-pruning.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/context-manager.ts +142 -10
- package/src/strategies/autobiographical.ts +815 -61
- package/src/types/index.ts +14 -1
- package/src/types/strategy.ts +223 -0
package/src/context-manager.ts
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|