@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41

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 (210) hide show
  1. package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/converters/envelopeConverter.js +17 -3
  7. package/dist/packlets/converters/envelopeConverter.js.map +1 -1
  8. package/dist/packlets/index/memoryIndex.js +58 -10
  9. package/dist/packlets/index/memoryIndex.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +13 -6
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -1
  12. package/dist/packlets/ingest/hostStages.js.map +1 -1
  13. package/dist/packlets/ingest/model.js.map +1 -1
  14. package/dist/packlets/ingest/orchestrator.js +94 -49
  15. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  16. package/dist/packlets/retrieve/hybridRetriever.js +13 -2
  17. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
  18. package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
  19. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  20. package/dist/packlets/retrieve/recencyRetriever.js +3 -3
  21. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  22. package/dist/packlets/retrieve/retriever.js +50 -7
  23. package/dist/packlets/retrieve/retriever.js.map +1 -1
  24. package/dist/packlets/retrieve/semanticRetriever.js +9 -3
  25. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  26. package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
  27. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  28. package/dist/packlets/retrieve/tagRetriever.js +3 -3
  29. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  30. package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
  31. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  32. package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
  33. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  34. package/dist/packlets/tools/memoryTools.js +104 -21
  35. package/dist/packlets/tools/memoryTools.js.map +1 -1
  36. package/dist/packlets/types/envelope.js +13 -1
  37. package/dist/packlets/types/envelope.js.map +1 -1
  38. package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
  39. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  40. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  41. package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  42. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  43. package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
  44. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
  45. package/dist/test/unit/index/memoryIndex.test.js +87 -25
  46. package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
  47. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  48. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  49. package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
  50. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
  51. package/dist/test/unit/ingest/orchestrator.test.js +187 -45
  52. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  53. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  54. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  55. package/dist/test/unit/retrieve/retrievers.test.js +299 -37
  56. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
  57. package/dist/test/unit/store/embedOnWrite.test.js +69 -12
  58. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
  59. package/dist/test/unit/store/listScoped.test.js +109 -0
  60. package/dist/test/unit/store/listScoped.test.js.map +1 -0
  61. package/dist/test/unit/store/rankAxis.test.js +254 -0
  62. package/dist/test/unit/store/rankAxis.test.js.map +1 -0
  63. package/dist/test/unit/tools/memoryTools.test.js +280 -11
  64. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  65. package/dist/test/unit/types/writePolicy.test.js +9 -2
  66. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  67. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  68. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  69. package/dist/test/unit/vector/vectorIndex.test.js +24 -15
  70. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
  71. package/dist/ts-agent-memory.d.ts +384 -97
  72. package/etc/ts-agent-memory.api.md +64 -22
  73. package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
  74. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
  75. package/lib/packlets/converters/envelopeConverter.js +18 -4
  76. package/lib/packlets/converters/envelopeConverter.js.map +1 -1
  77. package/lib/packlets/index/memoryIndex.d.ts +42 -10
  78. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  79. package/lib/packlets/index/memoryIndex.js +58 -10
  80. package/lib/packlets/index/memoryIndex.js.map +1 -1
  81. package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
  82. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
  83. package/lib/packlets/ingest/cycleGuard.js +13 -6
  84. package/lib/packlets/ingest/cycleGuard.js.map +1 -1
  85. package/lib/packlets/ingest/hostStages.d.ts +3 -3
  86. package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
  87. package/lib/packlets/ingest/hostStages.js.map +1 -1
  88. package/lib/packlets/ingest/model.d.ts +25 -14
  89. package/lib/packlets/ingest/model.d.ts.map +1 -1
  90. package/lib/packlets/ingest/model.js.map +1 -1
  91. package/lib/packlets/ingest/orchestrator.d.ts +18 -4
  92. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  93. package/lib/packlets/ingest/orchestrator.js +93 -48
  94. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  95. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
  96. package/lib/packlets/retrieve/hybridRetriever.js +12 -1
  97. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
  98. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
  99. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  100. package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
  101. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  102. package/lib/packlets/retrieve/recencyRetriever.js +2 -2
  103. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  104. package/lib/packlets/retrieve/retriever.d.ts +70 -11
  105. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  106. package/lib/packlets/retrieve/retriever.js +52 -7
  107. package/lib/packlets/retrieve/retriever.js.map +1 -1
  108. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  109. package/lib/packlets/retrieve/semanticRetriever.js +9 -3
  110. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  111. package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
  112. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  113. package/lib/packlets/retrieve/tagRetriever.js +2 -2
  114. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  115. package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
  116. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  117. package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
  118. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  119. package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
  120. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  121. package/lib/packlets/tools/memoryTools.d.ts +24 -0
  122. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  123. package/lib/packlets/tools/memoryTools.js +104 -21
  124. package/lib/packlets/tools/memoryTools.js.map +1 -1
  125. package/lib/packlets/types/envelope.d.ts +61 -8
  126. package/lib/packlets/types/envelope.d.ts.map +1 -1
  127. package/lib/packlets/types/envelope.js +14 -0
  128. package/lib/packlets/types/envelope.js.map +1 -1
  129. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
  130. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  131. package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
  132. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  133. package/lib/packlets/vector/vectorIndex.d.ts +46 -20
  134. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  135. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  136. package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  137. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  138. package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
  139. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
  140. package/lib/test/unit/index/memoryIndex.test.js +86 -24
  141. package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
  145. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
  146. package/lib/test/unit/ingest/orchestrator.test.js +186 -44
  147. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  148. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  149. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  150. package/lib/test/unit/retrieve/retrievers.test.js +298 -36
  151. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
  152. package/lib/test/unit/store/embedOnWrite.test.js +68 -11
  153. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
  154. package/lib/test/unit/store/listScoped.test.d.ts +2 -0
  155. package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
  156. package/lib/test/unit/store/listScoped.test.js +111 -0
  157. package/lib/test/unit/store/listScoped.test.js.map +1 -0
  158. package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
  159. package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
  160. package/lib/test/unit/store/rankAxis.test.js +256 -0
  161. package/lib/test/unit/store/rankAxis.test.js.map +1 -0
  162. package/lib/test/unit/tools/memoryTools.test.js +280 -11
  163. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  164. package/lib/test/unit/types/writePolicy.test.js +9 -2
  165. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  166. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  167. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  168. package/lib/test/unit/vector/vectorIndex.test.js +24 -15
  169. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
  170. package/package.json +7 -7
  171. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  172. package/rush-logs/ts-agent-memory.build.log +2 -2
  173. package/src/packlets/converters/envelopeConverter.ts +27 -4
  174. package/src/packlets/index/memoryIndex.ts +86 -22
  175. package/src/packlets/ingest/cycleGuard.ts +22 -11
  176. package/src/packlets/ingest/hostStages.ts +3 -3
  177. package/src/packlets/ingest/model.ts +25 -14
  178. package/src/packlets/ingest/orchestrator.ts +143 -67
  179. package/src/packlets/retrieve/hybridRetriever.ts +14 -1
  180. package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
  181. package/src/packlets/retrieve/recencyRetriever.ts +3 -3
  182. package/src/packlets/retrieve/retriever.ts +97 -13
  183. package/src/packlets/retrieve/semanticRetriever.ts +10 -5
  184. package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
  185. package/src/packlets/retrieve/tagRetriever.ts +3 -3
  186. package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
  187. package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
  188. package/src/packlets/tools/memoryTools.ts +152 -25
  189. package/src/packlets/types/envelope.ts +66 -8
  190. package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
  191. package/src/packlets/vector/vectorIndex.ts +47 -20
  192. package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
  193. package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
  194. package/src/test/unit/index/memoryIndex.test.ts +99 -14
  195. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
  196. package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
  197. package/src/test/unit/ingest/orchestrator.test.ts +234 -41
  198. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
  199. package/src/test/unit/retrieve/retrievers.test.ts +381 -25
  200. package/src/test/unit/store/embedOnWrite.test.ts +83 -11
  201. package/src/test/unit/store/listScoped.test.ts +138 -0
  202. package/src/test/unit/store/rankAxis.test.ts +349 -0
  203. package/src/test/unit/tools/memoryTools.test.ts +362 -13
  204. package/src/test/unit/types/writePolicy.test.ts +11 -2
  205. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
  206. package/src/test/unit/vector/vectorIndex.test.ts +33 -17
  207. package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
  208. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  209. package/temp/ts-agent-memory.api.json +1080 -136
  210. package/temp/ts-agent-memory.api.md +64 -22
@@ -61,7 +61,13 @@ function makeEntry(spec) {
61
61
  entityId: (_a = spec.entityId) !== null && _a !== void 0 ? _a : spec.id,
62
62
  kind: (_b = spec.kind) !== null && _b !== void 0 ? _b : 'knowledge',
63
63
  tags: (_c = spec.tags) !== null && _c !== void 0 ? _c : [],
64
- links: ((_d = spec.links) !== null && _d !== void 0 ? _d : []).map((target) => ({ type: 'rel', target })),
64
+ links: ((_d = spec.links) !== null && _d !== void 0 ? _d : []).map((target) => {
65
+ var _a;
66
+ return ({
67
+ type: 'rel',
68
+ target: { scope: (_a = spec.scope) !== null && _a !== void 0 ? _a : 'knowledge', id: target }
69
+ });
70
+ }),
65
71
  created: 0,
66
72
  updated: (_e = spec.updated) !== null && _e !== void 0 ? _e : 0,
67
73
  seq: 0,
@@ -205,9 +211,22 @@ describe('createMemoryTools', () => {
205
211
  entityId: 'doc-1',
206
212
  body: 'hello',
207
213
  tags: ['a'],
208
- links: [{ type: 'rel', target: 'doc-2', confidence: 0.5 }]
214
+ links: [{ type: 'rel', target: { id: 'doc-2' }, confidence: 0.5 }]
215
+ })).toSucceed();
216
+ expect(schema.convert({
217
+ kind: 'knowledge',
218
+ entityId: 'doc-1',
219
+ body: 'hello',
220
+ links: [{ type: 'rel', target: { scope: 'conversations/other', id: 'turn-1' } }]
209
221
  })).toSucceed();
210
222
  expect(schema.convert({ kind: 'knowledge', entityId: 'doc-1' })).toFail();
223
+ // A bare-string target (the pre-scoped format) is rejected at the schema.
224
+ expect(schema.convert({
225
+ kind: 'knowledge',
226
+ entityId: 'doc-1',
227
+ body: 'x',
228
+ links: [{ type: 'rel', target: 'doc-2' }]
229
+ })).toFail();
211
230
  });
212
231
  test('memory_read / memory_delete require kind and entityId', () => {
213
232
  for (const name of ['memory_read', 'memory_delete']) {
@@ -222,10 +241,22 @@ describe('createMemoryTools', () => {
222
241
  expect(schema.convert({ tag: 'x', limit: 3 })).toSucceed();
223
242
  expect(schema.convert({ limit: 'three' })).toFail();
224
243
  });
225
- test('memory_context requires a from seed', () => {
244
+ test('memory_context requires a from seed with a REQUIRED scope (nested { id, scope })', () => {
226
245
  const schema = toolByName(allTools(), 'memory_context').config.parametersSchema;
227
- expect(schema.convert({ from: 'doc-1' })).toSucceed();
246
+ expect(schema.convert({ from: { id: 'doc-1', scope: 'knowledge' } })).toSucceed();
247
+ // scope is schema-REQUIRED (unlike a write link target) — the wire schema must
248
+ // not advertise an optionality the tool does not honor. Omitting it fails.
249
+ expect(schema.convert({ from: { id: 'doc-1' } })).toFail();
228
250
  expect(schema.convert({ hops: 2 })).toFail();
251
+ // A bare-string seed (the pre-scoped format) is rejected at the schema.
252
+ expect(schema.convert({ from: 'doc-1' })).toFail();
253
+ });
254
+ test('the memory_context wire schema marks from.scope as required', () => {
255
+ // Guards the schema-accuracy contract at the wire level: an LLM reading the
256
+ // tool's `parameters` sees scope in the seed's `required` array.
257
+ const schema = toolByName(allTools(), 'memory_context').config.parametersSchema;
258
+ const wire = schema.toJson();
259
+ expect(wire.properties.from.required).toContain('scope');
229
260
  });
230
261
  });
231
262
  describe('memory_write', () => {
@@ -269,11 +300,39 @@ describe('createMemoryTools', () => {
269
300
  entityId: 'doc-1',
270
301
  body: 'linked',
271
302
  links: [
272
- { type: 'rel', target: 'doc-2', confidence: 0.9 },
273
- { type: 'rel', target: 'doc-3' }
303
+ // scope omitted defaults to the writing record's own scope (knowledge)
304
+ { type: 'rel', target: { id: 'doc-2' }, confidence: 0.9 },
305
+ { type: 'rel', target: { id: 'doc-3' } }
274
306
  ]
275
307
  })).toSucceedAndSatisfy((v) => expect(v.outcome).toBe('written'));
276
- expect(await toolByName(tools, 'memory_read').execute({ kind: 'knowledge', entityId: 'doc-1' })).toSucceedAndSatisfy(() => undefined);
308
+ // The persisted links carry the writing record's scope on each target.
309
+ expect(await store.get(knowledgeKind, 'doc-1')).toSucceedAndSatisfy((rec) => {
310
+ expect(rec === null || rec === void 0 ? void 0 : rec.envelope.links).toEqual([
311
+ { type: 'rel', target: { scope: 'knowledge', id: 'doc-2' }, confidence: 0.9 },
312
+ { type: 'rel', target: { scope: 'knowledge', id: 'doc-3' } }
313
+ ]);
314
+ });
315
+ });
316
+ test('an authored link with an EXPLICIT scope is honored verbatim (cross-scope edge)', async () => {
317
+ const store = makeStore();
318
+ const tools = createMemoryTools({
319
+ store,
320
+ retriever: makeRetriever([]),
321
+ registry: registryWith([{ kind: knowledgeKind }]),
322
+ codecs,
323
+ tools: ['memory_write']
324
+ });
325
+ expect(await toolByName(tools, 'memory_write').execute({
326
+ kind: 'knowledge',
327
+ entityId: 'doc-1',
328
+ body: 'cross',
329
+ links: [{ type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }]
330
+ })).toSucceed();
331
+ expect(await store.get(knowledgeKind, 'doc-1')).toSucceedAndSatisfy((rec) => {
332
+ expect(rec === null || rec === void 0 ? void 0 : rec.envelope.links).toEqual([
333
+ { type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }
334
+ ]);
335
+ });
277
336
  });
278
337
  test('an identical re-put of the same entity is a dedup no-op (outcome: deduped)', async () => {
279
338
  const store = makeStore();
@@ -355,6 +414,8 @@ describe('createMemoryTools', () => {
355
414
  get: async () => fail('disk fault'),
356
415
  getById: async () => fail('unused'),
357
416
  list: async () => succeed([]),
417
+ listScoped: async () => succeed([]),
418
+ asRecordSource: () => ({ list: async () => succeed([]) }),
358
419
  put: async (record) => succeed(record),
359
420
  delete: async () => fail('unused')
360
421
  };
@@ -510,9 +571,9 @@ describe('createMemoryTools', () => {
510
571
  retriever,
511
572
  registry: registryWith([{ kind: knowledgeKind }])
512
573
  }), 'memory_context');
513
- expect(await tool.execute({ from: 'a' })).toSucceedAndSatisfy((value) => {
574
+ expect(await tool.execute({ from: { id: 'a', scope: 'knowledge' } })).toSucceedAndSatisfy((value) => {
514
575
  const out = value;
515
- expect(out.seed).toBe('a');
576
+ expect(out.seed).toEqual({ scope: 'knowledge', id: 'a' });
516
577
  expect(out.results.map((r) => r.handle)).toEqual(['b', 'c']);
517
578
  });
518
579
  });
@@ -527,14 +588,46 @@ describe('createMemoryTools', () => {
527
588
  retriever,
528
589
  registry: registryWith([{ kind: knowledgeKind }])
529
590
  }), 'memory_context');
530
- expect(await tool.execute({ from: 'a', hops: 2, tag: 'keep', kind: 'knowledge', limit: 5 })).toSucceedAndSatisfy((value) => {
591
+ expect(await tool.execute({
592
+ from: { id: 'a', scope: 'knowledge' },
593
+ hops: 2,
594
+ tag: 'keep',
595
+ kind: 'knowledge',
596
+ limit: 5
597
+ })).toSucceedAndSatisfy((value) => {
531
598
  const out = value;
532
599
  expect(out.results.map((r) => r.handle).sort()).toEqual(['b', 'c']);
533
600
  });
534
601
  });
535
602
  test('rejects an invalid seed id', async () => {
536
603
  const tool = toolByName(allTools(), 'memory_context');
537
- expect(await tool.execute({ from: 'a/b' })).toFail();
604
+ expect(await tool.execute({ from: { id: 'a/b', scope: 'knowledge' } })).toFail();
605
+ });
606
+ test('fails at the schema when the seed omits its required scope', async () => {
607
+ // scope is schema-required, so an omitted scope is rejected as an invalid
608
+ // argument (the wire schema and the runtime contract agree — no lie).
609
+ const tool = toolByName(allTools(), 'memory_context');
610
+ expect(await tool.execute({ from: { id: 'a' } })).toFailWith(/invalid arguments/i);
611
+ });
612
+ test('a scoped seed reaches ONLY the correctly-scoped graph', async () => {
613
+ // Two conversations reuse the stem `turn-0`; each links a same-scope `turn-1`.
614
+ // Seeding conv-a must reach conv-a's turn-1 and NEVER conv-b's.
615
+ const retriever = makeLinkRetriever([
616
+ { id: 'turn-0', scope: 'conversations/conv-a', links: ['turn-1'] },
617
+ { id: 'turn-1', scope: 'conversations/conv-a', tags: ['in-a'] },
618
+ { id: 'turn-0', scope: 'conversations/conv-b', links: ['turn-1'] },
619
+ { id: 'turn-1', scope: 'conversations/conv-b', tags: ['in-b'] }
620
+ ]);
621
+ const tool = toolByName(createMemoryTools({
622
+ store: makeStore(),
623
+ retriever,
624
+ registry: registryWith([{ kind: knowledgeKind }])
625
+ }), 'memory_context');
626
+ expect(await tool.execute({ from: { id: 'turn-0', scope: 'conversations/conv-a' } })).toSucceedAndSatisfy((value) => {
627
+ const out = value;
628
+ expect(out.results).toHaveLength(1);
629
+ expect(out.results[0].tags).toEqual(['in-a']);
630
+ });
538
631
  });
539
632
  test('rejects malformed arguments', async () => {
540
633
  const tool = toolByName(allTools(), 'memory_context');
@@ -569,4 +662,180 @@ describe('createMemoryTools', () => {
569
662
  });
570
663
  });
571
664
  });
665
+ describe('result projection (projectItem host projector + detail tier)', () => {
666
+ /**
667
+ * A host projector that bounds the `'gist'` body to a sentinel and passes the
668
+ * full body through only for `'full'` — the shape a real host uses to keep the
669
+ * default (gist) path bounded.
670
+ */
671
+ const boundingProjector = (record, detail) => ({
672
+ handle: `h:${record.envelope.id}`,
673
+ kind: record.envelope.kind,
674
+ entityId: record.envelope.entityId,
675
+ tags: record.envelope.tags,
676
+ body: detail === 'full' ? record.body : '<<gist>>'
677
+ });
678
+ function searchToolWith(overrides) {
679
+ return toolByName(createMemoryTools(Object.assign({ store: makeStore(), retriever: makeRetriever([{ id: 'doc-1', tags: ['topic'] }]), registry: registryWith([{ kind: knowledgeKind }]) }, overrides)), 'memory_search');
680
+ }
681
+ describe('schema surface', () => {
682
+ test('memory_search declares optional detail + offset properties', () => {
683
+ const props = schemaProperties(toolByName(allTools(), 'memory_search'));
684
+ expect(props).toContain('detail');
685
+ expect(props).toContain('offset');
686
+ });
687
+ test('memory_context declares an optional detail property', () => {
688
+ const props = schemaProperties(toolByName(allTools(), 'memory_context'));
689
+ expect(props).toContain('detail');
690
+ });
691
+ test('memory_read declares an optional detail property', () => {
692
+ const props = schemaProperties(toolByName(allTools(), 'memory_read'));
693
+ expect(props).toContain('detail');
694
+ });
695
+ test('memory_search accepts detail and offset, and rejects an out-of-enum detail', () => {
696
+ const schema = toolByName(allTools(), 'memory_search').config.parametersSchema;
697
+ expect(schema.convert({ detail: 'full', offset: 2 })).toSucceed();
698
+ expect(schema.convert({ detail: 'gist' })).toSucceed();
699
+ expect(schema.convert({ offset: 'nope' })).toFail();
700
+ // detail is now an enum ('gist' | 'full') at the wire-schema level.
701
+ expect(schema.convert({ detail: 'verbose' })).toFail();
702
+ });
703
+ });
704
+ describe('absent projector = byte-identical default projection', () => {
705
+ test('returns the full body and the default handle (raw id) when no projector is supplied', async () => {
706
+ const tool = searchToolWith({});
707
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
708
+ const out = value;
709
+ expect(out.results[0].handle).toBe('doc-1');
710
+ expect(out.results[0].body).toBe('body-doc-1');
711
+ });
712
+ });
713
+ test('the detail tier is inert without a projector (gist and full both yield the full body)', async () => {
714
+ const tool = searchToolWith({});
715
+ for (const detail of ['gist', 'full']) {
716
+ expect(await tool.execute({ tag: 'topic', detail })).toSucceedAndSatisfy((value) => {
717
+ expect(value.results[0].body).toBe('body-doc-1');
718
+ });
719
+ }
720
+ });
721
+ });
722
+ describe('host projector + detail tier', () => {
723
+ test('defaults to the bounded gist projection when detail is absent', async () => {
724
+ const tool = searchToolWith({ projectItem: boundingProjector });
725
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
726
+ const out = value;
727
+ expect(out.results[0].handle).toBe('h:doc-1');
728
+ expect(out.results[0].body).toBe('<<gist>>');
729
+ });
730
+ });
731
+ test('opts into the full body only when detail=full is requested', async () => {
732
+ const tool = searchToolWith({ projectItem: boundingProjector });
733
+ expect(await tool.execute({ tag: 'topic', detail: 'full' })).toSucceedAndSatisfy((value) => {
734
+ expect(value.results[0].body).toBe('body-doc-1');
735
+ });
736
+ });
737
+ test('an out-of-enum detail is rejected at the schema boundary', async () => {
738
+ // detail is a JsonSchema.enumOf(['gist','full']); the tool re-validates args
739
+ // on execute, so an out-of-enum value fails loudly rather than reaching the projector.
740
+ const tool = searchToolWith({ projectItem: boundingProjector });
741
+ expect(await tool.execute({ tag: 'topic', detail: 'verbose' })).toFailWith(/invalid arguments/i);
742
+ });
743
+ test('an explicit detail=gist stays bounded (the non-full branch)', async () => {
744
+ const tool = searchToolWith({ projectItem: boundingProjector });
745
+ expect(await tool.execute({ tag: 'topic', detail: 'gist' })).toSucceedAndSatisfy((value) => {
746
+ expect(value.results[0].body).toBe('<<gist>>');
747
+ });
748
+ });
749
+ test('memory_context routes its results through the host projector', async () => {
750
+ const tool = toolByName(createMemoryTools({
751
+ store: makeStore(),
752
+ retriever: makeLinkRetriever([{ id: 'a', links: ['b'] }, { id: 'b' }]),
753
+ registry: registryWith([{ kind: knowledgeKind }]),
754
+ projectItem: boundingProjector
755
+ }), 'memory_context');
756
+ expect(await tool.execute({ from: { id: 'a', scope: 'knowledge' }, detail: 'full' })).toSucceedAndSatisfy((value) => {
757
+ const out = value;
758
+ expect(out.results.map((r) => r.handle)).toEqual(['h:b']);
759
+ expect(out.results[0].body).toBe('body-b');
760
+ });
761
+ });
762
+ test('a throwing host projector degrades to the default full-body projection (does not crash search)', async () => {
763
+ const tool = searchToolWith({
764
+ projectItem: () => {
765
+ throw new Error('host projector blew up');
766
+ }
767
+ });
768
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
769
+ const out = value;
770
+ // Degraded to the built-in default: raw-id handle + full body.
771
+ expect(out.results[0].handle).toBe('doc-1');
772
+ expect(out.results[0].body).toBe('body-doc-1');
773
+ });
774
+ });
775
+ test('a throwing projector still honors the handleFor hook in the fallback path', async () => {
776
+ const tool = searchToolWith({
777
+ handleFor: (r) => `@${r.envelope.id}`,
778
+ projectItem: () => {
779
+ throw new Error('nope');
780
+ }
781
+ });
782
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
783
+ expect(value.results[0].handle).toBe('@doc-1');
784
+ });
785
+ });
786
+ });
787
+ describe('offset threading (memory_search)', () => {
788
+ test('threads offset into the query to page the ordered result window', async () => {
789
+ const tool = toolByName(createMemoryTools({
790
+ store: makeStore(),
791
+ retriever: makeRetriever([
792
+ { id: 'doc-1', tags: ['topic'], updated: 30 },
793
+ { id: 'doc-2', tags: ['topic'], updated: 20 },
794
+ { id: 'doc-3', tags: ['topic'], updated: 10 }
795
+ ]),
796
+ registry: registryWith([{ kind: knowledgeKind }])
797
+ }), 'memory_search');
798
+ // Recency-ordered [doc-1, doc-2, doc-3]; offset 1 + limit 1 → [doc-2].
799
+ expect(await tool.execute({ tag: 'topic', offset: 1, limit: 1 })).toSucceedAndSatisfy((value) => {
800
+ const out = value;
801
+ expect(out.count).toBe(1);
802
+ expect(out.results.map((r) => r.handle)).toEqual(['doc-2']);
803
+ });
804
+ });
805
+ });
806
+ describe('memory_read detail (explicit drill-in defaults to FULL)', () => {
807
+ async function readToolWith(projector) {
808
+ const tools = createMemoryTools(Object.assign({ store: makeStore(), retriever: makeRetriever([]), registry: registryWith([{ kind: knowledgeKind }]), codecs, tools: ['memory_write', 'memory_read'] }, (projector !== undefined ? { projectItem: projector } : {})));
809
+ await toolByName(tools, 'memory_write').execute({
810
+ kind: 'knowledge',
811
+ entityId: 'doc-1',
812
+ body: 'FULLBODY'
813
+ });
814
+ return toolByName(tools, 'memory_read');
815
+ }
816
+ test('defaults to the FULL body (drill-in) with a bounding projector', async () => {
817
+ const tool = await readToolWith(boundingProjector);
818
+ expect(await tool.execute({ kind: 'knowledge', entityId: 'doc-1' })).toSucceedAndSatisfy((value) => {
819
+ expect(value.item.body).toBe('FULLBODY');
820
+ });
821
+ });
822
+ test('opts down to gist only when detail=gist is requested', async () => {
823
+ const tool = await readToolWith(boundingProjector);
824
+ expect(await tool.execute({ kind: 'knowledge', entityId: 'doc-1', detail: 'gist' })).toSucceedAndSatisfy((value) => {
825
+ expect(value.item.body).toBe('<<gist>>');
826
+ });
827
+ });
828
+ test('is byte-identical for a no-projector consumer (full body regardless of detail)', async () => {
829
+ const tool = await readToolWith(undefined);
830
+ for (const args of [
831
+ { kind: 'knowledge', entityId: 'doc-1' },
832
+ { kind: 'knowledge', entityId: 'doc-1', detail: 'gist' }
833
+ ]) {
834
+ expect(await tool.execute(args)).toSucceedAndSatisfy((value) => {
835
+ expect(value.item.body).toBe('FULLBODY');
836
+ });
837
+ }
838
+ });
839
+ });
840
+ });
572
841
  //# sourceMappingURL=memoryTools.test.js.map