@mastra/libsql 0.0.1-alpha.1

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.
@@ -0,0 +1,6 @@
1
+ import { createConfig } from '@internal/lint/eslint';
2
+
3
+ const config = await createConfig();
4
+
5
+ /** @type {import("eslint").Linter.Config[]} */
6
+ export default [...config.map(conf => ({ ...conf, ignores: [...(conf.ignores || []), '**/vitest.perf.config.ts'] }))];
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@mastra/libsql",
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "Libsql provider for Mastra - includes both vector and db storage capabilities",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@libsql/client": "^0.15.4",
24
+ "@mastra/core": "^0.9.1-alpha.1"
25
+ },
26
+ "devDependencies": {
27
+ "@microsoft/api-extractor": "^7.52.1",
28
+ "@types/node": "^20.17.27",
29
+ "eslint": "^9.23.0",
30
+ "tsup": "^8.4.0",
31
+ "typescript": "^5.8.2",
32
+ "vitest": "^3.0.9",
33
+ "@internal/lint": "0.0.2",
34
+ "@internal/storage-test-utils": "0.0.1-alpha.0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
38
+ "build:watch": "pnpm build --watch",
39
+ "test": "vitest run",
40
+ "lint": "eslint ."
41
+ }
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './vector';
2
+ export * from './storage';
@@ -0,0 +1,15 @@
1
+ import { createTestSuite } from '@internal/storage-test-utils';
2
+ import { Mastra } from '@mastra/core/mastra';
3
+
4
+ import { LibSQLStore } from './index';
5
+
6
+ // Test database configuration
7
+ const TEST_DB_URL = 'file::memory:?cache=shared'; // Use in-memory SQLite for tests
8
+
9
+ const mastra = new Mastra({
10
+ storage: new LibSQLStore({
11
+ url: TEST_DB_URL,
12
+ }),
13
+ });
14
+
15
+ createTestSuite(mastra.getStorage()!);
@@ -0,0 +1,624 @@
1
+ import { createClient } from '@libsql/client';
2
+ import type { Client, InValue } from '@libsql/client';
3
+ import type { MetricResult, TestInfo } from '@mastra/core/eval';
4
+ import type { MessageType, StorageThreadType } from '@mastra/core/memory';
5
+ import {
6
+ MastraStorage,
7
+ TABLE_MESSAGES,
8
+ TABLE_THREADS,
9
+ TABLE_TRACES,
10
+ TABLE_WORKFLOW_SNAPSHOT,
11
+ TABLE_EVALS,
12
+ } from '@mastra/core/storage';
13
+ import type { EvalRow, StorageColumn, StorageGetMessagesArg, TABLE_NAMES } from '@mastra/core/storage';
14
+ import type { WorkflowRunState } from '@mastra/core/workflows';
15
+
16
+ function safelyParseJSON(jsonString: string): any {
17
+ try {
18
+ return JSON.parse(jsonString);
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ export interface LibSQLConfig {
25
+ url: string;
26
+ authToken?: string;
27
+ }
28
+
29
+ export class LibSQLStore extends MastraStorage {
30
+ private client: Client;
31
+
32
+ constructor(config: LibSQLConfig) {
33
+ super({ name: `LibSQLStore` });
34
+
35
+ // need to re-init every time for in memory dbs or the tables might not exist
36
+ if (config.url.endsWith(':memory:')) {
37
+ this.shouldCacheInit = false;
38
+ }
39
+
40
+ this.client = createClient(config);
41
+ }
42
+
43
+ private getCreateTableSQL(tableName: TABLE_NAMES, schema: Record<string, StorageColumn>): string {
44
+ const columns = Object.entries(schema).map(([name, col]) => {
45
+ let type = col.type.toUpperCase();
46
+ if (type === 'TEXT') type = 'TEXT';
47
+ if (type === 'TIMESTAMP') type = 'TEXT'; // Store timestamps as ISO strings
48
+ // if (type === 'BIGINT') type = 'INTEGER';
49
+
50
+ const nullable = col.nullable ? '' : 'NOT NULL';
51
+ const primaryKey = col.primaryKey ? 'PRIMARY KEY' : '';
52
+
53
+ return `${name} ${type} ${nullable} ${primaryKey}`.trim();
54
+ });
55
+
56
+ // For workflow_snapshot table, create a composite primary key
57
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
58
+ const stmnt = `CREATE TABLE IF NOT EXISTS ${tableName} (
59
+ ${columns.join(',\n')},
60
+ PRIMARY KEY (workflow_name, run_id)
61
+ )`;
62
+ return stmnt;
63
+ }
64
+
65
+ return `CREATE TABLE IF NOT EXISTS ${tableName} (${columns.join(', ')})`;
66
+ }
67
+
68
+ async createTable({
69
+ tableName,
70
+ schema,
71
+ }: {
72
+ tableName: TABLE_NAMES;
73
+ schema: Record<string, StorageColumn>;
74
+ }): Promise<void> {
75
+ try {
76
+ this.logger.debug(`Creating database table`, { tableName, operation: 'schema init' });
77
+ const sql = this.getCreateTableSQL(tableName, schema);
78
+ await this.client.execute(sql);
79
+ } catch (error) {
80
+ this.logger.error(`Error creating table ${tableName}: ${error}`);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
86
+ try {
87
+ await this.client.execute(`DELETE FROM ${tableName}`);
88
+ } catch (e) {
89
+ if (e instanceof Error) {
90
+ this.logger.error(e.message);
91
+ }
92
+ }
93
+ }
94
+
95
+ private prepareStatement({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): {
96
+ sql: string;
97
+ args: InValue[];
98
+ } {
99
+ const columns = Object.keys(record);
100
+ const values = Object.values(record).map(v => {
101
+ if (typeof v === `undefined`) {
102
+ // returning an undefined value will cause libsql to throw
103
+ return null;
104
+ }
105
+ if (v instanceof Date) {
106
+ return v.toISOString();
107
+ }
108
+ return typeof v === 'object' ? JSON.stringify(v) : v;
109
+ });
110
+ const placeholders = values.map(() => '?').join(', ');
111
+
112
+ return {
113
+ sql: `INSERT OR REPLACE INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`,
114
+ args: values,
115
+ };
116
+ }
117
+
118
+ async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
119
+ try {
120
+ await this.client.execute(
121
+ this.prepareStatement({
122
+ tableName,
123
+ record,
124
+ }),
125
+ );
126
+ } catch (error) {
127
+ this.logger.error(`Error upserting into table ${tableName}: ${error}`);
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
133
+ if (records.length === 0) return;
134
+
135
+ try {
136
+ const batchStatements = records.map(r => this.prepareStatement({ tableName, record: r }));
137
+ await this.client.batch(batchStatements, 'write');
138
+ } catch (error) {
139
+ this.logger.error(`Error upserting into table ${tableName}: ${error}`);
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
145
+ const conditions = Object.entries(keys)
146
+ .map(([key]) => `${key} = ?`)
147
+ .join(' AND ');
148
+ const values = Object.values(keys);
149
+
150
+ const result = await this.client.execute({
151
+ sql: `SELECT * FROM ${tableName} WHERE ${conditions} ORDER BY createdAt DESC LIMIT 1`,
152
+ args: values,
153
+ });
154
+
155
+ if (!result.rows || result.rows.length === 0) {
156
+ return null;
157
+ }
158
+
159
+ const row = result.rows[0];
160
+ // Checks whether the string looks like a JSON object ({}) or array ([])
161
+ // If the string starts with { or [, it assumes it's JSON and parses it
162
+ // Otherwise, it just returns, preventing unintended number conversions
163
+ const parsed = Object.fromEntries(
164
+ Object.entries(row || {}).map(([k, v]) => {
165
+ try {
166
+ return [k, typeof v === 'string' ? (v.startsWith('{') || v.startsWith('[') ? JSON.parse(v) : v) : v];
167
+ } catch {
168
+ return [k, v];
169
+ }
170
+ }),
171
+ );
172
+
173
+ return parsed as R;
174
+ }
175
+
176
+ async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
177
+ const result = await this.load<StorageThreadType>({
178
+ tableName: TABLE_THREADS,
179
+ keys: { id: threadId },
180
+ });
181
+
182
+ if (!result) {
183
+ return null;
184
+ }
185
+
186
+ return {
187
+ ...result,
188
+ metadata: typeof result.metadata === 'string' ? JSON.parse(result.metadata) : result.metadata,
189
+ };
190
+ }
191
+
192
+ async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
193
+ const result = await this.client.execute({
194
+ sql: `SELECT * FROM ${TABLE_THREADS} WHERE resourceId = ?`,
195
+ args: [resourceId],
196
+ });
197
+
198
+ if (!result.rows) {
199
+ return [];
200
+ }
201
+
202
+ return result.rows.map(thread => ({
203
+ id: thread.id,
204
+ resourceId: thread.resourceId,
205
+ title: thread.title,
206
+ createdAt: thread.createdAt,
207
+ updatedAt: thread.updatedAt,
208
+ metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
209
+ })) as any as StorageThreadType[];
210
+ }
211
+
212
+ async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
213
+ await this.insert({
214
+ tableName: TABLE_THREADS,
215
+ record: {
216
+ ...thread,
217
+ metadata: JSON.stringify(thread.metadata),
218
+ },
219
+ });
220
+
221
+ return thread;
222
+ }
223
+
224
+ async updateThread({
225
+ id,
226
+ title,
227
+ metadata,
228
+ }: {
229
+ id: string;
230
+ title: string;
231
+ metadata: Record<string, unknown>;
232
+ }): Promise<StorageThreadType> {
233
+ const thread = await this.getThreadById({ threadId: id });
234
+ if (!thread) {
235
+ throw new Error(`Thread ${id} not found`);
236
+ }
237
+
238
+ const updatedThread = {
239
+ ...thread,
240
+ title,
241
+ metadata: {
242
+ ...thread.metadata,
243
+ ...metadata,
244
+ },
245
+ };
246
+
247
+ await this.client.execute({
248
+ sql: `UPDATE ${TABLE_THREADS} SET title = ?, metadata = ? WHERE id = ?`,
249
+ args: [title, JSON.stringify(updatedThread.metadata), id],
250
+ });
251
+
252
+ return updatedThread;
253
+ }
254
+
255
+ async deleteThread({ threadId }: { threadId: string }): Promise<void> {
256
+ await this.client.execute({
257
+ sql: `DELETE FROM ${TABLE_THREADS} WHERE id = ?`,
258
+ args: [threadId],
259
+ });
260
+ // Messages will be automatically deleted due to CASCADE constraint
261
+ }
262
+
263
+ private parseRow(row: any): MessageType {
264
+ let content = row.content;
265
+ try {
266
+ content = JSON.parse(row.content);
267
+ } catch {
268
+ // use content as is if it's not JSON
269
+ }
270
+ return {
271
+ id: row.id,
272
+ content,
273
+ role: row.role,
274
+ type: row.type,
275
+ createdAt: new Date(row.createdAt as string),
276
+ threadId: row.thread_id,
277
+ } as MessageType;
278
+ }
279
+
280
+ async getMessages<T extends MessageType[]>({ threadId, selectBy }: StorageGetMessagesArg): Promise<T> {
281
+ try {
282
+ const messages: MessageType[] = [];
283
+ const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
284
+
285
+ // If we have specific messages to select
286
+ if (selectBy?.include?.length) {
287
+ const includeIds = selectBy.include.map(i => i.id);
288
+ const maxPrev = Math.max(...selectBy.include.map(i => i.withPreviousMessages || 0));
289
+ const maxNext = Math.max(...selectBy.include.map(i => i.withNextMessages || 0));
290
+
291
+ // Get messages around all specified IDs in one query using row numbers
292
+ const includeResult = await this.client.execute({
293
+ sql: `
294
+ WITH numbered_messages AS (
295
+ SELECT
296
+ id,
297
+ content,
298
+ role,
299
+ type,
300
+ "createdAt",
301
+ thread_id,
302
+ ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
303
+ FROM "${TABLE_MESSAGES}"
304
+ WHERE thread_id = ?
305
+ ),
306
+ target_positions AS (
307
+ SELECT row_num as target_pos
308
+ FROM numbered_messages
309
+ WHERE id IN (${includeIds.map(() => '?').join(', ')})
310
+ )
311
+ SELECT DISTINCT m.*
312
+ FROM numbered_messages m
313
+ CROSS JOIN target_positions t
314
+ WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
315
+ ORDER BY m."createdAt" ASC
316
+ `,
317
+ args: [threadId, ...includeIds, maxPrev, maxNext],
318
+ });
319
+
320
+ if (includeResult.rows) {
321
+ messages.push(...includeResult.rows.map((row: any) => this.parseRow(row)));
322
+ }
323
+ }
324
+
325
+ // Get remaining messages, excluding already fetched IDs
326
+ const excludeIds = messages.map(m => m.id);
327
+ const remainingSql = `
328
+ SELECT
329
+ id,
330
+ content,
331
+ role,
332
+ type,
333
+ "createdAt",
334
+ thread_id
335
+ FROM "${TABLE_MESSAGES}"
336
+ WHERE thread_id = ?
337
+ ${excludeIds.length ? `AND id NOT IN (${excludeIds.map(() => '?').join(', ')})` : ''}
338
+ ORDER BY "createdAt" DESC
339
+ LIMIT ?
340
+ `;
341
+ const remainingArgs = [threadId, ...(excludeIds.length ? excludeIds : []), limit];
342
+
343
+ const remainingResult = await this.client.execute({
344
+ sql: remainingSql,
345
+ args: remainingArgs,
346
+ });
347
+
348
+ if (remainingResult.rows) {
349
+ messages.push(...remainingResult.rows.map((row: any) => this.parseRow(row)));
350
+ }
351
+
352
+ // Sort all messages by creation date
353
+ messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
354
+
355
+ return messages as T;
356
+ } catch (error) {
357
+ this.logger.error('Error getting messages:', error as Error);
358
+ throw error;
359
+ }
360
+ }
361
+
362
+ async saveMessages({ messages }: { messages: MessageType[] }): Promise<MessageType[]> {
363
+ if (messages.length === 0) return messages;
364
+
365
+ try {
366
+ const threadId = messages[0]?.threadId;
367
+ if (!threadId) {
368
+ throw new Error('Thread ID is required');
369
+ }
370
+
371
+ // Prepare batch statements for all messages
372
+ const batchStatements = messages.map(message => {
373
+ const time = message.createdAt || new Date();
374
+ return {
375
+ sql: `INSERT INTO ${TABLE_MESSAGES} (id, thread_id, content, role, type, createdAt)
376
+ VALUES (?, ?, ?, ?, ?, ?)`,
377
+ args: [
378
+ message.id,
379
+ threadId,
380
+ typeof message.content === 'object' ? JSON.stringify(message.content) : message.content,
381
+ message.role,
382
+ message.type,
383
+ time instanceof Date ? time.toISOString() : time,
384
+ ],
385
+ };
386
+ });
387
+
388
+ // Execute all inserts in a single batch
389
+ await this.client.batch(batchStatements, 'write');
390
+
391
+ return messages;
392
+ } catch (error) {
393
+ this.logger.error('Failed to save messages in database: ' + (error as { message: string })?.message);
394
+ throw error;
395
+ }
396
+ }
397
+
398
+ private transformEvalRow(row: Record<string, any>): EvalRow {
399
+ const resultValue = JSON.parse(row.result as string);
400
+ const testInfoValue = row.test_info ? JSON.parse(row.test_info as string) : undefined;
401
+
402
+ if (!resultValue || typeof resultValue !== 'object' || !('score' in resultValue)) {
403
+ throw new Error(`Invalid MetricResult format: ${JSON.stringify(resultValue)}`);
404
+ }
405
+
406
+ return {
407
+ input: row.input as string,
408
+ output: row.output as string,
409
+ result: resultValue as MetricResult,
410
+ agentName: row.agent_name as string,
411
+ metricName: row.metric_name as string,
412
+ instructions: row.instructions as string,
413
+ testInfo: testInfoValue as TestInfo,
414
+ globalRunId: row.global_run_id as string,
415
+ runId: row.run_id as string,
416
+ createdAt: row.created_at as string,
417
+ };
418
+ }
419
+
420
+ async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
421
+ try {
422
+ const baseQuery = `SELECT * FROM ${TABLE_EVALS} WHERE agent_name = ?`;
423
+ const typeCondition =
424
+ type === 'test'
425
+ ? " AND test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL"
426
+ : type === 'live'
427
+ ? " AND (test_info IS NULL OR test_info->>'testPath' IS NULL)"
428
+ : '';
429
+
430
+ const result = await this.client.execute({
431
+ sql: `${baseQuery}${typeCondition} ORDER BY created_at DESC`,
432
+ args: [agentName],
433
+ });
434
+
435
+ return result.rows?.map(row => this.transformEvalRow(row)) ?? [];
436
+ } catch (error) {
437
+ // Handle case where table doesn't exist yet
438
+ if (error instanceof Error && error.message.includes('no such table')) {
439
+ return [];
440
+ }
441
+ this.logger.error('Failed to get evals for the specified agent: ' + (error as any)?.message);
442
+ throw error;
443
+ }
444
+ }
445
+
446
+ // TODO: add types
447
+ async getTraces(
448
+ {
449
+ name,
450
+ scope,
451
+ page,
452
+ perPage,
453
+ attributes,
454
+ filters,
455
+ }: {
456
+ name?: string;
457
+ scope?: string;
458
+ page: number;
459
+ perPage: number;
460
+ attributes?: Record<string, string>;
461
+ filters?: Record<string, any>;
462
+ } = {
463
+ page: 0,
464
+ perPage: 100,
465
+ },
466
+ ): Promise<any[]> {
467
+ const limit = perPage;
468
+ const offset = page * perPage;
469
+
470
+ const args: (string | number)[] = [];
471
+
472
+ const conditions: string[] = [];
473
+ if (name) {
474
+ conditions.push("name LIKE CONCAT(?, '%')");
475
+ }
476
+ if (scope) {
477
+ conditions.push('scope = ?');
478
+ }
479
+ if (attributes) {
480
+ Object.keys(attributes).forEach(key => {
481
+ conditions.push(`attributes->>'$.${key}' = ?`);
482
+ });
483
+ }
484
+
485
+ if (filters) {
486
+ Object.entries(filters).forEach(([key, _value]) => {
487
+ conditions.push(`${key} = ?`);
488
+ });
489
+ }
490
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
491
+
492
+ if (name) {
493
+ args.push(name);
494
+ }
495
+
496
+ if (scope) {
497
+ args.push(scope);
498
+ }
499
+
500
+ if (attributes) {
501
+ for (const [, value] of Object.entries(attributes)) {
502
+ args.push(value);
503
+ }
504
+ }
505
+
506
+ if (filters) {
507
+ for (const [, value] of Object.entries(filters)) {
508
+ args.push(value);
509
+ }
510
+ }
511
+
512
+ args.push(limit, offset);
513
+
514
+ const result = await this.client.execute({
515
+ sql: `SELECT * FROM ${TABLE_TRACES} ${whereClause} ORDER BY "startTime" DESC LIMIT ? OFFSET ?`,
516
+ args,
517
+ });
518
+
519
+ if (!result.rows) {
520
+ return [];
521
+ }
522
+
523
+ return result.rows.map(row => ({
524
+ id: row.id,
525
+ parentSpanId: row.parentSpanId,
526
+ traceId: row.traceId,
527
+ name: row.name,
528
+ scope: row.scope,
529
+ kind: row.kind,
530
+ status: safelyParseJSON(row.status as string),
531
+ events: safelyParseJSON(row.events as string),
532
+ links: safelyParseJSON(row.links as string),
533
+ attributes: safelyParseJSON(row.attributes as string),
534
+ startTime: row.startTime,
535
+ endTime: row.endTime,
536
+ other: safelyParseJSON(row.other as string),
537
+ createdAt: row.createdAt,
538
+ })) as any;
539
+ }
540
+
541
+ async getWorkflowRuns({
542
+ workflowName,
543
+ fromDate,
544
+ toDate,
545
+ limit,
546
+ offset,
547
+ }: {
548
+ workflowName?: string;
549
+ fromDate?: Date;
550
+ toDate?: Date;
551
+ limit?: number;
552
+ offset?: number;
553
+ } = {}): Promise<{
554
+ runs: Array<{
555
+ workflowName: string;
556
+ runId: string;
557
+ snapshot: WorkflowRunState | string;
558
+ createdAt: Date;
559
+ updatedAt: Date;
560
+ }>;
561
+ total: number;
562
+ }> {
563
+ const conditions: string[] = [];
564
+ const args: InValue[] = [];
565
+
566
+ if (workflowName) {
567
+ conditions.push('workflow_name = ?');
568
+ args.push(workflowName);
569
+ }
570
+
571
+ if (fromDate) {
572
+ conditions.push('createdAt >= ?');
573
+ args.push(fromDate.toISOString());
574
+ }
575
+
576
+ if (toDate) {
577
+ conditions.push('createdAt <= ?');
578
+ args.push(toDate.toISOString());
579
+ }
580
+
581
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
582
+
583
+ let total = 0;
584
+ // Only get total count when using pagination
585
+ if (limit !== undefined && offset !== undefined) {
586
+ const countResult = await this.client.execute({
587
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
588
+ args,
589
+ });
590
+ total = Number(countResult.rows?.[0]?.count ?? 0);
591
+ }
592
+
593
+ // Get results
594
+ const result = await this.client.execute({
595
+ sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC${limit !== undefined && offset !== undefined ? ` LIMIT ? OFFSET ?` : ''}`,
596
+ args: limit !== undefined && offset !== undefined ? [...args, limit, offset] : args,
597
+ });
598
+
599
+ const runs = (result.rows || []).map(row => {
600
+ let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
601
+ if (typeof parsedSnapshot === 'string') {
602
+ try {
603
+ parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
604
+ } catch (e) {
605
+ // If parsing fails, return the raw snapshot string
606
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
607
+ }
608
+ }
609
+
610
+ return {
611
+ workflowName: row.workflow_name as string,
612
+ runId: row.run_id as string,
613
+ snapshot: parsedSnapshot,
614
+ createdAt: new Date(row.createdAt as string),
615
+ updatedAt: new Date(row.updatedAt as string),
616
+ };
617
+ });
618
+
619
+ // Use runs.length as total when not paginating
620
+ return { runs, total: total || runs.length };
621
+ }
622
+ }
623
+
624
+ export { LibSQLStore as DefaultStorage };