@seanhogg/builderforce-memory 2026.7.12 → 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,702 @@
1
+ /**
2
+ * vectorstore/dialects.ts — vendor wire shapes, as registry DATA.
3
+ *
4
+ * A dialect is everything that differs between vector databases: the URL shape,
5
+ * the request bodies, the response parsing, and how much of the filter algebra the
6
+ * vendor's query language can express. Supporting a new database is REGISTERING a
7
+ * dialect, never editing a branch in the store — which is the whole reason the
8
+ * adapter is one class over a registry rather than one class per vendor.
9
+ *
10
+ * `translateFilter` returns a `residual`: the part of the filter the vendor cannot
11
+ * express. The store applies the residual locally AFTER the server has done the
12
+ * part it could push down, and over-fetches to compensate. That is the honest
13
+ * design — the alternative is silently dropping a clause, which on an ACL clause
14
+ * is a cross-tenant leak.
15
+ *
16
+ * ⚠️ The three vendor dialects here are validated by unit tests that assert the
17
+ * emitted request bodies and parse recorded response shapes; they have NOT been
18
+ * run against live services in this repository (no credentials). The `evermind`
19
+ * dialect is the canonical contract this package defines and owns.
20
+ */
21
+
22
+ import type {
23
+ LeafFilter,
24
+ MetadataFilter,
25
+ MetadataRecord,
26
+ UpsertResult,
27
+ VectorMatch,
28
+ VectorRecord,
29
+ } from './types.js';
30
+
31
+ export interface HttpRequest {
32
+ /** Appended to the adapter's `baseUrl`. */
33
+ path: string;
34
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
35
+ body?: unknown;
36
+ }
37
+
38
+ export interface DialectContext {
39
+ /** Collection / index / namespace name. */
40
+ collection: string;
41
+ /** Extra per-deployment values a dialect needs (project, deployedIndexId, …). */
42
+ options: Record<string, string>;
43
+ }
44
+
45
+ export interface TranslatedFilter {
46
+ /** The vendor-native filter object, or `undefined` when there is nothing to push. */
47
+ pushed?: unknown;
48
+ /** The clauses the vendor could not express — applied locally by the store. */
49
+ residual?: MetadataFilter;
50
+ }
51
+
52
+ export interface VectorDialect {
53
+ readonly id: string;
54
+ /**
55
+ * False when the vendor stores vectors only (Vertex AI Vector Search). The
56
+ * store then hydrates `text` through its `textResolver`, and says so on the
57
+ * telemetry span, rather than returning matches with empty text.
58
+ */
59
+ readonly storesText: boolean;
60
+ readonly supportsKeywordSearch: boolean;
61
+ /** False when `count` must be emulated by a capped query. */
62
+ readonly supportsCount: boolean;
63
+ /** False when a filtered bulk delete must be emulated by query-then-delete. */
64
+ readonly supportsDeleteByFilter: boolean;
65
+
66
+ translateFilter(filter: MetadataFilter): TranslatedFilter;
67
+
68
+ upsert(ctx: DialectContext, records: readonly VectorRecord[]): HttpRequest;
69
+ parseUpsert(json: unknown, sent: number): UpsertResult;
70
+
71
+ query(
72
+ ctx: DialectContext,
73
+ args: { vector?: Float32Array; text?: string; topK: number; filter?: unknown; includeVectors: boolean },
74
+ ): HttpRequest;
75
+ parseMatches(json: unknown): VectorMatch[];
76
+
77
+ deleteByIds(ctx: DialectContext, ids: readonly string[]): HttpRequest;
78
+ fetch(ctx: DialectContext, ids: readonly string[]): HttpRequest;
79
+ parseRecords(json: unknown): VectorRecord[];
80
+
81
+ keywordSearch?(
82
+ ctx: DialectContext,
83
+ args: { text: string; topK: number; filter?: unknown },
84
+ ): HttpRequest;
85
+ deleteByFilter?(ctx: DialectContext, filter?: unknown): HttpRequest;
86
+ count?(ctx: DialectContext, filter?: unknown): HttpRequest;
87
+ parseCount?(json: unknown): number;
88
+ }
89
+
90
+ // ── helpers ───────────────────────────────────────────────────────────────────
91
+
92
+ const vecToArray = (v?: Float32Array): number[] | undefined => (v ? Array.from(v) : undefined);
93
+ const arrayToVec = (v: unknown): Float32Array | undefined =>
94
+ Array.isArray(v) ? Float32Array.from(v as number[]) : undefined;
95
+
96
+ const asRecord = (v: unknown): Record<string, unknown> => (v && typeof v === 'object' ? v as Record<string, unknown> : {});
97
+ const asMetadata = (v: unknown): MetadataRecord => asRecord(v) as MetadataRecord;
98
+
99
+ /**
100
+ * Splits a filter into the leaf clauses a dialect accepts and the rest.
101
+ *
102
+ * Only a top-level AND is split. Pushing half an OR to the server and keeping the
103
+ * other half local would widen the server-side result set to *everything* that
104
+ * matches either branch, so a disjunction (or a NOT) is sent to the local pass
105
+ * whole. That is a recall cost, never a correctness one.
106
+ */
107
+ function partition(
108
+ filter: MetadataFilter,
109
+ accepts: (leaf: LeafFilter) => boolean,
110
+ ): { supported: LeafFilter[]; residual: MetadataFilter[] } {
111
+ if (filter.op === 'and') {
112
+ const supported: LeafFilter[] = [];
113
+ const residual: MetadataFilter[] = [];
114
+ for (const clause of filter.filters) {
115
+ const split = partition(clause, accepts);
116
+ supported.push(...split.supported);
117
+ residual.push(...split.residual);
118
+ }
119
+ return { supported, residual };
120
+ }
121
+ if (filter.op === 'or' || filter.op === 'not') {
122
+ return { supported: [], residual: [filter] };
123
+ }
124
+ return accepts(filter) ? { supported: [filter], residual: [] } : { supported: [], residual: [filter] };
125
+ }
126
+
127
+ function joinResidual(residual: MetadataFilter[]): MetadataFilter | undefined {
128
+ if (residual.length === 0) return undefined;
129
+ return residual.length === 1 ? residual[0] as MetadataFilter : { op: 'and', filters: residual };
130
+ }
131
+
132
+ // ── evermind: the contract this package defines ───────────────────────────────
133
+
134
+ /**
135
+ * The canonical gateway contract. A thin service in front of ANY database can
136
+ * implement it (the BuilderForce.ai gateway does), which keeps a customer's chosen
137
+ * store behind their own network boundary while this package speaks one protocol.
138
+ * The filter algebra crosses the wire unchanged, so nothing is ever residual.
139
+ */
140
+ export const evermindDialect: VectorDialect = {
141
+ id: 'evermind',
142
+ storesText: true,
143
+ supportsKeywordSearch: true,
144
+ supportsCount: true,
145
+ supportsDeleteByFilter: true,
146
+
147
+ translateFilter: (filter) => ({ pushed: filter }),
148
+
149
+ upsert: (ctx, records) => ({
150
+ path: `/collections/${encodeURIComponent(ctx.collection)}/upsert`,
151
+ method: 'POST',
152
+ body: {
153
+ records: records.map((r) => ({
154
+ id: r.id,
155
+ text: r.text,
156
+ vector: vecToArray(r.vector),
157
+ metadata: r.metadata,
158
+ })),
159
+ },
160
+ }),
161
+ parseUpsert: (json, sent) => {
162
+ const body = asRecord(json);
163
+ return {
164
+ upserted: Number(body['upserted'] ?? sent) || 0,
165
+ skipped: Number(body['skipped'] ?? 0) || 0,
166
+ };
167
+ },
168
+
169
+ query: (ctx, args) => ({
170
+ path: `/collections/${encodeURIComponent(ctx.collection)}/query`,
171
+ method: 'POST',
172
+ body: {
173
+ vector: vecToArray(args.vector),
174
+ text: args.text,
175
+ topK: args.topK,
176
+ filter: args.filter,
177
+ includeVectors: args.includeVectors,
178
+ },
179
+ }),
180
+ parseMatches: (json) => {
181
+ const matches = asRecord(json)['matches'];
182
+ if (!Array.isArray(matches)) return [];
183
+ return matches.map((m) => {
184
+ const row = asRecord(m);
185
+ const match: VectorMatch = {
186
+ id: String(row['id'] ?? ''),
187
+ text: String(row['text'] ?? ''),
188
+ score: Number(row['score']) || 0,
189
+ metadata: asMetadata(row['metadata']),
190
+ };
191
+ const vector = arrayToVec(row['vector']);
192
+ if (vector) match.vector = vector;
193
+ return match;
194
+ });
195
+ },
196
+
197
+ keywordSearch: (ctx, args) => ({
198
+ path: `/collections/${encodeURIComponent(ctx.collection)}/keyword`,
199
+ method: 'POST',
200
+ body: { text: args.text, topK: args.topK, filter: args.filter },
201
+ }),
202
+
203
+ deleteByIds: (ctx, ids) => ({
204
+ path: `/collections/${encodeURIComponent(ctx.collection)}/delete`,
205
+ method: 'POST',
206
+ body: { ids },
207
+ }),
208
+ deleteByFilter: (ctx, filter) => ({
209
+ path: `/collections/${encodeURIComponent(ctx.collection)}/delete`,
210
+ method: 'POST',
211
+ body: { filter },
212
+ }),
213
+
214
+ fetch: (ctx, ids) => ({
215
+ path: `/collections/${encodeURIComponent(ctx.collection)}/fetch`,
216
+ method: 'POST',
217
+ body: { ids },
218
+ }),
219
+ parseRecords: (json) => {
220
+ const records = asRecord(json)['records'];
221
+ if (!Array.isArray(records)) return [];
222
+ return records.map((r) => {
223
+ const row = asRecord(r);
224
+ const record: VectorRecord = {
225
+ id: String(row['id'] ?? ''),
226
+ text: String(row['text'] ?? ''),
227
+ metadata: asMetadata(row['metadata']),
228
+ };
229
+ const vector = arrayToVec(row['vector']);
230
+ if (vector) record.vector = vector;
231
+ return record;
232
+ });
233
+ },
234
+
235
+ count: (ctx, filter) => ({
236
+ path: `/collections/${encodeURIComponent(ctx.collection)}/count`,
237
+ method: 'POST',
238
+ body: { filter },
239
+ }),
240
+ parseCount: (json) => Number(asRecord(json)['count']) || 0,
241
+ };
242
+
243
+ // ── Qdrant ────────────────────────────────────────────────────────────────────
244
+
245
+ /** Maps one leaf onto a Qdrant field condition. */
246
+ function qdrantLeaf(leaf: LeafFilter): Record<string, unknown> | undefined {
247
+ switch (leaf.op) {
248
+ case 'eq':
249
+ return { key: leaf.field, match: { value: leaf.value } };
250
+ case 'in':
251
+ case 'anyOf':
252
+ return { key: leaf.field, match: { any: leaf.values } };
253
+ case 'nin':
254
+ return { key: leaf.field, match: { except: leaf.values } };
255
+ case 'contains':
256
+ return { key: leaf.field, match: { text: leaf.value } };
257
+ case 'exists':
258
+ return { is_empty: { key: leaf.field } };
259
+ case 'gt': case 'gte': case 'lt': case 'lte':
260
+ return typeof leaf.value === 'number'
261
+ ? { key: leaf.field, range: { [leaf.op]: leaf.value } }
262
+ : undefined;
263
+ default:
264
+ return undefined;
265
+ }
266
+ }
267
+
268
+ export const qdrantDialect: VectorDialect = {
269
+ id: 'qdrant',
270
+ storesText: true,
271
+ // Qdrant has full-text conditions but no BM25 ranking over the whole corpus,
272
+ // so keyword search is declared unsupported rather than faked with a filter.
273
+ supportsKeywordSearch: false,
274
+ supportsCount: true,
275
+ supportsDeleteByFilter: true,
276
+
277
+ translateFilter: (filter) => {
278
+ const { supported, residual } = partition(filter, (leaf) => {
279
+ if (leaf.op === 'exists') return false; // `is_empty` is the inverse; keep it local
280
+ if (leaf.op === 'ne') return false;
281
+ return qdrantLeaf(leaf) !== undefined;
282
+ });
283
+ const must = supported.map(qdrantLeaf).filter(Boolean);
284
+ const out: TranslatedFilter = {};
285
+ if (must.length > 0) out.pushed = { must };
286
+ const rest = joinResidual(residual);
287
+ if (rest) out.residual = rest;
288
+ return out;
289
+ },
290
+
291
+ upsert: (ctx, records) => ({
292
+ path: `/collections/${encodeURIComponent(ctx.collection)}/points`,
293
+ method: 'PUT',
294
+ body: {
295
+ points: records.map((r) => ({
296
+ id: r.id,
297
+ vector: vecToArray(r.vector) ?? [],
298
+ // Qdrant has no first-class text field; the chunk rides in the
299
+ // payload beside its metadata under a reserved key.
300
+ payload: { ...r.metadata, __text: r.text },
301
+ })),
302
+ },
303
+ }),
304
+ parseUpsert: (_json, sent) => ({ upserted: sent, skipped: 0 }),
305
+
306
+ query: (ctx, args) => ({
307
+ path: `/collections/${encodeURIComponent(ctx.collection)}/points/search`,
308
+ method: 'POST',
309
+ body: {
310
+ vector: vecToArray(args.vector) ?? [],
311
+ limit: args.topK,
312
+ filter: args.filter,
313
+ with_payload: true,
314
+ with_vector: args.includeVectors,
315
+ },
316
+ }),
317
+ parseMatches: (json) => {
318
+ const result = asRecord(json)['result'];
319
+ if (!Array.isArray(result)) return [];
320
+ return result.map((r) => {
321
+ const row = asRecord(r);
322
+ const payload = asRecord(row['payload']);
323
+ const { __text, ...metadata } = payload;
324
+ const match: VectorMatch = {
325
+ id: String(row['id'] ?? ''),
326
+ text: String(__text ?? ''),
327
+ score: Number(row['score']) || 0,
328
+ metadata: metadata as MetadataRecord,
329
+ };
330
+ const vector = arrayToVec(row['vector']);
331
+ if (vector) match.vector = vector;
332
+ return match;
333
+ });
334
+ },
335
+
336
+ deleteByIds: (ctx, ids) => ({
337
+ path: `/collections/${encodeURIComponent(ctx.collection)}/points/delete`,
338
+ method: 'POST',
339
+ body: { points: ids },
340
+ }),
341
+ deleteByFilter: (ctx, filter) => ({
342
+ path: `/collections/${encodeURIComponent(ctx.collection)}/points/delete`,
343
+ method: 'POST',
344
+ body: { filter },
345
+ }),
346
+
347
+ fetch: (ctx, ids) => ({
348
+ path: `/collections/${encodeURIComponent(ctx.collection)}/points`,
349
+ method: 'POST',
350
+ body: { ids, with_payload: true, with_vector: true },
351
+ }),
352
+ parseRecords: (json) => {
353
+ const result = asRecord(json)['result'];
354
+ if (!Array.isArray(result)) return [];
355
+ return result.map((r) => {
356
+ const row = asRecord(r);
357
+ const payload = asRecord(row['payload']);
358
+ const { __text, ...metadata } = payload;
359
+ const record: VectorRecord = {
360
+ id: String(row['id'] ?? ''),
361
+ text: String(__text ?? ''),
362
+ metadata: metadata as MetadataRecord,
363
+ };
364
+ const vector = arrayToVec(row['vector']);
365
+ if (vector) record.vector = vector;
366
+ return record;
367
+ });
368
+ },
369
+
370
+ count: (ctx, filter) => ({
371
+ path: `/collections/${encodeURIComponent(ctx.collection)}/points/count`,
372
+ method: 'POST',
373
+ body: { filter, exact: true },
374
+ }),
375
+ parseCount: (json) => Number(asRecord(asRecord(json)['result'])['count']) || 0,
376
+ };
377
+
378
+ // ── Pinecone ──────────────────────────────────────────────────────────────────
379
+
380
+ function pineconeLeaf(leaf: LeafFilter): Record<string, unknown> | undefined {
381
+ switch (leaf.op) {
382
+ case 'eq': return { [leaf.field]: { $eq: leaf.value } };
383
+ case 'ne': return { [leaf.field]: { $ne: leaf.value } };
384
+ case 'gt': return { [leaf.field]: { $gt: leaf.value } };
385
+ case 'gte': return { [leaf.field]: { $gte: leaf.value } };
386
+ case 'lt': return { [leaf.field]: { $lt: leaf.value } };
387
+ case 'lte': return { [leaf.field]: { $lte: leaf.value } };
388
+ // `$in` matches when a list-valued field intersects, which is exactly the
389
+ // ACL semantics `anyOf` describes.
390
+ case 'in': case 'anyOf': return { [leaf.field]: { $in: leaf.values } };
391
+ case 'nin': return { [leaf.field]: { $nin: leaf.values } };
392
+ default: return undefined;
393
+ }
394
+ }
395
+
396
+ export const pineconeDialect: VectorDialect = {
397
+ id: 'pinecone',
398
+ storesText: true,
399
+ supportsKeywordSearch: false,
400
+ supportsCount: false,
401
+ supportsDeleteByFilter: true,
402
+
403
+ translateFilter: (filter) => {
404
+ const { supported, residual } = partition(filter, (leaf) => pineconeLeaf(leaf) !== undefined);
405
+ const clauses = supported.map(pineconeLeaf).filter(Boolean) as Record<string, unknown>[];
406
+ const out: TranslatedFilter = {};
407
+ if (clauses.length === 1) out.pushed = clauses[0];
408
+ else if (clauses.length > 1) out.pushed = { $and: clauses };
409
+ const rest = joinResidual(residual);
410
+ if (rest) out.residual = rest;
411
+ return out;
412
+ },
413
+
414
+ upsert: (ctx, records) => ({
415
+ path: '/vectors/upsert',
416
+ method: 'POST',
417
+ body: {
418
+ namespace: ctx.collection,
419
+ vectors: records.map((r) => ({
420
+ id: r.id,
421
+ values: vecToArray(r.vector) ?? [],
422
+ metadata: { ...r.metadata, __text: r.text },
423
+ })),
424
+ },
425
+ }),
426
+ parseUpsert: (json, sent) => ({
427
+ upserted: Number(asRecord(json)['upsertedCount'] ?? sent) || 0,
428
+ skipped: 0,
429
+ }),
430
+
431
+ query: (ctx, args) => ({
432
+ path: '/query',
433
+ method: 'POST',
434
+ body: {
435
+ namespace: ctx.collection,
436
+ vector: vecToArray(args.vector) ?? [],
437
+ topK: args.topK,
438
+ filter: args.filter,
439
+ includeMetadata: true,
440
+ includeValues: args.includeVectors,
441
+ },
442
+ }),
443
+ parseMatches: (json) => {
444
+ const matches = asRecord(json)['matches'];
445
+ if (!Array.isArray(matches)) return [];
446
+ return matches.map((m) => {
447
+ const row = asRecord(m);
448
+ const meta = asRecord(row['metadata']);
449
+ const { __text, ...metadata } = meta;
450
+ const match: VectorMatch = {
451
+ id: String(row['id'] ?? ''),
452
+ text: String(__text ?? ''),
453
+ score: Number(row['score']) || 0,
454
+ metadata: metadata as MetadataRecord,
455
+ };
456
+ const vector = arrayToVec(row['values']);
457
+ if (vector) match.vector = vector;
458
+ return match;
459
+ });
460
+ },
461
+
462
+ deleteByIds: (ctx, ids) => ({
463
+ path: '/vectors/delete',
464
+ method: 'POST',
465
+ body: { namespace: ctx.collection, ids },
466
+ }),
467
+ deleteByFilter: (ctx, filter) => ({
468
+ path: '/vectors/delete',
469
+ method: 'POST',
470
+ body: { namespace: ctx.collection, filter },
471
+ }),
472
+
473
+ fetch: (ctx, ids) => ({
474
+ path: '/vectors/fetch',
475
+ method: 'POST',
476
+ body: { namespace: ctx.collection, ids },
477
+ }),
478
+ parseRecords: (json) => {
479
+ const vectors = asRecord(asRecord(json)['vectors']);
480
+ return Object.entries(vectors).map(([id, raw]) => {
481
+ const row = asRecord(raw);
482
+ const meta = asRecord(row['metadata']);
483
+ const { __text, ...metadata } = meta;
484
+ const record: VectorRecord = {
485
+ id,
486
+ text: String(__text ?? ''),
487
+ metadata: metadata as MetadataRecord,
488
+ };
489
+ const vector = arrayToVec(row['values']);
490
+ if (vector) record.vector = vector;
491
+ return record;
492
+ });
493
+ },
494
+ };
495
+
496
+ // ── Vertex AI Vector Search (GCP) ─────────────────────────────────────────────
497
+
498
+ /**
499
+ * Vertex expresses filters as `restricts` (string namespaces with an allow list)
500
+ * and `numericRestricts` (numeric comparisons), which is a close fit for this
501
+ * algebra: `eq`/`in`/`anyOf` on strings become a restrict namespace, and numeric
502
+ * comparisons become a numeric restrict. Anything else stays residual.
503
+ *
504
+ * Vertex stores vectors and restricts, NOT text — hence `storesText: false`. The
505
+ * store hydrates chunk text through its `textResolver`, which in a real GCP
506
+ * architecture reads from Firestore, BigQuery or GCS beside the index. Pretending
507
+ * otherwise would return matches with empty text and an answer with no evidence.
508
+ */
509
+ export const vertexAiDialect: VectorDialect = {
510
+ id: 'vertex-ai',
511
+ storesText: false,
512
+ supportsKeywordSearch: false,
513
+ supportsCount: false,
514
+ supportsDeleteByFilter: false,
515
+
516
+ translateFilter: (filter) => {
517
+ const { supported, residual } = partition(filter, (leaf) => {
518
+ if (leaf.op === 'eq') return typeof leaf.value === 'string' || typeof leaf.value === 'number';
519
+ if (leaf.op === 'in' || leaf.op === 'anyOf') return leaf.values.every((v) => typeof v === 'string');
520
+ if (leaf.op === 'gt' || leaf.op === 'gte' || leaf.op === 'lt' || leaf.op === 'lte') {
521
+ return typeof leaf.value === 'number';
522
+ }
523
+ return false;
524
+ });
525
+
526
+ const restricts: Array<{ namespace: string; allowList: string[] }> = [];
527
+ const numericRestricts: Array<Record<string, unknown>> = [];
528
+ const VERTEX_NUMERIC_OP: Record<string, string> = {
529
+ gt: 'GREATER', gte: 'GREATER_EQUAL', lt: 'LESS', lte: 'LESS_EQUAL', eq: 'EQUAL',
530
+ };
531
+
532
+ for (const leaf of supported) {
533
+ if (leaf.op === 'in' || leaf.op === 'anyOf') {
534
+ restricts.push({ namespace: leaf.field, allowList: leaf.values as string[] });
535
+ } else if (leaf.op === 'eq' && typeof leaf.value === 'string') {
536
+ restricts.push({ namespace: leaf.field, allowList: [leaf.value] });
537
+ } else if ('value' in leaf && typeof leaf.value === 'number') {
538
+ numericRestricts.push({
539
+ namespace: leaf.field,
540
+ op: VERTEX_NUMERIC_OP[leaf.op] as string,
541
+ valueDouble: leaf.value,
542
+ });
543
+ }
544
+ }
545
+
546
+ const out: TranslatedFilter = {};
547
+ if (restricts.length || numericRestricts.length) out.pushed = { restricts, numericRestricts };
548
+ const rest = joinResidual(residual);
549
+ if (rest) out.residual = rest;
550
+ return out;
551
+ },
552
+
553
+ upsert: (ctx, records) => ({
554
+ path: `/v1/${ctx.options['index']}:upsertDatapoints`,
555
+ method: 'POST',
556
+ body: {
557
+ datapoints: records.map((r) => ({
558
+ datapointId: r.id,
559
+ featureVector: vecToArray(r.vector) ?? [],
560
+ restricts: stringRestricts(r.metadata),
561
+ numericRestricts: numericRestrictsOf(r.metadata),
562
+ })),
563
+ },
564
+ }),
565
+ parseUpsert: (_json, sent) => ({ upserted: sent, skipped: 0 }),
566
+
567
+ query: (ctx, args) => {
568
+ const pushed = asRecord(args.filter);
569
+ return {
570
+ path: `/v1/${ctx.options['indexEndpoint']}:findNeighbors`,
571
+ method: 'POST',
572
+ body: {
573
+ deployedIndexId: ctx.options['deployedIndexId'],
574
+ returnFullDatapoint: args.includeVectors,
575
+ queries: [{
576
+ datapoint: {
577
+ datapointId: 'query',
578
+ featureVector: vecToArray(args.vector) ?? [],
579
+ restricts: pushed['restricts'] ?? [],
580
+ numericRestricts: pushed['numericRestricts'] ?? [],
581
+ },
582
+ neighborCount: args.topK,
583
+ }],
584
+ },
585
+ };
586
+ },
587
+ parseMatches: (json) => {
588
+ const groups = asRecord(json)['nearestNeighbors'];
589
+ if (!Array.isArray(groups)) return [];
590
+ const neighbors = asRecord(groups[0])['neighbors'];
591
+ if (!Array.isArray(neighbors)) return [];
592
+ return neighbors.map((n) => {
593
+ const row = asRecord(n);
594
+ const datapoint = asRecord(row['datapoint']);
595
+ const match: VectorMatch = {
596
+ id: String(datapoint['datapointId'] ?? ''),
597
+ text: '',
598
+ // Vertex returns a DISTANCE; for the cosine measure similarity is
599
+ // 1 - distance. Reported on the same [0,1] scale as every adapter.
600
+ score: 1 - (Number(row['distance']) || 0),
601
+ metadata: restrictsToMetadata(datapoint),
602
+ };
603
+ const vector = arrayToVec(datapoint['featureVector']);
604
+ if (vector) match.vector = vector;
605
+ return match;
606
+ });
607
+ },
608
+
609
+ deleteByIds: (ctx, ids) => ({
610
+ path: `/v1/${ctx.options['index']}:removeDatapoints`,
611
+ method: 'POST',
612
+ body: { datapointIds: ids },
613
+ }),
614
+
615
+ fetch: (ctx, ids) => ({
616
+ path: `/v1/${ctx.options['indexEndpoint']}:readIndexDatapoints`,
617
+ method: 'POST',
618
+ body: { deployedIndexId: ctx.options['deployedIndexId'], ids },
619
+ }),
620
+ parseRecords: (json) => {
621
+ const datapoints = asRecord(json)['datapoints'];
622
+ if (!Array.isArray(datapoints)) return [];
623
+ return datapoints.map((d) => {
624
+ const row = asRecord(d);
625
+ const record: VectorRecord = {
626
+ id: String(row['datapointId'] ?? ''),
627
+ text: '',
628
+ metadata: restrictsToMetadata(row),
629
+ };
630
+ const vector = arrayToVec(row['featureVector']);
631
+ if (vector) record.vector = vector;
632
+ return record;
633
+ });
634
+ },
635
+ };
636
+
637
+ function stringRestricts(metadata: MetadataRecord): Array<{ namespace: string; allowList: string[] }> {
638
+ const out: Array<{ namespace: string; allowList: string[] }> = [];
639
+ for (const [namespace, value] of Object.entries(metadata)) {
640
+ if (typeof value === 'string') out.push({ namespace, allowList: [value] });
641
+ else if (Array.isArray(value)) out.push({ namespace, allowList: value });
642
+ }
643
+ return out;
644
+ }
645
+
646
+ function numericRestrictsOf(metadata: MetadataRecord): Array<Record<string, unknown>> {
647
+ return Object.entries(metadata)
648
+ .filter(([, v]) => typeof v === 'number')
649
+ .map(([namespace, v]) => ({ namespace, valueDouble: v as number }));
650
+ }
651
+
652
+ function restrictsToMetadata(datapoint: Record<string, unknown>): MetadataRecord {
653
+ const out: MetadataRecord = {};
654
+ const restricts = datapoint['restricts'];
655
+ if (Array.isArray(restricts)) {
656
+ for (const r of restricts) {
657
+ const row = asRecord(r);
658
+ const list = Array.isArray(row['allowList']) ? row['allowList'] as string[] : [];
659
+ const namespace = String(row['namespace'] ?? '');
660
+ if (namespace) out[namespace] = list.length === 1 ? list[0] as string : list;
661
+ }
662
+ }
663
+ const numeric = datapoint['numericRestricts'];
664
+ if (Array.isArray(numeric)) {
665
+ for (const r of numeric) {
666
+ const row = asRecord(r);
667
+ const namespace = String(row['namespace'] ?? '');
668
+ const value = row['valueDouble'] ?? row['valueFloat'] ?? row['valueInt'];
669
+ if (namespace && value !== undefined) out[namespace] = Number(value);
670
+ }
671
+ }
672
+ return out;
673
+ }
674
+
675
+ // ── registry ──────────────────────────────────────────────────────────────────
676
+
677
+ const REGISTRY = new Map<string, VectorDialect>([
678
+ [evermindDialect.id, evermindDialect],
679
+ [qdrantDialect.id, qdrantDialect],
680
+ [pineconeDialect.id, pineconeDialect],
681
+ [vertexAiDialect.id, vertexAiDialect],
682
+ ]);
683
+
684
+ /** Registers (or replaces) a dialect — how a customer adds their own database. */
685
+ export function registerDialect(dialect: VectorDialect): void {
686
+ REGISTRY.set(dialect.id, dialect);
687
+ }
688
+
689
+ export function getDialect(id: string): VectorDialect {
690
+ const dialect = REGISTRY.get(id);
691
+ if (!dialect) {
692
+ throw new Error(
693
+ `Unknown vector dialect "${id}". Registered: ${[...REGISTRY.keys()].join(', ')}. ` +
694
+ 'Add your own with registerDialect().',
695
+ );
696
+ }
697
+ return dialect;
698
+ }
699
+
700
+ export function listDialects(): string[] {
701
+ return [...REGISTRY.keys()];
702
+ }