@seanhogg/builderforce-memory 2026.7.14 → 2026.9.27

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 (223) hide show
  1. package/README.md +10 -0
  2. package/dist/bridges/AnthropicBridge.d.ts +9 -1
  3. package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
  4. package/dist/bridges/AnthropicBridge.js +58 -2
  5. package/dist/bridges/AnthropicBridge.js.map +1 -1
  6. package/dist/bridges/CachingBridge.d.ts +9 -1
  7. package/dist/bridges/CachingBridge.d.ts.map +1 -1
  8. package/dist/bridges/CachingBridge.js +24 -1
  9. package/dist/bridges/CachingBridge.js.map +1 -1
  10. package/dist/bridges/OpenAIBridge.d.ts +4 -1
  11. package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
  12. package/dist/bridges/OpenAIBridge.js +40 -2
  13. package/dist/bridges/OpenAIBridge.js.map +1 -1
  14. package/dist/bridges/SemanticCachingBridge.d.ts +8 -1
  15. package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -1
  16. package/dist/bridges/SemanticCachingBridge.js +26 -1
  17. package/dist/bridges/SemanticCachingBridge.js.map +1 -1
  18. package/dist/bridges/TransformerBridge.d.ts +13 -0
  19. package/dist/bridges/TransformerBridge.d.ts.map +1 -1
  20. package/dist/bridges/VertexAIBridge.d.ts +102 -0
  21. package/dist/bridges/VertexAIBridge.d.ts.map +1 -0
  22. package/dist/bridges/VertexAIBridge.js +224 -0
  23. package/dist/bridges/VertexAIBridge.js.map +1 -0
  24. package/dist/bridges/index.d.ts +3 -1
  25. package/dist/bridges/index.d.ts.map +1 -1
  26. package/dist/bridges/index.js +1 -0
  27. package/dist/bridges/index.js.map +1 -1
  28. package/dist/eval/EvalHarness.d.ts +37 -0
  29. package/dist/eval/EvalHarness.d.ts.map +1 -0
  30. package/dist/eval/EvalHarness.js +178 -0
  31. package/dist/eval/EvalHarness.js.map +1 -0
  32. package/dist/eval/graders.d.ts +76 -0
  33. package/dist/eval/graders.d.ts.map +1 -0
  34. package/dist/eval/graders.js +168 -0
  35. package/dist/eval/graders.js.map +1 -0
  36. package/dist/eval/index.d.ts +26 -0
  37. package/dist/eval/index.d.ts.map +1 -0
  38. package/dist/eval/index.js +26 -0
  39. package/dist/eval/index.js.map +1 -0
  40. package/dist/eval/types.d.ts +97 -0
  41. package/dist/eval/types.d.ts.map +1 -0
  42. package/dist/eval/types.js +5 -0
  43. package/dist/eval/types.js.map +1 -0
  44. package/dist/evermind/contract.d.ts +40 -0
  45. package/dist/evermind/contract.d.ts.map +1 -0
  46. package/dist/evermind/contract.js +13 -0
  47. package/dist/evermind/contract.js.map +1 -0
  48. package/dist/evermind/index.d.ts +15 -0
  49. package/dist/evermind/index.d.ts.map +1 -0
  50. package/dist/evermind/index.js +4 -0
  51. package/dist/evermind/index.js.map +1 -0
  52. package/dist/evermind/memoryBlock.d.ts +7 -0
  53. package/dist/evermind/memoryBlock.d.ts.map +1 -0
  54. package/dist/evermind/memoryBlock.js +37 -0
  55. package/dist/evermind/memoryBlock.js.map +1 -0
  56. package/dist/evermind/recall.d.ts +55 -0
  57. package/dist/evermind/recall.d.ts.map +1 -0
  58. package/dist/evermind/recall.js +105 -0
  59. package/dist/evermind/recall.js.map +1 -0
  60. package/dist/index.d.ts +18 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +14 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/ingest/IngestionPipeline.d.ts +95 -0
  65. package/dist/ingest/IngestionPipeline.d.ts.map +1 -0
  66. package/dist/ingest/IngestionPipeline.js +295 -0
  67. package/dist/ingest/IngestionPipeline.js.map +1 -0
  68. package/dist/ingest/index.d.ts +10 -0
  69. package/dist/ingest/index.d.ts.map +1 -0
  70. package/dist/ingest/index.js +8 -0
  71. package/dist/ingest/index.js.map +1 -0
  72. package/dist/ingest/parsers.d.ts +53 -0
  73. package/dist/ingest/parsers.d.ts.map +1 -0
  74. package/dist/ingest/parsers.js +306 -0
  75. package/dist/ingest/parsers.js.map +1 -0
  76. package/dist/ingest/types.d.ts +114 -0
  77. package/dist/ingest/types.d.ts.map +1 -0
  78. package/dist/ingest/types.js +19 -0
  79. package/dist/ingest/types.js.map +1 -0
  80. package/dist/memory/MemoryStore.d.ts +33 -0
  81. package/dist/memory/MemoryStore.d.ts.map +1 -1
  82. package/dist/memory/MemoryStore.js +29 -3
  83. package/dist/memory/MemoryStore.js.map +1 -1
  84. package/dist/memory/index.d.ts +1 -1
  85. package/dist/memory/index.d.ts.map +1 -1
  86. package/dist/orchestration/AgentGraph.d.ts +104 -0
  87. package/dist/orchestration/AgentGraph.d.ts.map +1 -0
  88. package/dist/orchestration/AgentGraph.js +364 -0
  89. package/dist/orchestration/AgentGraph.js.map +1 -0
  90. package/dist/orchestration/checkpoint.d.ts +58 -0
  91. package/dist/orchestration/checkpoint.d.ts.map +1 -0
  92. package/dist/orchestration/checkpoint.js +92 -0
  93. package/dist/orchestration/checkpoint.js.map +1 -0
  94. package/dist/orchestration/index.d.ts +14 -0
  95. package/dist/orchestration/index.d.ts.map +1 -0
  96. package/dist/orchestration/index.js +10 -0
  97. package/dist/orchestration/index.js.map +1 -0
  98. package/dist/orchestration/patterns.d.ts +159 -0
  99. package/dist/orchestration/patterns.d.ts.map +1 -0
  100. package/dist/orchestration/patterns.js +371 -0
  101. package/dist/orchestration/patterns.js.map +1 -0
  102. package/dist/orchestration/types.d.ts +146 -0
  103. package/dist/orchestration/types.d.ts.map +1 -0
  104. package/dist/orchestration/types.js +20 -0
  105. package/dist/orchestration/types.js.map +1 -0
  106. package/dist/rag/EnterpriseRetriever.d.ts +114 -0
  107. package/dist/rag/EnterpriseRetriever.d.ts.map +1 -0
  108. package/dist/rag/EnterpriseRetriever.js +240 -0
  109. package/dist/rag/EnterpriseRetriever.js.map +1 -0
  110. package/dist/rag/index.d.ts +6 -0
  111. package/dist/rag/index.d.ts.map +1 -0
  112. package/dist/rag/index.js +5 -0
  113. package/dist/rag/index.js.map +1 -0
  114. package/dist/retrieval/bm25.d.ts +48 -1
  115. package/dist/retrieval/bm25.d.ts.map +1 -1
  116. package/dist/retrieval/bm25.js +59 -13
  117. package/dist/retrieval/bm25.js.map +1 -1
  118. package/dist/retrieval/index.d.ts +2 -2
  119. package/dist/retrieval/index.d.ts.map +1 -1
  120. package/dist/retrieval/index.js +1 -1
  121. package/dist/retrieval/index.js.map +1 -1
  122. package/dist/telemetry/InstrumentedBridge.d.ts +62 -0
  123. package/dist/telemetry/InstrumentedBridge.d.ts.map +1 -0
  124. package/dist/telemetry/InstrumentedBridge.js +120 -0
  125. package/dist/telemetry/InstrumentedBridge.js.map +1 -0
  126. package/dist/telemetry/MetricsRegistry.d.ts +88 -0
  127. package/dist/telemetry/MetricsRegistry.d.ts.map +1 -0
  128. package/dist/telemetry/MetricsRegistry.js +167 -0
  129. package/dist/telemetry/MetricsRegistry.js.map +1 -0
  130. package/dist/telemetry/Tracer.d.ts +137 -0
  131. package/dist/telemetry/Tracer.d.ts.map +1 -0
  132. package/dist/telemetry/Tracer.js +244 -0
  133. package/dist/telemetry/Tracer.js.map +1 -0
  134. package/dist/telemetry/exporters.d.ts +68 -0
  135. package/dist/telemetry/exporters.d.ts.map +1 -0
  136. package/dist/telemetry/exporters.js +145 -0
  137. package/dist/telemetry/exporters.js.map +1 -0
  138. package/dist/telemetry/index.d.ts +19 -0
  139. package/dist/telemetry/index.d.ts.map +1 -0
  140. package/dist/telemetry/index.js +12 -0
  141. package/dist/telemetry/index.js.map +1 -0
  142. package/dist/telemetry/pricing.d.ts +74 -0
  143. package/dist/telemetry/pricing.d.ts.map +1 -0
  144. package/dist/telemetry/pricing.js +98 -0
  145. package/dist/telemetry/pricing.js.map +1 -0
  146. package/dist/telemetry/traceparent.d.ts +50 -0
  147. package/dist/telemetry/traceparent.d.ts.map +1 -0
  148. package/dist/telemetry/traceparent.js +77 -0
  149. package/dist/telemetry/traceparent.js.map +1 -0
  150. package/dist/telemetry/types.d.ts +90 -0
  151. package/dist/telemetry/types.d.ts.map +1 -0
  152. package/dist/telemetry/types.js +17 -0
  153. package/dist/telemetry/types.js.map +1 -0
  154. package/dist/vectorstore/MemoryVectorStore.d.ts +72 -0
  155. package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -0
  156. package/dist/vectorstore/MemoryVectorStore.js +205 -0
  157. package/dist/vectorstore/MemoryVectorStore.js.map +1 -0
  158. package/dist/vectorstore/RestVectorStore.d.ts +79 -0
  159. package/dist/vectorstore/RestVectorStore.d.ts.map +1 -0
  160. package/dist/vectorstore/RestVectorStore.js +170 -0
  161. package/dist/vectorstore/RestVectorStore.js.map +1 -0
  162. package/dist/vectorstore/dialects.d.ts +101 -0
  163. package/dist/vectorstore/dialects.d.ts.map +1 -0
  164. package/dist/vectorstore/dialects.js +618 -0
  165. package/dist/vectorstore/dialects.js.map +1 -0
  166. package/dist/vectorstore/filter.d.ts +42 -0
  167. package/dist/vectorstore/filter.d.ts.map +1 -0
  168. package/dist/vectorstore/filter.js +132 -0
  169. package/dist/vectorstore/filter.js.map +1 -0
  170. package/dist/vectorstore/index.d.ts +15 -0
  171. package/dist/vectorstore/index.d.ts.map +1 -0
  172. package/dist/vectorstore/index.js +11 -0
  173. package/dist/vectorstore/index.js.map +1 -0
  174. package/dist/vectorstore/types.d.ts +151 -0
  175. package/dist/vectorstore/types.d.ts.map +1 -0
  176. package/dist/vectorstore/types.js +30 -0
  177. package/dist/vectorstore/types.js.map +1 -0
  178. package/package.json +39 -5
  179. package/src/bridges/AnthropicBridge.ts +68 -3
  180. package/src/bridges/CachingBridge.ts +28 -2
  181. package/src/bridges/OpenAIBridge.ts +48 -3
  182. package/src/bridges/SemanticCachingBridge.ts +29 -2
  183. package/src/bridges/TransformerBridge.ts +16 -0
  184. package/src/bridges/VertexAIBridge.ts +297 -0
  185. package/src/bridges/index.ts +3 -1
  186. package/src/eval/EvalHarness.ts +214 -0
  187. package/src/eval/graders.ts +218 -0
  188. package/src/eval/index.ts +26 -0
  189. package/src/eval/types.ts +107 -0
  190. package/src/evermind/contract.ts +39 -0
  191. package/src/evermind/index.ts +21 -0
  192. package/src/evermind/memoryBlock.ts +38 -0
  193. package/src/evermind/recall.ts +122 -0
  194. package/src/index.ts +232 -0
  195. package/src/ingest/IngestionPipeline.ts +378 -0
  196. package/src/ingest/index.ts +46 -0
  197. package/src/ingest/parsers.ts +318 -0
  198. package/src/ingest/types.ts +116 -0
  199. package/src/memory/MemoryStore.ts +55 -3
  200. package/src/memory/index.ts +1 -1
  201. package/src/orchestration/AgentGraph.ts +462 -0
  202. package/src/orchestration/checkpoint.ts +129 -0
  203. package/src/orchestration/index.ts +63 -0
  204. package/src/orchestration/patterns.ts +532 -0
  205. package/src/orchestration/types.ts +128 -0
  206. package/src/rag/EnterpriseRetriever.ts +348 -0
  207. package/src/rag/index.ts +18 -0
  208. package/src/retrieval/bm25.ts +71 -13
  209. package/src/retrieval/index.ts +2 -2
  210. package/src/telemetry/InstrumentedBridge.ts +152 -0
  211. package/src/telemetry/MetricsRegistry.ts +263 -0
  212. package/src/telemetry/Tracer.ts +310 -0
  213. package/src/telemetry/exporters.ts +187 -0
  214. package/src/telemetry/index.ts +56 -0
  215. package/src/telemetry/pricing.ts +129 -0
  216. package/src/telemetry/traceparent.ts +89 -0
  217. package/src/telemetry/types.ts +105 -0
  218. package/src/vectorstore/MemoryVectorStore.ts +250 -0
  219. package/src/vectorstore/RestVectorStore.ts +236 -0
  220. package/src/vectorstore/dialects.ts +702 -0
  221. package/src/vectorstore/filter.ts +148 -0
  222. package/src/vectorstore/index.ts +53 -0
  223. package/src/vectorstore/types.ts +149 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * vectorstore/MemoryVectorStore.ts — the in-process reference adapter.
3
+ *
4
+ * It is the default store, the test double for every layer above it, and the
5
+ * conformance benchmark a remote adapter is checked against. It is also the
6
+ * honest answer for edge deployments (a Worker, a browser tab, an on-prem box)
7
+ * where standing up a vector database is not on the table.
8
+ *
9
+ * Performance notes, because "in-memory" is not an excuse for O(N) per query:
10
+ * • The HNSW graph is built ONCE and extended incrementally on upsert. The
11
+ * shared `denseSearch` helper rebuilds its index per call, which is right for
12
+ * a one-shot rerank over a candidate set and wrong for a store that is queried
13
+ * repeatedly — so this adapter owns a persistent index instead.
14
+ * • Deletes tombstone rather than rebuild; the graph is rebuilt only once
15
+ * tombstones pass a share of the index, amortising the cost.
16
+ * • Filtered queries over-fetch from the graph and post-filter, falling back to
17
+ * an exact scan only when the filter is selective enough that the graph could
18
+ * not fill `topK`. Correct in both regimes, fast in the common one.
19
+ */
20
+
21
+ import { bm25Search } from '../retrieval/bm25.js';
22
+ import { HnswIndex, type HnswOptions } from '../retrieval/hnsw.js';
23
+ import { cosineSimilarity } from '../similarity/index.js';
24
+ import { combineFilters, matchesFilter } from './filter.js';
25
+ import type {
26
+ AccessScope,
27
+ MetadataFilter,
28
+ UpsertResult,
29
+ VectorMatch,
30
+ VectorQuery,
31
+ VectorRecord,
32
+ VectorStore,
33
+ } from './types.js';
34
+
35
+ export interface MemoryVectorStoreOptions {
36
+ /** Adapter name for telemetry. Default `'memory'`. */
37
+ name?: string;
38
+ /**
39
+ * Record count at/above which dense search uses the HNSW graph instead of an
40
+ * exact cosine scan. Below it, a scan is both simpler and faster. Default 256.
41
+ */
42
+ annThreshold?: number;
43
+ /** HNSW construction parameters. */
44
+ hnsw?: HnswOptions;
45
+ /**
46
+ * Over-fetch multiplier for filtered ANN queries — how many graph candidates
47
+ * to pull per requested result before post-filtering. Default 4.
48
+ */
49
+ filterOverfetch?: number;
50
+ /**
51
+ * Rebuild the graph once tombstoned nodes exceed this share of it. Default 0.2.
52
+ */
53
+ rebuildTombstoneRatio?: number;
54
+ }
55
+
56
+ export class MemoryVectorStore implements VectorStore {
57
+ readonly name: string;
58
+
59
+ private readonly _records = new Map<string, VectorRecord>();
60
+ private readonly _annThreshold: number;
61
+ private readonly _hnswOpts: HnswOptions;
62
+ private readonly _overfetch: number;
63
+ private readonly _rebuildRatio: number;
64
+
65
+ private _index: HnswIndex | undefined;
66
+ /** Ids present in the graph but deleted from the store. */
67
+ private readonly _tombstones = new Set<string>();
68
+
69
+ constructor(opts: MemoryVectorStoreOptions = {}) {
70
+ this.name = opts.name ?? 'memory';
71
+ this._annThreshold = Math.max(1, opts.annThreshold ?? 256);
72
+ this._hnswOpts = opts.hnsw ?? {};
73
+ this._overfetch = Math.max(1, opts.filterOverfetch ?? 4);
74
+ this._rebuildRatio = opts.rebuildTombstoneRatio ?? 0.2;
75
+ }
76
+
77
+ get size(): number { return this._records.size; }
78
+
79
+ async upsert(records: readonly VectorRecord[]): Promise<UpsertResult> {
80
+ let upserted = 0;
81
+ for (const record of records) {
82
+ const previous = this._records.get(record.id);
83
+ this._records.set(record.id, record);
84
+ upserted += 1;
85
+
86
+ if (!record.vector) continue;
87
+ // A changed vector under an existing id invalidates its graph node;
88
+ // tombstone the old one so search cannot return the stale neighbour.
89
+ if (previous?.vector && this._index?.has(record.id)) {
90
+ this._tombstones.add(record.id);
91
+ this._maybeRebuild();
92
+ continue;
93
+ }
94
+ if (this._index && !this._index.has(record.id)) {
95
+ this._index.add(record.id, record.vector);
96
+ this._tombstones.delete(record.id);
97
+ }
98
+ }
99
+ return { upserted, skipped: 0 };
100
+ }
101
+
102
+ async fetch(ids: readonly string[]): Promise<VectorRecord[]> {
103
+ const out: VectorRecord[] = [];
104
+ for (const id of ids) {
105
+ const record = this._records.get(id);
106
+ if (record) out.push(record);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ async delete(ids: readonly string[]): Promise<number> {
112
+ let removed = 0;
113
+ for (const id of ids) {
114
+ if (!this._records.delete(id)) continue;
115
+ removed += 1;
116
+ if (this._index?.has(id)) this._tombstones.add(id);
117
+ }
118
+ this._maybeRebuild();
119
+ return removed;
120
+ }
121
+
122
+ async deleteByFilter(filter: MetadataFilter, scope?: AccessScope): Promise<number> {
123
+ const effective = combineFilters(filter, scope);
124
+ const doomed = [...this._records.values()]
125
+ .filter((r) => matchesFilter(r.metadata, effective))
126
+ .map((r) => r.id);
127
+ return this.delete(doomed);
128
+ }
129
+
130
+ async count(filter?: MetadataFilter, scope?: AccessScope): Promise<number> {
131
+ const effective = combineFilters(filter, scope);
132
+ if (!effective) return this._records.size;
133
+ let n = 0;
134
+ for (const record of this._records.values()) {
135
+ if (matchesFilter(record.metadata, effective)) n += 1;
136
+ }
137
+ return n;
138
+ }
139
+
140
+ async query(query: VectorQuery): Promise<VectorMatch[]> {
141
+ const topK = Math.max(1, query.topK ?? 10);
142
+ const minScore = query.minScore ?? 0;
143
+ const filter = combineFilters(query.filter, query.scope);
144
+
145
+ if (!query.vector) {
146
+ // No embedding: degrade to a filter-only listing rather than returning
147
+ // nothing, so metadata-only lookups do not need a second code path.
148
+ return this._visible(filter)
149
+ .slice(0, topK)
150
+ .map((r) => this._toMatch(r, 1, query.includeVectors));
151
+ }
152
+
153
+ const scored = this._denseScore(query.vector, topK, filter);
154
+ return scored
155
+ .filter((hit) => hit.score >= minScore)
156
+ .slice(0, topK)
157
+ .map(({ record, score }) => this._toMatch(record, score, query.includeVectors));
158
+ }
159
+
160
+ /**
161
+ * BM25 over the visible corpus. Present because lexical recall catches the
162
+ * exact identifiers (error codes, SKUs, function names) that embeddings blur —
163
+ * the failure mode enterprise RAG evaluations reliably surface.
164
+ */
165
+ async keywordSearch(query: VectorQuery): Promise<VectorMatch[]> {
166
+ const text = query.text?.trim();
167
+ if (!text) return [];
168
+
169
+ const topK = Math.max(1, query.topK ?? 10);
170
+ const filter = combineFilters(query.filter, query.scope);
171
+ const visible = this._visible(filter);
172
+ if (visible.length === 0) return [];
173
+
174
+ const hits = bm25Search(text, visible.map((r) => ({ id: r.id, text: r.text })));
175
+ const best = hits[0]?.score ?? 0;
176
+
177
+ return hits.slice(0, topK).map((hit) => {
178
+ const record = this._records.get(hit.id) as VectorRecord;
179
+ // BM25 scores are unbounded; normalising against the top hit puts them
180
+ // on the same [0,1] footing as cosine so fusion weights mean something.
181
+ return this._toMatch(record, best > 0 ? hit.score / best : 0, query.includeVectors);
182
+ });
183
+ }
184
+
185
+ private _denseScore(
186
+ vector: Float32Array,
187
+ topK: number,
188
+ filter?: MetadataFilter,
189
+ ): Array<{ record: VectorRecord; score: number }> {
190
+ const embedded = [...this._records.values()].filter((r) => r.vector);
191
+
192
+ const exactScan = () => embedded
193
+ .filter((r) => matchesFilter(r.metadata, filter))
194
+ .map((r) => ({ record: r, score: cosineSimilarity(vector, r.vector as Float32Array) }))
195
+ .sort((a, b) => b.score - a.score);
196
+
197
+ if (embedded.length < this._annThreshold) return exactScan();
198
+
199
+ this._ensureIndex(embedded);
200
+ const index = this._index as HnswIndex;
201
+ const wanted = filter ? topK * this._overfetch : topK;
202
+ const hits = index.search(vector, wanted + this._tombstones.size);
203
+
204
+ const matched: Array<{ record: VectorRecord; score: number }> = [];
205
+ for (const hit of hits) {
206
+ if (this._tombstones.has(hit.id)) continue;
207
+ const record = this._records.get(hit.id);
208
+ if (!record || !matchesFilter(record.metadata, filter)) continue;
209
+ matched.push({ record, score: hit.score });
210
+ }
211
+
212
+ // The graph could not fill the page under this filter — the filter is more
213
+ // selective than the over-fetch anticipated, so pay for an exact scan
214
+ // rather than return a short, silently-truncated result set.
215
+ if (matched.length < topK && filter) return exactScan();
216
+ return matched;
217
+ }
218
+
219
+ private _visible(filter?: MetadataFilter): VectorRecord[] {
220
+ const all = [...this._records.values()];
221
+ return filter ? all.filter((r) => matchesFilter(r.metadata, filter)) : all;
222
+ }
223
+
224
+ private _toMatch(record: VectorRecord, score: number, includeVectors?: boolean): VectorMatch {
225
+ const match: VectorMatch = {
226
+ id: record.id,
227
+ text: record.text,
228
+ score,
229
+ metadata: record.metadata,
230
+ };
231
+ if (includeVectors && record.vector) match.vector = record.vector;
232
+ return match;
233
+ }
234
+
235
+ private _ensureIndex(embedded: readonly VectorRecord[]): void {
236
+ if (this._index) return;
237
+ const index = new HnswIndex(this._hnswOpts);
238
+ for (const record of embedded) index.add(record.id, record.vector as Float32Array);
239
+ this._index = index;
240
+ this._tombstones.clear();
241
+ }
242
+
243
+ private _maybeRebuild(): void {
244
+ const index = this._index;
245
+ if (!index || index.size === 0) return;
246
+ if (this._tombstones.size / index.size < this._rebuildRatio) return;
247
+ this._index = undefined;
248
+ this._tombstones.clear();
249
+ }
250
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * vectorstore/RestVectorStore.ts — ONE adapter for every HTTP vector database.
3
+ *
4
+ * The vendor-specific part is the {@link VectorDialect}; everything that is the
5
+ * same everywhere lives here exactly once: auth headers, batching, retry with
6
+ * backoff, residual filtering, capability emulation, and text hydration for stores
7
+ * that hold vectors only.
8
+ *
9
+ * The residual pass is the part that matters for correctness. When a dialect
10
+ * cannot push a filter clause down, the store over-fetches and applies the
11
+ * remaining clauses locally with the SAME evaluator the in-process store uses. A
12
+ * clause is therefore never dropped — which on an ACL or tenant clause would be a
13
+ * data leak, and is precisely the failure mode a naive adapter ships with.
14
+ */
15
+
16
+ import { combineFilters, matchesFilter } from './filter.js';
17
+ import { getDialect, type DialectContext, type HttpRequest, type VectorDialect } from './dialects.js';
18
+ import type {
19
+ AccessScope,
20
+ MetadataFilter,
21
+ UpsertResult,
22
+ VectorMatch,
23
+ VectorQuery,
24
+ VectorRecord,
25
+ VectorStore,
26
+ } from './types.js';
27
+
28
+ export interface RestVectorStoreOptions {
29
+ /** Dialect id (`evermind`, `qdrant`, `pinecone`, `vertex-ai`) or an instance. */
30
+ dialect: string | VectorDialect;
31
+ /** Service root, e.g. `https://my-cluster.qdrant.io`. */
32
+ baseUrl: string;
33
+ /** Collection / index / namespace. */
34
+ collection: string;
35
+ /** Static headers — API keys, `Authorization: Bearer …`. */
36
+ headers?: Record<string, string>;
37
+ /**
38
+ * Per-request auth, for credentials that expire. On GCP this is where a
39
+ * Workload-Identity access token goes, refreshed by the caller's token source
40
+ * rather than cached here — the store must never own a credential lifecycle.
41
+ */
42
+ authorize?: () => Promise<Record<string, string>> | Record<string, string>;
43
+ /** Dialect-specific deployment values (`index`, `indexEndpoint`, `deployedIndexId`, …). */
44
+ options?: Record<string, string>;
45
+ /** Records per upsert request. Default 100. */
46
+ batchSize?: number;
47
+ /** Retries on 429/5xx/network failure. Default 2. */
48
+ maxRetries?: number;
49
+ /** Base backoff in ms, doubled per attempt. Default 200. */
50
+ retryBackoffMs?: number;
51
+ /** Over-fetch multiplier when a residual filter must be applied locally. Default 4. */
52
+ residualOverfetch?: number;
53
+ /**
54
+ * Supplies chunk text for dialects that store vectors only (Vertex AI). In a
55
+ * GCP reference architecture this reads the chunk table from Firestore or
56
+ * BigQuery keyed by the same ids the index holds.
57
+ */
58
+ textResolver?: (ids: readonly string[]) => Promise<Map<string, string>>;
59
+ /** Injected for test. Default global `fetch`. */
60
+ fetchImpl?: typeof fetch;
61
+ /** Injected for test so retry backoff does not really sleep. */
62
+ sleep?: (ms: number) => Promise<void>;
63
+ }
64
+
65
+ export class RestVectorStore implements VectorStore {
66
+ readonly name: string;
67
+ readonly dialect: VectorDialect;
68
+
69
+ private readonly _baseUrl: string;
70
+ private readonly _ctx: DialectContext;
71
+ private readonly _headers: Record<string, string>;
72
+ private readonly _authorize: (() => Promise<Record<string, string>> | Record<string, string>) | undefined;
73
+ private readonly _batchSize: number;
74
+ private readonly _maxRetries: number;
75
+ private readonly _backoffMs: number;
76
+ private readonly _overfetch: number;
77
+ private readonly _textResolver: RestVectorStoreOptions['textResolver'];
78
+ private readonly _fetch: typeof fetch;
79
+ private readonly _sleep: (ms: number) => Promise<void>;
80
+
81
+ constructor(opts: RestVectorStoreOptions) {
82
+ this.dialect = typeof opts.dialect === 'string' ? getDialect(opts.dialect) : opts.dialect;
83
+ this.name = `rest:${this.dialect.id}`;
84
+ this._baseUrl = opts.baseUrl.replace(/\/+$/, '');
85
+ this._ctx = { collection: opts.collection, options: opts.options ?? {} };
86
+ this._headers = opts.headers ?? {};
87
+ this._authorize = opts.authorize;
88
+ this._batchSize = Math.max(1, opts.batchSize ?? 100);
89
+ this._maxRetries = Math.max(0, opts.maxRetries ?? 2);
90
+ this._backoffMs = Math.max(0, opts.retryBackoffMs ?? 200);
91
+ this._overfetch = Math.max(1, opts.residualOverfetch ?? 4);
92
+ this._textResolver = opts.textResolver;
93
+ this._fetch = opts.fetchImpl ?? ((...args) => fetch(...args));
94
+ this._sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
95
+ }
96
+
97
+ async upsert(records: readonly VectorRecord[]): Promise<UpsertResult> {
98
+ let upserted = 0;
99
+ let skipped = 0;
100
+ // Batched because every vendor caps request size, and a 10k-record ingest
101
+ // that fails at record 9,999 must not lose the first 9,998.
102
+ for (let i = 0; i < records.length; i += this._batchSize) {
103
+ const batch = records.slice(i, i + this._batchSize);
104
+ const json = await this._send(this.dialect.upsert(this._ctx, batch));
105
+ const result = this.dialect.parseUpsert(json, batch.length);
106
+ upserted += result.upserted;
107
+ skipped += result.skipped;
108
+ }
109
+ return { upserted, skipped };
110
+ }
111
+
112
+ async query(query: VectorQuery): Promise<VectorMatch[]> {
113
+ return this._search(query, (translated, topK) =>
114
+ this.dialect.query(this._ctx, {
115
+ ...(query.vector ? { vector: query.vector } : {}),
116
+ ...(query.text ? { text: query.text } : {}),
117
+ topK,
118
+ ...(translated !== undefined ? { filter: translated } : {}),
119
+ includeVectors: query.includeVectors ?? false,
120
+ }),
121
+ );
122
+ }
123
+
124
+ async keywordSearch(query: VectorQuery): Promise<VectorMatch[]> {
125
+ const text = query.text?.trim();
126
+ if (!text || !this.dialect.supportsKeywordSearch || !this.dialect.keywordSearch) return [];
127
+ const build = this.dialect.keywordSearch.bind(this.dialect);
128
+ return this._search(query, (translated, topK) =>
129
+ build(this._ctx, { text, topK, ...(translated !== undefined ? { filter: translated } : {}) }),
130
+ );
131
+ }
132
+
133
+ async fetch(ids: readonly string[]): Promise<VectorRecord[]> {
134
+ if (ids.length === 0) return [];
135
+ const json = await this._send(this.dialect.fetch(this._ctx, ids));
136
+ const records = this.dialect.parseRecords(json);
137
+ if (this.dialect.storesText || !this._textResolver) return records;
138
+
139
+ const texts = await this._textResolver(records.map((r) => r.id));
140
+ return records.map((r) => ({ ...r, text: texts.get(r.id) ?? r.text }));
141
+ }
142
+
143
+ async delete(ids: readonly string[]): Promise<number> {
144
+ if (ids.length === 0) return 0;
145
+ for (let i = 0; i < ids.length; i += this._batchSize) {
146
+ await this._send(this.dialect.deleteByIds(this._ctx, ids.slice(i, i + this._batchSize)));
147
+ }
148
+ return ids.length;
149
+ }
150
+
151
+ async deleteByFilter(filter: MetadataFilter, scope?: AccessScope): Promise<number> {
152
+ const effective = combineFilters(filter, scope) as MetadataFilter;
153
+ const translated = this.dialect.translateFilter(effective);
154
+
155
+ // Server-side bulk delete is only safe when the dialect can express the
156
+ // WHOLE predicate. A residual would mean deleting a superset — so the
157
+ // emulated path (query, filter locally, delete by id) is used instead.
158
+ if (this.dialect.supportsDeleteByFilter && this.dialect.deleteByFilter && !translated.residual) {
159
+ await this._send(this.dialect.deleteByFilter(this._ctx, translated.pushed));
160
+ return -1; // vendor bulk deletes do not report a count
161
+ }
162
+
163
+ const doomed = await this.query({ filter, ...(scope ? { scope } : {}), topK: 10_000 });
164
+ return this.delete(doomed.map((m) => m.id));
165
+ }
166
+
167
+ async count(filter?: MetadataFilter, scope?: AccessScope): Promise<number> {
168
+ const effective = combineFilters(filter, scope);
169
+ const translated = effective ? this.dialect.translateFilter(effective) : undefined;
170
+
171
+ if (this.dialect.supportsCount && this.dialect.count && this.dialect.parseCount && !translated?.residual) {
172
+ const json = await this._send(this.dialect.count(this._ctx, translated?.pushed));
173
+ return this.dialect.parseCount(json);
174
+ }
175
+ const matches = await this.query({ ...(filter ? { filter } : {}), ...(scope ? { scope } : {}), topK: 10_000 });
176
+ return matches.length;
177
+ }
178
+
179
+ /** Shared query path: translate, over-fetch for the residual, filter, hydrate. */
180
+ private async _search(
181
+ query: VectorQuery,
182
+ build: (translated: unknown, topK: number) => HttpRequest,
183
+ ): Promise<VectorMatch[]> {
184
+ const topK = Math.max(1, query.topK ?? 10);
185
+ const effective = combineFilters(query.filter, query.scope);
186
+ const translated = effective ? this.dialect.translateFilter(effective) : undefined;
187
+ const wanted = translated?.residual ? topK * this._overfetch : topK;
188
+
189
+ const json = await this._send(build(translated?.pushed, wanted));
190
+ let matches = this.dialect.parseMatches(json);
191
+
192
+ if (translated?.residual) {
193
+ const residual = translated.residual;
194
+ matches = matches.filter((m) => matchesFilter(m.metadata, residual));
195
+ }
196
+ if (query.minScore !== undefined) {
197
+ matches = matches.filter((m) => m.score >= (query.minScore as number));
198
+ }
199
+ matches = matches.slice(0, topK);
200
+
201
+ if (!this.dialect.storesText && this._textResolver && matches.length > 0) {
202
+ const texts = await this._textResolver(matches.map((m) => m.id));
203
+ matches = matches.map((m) => ({ ...m, text: texts.get(m.id) ?? m.text }));
204
+ }
205
+ return matches;
206
+ }
207
+
208
+ private async _send(request: HttpRequest): Promise<unknown> {
209
+ const dynamic = this._authorize ? await this._authorize() : {};
210
+ const headers = { 'Content-Type': 'application/json', ...this._headers, ...dynamic };
211
+ const url = `${this._baseUrl}${request.path}`;
212
+
213
+ let lastError: unknown;
214
+ for (let attempt = 0; attempt <= this._maxRetries; attempt++) {
215
+ if (attempt > 0) await this._sleep(this._backoffMs * 2 ** (attempt - 1));
216
+ try {
217
+ const res = await this._fetch(url, {
218
+ method: request.method,
219
+ headers,
220
+ ...(request.body !== undefined ? { body: JSON.stringify(request.body) } : {}),
221
+ });
222
+ if (res.ok) return await res.json().catch(() => ({}));
223
+
224
+ const detail = await res.text().catch(() => '');
225
+ lastError = new Error(`${this.name} ${request.method} ${request.path} → ${res.status}: ${detail}`);
226
+ // 4xx other than rate-limiting is a request the server will reject
227
+ // identically on retry; failing fast keeps a bad filter from
228
+ // becoming a retry storm.
229
+ if (res.status !== 429 && res.status < 500) break;
230
+ } catch (err) {
231
+ lastError = err;
232
+ }
233
+ }
234
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
235
+ }
236
+ }