@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.
- package/dist/src/adaptive/chunker.d.ts +70 -0
- package/dist/src/adaptive/chunker.d.ts.map +1 -0
- package/dist/src/adaptive/chunker.js +233 -0
- package/dist/src/adaptive/chunker.js.map +1 -0
- package/dist/src/adaptive/folding-strategy.d.ts +118 -0
- package/dist/src/adaptive/folding-strategy.d.ts.map +1 -0
- package/dist/src/adaptive/folding-strategy.js +12 -0
- package/dist/src/adaptive/folding-strategy.js.map +1 -0
- package/dist/src/adaptive/index.d.ts +17 -0
- package/dist/src/adaptive/index.d.ts.map +1 -0
- package/dist/src/adaptive/index.js +23 -0
- package/dist/src/adaptive/index.js.map +1 -0
- package/dist/src/adaptive/picker.d.ts +103 -0
- package/dist/src/adaptive/picker.d.ts.map +1 -0
- package/dist/src/adaptive/picker.js +357 -0
- package/dist/src/adaptive/picker.js.map +1 -0
- package/dist/src/adaptive/render.d.ts +39 -0
- package/dist/src/adaptive/render.d.ts.map +1 -0
- package/dist/src/adaptive/render.js +104 -0
- package/dist/src/adaptive/render.js.map +1 -0
- package/dist/src/adaptive/strategies/flat-profile.d.ts +19 -0
- package/dist/src/adaptive/strategies/flat-profile.d.ts.map +1 -0
- package/dist/src/adaptive/strategies/flat-profile.js +86 -0
- package/dist/src/adaptive/strategies/flat-profile.js.map +1 -0
- package/dist/src/adaptive/strategies/oldest-first.d.ts +19 -0
- package/dist/src/adaptive/strategies/oldest-first.d.ts.map +1 -0
- package/dist/src/adaptive/strategies/oldest-first.js +39 -0
- package/dist/src/adaptive/strategies/oldest-first.js.map +1 -0
- package/dist/src/context-manager.d.ts +74 -2
- package/dist/src/context-manager.d.ts.map +1 -1
- package/dist/src/context-manager.js +150 -10
- package/dist/src/context-manager.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/message-store.d.ts +36 -1
- package/dist/src/message-store.d.ts.map +1 -1
- package/dist/src/message-store.js +134 -8
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/strategies/autobiographical.d.ts +422 -5
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +2248 -98
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/strategies/index.d.ts +1 -1
- package/dist/src/strategies/index.d.ts.map +1 -1
- package/dist/src/strategies/index.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/message.d.ts +38 -0
- package/dist/src/types/message.d.ts.map +1 -1
- package/dist/src/types/strategy.d.ts +302 -10
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js +28 -0
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/adaptive/branching.test.d.ts +20 -0
- package/dist/test/adaptive/branching.test.d.ts.map +1 -0
- package/dist/test/adaptive/branching.test.js +176 -0
- package/dist/test/adaptive/branching.test.js.map +1 -0
- package/dist/test/adaptive/chunker.test.d.ts +2 -0
- package/dist/test/adaptive/chunker.test.d.ts.map +1 -0
- package/dist/test/adaptive/chunker.test.js +120 -0
- package/dist/test/adaptive/chunker.test.js.map +1 -0
- package/dist/test/adaptive/deep-levels.test.d.ts +10 -0
- package/dist/test/adaptive/deep-levels.test.d.ts.map +1 -0
- package/dist/test/adaptive/deep-levels.test.js +167 -0
- package/dist/test/adaptive/deep-levels.test.js.map +1 -0
- package/dist/test/adaptive/doc-plus-chat.test.d.ts +10 -0
- package/dist/test/adaptive/doc-plus-chat.test.d.ts.map +1 -0
- package/dist/test/adaptive/doc-plus-chat.test.js +194 -0
- package/dist/test/adaptive/doc-plus-chat.test.js.map +1 -0
- package/dist/test/adaptive/hard-fail.test.d.ts +9 -0
- package/dist/test/adaptive/hard-fail.test.d.ts.map +1 -0
- package/dist/test/adaptive/hard-fail.test.js +154 -0
- package/dist/test/adaptive/hard-fail.test.js.map +1 -0
- package/dist/test/adaptive/harness.d.ts +109 -0
- package/dist/test/adaptive/harness.d.ts.map +1 -0
- package/dist/test/adaptive/harness.js +263 -0
- package/dist/test/adaptive/harness.js.map +1 -0
- package/dist/test/adaptive/ingestion.test.d.ts +13 -0
- package/dist/test/adaptive/ingestion.test.d.ts.map +1 -0
- package/dist/test/adaptive/ingestion.test.js +306 -0
- package/dist/test/adaptive/ingestion.test.js.map +1 -0
- package/dist/test/adaptive/integration.test.d.ts +12 -0
- package/dist/test/adaptive/integration.test.d.ts.map +1 -0
- package/dist/test/adaptive/integration.test.js +247 -0
- package/dist/test/adaptive/integration.test.js.map +1 -0
- package/dist/test/adaptive/long-chronicle.test.d.ts +13 -0
- package/dist/test/adaptive/long-chronicle.test.d.ts.map +1 -0
- package/dist/test/adaptive/long-chronicle.test.js +186 -0
- package/dist/test/adaptive/long-chronicle.test.js.map +1 -0
- package/dist/test/adaptive/persistence.test.d.ts +11 -0
- package/dist/test/adaptive/persistence.test.d.ts.map +1 -0
- package/dist/test/adaptive/persistence.test.js +200 -0
- package/dist/test/adaptive/persistence.test.js.map +1 -0
- package/dist/test/adaptive/picker.test.d.ts +2 -0
- package/dist/test/adaptive/picker.test.d.ts.map +1 -0
- package/dist/test/adaptive/picker.test.js +333 -0
- package/dist/test/adaptive/picker.test.js.map +1 -0
- package/dist/test/adaptive/render.test.d.ts +2 -0
- package/dist/test/adaptive/render.test.d.ts.map +1 -0
- package/dist/test/adaptive/render.test.js +109 -0
- package/dist/test/adaptive/render.test.js.map +1 -0
- package/dist/test/adaptive/shard-immutability.test.d.ts +14 -0
- package/dist/test/adaptive/shard-immutability.test.d.ts.map +1 -0
- package/dist/test/adaptive/shard-immutability.test.js +184 -0
- package/dist/test/adaptive/shard-immutability.test.js.map +1 -0
- package/dist/test/adaptive/strategy-integration.test.d.ts +9 -0
- package/dist/test/adaptive/strategy-integration.test.d.ts.map +1 -0
- package/dist/test/adaptive/strategy-integration.test.js +293 -0
- package/dist/test/adaptive/strategy-integration.test.js.map +1 -0
- package/dist/test/message-store-sequence.test.d.ts +25 -0
- package/dist/test/message-store-sequence.test.d.ts.map +1 -0
- package/dist/test/message-store-sequence.test.js +157 -0
- package/dist/test/message-store-sequence.test.js.map +1 -0
- 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/adaptive/chunker.ts +323 -0
- package/src/adaptive/folding-strategy.ts +129 -0
- package/src/adaptive/index.ts +51 -0
- package/src/adaptive/picker.ts +442 -0
- package/src/adaptive/render.ts +115 -0
- package/src/adaptive/strategies/flat-profile.ts +96 -0
- package/src/adaptive/strategies/oldest-first.ts +48 -0
- package/src/context-manager.ts +174 -10
- package/src/index.ts +1 -1
- package/src/message-store.ts +147 -8
- package/src/strategies/autobiographical.ts +2468 -166
- package/src/strategies/index.ts +1 -1
- package/src/types/index.ts +14 -1
- package/src/types/message.ts +43 -0
- package/src/types/strategy.ts +332 -10
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
|
);
|
|
@@ -196,6 +211,11 @@ export class ContextManager {
|
|
|
196
211
|
|
|
197
212
|
/**
|
|
198
213
|
* Add a message to the store.
|
|
214
|
+
*
|
|
215
|
+
* If the configured strategy implements `chunkIngressMessage` and returns
|
|
216
|
+
* a non-null sharding decision, the message is stored as multiple records
|
|
217
|
+
* sharing a `bodyGroupId` (per the adaptive-resolution design §3.6). The
|
|
218
|
+
* returned MessageId is the first shard's id.
|
|
199
219
|
*/
|
|
200
220
|
addMessage(
|
|
201
221
|
participant: string,
|
|
@@ -203,6 +223,33 @@ export class ContextManager {
|
|
|
203
223
|
metadata?: MessageMetadata,
|
|
204
224
|
causedBy?: MessageId[]
|
|
205
225
|
): MessageId {
|
|
226
|
+
// Optional strategy-driven ingestion-time chunking
|
|
227
|
+
const strategyAny = this.strategy as unknown as {
|
|
228
|
+
chunkIngressMessage?: (
|
|
229
|
+
participant: string,
|
|
230
|
+
content: ContentBlock[]
|
|
231
|
+
) => { bodyGroupId: string; shards: Array<{ content: ContentBlock[]; shardIndex: number }> } | null;
|
|
232
|
+
};
|
|
233
|
+
if (typeof strategyAny.chunkIngressMessage === 'function') {
|
|
234
|
+
const decision = strategyAny.chunkIngressMessage(participant, content);
|
|
235
|
+
if (decision && decision.shards.length > 1) {
|
|
236
|
+
let firstId: MessageId | null = null;
|
|
237
|
+
for (const shard of decision.shards) {
|
|
238
|
+
const message = this.messageStore.append(
|
|
239
|
+
participant,
|
|
240
|
+
shard.content,
|
|
241
|
+
metadata,
|
|
242
|
+
causedBy,
|
|
243
|
+
{
|
|
244
|
+
bodyGroupId: decision.bodyGroupId,
|
|
245
|
+
shardIndex: shard.shardIndex,
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
if (firstId === null) firstId = message.id;
|
|
249
|
+
}
|
|
250
|
+
return firstId!;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
206
253
|
const message = this.messageStore.append(participant, content, metadata, causedBy);
|
|
207
254
|
return message.id;
|
|
208
255
|
}
|
|
@@ -294,6 +341,10 @@ export class ContextManager {
|
|
|
294
341
|
/**
|
|
295
342
|
* Create a branch from a specific message.
|
|
296
343
|
* The new branch will have state as of that message's sequence (time-travel branching).
|
|
344
|
+
*
|
|
345
|
+
* Returns the new branch *name*, which is what `switchBranch` and `forkAt`
|
|
346
|
+
* expect. (Chronicle's branch APIs are name-keyed; the numeric `id` field
|
|
347
|
+
* on JsBranch is an internal identifier and isn't accepted by switchBranch.)
|
|
297
348
|
*/
|
|
298
349
|
branchAt(messageId: MessageId, name?: string): string {
|
|
299
350
|
const message = this.messageStore.get(messageId);
|
|
@@ -303,21 +354,50 @@ export class ContextManager {
|
|
|
303
354
|
|
|
304
355
|
// Create branch name if not provided
|
|
305
356
|
const branchName = name ?? `branch-${Date.now()}`;
|
|
306
|
-
|
|
357
|
+
|
|
307
358
|
// Get current branch name to branch from
|
|
308
359
|
const currentBranch = this.store.currentBranch();
|
|
309
|
-
|
|
360
|
+
|
|
310
361
|
// Use createBranchAt to branch at the message's sequence (time-travel)
|
|
311
362
|
const branch = this.store.createBranchAt(branchName, currentBranch.name, message.sequence);
|
|
312
363
|
|
|
313
|
-
return branch.
|
|
364
|
+
return branch.name;
|
|
314
365
|
}
|
|
315
366
|
|
|
316
367
|
/**
|
|
317
368
|
* Switch to a different branch.
|
|
369
|
+
*
|
|
370
|
+
* Re-initializes the strategy after switching so any branch-scoped state
|
|
371
|
+
* stored on Chronicle is reloaded. Strategies that hold derived in-memory
|
|
372
|
+
* caches (e.g. AutobiographicalStrategy.summaries) need this to avoid
|
|
373
|
+
* showing the previous branch's state on the new branch.
|
|
318
374
|
*/
|
|
319
|
-
switchBranch(branchId: string): void {
|
|
375
|
+
async switchBranch(branchId: string): Promise<void> {
|
|
320
376
|
this.store.switchBranch(branchId);
|
|
377
|
+
await this.initializeStrategy();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Fork from the current head: create a new branch at the current sequence
|
|
382
|
+
* and switch to it. The new branch starts with all current state (messages,
|
|
383
|
+
* context log, and strategy state) and diverges from there.
|
|
384
|
+
*
|
|
385
|
+
* Use this when an agent wants to explore an alternate timeline from
|
|
386
|
+
* "now" — e.g. trying a different response without committing.
|
|
387
|
+
*
|
|
388
|
+
* For time-travel branching at a specific historical message, use
|
|
389
|
+
* `branchAt(messageId, name?)` instead, then `switchBranch(name)`.
|
|
390
|
+
*
|
|
391
|
+
* Returns the new branch's name. The strategy is re-initialized on the
|
|
392
|
+
* new branch so it picks up the forked state.
|
|
393
|
+
*/
|
|
394
|
+
async fork(name?: string): Promise<string> {
|
|
395
|
+
const branchName = name ?? `fork-${Date.now()}`;
|
|
396
|
+
const currentBranch = this.store.currentBranch();
|
|
397
|
+
const currentSeq = this.store.currentSequence();
|
|
398
|
+
const branch = this.store.createBranchAt(branchName, currentBranch.name, currentSeq);
|
|
399
|
+
await this.switchBranch(branch.name);
|
|
400
|
+
return branch.name;
|
|
321
401
|
}
|
|
322
402
|
|
|
323
403
|
/**
|
|
@@ -390,11 +470,15 @@ export class ContextManager {
|
|
|
390
470
|
budget?: TokenBudget,
|
|
391
471
|
injections?: ContextInjection[]
|
|
392
472
|
): Promise<CompileResult> {
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
473
|
+
// Don't block the agent's turn on speculative compression — let it
|
|
474
|
+
// run in the background. The strategy renders whatever's available
|
|
475
|
+
// now; the next compile picks up the freshly-formed L1.
|
|
476
|
+
//
|
|
477
|
+
// Old behavior (await pendingWork to fold the latest chunk before
|
|
478
|
+
// the agent responds) added 30+ seconds of latency per turn whenever
|
|
479
|
+
// a chunk was forming, which is unacceptable UX for an agent that
|
|
480
|
+
// streams its responses. We accept "this turn doesn't have the very
|
|
481
|
+
// latest L1" in exchange for non-blocking compile.
|
|
398
482
|
|
|
399
483
|
// Default budget
|
|
400
484
|
const effectiveBudget: TokenBudget = budget ?? {
|
|
@@ -527,6 +611,84 @@ export class ContextManager {
|
|
|
527
611
|
return this.strategy;
|
|
528
612
|
}
|
|
529
613
|
|
|
614
|
+
// ==========================================================================
|
|
615
|
+
// Pins / documents (passthrough to the active strategy)
|
|
616
|
+
// ==========================================================================
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Pin a range of messages so they aren't compressed and render raw at
|
|
620
|
+
* their original chronological position. Returns the new pin id.
|
|
621
|
+
*
|
|
622
|
+
* Throws if the active strategy doesn't support pins.
|
|
623
|
+
*/
|
|
624
|
+
pinRange(firstMessageId: MessageId, lastMessageId: MessageId, opts?: { name?: string }): string {
|
|
625
|
+
if (!isPinnableStrategy(this.strategy)) {
|
|
626
|
+
throw new Error('Active strategy does not support pins');
|
|
627
|
+
}
|
|
628
|
+
return this.strategy.pinRange(firstMessageId, lastMessageId, opts);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Mark a single message as a "document" (semantically a body of
|
|
633
|
+
* information to retain in full). Same effect as a single-message pin
|
|
634
|
+
* with `kind: 'document'`. Returns the new pin id.
|
|
635
|
+
*/
|
|
636
|
+
markDocument(messageId: MessageId, opts?: { name?: string }): string {
|
|
637
|
+
if (!isPinnableStrategy(this.strategy)) {
|
|
638
|
+
throw new Error('Active strategy does not support documents');
|
|
639
|
+
}
|
|
640
|
+
return this.strategy.markDocument(messageId, opts);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Remove a pin or document mark. Returns true if removed. */
|
|
644
|
+
unpin(pinId: string): boolean {
|
|
645
|
+
if (!isPinnableStrategy(this.strategy)) {
|
|
646
|
+
throw new Error('Active strategy does not support pins');
|
|
647
|
+
}
|
|
648
|
+
return this.strategy.unpin(pinId);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** List all current pins. Returns empty array if strategy is not pinnable. */
|
|
652
|
+
listPins(): ReadonlyArray<ProtectedRange> {
|
|
653
|
+
if (!isPinnableStrategy(this.strategy)) return [];
|
|
654
|
+
return this.strategy.listPins();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ==========================================================================
|
|
658
|
+
// Search (passthrough to the active strategy)
|
|
659
|
+
// ==========================================================================
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Search the strategy's summary archive (substring or regex over content).
|
|
663
|
+
* Returns empty array if the strategy doesn't support search.
|
|
664
|
+
*
|
|
665
|
+
* Suitable for building memory-search agent tools at the framework layer
|
|
666
|
+
* — see e.g. agent-framework's MCPL host integration.
|
|
667
|
+
*/
|
|
668
|
+
searchSummaries(query: SearchQuery): SearchResult[] {
|
|
669
|
+
if (!isSearchableStrategy(this.strategy)) return [];
|
|
670
|
+
return this.strategy.searchSummaries(query);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Look up a single summary by id. Returns null if not found / unsupported. */
|
|
674
|
+
getSummary(id: string): SummaryEntry | null {
|
|
675
|
+
if (!isSearchableStrategy(this.strategy)) return null;
|
|
676
|
+
return this.strategy.getSummary(id);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Per-render stats from the active strategy: head/tail message + token
|
|
681
|
+
* counts, plus per-level summary counts and total tokens. Returns null
|
|
682
|
+
* if the strategy doesn't implement `getRenderStats`.
|
|
683
|
+
*
|
|
684
|
+
* Designed for TUIs / dashboards that want to display "how much of the
|
|
685
|
+
* agent's context is folded vs raw" at a glance.
|
|
686
|
+
*/
|
|
687
|
+
getRenderStats(): RenderStats | null {
|
|
688
|
+
if (!isRenderStatsCapable(this.strategy)) return null;
|
|
689
|
+
return this.strategy.getRenderStats(this.messageStore.createView());
|
|
690
|
+
}
|
|
691
|
+
|
|
530
692
|
/**
|
|
531
693
|
* Reset the head window to start from a new position.
|
|
532
694
|
* Old head window messages become compressible.
|
|
@@ -583,6 +745,8 @@ export class ContextManager {
|
|
|
583
745
|
contextLog: this.contextLog.createView(),
|
|
584
746
|
membrane: this.membrane,
|
|
585
747
|
currentSequence: this.store.currentSequence(),
|
|
748
|
+
store: this.store,
|
|
749
|
+
namespace: this.strategyNamespace,
|
|
586
750
|
};
|
|
587
751
|
}
|
|
588
752
|
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { BlobManager } from './blob-manager.js';
|
|
|
10
10
|
|
|
11
11
|
// Strategies
|
|
12
12
|
export { PassthroughStrategy } from './strategies/passthrough.js';
|
|
13
|
-
export { AutobiographicalStrategy } from './strategies/autobiographical.js';
|
|
13
|
+
export { AutobiographicalStrategy, type AutobiographicalProgressSnapshot } from './strategies/autobiographical.js';
|
|
14
14
|
export { KnowledgeStrategy } from './strategies/knowledge.js';
|
|
15
15
|
|
|
16
16
|
// Types
|
package/src/message-store.ts
CHANGED
|
@@ -101,14 +101,31 @@ export class MessageStore {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Optional extra fields for `append`, used by callers that need to set
|
|
106
|
+
* adaptive-resolution metadata (bodyGroupId for shards, initial
|
|
107
|
+
* resolution state, etc.) at ingestion time.
|
|
108
|
+
*/
|
|
109
|
+
static readonly _appendExtraKeys = ['bodyGroupId', 'shardIndex', 'currentResolution', 'lockedByAgent'] as const;
|
|
110
|
+
|
|
104
111
|
/**
|
|
105
112
|
* Append a new message to the store.
|
|
113
|
+
*
|
|
114
|
+
* `extra` is an optional bag of adaptive-resolution metadata
|
|
115
|
+
* (bodyGroupId / shardIndex / currentResolution / lockedByAgent) that
|
|
116
|
+
* callers may set at ingestion. Field semantics match StoredMessage.
|
|
106
117
|
*/
|
|
107
118
|
append(
|
|
108
119
|
participant: string,
|
|
109
120
|
content: ContentBlock[],
|
|
110
121
|
metadata?: MessageMetadata,
|
|
111
|
-
causedBy?: MessageId[]
|
|
122
|
+
causedBy?: MessageId[],
|
|
123
|
+
extra?: {
|
|
124
|
+
bodyGroupId?: string;
|
|
125
|
+
shardIndex?: number;
|
|
126
|
+
currentResolution?: number;
|
|
127
|
+
lockedByAgent?: boolean;
|
|
128
|
+
}
|
|
112
129
|
): StoredMessage {
|
|
113
130
|
// Extract blobs from content
|
|
114
131
|
const storedContent = this.blobManager.extractBlobs(content);
|
|
@@ -120,28 +137,54 @@ export class MessageStore {
|
|
|
120
137
|
metadata,
|
|
121
138
|
timestamp: Date.now(),
|
|
122
139
|
causedBy,
|
|
140
|
+
...(extra ?? {}),
|
|
123
141
|
};
|
|
124
142
|
|
|
125
143
|
const record = this.store.appendToStateJson(this.stateId, partialInternal);
|
|
126
144
|
const index = this.length() - 1;
|
|
127
145
|
|
|
128
|
-
//
|
|
146
|
+
// The append's payload doesn't carry the chronicle record's id /
|
|
147
|
+
// sequence — those are only known once the append returns. We patch
|
|
148
|
+
// them in via editStateItem below. The patch creates a new chronicle
|
|
149
|
+
// record at `record.sequence + 1` (the next sequence; nothing else
|
|
150
|
+
// writes between the append and the edit inside this function).
|
|
151
|
+
//
|
|
152
|
+
// We store the EDIT'S sequence (not the original append's) as the
|
|
153
|
+
// canonical "this message is fully readable at and after this seq"
|
|
154
|
+
// marker. Why: `ContextManager.branchAt(messageId)` forwards
|
|
155
|
+
// `message.sequence` to `chronicle.createBranchAt`, which forks
|
|
156
|
+
// visibility at that exact seq. If `sequence` pointed at the original
|
|
157
|
+
// append (record.sequence), the new branch would see the pre-patch
|
|
158
|
+
// payload (id/sequence undefined). Pointing at the edit's seq
|
|
159
|
+
// includes the patched payload in the inherited records — so a
|
|
160
|
+
// forked branch sees this message in its fully-established form.
|
|
161
|
+
//
|
|
162
|
+
// Invariant: this function does exactly ONE chronicle write between
|
|
163
|
+
// the append and the edit (the edit itself). If that ever changes,
|
|
164
|
+
// the +1 expression must be updated to match (or refactored to
|
|
165
|
+
// capture editStateItem's returned record.sequence — which is
|
|
166
|
+
// chicken-and-egg here since we need the value inside the payload
|
|
167
|
+
// we're writing).
|
|
168
|
+
const commitSequence = record.sequence + 1;
|
|
129
169
|
const fullInternal: StoredMessageInternal = {
|
|
130
170
|
id: record.id,
|
|
131
|
-
sequence:
|
|
171
|
+
sequence: commitSequence,
|
|
132
172
|
...partialInternal,
|
|
133
173
|
};
|
|
134
174
|
this.store.editStateItem(this.stateId, index, Buffer.from(JSON.stringify(fullInternal)));
|
|
135
175
|
|
|
136
|
-
// Build full message with ID and sequence from record
|
|
176
|
+
// Build full message with ID and sequence from record. The sequence
|
|
177
|
+
// we expose is the commit sequence (matches what's stored), so
|
|
178
|
+
// `branchAt(messageId)` forks at the right place for any caller.
|
|
137
179
|
const message: StoredMessage = {
|
|
138
180
|
id: record.id,
|
|
139
|
-
sequence:
|
|
181
|
+
sequence: commitSequence,
|
|
140
182
|
participant,
|
|
141
183
|
content, // Original content with inline data
|
|
142
184
|
metadata,
|
|
143
185
|
timestamp: new Date(partialInternal.timestamp),
|
|
144
186
|
causedBy,
|
|
187
|
+
...(extra ?? {}),
|
|
145
188
|
};
|
|
146
189
|
|
|
147
190
|
// Update index
|
|
@@ -153,6 +196,11 @@ export class MessageStore {
|
|
|
153
196
|
|
|
154
197
|
/**
|
|
155
198
|
* Edit a message's content.
|
|
199
|
+
*
|
|
200
|
+
* Throws if `messageId` belongs to a bodyGroup (i.e., is a shard of a
|
|
201
|
+
* larger sharded message). Editing one shard would silently corrupt the
|
|
202
|
+
* bodyGroup's byte-faithful reassembly invariant. To replace a sharded
|
|
203
|
+
* message, remove the whole bodyGroup and re-append.
|
|
156
204
|
*/
|
|
157
205
|
edit(messageId: MessageId, newContent: ContentBlock[]): void {
|
|
158
206
|
const index = this.idToIndex.get(messageId);
|
|
@@ -165,6 +213,13 @@ export class MessageStore {
|
|
|
165
213
|
throw new Error(`Message not found at index: ${index}`);
|
|
166
214
|
}
|
|
167
215
|
|
|
216
|
+
if (oldMessage.bodyGroupId) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Cannot edit shard ${messageId}: it is part of bodyGroup ${oldMessage.bodyGroupId}. ` +
|
|
219
|
+
`Sharded messages are immutable — remove the whole bodyGroup and re-append instead.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
168
223
|
const oldContent = this.blobManager.resolveBlobs(oldMessage.content);
|
|
169
224
|
const storedContent = this.blobManager.extractBlobs(newContent);
|
|
170
225
|
|
|
@@ -181,6 +236,11 @@ export class MessageStore {
|
|
|
181
236
|
|
|
182
237
|
/**
|
|
183
238
|
* Remove a message from the store.
|
|
239
|
+
*
|
|
240
|
+
* For a sharded message (one shard of a bodyGroup), the caller MUST
|
|
241
|
+
* remove all shards in the group together — removing one shard would
|
|
242
|
+
* orphan the rest and break byte-faithful reassembly. Use
|
|
243
|
+
* `removeBodyGroup(id)` for that case.
|
|
184
244
|
*/
|
|
185
245
|
remove(messageId: MessageId): void {
|
|
186
246
|
const index = this.idToIndex.get(messageId);
|
|
@@ -188,14 +248,61 @@ export class MessageStore {
|
|
|
188
248
|
throw new Error(`Message not found: ${messageId}`);
|
|
189
249
|
}
|
|
190
250
|
|
|
251
|
+
const target = this.getInternal(index);
|
|
252
|
+
if (target?.bodyGroupId) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Cannot remove shard ${messageId} in isolation: it is part of bodyGroup ${target.bodyGroupId}. ` +
|
|
255
|
+
`Use removeBodyGroup(${messageId}) to remove all shards atomically.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
191
259
|
this.store.redactStateItems(this.stateId, index, index + 1);
|
|
192
260
|
this.rebuildIndex();
|
|
193
261
|
|
|
194
262
|
this.emit({ type: 'remove', messageId });
|
|
195
263
|
}
|
|
196
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Remove every shard of the bodyGroup containing `messageId`. If the
|
|
267
|
+
* message is not part of a bodyGroup, falls back to single-message
|
|
268
|
+
* remove. Atomic from the caller's perspective.
|
|
269
|
+
*/
|
|
270
|
+
removeBodyGroup(messageId: MessageId): void {
|
|
271
|
+
const index = this.idToIndex.get(messageId);
|
|
272
|
+
if (index === undefined) {
|
|
273
|
+
throw new Error(`Message not found: ${messageId}`);
|
|
274
|
+
}
|
|
275
|
+
const target = this.getInternal(index);
|
|
276
|
+
if (!target?.bodyGroupId) {
|
|
277
|
+
// Not sharded — defer to normal remove path (re-look up via getInternal
|
|
278
|
+
// since the normal `remove` checks bodyGroupId).
|
|
279
|
+
this.store.redactStateItems(this.stateId, index, index + 1);
|
|
280
|
+
this.rebuildIndex();
|
|
281
|
+
this.emit({ type: 'remove', messageId });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Find the contiguous run of shards with this bodyGroupId. Shards are
|
|
285
|
+
// stored consecutively (they're appended one after the other at
|
|
286
|
+
// ingestion), so we can scan outward from `index`.
|
|
287
|
+
const groupId = target.bodyGroupId;
|
|
288
|
+
const all = this.getAllInternal();
|
|
289
|
+
let from = index;
|
|
290
|
+
while (from > 0 && all[from - 1].bodyGroupId === groupId) from--;
|
|
291
|
+
let to = index;
|
|
292
|
+
while (to + 1 < all.length && all[to + 1].bodyGroupId === groupId) to++;
|
|
293
|
+
const firstId = all[from].id;
|
|
294
|
+
const lastId = all[to].id;
|
|
295
|
+
this.store.redactStateItems(this.stateId, from, to + 1);
|
|
296
|
+
this.rebuildIndex();
|
|
297
|
+
this.emit({ type: 'removeRange', fromId: firstId, toId: lastId });
|
|
298
|
+
}
|
|
299
|
+
|
|
197
300
|
/**
|
|
198
301
|
* Remove a range of messages from the store.
|
|
302
|
+
*
|
|
303
|
+
* If the range starts or ends in the middle of a bodyGroup, throws —
|
|
304
|
+
* removeRange must align to bodyGroup boundaries. (Use `removeBodyGroup`
|
|
305
|
+
* to remove an entire group, then call `removeRange` over plain messages.)
|
|
199
306
|
*/
|
|
200
307
|
removeRange(fromId: MessageId, toId: MessageId): void {
|
|
201
308
|
const fromIndex = this.idToIndex.get(fromId);
|
|
@@ -208,6 +315,21 @@ export class MessageStore {
|
|
|
208
315
|
throw new Error(`Message not found: ${toId}`);
|
|
209
316
|
}
|
|
210
317
|
|
|
318
|
+
// Verify the range doesn't bisect a bodyGroup.
|
|
319
|
+
const all = this.getAllInternal();
|
|
320
|
+
const startGroup = all[fromIndex].bodyGroupId;
|
|
321
|
+
const endGroup = all[toIndex].bodyGroupId;
|
|
322
|
+
if (startGroup && fromIndex > 0 && all[fromIndex - 1].bodyGroupId === startGroup) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`removeRange would bisect bodyGroup ${startGroup} at start. Use removeBodyGroup(${fromId}) first.`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (endGroup && toIndex + 1 < all.length && all[toIndex + 1].bodyGroupId === endGroup) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`removeRange would bisect bodyGroup ${endGroup} at end. Use removeBodyGroup(${toId}) first.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
211
333
|
this.store.redactStateItems(this.stateId, fromIndex, toIndex + 1);
|
|
212
334
|
this.rebuildIndex();
|
|
213
335
|
|
|
@@ -457,17 +579,34 @@ export class MessageStore {
|
|
|
457
579
|
private internalToStored(
|
|
458
580
|
internal: StoredMessageInternal,
|
|
459
581
|
id: MessageId,
|
|
460
|
-
|
|
582
|
+
_index: number,
|
|
461
583
|
): StoredMessage {
|
|
462
|
-
|
|
584
|
+
const stored: StoredMessage = {
|
|
463
585
|
id,
|
|
464
|
-
|
|
586
|
+
// chronicle record sequence captured when the message was appended
|
|
587
|
+
// (see `add()` line 131: `sequence: record.sequence`). The previous
|
|
588
|
+
// implementation returned the slot index ("// Use index as sequence
|
|
589
|
+
// for now"), which silently corrupted any downstream code that
|
|
590
|
+
// forwarded this number to chronicle APIs expecting a real sequence
|
|
591
|
+
// — most notably `ContextManager.branchAt`, which would fork the
|
|
592
|
+
// chronicle at the index-mistaken-for-sequence and lose every
|
|
593
|
+
// record between the intended fork point and the actual one. For
|
|
594
|
+
// typical autobio sessions the index-vs-sequence ratio is ~1:2
|
|
595
|
+
// (each message append is accompanied by ~1 autobio state update),
|
|
596
|
+
// so /undo of a 800-message conversation forked ~400 messages back.
|
|
597
|
+
sequence: internal.sequence,
|
|
465
598
|
participant: internal.participant,
|
|
466
599
|
content: this.blobManager.resolveBlobs(internal.content),
|
|
467
600
|
metadata: internal.metadata,
|
|
468
601
|
timestamp: new Date(internal.timestamp),
|
|
469
602
|
causedBy: internal.causedBy,
|
|
470
603
|
};
|
|
604
|
+
// Carry adaptive-resolution fields through unchanged.
|
|
605
|
+
if (internal.bodyGroupId !== undefined) stored.bodyGroupId = internal.bodyGroupId;
|
|
606
|
+
if (internal.shardIndex !== undefined) stored.shardIndex = internal.shardIndex;
|
|
607
|
+
if (internal.currentResolution !== undefined) stored.currentResolution = internal.currentResolution;
|
|
608
|
+
if (internal.lockedByAgent !== undefined) stored.lockedByAgent = internal.lockedByAgent;
|
|
609
|
+
return stored;
|
|
471
610
|
}
|
|
472
611
|
}
|
|
473
612
|
|