ai-database 2.1.1 → 2.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 (268) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +1063 -186
  3. package/dist/actions.d.ts +2 -2
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +1 -1
  6. package/dist/actions.js.map +1 -1
  7. package/dist/ai-promise-db.d.ts +52 -23
  8. package/dist/ai-promise-db.d.ts.map +1 -1
  9. package/dist/ai-promise-db.js +185 -164
  10. package/dist/ai-promise-db.js.map +1 -1
  11. package/dist/authorization.d.ts.map +1 -1
  12. package/dist/authorization.js +38 -30
  13. package/dist/authorization.js.map +1 -1
  14. package/dist/cascade-orchestrator.d.ts +404 -0
  15. package/dist/cascade-orchestrator.d.ts.map +1 -0
  16. package/dist/cascade-orchestrator.js +828 -0
  17. package/dist/cascade-orchestrator.js.map +1 -0
  18. package/dist/cascade-write-strategy.d.ts +584 -0
  19. package/dist/cascade-write-strategy.d.ts.map +1 -0
  20. package/dist/cascade-write-strategy.js +590 -0
  21. package/dist/cascade-write-strategy.js.map +1 -0
  22. package/dist/ch-adapter.d.ts +358 -0
  23. package/dist/ch-adapter.d.ts.map +1 -0
  24. package/dist/ch-adapter.js +929 -0
  25. package/dist/ch-adapter.js.map +1 -0
  26. package/dist/client/index.d.ts +42 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +43 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client.d.ts +266 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +81 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/constants.d.ts +64 -1
  35. package/dist/constants.d.ts.map +1 -1
  36. package/dist/constants.js +52 -2
  37. package/dist/constants.js.map +1 -1
  38. package/dist/dataloader.d.ts +99 -0
  39. package/dist/dataloader.d.ts.map +1 -0
  40. package/dist/dataloader.js +225 -0
  41. package/dist/dataloader.js.map +1 -0
  42. package/dist/db-provider-port.d.ts +501 -0
  43. package/dist/db-provider-port.d.ts.map +1 -0
  44. package/dist/db-provider-port.js +113 -0
  45. package/dist/db-provider-port.js.map +1 -0
  46. package/dist/digital-objects-provider.d.ts +49 -0
  47. package/dist/digital-objects-provider.d.ts.map +1 -0
  48. package/dist/digital-objects-provider.js +55 -0
  49. package/dist/digital-objects-provider.js.map +1 -0
  50. package/dist/do-sqlite-adapter.d.ts +402 -0
  51. package/dist/do-sqlite-adapter.d.ts.map +1 -0
  52. package/dist/do-sqlite-adapter.js +745 -0
  53. package/dist/do-sqlite-adapter.js.map +1 -0
  54. package/dist/docs-rels/custom-types.d.ts +134 -0
  55. package/dist/docs-rels/custom-types.d.ts.map +1 -0
  56. package/dist/docs-rels/custom-types.js +70 -0
  57. package/dist/docs-rels/custom-types.js.map +1 -0
  58. package/dist/docs-rels/index.d.ts +16 -0
  59. package/dist/docs-rels/index.d.ts.map +1 -0
  60. package/dist/docs-rels/index.js +16 -0
  61. package/dist/docs-rels/index.js.map +1 -0
  62. package/dist/docs-rels/migrations/index.d.ts +30 -0
  63. package/dist/docs-rels/migrations/index.d.ts.map +1 -0
  64. package/dist/docs-rels/migrations/index.js +128 -0
  65. package/dist/docs-rels/migrations/index.js.map +1 -0
  66. package/dist/docs-rels/schema.d.ts +2961 -0
  67. package/dist/docs-rels/schema.d.ts.map +1 -0
  68. package/dist/docs-rels/schema.js +244 -0
  69. package/dist/docs-rels/schema.js.map +1 -0
  70. package/dist/durable-clickhouse.d.ts.map +1 -1
  71. package/dist/durable-clickhouse.js +16 -13
  72. package/dist/durable-clickhouse.js.map +1 -1
  73. package/dist/durable-promise.d.ts.map +1 -1
  74. package/dist/durable-promise.js +34 -15
  75. package/dist/durable-promise.js.map +1 -1
  76. package/dist/errors.d.ts +127 -0
  77. package/dist/errors.d.ts.map +1 -0
  78. package/dist/errors.js +210 -0
  79. package/dist/errors.js.map +1 -0
  80. package/dist/eventbridge.d.ts +117 -0
  81. package/dist/eventbridge.d.ts.map +1 -0
  82. package/dist/eventbridge.js +238 -0
  83. package/dist/eventbridge.js.map +1 -0
  84. package/dist/events.d.ts +2 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +1 -1
  87. package/dist/events.js.map +1 -1
  88. package/dist/execution-queue.d.ts.map +1 -1
  89. package/dist/execution-queue.js +4 -5
  90. package/dist/execution-queue.js.map +1 -1
  91. package/dist/index.d.ts +37 -8
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +112 -6
  94. package/dist/index.js.map +1 -1
  95. package/dist/linguistic.d.ts +3 -108
  96. package/dist/linguistic.d.ts.map +1 -1
  97. package/dist/linguistic.js +3 -372
  98. package/dist/linguistic.js.map +1 -1
  99. package/dist/logger.d.ts +132 -0
  100. package/dist/logger.d.ts.map +1 -0
  101. package/dist/logger.js +137 -0
  102. package/dist/logger.js.map +1 -0
  103. package/dist/memory-provider.d.ts +129 -0
  104. package/dist/memory-provider.d.ts.map +1 -1
  105. package/dist/memory-provider.js +592 -257
  106. package/dist/memory-provider.js.map +1 -1
  107. package/dist/pg-adapter.d.ts +424 -0
  108. package/dist/pg-adapter.d.ts.map +1 -0
  109. package/dist/pg-adapter.js +921 -0
  110. package/dist/pg-adapter.js.map +1 -0
  111. package/dist/pipelines-iceberg-emitter.d.ts +327 -0
  112. package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
  113. package/dist/pipelines-iceberg-emitter.js +351 -0
  114. package/dist/pipelines-iceberg-emitter.js.map +1 -0
  115. package/dist/provider-capabilities.d.ts +146 -0
  116. package/dist/provider-capabilities.d.ts.map +1 -0
  117. package/dist/provider-capabilities.js +214 -0
  118. package/dist/provider-capabilities.js.map +1 -0
  119. package/dist/rdb-provider-adapter.d.ts +195 -0
  120. package/dist/rdb-provider-adapter.d.ts.map +1 -0
  121. package/dist/rdb-provider-adapter.js +291 -0
  122. package/dist/rdb-provider-adapter.js.map +1 -0
  123. package/dist/schema/cascade.d.ts +49 -10
  124. package/dist/schema/cascade.d.ts.map +1 -1
  125. package/dist/schema/cascade.js +491 -273
  126. package/dist/schema/cascade.js.map +1 -1
  127. package/dist/schema/definition-caches.d.ts +24 -0
  128. package/dist/schema/definition-caches.d.ts.map +1 -0
  129. package/dist/schema/definition-caches.js +26 -0
  130. package/dist/schema/definition-caches.js.map +1 -0
  131. package/dist/schema/dependency-graph.d.ts +45 -0
  132. package/dist/schema/dependency-graph.d.ts.map +1 -0
  133. package/dist/schema/dependency-graph.js +47 -0
  134. package/dist/schema/dependency-graph.js.map +1 -0
  135. package/dist/schema/diff.d.ts +103 -0
  136. package/dist/schema/diff.d.ts.map +1 -0
  137. package/dist/schema/diff.js +329 -0
  138. package/dist/schema/diff.js.map +1 -0
  139. package/dist/schema/entity-operations.d.ts +99 -0
  140. package/dist/schema/entity-operations.d.ts.map +1 -0
  141. package/dist/schema/entity-operations.js +818 -0
  142. package/dist/schema/entity-operations.js.map +1 -0
  143. package/dist/schema/generation-context.d.ts +202 -0
  144. package/dist/schema/generation-context.d.ts.map +1 -0
  145. package/dist/schema/generation-context.js +393 -0
  146. package/dist/schema/generation-context.js.map +1 -0
  147. package/dist/schema/index.d.ts +32 -34
  148. package/dist/schema/index.d.ts.map +1 -1
  149. package/dist/schema/index.js +462 -519
  150. package/dist/schema/index.js.map +1 -1
  151. package/dist/schema/migration.d.ts +205 -0
  152. package/dist/schema/migration.d.ts.map +1 -0
  153. package/dist/schema/migration.js +327 -0
  154. package/dist/schema/migration.js.map +1 -0
  155. package/dist/schema/nl-query-generator.d.ts +68 -0
  156. package/dist/schema/nl-query-generator.d.ts.map +1 -0
  157. package/dist/schema/nl-query-generator.js +362 -0
  158. package/dist/schema/nl-query-generator.js.map +1 -0
  159. package/dist/schema/nl-query.d.ts +65 -0
  160. package/dist/schema/nl-query.d.ts.map +1 -0
  161. package/dist/schema/nl-query.js +178 -0
  162. package/dist/schema/nl-query.js.map +1 -0
  163. package/dist/schema/parse.d.ts.map +1 -1
  164. package/dist/schema/parse.js +152 -89
  165. package/dist/schema/parse.js.map +1 -1
  166. package/dist/schema/provider.d.ts +38 -0
  167. package/dist/schema/provider.d.ts.map +1 -1
  168. package/dist/schema/provider.js +15 -7
  169. package/dist/schema/provider.js.map +1 -1
  170. package/dist/schema/resolve.d.ts +46 -5
  171. package/dist/schema/resolve.d.ts.map +1 -1
  172. package/dist/schema/resolve.js +334 -117
  173. package/dist/schema/resolve.js.map +1 -1
  174. package/dist/schema/search-utils.d.ts +76 -0
  175. package/dist/schema/search-utils.d.ts.map +1 -0
  176. package/dist/schema/search-utils.js +86 -0
  177. package/dist/schema/search-utils.js.map +1 -0
  178. package/dist/schema/seed.d.ts +53 -0
  179. package/dist/schema/seed.d.ts.map +1 -0
  180. package/dist/schema/seed.js +94 -0
  181. package/dist/schema/seed.js.map +1 -0
  182. package/dist/schema/semantic.d.ts +11 -0
  183. package/dist/schema/semantic.d.ts.map +1 -1
  184. package/dist/schema/semantic.js +262 -68
  185. package/dist/schema/semantic.js.map +1 -1
  186. package/dist/schema/sub-apis.d.ts +52 -0
  187. package/dist/schema/sub-apis.d.ts.map +1 -0
  188. package/dist/schema/sub-apis.js +216 -0
  189. package/dist/schema/sub-apis.js.map +1 -0
  190. package/dist/schema/system-entities.d.ts +42 -0
  191. package/dist/schema/system-entities.d.ts.map +1 -0
  192. package/dist/schema/system-entities.js +101 -0
  193. package/dist/schema/system-entities.js.map +1 -0
  194. package/dist/schema/types.d.ts +91 -9
  195. package/dist/schema/types.d.ts.map +1 -1
  196. package/dist/schema/union-fallback.d.ts +219 -0
  197. package/dist/schema/union-fallback.d.ts.map +1 -0
  198. package/dist/schema/union-fallback.js +331 -0
  199. package/dist/schema/union-fallback.js.map +1 -0
  200. package/dist/schema/value-generators/ai.d.ts +54 -0
  201. package/dist/schema/value-generators/ai.d.ts.map +1 -0
  202. package/dist/schema/value-generators/ai.js +136 -0
  203. package/dist/schema/value-generators/ai.js.map +1 -0
  204. package/dist/schema/value-generators/index.d.ts +126 -0
  205. package/dist/schema/value-generators/index.d.ts.map +1 -0
  206. package/dist/schema/value-generators/index.js +219 -0
  207. package/dist/schema/value-generators/index.js.map +1 -0
  208. package/dist/schema/value-generators/placeholder.d.ts +52 -0
  209. package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
  210. package/dist/schema/value-generators/placeholder.js +328 -0
  211. package/dist/schema/value-generators/placeholder.js.map +1 -0
  212. package/dist/schema/value-generators/types.d.ts +116 -0
  213. package/dist/schema/value-generators/types.d.ts.map +1 -0
  214. package/dist/schema/value-generators/types.js +11 -0
  215. package/dist/schema/value-generators/types.js.map +1 -0
  216. package/dist/schema/verb-derivation.d.ts +167 -0
  217. package/dist/schema/verb-derivation.d.ts.map +1 -0
  218. package/dist/schema/verb-derivation.js +281 -0
  219. package/dist/schema/verb-derivation.js.map +1 -0
  220. package/dist/schema/version.d.ts +111 -0
  221. package/dist/schema/version.d.ts.map +1 -0
  222. package/dist/schema/version.js +190 -0
  223. package/dist/schema/version.js.map +1 -0
  224. package/dist/schema.d.ts +1095 -23
  225. package/dist/schema.d.ts.map +1 -1
  226. package/dist/schema.js +2854 -38
  227. package/dist/schema.js.map +1 -1
  228. package/dist/semantic-vectors.d.ts +39 -0
  229. package/dist/semantic-vectors.d.ts.map +1 -0
  230. package/dist/semantic-vectors.js +334 -0
  231. package/dist/semantic-vectors.js.map +1 -0
  232. package/dist/semantic.d.ts +29 -1
  233. package/dist/semantic.d.ts.map +1 -1
  234. package/dist/semantic.js +26 -16
  235. package/dist/semantic.js.map +1 -1
  236. package/dist/telemetry.d.ts +128 -0
  237. package/dist/telemetry.d.ts.map +1 -0
  238. package/dist/telemetry.js +305 -0
  239. package/dist/telemetry.js.map +1 -0
  240. package/dist/tests.d.ts.map +1 -1
  241. package/dist/tests.js +30 -22
  242. package/dist/tests.js.map +1 -1
  243. package/dist/type-guards.d.ts +212 -0
  244. package/dist/type-guards.d.ts.map +1 -0
  245. package/dist/type-guards.js +318 -0
  246. package/dist/type-guards.js.map +1 -0
  247. package/dist/types.d.ts +33 -245
  248. package/dist/types.d.ts.map +1 -1
  249. package/dist/types.js +62 -72
  250. package/dist/types.js.map +1 -1
  251. package/dist/validation.d.ts +165 -0
  252. package/dist/validation.d.ts.map +1 -0
  253. package/dist/validation.js +639 -0
  254. package/dist/validation.js.map +1 -0
  255. package/dist/worker/db-provider.d.ts +168 -0
  256. package/dist/worker/db-provider.d.ts.map +1 -0
  257. package/dist/worker/db-provider.js +277 -0
  258. package/dist/worker/db-provider.js.map +1 -0
  259. package/dist/worker/index.d.ts +35 -0
  260. package/dist/worker/index.d.ts.map +1 -0
  261. package/dist/worker/index.js +37 -0
  262. package/dist/worker/index.js.map +1 -0
  263. package/dist/worker.d.ts +779 -0
  264. package/dist/worker.d.ts.map +1 -0
  265. package/dist/worker.js +2786 -0
  266. package/dist/worker.js.map +1 -0
  267. package/package.json +38 -8
  268. package/src/docs-rels/migrations/0001-init.sql +125 -0
@@ -6,7 +6,11 @@
6
6
  * Supports automatic embedding generation on create/update.
7
7
  */
8
8
  import { cosineSimilarity, computeRRF, extractEmbeddableText, generateContentHash, } from './semantic.js';
9
- import { EMBEDDING_DIMENSIONS } from './constants.js';
9
+ import { DEFAULT_EMBEDDING_DIMENSIONS, EMBEDDING_DIMENSIONS } from './constants.js';
10
+ import { SEMANTIC_VECTORS, DEFAULT_VECTOR, BASE_VECTOR_DIMENSIONS } from './semantic-vectors.js';
11
+ import { validateTypeName, validateEntityId, validateSearchQuery, validateEntityData, validateRelationName, validateEventPattern, validateActionType, validateArtifactUrl, validateListOptions, validateSearchOptions, validateFieldName, isDangerousField, } from './validation.js';
12
+ import { EntityNotFoundError, EntityAlreadyExistsError } from './errors.js';
13
+ import { logWarn } from './logger.js';
10
14
  // =============================================================================
11
15
  // Semaphore for Concurrency Control
12
16
  // =============================================================================
@@ -83,6 +87,157 @@ function generateId() {
83
87
  return crypto.randomUUID();
84
88
  }
85
89
  // =============================================================================
90
+ // Embedding Helper Functions
91
+ // =============================================================================
92
+ /**
93
+ * Simple hash function for deterministic randomness
94
+ *
95
+ * Generates a consistent hash value for any input string, used for
96
+ * creating deterministic "random" variations in embedding generation.
97
+ *
98
+ * @param str - The string to hash
99
+ * @returns A positive integer hash value
100
+ *
101
+ * @internal
102
+ */
103
+ function simpleHash(str) {
104
+ let hash = 0;
105
+ for (let i = 0; i < str.length; i++) {
106
+ const char = str.charCodeAt(i);
107
+ hash = (hash << 5) - hash + char;
108
+ hash = hash & hash; // Convert to 32-bit integer
109
+ }
110
+ return Math.abs(hash);
111
+ }
112
+ /**
113
+ * Generate a deterministic pseudo-random number from seed
114
+ *
115
+ * Uses sin function to generate predictable values that appear random
116
+ * but are reproducible given the same seed and index.
117
+ *
118
+ * @param seed - The seed value (typically from a hash)
119
+ * @param index - The position in the sequence
120
+ * @returns A number between 0 and 1
121
+ *
122
+ * @internal
123
+ */
124
+ function seededRandom(seed, index) {
125
+ const x = Math.sin(seed + index) * 10000;
126
+ return x - Math.floor(x);
127
+ }
128
+ /**
129
+ * Tokenize text into lowercase words
130
+ *
131
+ * Splits text on whitespace and punctuation, filters empty strings,
132
+ * and converts all words to lowercase for consistent matching.
133
+ *
134
+ * @param text - The text to tokenize
135
+ * @returns Array of lowercase word tokens
136
+ *
137
+ * @internal
138
+ */
139
+ function tokenizeText(text) {
140
+ return text
141
+ .toLowerCase()
142
+ .replace(/[^\w\s]/g, ' ')
143
+ .split(/\s+/)
144
+ .filter((w) => w.length > 0);
145
+ }
146
+ /**
147
+ * Get semantic vector for a word
148
+ *
149
+ * Looks up the word in SEMANTIC_VECTORS, or generates a deterministic
150
+ * fallback vector based on the word's hash if not found.
151
+ *
152
+ * @param word - The word to get a vector for
153
+ * @returns A 4-dimensional semantic vector
154
+ *
155
+ * @internal
156
+ */
157
+ function getWordVector(word) {
158
+ const lower = word.toLowerCase();
159
+ const known = SEMANTIC_VECTORS[lower];
160
+ if (known) {
161
+ return known;
162
+ }
163
+ // Generate deterministic vector based on word hash
164
+ const hash = simpleHash(lower);
165
+ return DEFAULT_VECTOR.map((v, i) => v + seededRandom(hash, i) * 0.1);
166
+ }
167
+ /**
168
+ * Aggregate word vectors into a single base vector
169
+ *
170
+ * Sums up the semantic vectors for all words in the input,
171
+ * creating a combined representation of the text's meaning.
172
+ *
173
+ * @param words - Array of word tokens
174
+ * @returns Aggregated 4-dimensional vector (not normalized)
175
+ *
176
+ * @internal
177
+ */
178
+ function aggregateWordVectors(words) {
179
+ const aggregated = new Array(BASE_VECTOR_DIMENSIONS).fill(0);
180
+ for (const word of words) {
181
+ const vec = getWordVector(word);
182
+ for (let i = 0; i < BASE_VECTOR_DIMENSIONS; i++) {
183
+ aggregated[i] += vec[i];
184
+ }
185
+ }
186
+ return aggregated;
187
+ }
188
+ /**
189
+ * Normalize a vector to unit length
190
+ *
191
+ * Divides each component by the vector's magnitude to create
192
+ * a unit vector (length = 1), enabling cosine similarity comparison.
193
+ *
194
+ * @param vector - The vector to normalize
195
+ * @returns A new unit-length vector
196
+ *
197
+ * @internal
198
+ */
199
+ function normalizeVector(vector) {
200
+ const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
201
+ return vector.map((v) => v / (norm || 1));
202
+ }
203
+ /**
204
+ * Expand base vector to full embedding dimensions
205
+ *
206
+ * Takes a 4-dimensional base vector and expands it to the target
207
+ * dimensions by cycling through base values and adding deterministic noise.
208
+ *
209
+ * @param normalized - Normalized base vector (4 dimensions)
210
+ * @param dimensions - Target number of dimensions
211
+ * @param textHash - Hash of original text for deterministic noise
212
+ * @returns Expanded vector (not normalized)
213
+ *
214
+ * @internal
215
+ */
216
+ function expandToFullDimensions(normalized, dimensions, textHash) {
217
+ const embedding = new Array(dimensions);
218
+ for (let i = 0; i < dimensions; i++) {
219
+ const baseIndex = i % BASE_VECTOR_DIMENSIONS;
220
+ const base = normalized[baseIndex];
221
+ const noise = seededRandom(textHash, i) * 0.1 - 0.05;
222
+ embedding[i] = base + noise;
223
+ }
224
+ return embedding;
225
+ }
226
+ /**
227
+ * Generate embedding for empty text
228
+ *
229
+ * Creates a low-magnitude embedding for empty input,
230
+ * using deterministic small values based on position.
231
+ *
232
+ * @param dimensions - Number of embedding dimensions
233
+ * @returns A low-magnitude embedding vector
234
+ *
235
+ * @internal
236
+ */
237
+ function generateEmptyEmbedding(dimensions) {
238
+ return Array.from({ length: dimensions }, (_, i) => seededRandom(0, i) * 0.01);
239
+ }
240
+ // =============================================================================
86
241
  // Verb Conjugation (Linguistic Helpers)
87
242
  // =============================================================================
88
243
  /**
@@ -196,8 +351,11 @@ function toPresent(verb) {
196
351
  if (verb.endsWith('y') && !isVowel(verb[verb.length - 2])) {
197
352
  return verb.slice(0, -1) + 'ies';
198
353
  }
199
- if (verb.endsWith('s') || verb.endsWith('x') || verb.endsWith('z') ||
200
- verb.endsWith('ch') || verb.endsWith('sh')) {
354
+ if (verb.endsWith('s') ||
355
+ verb.endsWith('x') ||
356
+ verb.endsWith('z') ||
357
+ verb.endsWith('ch') ||
358
+ verb.endsWith('sh')) {
201
359
  return verb + 'es';
202
360
  }
203
361
  return verb + 's';
@@ -255,9 +413,37 @@ export class MemoryProvider {
255
413
  semaphore;
256
414
  // Embedding configuration
257
415
  embeddingsConfig;
416
+ // Flag to use ai-functions for embeddings
417
+ useAiFunctions;
418
+ // Custom embedding provider (for testing or alternative services)
419
+ embeddingProvider;
420
+ // Embedding dimensions for mock provider
421
+ embeddingDimensions;
258
422
  constructor(options = {}) {
259
423
  this.semaphore = new Semaphore(options.concurrency ?? 10);
260
424
  this.embeddingsConfig = options.embeddings ?? {};
425
+ this.useAiFunctions = options.useAiFunctions ?? false;
426
+ if (options.embeddingProvider !== undefined) {
427
+ this.embeddingProvider = options.embeddingProvider;
428
+ }
429
+ this.embeddingDimensions = options.embeddingDimensions ?? DEFAULT_EMBEDDING_DIMENSIONS;
430
+ }
431
+ /**
432
+ * Enable or disable ai-functions for embeddings
433
+ */
434
+ setUseAiFunctions(enabled) {
435
+ this.useAiFunctions = enabled;
436
+ }
437
+ /**
438
+ * Set a custom embedding provider
439
+ */
440
+ setEmbeddingProvider(provider) {
441
+ if (provider !== undefined) {
442
+ this.embeddingProvider = provider;
443
+ }
444
+ else {
445
+ this.embeddingProvider = undefined;
446
+ }
261
447
  }
262
448
  /**
263
449
  * Set embeddings configuration
@@ -273,210 +459,28 @@ export class MemoryProvider {
273
459
  *
274
460
  * Uses semantic word vectors to create meaningful embeddings
275
461
  * where similar concepts have higher cosine similarity.
462
+ *
463
+ * The embedding process:
464
+ * 1. Tokenize text into words
465
+ * 2. Look up semantic vectors for each word
466
+ * 3. Aggregate word vectors into a base vector
467
+ * 4. Normalize the aggregated vector
468
+ * 5. Expand to full embedding dimensions with deterministic noise
469
+ * 6. Final normalization to unit vector
470
+ *
471
+ * @param text - The text to generate an embedding for
472
+ * @returns A normalized embedding vector
276
473
  */
277
474
  generateEmbedding(text) {
278
- // Import semantic vectors for deterministic embeddings
279
- const SEMANTIC_VECTORS = {
280
- // AI/ML domain
281
- machine: [0.9, 0.1, 0.05, 0.02],
282
- learning: [0.85, 0.15, 0.08, 0.03],
283
- artificial: [0.88, 0.12, 0.06, 0.04],
284
- intelligence: [0.87, 0.13, 0.07, 0.05],
285
- neural: [0.82, 0.18, 0.09, 0.06],
286
- network: [0.75, 0.2, 0.15, 0.1],
287
- deep: [0.8, 0.17, 0.1, 0.08],
288
- ai: [0.92, 0.08, 0.04, 0.02],
289
- ml: [0.88, 0.12, 0.06, 0.03],
290
- // Programming domain
291
- programming: [0.15, 0.85, 0.1, 0.05],
292
- code: [0.12, 0.88, 0.12, 0.06],
293
- software: [0.18, 0.82, 0.15, 0.08],
294
- development: [0.2, 0.8, 0.18, 0.1],
295
- typescript: [0.1, 0.9, 0.08, 0.04],
296
- javascript: [0.12, 0.88, 0.1, 0.05],
297
- python: [0.25, 0.75, 0.12, 0.06],
298
- react: [0.08, 0.85, 0.2, 0.1],
299
- vue: [0.06, 0.84, 0.18, 0.08],
300
- frontend: [0.05, 0.8, 0.25, 0.12],
301
- // Database domain
302
- database: [0.1, 0.7, 0.08, 0.6],
303
- query: [0.12, 0.65, 0.1, 0.7],
304
- sql: [0.08, 0.6, 0.05, 0.75],
305
- index: [0.1, 0.58, 0.08, 0.72],
306
- optimization: [0.15, 0.55, 0.12, 0.68],
307
- performance: [0.18, 0.5, 0.15, 0.65],
308
- // DevOps domain
309
- kubernetes: [0.05, 0.6, 0.8, 0.15],
310
- docker: [0.08, 0.55, 0.82, 0.12],
311
- container: [0.06, 0.5, 0.85, 0.1],
312
- deployment: [0.1, 0.45, 0.78, 0.18],
313
- devops: [0.12, 0.48, 0.75, 0.2],
314
- // Food domain (distinctly different direction - high in dim 3, low elsewhere)
315
- cooking: [0.05, 0.08, 0.05, 0.95],
316
- recipe: [0.06, 0.07, 0.04, 0.93],
317
- food: [0.04, 0.06, 0.04, 0.96],
318
- pasta: [0.03, 0.05, 0.03, 0.97],
319
- pizza: [0.03, 0.06, 0.04, 0.96],
320
- italian: [0.04, 0.07, 0.04, 0.94],
321
- garden: [0.05, 0.04, 0.03, 0.92],
322
- flowers: [0.04, 0.03, 0.03, 0.91],
323
- chef: [0.05, 0.1, 0.05, 0.95],
324
- restaurant: [0.06, 0.08, 0.04, 0.93],
325
- kitchen: [0.05, 0.09, 0.05, 0.94],
326
- antonio: [0.05, 0.08, 0.04, 0.92],
327
- // Research/Academic domain (similar to AI/ML)
328
- researcher: [0.82, 0.2, 0.1, 0.08],
329
- phd: [0.8, 0.18, 0.12, 0.1],
330
- research: [0.85, 0.15, 0.1, 0.07],
331
- professor: [0.78, 0.22, 0.12, 0.1],
332
- academic: [0.75, 0.2, 0.15, 0.12],
333
- // Location/Venue domain (for fuzzy threshold tests - need distinct clusters)
334
- // "conference center downtown" cluster - high values in different dimensions
335
- conference: [0.2, 0.25, 0.85, 0.2],
336
- center: [0.18, 0.22, 0.88, 0.18],
337
- downtown: [0.15, 0.2, 0.9, 0.15],
338
- // "tech hub 123 main st" cluster - completely different direction
339
- hub: [0.85, 0.15, 0.2, 0.15],
340
- main: [0.12, 0.12, 0.15, 0.1],
341
- st: [0.1, 0.1, 0.12, 0.08],
342
- '123': [0.08, 0.08, 0.1, 0.05],
343
- // GraphQL/API
344
- graphql: [0.1, 0.75, 0.15, 0.55],
345
- api: [0.15, 0.7, 0.2, 0.5],
346
- rest: [0.12, 0.68, 0.18, 0.48],
347
- queries: [0.14, 0.65, 0.12, 0.6],
348
- // Testing
349
- testing: [0.1, 0.78, 0.08, 0.15],
350
- test: [0.08, 0.8, 0.06, 0.12],
351
- unit: [0.06, 0.82, 0.05, 0.1],
352
- integration: [0.12, 0.75, 0.1, 0.18],
353
- // State management
354
- state: [0.08, 0.82, 0.2, 0.08],
355
- management: [0.15, 0.75, 0.25, 0.12],
356
- hooks: [0.06, 0.88, 0.15, 0.05],
357
- usestate: [0.05, 0.9, 0.12, 0.04],
358
- useeffect: [0.04, 0.88, 0.1, 0.03],
359
- // Related/Concept domain (for semantic similarity tests)
360
- related: [0.5, 0.5, 0.5, 0.5],
361
- concept: [0.55, 0.45, 0.55, 0.45],
362
- similar: [0.52, 0.48, 0.52, 0.48],
363
- different: [0.48, 0.52, 0.48, 0.52],
364
- words: [0.45, 0.55, 0.45, 0.55],
365
- semantically: [0.6, 0.4, 0.6, 0.4],
366
- // Exact match domain (distinctly different vectors)
367
- exact: [0.1, 0.1, 0.1, 0.9],
368
- match: [0.15, 0.15, 0.1, 0.85],
369
- title: [0.1, 0.2, 0.1, 0.8],
370
- contains: [0.12, 0.18, 0.12, 0.78],
371
- search: [0.08, 0.22, 0.08, 0.82],
372
- terms: [0.05, 0.25, 0.05, 0.85],
373
- // Business domain (for fuzzy forward resolution tests)
374
- enterprise: [0.7, 0.3, 0.8, 0.6],
375
- large: [0.65, 0.25, 0.75, 0.55],
376
- corporations: [0.68, 0.28, 0.78, 0.58],
377
- companies: [0.6, 0.4, 0.7, 0.5],
378
- company: [0.62, 0.38, 0.72, 0.52],
379
- thousands: [0.7, 0.2, 0.7, 0.5],
380
- employees: [0.55, 0.35, 0.65, 0.45],
381
- big: [0.68, 0.3, 0.75, 0.58],
382
- small: [0.3, 0.6, 0.3, 0.4],
383
- business: [0.5, 0.5, 0.6, 0.5],
384
- owners: [0.4, 0.5, 0.5, 0.45],
385
- consumer: [0.35, 0.55, 0.35, 0.35],
386
- individual: [0.32, 0.58, 0.32, 0.32],
387
- b2c: [0.3, 0.6, 0.3, 0.35],
388
- // Tech professional domain
389
- developer: [0.2, 0.85, 0.15, 0.1],
390
- engineer: [0.25, 0.82, 0.18, 0.12],
391
- engineers: [0.27, 0.8, 0.2, 0.14],
392
- builds: [0.18, 0.78, 0.16, 0.08],
393
- writes: [0.15, 0.75, 0.12, 0.06],
394
- professional: [0.22, 0.72, 0.2, 0.15],
395
- applications: [0.2, 0.78, 0.18, 0.1],
396
- tech: [0.25, 0.8, 0.2, 0.12],
397
- technology: [0.28, 0.78, 0.22, 0.14],
398
- electronics: [0.3, 0.75, 0.25, 0.15],
399
- device: [0.25, 0.82, 0.2, 0.1],
400
- furniture: [0.1, 0.15, 0.2, 0.85],
401
- home: [0.12, 0.18, 0.22, 0.8],
402
- living: [0.1, 0.15, 0.2, 0.82],
403
- goods: [0.3, 0.5, 0.35, 0.4],
404
- leaders: [0.4, 0.5, 0.6, 0.4],
405
- senior: [0.35, 0.55, 0.55, 0.35],
406
- // Data science domain
407
- data: [0.75, 0.3, 0.15, 0.55],
408
- science: [0.78, 0.25, 0.12, 0.5],
409
- scientist: [0.8, 0.28, 0.1, 0.52],
410
- background: [0.72, 0.32, 0.14, 0.48],
411
- // DevOps/cloud domain
412
- cloud: [0.1, 0.55, 0.85, 0.15],
413
- expertise: [0.15, 0.5, 0.8, 0.18],
414
- // Support domain
415
- support: [0.2, 0.45, 0.3, 0.55],
416
- specialist: [0.22, 0.48, 0.32, 0.52],
417
- technical: [0.25, 0.65, 0.35, 0.4],
418
- issues: [0.18, 0.42, 0.28, 0.48],
419
- // Security domain
420
- security: [0.3, 0.6, 0.4, 0.7],
421
- auth: [0.28, 0.58, 0.38, 0.72],
422
- authentication: [0.32, 0.55, 0.42, 0.75],
423
- identity: [0.35, 0.52, 0.45, 0.68],
424
- oauth: [0.3, 0.62, 0.4, 0.7],
425
- // CRM domain
426
- crm: [0.45, 0.4, 0.7, 0.55],
427
- sales: [0.42, 0.38, 0.68, 0.52],
428
- salesforce: [0.48, 0.42, 0.72, 0.58],
429
- provider: [0.5, 0.45, 0.65, 0.5],
430
- };
431
- const DEFAULT_VECTOR = [0.1, 0.1, 0.1, 0.1];
432
- // Simple hash function
433
- const simpleHash = (str) => {
434
- let hash = 0;
435
- for (let i = 0; i < str.length; i++) {
436
- const char = str.charCodeAt(i);
437
- hash = ((hash << 5) - hash) + char;
438
- hash = hash & hash;
439
- }
440
- return Math.abs(hash);
441
- };
442
- // Seeded random
443
- const seededRandom = (seed, index) => {
444
- const x = Math.sin(seed + index) * 10000;
445
- return x - Math.floor(x);
446
- };
447
- // Tokenize
448
- const words = text
449
- .toLowerCase()
450
- .replace(/[^\w\s]/g, ' ')
451
- .split(/\s+/)
452
- .filter(w => w.length > 0);
475
+ const words = tokenizeText(text);
453
476
  if (words.length === 0) {
454
- return Array.from({ length: EMBEDDING_DIMENSIONS }, (_, i) => seededRandom(0, i) * 0.01);
455
- }
456
- // Aggregate word vectors
457
- const aggregated = [0, 0, 0, 0];
458
- for (const word of words) {
459
- const lower = word.toLowerCase();
460
- const vec = SEMANTIC_VECTORS[lower] ?? DEFAULT_VECTOR.map((v, i) => v + seededRandom(simpleHash(lower), i) * 0.1);
461
- for (let i = 0; i < 4; i++) {
462
- aggregated[i] += vec[i];
463
- }
477
+ return generateEmptyEmbedding(this.embeddingDimensions);
464
478
  }
465
- // Normalize
466
- const norm = Math.sqrt(aggregated.reduce((sum, v) => sum + v * v, 0));
467
- const normalized = aggregated.map(v => v / (norm || 1));
468
- // Expand to full dimensions
479
+ const aggregated = aggregateWordVectors(words);
480
+ const normalized = normalizeVector(aggregated);
469
481
  const textHash = simpleHash(text);
470
- const embedding = new Array(EMBEDDING_DIMENSIONS);
471
- for (let i = 0; i < EMBEDDING_DIMENSIONS; i++) {
472
- const baseIndex = i % 4;
473
- const base = normalized[baseIndex];
474
- const noise = seededRandom(textHash, i) * 0.1 - 0.05;
475
- embedding[i] = base + noise;
476
- }
477
- // Final normalization
478
- const finalNorm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
479
- return embedding.map((v) => v / (finalNorm || 1));
482
+ const expanded = expandToFullDimensions(normalized, this.embeddingDimensions, textHash);
483
+ return normalizeVector(expanded);
480
484
  }
481
485
  /**
482
486
  * Check if embeddings should be generated for a given entity type
@@ -509,6 +513,11 @@ export class MemoryProvider {
509
513
  * embeddings for entities based on their text content. The embedding
510
514
  * is stored as an artifact associated with the entity.
511
515
  *
516
+ * Priority for embedding generation:
517
+ * 1. Custom embeddingProvider if set (for testing or alternative services)
518
+ * 2. ai-functions if useAiFunctions is enabled
519
+ * 3. Deterministic mock embedding (default for testing)
520
+ *
512
521
  * @param type - The entity type name
513
522
  * @param id - The entity ID
514
523
  * @param data - The entity data to extract text from
@@ -523,8 +532,39 @@ export class MemoryProvider {
523
532
  const { text, fields: embeddedFields } = extractEmbeddableText(data, fields);
524
533
  if (!text.trim())
525
534
  return;
526
- // Generate embedding
527
- const embedding = this.generateEmbedding(text);
535
+ let embedding;
536
+ let dimensions = this.embeddingDimensions;
537
+ let source = 'mock';
538
+ // Priority: embeddingProvider > useAiFunctions > mock
539
+ if (this.embeddingProvider) {
540
+ try {
541
+ const result = await this.embeddingProvider.embedTexts([text]);
542
+ embedding = result.embeddings[0] ?? this.generateEmbedding(text);
543
+ dimensions = embedding.length;
544
+ source = 'custom-provider';
545
+ }
546
+ catch (err) {
547
+ logWarn('Custom embedding provider failed, falling back to mock:', err);
548
+ embedding = this.generateEmbedding(text);
549
+ }
550
+ }
551
+ else if (this.useAiFunctions) {
552
+ try {
553
+ const { embedTexts } = await import('ai-functions');
554
+ const result = await embedTexts([text]);
555
+ embedding = result.embeddings[0] ?? this.generateEmbedding(text);
556
+ dimensions = embedding.length;
557
+ source = 'ai-functions';
558
+ }
559
+ catch (err) {
560
+ // Fallback to mock embedding if ai-functions fails
561
+ logWarn('ai-functions embedTexts failed, falling back to mock:', err);
562
+ embedding = this.generateEmbedding(text);
563
+ }
564
+ }
565
+ else {
566
+ embedding = this.generateEmbedding(text);
567
+ }
528
568
  const contentHash = generateContentHash(text);
529
569
  // Store as artifact with complete metadata
530
570
  const url = `${type}/${id}`;
@@ -533,8 +573,9 @@ export class MemoryProvider {
533
573
  sourceHash: contentHash,
534
574
  metadata: {
535
575
  fields: embeddedFields,
536
- dimensions: EMBEDDING_DIMENSIONS,
576
+ dimensions,
537
577
  text: text.slice(0, 200),
578
+ source,
538
579
  },
539
580
  });
540
581
  }
@@ -559,11 +600,15 @@ export class MemoryProvider {
559
600
  return this.entities.get(type);
560
601
  }
561
602
  async get(type, id) {
603
+ validateTypeName(type);
604
+ validateEntityId(id);
562
605
  const store = this.getTypeStore(type);
563
606
  const entity = store.get(id);
564
607
  return entity ? { ...entity, $id: id, $type: type } : null;
565
608
  }
566
609
  async list(type, options) {
610
+ validateTypeName(type);
611
+ validateListOptions(options);
567
612
  const store = this.getTypeStore(type);
568
613
  let results = [];
569
614
  for (const [id, entity] of store) {
@@ -612,9 +657,18 @@ export class MemoryProvider {
612
657
  return results;
613
658
  }
614
659
  async search(type, query, options) {
660
+ validateTypeName(type);
661
+ validateSearchQuery(query);
662
+ validateSearchOptions(options);
615
663
  const all = await this.list(type, options);
616
664
  const queryLower = query.toLowerCase();
617
- const fields = options?.fields || ['$all'];
665
+ let fields = options?.fields || ['$all'];
666
+ // Filter out dangerous field names
667
+ fields = fields.filter((f) => !isDangerousField(f));
668
+ // If all fields were dangerous, return empty results
669
+ if (fields.length === 0) {
670
+ return [];
671
+ }
618
672
  const scored = [];
619
673
  for (const entity of all) {
620
674
  let searchText;
@@ -640,28 +694,111 @@ export class MemoryProvider {
640
694
  }
641
695
  /**
642
696
  * Semantic search using embedding similarity
697
+ *
698
+ * Priority for embedding and similarity operations:
699
+ * 1. Custom embeddingProvider if set
700
+ * 2. ai-functions if useAiFunctions is enabled
701
+ * 3. Local mock implementations (default)
643
702
  */
644
703
  async semanticSearch(type, query, options) {
645
704
  const store = this.getTypeStore(type);
646
705
  const limit = options?.limit ?? 10;
647
706
  const minScore = options?.minScore ?? 0;
648
707
  // Generate query embedding
649
- const queryEmbedding = this.generateEmbedding(query);
650
- const scored = [];
708
+ let queryEmbedding;
709
+ if (this.embeddingProvider) {
710
+ try {
711
+ const result = await this.embeddingProvider.embedTexts([query]);
712
+ queryEmbedding = result.embeddings[0] ?? this.generateEmbedding(query);
713
+ }
714
+ catch (err) {
715
+ logWarn('Custom embedding provider failed for query, falling back to mock:', err);
716
+ queryEmbedding = this.generateEmbedding(query);
717
+ }
718
+ }
719
+ else if (this.useAiFunctions) {
720
+ try {
721
+ const { embedTexts } = await import('ai-functions');
722
+ const result = await embedTexts([query]);
723
+ queryEmbedding = result.embeddings[0] ?? this.generateEmbedding(query);
724
+ }
725
+ catch (err) {
726
+ logWarn('ai-functions embedTexts failed for query, falling back to mock:', err);
727
+ queryEmbedding = this.generateEmbedding(query);
728
+ }
729
+ }
730
+ else {
731
+ queryEmbedding = this.generateEmbedding(query);
732
+ }
733
+ // Get similarity function
734
+ let similarityFn;
735
+ if (this.embeddingProvider?.cosineSimilarity) {
736
+ similarityFn = this.embeddingProvider.cosineSimilarity;
737
+ }
738
+ else if (this.useAiFunctions) {
739
+ try {
740
+ const { cosineSimilarity: aiCosineSimilarity } = await import('ai-functions');
741
+ similarityFn = aiCosineSimilarity;
742
+ }
743
+ catch (err) {
744
+ logWarn('ai-functions cosineSimilarity not available, using local:', err);
745
+ similarityFn = cosineSimilarity;
746
+ }
747
+ }
748
+ else {
749
+ similarityFn = cosineSimilarity;
750
+ }
751
+ // Collect embeddings and entities for potential findSimilar usage
752
+ const embeddings = [];
753
+ const entities = [];
651
754
  for (const [id, entity] of store) {
652
- // Get stored embedding from artifacts
653
755
  const url = `${type}/${id}`;
654
756
  const artifact = await this.getArtifact(url, 'embedding');
655
757
  if (!artifact || !Array.isArray(artifact.content)) {
656
758
  continue;
657
759
  }
658
- const embedding = artifact.content;
659
- const score = cosineSimilarity(queryEmbedding, embedding);
660
- if (score >= minScore) {
661
- scored.push({
662
- entity: { ...entity, $id: id, $type: type },
663
- score,
760
+ embeddings.push(artifact.content);
761
+ entities.push({ entity: { ...entity, $id: id, $type: type }, id });
762
+ }
763
+ // If using embeddingProvider with findSimilar, use it
764
+ if (this.embeddingProvider?.findSimilar && entities.length > 0) {
765
+ try {
766
+ const results = this.embeddingProvider.findSimilar(queryEmbedding, embeddings, entities, {
767
+ topK: limit,
768
+ minScore,
664
769
  });
770
+ return results.map(({ item, score }) => ({
771
+ ...item.entity,
772
+ $score: score,
773
+ }));
774
+ }
775
+ catch (err) {
776
+ logWarn('Custom embedding provider findSimilar failed, falling back to manual scoring:', err);
777
+ }
778
+ }
779
+ // If using ai-functions and we have entities, try to use findSimilar
780
+ if (this.useAiFunctions && entities.length > 0) {
781
+ try {
782
+ const { findSimilar } = await import('ai-functions');
783
+ const results = findSimilar(queryEmbedding, embeddings, entities, { topK: limit, minScore });
784
+ return results.map(({ item, score }) => ({
785
+ ...item.entity,
786
+ $score: score,
787
+ }));
788
+ }
789
+ catch (err) {
790
+ // Fall through to manual scoring if findSimilar fails
791
+ logWarn('ai-functions findSimilar failed, falling back to manual scoring:', err);
792
+ }
793
+ }
794
+ // Manual scoring fallback
795
+ const scored = [];
796
+ for (let i = 0; i < entities.length; i++) {
797
+ const embedding = embeddings[i];
798
+ const { entity } = entities[i];
799
+ const score = similarityFn(queryEmbedding, embedding);
800
+ if (score >= minScore) {
801
+ scored.push({ entity, score });
665
802
  }
666
803
  }
667
804
  // Sort by score descending
@@ -686,15 +823,18 @@ export class MemoryProvider {
686
823
  const ftsResults = await this.search(type, query);
687
824
  const ftsRanks = new Map();
688
825
  ftsResults.forEach((entity, index) => {
689
- const id = entity.$id || entity.id;
826
+ const id = entity['$id'] || entity['id'];
690
827
  ftsRanks.set(id, index + 1); // 1-indexed rank
691
828
  });
692
829
  // Get semantic results with their ranks and scores
693
830
  // Get more results to ensure we have enough after offset
694
- const semanticResults = await this.semanticSearch(type, query, { limit: (limit + offset) * 2, minScore });
831
+ const semanticResults = await this.semanticSearch(type, query, {
832
+ limit: (limit + offset) * 2,
833
+ minScore,
834
+ });
695
835
  const semanticRanks = new Map();
696
836
  semanticResults.forEach((entity, index) => {
697
- const id = entity.$id || entity.id;
837
+ const id = entity['$id'] || entity['id'];
698
838
  semanticRanks.set(id, { rank: index + 1, score: entity.$score });
699
839
  });
700
840
  // Combine results with RRF
@@ -724,7 +864,9 @@ export class MemoryProvider {
724
864
  // Sort by RRF score descending
725
865
  combined.sort((a, b) => b.rrfScore - a.rrfScore);
726
866
  // Apply offset and limit, then return with scoring fields
727
- return combined.slice(offset, offset + limit).map(({ entity, rrfScore, ftsRank, semanticRank, semanticScore }) => ({
867
+ return combined
868
+ .slice(offset, offset + limit)
869
+ .map(({ entity, rrfScore, ftsRank, semanticRank, semanticScore }) => ({
728
870
  ...entity,
729
871
  $rrfScore: rrfScore,
730
872
  $ftsRank: ftsRank,
@@ -751,10 +893,15 @@ export class MemoryProvider {
751
893
  return results;
752
894
  }
753
895
  async create(type, id, data) {
896
+ validateTypeName(type);
897
+ if (id !== undefined) {
898
+ validateEntityId(id);
899
+ }
900
+ validateEntityData(data);
754
901
  const store = this.getTypeStore(type);
755
902
  const entityId = id || generateId();
756
903
  if (store.has(entityId)) {
757
- throw new Error(`Entity already exists: ${type}/${entityId}`);
904
+ throw new EntityAlreadyExistsError(type, entityId, 'create');
758
905
  }
759
906
  const entity = {
760
907
  ...data,
@@ -766,15 +913,26 @@ export class MemoryProvider {
766
913
  await this.autoEmbed(type, entityId, entity);
767
914
  // Emit type-specific and global events
768
915
  const eventData = { $id: entityId, $type: type, ...entity };
769
- await this.emit(`${type}.created`, eventData);
770
- await this.emit('entity:created', eventData);
916
+ await this.emit({
917
+ event: `${type}.created`,
918
+ object: `${type}/${entityId}`,
919
+ objectData: eventData,
920
+ });
921
+ await this.emit({
922
+ event: 'entity:created',
923
+ object: `${type}/${entityId}`,
924
+ objectData: eventData,
925
+ });
771
926
  return { ...entity, $id: entityId, $type: type };
772
927
  }
773
928
  async update(type, id, data) {
929
+ validateTypeName(type);
930
+ validateEntityId(id);
931
+ validateEntityData(data);
774
932
  const store = this.getTypeStore(type);
775
933
  const existing = store.get(id);
776
934
  if (!existing) {
777
- throw new Error(`Entity not found: ${type}/${id}`);
935
+ throw new EntityNotFoundError(type, id, 'update');
778
936
  }
779
937
  const updated = {
780
938
  ...existing,
@@ -788,11 +946,21 @@ export class MemoryProvider {
788
946
  await this.invalidateArtifacts(`${type}/${id}`);
789
947
  // Emit type-specific and global events
790
948
  const eventData = { $id: id, $type: type, ...updated };
791
- await this.emit(`${type}.updated`, eventData);
792
- await this.emit('entity:updated', eventData);
949
+ await this.emit({
950
+ event: `${type}.updated`,
951
+ object: `${type}/${id}`,
952
+ objectData: eventData,
953
+ });
954
+ await this.emit({
955
+ event: 'entity:updated',
956
+ object: `${type}/${id}`,
957
+ objectData: eventData,
958
+ });
793
959
  return { ...updated, $id: id, $type: type };
794
960
  }
795
961
  async delete(type, id) {
962
+ validateTypeName(type);
963
+ validateEntityId(id);
796
964
  const store = this.getTypeStore(type);
797
965
  if (!store.has(id)) {
798
966
  return false;
@@ -800,8 +968,16 @@ export class MemoryProvider {
800
968
  store.delete(id);
801
969
  // Emit type-specific and global events
802
970
  const eventData = { $id: id, $type: type };
803
- await this.emit(`${type}.deleted`, eventData);
804
- await this.emit('entity:deleted', eventData);
971
+ await this.emit({
972
+ event: `${type}.deleted`,
973
+ object: `${type}/${id}`,
974
+ objectData: eventData,
975
+ });
976
+ await this.emit({
977
+ event: 'entity:deleted',
978
+ object: `${type}/${id}`,
979
+ objectData: eventData,
980
+ });
805
981
  // Clean up relations
806
982
  for (const [key, targets] of this.relations) {
807
983
  if (key.startsWith(`${type}:${id}:`)) {
@@ -848,6 +1024,11 @@ export class MemoryProvider {
848
1024
  return results;
849
1025
  }
850
1026
  async relate(fromType, fromId, relation, toType, toId, metadata) {
1027
+ validateTypeName(fromType);
1028
+ validateEntityId(fromId);
1029
+ validateRelationName(relation);
1030
+ validateTypeName(toType);
1031
+ validateEntityId(toId);
851
1032
  const key = this.relationKey(fromType, fromId, relation);
852
1033
  if (!this.relations.has(key)) {
853
1034
  this.relations.set(key, new Set());
@@ -903,11 +1084,11 @@ export class MemoryProvider {
903
1084
  id: generateId(),
904
1085
  actor: 'system',
905
1086
  event: eventOrType,
906
- objectData: data,
907
1087
  timestamp: new Date(),
908
1088
  // Legacy fields
909
1089
  type: eventOrType,
910
1090
  data,
1091
+ ...(data !== undefined && { objectData: data }),
911
1092
  };
912
1093
  }
913
1094
  else {
@@ -915,16 +1096,17 @@ export class MemoryProvider {
915
1096
  event = {
916
1097
  id: generateId(),
917
1098
  actor: eventOrType.actor ?? 'system',
918
- actorData: eventOrType.actorData,
919
1099
  event: eventOrType.event,
920
- object: eventOrType.object,
921
- objectData: eventOrType.objectData,
922
- result: eventOrType.result,
923
- resultData: eventOrType.resultData,
924
- meta: eventOrType.meta,
925
1100
  timestamp: new Date(),
926
- // Legacy fields
1101
+ // Legacy fields for backward compatibility
927
1102
  type: eventOrType.event,
1103
+ ...(eventOrType.objectData !== undefined && { data: eventOrType.objectData }),
1104
+ ...(eventOrType.actorData !== undefined && { actorData: eventOrType.actorData }),
1105
+ ...(eventOrType.object !== undefined && { object: eventOrType.object }),
1106
+ ...(eventOrType.objectData !== undefined && { objectData: eventOrType.objectData }),
1107
+ ...(eventOrType.result !== undefined && { result: eventOrType.result }),
1108
+ ...(eventOrType.resultData !== undefined && { resultData: eventOrType.resultData }),
1109
+ ...(eventOrType.meta !== undefined && { meta: eventOrType.meta }),
928
1110
  };
929
1111
  }
930
1112
  this.events.push(event);
@@ -947,9 +1129,9 @@ export class MemoryProvider {
947
1129
  */
948
1130
  getEventHandlers(type) {
949
1131
  const handlers = [];
950
- for (const [pattern, patternHandlers] of this.eventHandlers) {
1132
+ for (const [pattern, patternHandlers] of [...this.eventHandlers]) {
951
1133
  if (this.matchesPattern(type, pattern)) {
952
- handlers.push(...patternHandlers);
1134
+ handlers.push(...[...patternHandlers]);
953
1135
  }
954
1136
  }
955
1137
  return handlers;
@@ -985,6 +1167,7 @@ export class MemoryProvider {
985
1167
  return false;
986
1168
  }
987
1169
  on(pattern, handler) {
1170
+ validateEventPattern(pattern);
988
1171
  if (!this.eventHandlers.has(pattern)) {
989
1172
  this.eventHandlers.set(pattern, []);
990
1173
  }
@@ -1024,10 +1207,11 @@ export class MemoryProvider {
1024
1207
  return results;
1025
1208
  }
1026
1209
  async replayEvents(options) {
1210
+ const eventPattern = options.event ?? options.type;
1027
1211
  const events = await this.listEvents({
1028
- event: options.event ?? options.type,
1029
- actor: options.actor,
1030
- since: options.since,
1212
+ ...(eventPattern !== undefined && { event: eventPattern }),
1213
+ ...(options.actor !== undefined && { actor: options.actor }),
1214
+ ...(options.since !== undefined && { since: options.since }),
1031
1215
  });
1032
1216
  for (const event of events) {
1033
1217
  await this.semaphore.run(() => Promise.resolve(options.handler(event)));
@@ -1061,33 +1245,39 @@ export class MemoryProvider {
1061
1245
  async createAction(data) {
1062
1246
  // Get base verb from action or legacy type
1063
1247
  const baseVerb = data.action ?? data.type ?? 'process';
1248
+ // Validate action type
1249
+ validateActionType(baseVerb);
1064
1250
  // Auto-conjugate verb forms
1065
1251
  const conjugated = conjugateVerb(baseVerb);
1252
+ const objectData = data.objectData ?? data.data;
1066
1253
  const action = {
1067
1254
  id: generateId(),
1068
1255
  actor: data.actor ?? 'system',
1069
- actorData: data.actorData,
1070
1256
  act: conjugated.act,
1071
1257
  action: conjugated.action,
1072
1258
  activity: conjugated.activity,
1073
- object: data.object,
1074
- objectData: data.objectData ?? data.data,
1075
1259
  status: 'pending',
1076
1260
  progress: 0,
1077
- total: data.total,
1078
- meta: data.meta,
1079
1261
  createdAt: new Date(),
1080
1262
  // Legacy fields
1081
1263
  type: baseVerb,
1082
1264
  data: data.data,
1265
+ ...(data.actorData !== undefined && { actorData: data.actorData }),
1266
+ ...(data.object !== undefined && { object: data.object }),
1267
+ ...(objectData !== undefined && { objectData }),
1268
+ ...(data.total !== undefined && { total: data.total }),
1269
+ ...(data.meta !== undefined && { meta: data.meta }),
1083
1270
  };
1084
1271
  this.actions.set(action.id, action);
1085
1272
  await this.emit({
1086
1273
  actor: action.actor,
1087
- actorData: action.actorData,
1088
1274
  event: 'Action.created',
1089
1275
  object: action.id,
1090
- objectData: { action: action.action, object: action.object },
1276
+ objectData: {
1277
+ action: action.action,
1278
+ ...(action.object !== undefined && { object: action.object }),
1279
+ },
1280
+ ...(action.actorData !== undefined && { actorData: action.actorData }),
1091
1281
  });
1092
1282
  return action;
1093
1283
  }
@@ -1097,7 +1287,7 @@ export class MemoryProvider {
1097
1287
  async updateAction(id, updates) {
1098
1288
  const action = this.actions.get(id);
1099
1289
  if (!action) {
1100
- throw new Error(`Action not found: ${id}`);
1290
+ throw new EntityNotFoundError('Action', id, 'updateAction');
1101
1291
  }
1102
1292
  Object.assign(action, updates);
1103
1293
  if (updates.status === 'active' && !action.startedAt) {
@@ -1116,8 +1306,8 @@ export class MemoryProvider {
1116
1306
  event: 'Action.completed',
1117
1307
  object: action.id,
1118
1308
  objectData: { action: action.action },
1119
- result: action.object,
1120
- resultData: action.result,
1309
+ ...(action.object !== undefined && { result: action.object }),
1310
+ ...(action.result !== undefined && { resultData: action.result }),
1121
1311
  });
1122
1312
  }
1123
1313
  if (updates.status === 'failed') {
@@ -1170,15 +1360,15 @@ export class MemoryProvider {
1170
1360
  async retryAction(id) {
1171
1361
  const action = this.actions.get(id);
1172
1362
  if (!action) {
1173
- throw new Error(`Action not found: ${id}`);
1363
+ throw new EntityNotFoundError('Action', id, 'retryAction');
1174
1364
  }
1175
1365
  if (action.status !== 'failed') {
1176
1366
  throw new Error(`Can only retry failed actions: ${id}`);
1177
1367
  }
1178
1368
  action.status = 'pending';
1179
- action.error = undefined;
1180
- action.startedAt = undefined;
1181
- action.completedAt = undefined;
1369
+ delete action.error;
1370
+ delete action.startedAt;
1371
+ delete action.completedAt;
1182
1372
  await this.emit({
1183
1373
  actor: action.actor,
1184
1374
  event: 'Action.retried',
@@ -1190,9 +1380,11 @@ export class MemoryProvider {
1190
1380
  async cancelAction(id) {
1191
1381
  const action = this.actions.get(id);
1192
1382
  if (!action) {
1193
- throw new Error(`Action not found: ${id}`);
1383
+ throw new EntityNotFoundError('Action', id, 'cancelAction');
1194
1384
  }
1195
- if (action.status === 'completed' || action.status === 'failed' || action.status === 'cancelled') {
1385
+ if (action.status === 'completed' ||
1386
+ action.status === 'failed' ||
1387
+ action.status === 'cancelled') {
1196
1388
  throw new Error(`Cannot cancel finished action: ${id}`);
1197
1389
  }
1198
1390
  action.status = 'cancelled';
@@ -1226,13 +1418,14 @@ export class MemoryProvider {
1226
1418
  return this.artifacts.get(this.artifactKey(url, type)) ?? null;
1227
1419
  }
1228
1420
  async setArtifact(url, type, data) {
1421
+ validateArtifactUrl(url);
1229
1422
  const artifact = {
1230
1423
  url,
1231
1424
  type,
1232
1425
  sourceHash: data.sourceHash,
1233
1426
  content: data.content,
1234
- metadata: data.metadata,
1235
1427
  createdAt: new Date(),
1428
+ ...(data.metadata !== undefined && { metadata: data.metadata }),
1236
1429
  };
1237
1430
  this.artifacts.set(this.artifactKey(url, type), artifact);
1238
1431
  }
@@ -1292,6 +1485,19 @@ export class MemoryProvider {
1292
1485
  async mapWithConcurrency(items, fn) {
1293
1486
  return this.semaphore.map(items, fn);
1294
1487
  }
1488
+ // ===========================================================================
1489
+ // Transactions
1490
+ // ===========================================================================
1491
+ /**
1492
+ * Begin a new transaction.
1493
+ *
1494
+ * All writes (create, update, delete, relate) are buffered in memory.
1495
+ * On commit(), they are applied to the provider atomically.
1496
+ * On rollback(), all buffered writes are discarded.
1497
+ */
1498
+ async beginTransaction() {
1499
+ return new MemoryTransaction(this);
1500
+ }
1295
1501
  /**
1296
1502
  * Clear all data (useful for testing)
1297
1503
  */
@@ -1332,6 +1538,135 @@ export class MemoryProvider {
1332
1538
  };
1333
1539
  }
1334
1540
  }
1541
+ /**
1542
+ * In-memory transaction that buffers writes and applies them on commit.
1543
+ *
1544
+ * - get() checks the write buffer first, then falls through to the provider.
1545
+ * - create/update/delete/relate are buffered.
1546
+ * - commit() replays all buffered operations against the real provider.
1547
+ * - rollback() discards the buffer.
1548
+ */
1549
+ export class MemoryTransaction {
1550
+ provider;
1551
+ ops = [];
1552
+ committed = false;
1553
+ rolledBack = false;
1554
+ /** Buffered creates/updates: type -> id -> data */
1555
+ buffer = new Map();
1556
+ /** Buffered deletes: type -> Set<id> */
1557
+ deletions = new Map();
1558
+ /** Counter for generating temporary IDs */
1559
+ tempIdCounter = 0;
1560
+ constructor(provider) {
1561
+ this.provider = provider;
1562
+ }
1563
+ assertActive() {
1564
+ if (this.committed)
1565
+ throw new Error('Transaction already committed');
1566
+ if (this.rolledBack)
1567
+ throw new Error('Transaction already rolled back');
1568
+ }
1569
+ getBuffer(type) {
1570
+ if (!this.buffer.has(type)) {
1571
+ this.buffer.set(type, new Map());
1572
+ }
1573
+ return this.buffer.get(type);
1574
+ }
1575
+ async get(type, id) {
1576
+ this.assertActive();
1577
+ // Check if deleted in this transaction
1578
+ if (this.deletions.get(type)?.has(id))
1579
+ return null;
1580
+ // Check buffer first
1581
+ const buf = this.buffer.get(type);
1582
+ if (buf?.has(id)) {
1583
+ return { ...buf.get(id), $id: id, $type: type };
1584
+ }
1585
+ // Fall through to provider
1586
+ return this.provider.get(type, id);
1587
+ }
1588
+ async create(type, id, data) {
1589
+ this.assertActive();
1590
+ const entityId = id || `txn-temp-${++this.tempIdCounter}`;
1591
+ const entity = {
1592
+ ...data,
1593
+ createdAt: new Date().toISOString(),
1594
+ updatedAt: new Date().toISOString(),
1595
+ };
1596
+ this.getBuffer(type).set(entityId, entity);
1597
+ const result = { ...entity, $id: entityId, $type: type };
1598
+ this.ops.push({ kind: 'create', type, id: entityId, data, result });
1599
+ return result;
1600
+ }
1601
+ async update(type, id, data) {
1602
+ this.assertActive();
1603
+ // Get current state (from buffer or provider)
1604
+ const existing = await this.get(type, id);
1605
+ if (!existing)
1606
+ throw new Error(`update ${type}/${id}: Entity not found`);
1607
+ const { $id: _id, $type: _type, ...rest } = existing;
1608
+ const updated = { ...rest, ...data, updatedAt: new Date().toISOString() };
1609
+ this.getBuffer(type).set(id, updated);
1610
+ const result = { ...updated, $id: id, $type: type };
1611
+ this.ops.push({ kind: 'update', type, id, data, result });
1612
+ return result;
1613
+ }
1614
+ async delete(type, id) {
1615
+ this.assertActive();
1616
+ // Check existence
1617
+ const existing = await this.get(type, id);
1618
+ if (!existing)
1619
+ return false;
1620
+ // Remove from buffer if present
1621
+ this.buffer.get(type)?.delete(id);
1622
+ // Mark as deleted
1623
+ if (!this.deletions.has(type))
1624
+ this.deletions.set(type, new Set());
1625
+ this.deletions.get(type).add(id);
1626
+ this.ops.push({ kind: 'delete', type, id });
1627
+ return true;
1628
+ }
1629
+ async relate(fromType, fromId, relation, toType, toId, metadata) {
1630
+ this.assertActive();
1631
+ this.ops.push({
1632
+ kind: 'relate',
1633
+ fromType,
1634
+ fromId,
1635
+ relation,
1636
+ toType,
1637
+ toId,
1638
+ ...(metadata != null ? { metadata } : {}),
1639
+ });
1640
+ }
1641
+ async commit() {
1642
+ this.assertActive();
1643
+ this.committed = true;
1644
+ // Replay all operations against the real provider
1645
+ for (const op of this.ops) {
1646
+ switch (op.kind) {
1647
+ case 'create':
1648
+ await this.provider.create(op.type, op.id, op.data);
1649
+ break;
1650
+ case 'update':
1651
+ await this.provider.update(op.type, op.id, op.data);
1652
+ break;
1653
+ case 'delete':
1654
+ await this.provider.delete(op.type, op.id);
1655
+ break;
1656
+ case 'relate':
1657
+ await this.provider.relate(op.fromType, op.fromId, op.relation, op.toType, op.toId, op.metadata);
1658
+ break;
1659
+ }
1660
+ }
1661
+ }
1662
+ async rollback() {
1663
+ this.assertActive();
1664
+ this.rolledBack = true;
1665
+ this.ops = [];
1666
+ this.buffer.clear();
1667
+ this.deletions.clear();
1668
+ }
1669
+ }
1335
1670
  /**
1336
1671
  * Create an in-memory provider
1337
1672
  */