@animalabs/context-manager 0.1.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 (83) hide show
  1. package/dist/src/blob-manager.d.ts +37 -0
  2. package/dist/src/blob-manager.d.ts.map +1 -0
  3. package/dist/src/blob-manager.js +128 -0
  4. package/dist/src/blob-manager.js.map +1 -0
  5. package/dist/src/context-log.d.ts +99 -0
  6. package/dist/src/context-log.d.ts.map +1 -0
  7. package/dist/src/context-log.js +277 -0
  8. package/dist/src/context-log.js.map +1 -0
  9. package/dist/src/context-manager.d.ts +245 -0
  10. package/dist/src/context-manager.d.ts.map +1 -0
  11. package/dist/src/context-manager.js +553 -0
  12. package/dist/src/context-manager.js.map +1 -0
  13. package/dist/src/index.d.ts +12 -0
  14. package/dist/src/index.d.ts.map +1 -0
  15. package/dist/src/index.js +12 -0
  16. package/dist/src/index.js.map +1 -0
  17. package/dist/src/message-store.d.ts +135 -0
  18. package/dist/src/message-store.d.ts.map +1 -0
  19. package/dist/src/message-store.js +372 -0
  20. package/dist/src/message-store.js.map +1 -0
  21. package/dist/src/strategies/autobiographical.d.ts +199 -0
  22. package/dist/src/strategies/autobiographical.d.ts.map +1 -0
  23. package/dist/src/strategies/autobiographical.js +1122 -0
  24. package/dist/src/strategies/autobiographical.js.map +1 -0
  25. package/dist/src/strategies/index.d.ts +3 -0
  26. package/dist/src/strategies/index.d.ts.map +1 -0
  27. package/dist/src/strategies/index.js +3 -0
  28. package/dist/src/strategies/index.js.map +1 -0
  29. package/dist/src/strategies/knowledge.d.ts +46 -0
  30. package/dist/src/strategies/knowledge.d.ts.map +1 -0
  31. package/dist/src/strategies/knowledge.js +270 -0
  32. package/dist/src/strategies/knowledge.js.map +1 -0
  33. package/dist/src/strategies/passthrough.d.ts +17 -0
  34. package/dist/src/strategies/passthrough.d.ts.map +1 -0
  35. package/dist/src/strategies/passthrough.js +69 -0
  36. package/dist/src/strategies/passthrough.js.map +1 -0
  37. package/dist/src/types/context.d.ts +108 -0
  38. package/dist/src/types/context.d.ts.map +1 -0
  39. package/dist/src/types/context.js +2 -0
  40. package/dist/src/types/context.js.map +1 -0
  41. package/dist/src/types/index.d.ts +5 -0
  42. package/dist/src/types/index.d.ts.map +1 -0
  43. package/dist/src/types/index.js +2 -0
  44. package/dist/src/types/index.js.map +1 -0
  45. package/dist/src/types/message.d.ts +129 -0
  46. package/dist/src/types/message.d.ts.map +1 -0
  47. package/dist/src/types/message.js +2 -0
  48. package/dist/src/types/message.js.map +1 -0
  49. package/dist/src/types/strategy.d.ts +233 -0
  50. package/dist/src/types/strategy.d.ts.map +1 -0
  51. package/dist/src/types/strategy.js +32 -0
  52. package/dist/src/types/strategy.js.map +1 -0
  53. package/dist/test/autobiographical.test.d.ts +2 -0
  54. package/dist/test/autobiographical.test.d.ts.map +1 -0
  55. package/dist/test/autobiographical.test.js +46 -0
  56. package/dist/test/autobiographical.test.js.map +1 -0
  57. package/dist/test/head-window-reset.test.d.ts +17 -0
  58. package/dist/test/head-window-reset.test.d.ts.map +1 -0
  59. package/dist/test/head-window-reset.test.js +342 -0
  60. package/dist/test/head-window-reset.test.js.map +1 -0
  61. package/dist/test/integration.test.d.ts +2 -0
  62. package/dist/test/integration.test.d.ts.map +1 -0
  63. package/dist/test/integration.test.js +1341 -0
  64. package/dist/test/integration.test.js.map +1 -0
  65. package/dist/test/knowledge.test.d.ts +2 -0
  66. package/dist/test/knowledge.test.d.ts.map +1 -0
  67. package/dist/test/knowledge.test.js +617 -0
  68. package/dist/test/knowledge.test.js.map +1 -0
  69. package/dist/tsconfig.tsbuildinfo +1 -0
  70. package/package.json +48 -0
  71. package/src/blob-manager.ts +155 -0
  72. package/src/context-log.ts +342 -0
  73. package/src/context-manager.ts +726 -0
  74. package/src/index.ts +50 -0
  75. package/src/message-store.ts +479 -0
  76. package/src/strategies/autobiographical.ts +1355 -0
  77. package/src/strategies/index.ts +2 -0
  78. package/src/strategies/knowledge.ts +336 -0
  79. package/src/strategies/passthrough.ts +98 -0
  80. package/src/types/context.ts +119 -0
  81. package/src/types/index.ts +42 -0
  82. package/src/types/message.ts +140 -0
  83. package/src/types/strategy.ts +282 -0
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ // Main class
2
+ export { ContextManager } from './context-manager.js';
3
+ export type { ContextManagerConfig } from './context-manager.js';
4
+
5
+ // Storage
6
+ export { MessageStore } from './message-store.js';
7
+ export type { MessageStoreEvent, MessageStoreListener } from './message-store.js';
8
+ export { ContextLog } from './context-log.js';
9
+ export { BlobManager } from './blob-manager.js';
10
+
11
+ // Strategies
12
+ export { PassthroughStrategy } from './strategies/passthrough.js';
13
+ export { AutobiographicalStrategy } from './strategies/autobiographical.js';
14
+ export { KnowledgeStrategy } from './strategies/knowledge.js';
15
+
16
+ // Types
17
+ export type {
18
+ // Message types
19
+ MessageId,
20
+ Sequence,
21
+ BranchId,
22
+ MessageMetadata,
23
+ StoredMessage,
24
+ BlobReference,
25
+ StoredContentBlock,
26
+ MessageQuery,
27
+ MessageQueryResult,
28
+ // Context types
29
+ SourceRelation,
30
+ ContextEntry,
31
+ TokenBudget,
32
+ PendingWork,
33
+ BranchInfo,
34
+ ContextInjection,
35
+ CompileResult,
36
+ // Strategy types
37
+ MessageStoreView,
38
+ ContextLogView,
39
+ StrategyContext,
40
+ ReadinessState,
41
+ ContextStrategy,
42
+ AutobiographicalConfig,
43
+ SummaryLevel,
44
+ SummaryEntry,
45
+ PhaseType,
46
+ KnowledgeConfig,
47
+ ResettableStrategy,
48
+ } from './types/index.js';
49
+
50
+ export { DEFAULT_AUTOBIOGRAPHICAL_CONFIG, isResettableStrategy } from './types/index.js';
@@ -0,0 +1,479 @@
1
+ import type { JsStore } from '@animalabs/chronicle';
2
+ import type { ContentBlock } from '@animalabs/membrane';
3
+ import type {
4
+ MessageId,
5
+ Sequence,
6
+ MessageMetadata,
7
+ StoredMessage,
8
+ StoredMessageInternal,
9
+ MessageStoreView,
10
+ MessageQuery,
11
+ MessageQueryResult,
12
+ } from './types/index.js';
13
+ import { BlobManager } from './blob-manager.js';
14
+
15
+ const DEFAULT_MESSAGE_STATE_ID = 'messages';
16
+
17
+ /**
18
+ * Event emitted when the message store changes.
19
+ */
20
+ export type MessageStoreEvent =
21
+ | { type: 'add'; message: StoredMessage }
22
+ | { type: 'edit'; messageId: MessageId; oldContent: ContentBlock[]; newContent: ContentBlock[] }
23
+ | { type: 'remove'; messageId: MessageId }
24
+ | { type: 'removeRange'; fromId: MessageId; toId: MessageId };
25
+
26
+ /**
27
+ * Listener for message store events.
28
+ */
29
+ export type MessageStoreListener = (event: MessageStoreEvent) => void;
30
+
31
+ /**
32
+ * Options for token estimation.
33
+ */
34
+ export interface TokenEstimatorOptions {
35
+ /** Custom token estimator function */
36
+ estimator?: (text: string) => number;
37
+ }
38
+
39
+ /**
40
+ * Wrapper around Chronicle append_log state for message storage.
41
+ * Handles blob extraction and provides a clean interface for message operations.
42
+ */
43
+ export class MessageStore {
44
+ private blobManager: BlobManager;
45
+ private listeners: Set<MessageStoreListener> = new Set();
46
+ private idToIndex: Map<MessageId, number> = new Map();
47
+ private tokenEstimator: (text: string) => number;
48
+ private stateId: string;
49
+
50
+ constructor(
51
+ private store: JsStore,
52
+ options: TokenEstimatorOptions & {
53
+ /** Namespace for multi-agent support. Creates state ID: `{namespace}/messages` */
54
+ namespace?: string;
55
+ } = {}
56
+ ) {
57
+ this.stateId = options.namespace
58
+ ? `${options.namespace}/messages`
59
+ : DEFAULT_MESSAGE_STATE_ID;
60
+ this.blobManager = new BlobManager(store);
61
+ this.tokenEstimator = options.estimator ?? defaultTokenEstimator;
62
+ this.rebuildIndex();
63
+ }
64
+
65
+ /**
66
+ * Register the message store state in Chronicle.
67
+ * Should be called once when setting up the store.
68
+ *
69
+ * @param store The Chronicle store
70
+ * @param namespace Optional namespace for multi-agent support
71
+ */
72
+ static register(store: JsStore, namespace?: string): void {
73
+ const stateId = namespace ? `${namespace}/messages` : DEFAULT_MESSAGE_STATE_ID;
74
+ store.registerState({
75
+ id: stateId,
76
+ strategy: 'append_log',
77
+ deltaSnapshotEvery: 50,
78
+ fullSnapshotEvery: 10,
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Add a listener for store events.
84
+ */
85
+ addListener(listener: MessageStoreListener): () => void {
86
+ this.listeners.add(listener);
87
+ return () => this.listeners.delete(listener);
88
+ }
89
+
90
+ private emit(event: MessageStoreEvent): void {
91
+ for (const listener of this.listeners) {
92
+ listener(event);
93
+ }
94
+ }
95
+
96
+ private rebuildIndex(): void {
97
+ this.idToIndex.clear();
98
+ const messages = this.getAllInternal();
99
+ for (let i = 0; i < messages.length; i++) {
100
+ this.idToIndex.set(messages[i].id, i);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Append a new message to the store.
106
+ */
107
+ append(
108
+ participant: string,
109
+ content: ContentBlock[],
110
+ metadata?: MessageMetadata,
111
+ causedBy?: MessageId[]
112
+ ): StoredMessage {
113
+ // Extract blobs from content
114
+ const storedContent = this.blobManager.extractBlobs(content);
115
+
116
+ // Append to Chronicle first to get the record ID
117
+ const partialInternal = {
118
+ participant,
119
+ content: storedContent,
120
+ metadata,
121
+ timestamp: Date.now(),
122
+ causedBy,
123
+ };
124
+
125
+ const record = this.store.appendToStateJson(this.stateId, partialInternal);
126
+ const index = this.length() - 1;
127
+
128
+ // Now update the stored item to include the id (for rebuildIndex)
129
+ const fullInternal: StoredMessageInternal = {
130
+ id: record.id,
131
+ sequence: record.sequence,
132
+ ...partialInternal,
133
+ };
134
+ this.store.editStateItem(this.stateId, index, Buffer.from(JSON.stringify(fullInternal)));
135
+
136
+ // Build full message with ID and sequence from record
137
+ const message: StoredMessage = {
138
+ id: record.id,
139
+ sequence: record.sequence,
140
+ participant,
141
+ content, // Original content with inline data
142
+ metadata,
143
+ timestamp: new Date(partialInternal.timestamp),
144
+ causedBy,
145
+ };
146
+
147
+ // Update index
148
+ this.idToIndex.set(message.id, index);
149
+
150
+ this.emit({ type: 'add', message });
151
+ return message;
152
+ }
153
+
154
+ /**
155
+ * Edit a message's content.
156
+ */
157
+ edit(messageId: MessageId, newContent: ContentBlock[]): void {
158
+ const index = this.idToIndex.get(messageId);
159
+ if (index === undefined) {
160
+ throw new Error(`Message not found: ${messageId}`);
161
+ }
162
+
163
+ const oldMessage = this.getInternal(index);
164
+ if (!oldMessage) {
165
+ throw new Error(`Message not found at index: ${index}`);
166
+ }
167
+
168
+ const oldContent = this.blobManager.resolveBlobs(oldMessage.content);
169
+ const storedContent = this.blobManager.extractBlobs(newContent);
170
+
171
+ // Update the stored message
172
+ const updated: StoredMessageInternal = {
173
+ ...oldMessage,
174
+ content: storedContent,
175
+ };
176
+
177
+ this.store.editStateItem(this.stateId, index, Buffer.from(JSON.stringify(updated)));
178
+
179
+ this.emit({ type: 'edit', messageId, oldContent, newContent });
180
+ }
181
+
182
+ /**
183
+ * Remove a message from the store.
184
+ */
185
+ remove(messageId: MessageId): void {
186
+ const index = this.idToIndex.get(messageId);
187
+ if (index === undefined) {
188
+ throw new Error(`Message not found: ${messageId}`);
189
+ }
190
+
191
+ this.store.redactStateItems(this.stateId, index, index + 1);
192
+ this.rebuildIndex();
193
+
194
+ this.emit({ type: 'remove', messageId });
195
+ }
196
+
197
+ /**
198
+ * Remove a range of messages from the store.
199
+ */
200
+ removeRange(fromId: MessageId, toId: MessageId): void {
201
+ const fromIndex = this.idToIndex.get(fromId);
202
+ const toIndex = this.idToIndex.get(toId);
203
+
204
+ if (fromIndex === undefined) {
205
+ throw new Error(`Message not found: ${fromId}`);
206
+ }
207
+ if (toIndex === undefined) {
208
+ throw new Error(`Message not found: ${toId}`);
209
+ }
210
+
211
+ this.store.redactStateItems(this.stateId, fromIndex, toIndex + 1);
212
+ this.rebuildIndex();
213
+
214
+ this.emit({ type: 'removeRange', fromId, toId });
215
+ }
216
+
217
+ /**
218
+ * Get a message by ID.
219
+ */
220
+ get(messageId: MessageId): StoredMessage | null {
221
+ const index = this.idToIndex.get(messageId);
222
+ if (index === undefined) {
223
+ return null;
224
+ }
225
+
226
+ const internal = this.getInternal(index);
227
+ if (!internal) {
228
+ return null;
229
+ }
230
+
231
+ return this.internalToStored(internal, messageId, index);
232
+ }
233
+
234
+ /**
235
+ * Get a message as it was at a specific sequence (time travel).
236
+ */
237
+ getAt(messageId: MessageId, atSequence: Sequence): StoredMessage | null {
238
+ // Get historical state
239
+ const historicalState = this.store.getStateJsonAt(this.stateId, atSequence);
240
+ if (!historicalState || !Array.isArray(historicalState)) {
241
+ return null;
242
+ }
243
+
244
+ // Find the message in historical state
245
+ for (let i = 0; i < historicalState.length; i++) {
246
+ const internal = historicalState[i] as StoredMessageInternal;
247
+ if (internal.id === messageId) {
248
+ return this.internalToStored(internal, messageId, i);
249
+ }
250
+ }
251
+
252
+ return null;
253
+ }
254
+
255
+ /**
256
+ * Get all messages.
257
+ */
258
+ getAll(): StoredMessage[] {
259
+ const internals = this.getAllInternal();
260
+ return internals.map((internal, i) =>
261
+ this.internalToStored(internal, internal.id, i)
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Get messages from a specific index.
267
+ */
268
+ getFrom(index: number): StoredMessage[] {
269
+ return this.getAll().slice(index);
270
+ }
271
+
272
+ /**
273
+ * Get the last N messages.
274
+ */
275
+ getTail(count: number): StoredMessage[] {
276
+ const all = this.getAll();
277
+ return all.slice(Math.max(0, all.length - count));
278
+ }
279
+
280
+ /**
281
+ * Get the total number of messages.
282
+ */
283
+ length(): number {
284
+ return this.store.getStateLen(this.stateId) ?? 0;
285
+ }
286
+
287
+ /**
288
+ * Estimate tokens for a message.
289
+ */
290
+ estimateTokens(message: StoredMessage): number {
291
+ let tokens = 0;
292
+ for (const block of message.content) {
293
+ tokens += this.estimateBlockTokens(block);
294
+ }
295
+ return tokens;
296
+ }
297
+
298
+ private estimateBlockTokens(block: ContentBlock): number {
299
+ switch (block.type) {
300
+ case 'text':
301
+ return this.tokenEstimator(block.text);
302
+ case 'thinking':
303
+ return this.tokenEstimator(block.thinking);
304
+ case 'tool_use':
305
+ return this.tokenEstimator(JSON.stringify(block.input)) + 20; // overhead for name, id
306
+ case 'tool_result':
307
+ if (!block.content) return 0;
308
+ if (typeof block.content === 'string') {
309
+ return this.tokenEstimator(block.content);
310
+ }
311
+ if (Array.isArray(block.content)) {
312
+ return block.content.reduce((sum, b) => sum + this.estimateBlockTokens(b), 0);
313
+ }
314
+ return 0;
315
+ case 'image':
316
+ return block.tokenEstimate ?? 1000; // Default estimate for images
317
+ case 'document':
318
+ case 'audio':
319
+ case 'video':
320
+ return 1000; // Default estimate for media
321
+ default:
322
+ return 0;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Create a read-only view of the store for strategies.
328
+ */
329
+ createView(): MessageStoreView {
330
+ return {
331
+ getAll: () => this.getAll(),
332
+ get: (id) => this.get(id),
333
+ getFrom: (index) => this.getFrom(index),
334
+ getTail: (count) => this.getTail(count),
335
+ length: () => this.length(),
336
+ estimateTokens: (msg) => this.estimateTokens(msg),
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Query messages by filter criteria.
342
+ * Useful for finding messages from external sources, by participant, etc.
343
+ */
344
+ query(filter: MessageQuery): MessageQueryResult {
345
+ let messages = this.getAll();
346
+ let totalCount = 0;
347
+
348
+ // Apply filters
349
+ const filtered: StoredMessage[] = [];
350
+ for (const msg of messages) {
351
+ if (this.matchesFilter(msg, filter)) {
352
+ filtered.push(msg);
353
+ }
354
+ }
355
+
356
+ totalCount = filtered.length;
357
+
358
+ // Apply reverse if requested
359
+ let result = filter.reverse ? filtered.reverse() : filtered;
360
+
361
+ // Apply limit if specified
362
+ if (filter.limit !== undefined && filter.limit < result.length) {
363
+ result = result.slice(0, filter.limit);
364
+ }
365
+
366
+ return { messages: result, totalCount };
367
+ }
368
+
369
+ /**
370
+ * Find a message by external source and ID.
371
+ * Convenience method for common lookup pattern.
372
+ */
373
+ findByExternalId(source: string, externalId: string): StoredMessage | null {
374
+ const result = this.query({
375
+ source,
376
+ externalIds: [externalId],
377
+ limit: 1,
378
+ });
379
+ return result.messages[0] ?? null;
380
+ }
381
+
382
+ /**
383
+ * Check if a message matches the query filter.
384
+ */
385
+ private matchesFilter(msg: StoredMessage, filter: MessageQuery): boolean {
386
+ // Filter by source
387
+ if (filter.source !== undefined) {
388
+ const external = msg.metadata?.external as { source?: string } | undefined;
389
+ if (external?.source !== filter.source) {
390
+ return false;
391
+ }
392
+ }
393
+
394
+ // Filter by external IDs
395
+ if (filter.externalIds !== undefined && filter.externalIds.length > 0) {
396
+ const external = msg.metadata?.external as { id?: string } | undefined;
397
+ if (!external?.id || !filter.externalIds.includes(external.id)) {
398
+ return false;
399
+ }
400
+ }
401
+
402
+ // Filter by participant
403
+ if (filter.participant !== undefined) {
404
+ if (msg.participant !== filter.participant) {
405
+ return false;
406
+ }
407
+ }
408
+
409
+ // Filter by metadata fields
410
+ if (filter.metadata !== undefined) {
411
+ for (const [key, value] of Object.entries(filter.metadata)) {
412
+ const actual = this.getNestedValue(msg.metadata, key);
413
+ if (actual !== value) {
414
+ return false;
415
+ }
416
+ }
417
+ }
418
+
419
+ return true;
420
+ }
421
+
422
+ /**
423
+ * Get a nested value from an object using dot notation.
424
+ * e.g., getNestedValue(obj, 'external.channelId')
425
+ */
426
+ private getNestedValue(obj: unknown, path: string): unknown {
427
+ if (obj === undefined || obj === null) {
428
+ return undefined;
429
+ }
430
+
431
+ const parts = path.split('.');
432
+ let current: unknown = obj;
433
+
434
+ for (const part of parts) {
435
+ if (current === undefined || current === null || typeof current !== 'object') {
436
+ return undefined;
437
+ }
438
+ current = (current as Record<string, unknown>)[part];
439
+ }
440
+
441
+ return current;
442
+ }
443
+
444
+ private getAllInternal(): StoredMessageInternal[] {
445
+ const state = this.store.getStateJson(this.stateId);
446
+ if (!state || !Array.isArray(state)) {
447
+ return [];
448
+ }
449
+ return state as StoredMessageInternal[];
450
+ }
451
+
452
+ private getInternal(index: number): StoredMessageInternal | null {
453
+ const all = this.getAllInternal();
454
+ return all[index] ?? null;
455
+ }
456
+
457
+ private internalToStored(
458
+ internal: StoredMessageInternal,
459
+ id: MessageId,
460
+ index: number
461
+ ): StoredMessage {
462
+ return {
463
+ id,
464
+ sequence: index, // Use index as sequence for now
465
+ participant: internal.participant,
466
+ content: this.blobManager.resolveBlobs(internal.content),
467
+ metadata: internal.metadata,
468
+ timestamp: new Date(internal.timestamp),
469
+ causedBy: internal.causedBy,
470
+ };
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Default token estimator: chars / 4
476
+ */
477
+ function defaultTokenEstimator(text: string): number {
478
+ return Math.ceil(text.length / 4);
479
+ }