@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
@@ -63,7 +63,13 @@ function makeEntry(spec) {
63
63
  entityId: (_a = spec.entityId) !== null && _a !== void 0 ? _a : spec.id,
64
64
  kind: (_b = spec.kind) !== null && _b !== void 0 ? _b : 'knowledge',
65
65
  tags: (_c = spec.tags) !== null && _c !== void 0 ? _c : [],
66
- links: ((_d = spec.links) !== null && _d !== void 0 ? _d : []).map((target) => ({ type: 'rel', target })),
66
+ links: ((_d = spec.links) !== null && _d !== void 0 ? _d : []).map((target) => {
67
+ var _a;
68
+ return ({
69
+ type: 'rel',
70
+ target: { scope: (_a = spec.scope) !== null && _a !== void 0 ? _a : 'knowledge', id: target }
71
+ });
72
+ }),
67
73
  created: 0,
68
74
  updated: (_e = spec.updated) !== null && _e !== void 0 ? _e : 0,
69
75
  seq: 0,
@@ -207,9 +213,22 @@ describe('createMemoryTools', () => {
207
213
  entityId: 'doc-1',
208
214
  body: 'hello',
209
215
  tags: ['a'],
210
- links: [{ type: 'rel', target: 'doc-2', confidence: 0.5 }]
216
+ links: [{ type: 'rel', target: { id: 'doc-2' }, confidence: 0.5 }]
217
+ })).toSucceed();
218
+ expect(schema.convert({
219
+ kind: 'knowledge',
220
+ entityId: 'doc-1',
221
+ body: 'hello',
222
+ links: [{ type: 'rel', target: { scope: 'conversations/other', id: 'turn-1' } }]
211
223
  })).toSucceed();
212
224
  expect(schema.convert({ kind: 'knowledge', entityId: 'doc-1' })).toFail();
225
+ // A bare-string target (the pre-scoped format) is rejected at the schema.
226
+ expect(schema.convert({
227
+ kind: 'knowledge',
228
+ entityId: 'doc-1',
229
+ body: 'x',
230
+ links: [{ type: 'rel', target: 'doc-2' }]
231
+ })).toFail();
213
232
  });
214
233
  test('memory_read / memory_delete require kind and entityId', () => {
215
234
  for (const name of ['memory_read', 'memory_delete']) {
@@ -224,10 +243,22 @@ describe('createMemoryTools', () => {
224
243
  expect(schema.convert({ tag: 'x', limit: 3 })).toSucceed();
225
244
  expect(schema.convert({ limit: 'three' })).toFail();
226
245
  });
227
- test('memory_context requires a from seed', () => {
246
+ test('memory_context requires a from seed with a REQUIRED scope (nested { id, scope })', () => {
228
247
  const schema = toolByName(allTools(), 'memory_context').config.parametersSchema;
229
- expect(schema.convert({ from: 'doc-1' })).toSucceed();
248
+ expect(schema.convert({ from: { id: 'doc-1', scope: 'knowledge' } })).toSucceed();
249
+ // scope is schema-REQUIRED (unlike a write link target) — the wire schema must
250
+ // not advertise an optionality the tool does not honor. Omitting it fails.
251
+ expect(schema.convert({ from: { id: 'doc-1' } })).toFail();
230
252
  expect(schema.convert({ hops: 2 })).toFail();
253
+ // A bare-string seed (the pre-scoped format) is rejected at the schema.
254
+ expect(schema.convert({ from: 'doc-1' })).toFail();
255
+ });
256
+ test('the memory_context wire schema marks from.scope as required', () => {
257
+ // Guards the schema-accuracy contract at the wire level: an LLM reading the
258
+ // tool's `parameters` sees scope in the seed's `required` array.
259
+ const schema = toolByName(allTools(), 'memory_context').config.parametersSchema;
260
+ const wire = schema.toJson();
261
+ expect(wire.properties.from.required).toContain('scope');
231
262
  });
232
263
  });
233
264
  describe('memory_write', () => {
@@ -271,11 +302,39 @@ describe('createMemoryTools', () => {
271
302
  entityId: 'doc-1',
272
303
  body: 'linked',
273
304
  links: [
274
- { type: 'rel', target: 'doc-2', confidence: 0.9 },
275
- { type: 'rel', target: 'doc-3' }
305
+ // scope omitted defaults to the writing record's own scope (knowledge)
306
+ { type: 'rel', target: { id: 'doc-2' }, confidence: 0.9 },
307
+ { type: 'rel', target: { id: 'doc-3' } }
276
308
  ]
277
309
  })).toSucceedAndSatisfy((v) => expect(v.outcome).toBe('written'));
278
- expect(await toolByName(tools, 'memory_read').execute({ kind: 'knowledge', entityId: 'doc-1' })).toSucceedAndSatisfy(() => undefined);
310
+ // The persisted links carry the writing record's scope on each target.
311
+ expect(await store.get(knowledgeKind, 'doc-1')).toSucceedAndSatisfy((rec) => {
312
+ expect(rec === null || rec === void 0 ? void 0 : rec.envelope.links).toEqual([
313
+ { type: 'rel', target: { scope: 'knowledge', id: 'doc-2' }, confidence: 0.9 },
314
+ { type: 'rel', target: { scope: 'knowledge', id: 'doc-3' } }
315
+ ]);
316
+ });
317
+ });
318
+ test('an authored link with an EXPLICIT scope is honored verbatim (cross-scope edge)', async () => {
319
+ const store = makeStore();
320
+ const tools = (0, index_1.createMemoryTools)({
321
+ store,
322
+ retriever: makeRetriever([]),
323
+ registry: registryWith([{ kind: knowledgeKind }]),
324
+ codecs,
325
+ tools: ['memory_write']
326
+ });
327
+ expect(await toolByName(tools, 'memory_write').execute({
328
+ kind: 'knowledge',
329
+ entityId: 'doc-1',
330
+ body: 'cross',
331
+ links: [{ type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }]
332
+ })).toSucceed();
333
+ expect(await store.get(knowledgeKind, 'doc-1')).toSucceedAndSatisfy((rec) => {
334
+ expect(rec === null || rec === void 0 ? void 0 : rec.envelope.links).toEqual([
335
+ { type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }
336
+ ]);
337
+ });
279
338
  });
280
339
  test('an identical re-put of the same entity is a dedup no-op (outcome: deduped)', async () => {
281
340
  const store = makeStore();
@@ -357,6 +416,8 @@ describe('createMemoryTools', () => {
357
416
  get: async () => (0, ts_utils_1.fail)('disk fault'),
358
417
  getById: async () => (0, ts_utils_1.fail)('unused'),
359
418
  list: async () => (0, ts_utils_1.succeed)([]),
419
+ listScoped: async () => (0, ts_utils_1.succeed)([]),
420
+ asRecordSource: () => ({ list: async () => (0, ts_utils_1.succeed)([]) }),
360
421
  put: async (record) => (0, ts_utils_1.succeed)(record),
361
422
  delete: async () => (0, ts_utils_1.fail)('unused')
362
423
  };
@@ -512,9 +573,9 @@ describe('createMemoryTools', () => {
512
573
  retriever,
513
574
  registry: registryWith([{ kind: knowledgeKind }])
514
575
  }), 'memory_context');
515
- expect(await tool.execute({ from: 'a' })).toSucceedAndSatisfy((value) => {
576
+ expect(await tool.execute({ from: { id: 'a', scope: 'knowledge' } })).toSucceedAndSatisfy((value) => {
516
577
  const out = value;
517
- expect(out.seed).toBe('a');
578
+ expect(out.seed).toEqual({ scope: 'knowledge', id: 'a' });
518
579
  expect(out.results.map((r) => r.handle)).toEqual(['b', 'c']);
519
580
  });
520
581
  });
@@ -529,14 +590,46 @@ describe('createMemoryTools', () => {
529
590
  retriever,
530
591
  registry: registryWith([{ kind: knowledgeKind }])
531
592
  }), 'memory_context');
532
- expect(await tool.execute({ from: 'a', hops: 2, tag: 'keep', kind: 'knowledge', limit: 5 })).toSucceedAndSatisfy((value) => {
593
+ expect(await tool.execute({
594
+ from: { id: 'a', scope: 'knowledge' },
595
+ hops: 2,
596
+ tag: 'keep',
597
+ kind: 'knowledge',
598
+ limit: 5
599
+ })).toSucceedAndSatisfy((value) => {
533
600
  const out = value;
534
601
  expect(out.results.map((r) => r.handle).sort()).toEqual(['b', 'c']);
535
602
  });
536
603
  });
537
604
  test('rejects an invalid seed id', async () => {
538
605
  const tool = toolByName(allTools(), 'memory_context');
539
- expect(await tool.execute({ from: 'a/b' })).toFail();
606
+ expect(await tool.execute({ from: { id: 'a/b', scope: 'knowledge' } })).toFail();
607
+ });
608
+ test('fails at the schema when the seed omits its required scope', async () => {
609
+ // scope is schema-required, so an omitted scope is rejected as an invalid
610
+ // argument (the wire schema and the runtime contract agree — no lie).
611
+ const tool = toolByName(allTools(), 'memory_context');
612
+ expect(await tool.execute({ from: { id: 'a' } })).toFailWith(/invalid arguments/i);
613
+ });
614
+ test('a scoped seed reaches ONLY the correctly-scoped graph', async () => {
615
+ // Two conversations reuse the stem `turn-0`; each links a same-scope `turn-1`.
616
+ // Seeding conv-a must reach conv-a's turn-1 and NEVER conv-b's.
617
+ const retriever = makeLinkRetriever([
618
+ { id: 'turn-0', scope: 'conversations/conv-a', links: ['turn-1'] },
619
+ { id: 'turn-1', scope: 'conversations/conv-a', tags: ['in-a'] },
620
+ { id: 'turn-0', scope: 'conversations/conv-b', links: ['turn-1'] },
621
+ { id: 'turn-1', scope: 'conversations/conv-b', tags: ['in-b'] }
622
+ ]);
623
+ const tool = toolByName((0, index_1.createMemoryTools)({
624
+ store: makeStore(),
625
+ retriever,
626
+ registry: registryWith([{ kind: knowledgeKind }])
627
+ }), 'memory_context');
628
+ expect(await tool.execute({ from: { id: 'turn-0', scope: 'conversations/conv-a' } })).toSucceedAndSatisfy((value) => {
629
+ const out = value;
630
+ expect(out.results).toHaveLength(1);
631
+ expect(out.results[0].tags).toEqual(['in-a']);
632
+ });
540
633
  });
541
634
  test('rejects malformed arguments', async () => {
542
635
  const tool = toolByName(allTools(), 'memory_context');
@@ -571,4 +664,180 @@ describe('createMemoryTools', () => {
571
664
  });
572
665
  });
573
666
  });
667
+ describe('result projection (projectItem host projector + detail tier)', () => {
668
+ /**
669
+ * A host projector that bounds the `'gist'` body to a sentinel and passes the
670
+ * full body through only for `'full'` — the shape a real host uses to keep the
671
+ * default (gist) path bounded.
672
+ */
673
+ const boundingProjector = (record, detail) => ({
674
+ handle: `h:${record.envelope.id}`,
675
+ kind: record.envelope.kind,
676
+ entityId: record.envelope.entityId,
677
+ tags: record.envelope.tags,
678
+ body: detail === 'full' ? record.body : '<<gist>>'
679
+ });
680
+ function searchToolWith(overrides) {
681
+ return toolByName((0, index_1.createMemoryTools)(Object.assign({ store: makeStore(), retriever: makeRetriever([{ id: 'doc-1', tags: ['topic'] }]), registry: registryWith([{ kind: knowledgeKind }]) }, overrides)), 'memory_search');
682
+ }
683
+ describe('schema surface', () => {
684
+ test('memory_search declares optional detail + offset properties', () => {
685
+ const props = schemaProperties(toolByName(allTools(), 'memory_search'));
686
+ expect(props).toContain('detail');
687
+ expect(props).toContain('offset');
688
+ });
689
+ test('memory_context declares an optional detail property', () => {
690
+ const props = schemaProperties(toolByName(allTools(), 'memory_context'));
691
+ expect(props).toContain('detail');
692
+ });
693
+ test('memory_read declares an optional detail property', () => {
694
+ const props = schemaProperties(toolByName(allTools(), 'memory_read'));
695
+ expect(props).toContain('detail');
696
+ });
697
+ test('memory_search accepts detail and offset, and rejects an out-of-enum detail', () => {
698
+ const schema = toolByName(allTools(), 'memory_search').config.parametersSchema;
699
+ expect(schema.convert({ detail: 'full', offset: 2 })).toSucceed();
700
+ expect(schema.convert({ detail: 'gist' })).toSucceed();
701
+ expect(schema.convert({ offset: 'nope' })).toFail();
702
+ // detail is now an enum ('gist' | 'full') at the wire-schema level.
703
+ expect(schema.convert({ detail: 'verbose' })).toFail();
704
+ });
705
+ });
706
+ describe('absent projector = byte-identical default projection', () => {
707
+ test('returns the full body and the default handle (raw id) when no projector is supplied', async () => {
708
+ const tool = searchToolWith({});
709
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
710
+ const out = value;
711
+ expect(out.results[0].handle).toBe('doc-1');
712
+ expect(out.results[0].body).toBe('body-doc-1');
713
+ });
714
+ });
715
+ test('the detail tier is inert without a projector (gist and full both yield the full body)', async () => {
716
+ const tool = searchToolWith({});
717
+ for (const detail of ['gist', 'full']) {
718
+ expect(await tool.execute({ tag: 'topic', detail })).toSucceedAndSatisfy((value) => {
719
+ expect(value.results[0].body).toBe('body-doc-1');
720
+ });
721
+ }
722
+ });
723
+ });
724
+ describe('host projector + detail tier', () => {
725
+ test('defaults to the bounded gist projection when detail is absent', async () => {
726
+ const tool = searchToolWith({ projectItem: boundingProjector });
727
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
728
+ const out = value;
729
+ expect(out.results[0].handle).toBe('h:doc-1');
730
+ expect(out.results[0].body).toBe('<<gist>>');
731
+ });
732
+ });
733
+ test('opts into the full body only when detail=full is requested', async () => {
734
+ const tool = searchToolWith({ projectItem: boundingProjector });
735
+ expect(await tool.execute({ tag: 'topic', detail: 'full' })).toSucceedAndSatisfy((value) => {
736
+ expect(value.results[0].body).toBe('body-doc-1');
737
+ });
738
+ });
739
+ test('an out-of-enum detail is rejected at the schema boundary', async () => {
740
+ // detail is a JsonSchema.enumOf(['gist','full']); the tool re-validates args
741
+ // on execute, so an out-of-enum value fails loudly rather than reaching the projector.
742
+ const tool = searchToolWith({ projectItem: boundingProjector });
743
+ expect(await tool.execute({ tag: 'topic', detail: 'verbose' })).toFailWith(/invalid arguments/i);
744
+ });
745
+ test('an explicit detail=gist stays bounded (the non-full branch)', async () => {
746
+ const tool = searchToolWith({ projectItem: boundingProjector });
747
+ expect(await tool.execute({ tag: 'topic', detail: 'gist' })).toSucceedAndSatisfy((value) => {
748
+ expect(value.results[0].body).toBe('<<gist>>');
749
+ });
750
+ });
751
+ test('memory_context routes its results through the host projector', async () => {
752
+ const tool = toolByName((0, index_1.createMemoryTools)({
753
+ store: makeStore(),
754
+ retriever: makeLinkRetriever([{ id: 'a', links: ['b'] }, { id: 'b' }]),
755
+ registry: registryWith([{ kind: knowledgeKind }]),
756
+ projectItem: boundingProjector
757
+ }), 'memory_context');
758
+ expect(await tool.execute({ from: { id: 'a', scope: 'knowledge' }, detail: 'full' })).toSucceedAndSatisfy((value) => {
759
+ const out = value;
760
+ expect(out.results.map((r) => r.handle)).toEqual(['h:b']);
761
+ expect(out.results[0].body).toBe('body-b');
762
+ });
763
+ });
764
+ test('a throwing host projector degrades to the default full-body projection (does not crash search)', async () => {
765
+ const tool = searchToolWith({
766
+ projectItem: () => {
767
+ throw new Error('host projector blew up');
768
+ }
769
+ });
770
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
771
+ const out = value;
772
+ // Degraded to the built-in default: raw-id handle + full body.
773
+ expect(out.results[0].handle).toBe('doc-1');
774
+ expect(out.results[0].body).toBe('body-doc-1');
775
+ });
776
+ });
777
+ test('a throwing projector still honors the handleFor hook in the fallback path', async () => {
778
+ const tool = searchToolWith({
779
+ handleFor: (r) => `@${r.envelope.id}`,
780
+ projectItem: () => {
781
+ throw new Error('nope');
782
+ }
783
+ });
784
+ expect(await tool.execute({ tag: 'topic' })).toSucceedAndSatisfy((value) => {
785
+ expect(value.results[0].handle).toBe('@doc-1');
786
+ });
787
+ });
788
+ });
789
+ describe('offset threading (memory_search)', () => {
790
+ test('threads offset into the query to page the ordered result window', async () => {
791
+ const tool = toolByName((0, index_1.createMemoryTools)({
792
+ store: makeStore(),
793
+ retriever: makeRetriever([
794
+ { id: 'doc-1', tags: ['topic'], updated: 30 },
795
+ { id: 'doc-2', tags: ['topic'], updated: 20 },
796
+ { id: 'doc-3', tags: ['topic'], updated: 10 }
797
+ ]),
798
+ registry: registryWith([{ kind: knowledgeKind }])
799
+ }), 'memory_search');
800
+ // Recency-ordered [doc-1, doc-2, doc-3]; offset 1 + limit 1 → [doc-2].
801
+ expect(await tool.execute({ tag: 'topic', offset: 1, limit: 1 })).toSucceedAndSatisfy((value) => {
802
+ const out = value;
803
+ expect(out.count).toBe(1);
804
+ expect(out.results.map((r) => r.handle)).toEqual(['doc-2']);
805
+ });
806
+ });
807
+ });
808
+ describe('memory_read detail (explicit drill-in defaults to FULL)', () => {
809
+ async function readToolWith(projector) {
810
+ const tools = (0, index_1.createMemoryTools)(Object.assign({ store: makeStore(), retriever: makeRetriever([]), registry: registryWith([{ kind: knowledgeKind }]), codecs, tools: ['memory_write', 'memory_read'] }, (projector !== undefined ? { projectItem: projector } : {})));
811
+ await toolByName(tools, 'memory_write').execute({
812
+ kind: 'knowledge',
813
+ entityId: 'doc-1',
814
+ body: 'FULLBODY'
815
+ });
816
+ return toolByName(tools, 'memory_read');
817
+ }
818
+ test('defaults to the FULL body (drill-in) with a bounding projector', async () => {
819
+ const tool = await readToolWith(boundingProjector);
820
+ expect(await tool.execute({ kind: 'knowledge', entityId: 'doc-1' })).toSucceedAndSatisfy((value) => {
821
+ expect(value.item.body).toBe('FULLBODY');
822
+ });
823
+ });
824
+ test('opts down to gist only when detail=gist is requested', async () => {
825
+ const tool = await readToolWith(boundingProjector);
826
+ expect(await tool.execute({ kind: 'knowledge', entityId: 'doc-1', detail: 'gist' })).toSucceedAndSatisfy((value) => {
827
+ expect(value.item.body).toBe('<<gist>>');
828
+ });
829
+ });
830
+ test('is byte-identical for a no-projector consumer (full body regardless of detail)', async () => {
831
+ const tool = await readToolWith(undefined);
832
+ for (const args of [
833
+ { kind: 'knowledge', entityId: 'doc-1' },
834
+ { kind: 'knowledge', entityId: 'doc-1', detail: 'gist' }
835
+ ]) {
836
+ expect(await tool.execute(args)).toSucceedAndSatisfy((value) => {
837
+ expect(value.item.body).toBe('FULLBODY');
838
+ });
839
+ }
840
+ });
841
+ });
842
+ });
574
843
  //# sourceMappingURL=memoryTools.test.js.map