agentfootprint 9.2.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/adapters/memory/pgVector.js +731 -0
  2. package/dist/adapters/memory/pgVector.js.map +1 -0
  3. package/dist/adapters/memory/s3Vectors.js +628 -0
  4. package/dist/adapters/memory/s3Vectors.js.map +1 -0
  5. package/dist/adapters/memory/sqliteVector.js +16 -79
  6. package/dist/adapters/memory/sqliteVector.js.map +1 -1
  7. package/dist/embedders/index.js +240 -76
  8. package/dist/embedders/index.js.map +1 -1
  9. package/dist/esm/adapters/memory/pgVector.d.ts +243 -0
  10. package/dist/esm/adapters/memory/pgVector.js +726 -0
  11. package/dist/esm/adapters/memory/pgVector.js.map +1 -0
  12. package/dist/esm/adapters/memory/s3Vectors.d.ts +208 -0
  13. package/dist/esm/adapters/memory/s3Vectors.js +624 -0
  14. package/dist/esm/adapters/memory/s3Vectors.js.map +1 -0
  15. package/dist/esm/adapters/memory/sqliteVector.d.ts +5 -27
  16. package/dist/esm/adapters/memory/sqliteVector.js +10 -73
  17. package/dist/esm/adapters/memory/sqliteVector.js.map +1 -1
  18. package/dist/esm/embedders/index.d.ts +99 -19
  19. package/dist/esm/embedders/index.js +240 -76
  20. package/dist/esm/embedders/index.js.map +1 -1
  21. package/dist/esm/lib/embedderMismatch.d.ts +67 -0
  22. package/dist/esm/lib/embedderMismatch.js +96 -0
  23. package/dist/esm/lib/embedderMismatch.js.map +1 -0
  24. package/dist/esm/lib/rag/defineRAG.d.ts +28 -6
  25. package/dist/esm/lib/rag/defineRAG.js +39 -5
  26. package/dist/esm/lib/rag/defineRAG.js.map +1 -1
  27. package/dist/esm/memory/define.js +31 -1
  28. package/dist/esm/memory/define.js.map +1 -1
  29. package/dist/esm/memory/define.types.d.ts +13 -2
  30. package/dist/esm/memory/define.types.js.map +1 -1
  31. package/dist/esm/memory/embedding/loadRelevant.d.ts +10 -2
  32. package/dist/esm/memory/embedding/loadRelevant.js +36 -18
  33. package/dist/esm/memory/embedding/loadRelevant.js.map +1 -1
  34. package/dist/esm/memory/pipeline/semantic.d.ts +13 -2
  35. package/dist/esm/memory/pipeline/semantic.js +11 -5
  36. package/dist/esm/memory/pipeline/semantic.js.map +1 -1
  37. package/dist/esm/memory/store/capability.d.ts +25 -6
  38. package/dist/esm/memory/store/capability.js +68 -3
  39. package/dist/esm/memory/store/capability.js.map +1 -1
  40. package/dist/esm/memory/store/index.d.ts +1 -0
  41. package/dist/esm/memory/store/index.js +4 -0
  42. package/dist/esm/memory/store/index.js.map +1 -1
  43. package/dist/esm/memory/store/types.d.ts +40 -0
  44. package/dist/esm/memory-providers.d.ts +7 -4
  45. package/dist/esm/memory-providers.js +18 -4
  46. package/dist/esm/memory-providers.js.map +1 -1
  47. package/dist/lib/embedderMismatch.js +103 -0
  48. package/dist/lib/embedderMismatch.js.map +1 -0
  49. package/dist/lib/rag/defineRAG.js +39 -5
  50. package/dist/lib/rag/defineRAG.js.map +1 -1
  51. package/dist/memory/define.js +31 -1
  52. package/dist/memory/define.js.map +1 -1
  53. package/dist/memory/define.types.js.map +1 -1
  54. package/dist/memory/embedding/loadRelevant.js +36 -18
  55. package/dist/memory/embedding/loadRelevant.js.map +1 -1
  56. package/dist/memory/pipeline/semantic.js +11 -5
  57. package/dist/memory/pipeline/semantic.js.map +1 -1
  58. package/dist/memory/store/capability.js +70 -4
  59. package/dist/memory/store/capability.js.map +1 -1
  60. package/dist/memory/store/index.js +7 -1
  61. package/dist/memory/store/index.js.map +1 -1
  62. package/dist/memory-providers.js +22 -5
  63. package/dist/memory-providers.js.map +1 -1
  64. package/dist/types/adapters/memory/pgVector.d.ts +244 -0
  65. package/dist/types/adapters/memory/pgVector.d.ts.map +1 -0
  66. package/dist/types/adapters/memory/s3Vectors.d.ts +209 -0
  67. package/dist/types/adapters/memory/s3Vectors.d.ts.map +1 -0
  68. package/dist/types/adapters/memory/sqliteVector.d.ts +5 -27
  69. package/dist/types/adapters/memory/sqliteVector.d.ts.map +1 -1
  70. package/dist/types/embedders/index.d.ts +99 -19
  71. package/dist/types/embedders/index.d.ts.map +1 -1
  72. package/dist/types/lib/embedderMismatch.d.ts +68 -0
  73. package/dist/types/lib/embedderMismatch.d.ts.map +1 -0
  74. package/dist/types/lib/rag/defineRAG.d.ts +28 -6
  75. package/dist/types/lib/rag/defineRAG.d.ts.map +1 -1
  76. package/dist/types/memory/define.d.ts.map +1 -1
  77. package/dist/types/memory/define.types.d.ts +13 -2
  78. package/dist/types/memory/define.types.d.ts.map +1 -1
  79. package/dist/types/memory/embedding/loadRelevant.d.ts +10 -2
  80. package/dist/types/memory/embedding/loadRelevant.d.ts.map +1 -1
  81. package/dist/types/memory/pipeline/semantic.d.ts +13 -2
  82. package/dist/types/memory/pipeline/semantic.d.ts.map +1 -1
  83. package/dist/types/memory/store/capability.d.ts +25 -6
  84. package/dist/types/memory/store/capability.d.ts.map +1 -1
  85. package/dist/types/memory/store/index.d.ts +1 -0
  86. package/dist/types/memory/store/index.d.ts.map +1 -1
  87. package/dist/types/memory/store/types.d.ts +40 -0
  88. package/dist/types/memory/store/types.d.ts.map +1 -1
  89. package/dist/types/memory-providers.d.ts +7 -4
  90. package/dist/types/memory-providers.d.ts.map +1 -1
  91. package/package.json +9 -1
@@ -0,0 +1,726 @@
1
+ /**
2
+ * adapters/memory/pgVector — the target our own port has named since 2.x.
3
+ *
4
+ * `MemoryStore`'s docstring has listed the backends it was designed for from
5
+ * the beginning — *"Every storage backend (InMemory, Redis, DynamoDB,
6
+ * **Postgres**, Bedrock AgentCore) implements this interface"* — and named the
7
+ * query, twice, in the places an implementer would look: *"**Postgres**:
8
+ * multi-row INSERT … ON CONFLICT DO UPDATE"* for `putMany`, *"**pgvector**:
9
+ * `ORDER BY embedding <=> query LIMIT k`"* for `search`. Every one of those
10
+ * sentences was true about the design and false about the shipped package.
11
+ * This is the adapter that makes them the same sentence.
12
+ *
13
+ * It matters more than one row in a table of backends. Postgres is the database
14
+ * most teams already run, `pgvector` is an extension away, and a corpus that
15
+ * lives beside the application's own data inherits its backups, its failover,
16
+ * its access control and its migrations. `sqliteVectorStore` is one machine;
17
+ * `s3VectorsStore` is serverless and eventually consistent; this is the one for
18
+ * a fleet that already has a database.
19
+ *
20
+ * ── The table, and why this does not create it ──────────────────────────────
21
+ * A `vector(N)` column fixes N at creation, and N is a fact about your
22
+ * embedder. Creating the table implicitly would pick that number — and the
23
+ * index type, and the operator class — on your behalf, in a migration you never
24
+ * reviewed, in a database whose DDL is usually somebody's job. So the schema is
25
+ * yours to run, and this store REFUSES a table that is missing rather than
26
+ * silently answering "no matches" against nothing:
27
+ *
28
+ * ```sql
29
+ * CREATE EXTENSION IF NOT EXISTS vector;
30
+ *
31
+ * -- 1024 = your embedder's dimensions. bedrockEmbedder() default: 1024.
32
+ * -- openaiEmbedder() default: 1536. staticEmbedder(): 256.
33
+ * CREATE TABLE af_vectors (
34
+ * namespace TEXT NOT NULL,
35
+ * id TEXT NOT NULL,
36
+ * value JSONB NOT NULL,
37
+ * metadata JSONB,
38
+ * embedding vector(1024),
39
+ * embedder_fp TEXT,
40
+ * version INTEGER NOT NULL,
41
+ * created_at BIGINT NOT NULL,
42
+ * updated_at BIGINT NOT NULL,
43
+ * last_accessed_at BIGINT NOT NULL,
44
+ * access_count INTEGER NOT NULL,
45
+ * ttl BIGINT,
46
+ * tier TEXT,
47
+ * source JSONB,
48
+ * embedding_model TEXT,
49
+ * PRIMARY KEY (namespace, id)
50
+ * );
51
+ *
52
+ * -- Cosine, because that is the score this port reports and every threshold
53
+ * -- in this library is calibrated on. Match the operator class to the metric.
54
+ * CREATE INDEX af_vectors_hnsw ON af_vectors
55
+ * USING hnsw (embedding vector_cosine_ops);
56
+ * CREATE INDEX af_vectors_ns ON af_vectors (namespace);
57
+ *
58
+ * -- Recognition (`seen`/`recordSignature`), usefulness feedback, and the
59
+ * -- per-namespace embedder fingerprint. Small, and each one is a port method
60
+ * -- that would otherwise have to be refused.
61
+ * CREATE TABLE af_signatures (
62
+ * namespace TEXT NOT NULL, signature TEXT NOT NULL,
63
+ * PRIMARY KEY (namespace, signature)
64
+ * );
65
+ * CREATE TABLE af_feedback (
66
+ * namespace TEXT NOT NULL, id TEXT NOT NULL,
67
+ * total DOUBLE PRECISION NOT NULL, count INTEGER NOT NULL,
68
+ * PRIMARY KEY (namespace, id)
69
+ * );
70
+ * CREATE TABLE af_index_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
71
+ * ```
72
+ *
73
+ * Every table and column name above is an OPTION with that value as its
74
+ * default, so this drops into a schema that already has naming conventions —
75
+ * see {@link PgVectorStoreOptions}. Identifiers are validated and quoted; a
76
+ * name that is not a plain SQL identifier is refused rather than interpolated.
77
+ *
78
+ * ── Cosine, and only cosine ─────────────────────────────────────────────────
79
+ * `search` is `1 - (embedding <=> $query::vector)` — pgvector's cosine
80
+ * DISTANCE, converted to the cosine SIMILARITY the port reports and
81
+ * `defineRAG`'s 0.7 default is calibrated on. `<->` (L2) and `<#>` (inner
82
+ * product) are deliberately not options: their ranges are not that range, and a
83
+ * number that reads like a cosine and is not one is the failure mode the whole
84
+ * fingerprint machinery exists to prevent. Build the HNSW index with
85
+ * `vector_cosine_ops` so the operator and the index agree — with the wrong
86
+ * operator class the query still returns the right answer, slowly, by scanning.
87
+ *
88
+ * ── One statement at a time, on purpose ─────────────────────────────────────
89
+ * The client here is anything with `query()` — a `pg.Pool` is the expected one,
90
+ * and a Pool hands each `query()` its own connection. `BEGIN` on one and the
91
+ * next statement on another is a transaction that silently is not one, so this
92
+ * adapter never writes multi-statement transactions. Everything that must be
93
+ * atomic is ONE statement: `putMany` is one multi-row upsert, `putIfVersion` is
94
+ * one conditional upsert, `forget` is one statement with CTEs across all four
95
+ * tables. That is a constraint that made the code better.
96
+ *
97
+ * ── Lazy peer dependency ────────────────────────────────────────────────────
98
+ * `pg` is an OPTIONAL peer dependency, required at construction time. Importing
99
+ * `agentfootprint/memory` costs nothing for consumers who never build one of
100
+ * these. Pass `client` to reuse the pool your app already has — which is the
101
+ * recommended shape, because a second pool to the same database is a second set
102
+ * of connections nobody counted.
103
+ */
104
+ import { identityNamespace } from '../../memory/identity/index.js';
105
+ import { lazyRequire } from '../../lib/lazyRequire.js';
106
+ import { EmbedderMismatchError, fingerprintConflict, fingerprintText, parseFingerprint, } from '../../lib/embedderMismatch.js';
107
+ /**
108
+ * Raised when the database is reachable but its schema is not this store's.
109
+ *
110
+ * The law `sqliteVectorStore` states for a file, one backend over: **an
111
+ * unreadable index and an empty one are different facts, and only one of them
112
+ * is safe to answer with "no matches".** A store that treated a missing table
113
+ * as an empty corpus would answer every question from the model's own weights
114
+ * and log nothing.
115
+ */
116
+ export class PgVectorSchemaError extends Error {
117
+ code = 'ERR_PGVECTOR_SCHEMA';
118
+ /** The schema-qualified table that could not be used. */
119
+ table;
120
+ /** Columns this store needs and did not find. Empty when the table is absent entirely. */
121
+ missingColumns;
122
+ constructor(table, missingColumns, detail) {
123
+ super(`[memory] the pgVectorStore table ${table} cannot be used: ${detail}\n` +
124
+ ` A missing table and an empty corpus are different facts, and only one of them is ` +
125
+ `safe to answer with "no matches" — so this refuses rather than quietly answering ` +
126
+ `every question from the model alone.\n` +
127
+ ` Fix: run the CREATE TABLE from the pgVectorStore docs (it also needs ` +
128
+ `CREATE EXTENSION vector), or point \`table\`/\`columns\` at the schema you have.`);
129
+ this.name = 'PgVectorSchemaError';
130
+ this.table = table;
131
+ this.missingColumns = missingColumns;
132
+ }
133
+ }
134
+ const DEFAULT_COLUMNS = {
135
+ namespace: 'namespace',
136
+ id: 'id',
137
+ value: 'value',
138
+ metadata: 'metadata',
139
+ embedding: 'embedding',
140
+ embedderFp: 'embedder_fp',
141
+ version: 'version',
142
+ createdAt: 'created_at',
143
+ updatedAt: 'updated_at',
144
+ lastAccessedAt: 'last_accessed_at',
145
+ accessCount: 'access_count',
146
+ ttl: 'ttl',
147
+ tier: 'tier',
148
+ source: 'source',
149
+ embeddingModel: 'embedding_model',
150
+ };
151
+ /**
152
+ * Open a `MemoryStore` over an existing Postgres + pgvector table.
153
+ *
154
+ * @throws when `pg` is absent and no `client` was passed.
155
+ * @throws PgVectorSchemaError on the first call, when the table or a column it
156
+ * needs is not there.
157
+ * @throws EmbedderMismatchError from `put`/`putMany`/`search` when a vector
158
+ * meets a namespace built by a different embedder.
159
+ *
160
+ * @example A corpus beside the application's own data
161
+ * ```ts
162
+ * import { Pool } from 'pg';
163
+ * import { defineRAG, indexDocuments } from 'agentfootprint';
164
+ * import { pgVectorStore } from 'agentfootprint/memory';
165
+ * import { openaiEmbedder } from 'agentfootprint/providers';
166
+ *
167
+ * const store = pgVectorStore({ client: new Pool({ connectionString: process.env.DATABASE_URL }) });
168
+ * const embedder = openaiEmbedder();
169
+ *
170
+ * await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
171
+ *
172
+ * const agent = Agent.create({ provider })
173
+ * .rag(defineRAG({ id: 'docs', store, embedder, embedderId: embedder.id }))
174
+ * .build();
175
+ * ```
176
+ */
177
+ export function pgVectorStore(options = {}) {
178
+ const schema = ident(options.schema ?? 'public', 'schema');
179
+ const vectorsTable = qualified(schema, ident(options.table ?? 'af_vectors', 'table'));
180
+ const signaturesTable = qualified(schema, ident(options.signaturesTable ?? 'af_signatures', 'signaturesTable'));
181
+ const feedbackTable = qualified(schema, ident(options.feedbackTable ?? 'af_feedback', 'feedbackTable'));
182
+ const metaTable = qualified(schema, ident(options.metaTable ?? 'af_index_meta', 'metaTable'));
183
+ const rawTableName = options.table ?? 'af_vectors';
184
+ const rawSchemaName = options.schema ?? 'public';
185
+ const col = resolveColumns(options.columns);
186
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? 500));
187
+ /** The 15 columns a row is written with and read back through, in one order. */
188
+ const WRITE_COLUMNS = [
189
+ col.namespace,
190
+ col.id,
191
+ col.value,
192
+ col.metadata,
193
+ col.embedding,
194
+ col.embedderFp,
195
+ col.version,
196
+ col.createdAt,
197
+ col.updatedAt,
198
+ col.lastAccessedAt,
199
+ col.accessCount,
200
+ col.ttl,
201
+ col.tier,
202
+ col.source,
203
+ col.embeddingModel,
204
+ ];
205
+ const READ_COLUMNS = [
206
+ col.id,
207
+ col.value,
208
+ col.metadata,
209
+ col.version,
210
+ col.createdAt,
211
+ col.updatedAt,
212
+ col.lastAccessedAt,
213
+ col.accessCount,
214
+ col.ttl,
215
+ col.tier,
216
+ col.source,
217
+ col.embeddingModel,
218
+ ]
219
+ .map(quote)
220
+ .join(', ');
221
+ let owned = false;
222
+ let client;
223
+ const connect = () => {
224
+ if (client)
225
+ return client;
226
+ if (options._client) {
227
+ client = options._client;
228
+ return client;
229
+ }
230
+ if (options.client) {
231
+ client = options.client;
232
+ return client;
233
+ }
234
+ const pg = options._pg ?? loadPg();
235
+ if (typeof pg.Pool !== 'function') {
236
+ throw new Error('pgVectorStore: `pg` is installed but `Pool` was not found. Update the driver, or ' +
237
+ 'pass `client` with a pre-built pool.');
238
+ }
239
+ if (!options.connectionString) {
240
+ throw new Error('pgVectorStore: pass `client` (recommended — reuse the pool your app already has) ' +
241
+ 'or `connectionString` for this store to build one.');
242
+ }
243
+ client = new pg.Pool({ connectionString: options.connectionString });
244
+ owned = true;
245
+ return client;
246
+ };
247
+ let closed = false;
248
+ const open = (verb) => {
249
+ if (closed) {
250
+ throw new Error(`[memory] the pgVectorStore for ${vectorsTable} is closed, so it cannot ${verb}. ` +
251
+ `close() is final by design — reopening the pool behind you would hide a ` +
252
+ `shutdown-ordering bug rather than surface it. Build a new store if you need one ` +
253
+ `after closing this.`);
254
+ }
255
+ };
256
+ /**
257
+ * Verify the schema ONCE, on the first call, and refuse by name.
258
+ *
259
+ * Not at construction: building a store must not require a live connection
260
+ * (a module that connects on import is a module that fails at import). Not on
261
+ * every call either — the answer cannot change under a running process
262
+ * without a migration, and paying an `information_schema` read per retrieval
263
+ * would be a tax on the hot path for a fact checked at boot.
264
+ */
265
+ let verified;
266
+ const ensureSchema = () => {
267
+ return (verified ??= (async () => {
268
+ const db = connect();
269
+ const result = await db.query(`SELECT column_name FROM information_schema.columns ` +
270
+ `WHERE table_schema = $1 AND table_name = $2`, [rawSchemaName, rawTableName]);
271
+ const found = new Set(result.rows.map((r) => String(r['column_name'])));
272
+ if (found.size === 0) {
273
+ throw new PgVectorSchemaError(vectorsTable, [], `no such table (looked in schema '${rawSchemaName}').`);
274
+ }
275
+ const missing = WRITE_COLUMNS.filter((name) => !found.has(name));
276
+ if (missing.length > 0) {
277
+ throw new PgVectorSchemaError(vectorsTable, missing, `it exists but is missing ${missing.join(', ')} — this is a table of that name ` +
278
+ `belonging to something else, or a schema from a different release.`);
279
+ }
280
+ })());
281
+ };
282
+ const query = async (text, params) => {
283
+ await ensureSchema();
284
+ const result = await connect().query(text, params);
285
+ return result.rows ?? [];
286
+ };
287
+ // ── Fingerprints ──────────────────────────────────────────────────
288
+ const fingerprints = new Map();
289
+ const readFingerprint = async (ns) => {
290
+ const held = fingerprints.get(ns);
291
+ if (held !== undefined)
292
+ return held;
293
+ const rows = await query(`SELECT "value" FROM ${metaTable} WHERE "key" = $1`, [`fp:${ns}`]);
294
+ const value = rows[0]?.['value'];
295
+ if (typeof value !== 'string')
296
+ return undefined;
297
+ fingerprints.set(ns, value);
298
+ return value;
299
+ };
300
+ const recordFingerprint = async (ns, fp) => {
301
+ await query(`INSERT INTO ${metaTable} ("key", "value") VALUES ($1, $2) ` +
302
+ `ON CONFLICT ("key") DO UPDATE SET "value" = EXCLUDED."value"`, [`fp:${ns}`, fp]);
303
+ fingerprints.set(ns, fp);
304
+ };
305
+ /** The `sqliteVectorStore` rule, one backend over: dimensions always decide. */
306
+ const reconcile = async (ns, incoming, operation) => {
307
+ const storedText = await readFingerprint(ns);
308
+ if (storedText === undefined) {
309
+ if (operation === 'write to')
310
+ await recordFingerprint(ns, fingerprintText(incoming));
311
+ return;
312
+ }
313
+ const stored = parseFingerprint(storedText);
314
+ const conflict = fingerprintConflict(stored, incoming);
315
+ if (conflict !== null) {
316
+ throw new EmbedderMismatchError(ns, storedText, fingerprintText(incoming), conflict, operation, 'point this store at a different table');
317
+ }
318
+ if (operation === 'write to' && stored.id === undefined && incoming.id !== undefined) {
319
+ await recordFingerprint(ns, fingerprintText(incoming));
320
+ }
321
+ };
322
+ /** The 15 bound values one row is written with, in `WRITE_COLUMNS` order. */
323
+ const rowValues = (ns, entry) => {
324
+ const embedding = entry.embedding;
325
+ const fp = embedding && embedding.length > 0
326
+ ? fingerprintText({
327
+ ...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
328
+ dims: embedding.length,
329
+ })
330
+ : null;
331
+ return [
332
+ ns,
333
+ entry.id,
334
+ JSON.stringify(entry.value ?? null),
335
+ entry.metadata === undefined ? null : JSON.stringify(entry.metadata),
336
+ // pgvector reads its own literal form, `'[1,2,3]'`, cast to `vector`.
337
+ embedding && embedding.length > 0 ? `[${embedding.join(',')}]` : null,
338
+ fp,
339
+ entry.version,
340
+ entry.createdAt,
341
+ entry.updatedAt,
342
+ entry.lastAccessedAt,
343
+ entry.accessCount,
344
+ entry.ttl ?? null,
345
+ entry.tier ?? null,
346
+ entry.source === undefined ? null : JSON.stringify(entry.source),
347
+ entry.embeddingModel ?? null,
348
+ ];
349
+ };
350
+ /**
351
+ * The SQL type of each bound value, in `WRITE_COLUMNS` order.
352
+ *
353
+ * Every parameter is cast EXPLICITLY rather than left to inference. In an
354
+ * `INSERT … VALUES` Postgres can infer from the target column, but in the
355
+ * `INSERT … SELECT` form `putIfVersion` needs it cannot, and an uncast
356
+ * parameter arrives as text — which fails on the first integer column with
357
+ * an error about a type nobody wrote. One list, both statements, no
358
+ * inference to reason about.
359
+ */
360
+ const CASTS = [
361
+ 'text', // namespace
362
+ 'text', // id
363
+ 'jsonb', // value
364
+ 'jsonb', // metadata
365
+ 'vector', // embedding
366
+ 'text', // embedder_fp
367
+ 'int', // version
368
+ 'bigint', // created_at
369
+ 'bigint', // updated_at
370
+ 'bigint', // last_accessed_at
371
+ 'int', // access_count
372
+ 'bigint', // ttl
373
+ 'text', // tier
374
+ 'jsonb', // source
375
+ 'text', // embedding_model
376
+ ];
377
+ /** `$1::text, $2::text, $3::jsonb, …` for one row, offset into the batch. */
378
+ const rowParams = (offset) => CASTS.map((cast, i) => `$${offset + i + 1}::${cast}`).join(', ');
379
+ const upsertSet = WRITE_COLUMNS.slice(2)
380
+ .map((name) => `${quote(name)} = EXCLUDED.${quote(name)}`)
381
+ .join(', ');
382
+ const insertColumns = WRITE_COLUMNS.map(quote).join(', ');
383
+ const conflictTarget = `(${quote(col.namespace)}, ${quote(col.id)})`;
384
+ const store = {
385
+ // Vectors in, ranked vectors out — `ORDER BY embedding <=> $query` ranks
386
+ // the embeddings this store was handed.
387
+ supportsVectorSearch: true,
388
+ ranksBy: 'vector',
389
+ async fingerprintOf(identity) {
390
+ open('report a fingerprint');
391
+ return readFingerprint(identityNamespace(identity));
392
+ },
393
+ async get(identity, id) {
394
+ open('read an entry');
395
+ const ns = identityNamespace(identity);
396
+ const rows = await query(`SELECT ${READ_COLUMNS} FROM ${vectorsTable} ` +
397
+ `WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} = $2`, [ns, id]);
398
+ const row = rows[0];
399
+ if (row === undefined)
400
+ return null;
401
+ const entry = rowToEntry(row, col);
402
+ if (entry.ttl !== undefined && entry.ttl <= Date.now())
403
+ return null;
404
+ // Decay signals, the same side effect the port documents for `get`.
405
+ await query(`UPDATE ${vectorsTable} SET ${quote(col.lastAccessedAt)} = $1, ` +
406
+ `${quote(col.accessCount)} = ${quote(col.accessCount)} + 1 ` +
407
+ `WHERE ${quote(col.namespace)} = $2 AND ${quote(col.id)} = $3`, [Date.now(), ns, id]);
408
+ return entry;
409
+ },
410
+ async put(identity, entry) {
411
+ await this.putMany(identity, [entry]);
412
+ },
413
+ async putMany(identity, entries) {
414
+ open('write entries');
415
+ // The port requires an empty batch to be a no-op — callers rely on it to
416
+ // skip a round-trip on a turn that produced nothing.
417
+ if (entries.length === 0)
418
+ return;
419
+ const ns = identityNamespace(identity);
420
+ for (const entry of entries) {
421
+ const embedding = entry.embedding;
422
+ if (embedding && embedding.length > 0) {
423
+ await reconcile(ns, {
424
+ ...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
425
+ dims: embedding.length,
426
+ }, 'write to');
427
+ }
428
+ }
429
+ // ONE multi-row upsert per chunk — the statement the port's own docstring
430
+ // has named for this backend since 2.x. One statement is one implicit
431
+ // transaction, so a chunk lands whole or not at all: a HALF-INDEXED
432
+ // corpus keeps answering and quietly cannot see what did not land.
433
+ for (let i = 0; i < entries.length; i += batchSize) {
434
+ const chunk = entries.slice(i, i + batchSize);
435
+ const params = [];
436
+ const tuples = [];
437
+ for (const entry of chunk) {
438
+ tuples.push(`(${rowParams(params.length)})`);
439
+ params.push(...rowValues(ns, entry));
440
+ }
441
+ await query(`INSERT INTO ${vectorsTable} (${insertColumns}) VALUES ${tuples.join(', ')} ` +
442
+ `ON CONFLICT ${conflictTarget} DO UPDATE SET ${upsertSet}`, params);
443
+ }
444
+ },
445
+ async putIfVersion(identity, entry, expectedVersion) {
446
+ open('write an entry');
447
+ const ns = identityNamespace(identity);
448
+ const embedding = entry.embedding;
449
+ if (embedding && embedding.length > 0) {
450
+ await reconcile(ns, {
451
+ ...(entry.embeddingModel !== undefined && { id: entry.embeddingModel }),
452
+ dims: embedding.length,
453
+ }, 'write to');
454
+ }
455
+ // ONE statement, so the check and the write cannot be separated by
456
+ // another writer — which is the entire point of a compare-and-set, and
457
+ // is why this is not a SELECT followed by an UPDATE (a Pool would happily
458
+ // run those two on different connections).
459
+ //
460
+ // The `WHERE` on the source SELECT is what makes the absent-row case
461
+ // correct: with `expectedVersion !== 0` and no existing row, no source
462
+ // row is produced, nothing is inserted, and nothing comes back. With a
463
+ // row present, the source row IS produced, the conflict fires, and the
464
+ // `DO UPDATE … WHERE version = expected` decides.
465
+ const params = rowValues(ns, entry);
466
+ const expectedParam = `$${params.length + 1}`;
467
+ params.push(expectedVersion);
468
+ const rows = await query(`INSERT INTO ${vectorsTable} AS af_target (${insertColumns}) ` +
469
+ `SELECT ${rowParams(0)} ` +
470
+ `WHERE ${expectedParam}::int = 0 OR EXISTS (SELECT 1 FROM ${vectorsTable} AS af_probe ` +
471
+ `WHERE af_probe.${quote(col.namespace)} = $1 AND af_probe.${quote(col.id)} = $2) ` +
472
+ `ON CONFLICT ${conflictTarget} DO UPDATE SET ${upsertSet} ` +
473
+ `WHERE af_target.${quote(col.version)} = ${expectedParam}::int ` +
474
+ `RETURNING af_target.${quote(col.version)}`, params);
475
+ if (rows.length > 0)
476
+ return { applied: true };
477
+ // Advisory, and read AFTER the failed attempt rather than inside it:
478
+ // the port describes `currentVersion` as what the caller decides a retry
479
+ // against, not as a value it may assume is still current.
480
+ const current = await query(`SELECT ${quote(col.version)} FROM ${vectorsTable} ` +
481
+ `WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} = $2`, [ns, entry.id]);
482
+ const version = current[0]?.[col.version];
483
+ return version === undefined || version === null
484
+ ? { applied: false }
485
+ : { applied: false, currentVersion: num(version) };
486
+ },
487
+ async list(identity, listOptions) {
488
+ open('list entries');
489
+ const ns = identityNamespace(identity);
490
+ const limit = Math.max(1, Math.floor(listOptions?.limit ?? 100));
491
+ const after = listOptions?.cursor ?? '';
492
+ // Keyset pagination on the primary key: stable under concurrent writes,
493
+ // unlike OFFSET, and the cursor is the last id rather than a position.
494
+ const rows = await query(`SELECT ${READ_COLUMNS} FROM ${vectorsTable} ` +
495
+ `WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} > $2 ` +
496
+ `ORDER BY ${quote(col.id)} LIMIT $3`, [ns, after, limit + 1]);
497
+ const tierFilter = listOptions?.tiers ? new Set(listOptions.tiers) : undefined;
498
+ const page = [];
499
+ let cursor;
500
+ for (const row of rows) {
501
+ if (page.length === limit) {
502
+ // The extra row only ever exists to prove there IS a next page.
503
+ cursor = page[page.length - 1]?.id;
504
+ break;
505
+ }
506
+ const entry = rowToEntry(row, col);
507
+ if (entry.ttl !== undefined && entry.ttl <= Date.now())
508
+ continue;
509
+ if (tierFilter && (entry.tier === undefined || !tierFilter.has(entry.tier)))
510
+ continue;
511
+ page.push(entry);
512
+ }
513
+ return { entries: page, ...(cursor !== undefined && { cursor }) };
514
+ },
515
+ async delete(identity, id) {
516
+ open('delete an entry');
517
+ await query(`DELETE FROM ${vectorsTable} WHERE ${quote(col.namespace)} = $1 AND ${quote(col.id)} = $2`, [identityNamespace(identity), id]);
518
+ },
519
+ async seen(identity, signature) {
520
+ open('check a signature');
521
+ const rows = await query(`SELECT 1 FROM ${signaturesTable} WHERE "namespace" = $1 AND "signature" = $2`, [identityNamespace(identity), signature]);
522
+ return rows.length > 0;
523
+ },
524
+ async recordSignature(identity, signature) {
525
+ open('record a signature');
526
+ await query(`INSERT INTO ${signaturesTable} ("namespace", "signature") VALUES ($1, $2) ` +
527
+ `ON CONFLICT ("namespace", "signature") DO NOTHING`, [identityNamespace(identity), signature]);
528
+ },
529
+ async feedback(identity, id, usefulness) {
530
+ open('record feedback');
531
+ // Non-finite values poison the aggregate; the port says adapters must
532
+ // reject them, and clamp the rest.
533
+ if (!Number.isFinite(usefulness))
534
+ return;
535
+ const clamped = Math.max(-1, Math.min(1, usefulness));
536
+ await query(`INSERT INTO ${feedbackTable} AS af_fb ("namespace", "id", "total", "count") ` +
537
+ `VALUES ($1::text, $2::text, $3::double precision, 1) ` +
538
+ `ON CONFLICT ("namespace", "id") DO UPDATE SET ` +
539
+ `"total" = af_fb."total" + EXCLUDED."total", "count" = af_fb."count" + 1`, [identityNamespace(identity), id, clamped]);
540
+ },
541
+ async getFeedback(identity, id) {
542
+ open('read feedback');
543
+ const rows = await query(`SELECT "total", "count" FROM ${feedbackTable} WHERE "namespace" = $1 AND "id" = $2`, [identityNamespace(identity), id]);
544
+ const row = rows[0];
545
+ const count = row === undefined ? 0 : num(row['count']);
546
+ if (count === 0)
547
+ return null;
548
+ return { average: num(row?.['total']) / count, count };
549
+ },
550
+ async forget(identity) {
551
+ open('forget a namespace');
552
+ const ns = identityNamespace(identity);
553
+ // ONE statement across all four tables. A GDPR erasure that removed the
554
+ // vectors and left the signatures behind would be a deletion that is not
555
+ // one, and a Pool cannot be trusted to keep four statements on one
556
+ // connection — so they are one statement, and therefore one transaction.
557
+ await query(`WITH cleared_vectors AS (DELETE FROM ${vectorsTable} WHERE ${quote(col.namespace)} = $1), ` +
558
+ `cleared_signatures AS (DELETE FROM ${signaturesTable} WHERE "namespace" = $1), ` +
559
+ `cleared_feedback AS (DELETE FROM ${feedbackTable} WHERE "namespace" = $1) ` +
560
+ `DELETE FROM ${metaTable} WHERE "key" = $2`, [ns, `fp:${ns}`]);
561
+ fingerprints.delete(ns);
562
+ },
563
+ async search(identity, queryVector, searchOptions) {
564
+ open('search');
565
+ const ns = identityNamespace(identity);
566
+ const k = Math.max(1, Math.floor(searchOptions?.k ?? 10));
567
+ if (queryVector.length === 0)
568
+ return [];
569
+ // pgvector's cosine distance is undefined for a zero vector (it divides
570
+ // by the norm) and comes back as NaN, which sorts unpredictably. An
571
+ // unscoreable query is empty rather than arbitrary.
572
+ if (queryVector.every((v) => v === 0))
573
+ return [];
574
+ // Refused BEFORE the scan, on both halves of the fingerprint. A swapped
575
+ // embedder must not score against the old vectors — the numbers come
576
+ // back in the same range as real ones and no threshold separates them.
577
+ await reconcile(ns, {
578
+ ...(searchOptions?.embedderId !== undefined && { id: searchOptions.embedderId }),
579
+ dims: queryVector.length,
580
+ }, 'search');
581
+ const params = [ns, `[${queryVector.join(',')}]`];
582
+ const where = [
583
+ `${quote(col.namespace)} = $1`,
584
+ `${quote(col.embedding)} IS NOT NULL`,
585
+ `(${quote(col.ttl)} IS NULL OR ${quote(col.ttl)} > ${bind(params, Date.now())})`,
586
+ ];
587
+ const tiers = searchOptions?.tiers;
588
+ if (tiers && tiers.length > 0) {
589
+ where.push(`${quote(col.tier)} = ANY(${bind(params, [...tiers])}::text[])`);
590
+ }
591
+ if (searchOptions?.embedderId !== undefined) {
592
+ // A row that never named its embedder is not evidence of a different
593
+ // one — the same asymmetry the fingerprint rule uses.
594
+ const p = bind(params, searchOptions.embedderId);
595
+ where.push(`(${quote(col.embeddingModel)} IS NULL OR ${quote(col.embeddingModel)} = ${p})`);
596
+ }
597
+ if (searchOptions?.minScore !== undefined) {
598
+ const p = bind(params, searchOptions.minScore);
599
+ where.push(`1 - (${quote(col.embedding)} <=> $2::vector) >= ${p}`);
600
+ }
601
+ const limitParam = bind(params, k);
602
+ // The query the port's docstring has named for this backend since 2.x —
603
+ // `ORDER BY embedding <=> query LIMIT k` — with the distance turned into
604
+ // the cosine SIMILARITY the port reports.
605
+ const rows = await query(`SELECT ${READ_COLUMNS}, 1 - (${quote(col.embedding)} <=> $2::vector) AS af_score ` +
606
+ `FROM ${vectorsTable} WHERE ${where.join(' AND ')} ` +
607
+ `ORDER BY ${quote(col.embedding)} <=> $2::vector, ${quote(col.id)} ` +
608
+ `LIMIT ${limitParam}`, params);
609
+ return rows.map((row) => ({
610
+ entry: rowToEntry(row, col),
611
+ score: num(row['af_score']),
612
+ }));
613
+ },
614
+ async close() {
615
+ if (closed)
616
+ return;
617
+ closed = true;
618
+ if (owned)
619
+ await client?.end?.();
620
+ },
621
+ };
622
+ return store;
623
+ }
624
+ // ─── Identifiers ─────────────────────────────────────────────────────
625
+ /**
626
+ * Validate a caller-supplied identifier, or refuse it.
627
+ *
628
+ * Table and column names are the one part of a query that cannot be a bound
629
+ * parameter — they are interpolated into the SQL text. So they are checked
630
+ * against a plain-identifier shape and refused otherwise, rather than escaped
631
+ * and hoped for. A store configured from an environment variable is one
632
+ * `DB_TABLE` away from being an injection point, and "we quoted it" is not the
633
+ * answer that lets you stop thinking about it.
634
+ */
635
+ function ident(name, option) {
636
+ if (!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) {
637
+ throw new TypeError(`pgVectorStore: \`${option}\` must be a plain SQL identifier — letters, digits and ` +
638
+ `underscores, not starting with a digit. Received ${JSON.stringify(name)}.\n` +
639
+ ` Table and column names cannot be bound parameters; they are interpolated into the ` +
640
+ `statement, so anything else is refused rather than escaped and hoped for.`);
641
+ }
642
+ return name;
643
+ }
644
+ function quote(name) {
645
+ return `"${name}"`;
646
+ }
647
+ function qualified(schema, table) {
648
+ return `${quote(schema)}.${quote(table)}`;
649
+ }
650
+ function resolveColumns(overrides) {
651
+ const out = { ...DEFAULT_COLUMNS };
652
+ for (const key of Object.keys(DEFAULT_COLUMNS)) {
653
+ const value = overrides?.[key];
654
+ if (value !== undefined)
655
+ out[key] = ident(value, `columns.${key}`);
656
+ }
657
+ return out;
658
+ }
659
+ /** Append a value to the parameter list and return its `$n` placeholder. */
660
+ function bind(params, value) {
661
+ params.push(value);
662
+ return `$${params.length}`;
663
+ }
664
+ // ─── Row mapping ─────────────────────────────────────────────────────
665
+ /**
666
+ * `BIGINT` comes back from `pg` as a STRING by default (it does not fit in a
667
+ * JavaScript number in general), and every timestamp here is one. Coerced in
668
+ * one place, so a `createdAt` never reaches a consumer as `'1754640000000'`.
669
+ */
670
+ function num(value) {
671
+ if (typeof value === 'number')
672
+ return value;
673
+ if (typeof value === 'string') {
674
+ const n = Number(value);
675
+ return Number.isFinite(n) ? n : 0;
676
+ }
677
+ return 0;
678
+ }
679
+ /** `JSONB` arrives already parsed from `pg`; a mock or a `TEXT` column may not. */
680
+ function json(value) {
681
+ if (typeof value !== 'string')
682
+ return value;
683
+ try {
684
+ return JSON.parse(value);
685
+ }
686
+ catch {
687
+ return value;
688
+ }
689
+ }
690
+ function rowToEntry(row, col) {
691
+ const metadata = row[col.metadata];
692
+ const source = row[col.source];
693
+ const ttl = row[col.ttl];
694
+ const tier = row[col.tier];
695
+ const model = row[col.embeddingModel];
696
+ return {
697
+ id: String(row[col.id]),
698
+ value: json(row[col.value]),
699
+ ...(metadata !== null &&
700
+ metadata !== undefined && { metadata: json(metadata) }),
701
+ version: num(row[col.version]),
702
+ createdAt: num(row[col.createdAt]),
703
+ updatedAt: num(row[col.updatedAt]),
704
+ lastAccessedAt: num(row[col.lastAccessedAt]),
705
+ accessCount: num(row[col.accessCount]),
706
+ ...(ttl !== null && ttl !== undefined && { ttl: num(ttl) }),
707
+ ...(typeof tier === 'string' && { tier: tier }),
708
+ ...(source !== null &&
709
+ source !== undefined && { source: json(source) }),
710
+ ...(typeof model === 'string' && { embeddingModel: model }),
711
+ };
712
+ }
713
+ // ─── Driver loading ──────────────────────────────────────────────────
714
+ function loadPg() {
715
+ try {
716
+ return lazyRequire('pg');
717
+ }
718
+ catch {
719
+ throw new Error('pgVectorStore requires the `pg` peer dependency.\n' +
720
+ ' Install: npm install pg\n' +
721
+ ' And in the database: CREATE EXTENSION IF NOT EXISTS vector;\n' +
722
+ ' Or pass `client` with a pre-built pool — recommended, so one pool serves the ' +
723
+ 'whole app.');
724
+ }
725
+ }
726
+ //# sourceMappingURL=pgVector.js.map