@agentuity/local 3.0.0-alpha.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 (56) hide show
  1. package/dist/bun/db.d.ts +4 -0
  2. package/dist/bun/db.d.ts.map +1 -0
  3. package/dist/bun/db.js +281 -0
  4. package/dist/bun/db.js.map +1 -0
  5. package/dist/bun/email.d.ts +24 -0
  6. package/dist/bun/email.d.ts.map +1 -0
  7. package/dist/bun/email.js +58 -0
  8. package/dist/bun/email.js.map +1 -0
  9. package/dist/bun/index.d.ts +14 -0
  10. package/dist/bun/index.d.ts.map +1 -0
  11. package/dist/bun/index.js +14 -0
  12. package/dist/bun/index.js.map +1 -0
  13. package/dist/bun/kv.d.ts +17 -0
  14. package/dist/bun/kv.d.ts.map +1 -0
  15. package/dist/bun/kv.js +133 -0
  16. package/dist/bun/kv.js.map +1 -0
  17. package/dist/bun/queue.d.ts +10 -0
  18. package/dist/bun/queue.d.ts.map +1 -0
  19. package/dist/bun/queue.js +96 -0
  20. package/dist/bun/queue.js.map +1 -0
  21. package/dist/bun/stream.d.ts +12 -0
  22. package/dist/bun/stream.d.ts.map +1 -0
  23. package/dist/bun/stream.js +266 -0
  24. package/dist/bun/stream.js.map +1 -0
  25. package/dist/bun/task.d.ts +55 -0
  26. package/dist/bun/task.d.ts.map +1 -0
  27. package/dist/bun/task.js +1248 -0
  28. package/dist/bun/task.js.map +1 -0
  29. package/dist/bun/util.d.ts +18 -0
  30. package/dist/bun/util.d.ts.map +1 -0
  31. package/dist/bun/util.js +44 -0
  32. package/dist/bun/util.js.map +1 -0
  33. package/dist/bun/vector.d.ts +17 -0
  34. package/dist/bun/vector.d.ts.map +1 -0
  35. package/dist/bun/vector.js +303 -0
  36. package/dist/bun/vector.js.map +1 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +14 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/runtime.d.ts +20 -0
  42. package/dist/runtime.d.ts.map +1 -0
  43. package/dist/runtime.js +44 -0
  44. package/dist/runtime.js.map +1 -0
  45. package/package.json +42 -0
  46. package/src/bun/db.ts +353 -0
  47. package/src/bun/email.ts +91 -0
  48. package/src/bun/index.ts +14 -0
  49. package/src/bun/kv.ts +174 -0
  50. package/src/bun/queue.ts +145 -0
  51. package/src/bun/stream.ts +358 -0
  52. package/src/bun/task.ts +1711 -0
  53. package/src/bun/util.ts +55 -0
  54. package/src/bun/vector.ts +438 -0
  55. package/src/index.ts +36 -0
  56. package/src/runtime.ts +56 -0
@@ -0,0 +1,55 @@
1
+ import { StructuredError } from '@agentuity/core';
2
+ import { resolve } from 'node:path';
3
+
4
+ /**
5
+ * Normalize a project path to an absolute path for consistent DB keys
6
+ */
7
+ export function normalizeProjectPath(cwd: string = process.cwd()): string {
8
+ return resolve(cwd);
9
+ }
10
+
11
+ /**
12
+ * Simple character-based embedding for local vector search
13
+ * Not production-quality, but good enough for local dev/testing
14
+ */
15
+ export function simpleEmbedding(text: string, dimensions = 128): number[] {
16
+ const vec = new Array(dimensions).fill(0);
17
+ const normalized = text.toLowerCase();
18
+
19
+ for (let i = 0; i < normalized.length; i++) {
20
+ const charCode = normalized.charCodeAt(i);
21
+ vec[i % dimensions] += Math.sin(charCode * (i + 1));
22
+ vec[(i * 2) % dimensions] += Math.cos(charCode);
23
+ }
24
+
25
+ // Normalize vector
26
+ const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
27
+ return magnitude > 0 ? vec.map((v) => v / magnitude) : vec;
28
+ }
29
+
30
+ const InvalidVectorError = StructuredError(
31
+ 'InvalidVectorError',
32
+ 'Vectors must have the same dimension'
33
+ );
34
+
35
+ /**
36
+ * Calculate cosine similarity between two vectors
37
+ */
38
+ export function cosineSimilarity(a: number[], b: number[]): number {
39
+ if (a.length !== b.length) {
40
+ throw new InvalidVectorError();
41
+ }
42
+
43
+ const dot = a.reduce((sum, ai, i) => sum + ai * (b[i] ?? 0), 0);
44
+ const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
45
+ const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
46
+
47
+ return normA > 0 && normB > 0 ? dot / (normA * normB) : 0;
48
+ }
49
+
50
+ /**
51
+ * Get current timestamp in milliseconds
52
+ */
53
+ export function now(): number {
54
+ return Date.now();
55
+ }
@@ -0,0 +1,438 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type {
3
+ VectorStorage,
4
+ VectorUpsertParams,
5
+ VectorUpsertResult,
6
+ VectorResult,
7
+ VectorResultNotFound,
8
+ VectorSearchResultWithDocument,
9
+ VectorSearchParams,
10
+ VectorSearchResult,
11
+ VectorNamespaceStats,
12
+ VectorNamespaceStatsWithSamples,
13
+ VectorGetAllStatsParams,
14
+ VectorStatsPaginated,
15
+ } from '@agentuity/core';
16
+ import { now, simpleEmbedding, cosineSimilarity } from './util';
17
+ import { randomUUID } from 'node:crypto';
18
+
19
+ export class LocalVectorStorage implements VectorStorage {
20
+ #db: Database;
21
+ #projectPath: string;
22
+
23
+ constructor(db: Database, projectPath: string) {
24
+ this.#db = db;
25
+ this.#projectPath = projectPath;
26
+ }
27
+
28
+ async upsert(name: string, ...documents: VectorUpsertParams[]): Promise<VectorUpsertResult[]> {
29
+ if (!name?.trim()) {
30
+ throw new Error('Vector storage name is required');
31
+ }
32
+ if (documents.length === 0) {
33
+ throw new Error('At least one document is required');
34
+ }
35
+
36
+ const results: VectorUpsertResult[] = [];
37
+ const stmt = this.#db.prepare(`
38
+ INSERT INTO vector_storage (
39
+ project_path, name, id, key, embedding, document, metadata, created_at, updated_at
40
+ )
41
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
42
+ ON CONFLICT(project_path, name, key)
43
+ DO UPDATE SET
44
+ embedding = excluded.embedding,
45
+ document = excluded.document,
46
+ metadata = excluded.metadata,
47
+ updated_at = excluded.updated_at
48
+ `);
49
+
50
+ for (const doc of documents) {
51
+ if (!doc.key?.trim()) {
52
+ throw new Error('Each document must have a non-empty key');
53
+ }
54
+
55
+ // Generate or use provided embeddings
56
+ let embedding: number[];
57
+ if ('embeddings' in doc && doc.embeddings) {
58
+ if (!Array.isArray(doc.embeddings) || doc.embeddings.length === 0) {
59
+ throw new Error('Embeddings must be a non-empty array');
60
+ }
61
+ embedding = doc.embeddings;
62
+ } else if ('document' in doc && doc.document) {
63
+ if (!doc.document?.trim()) {
64
+ throw new Error('Document text must be non-empty');
65
+ }
66
+ embedding = simpleEmbedding(doc.document);
67
+ } else {
68
+ throw new Error('Each document must have either embeddings or document text');
69
+ }
70
+
71
+ const id = randomUUID();
72
+ const timestamp = now();
73
+ const embeddingJson = JSON.stringify(embedding);
74
+ const documentText = 'document' in doc ? doc.document : null;
75
+ const metadata = doc.metadata ? JSON.stringify(doc.metadata) : null;
76
+
77
+ stmt.run(
78
+ this.#projectPath,
79
+ name,
80
+ id,
81
+ doc.key,
82
+ embeddingJson,
83
+ documentText ?? null,
84
+ metadata ?? null,
85
+ timestamp,
86
+ timestamp
87
+ );
88
+
89
+ const row = this.#db
90
+ .prepare(
91
+ 'SELECT id FROM vector_storage WHERE project_path = ? AND name = ? AND key = ?'
92
+ )
93
+ .get(this.#projectPath, name, doc.key) as { id: string } | undefined;
94
+
95
+ const actualId = row?.id ?? id;
96
+ results.push({ key: doc.key, id: actualId });
97
+ }
98
+
99
+ return results;
100
+ }
101
+
102
+ async get<T extends Record<string, unknown> = Record<string, unknown>>(
103
+ name: string,
104
+ key: string
105
+ ): Promise<VectorResult<T>> {
106
+ if (!name?.trim() || !key?.trim()) {
107
+ throw new Error('Vector storage name and key are required');
108
+ }
109
+
110
+ const query = this.#db.query(`
111
+ SELECT id, key, embedding, document, metadata
112
+ FROM vector_storage
113
+ WHERE project_path = ? AND name = ? AND key = ?
114
+ `);
115
+
116
+ const row = query.get(this.#projectPath, name, key) as {
117
+ id: string;
118
+ key: string;
119
+ embedding: string;
120
+ document: string | null;
121
+ metadata: string | null;
122
+ } | null;
123
+
124
+ if (!row) {
125
+ return { exists: false } as VectorResultNotFound;
126
+ }
127
+
128
+ return {
129
+ exists: true,
130
+ data: {
131
+ id: row.id,
132
+ key: row.key,
133
+ embeddings: JSON.parse(row.embedding),
134
+ document: row.document || undefined,
135
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
136
+ similarity: 1.0, // Perfect match for direct get
137
+ } as VectorSearchResultWithDocument<T>,
138
+ };
139
+ }
140
+
141
+ async getMany<T extends Record<string, unknown> = Record<string, unknown>>(
142
+ name: string,
143
+ ...keys: string[]
144
+ ): Promise<Map<string, VectorSearchResultWithDocument<T>>> {
145
+ if (!name?.trim()) {
146
+ throw new Error('Vector storage name is required');
147
+ }
148
+ if (keys.length === 0) {
149
+ return new Map();
150
+ }
151
+
152
+ const results = await Promise.all(
153
+ keys.map(async (key) => {
154
+ const result = await this.get<T>(name, key);
155
+ return { key, result };
156
+ })
157
+ );
158
+
159
+ const map = new Map<string, VectorSearchResultWithDocument<T>>();
160
+ for (const { key, result } of results) {
161
+ if (result.exists) {
162
+ map.set(key, result.data);
163
+ }
164
+ }
165
+
166
+ return map;
167
+ }
168
+
169
+ async search<T extends Record<string, unknown> = Record<string, unknown>>(
170
+ name: string,
171
+ params: VectorSearchParams<T>
172
+ ): Promise<VectorSearchResult<T>[]> {
173
+ if (!name?.trim()) {
174
+ throw new Error('Vector storage name is required');
175
+ }
176
+ if (!params.query?.trim()) {
177
+ throw new Error('Query is required');
178
+ }
179
+
180
+ // Fetch all vectors for this name
181
+ const query = this.#db.query(`
182
+ SELECT id, key, embedding, metadata
183
+ FROM vector_storage
184
+ WHERE project_path = ? AND name = ?
185
+ `);
186
+
187
+ const rows = query.all(this.#projectPath, name) as Array<{
188
+ id: string;
189
+ key: string;
190
+ embedding: string;
191
+ metadata: string | null;
192
+ }>;
193
+
194
+ // If no vectors exist, return empty results
195
+ const row = rows[0];
196
+ if (!row) {
197
+ return [];
198
+ }
199
+
200
+ // Detect dimensionality from first stored vector
201
+ const firstEmbedding = JSON.parse(row.embedding);
202
+ const dimensions = firstEmbedding.length;
203
+
204
+ // Generate query embedding with matching dimensions
205
+ const queryEmbedding = simpleEmbedding(params.query, dimensions);
206
+
207
+ // Calculate similarities
208
+ const results: Array<VectorSearchResult<T> & { similarity: number }> = [];
209
+
210
+ for (const row of rows) {
211
+ const embedding = JSON.parse(row.embedding);
212
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
213
+
214
+ // Apply similarity threshold
215
+ if (params.similarity !== undefined && similarity < params.similarity) {
216
+ continue;
217
+ }
218
+
219
+ // Apply metadata filter
220
+ if (params.metadata) {
221
+ const rowMetadata = row.metadata ? JSON.parse(row.metadata) : {};
222
+ const matches = Object.entries(params.metadata).every(
223
+ ([key, value]) => rowMetadata[key] === value
224
+ );
225
+ if (!matches) {
226
+ continue;
227
+ }
228
+ }
229
+
230
+ results.push({
231
+ id: row.id,
232
+ key: row.key,
233
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
234
+ similarity,
235
+ } as VectorSearchResult<T> & { similarity: number });
236
+ }
237
+
238
+ // Sort by similarity descending
239
+ results.sort((a, b) => b.similarity - a.similarity);
240
+
241
+ // Apply limit
242
+ const limit = params.limit || 10;
243
+ return results.slice(0, limit);
244
+ }
245
+
246
+ async delete(name: string, ...keys: string[]): Promise<number> {
247
+ if (!name?.trim()) {
248
+ throw new Error('Vector storage name is required');
249
+ }
250
+ if (keys.length === 0) {
251
+ return 0;
252
+ }
253
+
254
+ const placeholders = keys.map(() => '?').join(', ');
255
+ const stmt = this.#db.prepare(`
256
+ DELETE FROM vector_storage
257
+ WHERE project_path = ? AND name = ? AND key IN (${placeholders})
258
+ `);
259
+
260
+ const result = stmt.run(this.#projectPath, name, ...keys);
261
+ return result.changes;
262
+ }
263
+
264
+ async exists(name: string): Promise<boolean> {
265
+ if (!name?.trim()) {
266
+ throw new Error('Vector storage name is required');
267
+ }
268
+
269
+ const query = this.#db.query(`
270
+ SELECT COUNT(*) as count
271
+ FROM vector_storage
272
+ WHERE project_path = ? AND name = ?
273
+ `);
274
+
275
+ const { count } = query.get(this.#projectPath, name) as { count: number };
276
+ return count > 0;
277
+ }
278
+
279
+ async getStats(name: string): Promise<VectorNamespaceStatsWithSamples> {
280
+ if (!name?.trim()) {
281
+ throw new Error('Vector storage name is required');
282
+ }
283
+
284
+ const countQuery = this.#db.query(`
285
+ SELECT COUNT(*) as count,
286
+ MIN(created_at) as created_at, MAX(updated_at) as last_used
287
+ FROM vector_storage
288
+ WHERE project_path = ? AND name = ?
289
+ `);
290
+
291
+ const stats = countQuery.get(this.#projectPath, name) as {
292
+ count: number;
293
+ created_at: number | null;
294
+ last_used: number | null;
295
+ };
296
+
297
+ if (stats.count === 0) {
298
+ return { sum: 0, count: 0 };
299
+ }
300
+
301
+ const sampleQuery = this.#db.query(`
302
+ SELECT key, embedding, document, metadata, created_at, updated_at
303
+ FROM vector_storage
304
+ WHERE project_path = ? AND name = ?
305
+ LIMIT 20
306
+ `);
307
+
308
+ const samples = sampleQuery.all(this.#projectPath, name) as Array<{
309
+ key: string;
310
+ embedding: string;
311
+ document: string | null;
312
+ metadata: string | null;
313
+ created_at: number;
314
+ updated_at: number;
315
+ }>;
316
+
317
+ const encoder = new TextEncoder();
318
+ let totalSum = 0;
319
+ const sampledResults: VectorNamespaceStatsWithSamples['sampledResults'] = {};
320
+ for (const sample of samples) {
321
+ const embeddingBytes = encoder.encode(sample.embedding).length;
322
+ const documentBytes = sample.document ? encoder.encode(sample.document).length : 0;
323
+ const size = embeddingBytes + documentBytes;
324
+ totalSum += size;
325
+ sampledResults![sample.key] = {
326
+ embedding: JSON.parse(sample.embedding),
327
+ document: sample.document || undefined,
328
+ size,
329
+ metadata: sample.metadata ? JSON.parse(sample.metadata) : undefined,
330
+ firstUsed: sample.created_at,
331
+ lastUsed: sample.updated_at,
332
+ };
333
+ }
334
+
335
+ // Estimate total size based on sampled average if we have more records than samples
336
+ const estimatedSum =
337
+ stats.count <= samples.length
338
+ ? totalSum
339
+ : Math.round((totalSum / samples.length) * stats.count);
340
+
341
+ return {
342
+ sum: estimatedSum,
343
+ count: stats.count,
344
+ createdAt: stats.created_at || undefined,
345
+ lastUsed: stats.last_used || undefined,
346
+ sampledResults,
347
+ };
348
+ }
349
+
350
+ async getAllStats(
351
+ _params?: VectorGetAllStatsParams
352
+ ): Promise<Record<string, VectorNamespaceStats> | VectorStatsPaginated> {
353
+ const query = this.#db.query(`
354
+ SELECT name, embedding, document
355
+ FROM vector_storage
356
+ WHERE project_path = ?
357
+ `);
358
+
359
+ const rows = query.all(this.#projectPath) as Array<{
360
+ name: string;
361
+ embedding: string;
362
+ document: string | null;
363
+ }>;
364
+
365
+ const encoder = new TextEncoder();
366
+ const namespaceStats = new Map<
367
+ string,
368
+ { sum: number; count: number; createdAt?: number; lastUsed?: number }
369
+ >();
370
+
371
+ for (const row of rows) {
372
+ const embeddingBytes = encoder.encode(row.embedding).length;
373
+ const documentBytes = row.document ? encoder.encode(row.document).length : 0;
374
+ const size = embeddingBytes + documentBytes;
375
+
376
+ const existing = namespaceStats.get(row.name);
377
+ if (existing) {
378
+ existing.sum += size;
379
+ existing.count += 1;
380
+ } else {
381
+ namespaceStats.set(row.name, { sum: size, count: 1 });
382
+ }
383
+ }
384
+
385
+ // Get timestamps in a separate query
386
+ const timestampQuery = this.#db.query(`
387
+ SELECT name, MIN(created_at) as created_at, MAX(updated_at) as last_used
388
+ FROM vector_storage
389
+ WHERE project_path = ?
390
+ GROUP BY name
391
+ `);
392
+
393
+ const timestamps = timestampQuery.all(this.#projectPath) as Array<{
394
+ name: string;
395
+ created_at: number | null;
396
+ last_used: number | null;
397
+ }>;
398
+
399
+ for (const ts of timestamps) {
400
+ const stats = namespaceStats.get(ts.name);
401
+ if (stats) {
402
+ stats.createdAt = ts.created_at || undefined;
403
+ stats.lastUsed = ts.last_used || undefined;
404
+ }
405
+ }
406
+
407
+ const results: Record<string, VectorNamespaceStats> = {};
408
+ for (const [name, stats] of namespaceStats) {
409
+ results[name] = stats;
410
+ }
411
+
412
+ return results;
413
+ }
414
+
415
+ async getNamespaces(): Promise<string[]> {
416
+ const query = this.#db.query(`
417
+ SELECT DISTINCT name
418
+ FROM vector_storage
419
+ WHERE project_path = ?
420
+ `);
421
+
422
+ const rows = query.all(this.#projectPath) as Array<{ name: string }>;
423
+ return rows.map((row) => row.name);
424
+ }
425
+
426
+ async deleteNamespace(name: string): Promise<void> {
427
+ if (!name?.trim()) {
428
+ throw new Error('Vector storage name is required');
429
+ }
430
+
431
+ const stmt = this.#db.prepare(`
432
+ DELETE FROM vector_storage
433
+ WHERE project_path = ? AND name = ?
434
+ `);
435
+
436
+ stmt.run(this.#projectPath, name);
437
+ }
438
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @agentuity/local - Local development services
3
+ *
4
+ * Provides local storage implementations for development.
5
+ * Runtime-specific implementations are auto-detected.
6
+ *
7
+ * Users can provide their own implementations via service overrides
8
+ * by implementing these interfaces.
9
+ */
10
+
11
+ // Re-export core interfaces so users can implement their own
12
+ export type {
13
+ KeyValueStorage,
14
+ StreamStorage,
15
+ VectorStorage,
16
+ QueueService,
17
+ EmailService,
18
+ TaskStorage,
19
+ } from '@agentuity/core';
20
+
21
+ // Runtime detection
22
+ export { detectRuntime, isLocalAvailable, getRuntimeName, type Runtime } from './runtime';
23
+
24
+ // Bun implementations (only available when running in Bun)
25
+ export {
26
+ getLocalDB,
27
+ closeLocalDB,
28
+ LocalKeyValueStorage,
29
+ LocalStreamStorage,
30
+ LocalVectorStorage,
31
+ LocalQueueStorage,
32
+ LocalEmailStorage,
33
+ LocalTaskStorage,
34
+ now,
35
+ normalizeProjectPath,
36
+ } from './bun';
package/src/runtime.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Runtime detection for @agentuity/local
3
+ *
4
+ * Detects the current JavaScript runtime and provides
5
+ * appropriate local storage implementations.
6
+ */
7
+
8
+ export type Runtime = 'bun' | 'node' | 'deno' | 'workers' | 'unknown';
9
+
10
+ /**
11
+ * Detect the current runtime environment.
12
+ */
13
+ export function detectRuntime(): Runtime {
14
+ // Bun has a global Bun object
15
+ if (typeof (globalThis as any).Bun !== 'undefined') {
16
+ return 'bun';
17
+ }
18
+
19
+ // Deno has a global Deno object
20
+ if (typeof (globalThis as any).Deno !== 'undefined') {
21
+ return 'deno';
22
+ }
23
+
24
+ // Cloudflare Workers have caches.default
25
+ if (
26
+ typeof (globalThis as any).caches !== 'undefined' &&
27
+ 'default' in (globalThis as any).caches
28
+ ) {
29
+ return 'workers';
30
+ }
31
+
32
+ // Node.js has process.versions.node
33
+ if (
34
+ typeof (globalThis as any).process !== 'undefined' &&
35
+ (globalThis as any).process?.versions?.node
36
+ ) {
37
+ return 'node';
38
+ }
39
+
40
+ return 'unknown';
41
+ }
42
+
43
+ /**
44
+ * Check if local services are available for the current runtime.
45
+ */
46
+ export function isLocalAvailable(): boolean {
47
+ const runtime = detectRuntime();
48
+ return runtime === 'bun'; // Only Bun is supported for now
49
+ }
50
+
51
+ /**
52
+ * Get the current runtime name for logging.
53
+ */
54
+ export function getRuntimeName(): string {
55
+ return detectRuntime();
56
+ }