@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.
- package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
- package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
- package/.rush/temp/operation/build/all.log +2 -2
- package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
- package/.rush/temp/operation/build/state.json +1 -1
- package/dist/packlets/converters/envelopeConverter.js +17 -3
- package/dist/packlets/converters/envelopeConverter.js.map +1 -1
- package/dist/packlets/index/memoryIndex.js +58 -10
- package/dist/packlets/index/memoryIndex.js.map +1 -1
- package/dist/packlets/ingest/cycleGuard.js +13 -6
- package/dist/packlets/ingest/cycleGuard.js.map +1 -1
- package/dist/packlets/ingest/hostStages.js.map +1 -1
- package/dist/packlets/ingest/model.js.map +1 -1
- package/dist/packlets/ingest/orchestrator.js +94 -49
- package/dist/packlets/ingest/orchestrator.js.map +1 -1
- package/dist/packlets/retrieve/hybridRetriever.js +13 -2
- package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
- package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +3 -3
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +50 -7
- package/dist/packlets/retrieve/retriever.js.map +1 -1
- package/dist/packlets/retrieve/semanticRetriever.js +9 -3
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +3 -3
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/tools/memoryTools.js +104 -21
- package/dist/packlets/tools/memoryTools.js.map +1 -1
- package/dist/packlets/types/envelope.js +13 -1
- package/dist/packlets/types/envelope.js.map +1 -1
- package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
- package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
- package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
- package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
- package/dist/test/unit/index/memoryIndex.test.js +87 -25
- package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
- package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
- package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
- package/dist/test/unit/ingest/orchestrator.test.js +187 -45
- package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
- package/dist/test/unit/retrieve/retrievers.test.js +299 -37
- package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
- package/dist/test/unit/store/embedOnWrite.test.js +69 -12
- package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
- package/dist/test/unit/store/listScoped.test.js +109 -0
- package/dist/test/unit/store/listScoped.test.js.map +1 -0
- package/dist/test/unit/store/rankAxis.test.js +254 -0
- package/dist/test/unit/store/rankAxis.test.js.map +1 -0
- package/dist/test/unit/tools/memoryTools.test.js +280 -11
- package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
- package/dist/test/unit/types/writePolicy.test.js +9 -2
- package/dist/test/unit/types/writePolicy.test.js.map +1 -1
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
- package/dist/test/unit/vector/vectorIndex.test.js +24 -15
- package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +384 -97
- package/etc/ts-agent-memory.api.md +64 -22
- package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
- package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
- package/lib/packlets/converters/envelopeConverter.js +18 -4
- package/lib/packlets/converters/envelopeConverter.js.map +1 -1
- package/lib/packlets/index/memoryIndex.d.ts +42 -10
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
- package/lib/packlets/index/memoryIndex.js +58 -10
- package/lib/packlets/index/memoryIndex.js.map +1 -1
- package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
- package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
- package/lib/packlets/ingest/cycleGuard.js +13 -6
- package/lib/packlets/ingest/cycleGuard.js.map +1 -1
- package/lib/packlets/ingest/hostStages.d.ts +3 -3
- package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
- package/lib/packlets/ingest/hostStages.js.map +1 -1
- package/lib/packlets/ingest/model.d.ts +25 -14
- package/lib/packlets/ingest/model.d.ts.map +1 -1
- package/lib/packlets/ingest/model.js.map +1 -1
- package/lib/packlets/ingest/orchestrator.d.ts +18 -4
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
- package/lib/packlets/ingest/orchestrator.js +93 -48
- package/lib/packlets/ingest/orchestrator.js.map +1 -1
- package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/hybridRetriever.js +12 -1
- package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +2 -2
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +70 -11
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +52 -7
- package/lib/packlets/retrieve/retriever.js.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.js +9 -3
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +2 -2
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/tools/memoryTools.d.ts +24 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
- package/lib/packlets/tools/memoryTools.js +104 -21
- package/lib/packlets/tools/memoryTools.js.map +1 -1
- package/lib/packlets/types/envelope.d.ts +61 -8
- package/lib/packlets/types/envelope.d.ts.map +1 -1
- package/lib/packlets/types/envelope.js +14 -0
- package/lib/packlets/types/envelope.js.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/lib/packlets/vector/vectorIndex.d.ts +46 -20
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
- package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
- package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
- package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
- package/lib/test/unit/index/memoryIndex.test.js +86 -24
- package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
- package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
- package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
- package/lib/test/unit/ingest/orchestrator.test.js +186 -44
- package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
- package/lib/test/unit/retrieve/retrievers.test.js +298 -36
- package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
- package/lib/test/unit/store/embedOnWrite.test.js +68 -11
- package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
- package/lib/test/unit/store/listScoped.test.d.ts +2 -0
- package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
- package/lib/test/unit/store/listScoped.test.js +111 -0
- package/lib/test/unit/store/listScoped.test.js.map +1 -0
- package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
- package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
- package/lib/test/unit/store/rankAxis.test.js +256 -0
- package/lib/test/unit/store/rankAxis.test.js.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.js +280 -11
- package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
- package/lib/test/unit/types/writePolicy.test.js +9 -2
- package/lib/test/unit/types/writePolicy.test.js.map +1 -1
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
- package/lib/test/unit/vector/vectorIndex.test.js +24 -15
- package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
- package/package.json +7 -7
- package/rush-logs/ts-agent-memory.build.cache.log +1 -1
- package/rush-logs/ts-agent-memory.build.log +2 -2
- package/src/packlets/converters/envelopeConverter.ts +27 -4
- package/src/packlets/index/memoryIndex.ts +86 -22
- package/src/packlets/ingest/cycleGuard.ts +22 -11
- package/src/packlets/ingest/hostStages.ts +3 -3
- package/src/packlets/ingest/model.ts +25 -14
- package/src/packlets/ingest/orchestrator.ts +143 -67
- package/src/packlets/retrieve/hybridRetriever.ts +14 -1
- package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
- package/src/packlets/retrieve/recencyRetriever.ts +3 -3
- package/src/packlets/retrieve/retriever.ts +97 -13
- package/src/packlets/retrieve/semanticRetriever.ts +10 -5
- package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
- package/src/packlets/retrieve/tagRetriever.ts +3 -3
- package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
- package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
- package/src/packlets/tools/memoryTools.ts +152 -25
- package/src/packlets/types/envelope.ts +66 -8
- package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
- package/src/packlets/vector/vectorIndex.ts +47 -20
- package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
- package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
- package/src/test/unit/index/memoryIndex.test.ts +99 -14
- package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
- package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
- package/src/test/unit/ingest/orchestrator.test.ts +234 -41
- package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
- package/src/test/unit/retrieve/retrievers.test.ts +381 -25
- package/src/test/unit/store/embedOnWrite.test.ts +83 -11
- package/src/test/unit/store/listScoped.test.ts +138 -0
- package/src/test/unit/store/rankAxis.test.ts +349 -0
- package/src/test/unit/tools/memoryTools.test.ts +362 -13
- package/src/test/unit/types/writePolicy.test.ts +11 -2
- package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
- package/src/test/unit/vector/vectorIndex.test.ts +33 -17
- package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/ts-agent-memory.api.json +1080 -136
- 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:
|
|
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:
|
|
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
|
-
/**
|
|
258
|
-
|
|
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 →
|
|
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) =>
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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({
|
|
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
|
-
/**
|
|
33
|
-
|
|
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
|
|
41
|
-
* Replaces bare string references (e.g.
|
|
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:
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
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<
|
|
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(
|
|
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 '${
|
|
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 '${
|
|
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(
|
|
70
|
-
// The in-memory index keys entries by
|
|
71
|
-
|
|
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(
|
|
76
|
-
this._vectors.delete(
|
|
77
|
-
return Promise.resolve(succeed(
|
|
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
|
|
95
|
-
hits.push({
|
|
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
|
|
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<
|
|
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
|
|
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(
|
|
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(
|
|
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 {
|
|
7
|
+
import { IEdgeTarget, IMemoryRecord } from '../types';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* A single hit returned by {@link IVectorIndex.query}: the matched record
|
|
11
|
-
*
|
|
12
|
-
* backend-defined). Hits are
|
|
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
|
|
17
|
-
readonly
|
|
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 `
|
|
40
|
-
* {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps
|
|
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(
|
|
53
|
+
add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
|
-
* Remove the embedding for `
|
|
47
|
-
*
|
|
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(
|
|
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.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* for {@link IVectorIndex}, so the reverse
|
|
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<
|
|
103
|
+
/** List every record in the vault, each paired with its scoped address. */
|
|
104
|
+
list(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
|
|
78
105
|
}
|