@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
@@ -6,7 +6,7 @@
6
6
  import { Result, captureResult, fail, succeed } from '@fgv/ts-utils';
7
7
  import { JsonSchema } from '@fgv/ts-json-base';
8
8
  import { AiAssist } from '@fgv/ts-extras';
9
- import { Convert, EntityId, IIdentityCodec, IMemoryRecord, Kind, MemoryId, Tag } from '../types';
9
+ import { Convert, EntityId, IEdgeTarget, IIdentityCodec, IMemoryRecord, Kind, MemoryId, Tag } from '../types';
10
10
  import { IBodyConverterRegistry, envelopeConverter } from '../converters';
11
11
  import { IMemoryStore } from '../store';
12
12
  import { IMemoryQuery, IMemoryRetriever } from '../retrieve';
@@ -135,8 +135,33 @@ export interface ICreateMemoryToolsParams {
135
135
  * {@link MemoryId} is used.
136
136
  */
137
137
  readonly handleFor?: (record: IMemoryRecord<unknown>) => string;
138
+ /**
139
+ * Optional host projector mapping a record (and the requested detail tier) to
140
+ * its agent-visible {@link IMemoryToolResultItem}. When supplied, every
141
+ * `memory_search` / `memory_context` / `memory_read` result item is produced by
142
+ * this callback — the host owns how much of the body a `'gist'` vs `'full'`
143
+ * result carries, so it can bound the default (`'gist'`) path.
144
+ *
145
+ * When absent, the built-in default projection is used (full body plus the
146
+ * {@link ICreateMemoryToolsParams.handleFor | handleFor} handle), which ignores
147
+ * the detail tier — behavior is byte-identical to a build with no projector.
148
+ *
149
+ * The callback is guarded exactly like `handleFor`: a throw degrades to the
150
+ * default full-body projection for that item rather than failing the whole
151
+ * search.
152
+ */
153
+ readonly projectItem?: (record: IMemoryRecord<unknown>, detail: MemoryDetailTier) => IMemoryToolResultItem;
138
154
  }
139
155
 
156
+ /**
157
+ * The detail tier a `memory_search` / `memory_context` result is projected at.
158
+ * `'gist'` is the default (bounded) path; `'full'` is opt-in. Only meaningful
159
+ * when a host {@link ICreateMemoryToolsParams.projectItem | projectItem} is
160
+ * supplied — the built-in default projection returns the full body regardless.
161
+ * @public
162
+ */
163
+ export type MemoryDetailTier = 'gist' | 'full';
164
+
140
165
  /** The resolved factory context threaded into each tool's `execute`. */
141
166
  interface IToolContext {
142
167
  readonly store: IMemoryStore;
@@ -146,6 +171,7 @@ interface IToolContext {
146
171
  readonly codecs?: ReadonlyMap<Kind, IIdentityCodec>;
147
172
  readonly defaultCodec?: IIdentityCodec;
148
173
  readonly handleFor?: (record: IMemoryRecord<unknown>) => string;
174
+ readonly projectItem?: (record: IMemoryRecord<unknown>, detail: MemoryDetailTier) => IMemoryToolResultItem;
149
175
  }
150
176
 
151
177
  // ---------------------------------------------------------------------------
@@ -155,11 +181,27 @@ interface IToolContext {
155
181
  // gate is enforced structurally and asserted in the tests.
156
182
  // ---------------------------------------------------------------------------
157
183
 
184
+ /**
185
+ * The scope-qualified target of a link edge authored by the agent on a write.
186
+ * `scope` is optional: when omitted it defaults to the writing record's OWN
187
+ * resolved scope (the common same-conversation case); supply it explicitly to
188
+ * point an edge at a record in a different scope.
189
+ */
190
+ // eslint-disable-next-line @rushstack/typedef-var
191
+ const linkTargetSchema = JsonSchema.object({
192
+ id: JsonSchema.string({ description: 'The MemoryId of the record this edge points at.' }),
193
+ scope: JsonSchema.optional(
194
+ JsonSchema.string({
195
+ description: "The target record's scope. Defaults to the writing record's own scope when omitted."
196
+ })
197
+ )
198
+ });
199
+
158
200
  /** A single attributed link edge as authored by the agent on a write. */
159
201
  // eslint-disable-next-line @rushstack/typedef-var
160
202
  const linkEdgeSchema = JsonSchema.object({
161
203
  type: JsonSchema.string({ description: 'The relation type of the link.' }),
162
- target: JsonSchema.string({ description: 'The MemoryId this edge points at.' }),
204
+ target: linkTargetSchema,
163
205
  confidence: JsonSchema.optional(JsonSchema.number({ description: 'Optional confidence in [0, 1].' }))
164
206
  });
165
207
 
@@ -179,7 +221,10 @@ type WriteArgs = JsonSchema.Static<typeof writeSchema>;
179
221
  // eslint-disable-next-line @rushstack/typedef-var
180
222
  const readSchema = JsonSchema.object({
181
223
  kind: JsonSchema.string({ description: 'The record kind.' }),
182
- entityId: JsonSchema.string({ description: 'The domain entity id to read.' })
224
+ entityId: JsonSchema.string({ description: 'The domain entity id to read.' }),
225
+ detail: JsonSchema.optional(
226
+ JsonSchema.enumOf(['gist', 'full'] as const, { description: "'gist' | 'full' (default)." })
227
+ )
183
228
  });
184
229
 
185
230
  // eslint-disable-next-line @rushstack/typedef-var
@@ -195,16 +240,41 @@ const searchSchema = JsonSchema.object({
195
240
  semantic: JsonSchema.optional(
196
241
  JsonSchema.string({ description: 'Semantic query text (requires a semantic-capable retriever).' })
197
242
  ),
198
- limit: JsonSchema.optional(JsonSchema.integer({ description: 'Maximum number of results to return.' }))
243
+ limit: JsonSchema.optional(JsonSchema.integer({ description: 'Maximum number of results to return.' })),
244
+ offset: JsonSchema.optional(
245
+ JsonSchema.integer({ description: 'Number of results to skip after ordering, before limit. Default 0.' })
246
+ ),
247
+ detail: JsonSchema.optional(
248
+ JsonSchema.enumOf(['gist', 'full'] as const, { description: "'gist' (default) | 'full'." })
249
+ )
250
+ });
251
+
252
+ /**
253
+ * The scope-qualified seed a `memory_context` traversal starts from. Nested
254
+ * `{ id, scope }` shape like a link target, but — unlike a write edge — there is
255
+ * no writing record to default the scope from, so `scope` is REQUIRED to
256
+ * disambiguate the seed across scopes (a bare stem like `turn-3` is otherwise
257
+ * ambiguous). It is schema-required (not just runtime-required) so the wire
258
+ * schema an LLM reads never advertises an optionality the tool does not honor.
259
+ */
260
+ // eslint-disable-next-line @rushstack/typedef-var
261
+ const contextSeedSchema = JsonSchema.object({
262
+ id: JsonSchema.string({ description: 'The MemoryId of the seed record to traverse links from.' }),
263
+ scope: JsonSchema.string({
264
+ description: 'The scope of the seed record (required — a bare seed id is ambiguous across scopes).'
265
+ })
199
266
  });
200
267
 
201
268
  // eslint-disable-next-line @rushstack/typedef-var
202
269
  const contextSchema = JsonSchema.object({
203
- from: JsonSchema.string({ description: 'The seed MemoryId to traverse links from.' }),
270
+ from: contextSeedSchema,
204
271
  kind: JsonSchema.optional(JsonSchema.string({ description: 'Restrict reached records to this kind.' })),
205
272
  tag: JsonSchema.optional(JsonSchema.string({ description: 'Restrict reached records carrying this tag.' })),
206
273
  hops: JsonSchema.optional(JsonSchema.integer({ description: 'BFS hop count (default 1).' })),
207
- limit: JsonSchema.optional(JsonSchema.integer({ description: 'Maximum number of results to return.' }))
274
+ limit: JsonSchema.optional(JsonSchema.integer({ description: 'Maximum number of results to return.' })),
275
+ detail: JsonSchema.optional(
276
+ JsonSchema.enumOf(['gist', 'full'] as const, { description: "'gist' (default) | 'full'." })
277
+ )
208
278
  });
209
279
 
210
280
  // ---------------------------------------------------------------------------
@@ -254,8 +324,17 @@ function resolveOptionalKind(ctx: IToolContext, kindStr?: string): Result<Kind |
254
324
  return assertKindEnabled(ctx, kindStr);
255
325
  }
256
326
 
257
- /** Project a record into an agent-visible result item, applying the host handle hook when present. */
258
- function projectItem(ctx: IToolContext, record: IMemoryRecord<unknown>): IMemoryToolResultItem {
327
+ /**
328
+ * Resolve the requested detail tier from the optional tool `detail` string.
329
+ * `'full'` is the only opt-in value; every other input (absent, or an
330
+ * unrecognized string) resolves safely to the bounded default `'gist'`.
331
+ */
332
+ function resolveDetail(detail?: string): MemoryDetailTier {
333
+ return detail === 'full' ? 'full' : 'gist';
334
+ }
335
+
336
+ /** The built-in default projection: full body plus the guarded host handle. Ignores the detail tier. */
337
+ function defaultProjectItem(ctx: IToolContext, record: IMemoryRecord<unknown>): IMemoryToolResultItem {
259
338
  // `handleFor` is a host callback; guard it so a throw degrades to the raw id rather than
260
339
  // escaping the Result chain (and crashing the whole search/context call).
261
340
  const handle =
@@ -271,6 +350,27 @@ function projectItem(ctx: IToolContext, record: IMemoryRecord<unknown>): IMemory
271
350
  };
272
351
  }
273
352
 
353
+ /**
354
+ * Project a record into an agent-visible result item at the requested detail
355
+ * tier. When a host {@link ICreateMemoryToolsParams.projectItem | projectItem}
356
+ * is supplied it owns the projection; otherwise the built-in
357
+ * {@link defaultProjectItem} (full body) is used. The host callback is guarded
358
+ * like `handleFor` — a throw degrades to the default full-body projection for
359
+ * that item rather than failing the whole search/context call.
360
+ */
361
+ function projectItem(
362
+ ctx: IToolContext,
363
+ record: IMemoryRecord<unknown>,
364
+ detail: MemoryDetailTier
365
+ ): IMemoryToolResultItem {
366
+ if (ctx.projectItem === undefined) {
367
+ return defaultProjectItem(ctx, record);
368
+ }
369
+ // Guard the host projector like `handleFor`: a throw degrades to the built-in
370
+ // full-body projection (itself throw-safe) rather than escaping the chain.
371
+ return captureResult(() => ctx.projectItem!(record, detail)).orDefault(defaultProjectItem(ctx, record));
372
+ }
373
+
274
374
  /** Resolve the identity codec used by `memory_write` to derive the storage id. */
275
375
  function codecForWrite(ctx: IToolContext, kind: Kind): Result<IIdentityCodec> {
276
376
  const codec: IIdentityCodec | undefined = ctx.codecs?.get(kind) ?? ctx.defaultCodec;
@@ -285,13 +385,16 @@ function buildWriteRecord(
285
385
  typed: WriteArgs,
286
386
  kind: Kind,
287
387
  entityId: EntityId,
288
- idStem: string
388
+ idStem: string,
389
+ sourceScope: string
289
390
  ): Result<IMemoryRecord<unknown>> {
290
391
  // Plain shapes handed to `envelopeConverter`, which validates each field
291
- // (type → LinkType, target → MemoryId) and produces the branded IEdge[].
392
+ // (type → LinkType, target → { scope, id }) and produces the branded IEdge[].
393
+ // An edge target with no explicit `scope` defaults to the writing record's own
394
+ // resolved scope — the same-conversation case authors just an id.
292
395
  const links: ReadonlyArray<Record<string, unknown>> = (typed.links ?? []).map((link) => ({
293
396
  type: link.type,
294
- target: link.target,
397
+ target: { scope: link.target.scope ?? sourceScope, id: link.target.id },
295
398
  ...(link.confidence !== undefined ? { confidence: link.confidence } : {})
296
399
  }));
297
400
  return envelopeConverter
@@ -323,7 +426,7 @@ function prepareWrite(
323
426
  if (addr.isVersioned) {
324
427
  return fail(`memory_write: versioned/temporal kind '${kind}' is not supported`);
325
428
  }
326
- return buildWriteRecord(typed, kind, entityId, addr.idStem).onSuccess((record) =>
429
+ return buildWriteRecord(typed, kind, entityId, addr.idStem, addr.scope).onSuccess((record) =>
327
430
  succeed({ kind, entityId, record })
328
431
  );
329
432
  })
@@ -418,14 +521,19 @@ function buildReadTool(ctx: IToolContext): AiAssist.IAiClientTool {
418
521
  .withErrorFormat((msg) => `memory_read: invalid arguments: ${msg}`)
419
522
  .onSuccess((typed) =>
420
523
  assertKindEnabled(ctx, typed.kind).onSuccess((kind) =>
421
- Convert.entityId.convert(typed.entityId).onSuccess((entityId) => succeed({ kind, entityId }))
524
+ Convert.entityId.convert(typed.entityId).onSuccess((entityId) => {
525
+ // `memory_read` is the explicit drill-in path, so its detail default is
526
+ // INVERTED vs search/context: `'full'` unless the caller opts down to `'gist'`.
527
+ const detail: MemoryDetailTier = typed.detail === 'gist' ? 'gist' : 'full';
528
+ return succeed({ kind, entityId, detail });
529
+ })
422
530
  )
423
531
  )
424
- .thenOnSuccess(async ({ kind, entityId }) =>
532
+ .thenOnSuccess(async ({ kind, entityId, detail }) =>
425
533
  (await ctx.store.get(kind, entityId)).onSuccess((record) =>
426
534
  record === undefined
427
535
  ? succeed({ found: false })
428
- : succeed({ found: true, item: projectItem(ctx, record) })
536
+ : succeed({ found: true, item: projectItem(ctx, record, detail) })
429
537
  )
430
538
  )
431
539
  };
@@ -450,14 +558,16 @@ function buildSearchTool(ctx: IToolContext): AiAssist.IAiClientTool {
450
558
  )
451
559
  )
452
560
  .thenOnSuccess(async ({ typed, kind, tag }) => {
561
+ const detail: MemoryDetailTier = resolveDetail(typed.detail);
453
562
  const query: IMemoryQuery = {
454
563
  ...(kind !== undefined ? { kind } : {}),
455
564
  ...(tag !== undefined ? { tag } : {}),
456
565
  ...(typed.semantic !== undefined ? { semantic: typed.semantic } : {}),
457
- ...(typed.limit !== undefined ? { limit: typed.limit } : {})
566
+ ...(typed.limit !== undefined ? { limit: typed.limit } : {}),
567
+ ...(typed.offset !== undefined ? { offset: typed.offset } : {})
458
568
  };
459
569
  return (await ctx.retriever.retrieve(query)).onSuccess((records) =>
460
- succeed({ count: records.length, results: records.map((r) => projectItem(ctx, r)) })
570
+ succeed({ count: records.length, results: records.map((r) => projectItem(ctx, r, detail)) })
461
571
  );
462
572
  })
463
573
  };
@@ -478,15 +588,14 @@ function buildContextTool(ctx: IToolContext): AiAssist.IAiClientTool {
478
588
  .convert(args)
479
589
  .withErrorFormat((msg) => `memory_context: invalid arguments: ${msg}`)
480
590
  .onSuccess((typed) =>
481
- Convert.memoryId
482
- .convert(typed.from)
483
- .onSuccess((from) =>
484
- resolveOptionalKind(ctx, typed.kind).onSuccess((kind) =>
485
- resolveOptionalTag(typed.tag).onSuccess((tag) => succeed({ typed, from, kind, tag }))
486
- )
591
+ resolveContextSeed(typed.from).onSuccess((from) =>
592
+ resolveOptionalKind(ctx, typed.kind).onSuccess((kind) =>
593
+ resolveOptionalTag(typed.tag).onSuccess((tag) => succeed({ typed, from, kind, tag }))
487
594
  )
595
+ )
488
596
  )
489
597
  .thenOnSuccess(async ({ typed, from, kind, tag }) => {
598
+ const detail: MemoryDetailTier = resolveDetail(typed.detail);
490
599
  const query: IMemoryQuery = {
491
600
  linkedFrom: from,
492
601
  ...(kind !== undefined ? { kind } : {}),
@@ -495,7 +604,11 @@ function buildContextTool(ctx: IToolContext): AiAssist.IAiClientTool {
495
604
  ...(typed.limit !== undefined ? { limit: typed.limit } : {})
496
605
  };
497
606
  return (await ctx.retriever.retrieve(query)).onSuccess((records) =>
498
- succeed({ seed: from, count: records.length, results: records.map((r) => projectItem(ctx, r)) })
607
+ succeed({
608
+ seed: from,
609
+ count: records.length,
610
+ results: records.map((r) => projectItem(ctx, r, detail))
611
+ })
499
612
  );
500
613
  })
501
614
  };
@@ -527,6 +640,19 @@ function buildDeleteTool(ctx: IToolContext): AiAssist.IAiClientTool {
527
640
  };
528
641
  }
529
642
 
643
+ /**
644
+ * Resolve a `memory_context` seed argument into a scope-qualified
645
+ * {@link IEdgeTarget}. Both `id` and `scope` are present here — the tool's
646
+ * `parametersSchema` ({@link contextSeedSchema}) makes `scope` schema-required —
647
+ * so this only brands the two fields; a malformed value fails via the branded
648
+ * converters (e.g. a path-unsafe seed id).
649
+ */
650
+ function resolveContextSeed(from: { readonly id: string; readonly scope: string }): Result<IEdgeTarget> {
651
+ return Convert.memoryId
652
+ .convert(from.id)
653
+ .onSuccess((id) => Convert.scopeKey.convert(from.scope).onSuccess((scope) => succeed({ scope, id })));
654
+ }
655
+
530
656
  /** Validate an optional `tag` string (`undefined` passes through). */
531
657
  function resolveOptionalTag(tagStr?: string): Result<Tag | undefined> {
532
658
  if (tagStr === undefined) {
@@ -572,7 +698,8 @@ export function createMemoryTools(params: ICreateMemoryToolsParams): ReadonlyArr
572
698
  kinds: params.kinds,
573
699
  codecs: params.codecs,
574
700
  defaultCodec: params.defaultCodec,
575
- handleFor: params.handleFor
701
+ handleFor: params.handleFor,
702
+ projectItem: params.projectItem
576
703
  };
577
704
  const selected: ReadonlySet<MemoryToolName> = new Set<MemoryToolName>(params.tools ?? DEFAULT_MEMORY_TOOLS);
578
705
  return TOOL_BUILDERS.filter((builder) => selected.has(builder.name)).map((builder) => builder.build(ctx));
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
 
6
- import { EntityId, Kind, LinkType, MemoryId, Tag } from './ids';
6
+ import { EntityId, Kind, LinkType, MemoryId, MemoryScopeKey, Tag } from './ids';
7
7
 
8
8
  /**
9
9
  * Origin of a provenance attribution. Open vocabulary: the three named
@@ -29,24 +29,61 @@ export interface IProvenance {
29
29
  readonly model?: string;
30
30
  /** Optional confidence in `[0, 1]`. */
31
31
  readonly confidence?: number;
32
- /** Back-link to the source experience record. Enables the cross-kind provenance spine. */
33
- readonly derivedFrom?: MemoryId;
32
+ /**
33
+ * Scope-qualified back-link to the source record. Enables the cross-kind
34
+ * provenance spine. A scope-qualified {@link IEdgeTarget} (not a bare
35
+ * {@link MemoryId}) because per-scope codecs (e.g. the MTM codec's `turn-<n>`
36
+ * stems) legally reuse a stem across scopes, so a bare id would be ambiguous —
37
+ * the same reason {@link IEdge.target} is scope-qualified.
38
+ */
39
+ readonly derivedFrom?: IEdgeTarget;
34
40
  /** Opaque extension payload — consumer-owned, never interpreted by the store. */
35
41
  readonly [key: string]: unknown;
36
42
  }
37
43
 
44
+ /**
45
+ * The physical address of a linked-to record: the `(scope, id)` pair that
46
+ * uniquely identifies it. Both components are required because a bare
47
+ * {@link MemoryId} is NOT unique across scopes — per-scope codecs (e.g. the
48
+ * medium-term codec's `turn-<n>` stems) legally mint the same stem under
49
+ * different scopes, so an edge that carried only the id would be ambiguous.
50
+ * `(scope, id)` matches the store's `getById(scope, id)` addressing and the
51
+ * index's composite primary key.
52
+ * @public
53
+ */
54
+ export interface IEdgeTarget {
55
+ /** The scope the target record lives under. */
56
+ readonly scope: MemoryScopeKey;
57
+ /** The target record's stable file-stem id (unique WITHIN {@link IEdgeTarget.scope}). */
58
+ readonly id: MemoryId;
59
+ }
60
+
61
+ /**
62
+ * The canonical composite-key string for an {@link IEdgeTarget}: scope + id,
63
+ * NUL-separated. NUL is excluded from both components (scope segments are
64
+ * filename-safe; {@link MemoryId} is portable-filename-safe), so it is a
65
+ * collision-proof separator. This is the ONE canonicalization every consumer
66
+ * that keys on a scoped target uses — the backlink index, the cycle guard, and
67
+ * the ingest edge-validation path all route through it so their notions of
68
+ * "same target" cannot drift.
69
+ * @public
70
+ */
71
+ export function edgeTargetKey(target: IEdgeTarget): string {
72
+ return `${target.scope}\0${target.id}`;
73
+ }
74
+
38
75
  /**
39
76
  * An attributed link between two records. Carries the relation type, the
40
- * target id, and optional confidence / provenance / world-truth validity.
41
- * Replaces bare string references (e.g. PersonAIlity's `IMtmRef` becomes an
42
- * `IEdge` with `type: LinkType('mtm-ref')`).
77
+ * scope-qualified {@link IEdgeTarget | target}, and optional confidence /
78
+ * provenance / world-truth validity. Replaces bare string references (e.g.
79
+ * PersonAIlity's `IMtmRef` becomes an `IEdge` with `type: LinkType('mtm-ref')`).
43
80
  * @public
44
81
  */
45
82
  export interface IEdge {
46
83
  /** Open-vocabulary relation type. */
47
84
  readonly type: LinkType;
48
- /** The linked-to record. */
49
- readonly target: MemoryId;
85
+ /** The scope-qualified address of the linked-to record. */
86
+ readonly target: IEdgeTarget;
50
87
  /** Optional confidence in `[0, 1]`. */
51
88
  readonly confidence?: number;
52
89
  /** Optional structured provenance for the link itself. */
@@ -108,6 +145,15 @@ export interface IMemoryEnvelope {
108
145
  * an exact match is a no-op upsert that returns the existing record.
109
146
  */
110
147
  readonly contentHash: string;
148
+ /**
149
+ * Store-computed host-defined ordering value, produced by the kind's
150
+ * {@link RankProjector} on every put/update and stamped into the envelope in
151
+ * the same pass that recomputes {@link IMemoryEnvelope.contentHash | contentHash}.
152
+ * Absent when the kind has no registered projector (or the projector threw on
153
+ * this record). Ordered retrieval (`orderBy: 'rank'`) and the index's rank view
154
+ * sort by this value descending, placing records with an absent `rank` last.
155
+ */
156
+ readonly rank?: number;
111
157
  /** Structured provenance (never a flat enum). */
112
158
  readonly provenance: IProvenance;
113
159
 
@@ -136,3 +182,15 @@ export interface IMemoryRecord<TBody = unknown> {
136
182
  /** The per-kind, Converter-validated body. */
137
183
  readonly body: TBody;
138
184
  }
185
+
186
+ /**
187
+ * A per-kind host projection from a fully-resolved (post-merge) memory record
188
+ * to a numeric ordering value. Registered per kind at store construction (see
189
+ * `rankProjectors`); the store runs it on every put/update over the same
190
+ * resolved record whose `contentHash` it computes, stamping the result into
191
+ * {@link IMemoryEnvelope.rank}. The store never interprets the body — the host
192
+ * owns what the number means. A projector that throws is treated as "no rank
193
+ * for this record" (logged at `warn`), never failing the write.
194
+ * @public
195
+ */
196
+ export type RankProjector = (record: IMemoryRecord<unknown>) => number;
@@ -4,8 +4,20 @@
4
4
  */
5
5
 
6
6
  import { Result, fail, succeed } from '@fgv/ts-utils';
7
- import { IMemoryRecord, MemoryId } from '../types';
8
- import { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, MemoryEmbedder } from './vectorIndex';
7
+ import { IEdgeTarget, edgeTargetKey } from '../types';
8
+ import {
9
+ IMemoryRecordSource,
10
+ IScopedMemoryRecord,
11
+ IVectorIndex,
12
+ IVectorQueryHit,
13
+ MemoryEmbedder
14
+ } from './vectorIndex';
15
+
16
+ /** One stored embedding: the scope-qualified address plus its vector. */
17
+ interface IStoredVector {
18
+ readonly target: IEdgeTarget;
19
+ readonly vector: Float32Array;
20
+ }
9
21
 
10
22
  /**
11
23
  * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one
@@ -31,12 +43,17 @@ import { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, MemoryEmbedder } fr
31
43
  * @public
32
44
  */
33
45
  export class InMemoryCosineIndex implements IVectorIndex {
34
- private readonly _vectors: Map<MemoryId, Float32Array>;
46
+ /**
47
+ * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the
48
+ * record's scope-qualified address, so two records that share a filename stem
49
+ * across scopes occupy distinct entries and never overwrite each other.
50
+ */
51
+ private readonly _vectors: Map<string, IStoredVector>;
35
52
  /** The dimension of every stored vector; `undefined` until the first `add`. */
36
53
  private _dimension: number | undefined;
37
54
 
38
55
  private constructor() {
39
- this._vectors = new Map<MemoryId, Float32Array>();
56
+ this._vectors = new Map<string, IStoredVector>();
40
57
  this._dimension = undefined;
41
58
  }
42
59
 
@@ -51,30 +68,32 @@ export class InMemoryCosineIndex implements IVectorIndex {
51
68
  }
52
69
 
53
70
  /** {@inheritDoc IVectorIndex.add} */
54
- public add(id: MemoryId, vector: Float32Array): Promise<Result<string>> {
71
+ public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {
72
+ const key: string = edgeTargetKey(target);
55
73
  if (vector.length === 0) {
56
- return Promise.resolve(fail(`vector index: cannot add '${id}': empty vector`));
74
+ return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));
57
75
  }
58
76
  if (this._dimension === undefined) {
59
77
  this._dimension = vector.length;
60
78
  } else if (vector.length !== this._dimension) {
61
79
  return Promise.resolve(
62
80
  fail(
63
- `vector index: cannot add '${id}': dimension ${vector.length} does not match index dimension ${this._dimension}`
81
+ `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`
64
82
  )
65
83
  );
66
84
  }
67
85
  // Defensive copy: the caller may reuse or mutate the buffer after `add`, and
68
86
  // the index must keep serving the embedding it was given.
69
- this._vectors.set(id, Float32Array.from(vector));
70
- // The in-memory index keys entries by id, so the id IS the entry reference.
71
- return Promise.resolve(succeed(id as string));
87
+ this._vectors.set(key, { target, vector: Float32Array.from(vector) });
88
+ // The in-memory index keys entries by the canonical scoped-target string, so
89
+ // that key IS the entry reference.
90
+ return Promise.resolve(succeed(key));
72
91
  }
73
92
 
74
93
  /** {@inheritDoc IVectorIndex.remove} */
75
- public remove(id: MemoryId): Promise<Result<MemoryId>> {
76
- this._vectors.delete(id);
77
- return Promise.resolve(succeed(id));
94
+ public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {
95
+ this._vectors.delete(edgeTargetKey(target));
96
+ return Promise.resolve(succeed(target));
78
97
  }
79
98
 
80
99
  /** {@inheritDoc IVectorIndex.query} */
@@ -91,8 +110,11 @@ export class InMemoryCosineIndex implements IVectorIndex {
91
110
  }
92
111
  const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);
93
112
  const hits: IVectorQueryHit[] = [];
94
- for (const [id, stored] of this._vectors) {
95
- hits.push({ id, score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored) });
113
+ for (const stored of this._vectors.values()) {
114
+ hits.push({
115
+ target: stored.target,
116
+ score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)
117
+ });
96
118
  }
97
119
  // Descending by score; a `seq`-free tiebreak is unnecessary here because the
98
120
  // caller (SemanticRetriever) re-resolves hits against the record index.
@@ -110,8 +132,7 @@ export class InMemoryCosineIndex implements IVectorIndex {
110
132
  * rather than left in a partially-rebuilt state — a caller that retries a query
111
133
  * after a failed rebuild sees a clean empty index, never a half-populated one.
112
134
  *
113
- * @param source - The record source to re-embed (an {@link IMemoryStore}
114
- * satisfies this structurally).
135
+ * @param source - The scope-qualified record source to re-embed.
115
136
  * @param embed - The embedder applied to each record.
116
137
  */
117
138
  public async rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder): Promise<Result<number>> {
@@ -119,17 +140,19 @@ export class InMemoryCosineIndex implements IVectorIndex {
119
140
  // even when the listing itself fails (no stale vectors survive a failed
120
141
  // rebuild).
121
142
  this._reset();
122
- const listed: Result<ReadonlyArray<IMemoryRecord<unknown>>> = await source.list();
143
+ const listed: Result<ReadonlyArray<IScopedMemoryRecord>> = await source.list();
123
144
  if (listed.isFailure()) {
124
145
  return fail(`vector index rebuild: failed to list records: ${listed.message}`);
125
146
  }
126
- for (const record of listed.value) {
127
- const embedded: Result<Float32Array> = await embed(record);
147
+ for (const scoped of listed.value) {
148
+ const embedded: Result<Float32Array> = await embed(scoped.record);
128
149
  if (embedded.isFailure()) {
129
150
  this._reset();
130
- return fail(`vector index rebuild: embedding '${record.envelope.id}' failed: ${embedded.message}`);
151
+ return fail(
152
+ `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${embedded.message}`
153
+ );
131
154
  }
132
- const added: Result<string> = await this.add(record.envelope.id, embedded.value);
155
+ const added: Result<string> = await this.add(scoped.target, embedded.value);
133
156
  if (added.isFailure()) {
134
157
  this._reset();
135
158
  return fail(`vector index rebuild: ${added.message}`);
@@ -4,17 +4,25 @@
4
4
  */
5
5
 
6
6
  import { Result } from '@fgv/ts-utils';
7
- import { IMemoryRecord, MemoryId } from '../types';
7
+ import { IEdgeTarget, IMemoryRecord } from '../types';
8
8
 
9
9
  /**
10
- * A single hit returned by {@link IVectorIndex.query}: the matched record id and
11
- * the backend's similarity score (higher = more similar; the exact scale is
12
- * backend-defined). Hits are returned in descending score order.
10
+ * A single hit returned by {@link IVectorIndex.query}: the matched record's
11
+ * scope-qualified {@link IEdgeTarget | address} and the backend's similarity
12
+ * score (higher = more similar; the exact scale is backend-defined). Hits are
13
+ * returned in descending score order.
14
+ *
15
+ * @remarks
16
+ * The address is a `(scope, id)` pair, NOT a bare {@link MemoryId} — per-scope
17
+ * codecs (e.g. the medium-term codec's `turn-<n>` stems) legally mint the same
18
+ * stem under different scopes, so a bare id could not disambiguate two records
19
+ * that share a stem. The caller re-resolves the hit against the record index by
20
+ * the same scoped address.
13
21
  * @public
14
22
  */
15
23
  export interface IVectorQueryHit {
16
- /** The id of the matched record. */
17
- readonly id: MemoryId;
24
+ /** The scope-qualified address of the matched record. */
25
+ readonly target: IEdgeTarget;
18
26
  /** Backend similarity score; higher is more similar. */
19
27
  readonly score: number;
20
28
  }
@@ -36,17 +44,20 @@ export interface IVectorQueryHit {
36
44
  */
37
45
  export interface IVectorIndex {
38
46
  /**
39
- * Add (or replace) the embedding for `id`. Returns the opaque
40
- * {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps onto
41
- * the envelope so a later read knows the record is embedded.
47
+ * Add (or replace) the embedding for the scope-qualified `target`. Returns the
48
+ * opaque {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps
49
+ * onto the envelope so a later read knows the record is embedded. Keying on the
50
+ * `(scope, id)` address (not a bare id) is load-bearing: two records that share
51
+ * a filename stem across scopes must not clobber each other's embedding.
42
52
  */
43
- add(id: MemoryId, vector: Float32Array): Promise<Result<string>>;
53
+ add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
44
54
 
45
55
  /**
46
- * Remove the embedding for `id`. Returns the removed id. Idempotent — removing
47
- * an id with no embedding still succeeds (returns the id).
56
+ * Remove the embedding for the scope-qualified `target`. Returns the removed
57
+ * target. Idempotent — removing a target with no embedding still succeeds
58
+ * (returns the target).
48
59
  */
49
- remove(id: MemoryId): Promise<Result<MemoryId>>;
60
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
50
61
 
51
62
  /**
52
63
  * Return the `topK` nearest records to `vector`, in descending score order.
@@ -63,16 +74,32 @@ export interface IVectorIndex {
63
74
  */
64
75
  export type MemoryEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<Float32Array>>;
65
76
 
77
+ /**
78
+ * A record paired with its scope-qualified {@link IEdgeTarget | address}, as
79
+ * yielded by {@link IMemoryRecordSource.list}. The address is required because
80
+ * {@link InMemoryCosineIndex.rebuild} keys each re-embedded entry on the
81
+ * scope-qualified target, not a bare {@link MemoryId} — two records that share a
82
+ * filename stem across scopes must not collide when the whole vault is re-indexed.
83
+ * @public
84
+ */
85
+ export interface IScopedMemoryRecord {
86
+ /** The record's scope-qualified `(scope, id)` address. */
87
+ readonly target: IEdgeTarget;
88
+ /** The record itself, passed to the embedder. */
89
+ readonly record: IMemoryRecord<unknown>;
90
+ }
91
+
66
92
  /**
67
93
  * The minimal record-source surface {@link InMemoryCosineIndex.rebuild} reads to
68
- * re-embed an entire vault. {@link IMemoryStore} satisfies it structurally (its
69
- * `list` accepts an optional filter, which is assignable to this no-argument
70
- * shape), so a consumer passes the store directly without the vector packlet
71
- * taking a dependency on the store packlet (which depends on the vector packlet
72
- * for {@link IVectorIndex}, so the reverse import would be a cycle).
94
+ * re-embed an entire vault. Each entry carries the record's scope-qualified
95
+ * address (see {@link IScopedMemoryRecord}) so the rebuild keys the vector index
96
+ * exactly as the incremental embed-on-write path does. A consumer backs this with
97
+ * the store's scoped index the vector packlet does not import the store packlet
98
+ * (which depends on the vector packlet for {@link IVectorIndex}, so the reverse
99
+ * import would be a cycle).
73
100
  * @public
74
101
  */
75
102
  export interface IMemoryRecordSource {
76
- /** List every record in the vault. */
77
- list(): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
103
+ /** List every record in the vault, each paired with its scoped address. */
104
+ list(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
78
105
  }