@fgv/ts-agent-memory-sqlite-vec 5.1.0-47 → 5.1.0-49
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/LICENSE +21 -0
- package/dist/packlets/sqlite-vec-index/sqliteVecVectorIndex.js +107 -1
- package/dist/packlets/sqlite-vec-index/sqliteVecVectorIndex.js.map +1 -1
- package/dist/ts-agent-memory-sqlite-vec.d.ts +26 -0
- package/lib/packlets/sqlite-vec-index/sqliteVecVectorIndex.d.ts +23 -1
- package/lib/packlets/sqlite-vec-index/sqliteVecVectorIndex.d.ts.map +1 -1
- package/lib/packlets/sqlite-vec-index/sqliteVecVectorIndex.js +106 -0
- package/lib/packlets/sqlite-vec-index/sqliteVecVectorIndex.js.map +1 -1
- package/package.json +17 -7
- package/.rush/temp/2b1ffec4a34b11a2a7317510ba4142ae814069d2.tar.log +0 -60
- package/.rush/temp/chunked-rush-logs/ts-agent-memory-sqlite-vec.build.chunks.jsonl +0 -9
- package/.rush/temp/operation/build/all.log +0 -9
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -9
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -720
- package/config/api-extractor.json +0 -38
- package/config/jest.config.json +0 -13
- package/config/rig.json +0 -6
- package/dist/test/unit/sqliteVecFragmentIndex.test.js +0 -513
- package/dist/test/unit/sqliteVecFragmentIndex.test.js.map +0 -1
- package/dist/test/unit/sqliteVecVectorIndex.test.js +0 -199
- package/dist/test/unit/sqliteVecVectorIndex.test.js.map +0 -1
- package/eslint.config.js +0 -15
- package/etc/ts-agent-memory-sqlite-vec.api.md +0 -66
- package/lib/test/unit/sqliteVecFragmentIndex.test.d.ts +0 -2
- package/lib/test/unit/sqliteVecFragmentIndex.test.d.ts.map +0 -1
- package/lib/test/unit/sqliteVecFragmentIndex.test.js +0 -551
- package/lib/test/unit/sqliteVecFragmentIndex.test.js.map +0 -1
- package/lib/test/unit/sqliteVecVectorIndex.test.d.ts +0 -2
- package/lib/test/unit/sqliteVecVectorIndex.test.d.ts.map +0 -1
- package/lib/test/unit/sqliteVecVectorIndex.test.js +0 -237
- package/lib/test/unit/sqliteVecVectorIndex.test.js.map +0 -1
- package/rush-logs/ts-agent-memory-sqlite-vec.build.cache.log +0 -3
- package/rush-logs/ts-agent-memory-sqlite-vec.build.log +0 -9
- package/src/index.ts +0 -6
- package/src/packlets/sqlite-vec-index/index.ts +0 -8
- package/src/packlets/sqlite-vec-index/model.ts +0 -56
- package/src/packlets/sqlite-vec-index/sqliteVecFragmentIndex.ts +0 -540
- package/src/packlets/sqlite-vec-index/sqliteVecVectorIndex.ts +0 -255
- package/src/test/unit/sqliteVecFragmentIndex.test.ts +0 -691
- package/src/test/unit/sqliteVecVectorIndex.test.ts +0 -253
- package/temp/build/lint/_eslint-5eVG3S6w.json +0 -34
- package/temp/build/typescript/ts_8nwakTlr.json +0 -1
- package/temp/ts-agent-memory-sqlite-vec.api.json +0 -1167
- package/temp/ts-agent-memory-sqlite-vec.api.md +0 -66
- package/tsconfig.json +0 -8
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Erik Fortune
|
|
3
|
-
* SPDX-License-Identifier: MIT
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type BetterSqlite3 from 'better-sqlite3';
|
|
7
|
-
import { load as loadSqliteVec } from 'sqlite-vec';
|
|
8
|
-
import { Result, captureResult, fail, succeed } from '@fgv/ts-utils';
|
|
9
|
-
import {
|
|
10
|
-
IEdgeTarget,
|
|
11
|
-
IVectorIndex,
|
|
12
|
-
IVectorQueryHit,
|
|
13
|
-
MemoryId,
|
|
14
|
-
MemoryScopeKey,
|
|
15
|
-
edgeTargetKey
|
|
16
|
-
} from '@fgv/ts-agent-memory';
|
|
17
|
-
import { ISqliteVecVectorIndexCreateParams } from './model';
|
|
18
|
-
|
|
19
|
-
/** Default name for the `vec0` virtual table. */
|
|
20
|
-
const DEFAULT_TABLE_NAME: string = 'memory_vectors';
|
|
21
|
-
|
|
22
|
-
/** A simple SQL identifier — the only shape allowed for the table name (it is interpolated into DDL). */
|
|
23
|
-
const IDENTIFIER_RE: RegExp = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
24
|
-
|
|
25
|
-
/** One KNN row as returned by the `vec0` MATCH query. */
|
|
26
|
-
interface IKnnRow {
|
|
27
|
-
readonly target_key: string;
|
|
28
|
-
readonly distance: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A persistent, `sqlite-vec`-backed `IVectorIndex` for `@fgv/ts-agent-memory`.
|
|
33
|
-
*
|
|
34
|
-
* @remarks
|
|
35
|
-
* This is the **durable** counterpart to the in-memory `InMemoryCosineIndex`:
|
|
36
|
-
* embeddings live in a `sqlite-vec` `vec0` virtual table inside a `better-sqlite3`
|
|
37
|
-
* database, so they survive a process restart. A consumer that wires this index
|
|
38
|
-
* into `FileTreeMemoryStore` (instead of the in-memory index) opens an existing
|
|
39
|
-
* vault **without re-embedding it** — the vectors are already on disk. New writes
|
|
40
|
-
* still flow through the store's incremental embed-on-write path; there is no core
|
|
41
|
-
* store change.
|
|
42
|
-
*
|
|
43
|
-
* The index is keyed by the canonical `edgeTargetKey` of each record's
|
|
44
|
-
* scope-qualified `(scope, id)` address (a `TEXT PRIMARY KEY` on the `vec0` table),
|
|
45
|
-
* so two records that share a filename stem across scopes never collide. The
|
|
46
|
-
* dimension is established by the first `add` (the `vec0` column is fixed-width) and
|
|
47
|
-
* recovered from the table schema when a persistent file is reopened; every later
|
|
48
|
-
* `add`/`query` must match it or fail loudly, exactly as the in-memory index does.
|
|
49
|
-
* Similarity is cosine (`distance_metric=cosine`): the returned `score` is
|
|
50
|
-
* `1 - cosineDistance`, i.e. cosine similarity in `[-1, 1]`, higher = more similar —
|
|
51
|
-
* byte-for-byte the same scoring contract as `InMemoryCosineIndex`.
|
|
52
|
-
*
|
|
53
|
-
* Query is a brute-force `vec0` KNN scan (not an ANN structure): correct and
|
|
54
|
-
* durable, appropriate for the same "thousands of records" regime the in-memory
|
|
55
|
-
* index targets. Large-N ANN indexing is explicitly out of scope — see the README.
|
|
56
|
-
*
|
|
57
|
-
* The `better-sqlite3` `Database` is consumer-owned (bring-your-own): this index
|
|
58
|
-
* loads the `sqlite-vec` extension onto it and reads/writes the table, but never
|
|
59
|
-
* opens or closes the connection.
|
|
60
|
-
* @public
|
|
61
|
-
*/
|
|
62
|
-
export class SqliteVecVectorIndex implements IVectorIndex {
|
|
63
|
-
private readonly _db: BetterSqlite3.Database;
|
|
64
|
-
private readonly _table: string;
|
|
65
|
-
/** The dimension of every stored vector; `undefined` until the table exists (first `add` or a reopened non-empty file). */
|
|
66
|
-
private _dimension: number | undefined;
|
|
67
|
-
/** Prepared statements; created once the table exists (established or recovered). */
|
|
68
|
-
private _stmts: ISqliteVecStatements | undefined;
|
|
69
|
-
|
|
70
|
-
private constructor(db: BetterSqlite3.Database, table: string, dimension: number | undefined) {
|
|
71
|
-
this._db = db;
|
|
72
|
-
this._table = table;
|
|
73
|
-
this._dimension = dimension;
|
|
74
|
-
this._stmts = dimension === undefined ? undefined : this._prepare();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** The number of vectors currently held. Zero before the first `add`. */
|
|
78
|
-
public get size(): number {
|
|
79
|
-
if (this._stmts === undefined) {
|
|
80
|
-
return 0;
|
|
81
|
-
}
|
|
82
|
-
return (this._stmts.count.get() as { c: number }).c;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Family-convention factory. Loads the `sqlite-vec` extension onto the supplied
|
|
87
|
-
* `better-sqlite3` connection and, if the vector table already exists (a reopened
|
|
88
|
-
* persistent file), recovers its established dimension so no re-embedding is
|
|
89
|
-
* needed on open.
|
|
90
|
-
*
|
|
91
|
-
* @param params - See {@link ISqliteVecVectorIndexCreateParams}.
|
|
92
|
-
* @returns `Success` with the index, or `Failure` if the table name is not a
|
|
93
|
-
* simple identifier or the extension fails to load.
|
|
94
|
-
*/
|
|
95
|
-
public static create(params: ISqliteVecVectorIndexCreateParams): Promise<Result<SqliteVecVectorIndex>> {
|
|
96
|
-
const table: string = params.tableName ?? DEFAULT_TABLE_NAME;
|
|
97
|
-
if (!IDENTIFIER_RE.test(table)) {
|
|
98
|
-
return Promise.resolve(fail(`sqlite-vec index: table name '${table}' is not a simple SQL identifier`));
|
|
99
|
-
}
|
|
100
|
-
return Promise.resolve(
|
|
101
|
-
captureResult(() => {
|
|
102
|
-
loadSqliteVec(params.database);
|
|
103
|
-
const dimension: number | undefined = SqliteVecVectorIndex._readExistingDimension(
|
|
104
|
-
params.database,
|
|
105
|
-
table
|
|
106
|
-
);
|
|
107
|
-
return new SqliteVecVectorIndex(params.database, table, dimension);
|
|
108
|
-
}).withErrorFormat((e) => `sqlite-vec index: failed to initialize: ${e}`)
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** {@inheritDoc IVectorIndex.add} */
|
|
113
|
-
public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {
|
|
114
|
-
const key: string = edgeTargetKey(target);
|
|
115
|
-
if (vector.length === 0) {
|
|
116
|
-
return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));
|
|
117
|
-
}
|
|
118
|
-
if (this._dimension !== undefined && vector.length !== this._dimension) {
|
|
119
|
-
return Promise.resolve(
|
|
120
|
-
fail(
|
|
121
|
-
`vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`
|
|
122
|
-
)
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
return Promise.resolve(
|
|
126
|
-
captureResult(() => {
|
|
127
|
-
if (this._stmts === undefined) {
|
|
128
|
-
this._createTable(vector.length);
|
|
129
|
-
this._dimension = vector.length;
|
|
130
|
-
this._stmts = this._prepare();
|
|
131
|
-
}
|
|
132
|
-
this._stmts.replace(key, SqliteVecVectorIndex._toBlob(vector));
|
|
133
|
-
return key;
|
|
134
|
-
}).withErrorFormat((e) => `vector index: cannot add '${key}': ${e}`)
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** {@inheritDoc IVectorIndex.remove} */
|
|
139
|
-
public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {
|
|
140
|
-
return Promise.resolve(
|
|
141
|
-
captureResult(() => {
|
|
142
|
-
// Idempotent: removing a target with no embedding (or before any `add`
|
|
143
|
-
// created the table) still succeeds.
|
|
144
|
-
if (this._stmts !== undefined) {
|
|
145
|
-
this._stmts.delete.run(edgeTargetKey(target));
|
|
146
|
-
}
|
|
147
|
-
return target;
|
|
148
|
-
}).withErrorFormat((e) => `vector index: cannot remove '${edgeTargetKey(target)}': ${e}`)
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** {@inheritDoc IVectorIndex.query} */
|
|
153
|
-
public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {
|
|
154
|
-
if (topK <= 0 || this._stmts === undefined) {
|
|
155
|
-
return Promise.resolve(succeed([]));
|
|
156
|
-
}
|
|
157
|
-
if (vector.length !== this._dimension) {
|
|
158
|
-
return Promise.resolve(
|
|
159
|
-
fail(
|
|
160
|
-
`vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`
|
|
161
|
-
)
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
return Promise.resolve(
|
|
165
|
-
captureResult<ReadonlyArray<IVectorQueryHit>>(() => {
|
|
166
|
-
const rows: ReadonlyArray<IKnnRow> = this._stmts!.query.all(
|
|
167
|
-
SqliteVecVectorIndex._toBlob(vector),
|
|
168
|
-
topK
|
|
169
|
-
) as ReadonlyArray<IKnnRow>;
|
|
170
|
-
// sqlite-vec returns rows in ascending distance (nearest first); score is
|
|
171
|
-
// `1 - cosineDistance` = cosine similarity, so descending score is preserved.
|
|
172
|
-
return rows.map((row) => ({
|
|
173
|
-
target: SqliteVecVectorIndex._parseKey(row.target_key),
|
|
174
|
-
score: 1 - row.distance
|
|
175
|
-
}));
|
|
176
|
-
}).withErrorFormat((e) => `vector index: query failed: ${e}`)
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Create the `vec0` virtual table with the established dimension. */
|
|
181
|
-
private _createTable(dimension: number): void {
|
|
182
|
-
this._db.exec(
|
|
183
|
-
`CREATE VIRTUAL TABLE IF NOT EXISTS "${this._table}" USING vec0(` +
|
|
184
|
-
`target_key TEXT PRIMARY KEY, embedding float[${dimension}] distance_metric=cosine)`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Prepare the statements the index reuses. Requires the table to exist. */
|
|
189
|
-
private _prepare(): ISqliteVecStatements {
|
|
190
|
-
const del: BetterSqlite3.Statement = this._db.prepare(
|
|
191
|
-
`DELETE FROM "${this._table}" WHERE target_key = ?`
|
|
192
|
-
);
|
|
193
|
-
const ins: BetterSqlite3.Statement = this._db.prepare(
|
|
194
|
-
`INSERT INTO "${this._table}"(target_key, embedding) VALUES (?, ?)`
|
|
195
|
-
);
|
|
196
|
-
// vec0 rejects INSERT OR REPLACE on a TEXT primary key, so replace is a
|
|
197
|
-
// delete-then-insert inside a single transaction.
|
|
198
|
-
const replaceTxn: BetterSqlite3.Transaction<(key: string, blob: Uint8Array) => void> =
|
|
199
|
-
this._db.transaction((key: string, blob: Uint8Array) => {
|
|
200
|
-
del.run(key);
|
|
201
|
-
ins.run(key, blob);
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
delete: del,
|
|
205
|
-
replace: (key: string, blob: Uint8Array): void => {
|
|
206
|
-
replaceTxn(key, blob);
|
|
207
|
-
},
|
|
208
|
-
query: this._db.prepare(
|
|
209
|
-
`SELECT target_key, distance FROM "${this._table}" WHERE embedding MATCH ? AND k = ?`
|
|
210
|
-
),
|
|
211
|
-
count: this._db.prepare(`SELECT count(*) AS c FROM "${this._table}"`)
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Recover the established dimension of an existing `vec0` table from its stored
|
|
217
|
-
* `CREATE VIRTUAL TABLE` SQL (`float[<n>]`). Returns `undefined` when the table
|
|
218
|
-
* does not exist yet (a fresh database — dimension is set by the first `add`).
|
|
219
|
-
*/
|
|
220
|
-
private static _readExistingDimension(db: BetterSqlite3.Database, table: string): number | undefined {
|
|
221
|
-
const row: { sql: string } | undefined = db
|
|
222
|
-
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
223
|
-
.get(table) as { sql: string } | undefined;
|
|
224
|
-
if (row === undefined) {
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
227
|
-
const match: RegExpMatchArray | null = row.sql.match(/float\[(\d+)\]/);
|
|
228
|
-
return match === null ? undefined : Number(match[1]);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Pack a `Float32Array` as the little-endian byte blob `vec0` stores. Copies, so the caller may reuse its buffer. */
|
|
232
|
-
private static _toBlob(vector: Float32Array): Uint8Array {
|
|
233
|
-
return new Uint8Array(Float32Array.from(vector).buffer);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Reverse `edgeTargetKey` — the canonical key is `scope\0id` with NUL
|
|
238
|
-
* excluded from both components, so the first NUL splits it unambiguously.
|
|
239
|
-
*/
|
|
240
|
-
private static _parseKey(key: string): IEdgeTarget {
|
|
241
|
-
const nul: number = key.indexOf('\0');
|
|
242
|
-
return {
|
|
243
|
-
scope: key.slice(0, nul) as unknown as MemoryScopeKey,
|
|
244
|
-
id: key.slice(nul + 1) as unknown as MemoryId
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** The prepared statements / helpers the index reuses once its table exists. */
|
|
250
|
-
interface ISqliteVecStatements {
|
|
251
|
-
readonly delete: BetterSqlite3.Statement;
|
|
252
|
-
readonly replace: (key: string, blob: Uint8Array) => void;
|
|
253
|
-
readonly query: BetterSqlite3.Statement;
|
|
254
|
-
readonly count: BetterSqlite3.Statement;
|
|
255
|
-
}
|