@mastra/upstash 0.10.2 → 0.10.3-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.
@@ -204,27 +204,30 @@ export class UpstashStore extends MastraStorage {
204
204
  return { key, processedRecord };
205
205
  }
206
206
 
207
+ /**
208
+ * @deprecated Use getEvals instead
209
+ */
207
210
  async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
208
211
  try {
209
- // Get all keys that match the evals table pattern
210
212
  const pattern = `${TABLE_EVALS}:*`;
211
213
  const keys = await this.scanKeys(pattern);
212
214
 
213
- // Fetch all eval records
214
- const evalRecords = await Promise.all(
215
- keys.map(async key => {
216
- const data = await this.redis.get<Record<string, any>>(key);
217
- return data;
218
- }),
219
- );
215
+ // Check if we have any keys before using pipeline
216
+ if (keys.length === 0) {
217
+ return [];
218
+ }
219
+
220
+ // Use pipeline for batch fetching to improve performance
221
+ const pipeline = this.redis.pipeline();
222
+ keys.forEach(key => pipeline.get(key));
223
+ const results = await pipeline.exec();
220
224
 
221
225
  // Filter by agent name and remove nulls
222
- const nonNullRecords = evalRecords.filter(
226
+ const nonNullRecords = results.filter(
223
227
  (record): record is Record<string, any> =>
224
228
  record !== null && typeof record === 'object' && 'agent_name' in record && record.agent_name === agentName,
225
229
  );
226
230
 
227
- // Apply additional filtering based on type
228
231
  let filteredEvals = nonNullRecords;
229
232
 
230
233
  if (type === 'test') {
@@ -271,103 +274,126 @@ export class UpstashStore extends MastraStorage {
271
274
  }
272
275
  }
273
276
 
274
- async getTraces(
275
- {
277
+ public async getTraces(args: {
278
+ name?: string;
279
+ scope?: string;
280
+ attributes?: Record<string, string>;
281
+ filters?: Record<string, any>;
282
+ page: number;
283
+ perPage?: number;
284
+ fromDate?: Date;
285
+ toDate?: Date;
286
+ }): Promise<any[]>;
287
+ public async getTraces(args: {
288
+ name?: string;
289
+ scope?: string;
290
+ page: number;
291
+ perPage?: number;
292
+ attributes?: Record<string, string>;
293
+ filters?: Record<string, any>;
294
+ fromDate?: Date;
295
+ toDate?: Date;
296
+ returnPaginationResults: true;
297
+ }): Promise<{
298
+ traces: any[];
299
+ total: number;
300
+ page: number;
301
+ perPage: number;
302
+ hasMore: boolean;
303
+ }>;
304
+ public async getTraces(args: {
305
+ name?: string;
306
+ scope?: string;
307
+ page: number;
308
+ perPage?: number;
309
+ attributes?: Record<string, string>;
310
+ filters?: Record<string, any>;
311
+ fromDate?: Date;
312
+ toDate?: Date;
313
+ returnPaginationResults?: boolean;
314
+ }): Promise<
315
+ | any[]
316
+ | {
317
+ traces: any[];
318
+ total: number;
319
+ page: number;
320
+ perPage: number;
321
+ hasMore: boolean;
322
+ }
323
+ > {
324
+ const {
276
325
  name,
277
326
  scope,
278
- page = 0,
279
- perPage = 100,
327
+ page,
328
+ perPage: perPageInput,
280
329
  attributes,
281
330
  filters,
282
331
  fromDate,
283
332
  toDate,
284
- }: {
285
- name?: string;
286
- scope?: string;
287
- page: number;
288
- perPage: number;
289
- attributes?: Record<string, string>;
290
- filters?: Record<string, any>;
291
- fromDate?: Date;
292
- toDate?: Date;
293
- } = {
294
- page: 0,
295
- perPage: 100,
296
- },
297
- ): Promise<any[]> {
333
+ returnPaginationResults,
334
+ } = args;
335
+
336
+ const perPage = perPageInput !== undefined ? perPageInput : 100;
337
+
298
338
  try {
299
- // Get all keys that match the traces table pattern
300
339
  const pattern = `${TABLE_TRACES}:*`;
301
340
  const keys = await this.scanKeys(pattern);
302
341
 
303
- // Fetch all trace records
304
- const traceRecords = await Promise.all(
305
- keys.map(async key => {
306
- const data = await this.redis.get<Record<string, any>>(key);
307
- return data;
308
- }),
309
- );
342
+ if (keys.length === 0) {
343
+ if (returnPaginationResults) {
344
+ return {
345
+ traces: [],
346
+ total: 0,
347
+ page,
348
+ perPage: perPage || 100,
349
+ hasMore: false,
350
+ };
351
+ }
352
+ return [];
353
+ }
354
+
355
+ const pipeline = this.redis.pipeline();
356
+ keys.forEach(key => pipeline.get(key));
357
+ const results = await pipeline.exec();
310
358
 
311
- // Filter out nulls and apply filters
312
- let filteredTraces = traceRecords.filter(
359
+ let filteredTraces = results.filter(
313
360
  (record): record is Record<string, any> => record !== null && typeof record === 'object',
314
361
  );
315
362
 
316
- // Apply name filter if provided
317
363
  if (name) {
318
364
  filteredTraces = filteredTraces.filter(record => record.name?.toLowerCase().startsWith(name.toLowerCase()));
319
365
  }
320
-
321
- // Apply scope filter if provided
322
366
  if (scope) {
323
367
  filteredTraces = filteredTraces.filter(record => record.scope === scope);
324
368
  }
325
-
326
- // Apply attributes filter if provided
327
369
  if (attributes) {
328
370
  filteredTraces = filteredTraces.filter(record => {
329
371
  const recordAttributes = record.attributes;
330
372
  if (!recordAttributes) return false;
331
-
332
- // Parse attributes if stored as string
333
373
  const parsedAttributes =
334
374
  typeof recordAttributes === 'string' ? JSON.parse(recordAttributes) : recordAttributes;
335
-
336
375
  return Object.entries(attributes).every(([key, value]) => parsedAttributes[key] === value);
337
376
  });
338
377
  }
339
-
340
- // Apply custom filters if provided
341
378
  if (filters) {
342
379
  filteredTraces = filteredTraces.filter(record =>
343
380
  Object.entries(filters).every(([key, value]) => record[key] === value),
344
381
  );
345
382
  }
346
-
347
- // Apply fromDate filter if provided
348
383
  if (fromDate) {
349
384
  filteredTraces = filteredTraces.filter(
350
385
  record => new Date(record.createdAt).getTime() >= new Date(fromDate).getTime(),
351
386
  );
352
387
  }
353
-
354
- // Apply toDate filter if provided
355
388
  if (toDate) {
356
389
  filteredTraces = filteredTraces.filter(
357
390
  record => new Date(record.createdAt).getTime() <= new Date(toDate).getTime(),
358
391
  );
359
392
  }
360
393
 
361
- // Sort traces by creation date (newest first)
362
394
  filteredTraces.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
363
395
 
364
- // Apply pagination
365
- const start = page * perPage;
366
- const end = start + perPage;
367
- const paginatedTraces = filteredTraces.slice(start, end);
368
-
369
- // Transform and return the traces
370
- return paginatedTraces.map(record => ({
396
+ const transformedTraces = filteredTraces.map(record => ({
371
397
  id: record.id,
372
398
  parentSpanId: record.parentSpanId,
373
399
  traceId: record.traceId,
@@ -383,8 +409,35 @@ export class UpstashStore extends MastraStorage {
383
409
  other: this.parseJSON(record.other),
384
410
  createdAt: this.ensureDate(record.createdAt),
385
411
  }));
412
+
413
+ const total = transformedTraces.length;
414
+ const resolvedPerPage = perPage || 100;
415
+ const start = page * resolvedPerPage;
416
+ const end = start + resolvedPerPage;
417
+ const paginatedTraces = transformedTraces.slice(start, end);
418
+ const hasMore = end < total;
419
+ if (returnPaginationResults) {
420
+ return {
421
+ traces: paginatedTraces,
422
+ total,
423
+ page,
424
+ perPage: resolvedPerPage,
425
+ hasMore,
426
+ };
427
+ } else {
428
+ return paginatedTraces;
429
+ }
386
430
  } catch (error) {
387
431
  console.error('Failed to get traces:', error);
432
+ if (returnPaginationResults) {
433
+ return {
434
+ traces: [],
435
+ total: 0,
436
+ page,
437
+ perPage: perPage || 100,
438
+ hasMore: false,
439
+ };
440
+ }
388
441
  return [];
389
442
  }
390
443
  }
@@ -450,24 +503,97 @@ export class UpstashStore extends MastraStorage {
450
503
  };
451
504
  }
452
505
 
453
- async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
454
- const pattern = `${TABLE_THREADS}:*`;
455
- const keys = await this.scanKeys(pattern);
456
- const threads = await Promise.all(
457
- keys.map(async key => {
458
- const data = await this.redis.get<StorageThreadType>(key);
459
- return data;
460
- }),
461
- );
462
-
463
- return threads
464
- .filter(thread => thread && thread.resourceId === resourceId)
465
- .map(thread => ({
466
- ...thread!,
467
- createdAt: this.ensureDate(thread!.createdAt)!,
468
- updatedAt: this.ensureDate(thread!.updatedAt)!,
469
- metadata: typeof thread!.metadata === 'string' ? JSON.parse(thread!.metadata) : thread!.metadata,
470
- }));
506
+ async getThreadsByResourceId(args: { resourceId: string }): Promise<StorageThreadType[]>;
507
+ async getThreadsByResourceId(args: { resourceId: string; page: number; perPage?: number }): Promise<{
508
+ threads: StorageThreadType[];
509
+ total: number;
510
+ page: number;
511
+ perPage: number;
512
+ hasMore: boolean;
513
+ }>;
514
+ async getThreadsByResourceId(args: { resourceId: string; page?: number; perPage?: number }): Promise<
515
+ | StorageThreadType[]
516
+ | {
517
+ threads: StorageThreadType[];
518
+ total: number;
519
+ page: number;
520
+ perPage: number;
521
+ hasMore: boolean;
522
+ }
523
+ > {
524
+ const resourceId: string = args.resourceId;
525
+ const page: number | undefined = args.page;
526
+ // Determine perPage only if page is actually provided. Otherwise, its value is not critical for the non-paginated path.
527
+ // If page is provided, perPage defaults to 100 if not specified.
528
+ const perPage: number = page !== undefined ? (args.perPage !== undefined ? args.perPage : 100) : 100;
529
+
530
+ try {
531
+ const pattern = `${TABLE_THREADS}:*`;
532
+ const keys = await this.scanKeys(pattern);
533
+
534
+ if (keys.length === 0) {
535
+ if (page !== undefined) {
536
+ return {
537
+ threads: [],
538
+ total: 0,
539
+ page,
540
+ perPage, // perPage is number here
541
+ hasMore: false,
542
+ };
543
+ }
544
+ return [];
545
+ }
546
+
547
+ const allThreads: StorageThreadType[] = [];
548
+ const pipeline = this.redis.pipeline();
549
+ keys.forEach(key => pipeline.get(key));
550
+ const results = await pipeline.exec();
551
+
552
+ for (let i = 0; i < results.length; i++) {
553
+ const thread = results[i] as StorageThreadType | null;
554
+ if (thread && thread.resourceId === resourceId) {
555
+ allThreads.push({
556
+ ...thread,
557
+ createdAt: this.ensureDate(thread.createdAt)!,
558
+ updatedAt: this.ensureDate(thread.updatedAt)!,
559
+ metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
560
+ });
561
+ }
562
+ }
563
+
564
+ allThreads.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
565
+
566
+ if (page !== undefined) {
567
+ // If page is defined, perPage is also a number (due to the defaulting logic above)
568
+ const total = allThreads.length;
569
+ const start = page * perPage;
570
+ const end = start + perPage;
571
+ const paginatedThreads = allThreads.slice(start, end);
572
+ const hasMore = end < total;
573
+ return {
574
+ threads: paginatedThreads,
575
+ total,
576
+ page,
577
+ perPage,
578
+ hasMore,
579
+ };
580
+ } else {
581
+ // page is undefined, return all threads
582
+ return allThreads;
583
+ }
584
+ } catch (error) {
585
+ console.error('Error in getThreadsByResourceId:', error);
586
+ if (page !== undefined) {
587
+ return {
588
+ threads: [],
589
+ total: 0,
590
+ page,
591
+ perPage, // perPage is number here
592
+ hasMore: false,
593
+ };
594
+ }
595
+ return [];
596
+ }
471
597
  }
472
598
 
473
599
  async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
@@ -550,17 +676,155 @@ export class UpstashStore extends MastraStorage {
550
676
  return list.get.all.v1();
551
677
  }
552
678
 
679
+ // Function overloads for different return types
553
680
  public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
554
681
  public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
682
+ public async getMessages(
683
+ args: StorageGetMessagesArg & {
684
+ format?: 'v1' | 'v2';
685
+ page: number;
686
+ perPage?: number;
687
+ fromDate?: Date;
688
+ toDate?: Date;
689
+ },
690
+ ): Promise<{
691
+ messages: MastraMessageV1[] | MastraMessageV2[];
692
+ total: number;
693
+ page: number;
694
+ perPage: number;
695
+ hasMore: boolean;
696
+ }>;
555
697
  public async getMessages({
556
698
  threadId,
557
699
  selectBy,
558
700
  format,
559
- }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
560
- const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
561
- const messageIds = new Set<string>();
701
+ page,
702
+ perPage = 40,
703
+ fromDate,
704
+ toDate,
705
+ }: StorageGetMessagesArg & {
706
+ format?: 'v1' | 'v2';
707
+ page?: number;
708
+ perPage?: number;
709
+ fromDate?: Date;
710
+ toDate?: Date;
711
+ }): Promise<
712
+ | MastraMessageV1[]
713
+ | MastraMessageV2[]
714
+ | {
715
+ messages: MastraMessageV1[] | MastraMessageV2[];
716
+ total: number;
717
+ page: number;
718
+ perPage: number;
719
+ hasMore: boolean;
720
+ }
721
+ > {
562
722
  const threadMessagesKey = this.getThreadMessagesKey(threadId);
563
723
 
724
+ const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
725
+ // If pagination is requested, use the new pagination logic
726
+ if (page !== undefined) {
727
+ try {
728
+ // Get all message IDs from the sorted set
729
+
730
+ if (allMessageIds.length === 0) {
731
+ return {
732
+ messages: [],
733
+ total: 0,
734
+ page,
735
+ perPage,
736
+ hasMore: false,
737
+ };
738
+ }
739
+
740
+ // Use pipeline to fetch all messages efficiently
741
+ const pipeline = this.redis.pipeline();
742
+ allMessageIds.forEach(id => pipeline.get(this.getMessageKey(threadId, id as string)));
743
+ const results = await pipeline.exec();
744
+
745
+ // Process messages and apply filters - handle undefined results from pipeline
746
+ let messages = results
747
+ .map((result: any) => result as MastraMessageV2 | null)
748
+ .filter((msg): msg is MastraMessageV2 => msg !== null) as (MastraMessageV2 & { _index?: number })[];
749
+
750
+ // Apply date filters if provided
751
+ if (fromDate) {
752
+ messages = messages.filter(msg => msg && new Date(msg.createdAt).getTime() >= fromDate.getTime());
753
+ }
754
+
755
+ if (toDate) {
756
+ messages = messages.filter(msg => msg && new Date(msg.createdAt).getTime() <= toDate.getTime());
757
+ }
758
+
759
+ // Sort messages by their position in the sorted set
760
+ messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
761
+
762
+ const total = messages.length;
763
+
764
+ // Apply pagination
765
+ const start = page * perPage;
766
+ const end = start + perPage;
767
+ const hasMore = end < total;
768
+ const paginatedMessages = messages.slice(start, end);
769
+
770
+ // Remove _index before returning and handle format conversion properly
771
+ const prepared = paginatedMessages
772
+ .filter(message => message !== null && message !== undefined)
773
+ .map(message => {
774
+ const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
775
+ return messageWithoutIndex as unknown as MastraMessageV1;
776
+ });
777
+
778
+ // Return pagination object with correct format
779
+ if (format === 'v2') {
780
+ // Convert V1 format back to V2 format
781
+ const v2Messages = prepared.map(msg => ({
782
+ ...msg,
783
+ content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
784
+ })) as MastraMessageV2[];
785
+
786
+ return {
787
+ messages: v2Messages,
788
+ total,
789
+ page,
790
+ perPage,
791
+ hasMore,
792
+ };
793
+ }
794
+
795
+ return {
796
+ messages: prepared,
797
+ total,
798
+ page,
799
+ perPage,
800
+ hasMore,
801
+ };
802
+ } catch (error) {
803
+ console.error('Failed to get paginated messages:', error);
804
+ return {
805
+ messages: [],
806
+ total: 0,
807
+ page,
808
+ perPage,
809
+ hasMore: false,
810
+ };
811
+ }
812
+ }
813
+
814
+ // Original logic for backward compatibility
815
+ // When selectBy is undefined or selectBy.last is undefined, get ALL messages (not just 40)
816
+ let limit: number;
817
+ if (typeof selectBy?.last === 'number') {
818
+ limit = Math.max(0, selectBy.last);
819
+ } else if (selectBy?.last === false) {
820
+ limit = 0;
821
+ } else {
822
+ // No limit specified - get all messages
823
+ limit = Number.MAX_SAFE_INTEGER;
824
+ }
825
+
826
+ const messageIds = new Set<string>();
827
+
564
828
  if (limit === 0 && !selectBy?.include) {
565
829
  return [];
566
830
  }
@@ -591,9 +855,16 @@ export class UpstashStore extends MastraStorage {
591
855
  }
592
856
  }
593
857
 
594
- // Then get the most recent messages
595
- const latestIds = limit === 0 ? [] : await this.redis.zrange(threadMessagesKey, -limit, -1);
596
- latestIds.forEach(id => messageIds.add(id as string));
858
+ // Then get the most recent messages (or all if no limit)
859
+ if (limit === Number.MAX_SAFE_INTEGER) {
860
+ // Get all messages
861
+ const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
862
+ allIds.forEach(id => messageIds.add(id as string));
863
+ } else if (limit > 0) {
864
+ // Get limited number of recent messages
865
+ const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
866
+ latestIds.forEach(id => messageIds.add(id as string));
867
+ }
597
868
 
598
869
  // Fetch all needed messages in parallel
599
870
  const messages = (
@@ -605,15 +876,27 @@ export class UpstashStore extends MastraStorage {
605
876
  ).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[];
606
877
 
607
878
  // Sort messages by their position in the sorted set
608
- const messageOrder = await this.redis.zrange(threadMessagesKey, 0, -1);
609
- messages.sort((a, b) => messageOrder.indexOf(a!.id) - messageOrder.indexOf(b!.id));
879
+ messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
880
+
881
+ // Remove _index before returning and handle format conversion properly
882
+ const prepared = messages
883
+ .filter(message => message !== null && message !== undefined)
884
+ .map(message => {
885
+ const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
886
+ return messageWithoutIndex as unknown as MastraMessageV1;
887
+ });
610
888
 
611
- // Remove _index before returning
612
- const prepared = messages.map(({ _index, ...message }) => message as unknown as MastraMessageV1);
889
+ // For backward compatibility, return messages directly without using MessageList
890
+ // since MessageList has deduplication logic that can cause issues
891
+ if (format === 'v2') {
892
+ // Convert V1 format back to V2 format
893
+ return prepared.map(msg => ({
894
+ ...msg,
895
+ content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
896
+ })) as MastraMessageV2[];
897
+ }
613
898
 
614
- const list = new MessageList().add(prepared, 'memory');
615
- if (format === `v2`) return list.get.all.v2();
616
- return list.get.all.v1();
899
+ return prepared;
617
900
  }
618
901
 
619
902
  async persistWorkflowSnapshot(params: {
@@ -657,6 +940,157 @@ export class UpstashStore extends MastraStorage {
657
940
  return data.snapshot;
658
941
  }
659
942
 
943
+ /**
944
+ * Get all evaluations with pagination and total count
945
+ * @param options Pagination and filtering options
946
+ * @returns Object with evals array and total count
947
+ */
948
+ async getEvals(options?: {
949
+ agentName?: string;
950
+ type?: 'test' | 'live';
951
+ page?: number;
952
+ perPage?: number;
953
+ limit?: number;
954
+ offset?: number;
955
+ fromDate?: Date;
956
+ toDate?: Date;
957
+ }): Promise<{
958
+ evals: EvalRow[];
959
+ total: number;
960
+ page?: number;
961
+ perPage?: number;
962
+ hasMore?: boolean;
963
+ }> {
964
+ try {
965
+ // Default pagination parameters
966
+ const page = options?.page ?? 0;
967
+ const perPage = options?.perPage ?? 100;
968
+ const limit = options?.limit;
969
+ const offset = options?.offset;
970
+
971
+ // Get all keys that match the evals table pattern using cursor-based scanning
972
+ const pattern = `${TABLE_EVALS}:*`;
973
+ const keys = await this.scanKeys(pattern);
974
+
975
+ // Check if we have any keys before using pipeline
976
+ if (keys.length === 0) {
977
+ return {
978
+ evals: [],
979
+ total: 0,
980
+ page: options?.page ?? 0,
981
+ perPage: options?.perPage ?? 100,
982
+ hasMore: false,
983
+ };
984
+ }
985
+
986
+ // Use pipeline for batch fetching to improve performance
987
+ const pipeline = this.redis.pipeline();
988
+ keys.forEach(key => pipeline.get(key));
989
+ const results = await pipeline.exec();
990
+
991
+ // Process results and apply filters
992
+ let filteredEvals = results
993
+ .map((result: any) => result as Record<string, any> | null)
994
+ .filter((record): record is Record<string, any> => record !== null && typeof record === 'object');
995
+
996
+ // Apply agent name filter if provided
997
+ if (options?.agentName) {
998
+ filteredEvals = filteredEvals.filter(record => record.agent_name === options.agentName);
999
+ }
1000
+
1001
+ // Apply type filter if provided
1002
+ if (options?.type === 'test') {
1003
+ filteredEvals = filteredEvals.filter(record => {
1004
+ if (!record.test_info) return false;
1005
+
1006
+ try {
1007
+ if (typeof record.test_info === 'string') {
1008
+ const parsedTestInfo = JSON.parse(record.test_info);
1009
+ return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
1010
+ }
1011
+ return typeof record.test_info === 'object' && 'testPath' in record.test_info;
1012
+ } catch {
1013
+ return false;
1014
+ }
1015
+ });
1016
+ } else if (options?.type === 'live') {
1017
+ filteredEvals = filteredEvals.filter(record => {
1018
+ if (!record.test_info) return true;
1019
+
1020
+ try {
1021
+ if (typeof record.test_info === 'string') {
1022
+ const parsedTestInfo = JSON.parse(record.test_info);
1023
+ return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
1024
+ }
1025
+ return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
1026
+ } catch {
1027
+ return true;
1028
+ }
1029
+ });
1030
+ }
1031
+
1032
+ // Apply date filters if provided
1033
+ if (options?.fromDate) {
1034
+ filteredEvals = filteredEvals.filter(record => {
1035
+ const createdAt = new Date(record.created_at || record.createdAt || 0);
1036
+ return createdAt.getTime() >= options.fromDate!.getTime();
1037
+ });
1038
+ }
1039
+
1040
+ if (options?.toDate) {
1041
+ filteredEvals = filteredEvals.filter(record => {
1042
+ const createdAt = new Date(record.created_at || record.createdAt || 0);
1043
+ return createdAt.getTime() <= options.toDate!.getTime();
1044
+ });
1045
+ }
1046
+
1047
+ // Sort by creation date (newest first)
1048
+ filteredEvals.sort((a, b) => {
1049
+ const dateA = new Date(a.created_at || a.createdAt || 0).getTime();
1050
+ const dateB = new Date(b.created_at || b.createdAt || 0).getTime();
1051
+ return dateB - dateA;
1052
+ });
1053
+
1054
+ const total = filteredEvals.length;
1055
+
1056
+ // Apply pagination - support both page/perPage and limit/offset patterns
1057
+ let paginatedEvals: Record<string, any>[];
1058
+ let hasMore = false;
1059
+
1060
+ if (limit !== undefined && offset !== undefined) {
1061
+ // Offset-based pagination
1062
+ paginatedEvals = filteredEvals.slice(offset, offset + limit);
1063
+ hasMore = offset + limit < total;
1064
+ } else {
1065
+ // Page-based pagination
1066
+ const start = page * perPage;
1067
+ const end = start + perPage;
1068
+ paginatedEvals = filteredEvals.slice(start, end);
1069
+ hasMore = end < total;
1070
+ }
1071
+
1072
+ // Transform to EvalRow format
1073
+ const evals = paginatedEvals.map(record => this.transformEvalRecord(record));
1074
+
1075
+ return {
1076
+ evals,
1077
+ total,
1078
+ page: limit !== undefined ? undefined : page,
1079
+ perPage: limit !== undefined ? undefined : perPage,
1080
+ hasMore,
1081
+ };
1082
+ } catch (error) {
1083
+ console.error('Failed to get evals:', error);
1084
+ return {
1085
+ evals: [],
1086
+ total: 0,
1087
+ page: options?.page ?? 0,
1088
+ perPage: options?.perPage ?? 100,
1089
+ hasMore: false,
1090
+ };
1091
+ }
1092
+ }
1093
+
660
1094
  async getWorkflowRuns(
661
1095
  {
662
1096
  namespace,
@@ -693,24 +1127,25 @@ export class UpstashStore extends MastraStorage {
693
1127
  }
694
1128
  const keys = await this.scanKeys(pattern);
695
1129
 
696
- // Get all workflow data
697
- const workflows = await Promise.all(
698
- keys.map(async key => {
699
- const data = await this.redis.get<{
700
- workflow_name: string;
701
- run_id: string;
702
- snapshot: WorkflowRunState | string;
703
- createdAt: string | Date;
704
- updatedAt: string | Date;
705
- resourceId: string;
706
- }>(key);
707
- return data;
708
- }),
709
- );
1130
+ // Check if we have any keys before using pipeline
1131
+ if (keys.length === 0) {
1132
+ return { runs: [], total: 0 };
1133
+ }
710
1134
 
711
- // Filter and transform results
712
- let runs = workflows
713
- .filter(w => w !== null)
1135
+ // Use pipeline for batch fetching to improve performance
1136
+ const pipeline = this.redis.pipeline();
1137
+ keys.forEach(key => pipeline.get(key));
1138
+ const results = await pipeline.exec();
1139
+
1140
+ // Filter and transform results - handle undefined results
1141
+ let runs = results
1142
+ .map((result: any) => result as Record<string, any> | null)
1143
+ .filter(
1144
+ (record): record is Record<string, any> =>
1145
+ record !== null && record !== undefined && typeof record === 'object' && 'workflow_name' in record,
1146
+ )
1147
+ // Only filter by workflowName if it was specifically requested
1148
+ .filter(record => !workflowName || record.workflow_name === workflowName)
714
1149
  .map(w => this.parseWorkflowRun(w!))
715
1150
  .filter(w => {
716
1151
  if (fromDate && w.createdAt < fromDate) return false;