@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,145 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type {
3
+ QueueService,
4
+ QueuePublishParams,
5
+ QueuePublishResult,
6
+ QueueCreateParams,
7
+ QueueCreateResult,
8
+ } from '@agentuity/core';
9
+
10
+ export class LocalQueueStorage implements QueueService {
11
+ #db: Database;
12
+ #projectPath: string;
13
+
14
+ constructor(db: Database, projectPath: string) {
15
+ this.#db = db;
16
+ this.#projectPath = projectPath;
17
+ this.#initializeTable();
18
+ }
19
+
20
+ #initializeTable(): void {
21
+ this.#db.run(`
22
+ CREATE TABLE IF NOT EXISTS queue_messages (
23
+ id TEXT PRIMARY KEY,
24
+ project_path TEXT NOT NULL,
25
+ queue_name TEXT NOT NULL,
26
+ offset INTEGER NOT NULL,
27
+ payload TEXT NOT NULL,
28
+ metadata TEXT,
29
+ partition_key TEXT,
30
+ idempotency_key TEXT,
31
+ ttl_seconds INTEGER,
32
+ expires_at TEXT,
33
+ state TEXT NOT NULL DEFAULT 'pending',
34
+ created_at TEXT NOT NULL,
35
+ published_at TEXT NOT NULL
36
+ )
37
+ `);
38
+
39
+ this.#db.run(`
40
+ CREATE INDEX IF NOT EXISTS idx_queue_name
41
+ ON queue_messages(project_path, queue_name)
42
+ `);
43
+
44
+ this.#db.run(`
45
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_idempotency_unique
46
+ ON queue_messages(project_path, queue_name, idempotency_key)
47
+ WHERE idempotency_key IS NOT NULL
48
+ `);
49
+ }
50
+
51
+ async publish(
52
+ queueName: string,
53
+ payload: string | object,
54
+ params?: QueuePublishParams
55
+ ): Promise<QueuePublishResult> {
56
+ const id = crypto.randomUUID();
57
+ const timestamp = new Date().toISOString();
58
+ const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);
59
+ const metadataStr = params?.metadata ? JSON.stringify(params.metadata) : null;
60
+ const ttlSeconds = params?.ttl ?? null;
61
+ const expiresAt = ttlSeconds ? new Date(Date.now() + ttlSeconds * 1000).toISOString() : null;
62
+
63
+ const publishInTransaction = this.#db.transaction(() => {
64
+ if (params?.idempotencyKey) {
65
+ const existing = this.#db
66
+ .query(
67
+ `
68
+ SELECT id, offset, published_at
69
+ FROM queue_messages
70
+ WHERE project_path = ? AND queue_name = ? AND idempotency_key = ?
71
+ `
72
+ )
73
+ .get(this.#projectPath, queueName, params.idempotencyKey) as {
74
+ id: string;
75
+ offset: number;
76
+ published_at: string;
77
+ } | null;
78
+
79
+ if (existing) {
80
+ return {
81
+ id: existing.id,
82
+ offset: existing.offset,
83
+ publishedAt: existing.published_at,
84
+ };
85
+ }
86
+ }
87
+
88
+ const offsetResult = this.#db
89
+ .query(
90
+ `
91
+ SELECT COALESCE(MAX(offset), -1) + 1 as next_offset
92
+ FROM queue_messages
93
+ WHERE project_path = ? AND queue_name = ?
94
+ `
95
+ )
96
+ .get(this.#projectPath, queueName) as { next_offset: number };
97
+
98
+ const offset = offsetResult.next_offset;
99
+
100
+ this.#db
101
+ .prepare(
102
+ `
103
+ INSERT INTO queue_messages (
104
+ id, project_path, queue_name, offset, payload, metadata,
105
+ partition_key, idempotency_key, ttl_seconds, expires_at, state, created_at, published_at
106
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
107
+ `
108
+ )
109
+ .run(
110
+ id,
111
+ this.#projectPath,
112
+ queueName,
113
+ offset,
114
+ payloadStr,
115
+ metadataStr,
116
+ params?.partitionKey ?? null,
117
+ params?.idempotencyKey ?? null,
118
+ ttlSeconds,
119
+ expiresAt,
120
+ timestamp,
121
+ timestamp
122
+ );
123
+
124
+ return {
125
+ id,
126
+ offset,
127
+ publishedAt: timestamp,
128
+ };
129
+ });
130
+
131
+ return publishInTransaction.immediate();
132
+ }
133
+
134
+ async createQueue(queueName: string, params?: QueueCreateParams): Promise<QueueCreateResult> {
135
+ console.debug(`[local] createQueue: ${queueName}`);
136
+ return {
137
+ name: queueName,
138
+ queueType: params?.queueType ?? 'worker',
139
+ };
140
+ }
141
+
142
+ async deleteQueue(_queueName: string): Promise<void> {
143
+ // No-op in local mode — queues don't need provisioning locally
144
+ }
145
+ }
@@ -0,0 +1,358 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type {
3
+ StreamStorage,
4
+ Stream,
5
+ CreateStreamProps,
6
+ ListStreamsParams,
7
+ ListStreamsResponse,
8
+ StreamInfo,
9
+ } from '@agentuity/core';
10
+ import { now } from './util';
11
+ import { join } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { mkdirSync, existsSync, unlinkSync } from 'node:fs';
15
+ import { openSync, writeSync, closeSync, readFileSync } from 'node:fs';
16
+
17
+ export class LocalStreamStorage implements StreamStorage {
18
+ #db: Database;
19
+ #projectPath: string;
20
+ #serverUrl: string;
21
+ #tempDir: string;
22
+
23
+ constructor(db: Database, projectPath: string, serverUrl: string) {
24
+ this.#db = db;
25
+ this.#projectPath = projectPath;
26
+ this.#serverUrl = serverUrl;
27
+
28
+ // Create temp directory for stream buffering
29
+ this.#tempDir = join(homedir(), '.config', 'agentuity', 'streams');
30
+ if (!existsSync(this.#tempDir)) {
31
+ mkdirSync(this.#tempDir, { recursive: true });
32
+ }
33
+ }
34
+
35
+ async create(namespace: string, props?: CreateStreamProps): Promise<Stream> {
36
+ if (!namespace || namespace.length < 1 || namespace.length > 254) {
37
+ throw new Error('Stream namespace must be between 1 and 254 characters');
38
+ }
39
+
40
+ const id = randomUUID();
41
+ const timestamp = now();
42
+ const metadata = props?.metadata ? JSON.stringify(props.metadata) : null;
43
+
44
+ // Insert stream record with NULL data
45
+ const stmt = this.#db.prepare(`
46
+ INSERT INTO stream_storage (
47
+ project_path, id, name, metadata, content_type, created_at
48
+ )
49
+ VALUES (?, ?, ?, ?, ?, ?)
50
+ `);
51
+
52
+ stmt.run(
53
+ this.#projectPath,
54
+ id,
55
+ namespace,
56
+ metadata,
57
+ props?.contentType || 'application/octet-stream',
58
+ timestamp
59
+ );
60
+
61
+ const url = `${this.#serverUrl}/_agentuity/local/stream/${id}`;
62
+
63
+ return new LocalStream(
64
+ id,
65
+ url,
66
+ this.#db,
67
+ this.#projectPath,
68
+ this.#tempDir,
69
+ props?.compress ?? false
70
+ );
71
+ }
72
+
73
+ async list(params?: ListStreamsParams): Promise<ListStreamsResponse> {
74
+ if (params?.limit && (params.limit <= 0 || params.limit > 1000)) {
75
+ throw new Error('limit must be between 1 and 1000');
76
+ }
77
+
78
+ let query = `
79
+ SELECT id, name, metadata, size_bytes
80
+ FROM stream_storage
81
+ WHERE project_path = ?
82
+ `;
83
+ const queryParams: (string | number)[] = [this.#projectPath];
84
+
85
+ // Add filters (map namespace to name for the database)
86
+ if (params?.namespace) {
87
+ query += ` AND name = ?`;
88
+ queryParams.push(params.namespace);
89
+ }
90
+
91
+ if (params?.metadata) {
92
+ // Simple JSON matching - check if metadata contains all key-value pairs
93
+ for (const [key, value] of Object.entries(params.metadata)) {
94
+ query += ` AND metadata LIKE ?`;
95
+ queryParams.push(`%"${key}":"${value}"%`);
96
+ }
97
+ }
98
+
99
+ // Get total count
100
+ const countQuery = this.#db.query(
101
+ query.replace('SELECT id, name, metadata, size_bytes', 'SELECT COUNT(*) as count')
102
+ );
103
+ const { count } = countQuery.get(...queryParams) as { count: number };
104
+
105
+ // Add pagination
106
+ query += ` ORDER BY created_at DESC`;
107
+ if (params?.limit) {
108
+ query += ` LIMIT ${params.limit}`;
109
+ }
110
+ if (params?.offset) {
111
+ query += ` OFFSET ${params.offset}`;
112
+ }
113
+
114
+ const stmt = this.#db.query(query);
115
+ const rows = stmt.all(...queryParams) as Array<{
116
+ id: string;
117
+ name: string;
118
+ metadata: string | null;
119
+ size_bytes: number;
120
+ }>;
121
+
122
+ // Map name to namespace for the SDK interface
123
+ const streams: StreamInfo[] = rows.map((row) => ({
124
+ id: row.id,
125
+ namespace: row.name,
126
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
127
+ url: `${this.#serverUrl}/_agentuity/local/stream/${row.id}`,
128
+ sizeBytes: row.size_bytes,
129
+ expiresAt: null,
130
+ }));
131
+
132
+ return {
133
+ success: true,
134
+ streams,
135
+ total: count,
136
+ };
137
+ }
138
+
139
+ async get(id: string): Promise<StreamInfo> {
140
+ if (!id?.trim()) {
141
+ throw new Error('Stream id is required');
142
+ }
143
+
144
+ const stmt = this.#db.query<
145
+ { id: string; name: string; metadata: string | null; size_bytes: number },
146
+ [string, string]
147
+ >(`
148
+ SELECT id, name, metadata, size_bytes
149
+ FROM stream_storage
150
+ WHERE project_path = ? AND id = ?
151
+ `);
152
+
153
+ const row = stmt.get(this.#projectPath, id);
154
+
155
+ if (!row) {
156
+ throw new Error(`Stream not found: ${id}`);
157
+ }
158
+
159
+ const metadata = row.metadata ? JSON.parse(row.metadata) : {};
160
+ const url = `${this.#serverUrl}/_agentuity/local/stream/${id}`;
161
+
162
+ // Map name to namespace for the SDK interface
163
+ return {
164
+ id: row.id,
165
+ namespace: row.name,
166
+ metadata,
167
+ url,
168
+ sizeBytes: row.size_bytes,
169
+ expiresAt: null,
170
+ };
171
+ }
172
+
173
+ async download(id: string): Promise<ReadableStream<Uint8Array>> {
174
+ if (!id?.trim()) {
175
+ throw new Error('Stream id is required');
176
+ }
177
+
178
+ const stmt = this.#db.query<{ data: Buffer | null }, [string, string]>(`
179
+ SELECT data FROM stream_storage
180
+ WHERE project_path = ? AND id = ?
181
+ `);
182
+
183
+ const row = stmt.get(this.#projectPath, id);
184
+
185
+ if (!row || !row.data) {
186
+ throw new Error(`Stream not found or empty: ${id}`);
187
+ }
188
+
189
+ // Convert Buffer to ReadableStream
190
+ const buffer = row.data;
191
+ return new ReadableStream({
192
+ start(controller) {
193
+ controller.enqueue(new Uint8Array(buffer));
194
+ controller.close();
195
+ },
196
+ });
197
+ }
198
+
199
+ async delete(id: string): Promise<void> {
200
+ if (!id?.trim()) {
201
+ throw new Error('Stream id is required');
202
+ }
203
+
204
+ const stmt = this.#db.prepare(`
205
+ DELETE FROM stream_storage
206
+ WHERE project_path = ? AND id = ?
207
+ `);
208
+
209
+ stmt.run(this.#projectPath, id);
210
+ }
211
+ }
212
+
213
+ class LocalStream extends WritableStream implements Stream {
214
+ public readonly id: string;
215
+ public readonly url: string;
216
+
217
+ #db: Database;
218
+ #projectPath: string;
219
+ #compressed: boolean;
220
+ #tempFilePath: string;
221
+ #fileHandle: number | null = null;
222
+ #bytesWritten = 0;
223
+ #closed = false;
224
+
225
+ constructor(
226
+ id: string,
227
+ url: string,
228
+ db: Database,
229
+ projectPath: string,
230
+ tempDir: string,
231
+ compressed: boolean
232
+ ) {
233
+ super({
234
+ write: async (chunk: Uint8Array) => {
235
+ await this.#writeToFile(chunk);
236
+ },
237
+ close: async () => {
238
+ await this.#persist();
239
+ },
240
+ });
241
+
242
+ this.id = id;
243
+ this.url = url;
244
+ this.#db = db;
245
+ this.#projectPath = projectPath;
246
+ this.#compressed = compressed;
247
+ this.#tempFilePath = join(tempDir, `${id}.tmp`);
248
+
249
+ // Open file for writing
250
+ this.#fileHandle = openSync(this.#tempFilePath, 'w');
251
+ }
252
+
253
+ get bytesWritten(): number {
254
+ return this.#bytesWritten;
255
+ }
256
+
257
+ get compressed(): boolean {
258
+ return this.#compressed;
259
+ }
260
+
261
+ async write(chunk: string | Uint8Array | ArrayBuffer | Buffer | object): Promise<void> {
262
+ if (this.#closed) {
263
+ throw new Error('Stream is closed');
264
+ }
265
+
266
+ let binary: Uint8Array;
267
+ if (chunk instanceof Uint8Array) {
268
+ binary = chunk;
269
+ } else if (typeof chunk === 'string') {
270
+ binary = new TextEncoder().encode(chunk);
271
+ } else if (chunk instanceof ArrayBuffer) {
272
+ binary = new Uint8Array(chunk);
273
+ } else if (typeof chunk === 'object') {
274
+ binary = new TextEncoder().encode(JSON.stringify(chunk));
275
+ } else {
276
+ binary = new TextEncoder().encode(String(chunk));
277
+ }
278
+
279
+ await this.#writeToFile(binary);
280
+ }
281
+
282
+ override async close(): Promise<void> {
283
+ if (this.#closed) {
284
+ return;
285
+ }
286
+
287
+ this.#closed = true;
288
+
289
+ // Close file handle if open
290
+ if (this.#fileHandle !== null) {
291
+ closeSync(this.#fileHandle);
292
+ this.#fileHandle = null;
293
+ }
294
+
295
+ await this.#persist();
296
+ }
297
+
298
+ getReader(): ReadableStream<Uint8Array> {
299
+ const db = this.#db;
300
+ const projectPath = this.#projectPath;
301
+ const id = this.id;
302
+
303
+ return new ReadableStream({
304
+ start(controller) {
305
+ const query = db.query(`
306
+ SELECT data FROM stream_storage
307
+ WHERE project_path = ? AND id = ?
308
+ `);
309
+
310
+ const row = query.get(projectPath, id) as { data: Buffer | null } | null;
311
+
312
+ if (!row || !row.data) {
313
+ controller.error(new Error('Stream not found or not finalized'));
314
+ return;
315
+ }
316
+
317
+ controller.enqueue(new Uint8Array(row.data));
318
+ controller.close();
319
+ },
320
+ });
321
+ }
322
+
323
+ async #writeToFile(chunk: Uint8Array): Promise<void> {
324
+ if (this.#fileHandle === null) {
325
+ throw new Error('File handle is closed');
326
+ }
327
+
328
+ const written = writeSync(this.#fileHandle, chunk);
329
+ this.#bytesWritten += written;
330
+ }
331
+
332
+ async #persist(): Promise<void> {
333
+ // Read buffered file
334
+ let data: Buffer = readFileSync(this.#tempFilePath);
335
+
336
+ // Optional: Apply compression if enabled
337
+ if (this.#compressed) {
338
+ const { gzipSync } = await import('node:zlib');
339
+ data = gzipSync(data) as Buffer;
340
+ }
341
+
342
+ // Update DB with finalized data
343
+ const stmt = this.#db.prepare(`
344
+ UPDATE stream_storage
345
+ SET data = ?, size_bytes = ?
346
+ WHERE project_path = ? AND id = ?
347
+ `);
348
+
349
+ stmt.run(data, this.#bytesWritten, this.#projectPath, this.id);
350
+
351
+ // Clean up temp file
352
+ try {
353
+ unlinkSync(this.#tempFilePath);
354
+ } catch {
355
+ // Ignore cleanup errors
356
+ }
357
+ }
358
+ }