@animalabs/context-manager 0.4.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.
Files changed (128) hide show
  1. package/dist/src/adaptive/chunker.d.ts +70 -0
  2. package/dist/src/adaptive/chunker.d.ts.map +1 -0
  3. package/dist/src/adaptive/chunker.js +233 -0
  4. package/dist/src/adaptive/chunker.js.map +1 -0
  5. package/dist/src/adaptive/folding-strategy.d.ts +118 -0
  6. package/dist/src/adaptive/folding-strategy.d.ts.map +1 -0
  7. package/dist/src/adaptive/folding-strategy.js +12 -0
  8. package/dist/src/adaptive/folding-strategy.js.map +1 -0
  9. package/dist/src/adaptive/index.d.ts +17 -0
  10. package/dist/src/adaptive/index.d.ts.map +1 -0
  11. package/dist/src/adaptive/index.js +23 -0
  12. package/dist/src/adaptive/index.js.map +1 -0
  13. package/dist/src/adaptive/picker.d.ts +103 -0
  14. package/dist/src/adaptive/picker.d.ts.map +1 -0
  15. package/dist/src/adaptive/picker.js +357 -0
  16. package/dist/src/adaptive/picker.js.map +1 -0
  17. package/dist/src/adaptive/render.d.ts +39 -0
  18. package/dist/src/adaptive/render.d.ts.map +1 -0
  19. package/dist/src/adaptive/render.js +104 -0
  20. package/dist/src/adaptive/render.js.map +1 -0
  21. package/dist/src/adaptive/strategies/flat-profile.d.ts +19 -0
  22. package/dist/src/adaptive/strategies/flat-profile.d.ts.map +1 -0
  23. package/dist/src/adaptive/strategies/flat-profile.js +86 -0
  24. package/dist/src/adaptive/strategies/flat-profile.js.map +1 -0
  25. package/dist/src/adaptive/strategies/oldest-first.d.ts +19 -0
  26. package/dist/src/adaptive/strategies/oldest-first.d.ts.map +1 -0
  27. package/dist/src/adaptive/strategies/oldest-first.js +39 -0
  28. package/dist/src/adaptive/strategies/oldest-first.js.map +1 -0
  29. package/dist/src/context-manager.d.ts +5 -0
  30. package/dist/src/context-manager.d.ts.map +1 -1
  31. package/dist/src/context-manager.js +22 -0
  32. package/dist/src/context-manager.js.map +1 -1
  33. package/dist/src/index.d.ts +1 -1
  34. package/dist/src/index.d.ts.map +1 -1
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/message-store.d.ts +36 -1
  37. package/dist/src/message-store.d.ts.map +1 -1
  38. package/dist/src/message-store.js +134 -8
  39. package/dist/src/message-store.js.map +1 -1
  40. package/dist/src/strategies/autobiographical.d.ts +217 -2
  41. package/dist/src/strategies/autobiographical.d.ts.map +1 -1
  42. package/dist/src/strategies/autobiographical.js +1479 -37
  43. package/dist/src/strategies/autobiographical.js.map +1 -1
  44. package/dist/src/strategies/index.d.ts +1 -1
  45. package/dist/src/strategies/index.d.ts.map +1 -1
  46. package/dist/src/strategies/index.js.map +1 -1
  47. package/dist/src/types/message.d.ts +38 -0
  48. package/dist/src/types/message.d.ts.map +1 -1
  49. package/dist/src/types/strategy.d.ts +94 -10
  50. package/dist/src/types/strategy.d.ts.map +1 -1
  51. package/dist/src/types/strategy.js +7 -0
  52. package/dist/src/types/strategy.js.map +1 -1
  53. package/dist/test/adaptive/branching.test.d.ts +20 -0
  54. package/dist/test/adaptive/branching.test.d.ts.map +1 -0
  55. package/dist/test/adaptive/branching.test.js +176 -0
  56. package/dist/test/adaptive/branching.test.js.map +1 -0
  57. package/dist/test/adaptive/chunker.test.d.ts +2 -0
  58. package/dist/test/adaptive/chunker.test.d.ts.map +1 -0
  59. package/dist/test/adaptive/chunker.test.js +120 -0
  60. package/dist/test/adaptive/chunker.test.js.map +1 -0
  61. package/dist/test/adaptive/deep-levels.test.d.ts +10 -0
  62. package/dist/test/adaptive/deep-levels.test.d.ts.map +1 -0
  63. package/dist/test/adaptive/deep-levels.test.js +167 -0
  64. package/dist/test/adaptive/deep-levels.test.js.map +1 -0
  65. package/dist/test/adaptive/doc-plus-chat.test.d.ts +10 -0
  66. package/dist/test/adaptive/doc-plus-chat.test.d.ts.map +1 -0
  67. package/dist/test/adaptive/doc-plus-chat.test.js +194 -0
  68. package/dist/test/adaptive/doc-plus-chat.test.js.map +1 -0
  69. package/dist/test/adaptive/hard-fail.test.d.ts +9 -0
  70. package/dist/test/adaptive/hard-fail.test.d.ts.map +1 -0
  71. package/dist/test/adaptive/hard-fail.test.js +154 -0
  72. package/dist/test/adaptive/hard-fail.test.js.map +1 -0
  73. package/dist/test/adaptive/harness.d.ts +109 -0
  74. package/dist/test/adaptive/harness.d.ts.map +1 -0
  75. package/dist/test/adaptive/harness.js +263 -0
  76. package/dist/test/adaptive/harness.js.map +1 -0
  77. package/dist/test/adaptive/ingestion.test.d.ts +13 -0
  78. package/dist/test/adaptive/ingestion.test.d.ts.map +1 -0
  79. package/dist/test/adaptive/ingestion.test.js +306 -0
  80. package/dist/test/adaptive/ingestion.test.js.map +1 -0
  81. package/dist/test/adaptive/integration.test.d.ts +12 -0
  82. package/dist/test/adaptive/integration.test.d.ts.map +1 -0
  83. package/dist/test/adaptive/integration.test.js +247 -0
  84. package/dist/test/adaptive/integration.test.js.map +1 -0
  85. package/dist/test/adaptive/long-chronicle.test.d.ts +13 -0
  86. package/dist/test/adaptive/long-chronicle.test.d.ts.map +1 -0
  87. package/dist/test/adaptive/long-chronicle.test.js +186 -0
  88. package/dist/test/adaptive/long-chronicle.test.js.map +1 -0
  89. package/dist/test/adaptive/persistence.test.d.ts +11 -0
  90. package/dist/test/adaptive/persistence.test.d.ts.map +1 -0
  91. package/dist/test/adaptive/persistence.test.js +200 -0
  92. package/dist/test/adaptive/persistence.test.js.map +1 -0
  93. package/dist/test/adaptive/picker.test.d.ts +2 -0
  94. package/dist/test/adaptive/picker.test.d.ts.map +1 -0
  95. package/dist/test/adaptive/picker.test.js +333 -0
  96. package/dist/test/adaptive/picker.test.js.map +1 -0
  97. package/dist/test/adaptive/render.test.d.ts +2 -0
  98. package/dist/test/adaptive/render.test.d.ts.map +1 -0
  99. package/dist/test/adaptive/render.test.js +109 -0
  100. package/dist/test/adaptive/render.test.js.map +1 -0
  101. package/dist/test/adaptive/shard-immutability.test.d.ts +14 -0
  102. package/dist/test/adaptive/shard-immutability.test.d.ts.map +1 -0
  103. package/dist/test/adaptive/shard-immutability.test.js +184 -0
  104. package/dist/test/adaptive/shard-immutability.test.js.map +1 -0
  105. package/dist/test/adaptive/strategy-integration.test.d.ts +9 -0
  106. package/dist/test/adaptive/strategy-integration.test.d.ts.map +1 -0
  107. package/dist/test/adaptive/strategy-integration.test.js +293 -0
  108. package/dist/test/adaptive/strategy-integration.test.js.map +1 -0
  109. package/dist/test/message-store-sequence.test.d.ts +25 -0
  110. package/dist/test/message-store-sequence.test.d.ts.map +1 -0
  111. package/dist/test/message-store-sequence.test.js +157 -0
  112. package/dist/test/message-store-sequence.test.js.map +1 -0
  113. package/dist/tsconfig.tsbuildinfo +1 -1
  114. package/package.json +1 -1
  115. package/src/adaptive/chunker.ts +323 -0
  116. package/src/adaptive/folding-strategy.ts +129 -0
  117. package/src/adaptive/index.ts +51 -0
  118. package/src/adaptive/picker.ts +442 -0
  119. package/src/adaptive/render.ts +115 -0
  120. package/src/adaptive/strategies/flat-profile.ts +96 -0
  121. package/src/adaptive/strategies/oldest-first.ts +48 -0
  122. package/src/context-manager.ts +32 -0
  123. package/src/index.ts +1 -1
  124. package/src/message-store.ts +147 -8
  125. package/src/strategies/autobiographical.ts +1586 -38
  126. package/src/strategies/index.ts +1 -1
  127. package/src/types/message.ts +43 -0
  128. package/src/types/strategy.ts +109 -10
@@ -211,6 +211,11 @@ export class ContextManager {
211
211
 
212
212
  /**
213
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.
214
219
  */
215
220
  addMessage(
216
221
  participant: string,
@@ -218,6 +223,33 @@ export class ContextManager {
218
223
  metadata?: MessageMetadata,
219
224
  causedBy?: MessageId[]
220
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
+ }
221
253
  const message = this.messageStore.append(participant, content, metadata, causedBy);
222
254
  return message.id;
223
255
  }
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
@@ -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
- // Now update the stored item to include the id (for rebuildIndex)
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: record.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: record.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
- index: number
582
+ _index: number,
461
583
  ): StoredMessage {
462
- return {
584
+ const stored: StoredMessage = {
463
585
  id,
464
- sequence: index, // Use index as sequence for now
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