@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
|
@@ -4,23 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import '@fgv/ts-utils-jest';
|
|
6
6
|
import { fail, succeed } from '@fgv/ts-utils';
|
|
7
|
-
import { HybridRetriever, MemoryIndex, RecencyRetriever, ScoreUnionMergeStrategy, SemanticRetriever, StructuredFilterRetriever, TagRetriever, envelopeConverter, guardRetrieverCapabilities, limitRecords } from '../../../index';
|
|
7
|
+
import { HybridRetriever, MemoryIndex, RecencyRetriever, ScoreUnionMergeStrategy, SemanticRetriever, StructuredFilterRetriever, TagRetriever, envelopeConverter, guardRetrieverCapabilities, limitRecords, orderingCompare, rankCompare, recencyCompare } from '../../../index';
|
|
8
|
+
/** A scope-qualified link-traversal seed for the retriever tests. */
|
|
9
|
+
function et(id, scope = 'knowledge') {
|
|
10
|
+
return { scope: scope, id: id };
|
|
11
|
+
}
|
|
8
12
|
function makeEntry(spec) {
|
|
9
13
|
var _a, _b, _c, _d, _e;
|
|
10
14
|
const record = {
|
|
11
15
|
envelope: envelopeConverter
|
|
12
|
-
.convert({
|
|
13
|
-
id: spec.id,
|
|
14
|
-
entityId: spec.id,
|
|
15
|
-
kind: (_a = spec.kind) !== null && _a !== void 0 ? _a : 'knowledge',
|
|
16
|
-
tags: (_b = spec.tags) !== null && _b !== void 0 ? _b : [],
|
|
17
|
-
links: [],
|
|
18
|
-
created: 0,
|
|
19
|
-
updated: (_c = spec.updated) !== null && _c !== void 0 ? _c : 0,
|
|
20
|
-
seq: (_d = spec.seq) !== null && _d !== void 0 ? _d : 0,
|
|
21
|
-
contentHash: 'h',
|
|
22
|
-
provenance: { source: 'agent' }
|
|
23
|
-
})
|
|
16
|
+
.convert(Object.assign(Object.assign({ id: spec.id, entityId: spec.id, kind: (_a = spec.kind) !== null && _a !== void 0 ? _a : 'knowledge', tags: (_b = spec.tags) !== null && _b !== void 0 ? _b : [], links: [], created: 0, updated: (_c = spec.updated) !== null && _c !== void 0 ? _c : 0, seq: (_d = spec.seq) !== null && _d !== void 0 ? _d : 0, contentHash: 'h' }, (spec.rank !== undefined ? { rank: spec.rank } : {})), { provenance: { source: 'agent' } }))
|
|
24
17
|
.orThrow(),
|
|
25
18
|
body: `body-${spec.id}`
|
|
26
19
|
};
|
|
@@ -55,12 +48,12 @@ describe('retrieve helpers', () => {
|
|
|
55
48
|
expect(guardRetrieverCapabilities({ asOf: 5, kind: knowledge }, noCaps)).toFailWith(/none configured for kind knowledge/i);
|
|
56
49
|
});
|
|
57
50
|
test('fails loudly when a link-traversal axis is requested but unsupported', () => {
|
|
58
|
-
expect(guardRetrieverCapabilities({ linkedTo: 'x' }, noCaps)).toFailWith(/link traversal requires a backlink index; none configured/i);
|
|
59
|
-
expect(guardRetrieverCapabilities({ linkedFrom: 'x' }, noCaps)).toFailWith(/link traversal requires/i);
|
|
51
|
+
expect(guardRetrieverCapabilities({ linkedTo: et('x') }, noCaps)).toFailWith(/link traversal requires a backlink index; none configured/i);
|
|
52
|
+
expect(guardRetrieverCapabilities({ linkedFrom: et('x') }, noCaps)).toFailWith(/link traversal requires/i);
|
|
60
53
|
expect(guardRetrieverCapabilities({ hops: 2 }, noCaps)).toFailWith(/link traversal requires/i);
|
|
61
54
|
});
|
|
62
55
|
test('passes a link-traversal axis when the capability is supported', () => {
|
|
63
|
-
expect(guardRetrieverCapabilities({ linkedTo: 'x' }, Object.assign(Object.assign({}, noCaps), { supportsLinkTraversal: true }))).toSucceed();
|
|
56
|
+
expect(guardRetrieverCapabilities({ linkedTo: et('x') }, Object.assign(Object.assign({}, noCaps), { supportsLinkTraversal: true }))).toSucceed();
|
|
64
57
|
});
|
|
65
58
|
});
|
|
66
59
|
describe('limitRecords', () => {
|
|
@@ -77,6 +70,25 @@ describe('retrieve helpers', () => {
|
|
|
77
70
|
expect(limitRecords(records, 0)).toEqual([]);
|
|
78
71
|
expect(limitRecords(records, -1)).toEqual([]);
|
|
79
72
|
});
|
|
73
|
+
test('an absent offset is a no-op (byte-identical to today) — same reference returned', () => {
|
|
74
|
+
expect(limitRecords(records, undefined, undefined)).toBe(records);
|
|
75
|
+
expect(limitRecords(records, 2, undefined)).toHaveLength(2);
|
|
76
|
+
// A non-positive offset is guarded like a non-positive limit — no skip.
|
|
77
|
+
expect(limitRecords(records, undefined, 0)).toBe(records);
|
|
78
|
+
expect(ids(limitRecords(records, undefined, -5))).toEqual(['a', 'b', 'c']);
|
|
79
|
+
});
|
|
80
|
+
test('offset skips after ordering; { offset, limit } is a stable page window', () => {
|
|
81
|
+
expect(ids(limitRecords(records, undefined, 1))).toEqual(['b', 'c']);
|
|
82
|
+
expect(ids(limitRecords(records, 1, 1))).toEqual(['b']);
|
|
83
|
+
expect(ids(limitRecords(records, 2, 1))).toEqual(['b', 'c']);
|
|
84
|
+
});
|
|
85
|
+
test('an offset past the end yields an empty page (never a throw)', () => {
|
|
86
|
+
expect(limitRecords(records, undefined, 3)).toEqual([]);
|
|
87
|
+
expect(limitRecords(records, 5, 99)).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
test('a non-positive limit still wins over an offset window', () => {
|
|
90
|
+
expect(limitRecords(records, 0, 1)).toEqual([]);
|
|
91
|
+
});
|
|
80
92
|
});
|
|
81
93
|
});
|
|
82
94
|
describe('RecencyRetriever', () => {
|
|
@@ -142,7 +154,200 @@ describe('RecencyRetriever', () => {
|
|
|
142
154
|
});
|
|
143
155
|
test('degrades loudly on a link-traversal request', async () => {
|
|
144
156
|
const r = RecencyRetriever.create(buildIndex([{ id: 'a' }])).orThrow();
|
|
145
|
-
expect(await r.retrieve({ linkedTo: 'a' })).toFailWith(/link traversal requires/i);
|
|
157
|
+
expect(await r.retrieve({ linkedTo: et('a') })).toFailWith(/link traversal requires/i);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('orderBy: rank axis', () => {
|
|
161
|
+
const a1 = makeEntry({ id: 'a', rank: 1, updated: 100, seq: 1 }).record;
|
|
162
|
+
const b3 = makeEntry({ id: 'b', rank: 3, updated: 100, seq: 2 }).record;
|
|
163
|
+
const c2 = makeEntry({ id: 'c', rank: 2, updated: 100, seq: 3 }).record;
|
|
164
|
+
const unranked = makeEntry({ id: 'z', updated: 999, seq: 4 }).record;
|
|
165
|
+
describe('rankCompare', () => {
|
|
166
|
+
test('orders by rank descending', () => {
|
|
167
|
+
expect([a1, b3, c2]
|
|
168
|
+
.slice()
|
|
169
|
+
.sort(rankCompare)
|
|
170
|
+
.map((r) => r.envelope.id)).toEqual(['b', 'c', 'a']);
|
|
171
|
+
});
|
|
172
|
+
test('places an absent-rank record last regardless of recency', () => {
|
|
173
|
+
// `unranked` has the newest `updated`, but an absent rank sorts after every ranked record.
|
|
174
|
+
expect([unranked, a1, b3]
|
|
175
|
+
.slice()
|
|
176
|
+
.sort(rankCompare)
|
|
177
|
+
.map((r) => r.envelope.id)).toEqual(['b', 'a', 'z']);
|
|
178
|
+
// Symmetric: absent on the left side of the compare too.
|
|
179
|
+
expect([a1, unranked]
|
|
180
|
+
.slice()
|
|
181
|
+
.sort(rankCompare)
|
|
182
|
+
.map((r) => r.envelope.id)).toEqual(['a', 'z']);
|
|
183
|
+
});
|
|
184
|
+
test('breaks an equal-rank tie by recency', () => {
|
|
185
|
+
const b3b = makeEntry({ id: 'b2', rank: 3, updated: 50, seq: 9 }).record;
|
|
186
|
+
// Equal rank 3 → newer `updated` (b3 at 100) first.
|
|
187
|
+
expect([b3b, b3]
|
|
188
|
+
.slice()
|
|
189
|
+
.sort(rankCompare)
|
|
190
|
+
.map((r) => r.envelope.id)).toEqual(['b', 'b2']);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('orderingCompare', () => {
|
|
194
|
+
test("returns recencyCompare for absent orderBy and 'recency'", () => {
|
|
195
|
+
expect(orderingCompare()).toBe(recencyCompare);
|
|
196
|
+
expect(orderingCompare('recency')).toBe(recencyCompare);
|
|
197
|
+
});
|
|
198
|
+
test("returns rankCompare for 'rank'", () => {
|
|
199
|
+
expect(orderingCompare('rank')).toBe(rankCompare);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe.each([
|
|
203
|
+
['RecencyRetriever', (index) => RecencyRetriever.create(index).orThrow()],
|
|
204
|
+
['TagRetriever', (index) => TagRetriever.create(index).orThrow()],
|
|
205
|
+
[
|
|
206
|
+
'StructuredFilterRetriever',
|
|
207
|
+
(index) => StructuredFilterRetriever.create(index).orThrow()
|
|
208
|
+
]
|
|
209
|
+
])('%s honors orderBy', (name, make) => {
|
|
210
|
+
// Tag / structured-filter need their axis present to return anything.
|
|
211
|
+
const axis = name === 'TagRetriever'
|
|
212
|
+
? { tag: 'x' }
|
|
213
|
+
: name === 'StructuredFilterRetriever'
|
|
214
|
+
? { filter: () => true }
|
|
215
|
+
: {};
|
|
216
|
+
function orderedIndex() {
|
|
217
|
+
return buildIndex([
|
|
218
|
+
{ id: 'a', rank: 1, tags: ['x'], updated: 100, seq: 1 },
|
|
219
|
+
{ id: 'b', rank: 3, tags: ['x'], updated: 100, seq: 2 },
|
|
220
|
+
{ id: 'c', rank: 2, tags: ['x'], updated: 100, seq: 3 },
|
|
221
|
+
{ id: 'z', tags: ['x'], updated: 999, seq: 4 }
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
test("orderBy 'rank' orders by rank descending, absent last", async () => {
|
|
225
|
+
const r = make(orderedIndex());
|
|
226
|
+
expect(await r.retrieve(Object.assign(Object.assign({}, axis), { orderBy: 'rank' }))).toSucceedAndSatisfy((records) => {
|
|
227
|
+
expect(ids(records)).toEqual(['b', 'c', 'a', 'z']);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
test('orderBy absent is unchanged recency order', async () => {
|
|
231
|
+
const r = make(orderedIndex());
|
|
232
|
+
expect(await r.retrieve(Object.assign({}, axis))).toSucceedAndSatisfy((records) => {
|
|
233
|
+
// `z` has the newest `updated`; the rest tie on `updated` and break by
|
|
234
|
+
// seq descending (c=3, b=2, a=1) — recency order regardless of rank.
|
|
235
|
+
expect(ids(records)).toEqual(['z', 'c', 'b', 'a']);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
test('rank ordering composes with { limit, offset } for a bounded top-M page', async () => {
|
|
239
|
+
const r = make(orderedIndex());
|
|
240
|
+
expect(await r.retrieve(Object.assign(Object.assign({}, axis), { orderBy: 'rank', limit: 1, offset: 1 }))).toSucceedAndSatisfy((records) => {
|
|
241
|
+
expect(ids(records)).toEqual(['c']);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
test("HybridRetriever re-orders the merged set by rank for orderBy 'rank'", async () => {
|
|
246
|
+
const index = buildIndex([
|
|
247
|
+
{ id: 'a', rank: 1, tags: ['x'], updated: 100, seq: 1 },
|
|
248
|
+
{ id: 'b', rank: 3, tags: ['x'], updated: 100, seq: 2 },
|
|
249
|
+
{ id: 'c', rank: 2, tags: ['x'], updated: 100, seq: 3 }
|
|
250
|
+
]);
|
|
251
|
+
const hybrid = HybridRetriever.create([RecencyRetriever.create(index).orThrow(), TagRetriever.create(index).orThrow()], ScoreUnionMergeStrategy.create().orThrow()).orThrow();
|
|
252
|
+
expect(await hybrid.retrieve({ tag: 'x', orderBy: 'rank' })).toSucceedAndSatisfy((records) => {
|
|
253
|
+
// All three score 2 (surfaced by both children); orderBy rank re-sorts the merge.
|
|
254
|
+
expect(ids(records)).toEqual(['b', 'c', 'a']);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
test("SemanticRetriever preserves vector-similarity order regardless of orderBy 'rank'", async () => {
|
|
258
|
+
const index = buildIndex([
|
|
259
|
+
{ id: 'a', rank: 9, updated: 1 },
|
|
260
|
+
{ id: 'b', rank: 1, updated: 1 }
|
|
261
|
+
]);
|
|
262
|
+
const vectorIndex = {
|
|
263
|
+
add: (t) => Promise.resolve(succeed(`ref-${t.id}`)),
|
|
264
|
+
remove: (t) => Promise.resolve(succeed(t)),
|
|
265
|
+
query: () => Promise.resolve(succeed([
|
|
266
|
+
{ target: et('b'), score: 0.9 },
|
|
267
|
+
{ target: et('a'), score: 0.5 }
|
|
268
|
+
]))
|
|
269
|
+
};
|
|
270
|
+
const r = SemanticRetriever.create({
|
|
271
|
+
index,
|
|
272
|
+
backend: {
|
|
273
|
+
vectorIndex,
|
|
274
|
+
embedQuery: () => Promise.resolve(succeed(Float32Array.from([0.1])))
|
|
275
|
+
}
|
|
276
|
+
}).orThrow();
|
|
277
|
+
// Even though `a` has the higher rank, semantic keeps the similarity order (b before a).
|
|
278
|
+
expect(await r.retrieve({ semantic: 'q', orderBy: 'rank' })).toSucceedAndSatisfy((records) => {
|
|
279
|
+
expect(ids(records)).toEqual(['b', 'a']);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe('query axes: kinds (kind-set) and offset', () => {
|
|
284
|
+
function multiKindRetriever() {
|
|
285
|
+
return RecencyRetriever.create(buildIndex([
|
|
286
|
+
{ id: 'k1', kind: 'knowledge', updated: 40 },
|
|
287
|
+
{ id: 'n1', kind: 'note', updated: 30 },
|
|
288
|
+
{ id: 'e1', kind: 'event', updated: 20 },
|
|
289
|
+
{ id: 'k2', kind: 'knowledge', updated: 10 }
|
|
290
|
+
])).orThrow();
|
|
291
|
+
}
|
|
292
|
+
describe('kinds', () => {
|
|
293
|
+
test("an absent kinds axis imposes no kind constraint (today's behavior)", async () => {
|
|
294
|
+
expect(await multiKindRetriever().retrieve({})).toSucceedAndSatisfy((records) => {
|
|
295
|
+
expect([...ids(records)].sort()).toEqual(['e1', 'k1', 'k2', 'n1']);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
test('restricts to records in ANY of the listed kinds', async () => {
|
|
299
|
+
expect(await multiKindRetriever().retrieve({
|
|
300
|
+
kinds: ['knowledge', 'event']
|
|
301
|
+
})).toSucceedAndSatisfy((records) => {
|
|
302
|
+
expect([...ids(records)].sort()).toEqual(['e1', 'k1', 'k2']);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
test('an explicit empty kinds array matches NOTHING (not match-all)', async () => {
|
|
306
|
+
expect(await multiKindRetriever().retrieve({ kinds: [] })).toSucceedWith([]);
|
|
307
|
+
});
|
|
308
|
+
test('kind and kinds compose as AND (kind must be a member of kinds)', async () => {
|
|
309
|
+
// kind=knowledge AND kinds includes knowledge → knowledge records only.
|
|
310
|
+
expect(await multiKindRetriever().retrieve({
|
|
311
|
+
kind: knowledge,
|
|
312
|
+
kinds: ['knowledge', 'note']
|
|
313
|
+
})).toSucceedAndSatisfy((records) => {
|
|
314
|
+
expect([...ids(records)].sort()).toEqual(['k1', 'k2']);
|
|
315
|
+
});
|
|
316
|
+
// kind=knowledge but kinds excludes knowledge → nothing can satisfy both.
|
|
317
|
+
expect(await multiKindRetriever().retrieve({
|
|
318
|
+
kind: knowledge,
|
|
319
|
+
kinds: ['note', 'event']
|
|
320
|
+
})).toSucceedWith([]);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('offset', () => {
|
|
324
|
+
function ordered() {
|
|
325
|
+
return RecencyRetriever.create(buildIndex([
|
|
326
|
+
{ id: 'a', updated: 10 },
|
|
327
|
+
{ id: 'b', updated: 40 },
|
|
328
|
+
{ id: 'c', updated: 30 },
|
|
329
|
+
{ id: 'd', updated: 20 }
|
|
330
|
+
])).orThrow();
|
|
331
|
+
}
|
|
332
|
+
test('skips after ordering, forming a stable { offset, limit } page window', async () => {
|
|
333
|
+
// Recency order is [b, c, d, a].
|
|
334
|
+
expect(await ordered().retrieve({ offset: 1, limit: 2 })).toSucceedAndSatisfy((records) => {
|
|
335
|
+
expect(ids(records)).toEqual(['c', 'd']);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
test('an absent offset is unchanged from today', async () => {
|
|
339
|
+
expect(await ordered().retrieve({ limit: 2 })).toSucceedAndSatisfy((records) => {
|
|
340
|
+
expect(ids(records)).toEqual(['b', 'c']);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
test('an offset past the end yields an empty page', async () => {
|
|
344
|
+
expect(await ordered().retrieve({ offset: 99 })).toSucceedWith([]);
|
|
345
|
+
});
|
|
346
|
+
test('a negative offset does not slip into slice (treated as no skip)', async () => {
|
|
347
|
+
expect(await ordered().retrieve({ offset: -3, limit: 2 })).toSucceedAndSatisfy((records) => {
|
|
348
|
+
expect(ids(records)).toEqual(['b', 'c']);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
146
351
|
});
|
|
147
352
|
});
|
|
148
353
|
/** A retriever with configurable capabilities that records the query it received. */
|
|
@@ -197,11 +402,11 @@ class FakeVectorIndex {
|
|
|
197
402
|
this._hits = hits;
|
|
198
403
|
this._failQuery = failQuery;
|
|
199
404
|
}
|
|
200
|
-
add(
|
|
201
|
-
return Promise.resolve(succeed(`ref-${id}`));
|
|
405
|
+
add(t) {
|
|
406
|
+
return Promise.resolve(succeed(`ref-${t.id}`));
|
|
202
407
|
}
|
|
203
|
-
remove(
|
|
204
|
-
return Promise.resolve(succeed(
|
|
408
|
+
remove(t) {
|
|
409
|
+
return Promise.resolve(succeed(t));
|
|
205
410
|
}
|
|
206
411
|
query(__vector, topK) {
|
|
207
412
|
this.lastTopK = topK;
|
|
@@ -243,8 +448,8 @@ describe('SemanticRetriever', () => {
|
|
|
243
448
|
index,
|
|
244
449
|
backend: {
|
|
245
450
|
vectorIndex: new FakeVectorIndex([
|
|
246
|
-
{
|
|
247
|
-
{
|
|
451
|
+
{ target: et('b'), score: 0.9 },
|
|
452
|
+
{ target: et('a'), score: 0.5 }
|
|
248
453
|
]),
|
|
249
454
|
embedQuery: okEmbed
|
|
250
455
|
}
|
|
@@ -263,9 +468,9 @@ describe('SemanticRetriever', () => {
|
|
|
263
468
|
index,
|
|
264
469
|
backend: {
|
|
265
470
|
vectorIndex: new FakeVectorIndex([
|
|
266
|
-
{
|
|
267
|
-
{
|
|
268
|
-
{
|
|
471
|
+
{ target: et('gone'), score: 0.99 },
|
|
472
|
+
{ target: et('b'), score: 0.8 },
|
|
473
|
+
{ target: et('a'), score: 0.7 }
|
|
269
474
|
]),
|
|
270
475
|
embedQuery: okEmbed
|
|
271
476
|
}
|
|
@@ -274,12 +479,37 @@ describe('SemanticRetriever', () => {
|
|
|
274
479
|
expect(ids(records)).toEqual(['a']);
|
|
275
480
|
});
|
|
276
481
|
});
|
|
482
|
+
test('resolves a hit to the correctly-scoped record when two records share an id stem across scopes', async () => {
|
|
483
|
+
// Two records with the identical stem `turn-3` under different scopes — the
|
|
484
|
+
// exact same-stem-across-scopes collision the scope-qualified hit fixes. The
|
|
485
|
+
// retriever must re-resolve each hit through its full `(scope, id)` key, not a
|
|
486
|
+
// bare id (which would ambiguously match either record).
|
|
487
|
+
const index = buildIndex([
|
|
488
|
+
{ id: 'turn-3', scope: 'conv-a', tags: ['from-a'] },
|
|
489
|
+
{ id: 'turn-3', scope: 'conv-b', tags: ['from-b'] }
|
|
490
|
+
]);
|
|
491
|
+
const r = SemanticRetriever.create({
|
|
492
|
+
index,
|
|
493
|
+
backend: {
|
|
494
|
+
// The vector backend scored the conv-b record; the retriever must return
|
|
495
|
+
// exactly that record, never the same-stem conv-a record.
|
|
496
|
+
vectorIndex: new FakeVectorIndex([{ target: et('turn-3', 'conv-b'), score: 0.9 }]),
|
|
497
|
+
embedQuery: okEmbed
|
|
498
|
+
}
|
|
499
|
+
}).orThrow();
|
|
500
|
+
expect(await r.retrieve({ semantic: 'q' })).toSucceedAndSatisfy((records) => {
|
|
501
|
+
expect(records).toHaveLength(1);
|
|
502
|
+
expect(records[0].envelope.id).toBe('turn-3');
|
|
503
|
+
// The tag proves it is the conv-b record, not the same-stem conv-a one.
|
|
504
|
+
expect(records[0].envelope.tags).toEqual(['from-b']);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
277
507
|
test('forwards topK to the vector index and applies the post-filter limit', async () => {
|
|
278
508
|
const index = buildIndex([{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
|
|
279
509
|
const vectorIndex = new FakeVectorIndex([
|
|
280
|
-
{
|
|
281
|
-
{
|
|
282
|
-
{
|
|
510
|
+
{ target: et('a'), score: 0.9 },
|
|
511
|
+
{ target: et('b'), score: 0.8 },
|
|
512
|
+
{ target: et('c'), score: 0.7 }
|
|
283
513
|
]);
|
|
284
514
|
const r = SemanticRetriever.create({ index, backend: { vectorIndex, embedQuery: okEmbed } }).orThrow();
|
|
285
515
|
expect(await r.retrieve({ semantic: 'q', topK: 2, limit: 2 })).toSucceedAndSatisfy((records) => {
|
|
@@ -289,7 +519,7 @@ describe('SemanticRetriever', () => {
|
|
|
289
519
|
expect(vectorIndex.lastTopK).toBe(2);
|
|
290
520
|
});
|
|
291
521
|
test('defaults topK to 10 when the query omits it', async () => {
|
|
292
|
-
const vectorIndex = new FakeVectorIndex([{
|
|
522
|
+
const vectorIndex = new FakeVectorIndex([{ target: et('a'), score: 0.9 }]);
|
|
293
523
|
const r = SemanticRetriever.create({
|
|
294
524
|
index: buildIndex([{ id: 'a' }]),
|
|
295
525
|
backend: { vectorIndex, embedQuery: okEmbed }
|
|
@@ -327,8 +557,8 @@ describe('SemanticRetriever', () => {
|
|
|
327
557
|
test('normalizes a rejecting vector backend into a Failure', async () => {
|
|
328
558
|
// A vector index whose `query` rejects (throws) rather than returning a fail.
|
|
329
559
|
const rejectingIndex = {
|
|
330
|
-
add: (
|
|
331
|
-
remove: (
|
|
560
|
+
add: (t) => Promise.resolve(succeed(`ref-${t.id}`)),
|
|
561
|
+
remove: (t) => Promise.resolve(succeed(t)),
|
|
332
562
|
query: () => Promise.reject(new Error('socket hangup'))
|
|
333
563
|
};
|
|
334
564
|
const r = SemanticRetriever.create({
|
|
@@ -413,7 +643,7 @@ describe('HybridRetriever', () => {
|
|
|
413
643
|
const semantic = SemanticRetriever.create({
|
|
414
644
|
index,
|
|
415
645
|
backend: {
|
|
416
|
-
vectorIndex: new FakeVectorIndex([{
|
|
646
|
+
vectorIndex: new FakeVectorIndex([{ target: et('b'), score: 0.9 }]),
|
|
417
647
|
embedQuery: () => Promise.resolve(succeed(Float32Array.from([1])))
|
|
418
648
|
}
|
|
419
649
|
}).orThrow();
|
|
@@ -440,9 +670,9 @@ describe('HybridRetriever', () => {
|
|
|
440
670
|
const hybrid = HybridRetriever.create([linkChild, plainChild], ScoreUnionMergeStrategy.create().orThrow()).orThrow();
|
|
441
671
|
expect(hybrid.capabilities.supportsLinkTraversal).toBe(true);
|
|
442
672
|
// Union supports link traversal, so the hybrid does NOT loud-fail.
|
|
443
|
-
expect(await hybrid.retrieve({ linkedTo: 'a', hops: 2 })).toSucceed();
|
|
673
|
+
expect(await hybrid.retrieve({ linkedTo: et('a'), hops: 2 })).toSucceed();
|
|
444
674
|
// The link-capable child keeps the axes; the plain child has them stripped.
|
|
445
|
-
expect((_a = linkChild.lastQuery) === null || _a === void 0 ? void 0 : _a.linkedTo).
|
|
675
|
+
expect((_a = linkChild.lastQuery) === null || _a === void 0 ? void 0 : _a.linkedTo).toEqual(et('a'));
|
|
446
676
|
expect((_b = linkChild.lastQuery) === null || _b === void 0 ? void 0 : _b.hops).toBe(2);
|
|
447
677
|
expect((_c = plainChild.lastQuery) === null || _c === void 0 ? void 0 : _c.linkedTo).toBeUndefined();
|
|
448
678
|
expect((_d = plainChild.lastQuery) === null || _d === void 0 ? void 0 : _d.hops).toBeUndefined();
|
|
@@ -482,8 +712,8 @@ describe('HybridRetriever', () => {
|
|
|
482
712
|
index,
|
|
483
713
|
backend: {
|
|
484
714
|
vectorIndex: new FakeVectorIndex([
|
|
485
|
-
{
|
|
486
|
-
{
|
|
715
|
+
{ target: et('d'), score: 0.9 },
|
|
716
|
+
{ target: et('a'), score: 0.8 }
|
|
487
717
|
]),
|
|
488
718
|
embedQuery: () => Promise.resolve(succeed(Float32Array.from([1])))
|
|
489
719
|
}
|
|
@@ -502,5 +732,37 @@ describe('HybridRetriever', () => {
|
|
|
502
732
|
expect(records[0].envelope.id).toBe('b');
|
|
503
733
|
});
|
|
504
734
|
});
|
|
735
|
+
test('applies the { offset, limit } window after merge', async () => {
|
|
736
|
+
const { index } = build();
|
|
737
|
+
const recency = RecencyRetriever.create(index).orThrow();
|
|
738
|
+
const hybrid = HybridRetriever.create([recency], ScoreUnionMergeStrategy.create().orThrow()).orThrow();
|
|
739
|
+
// Merged recency order is [b(30), c(20), a(10)]; offset 1 + limit 1 → [c].
|
|
740
|
+
expect(await hybrid.retrieve({ offset: 1, limit: 1 })).toSucceedAndSatisfy((records) => {
|
|
741
|
+
expect(ids(records)).toEqual(['c']);
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
test('strips offset from child queries (offset is a post-merge concern)', async () => {
|
|
745
|
+
var _a, _b;
|
|
746
|
+
const child = new RecordingRetriever({
|
|
747
|
+
supportsSemanticRecall: false,
|
|
748
|
+
supportsTemporalQuery: false,
|
|
749
|
+
supportsLinkTraversal: false
|
|
750
|
+
});
|
|
751
|
+
const hybrid = HybridRetriever.create([child], ScoreUnionMergeStrategy.create().orThrow()).orThrow();
|
|
752
|
+
expect(await hybrid.retrieve({ offset: 2, limit: 3 })).toSucceed();
|
|
753
|
+
expect((_a = child.lastQuery) === null || _a === void 0 ? void 0 : _a.offset).toBeUndefined();
|
|
754
|
+
expect((_b = child.lastQuery) === null || _b === void 0 ? void 0 : _b.limit).toBeUndefined();
|
|
755
|
+
});
|
|
756
|
+
test('passes the kinds axis through to children (a shared pre-filter axis)', async () => {
|
|
757
|
+
var _a;
|
|
758
|
+
const child = new RecordingRetriever({
|
|
759
|
+
supportsSemanticRecall: false,
|
|
760
|
+
supportsTemporalQuery: false,
|
|
761
|
+
supportsLinkTraversal: false
|
|
762
|
+
});
|
|
763
|
+
const hybrid = HybridRetriever.create([child], ScoreUnionMergeStrategy.create().orThrow()).orThrow();
|
|
764
|
+
expect(await hybrid.retrieve({ kinds: ['knowledge'] })).toSucceed();
|
|
765
|
+
expect((_a = child.lastQuery) === null || _a === void 0 ? void 0 : _a.kinds).toEqual(['knowledge']);
|
|
766
|
+
});
|
|
505
767
|
});
|
|
506
768
|
//# sourceMappingURL=retrievers.test.js.map
|