@mastra/libsql 0.10.1 → 0.10.2-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.
@@ -6,20 +6,23 @@ import type { MetricResult, TestInfo } from '@mastra/core/eval';
6
6
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
7
7
  import {
8
8
  MastraStorage,
9
+ TABLE_EVALS,
9
10
  TABLE_MESSAGES,
10
11
  TABLE_THREADS,
11
12
  TABLE_TRACES,
12
13
  TABLE_WORKFLOW_SNAPSHOT,
13
- TABLE_EVALS,
14
14
  } from '@mastra/core/storage';
15
15
  import type {
16
16
  EvalRow,
17
+ PaginationArgs,
18
+ PaginationInfo,
17
19
  StorageColumn,
18
20
  StorageGetMessagesArg,
19
21
  TABLE_NAMES,
20
22
  WorkflowRun,
21
23
  WorkflowRuns,
22
24
  } from '@mastra/core/storage';
25
+ import type { Trace } from '@mastra/core/telemetry';
23
26
  import { parseSqlIdentifier } from '@mastra/core/utils';
24
27
  import type { WorkflowRunState } from '@mastra/core/workflows';
25
28
 
@@ -274,24 +277,97 @@ export class LibSQLStore extends MastraStorage {
274
277
  };
275
278
  }
276
279
 
277
- async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
278
- const result = await this.client.execute({
279
- sql: `SELECT * FROM ${TABLE_THREADS} WHERE resourceId = ?`,
280
- args: [resourceId],
281
- });
280
+ /**
281
+ * @deprecated use getThreadsByResourceIdPaginated instead for paginated results.
282
+ */
283
+ public async getThreadsByResourceId(args: { resourceId: string }): Promise<StorageThreadType[]> {
284
+ const { resourceId } = args;
285
+
286
+ try {
287
+ const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
288
+ const queryParams: InValue[] = [resourceId];
289
+
290
+ const mapRowToStorageThreadType = (row: any): StorageThreadType => ({
291
+ id: row.id as string,
292
+ resourceId: row.resourceId as string,
293
+ title: row.title as string,
294
+ createdAt: new Date(row.createdAt as string), // Convert string to Date
295
+ updatedAt: new Date(row.updatedAt as string), // Convert string to Date
296
+ metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
297
+ });
282
298
 
283
- if (!result.rows) {
299
+ // Non-paginated path
300
+ const result = await this.client.execute({
301
+ sql: `SELECT * ${baseQuery} ORDER BY createdAt DESC`,
302
+ args: queryParams,
303
+ });
304
+
305
+ if (!result.rows) {
306
+ return [];
307
+ }
308
+ return result.rows.map(mapRowToStorageThreadType);
309
+ } catch (error) {
310
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
284
311
  return [];
285
312
  }
313
+ }
286
314
 
287
- return result.rows.map(thread => ({
288
- id: thread.id,
289
- resourceId: thread.resourceId,
290
- title: thread.title,
291
- createdAt: thread.createdAt,
292
- updatedAt: thread.updatedAt,
293
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
294
- })) as any as StorageThreadType[];
315
+ public async getThreadsByResourceIdPaginated(
316
+ args: {
317
+ resourceId: string;
318
+ } & PaginationArgs,
319
+ ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
320
+ const { resourceId, page = 0, perPage = 100 } = args;
321
+
322
+ try {
323
+ const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
324
+ const queryParams: InValue[] = [resourceId];
325
+
326
+ const mapRowToStorageThreadType = (row: any): StorageThreadType => ({
327
+ id: row.id as string,
328
+ resourceId: row.resourceId as string,
329
+ title: row.title as string,
330
+ createdAt: new Date(row.createdAt as string), // Convert string to Date
331
+ updatedAt: new Date(row.updatedAt as string), // Convert string to Date
332
+ metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
333
+ });
334
+
335
+ const currentOffset = page * perPage;
336
+
337
+ const countResult = await this.client.execute({
338
+ sql: `SELECT COUNT(*) as count ${baseQuery}`,
339
+ args: queryParams,
340
+ });
341
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
342
+
343
+ if (total === 0) {
344
+ return {
345
+ threads: [],
346
+ total: 0,
347
+ page,
348
+ perPage,
349
+ hasMore: false,
350
+ };
351
+ }
352
+
353
+ const dataResult = await this.client.execute({
354
+ sql: `SELECT * ${baseQuery} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
355
+ args: [...queryParams, perPage, currentOffset],
356
+ });
357
+
358
+ const threads = (dataResult.rows || []).map(mapRowToStorageThreadType);
359
+
360
+ return {
361
+ threads,
362
+ total,
363
+ page,
364
+ perPage,
365
+ hasMore: currentOffset + threads.length < total,
366
+ };
367
+ } catch (error) {
368
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
369
+ return { threads: [], total: 0, page, perPage, hasMore: false };
370
+ }
295
371
  }
296
372
 
297
373
  async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
@@ -363,87 +439,76 @@ export class LibSQLStore extends MastraStorage {
363
439
  return result;
364
440
  }
365
441
 
442
+ private async _getIncludedMessages(threadId: string, selectBy: StorageGetMessagesArg['selectBy']) {
443
+ const include = selectBy?.include;
444
+ if (!include) return null;
445
+
446
+ const includeIds = include.map(i => i.id);
447
+ const maxPrev = Math.max(...include.map(i => i.withPreviousMessages || 0));
448
+ const maxNext = Math.max(...include.map(i => i.withNextMessages || 0));
449
+
450
+ const includeResult = await this.client.execute({
451
+ sql: `
452
+ WITH numbered_messages AS (
453
+ SELECT
454
+ id, content, role, type, "createdAt", thread_id,
455
+ ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
456
+ FROM "${TABLE_MESSAGES}"
457
+ WHERE thread_id = ?
458
+ ),
459
+ target_positions AS (
460
+ SELECT row_num as target_pos
461
+ FROM numbered_messages
462
+ WHERE id IN (${includeIds.map(() => '?').join(', ')})
463
+ )
464
+ SELECT DISTINCT m.*
465
+ FROM numbered_messages m
466
+ CROSS JOIN target_positions t
467
+ WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
468
+ ORDER BY m."createdAt" ASC
469
+ `,
470
+ args: [threadId, ...includeIds, maxPrev, maxNext],
471
+ });
472
+ return includeResult.rows?.map((row: any) => this.parseRow(row));
473
+ }
474
+
475
+ /**
476
+ * @deprecated use getMessagesPaginated instead for paginated results.
477
+ */
366
478
  public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
367
479
  public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
368
480
  public async getMessages({
369
481
  threadId,
370
482
  selectBy,
371
483
  format,
372
- }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
484
+ }: StorageGetMessagesArg & {
485
+ format?: 'v1' | 'v2';
486
+ }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
373
487
  try {
374
488
  const messages: MastraMessageV2[] = [];
375
489
  const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
376
490
 
377
- // If we have specific messages to select
378
491
  if (selectBy?.include?.length) {
379
- const includeIds = selectBy.include.map(i => i.id);
380
- const maxPrev = Math.max(...selectBy.include.map(i => i.withPreviousMessages || 0));
381
- const maxNext = Math.max(...selectBy.include.map(i => i.withNextMessages || 0));
382
-
383
- // Get messages around all specified IDs in one query using row numbers
384
- const includeResult = await this.client.execute({
385
- sql: `
386
- WITH numbered_messages AS (
387
- SELECT
388
- id,
389
- content,
390
- role,
391
- type,
392
- "createdAt",
393
- thread_id,
394
- ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
395
- FROM "${TABLE_MESSAGES}"
396
- WHERE thread_id = ?
397
- ),
398
- target_positions AS (
399
- SELECT row_num as target_pos
400
- FROM numbered_messages
401
- WHERE id IN (${includeIds.map(() => '?').join(', ')})
402
- )
403
- SELECT DISTINCT m.*
404
- FROM numbered_messages m
405
- CROSS JOIN target_positions t
406
- WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
407
- ORDER BY m."createdAt" ASC
408
- `,
409
- args: [threadId, ...includeIds, maxPrev, maxNext],
410
- });
411
-
412
- if (includeResult.rows) {
413
- messages.push(...includeResult.rows.map((row: any) => this.parseRow(row)));
492
+ const includeMessages = await this._getIncludedMessages(threadId, selectBy);
493
+ if (includeMessages) {
494
+ messages.push(...includeMessages);
414
495
  }
415
496
  }
416
497
 
417
- // Get remaining messages, excluding already fetched IDs
418
498
  const excludeIds = messages.map(m => m.id);
419
499
  const remainingSql = `
420
- SELECT
421
- id,
422
- content,
423
- role,
424
- type,
425
- "createdAt",
426
- thread_id
427
- FROM "${TABLE_MESSAGES}"
428
- WHERE thread_id = ?
429
- ${excludeIds.length ? `AND id NOT IN (${excludeIds.map(() => '?').join(', ')})` : ''}
430
- ORDER BY "createdAt" DESC
431
- LIMIT ?
432
- `;
500
+ SELECT id, content, role, type, "createdAt", thread_id
501
+ FROM "${TABLE_MESSAGES}"
502
+ WHERE thread_id = ?
503
+ ${excludeIds.length ? `AND id NOT IN (${excludeIds.map(() => '?').join(', ')})` : ''}
504
+ ORDER BY "createdAt" DESC LIMIT ?
505
+ `;
433
506
  const remainingArgs = [threadId, ...(excludeIds.length ? excludeIds : []), limit];
434
-
435
- const remainingResult = await this.client.execute({
436
- sql: remainingSql,
437
- args: remainingArgs,
438
- });
439
-
507
+ const remainingResult = await this.client.execute({ sql: remainingSql, args: remainingArgs });
440
508
  if (remainingResult.rows) {
441
509
  messages.push(...remainingResult.rows.map((row: any) => this.parseRow(row)));
442
510
  }
443
-
444
- // Sort all messages by creation date
445
511
  messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
446
-
447
512
  const list = new MessageList().add(messages, 'memory');
448
513
  if (format === `v2`) return list.get.all.v2();
449
514
  return list.get.all.v1();
@@ -453,6 +518,82 @@ export class LibSQLStore extends MastraStorage {
453
518
  }
454
519
  }
455
520
 
521
+ public async getMessagesPaginated(
522
+ args: StorageGetMessagesArg & {
523
+ format?: 'v1' | 'v2';
524
+ },
525
+ ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
526
+ const { threadId, format, selectBy } = args;
527
+ const { page = 0, perPage = 40, dateRange } = selectBy?.pagination || {};
528
+ const fromDate = dateRange?.start;
529
+ const toDate = dateRange?.end;
530
+
531
+ const messages: MastraMessageV2[] = [];
532
+
533
+ if (selectBy?.include?.length) {
534
+ const includeMessages = await this._getIncludedMessages(threadId, selectBy);
535
+ if (includeMessages) {
536
+ messages.push(...includeMessages);
537
+ }
538
+ }
539
+
540
+ try {
541
+ const currentOffset = page * perPage;
542
+
543
+ const conditions: string[] = [`thread_id = ?`];
544
+ const queryParams: InValue[] = [threadId];
545
+
546
+ if (fromDate) {
547
+ conditions.push(`"createdAt" >= ?`);
548
+ queryParams.push(fromDate.toISOString());
549
+ }
550
+ if (toDate) {
551
+ conditions.push(`"createdAt" <= ?`);
552
+ queryParams.push(toDate.toISOString());
553
+ }
554
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
555
+
556
+ const countResult = await this.client.execute({
557
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_MESSAGES} ${whereClause}`,
558
+ args: queryParams,
559
+ });
560
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
561
+
562
+ if (total === 0) {
563
+ return {
564
+ messages: [],
565
+ total: 0,
566
+ page,
567
+ perPage,
568
+ hasMore: false,
569
+ };
570
+ }
571
+
572
+ const dataResult = await this.client.execute({
573
+ sql: `SELECT id, content, role, type, "createdAt", thread_id FROM ${TABLE_MESSAGES} ${whereClause} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`,
574
+ args: [...queryParams, perPage, currentOffset],
575
+ });
576
+
577
+ messages.push(...(dataResult.rows || []).map((row: any) => this.parseRow(row)));
578
+
579
+ const messagesToReturn =
580
+ format === 'v1'
581
+ ? new MessageList().add(messages, 'memory').get.all.v1()
582
+ : new MessageList().add(messages, 'memory').get.all.v2();
583
+
584
+ return {
585
+ messages: messagesToReturn,
586
+ total,
587
+ page,
588
+ perPage,
589
+ hasMore: currentOffset + messages.length < total,
590
+ };
591
+ } catch (error) {
592
+ this.logger.error('Error getting paginated messages:', error as Error);
593
+ return { messages: [], total: 0, page, perPage, hasMore: false };
594
+ }
595
+ }
596
+
456
597
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
457
598
  async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
458
599
  async saveMessages({
@@ -526,6 +667,7 @@ export class LibSQLStore extends MastraStorage {
526
667
  };
527
668
  }
528
669
 
670
+ /** @deprecated use getEvals instead */
529
671
  async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
530
672
  try {
531
673
  const baseQuery = `SELECT * FROM ${TABLE_EVALS} WHERE agent_name = ?`;
@@ -552,120 +694,194 @@ export class LibSQLStore extends MastraStorage {
552
694
  }
553
695
  }
554
696
 
555
- // TODO: add types
556
- async getTraces(
557
- {
558
- name,
559
- scope,
697
+ async getEvals(
698
+ options: {
699
+ agentName?: string;
700
+ type?: 'test' | 'live';
701
+ } & PaginationArgs = {},
702
+ ): Promise<PaginationInfo & { evals: EvalRow[] }> {
703
+ const { agentName, type, page = 0, perPage = 100, dateRange } = options;
704
+ const fromDate = dateRange?.start;
705
+ const toDate = dateRange?.end;
706
+
707
+ const conditions: string[] = [];
708
+ const queryParams: InValue[] = [];
709
+
710
+ if (agentName) {
711
+ conditions.push(`agent_name = ?`);
712
+ queryParams.push(agentName);
713
+ }
714
+
715
+ if (type === 'test') {
716
+ conditions.push(`(test_info IS NOT NULL AND json_extract(test_info, '$.testPath') IS NOT NULL)`);
717
+ } else if (type === 'live') {
718
+ conditions.push(`(test_info IS NULL OR json_extract(test_info, '$.testPath') IS NULL)`);
719
+ }
720
+
721
+ if (fromDate) {
722
+ conditions.push(`created_at >= ?`);
723
+ queryParams.push(fromDate.toISOString());
724
+ }
725
+
726
+ if (toDate) {
727
+ conditions.push(`created_at <= ?`);
728
+ queryParams.push(toDate.toISOString());
729
+ }
730
+
731
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
732
+
733
+ const countResult = await this.client.execute({
734
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_EVALS} ${whereClause}`,
735
+ args: queryParams,
736
+ });
737
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
738
+
739
+ const currentOffset = page * perPage;
740
+ const hasMore = currentOffset + perPage < total;
741
+
742
+ if (total === 0) {
743
+ return {
744
+ evals: [],
745
+ total: 0,
746
+ page,
747
+ perPage,
748
+ hasMore: false,
749
+ };
750
+ }
751
+
752
+ const dataResult = await this.client.execute({
753
+ sql: `SELECT * FROM ${TABLE_EVALS} ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
754
+ args: [...queryParams, perPage, currentOffset],
755
+ });
756
+
757
+ return {
758
+ evals: dataResult.rows?.map(row => this.transformEvalRow(row)) ?? [],
759
+ total,
560
760
  page,
561
761
  perPage,
562
- attributes,
563
- filters,
564
- fromDate,
565
- toDate,
566
- }: {
762
+ hasMore,
763
+ };
764
+ }
765
+
766
+ /**
767
+ * @deprecated use getTracesPaginated instead.
768
+ */
769
+ public async getTraces(args: {
770
+ name?: string;
771
+ scope?: string;
772
+ page: number;
773
+ perPage: number;
774
+ attributes?: Record<string, string>;
775
+ filters?: Record<string, any>;
776
+ fromDate?: Date;
777
+ toDate?: Date;
778
+ }): Promise<Trace[]> {
779
+ if (args.fromDate || args.toDate) {
780
+ (args as any).dateRange = {
781
+ start: args.fromDate,
782
+ end: args.toDate,
783
+ };
784
+ }
785
+ const result = await this.getTracesPaginated(args);
786
+ return result.traces;
787
+ }
788
+
789
+ public async getTracesPaginated(
790
+ args: {
567
791
  name?: string;
568
792
  scope?: string;
569
- page: number;
570
- perPage: number;
571
793
  attributes?: Record<string, string>;
572
794
  filters?: Record<string, any>;
573
- fromDate?: Date;
574
- toDate?: Date;
575
- } = {
576
- page: 0,
577
- perPage: 100,
578
- },
579
- ): Promise<any[]> {
580
- const limit = perPage;
581
- const offset = page * perPage;
582
-
583
- const args: (string | number)[] = [];
584
-
795
+ } & PaginationArgs,
796
+ ): Promise<PaginationInfo & { traces: Trace[] }> {
797
+ const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
798
+ const fromDate = dateRange?.start;
799
+ const toDate = dateRange?.end;
800
+ const currentOffset = page * perPage;
801
+
802
+ const queryArgs: InValue[] = [];
585
803
  const conditions: string[] = [];
804
+
586
805
  if (name) {
587
- conditions.push("name LIKE CONCAT(?, '%')");
806
+ conditions.push('name LIKE ?');
807
+ queryArgs.push(`${name}%`);
588
808
  }
589
809
  if (scope) {
590
810
  conditions.push('scope = ?');
811
+ queryArgs.push(scope);
591
812
  }
592
813
  if (attributes) {
593
- Object.keys(attributes).forEach(key => {
594
- conditions.push(`attributes->>'$.${key}' = ?`);
814
+ Object.entries(attributes).forEach(([key, value]) => {
815
+ conditions.push(`json_extract(attributes, '$.${key}') = ?`);
816
+ queryArgs.push(value);
595
817
  });
596
818
  }
597
-
598
819
  if (filters) {
599
- Object.entries(filters).forEach(([key, _value]) => {
600
- conditions.push(`${key} = ?`);
820
+ Object.entries(filters).forEach(([key, value]) => {
821
+ conditions.push(`${parseSqlIdentifier(key, 'filter key')} = ?`);
822
+ queryArgs.push(value);
601
823
  });
602
824
  }
603
-
604
825
  if (fromDate) {
605
826
  conditions.push('createdAt >= ?');
827
+ queryArgs.push(fromDate.toISOString());
606
828
  }
607
-
608
829
  if (toDate) {
609
830
  conditions.push('createdAt <= ?');
831
+ queryArgs.push(toDate.toISOString());
610
832
  }
611
833
 
612
834
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
613
835
 
614
- if (name) {
615
- args.push(name);
616
- }
617
-
618
- if (scope) {
619
- args.push(scope);
620
- }
621
-
622
- if (attributes) {
623
- for (const [, value] of Object.entries(attributes)) {
624
- args.push(value);
625
- }
626
- }
627
-
628
- if (filters) {
629
- for (const [, value] of Object.entries(filters)) {
630
- args.push(value);
631
- }
632
- }
633
-
634
- if (fromDate) {
635
- args.push(fromDate.toISOString());
636
- }
836
+ const countResult = await this.client.execute({
837
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_TRACES} ${whereClause}`,
838
+ args: queryArgs,
839
+ });
840
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
637
841
 
638
- if (toDate) {
639
- args.push(toDate.toISOString());
842
+ if (total === 0) {
843
+ return {
844
+ traces: [],
845
+ total: 0,
846
+ page,
847
+ perPage,
848
+ hasMore: false,
849
+ };
640
850
  }
641
851
 
642
- args.push(limit, offset);
643
-
644
- const result = await this.client.execute({
852
+ const dataResult = await this.client.execute({
645
853
  sql: `SELECT * FROM ${TABLE_TRACES} ${whereClause} ORDER BY "startTime" DESC LIMIT ? OFFSET ?`,
646
- args,
854
+ args: [...queryArgs, perPage, currentOffset],
647
855
  });
648
856
 
649
- if (!result.rows) {
650
- return [];
651
- }
857
+ const traces =
858
+ dataResult.rows?.map(
859
+ row =>
860
+ ({
861
+ id: row.id,
862
+ parentSpanId: row.parentSpanId,
863
+ traceId: row.traceId,
864
+ name: row.name,
865
+ scope: row.scope,
866
+ kind: row.kind,
867
+ status: safelyParseJSON(row.status as string),
868
+ events: safelyParseJSON(row.events as string),
869
+ links: safelyParseJSON(row.links as string),
870
+ attributes: safelyParseJSON(row.attributes as string),
871
+ startTime: row.startTime,
872
+ endTime: row.endTime,
873
+ other: safelyParseJSON(row.other as string),
874
+ createdAt: row.createdAt,
875
+ }) as Trace,
876
+ ) ?? [];
652
877
 
653
- return result.rows.map(row => ({
654
- id: row.id,
655
- parentSpanId: row.parentSpanId,
656
- traceId: row.traceId,
657
- name: row.name,
658
- scope: row.scope,
659
- kind: row.kind,
660
- status: safelyParseJSON(row.status as string),
661
- events: safelyParseJSON(row.events as string),
662
- links: safelyParseJSON(row.links as string),
663
- attributes: safelyParseJSON(row.attributes as string),
664
- startTime: row.startTime,
665
- endTime: row.endTime,
666
- other: safelyParseJSON(row.other as string),
667
- createdAt: row.createdAt,
668
- })) as any;
878
+ return {
879
+ traces,
880
+ total,
881
+ page,
882
+ perPage,
883
+ hasMore: currentOffset + traces.length < total,
884
+ };
669
885
  }
670
886
 
671
887
  async getWorkflowRuns({