@mastra/upstash 0.12.1 → 0.12.2

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.
@@ -1,17 +1,7 @@
1
- import { MessageList } from '@mastra/core/agent';
2
1
  import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
3
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
- import type { MetricResult, TestInfo } from '@mastra/core/eval';
5
2
  import type { StorageThreadType, MastraMessageV1 } from '@mastra/core/memory';
6
- import {
7
- MastraStorage,
8
- TABLE_MESSAGES,
9
- TABLE_THREADS,
10
- TABLE_RESOURCES,
11
- TABLE_WORKFLOW_SNAPSHOT,
12
- TABLE_EVALS,
13
- TABLE_TRACES,
14
- } from '@mastra/core/storage';
3
+ import type { ScoreRowData } from '@mastra/core/scores';
4
+ import { MastraStorage } from '@mastra/core/storage';
15
5
  import type {
16
6
  TABLE_NAMES,
17
7
  StorageColumn,
@@ -23,9 +13,18 @@ import type {
23
13
  PaginationInfo,
24
14
  PaginationArgs,
25
15
  StorageGetTracesArg,
16
+ StoragePagination,
17
+ StorageDomains,
26
18
  } from '@mastra/core/storage';
19
+
27
20
  import type { WorkflowRunState } from '@mastra/core/workflows';
28
21
  import { Redis } from '@upstash/redis';
22
+ import { StoreLegacyEvalsUpstash } from './domains/legacy-evals';
23
+ import { StoreMemoryUpstash } from './domains/memory';
24
+ import { StoreOperationsUpstash } from './domains/operations';
25
+ import { ScoresUpstash } from './domains/scores';
26
+ import { TracesUpstash } from './domains/traces';
27
+ import { WorkflowsUpstash } from './domains/workflows';
29
28
 
30
29
  export interface UpstashConfig {
31
30
  url: string;
@@ -34,6 +33,7 @@ export interface UpstashConfig {
34
33
 
35
34
  export class UpstashStore extends MastraStorage {
36
35
  private redis: Redis;
36
+ stores: StorageDomains;
37
37
 
38
38
  constructor(config: UpstashConfig) {
39
39
  super({ name: 'Upstash' });
@@ -41,279 +41,59 @@ export class UpstashStore extends MastraStorage {
41
41
  url: config.url,
42
42
  token: config.token,
43
43
  });
44
- }
45
44
 
46
- public get supports(): {
47
- selectByIncludeResourceScope: boolean;
48
- resourceWorkingMemory: boolean;
49
- } {
50
- return {
51
- selectByIncludeResourceScope: true,
52
- resourceWorkingMemory: true,
45
+ const operations = new StoreOperationsUpstash({ client: this.redis });
46
+ const traces = new TracesUpstash({ client: this.redis, operations });
47
+ const scores = new ScoresUpstash({ client: this.redis, operations });
48
+ const workflows = new WorkflowsUpstash({ client: this.redis, operations });
49
+ const memory = new StoreMemoryUpstash({ client: this.redis, operations });
50
+ const legacyEvals = new StoreLegacyEvalsUpstash({ client: this.redis, operations });
51
+
52
+ this.stores = {
53
+ operations,
54
+ traces,
55
+ scores,
56
+ workflows,
57
+ memory,
58
+ legacyEvals,
53
59
  };
54
60
  }
55
61
 
56
- private transformEvalRecord(record: Record<string, any>): EvalRow {
57
- // Parse JSON strings if needed
58
- let result = record.result;
59
- if (typeof result === 'string') {
60
- try {
61
- result = JSON.parse(result);
62
- } catch {
63
- console.warn('Failed to parse result JSON:');
64
- }
65
- }
66
-
67
- let testInfo = record.test_info;
68
- if (typeof testInfo === 'string') {
69
- try {
70
- testInfo = JSON.parse(testInfo);
71
- } catch {
72
- console.warn('Failed to parse test_info JSON:');
73
- }
74
- }
75
-
62
+ public get supports() {
76
63
  return {
77
- agentName: record.agent_name,
78
- input: record.input,
79
- output: record.output,
80
- result: result as MetricResult,
81
- metricName: record.metric_name,
82
- instructions: record.instructions,
83
- testInfo: testInfo as TestInfo | undefined,
84
- globalRunId: record.global_run_id,
85
- runId: record.run_id,
86
- createdAt:
87
- typeof record.created_at === 'string'
88
- ? record.created_at
89
- : record.created_at instanceof Date
90
- ? record.created_at.toISOString()
91
- : new Date().toISOString(),
64
+ selectByIncludeResourceScope: true,
65
+ resourceWorkingMemory: true,
66
+ hasColumn: false,
67
+ createTable: false,
92
68
  };
93
69
  }
94
70
 
95
- private parseJSON(value: any): any {
96
- if (typeof value === 'string') {
97
- try {
98
- return JSON.parse(value);
99
- } catch {
100
- return value;
101
- }
102
- }
103
- return value;
104
- }
105
-
106
- private getKey(tableName: TABLE_NAMES, keys: Record<string, any>): string {
107
- const keyParts = Object.entries(keys)
108
- .filter(([_, value]) => value !== undefined)
109
- .map(([key, value]) => `${key}:${value}`);
110
- return `${tableName}:${keyParts.join(':')}`;
111
- }
112
-
113
- /**
114
- * Scans for keys matching the given pattern using SCAN and returns them as an array.
115
- * @param pattern Redis key pattern, e.g. "table:*"
116
- * @param batchSize Number of keys to scan per batch (default: 1000)
117
- */
118
- private async scanKeys(pattern: string, batchSize = 10000): Promise<string[]> {
119
- let cursor = '0';
120
- let keys: string[] = [];
121
- do {
122
- // Upstash: scan(cursor, { match, count })
123
- const [nextCursor, batch] = await this.redis.scan(cursor, {
124
- match: pattern,
125
- count: batchSize,
126
- });
127
- keys.push(...batch);
128
- cursor = nextCursor;
129
- } while (cursor !== '0');
130
- return keys;
131
- }
132
-
133
71
  /**
134
- * Deletes all keys matching the given pattern using SCAN and DEL in batches.
135
- * @param pattern Redis key pattern, e.g. "table:*"
136
- * @param batchSize Number of keys to delete per batch (default: 1000)
72
+ * @deprecated Use getEvals instead
137
73
  */
138
- private async scanAndDelete(pattern: string, batchSize = 10000): Promise<number> {
139
- let cursor = '0';
140
- let totalDeleted = 0;
141
- do {
142
- const [nextCursor, keys] = await this.redis.scan(cursor, {
143
- match: pattern,
144
- count: batchSize,
145
- });
146
- if (keys.length > 0) {
147
- await this.redis.del(...keys);
148
- totalDeleted += keys.length;
149
- }
150
- cursor = nextCursor;
151
- } while (cursor !== '0');
152
- return totalDeleted;
153
- }
154
-
155
- private getMessageKey(threadId: string, messageId: string): string {
156
- const key = this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
157
- return key;
158
- }
159
-
160
- private getThreadMessagesKey(threadId: string): string {
161
- return `thread:${threadId}:messages`;
162
- }
163
-
164
- private parseWorkflowRun(row: any): WorkflowRun {
165
- let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
166
- if (typeof parsedSnapshot === 'string') {
167
- try {
168
- parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
169
- } catch (e) {
170
- // If parsing fails, return the raw snapshot string
171
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
172
- }
173
- }
174
-
175
- return {
176
- workflowName: row.workflow_name,
177
- runId: row.run_id,
178
- snapshot: parsedSnapshot,
179
- createdAt: this.ensureDate(row.createdAt)!,
180
- updatedAt: this.ensureDate(row.updatedAt)!,
181
- resourceId: row.resourceId,
182
- };
183
- }
184
-
185
- private processRecord(tableName: TABLE_NAMES, record: Record<string, any>) {
186
- let key: string;
187
-
188
- if (tableName === TABLE_MESSAGES) {
189
- // For messages, use threadId as the primary key component
190
- key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
191
- } else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
192
- key = this.getKey(tableName, {
193
- namespace: record.namespace || 'workflows',
194
- workflow_name: record.workflow_name,
195
- run_id: record.run_id,
196
- ...(record.resourceId ? { resourceId: record.resourceId } : {}),
197
- });
198
- } else if (tableName === TABLE_EVALS) {
199
- key = this.getKey(tableName, { id: record.run_id });
200
- } else {
201
- key = this.getKey(tableName, { id: record.id });
202
- }
203
-
204
- // Convert dates to ISO strings before storing
205
- const processedRecord = {
206
- ...record,
207
- createdAt: this.serializeDate(record.createdAt),
208
- updatedAt: this.serializeDate(record.updatedAt),
209
- };
210
-
211
- return { key, processedRecord };
74
+ async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
75
+ return this.stores.legacyEvals.getEvalsByAgentName(agentName, type);
212
76
  }
213
77
 
214
78
  /**
215
- * @deprecated Use getEvals instead
79
+ * Get all evaluations with pagination and total count
80
+ * @param options Pagination and filtering options
81
+ * @returns Object with evals array and total count
216
82
  */
217
- async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
218
- try {
219
- const pattern = `${TABLE_EVALS}:*`;
220
- const keys = await this.scanKeys(pattern);
221
-
222
- // Check if we have any keys before using pipeline
223
- if (keys.length === 0) {
224
- return [];
225
- }
226
-
227
- // Use pipeline for batch fetching to improve performance
228
- const pipeline = this.redis.pipeline();
229
- keys.forEach(key => pipeline.get(key));
230
- const results = await pipeline.exec();
231
-
232
- // Filter by agent name and remove nulls
233
- const nonNullRecords = results.filter(
234
- (record): record is Record<string, any> =>
235
- record !== null && typeof record === 'object' && 'agent_name' in record && record.agent_name === agentName,
236
- );
237
-
238
- let filteredEvals = nonNullRecords;
239
-
240
- if (type === 'test') {
241
- filteredEvals = filteredEvals.filter(record => {
242
- if (!record.test_info) return false;
243
-
244
- // Handle test_info as a JSON string
245
- try {
246
- if (typeof record.test_info === 'string') {
247
- const parsedTestInfo = JSON.parse(record.test_info);
248
- return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
249
- }
250
-
251
- // Handle test_info as an object
252
- return typeof record.test_info === 'object' && 'testPath' in record.test_info;
253
- } catch {
254
- return false;
255
- }
256
- });
257
- } else if (type === 'live') {
258
- filteredEvals = filteredEvals.filter(record => {
259
- if (!record.test_info) return true;
260
-
261
- // Handle test_info as a JSON string
262
- try {
263
- if (typeof record.test_info === 'string') {
264
- const parsedTestInfo = JSON.parse(record.test_info);
265
- return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
266
- }
267
-
268
- // Handle test_info as an object
269
- return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
270
- } catch {
271
- return true;
272
- }
273
- });
274
- }
275
-
276
- // Transform to EvalRow format
277
- return filteredEvals.map(record => this.transformEvalRecord(record));
278
- } catch (error) {
279
- const mastraError = new MastraError(
280
- {
281
- id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_BY_AGENT_NAME_FAILED',
282
- domain: ErrorDomain.STORAGE,
283
- category: ErrorCategory.THIRD_PARTY,
284
- details: { agentName },
285
- },
286
- error,
287
- );
288
- this.logger?.trackException(mastraError);
289
- this.logger.error(mastraError.toString());
290
- return [];
291
- }
83
+ async getEvals(
84
+ options: {
85
+ agentName?: string;
86
+ type?: 'test' | 'live';
87
+ } & PaginationArgs,
88
+ ): Promise<PaginationInfo & { evals: EvalRow[] }> {
89
+ return this.stores.legacyEvals.getEvals(options);
292
90
  }
293
91
 
294
92
  /**
295
93
  * @deprecated use getTracesPaginated instead
296
94
  */
297
95
  public async getTraces(args: StorageGetTracesArg): Promise<any[]> {
298
- if (args.fromDate || args.toDate) {
299
- (args as any).dateRange = {
300
- start: args.fromDate,
301
- end: args.toDate,
302
- };
303
- }
304
- try {
305
- const { traces } = await this.getTracesPaginated(args);
306
- return traces;
307
- } catch (error) {
308
- throw new MastraError(
309
- {
310
- id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_FAILED',
311
- domain: ErrorDomain.STORAGE,
312
- category: ErrorCategory.THIRD_PARTY,
313
- },
314
- error,
315
- );
316
- }
96
+ return this.stores.traces.getTraces(args);
317
97
  }
318
98
 
319
99
  public async getTracesPaginated(
@@ -324,119 +104,11 @@ export class UpstashStore extends MastraStorage {
324
104
  filters?: Record<string, any>;
325
105
  } & PaginationArgs,
326
106
  ): Promise<PaginationInfo & { traces: any[] }> {
327
- const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
328
- const fromDate = dateRange?.start;
329
- const toDate = dateRange?.end;
330
-
331
- try {
332
- const pattern = `${TABLE_TRACES}:*`;
333
- const keys = await this.scanKeys(pattern);
334
-
335
- if (keys.length === 0) {
336
- return {
337
- traces: [],
338
- total: 0,
339
- page,
340
- perPage: perPage || 100,
341
- hasMore: false,
342
- };
343
- }
344
-
345
- const pipeline = this.redis.pipeline();
346
- keys.forEach(key => pipeline.get(key));
347
- const results = await pipeline.exec();
348
-
349
- let filteredTraces = results.filter(
350
- (record): record is Record<string, any> => record !== null && typeof record === 'object',
351
- );
352
-
353
- if (name) {
354
- filteredTraces = filteredTraces.filter(record => record.name?.toLowerCase().startsWith(name.toLowerCase()));
355
- }
356
- if (scope) {
357
- filteredTraces = filteredTraces.filter(record => record.scope === scope);
358
- }
359
- if (attributes) {
360
- filteredTraces = filteredTraces.filter(record => {
361
- const recordAttributes = record.attributes;
362
- if (!recordAttributes) return false;
363
- const parsedAttributes =
364
- typeof recordAttributes === 'string' ? JSON.parse(recordAttributes) : recordAttributes;
365
- return Object.entries(attributes).every(([key, value]) => parsedAttributes[key] === value);
366
- });
367
- }
368
- if (filters) {
369
- filteredTraces = filteredTraces.filter(record =>
370
- Object.entries(filters).every(([key, value]) => record[key] === value),
371
- );
372
- }
373
- if (fromDate) {
374
- filteredTraces = filteredTraces.filter(
375
- record => new Date(record.createdAt).getTime() >= new Date(fromDate).getTime(),
376
- );
377
- }
378
- if (toDate) {
379
- filteredTraces = filteredTraces.filter(
380
- record => new Date(record.createdAt).getTime() <= new Date(toDate).getTime(),
381
- );
382
- }
383
-
384
- filteredTraces.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
385
-
386
- const transformedTraces = filteredTraces.map(record => ({
387
- id: record.id,
388
- parentSpanId: record.parentSpanId,
389
- traceId: record.traceId,
390
- name: record.name,
391
- scope: record.scope,
392
- kind: record.kind,
393
- status: this.parseJSON(record.status),
394
- events: this.parseJSON(record.events),
395
- links: this.parseJSON(record.links),
396
- attributes: this.parseJSON(record.attributes),
397
- startTime: record.startTime,
398
- endTime: record.endTime,
399
- other: this.parseJSON(record.other),
400
- createdAt: this.ensureDate(record.createdAt),
401
- }));
402
-
403
- const total = transformedTraces.length;
404
- const resolvedPerPage = perPage || 100;
405
- const start = page * resolvedPerPage;
406
- const end = start + resolvedPerPage;
407
- const paginatedTraces = transformedTraces.slice(start, end);
408
- const hasMore = end < total;
107
+ return this.stores.traces.getTracesPaginated(args);
108
+ }
409
109
 
410
- return {
411
- traces: paginatedTraces,
412
- total,
413
- page,
414
- perPage: resolvedPerPage,
415
- hasMore,
416
- };
417
- } catch (error) {
418
- const mastraError = new MastraError(
419
- {
420
- id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_PAGINATED_FAILED',
421
- domain: ErrorDomain.STORAGE,
422
- category: ErrorCategory.THIRD_PARTY,
423
- details: {
424
- name: args.name || '',
425
- scope: args.scope || '',
426
- },
427
- },
428
- error,
429
- );
430
- this.logger?.trackException(mastraError);
431
- this.logger.error(mastraError.toString());
432
- return {
433
- traces: [],
434
- total: 0,
435
- page,
436
- perPage: perPage || 100,
437
- hasMore: false,
438
- };
439
- }
110
+ async batchTraceInsert(args: { records: Record<string, any>[] }): Promise<void> {
111
+ return this.stores.traces.batchTraceInsert(args);
440
112
  }
441
113
 
442
114
  async createTable({
@@ -446,23 +118,7 @@ export class UpstashStore extends MastraStorage {
446
118
  tableName: TABLE_NAMES;
447
119
  schema: Record<string, StorageColumn>;
448
120
  }): Promise<void> {
449
- // Redis is schemaless, so we don't need to create tables
450
- // But we can store the schema for reference
451
- try {
452
- await this.redis.set(`schema:${tableName}`, schema);
453
- } catch (error) {
454
- throw new MastraError(
455
- {
456
- id: 'STORAGE_UPSTASH_STORAGE_CREATE_TABLE_FAILED',
457
- domain: ErrorDomain.STORAGE,
458
- category: ErrorCategory.THIRD_PARTY,
459
- details: {
460
- tableName,
461
- },
462
- },
463
- error,
464
- );
465
- }
121
+ return this.stores.operations.createTable({ tableName, schema });
466
122
  }
467
123
 
468
124
  /**
@@ -471,254 +127,55 @@ export class UpstashStore extends MastraStorage {
471
127
  * @param schema Schema of the table
472
128
  * @param ifNotExists Array of column names to add if they don't exist
473
129
  */
474
- async alterTable(_args: {
130
+ async alterTable(args: {
475
131
  tableName: TABLE_NAMES;
476
132
  schema: Record<string, StorageColumn>;
477
133
  ifNotExists: string[];
478
134
  }): Promise<void> {
479
- // Nothing to do here, Redis is schemaless
135
+ return this.stores.operations.alterTable(args);
480
136
  }
481
137
 
482
138
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
483
- const pattern = `${tableName}:*`;
484
- try {
485
- await this.scanAndDelete(pattern);
486
- } catch (error) {
487
- throw new MastraError(
488
- {
489
- id: 'STORAGE_UPSTASH_STORAGE_CLEAR_TABLE_FAILED',
490
- domain: ErrorDomain.STORAGE,
491
- category: ErrorCategory.THIRD_PARTY,
492
- details: {
493
- tableName,
494
- },
495
- },
496
- error,
497
- );
498
- }
139
+ return this.stores.operations.clearTable({ tableName });
499
140
  }
500
141
 
501
- async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
502
- const { key, processedRecord } = this.processRecord(tableName, record);
142
+ async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
143
+ return this.stores.operations.dropTable({ tableName });
144
+ }
503
145
 
504
- try {
505
- await this.redis.set(key, processedRecord);
506
- } catch (error) {
507
- throw new MastraError(
508
- {
509
- id: 'STORAGE_UPSTASH_STORAGE_INSERT_FAILED',
510
- domain: ErrorDomain.STORAGE,
511
- category: ErrorCategory.THIRD_PARTY,
512
- details: {
513
- tableName,
514
- },
515
- },
516
- error,
517
- );
518
- }
146
+ async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
147
+ return this.stores.operations.insert({ tableName, record });
519
148
  }
520
149
 
521
150
  async batchInsert(input: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
522
- const { tableName, records } = input;
523
- if (!records.length) return;
524
-
525
- const batchSize = 1000;
526
- try {
527
- for (let i = 0; i < records.length; i += batchSize) {
528
- const batch = records.slice(i, i + batchSize);
529
- const pipeline = this.redis.pipeline();
530
- for (const record of batch) {
531
- const { key, processedRecord } = this.processRecord(tableName, record);
532
- pipeline.set(key, processedRecord);
533
- }
534
- await pipeline.exec();
535
- }
536
- } catch (error) {
537
- throw new MastraError(
538
- {
539
- id: 'STORAGE_UPSTASH_STORAGE_BATCH_INSERT_FAILED',
540
- domain: ErrorDomain.STORAGE,
541
- category: ErrorCategory.THIRD_PARTY,
542
- details: {
543
- tableName,
544
- },
545
- },
546
- error,
547
- );
548
- }
151
+ return this.stores.operations.batchInsert(input);
549
152
  }
550
153
 
551
154
  async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
552
- const key = this.getKey(tableName, keys);
553
- try {
554
- const data = await this.redis.get<R>(key);
555
- return data || null;
556
- } catch (error) {
557
- throw new MastraError(
558
- {
559
- id: 'STORAGE_UPSTASH_STORAGE_LOAD_FAILED',
560
- domain: ErrorDomain.STORAGE,
561
- category: ErrorCategory.THIRD_PARTY,
562
- details: {
563
- tableName,
564
- },
565
- },
566
- error,
567
- );
568
- }
155
+ return this.stores.operations.load<R>({ tableName, keys });
569
156
  }
570
157
 
571
158
  async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
572
- try {
573
- const thread = await this.load<StorageThreadType>({
574
- tableName: TABLE_THREADS,
575
- keys: { id: threadId },
576
- });
577
-
578
- if (!thread) return null;
579
-
580
- return {
581
- ...thread,
582
- createdAt: this.ensureDate(thread.createdAt)!,
583
- updatedAt: this.ensureDate(thread.updatedAt)!,
584
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
585
- };
586
- } catch (error) {
587
- throw new MastraError(
588
- {
589
- id: 'STORAGE_UPSTASH_STORAGE_GET_THREAD_BY_ID_FAILED',
590
- domain: ErrorDomain.STORAGE,
591
- category: ErrorCategory.THIRD_PARTY,
592
- details: {
593
- threadId,
594
- },
595
- },
596
- error,
597
- );
598
- }
159
+ return this.stores.memory.getThreadById({ threadId });
599
160
  }
600
161
 
601
162
  /**
602
163
  * @deprecated use getThreadsByResourceIdPaginated instead
603
164
  */
604
165
  async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
605
- try {
606
- const pattern = `${TABLE_THREADS}:*`;
607
- const keys = await this.scanKeys(pattern);
608
-
609
- if (keys.length === 0) {
610
- return [];
611
- }
612
-
613
- const allThreads: StorageThreadType[] = [];
614
- const pipeline = this.redis.pipeline();
615
- keys.forEach(key => pipeline.get(key));
616
- const results = await pipeline.exec();
617
-
618
- for (let i = 0; i < results.length; i++) {
619
- const thread = results[i] as StorageThreadType | null;
620
- if (thread && thread.resourceId === resourceId) {
621
- allThreads.push({
622
- ...thread,
623
- createdAt: this.ensureDate(thread.createdAt)!,
624
- updatedAt: this.ensureDate(thread.updatedAt)!,
625
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
626
- });
627
- }
628
- }
629
-
630
- allThreads.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
631
- return allThreads;
632
- } catch (error) {
633
- const mastraError = new MastraError(
634
- {
635
- id: 'STORAGE_UPSTASH_STORAGE_GET_THREADS_BY_RESOURCE_ID_FAILED',
636
- domain: ErrorDomain.STORAGE,
637
- category: ErrorCategory.THIRD_PARTY,
638
- details: {
639
- resourceId,
640
- },
641
- },
642
- error,
643
- );
644
- this.logger?.trackException(mastraError);
645
- this.logger.error(mastraError.toString());
646
- return [];
647
- }
166
+ return this.stores.memory.getThreadsByResourceId({ resourceId });
648
167
  }
649
168
 
650
- public async getThreadsByResourceIdPaginated(
651
- args: {
652
- resourceId: string;
653
- } & PaginationArgs,
654
- ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
655
- const { resourceId, page = 0, perPage = 100 } = args;
656
-
657
- try {
658
- const allThreads = await this.getThreadsByResourceId({ resourceId });
659
-
660
- const total = allThreads.length;
661
- const start = page * perPage;
662
- const end = start + perPage;
663
- const paginatedThreads = allThreads.slice(start, end);
664
- const hasMore = end < total;
665
-
666
- return {
667
- threads: paginatedThreads,
668
- total,
669
- page,
670
- perPage,
671
- hasMore,
672
- };
673
- } catch (error) {
674
- const mastraError = new MastraError(
675
- {
676
- id: 'STORAGE_UPSTASH_STORAGE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
677
- domain: ErrorDomain.STORAGE,
678
- category: ErrorCategory.THIRD_PARTY,
679
- details: {
680
- resourceId,
681
- page,
682
- perPage,
683
- },
684
- },
685
- error,
686
- );
687
- this.logger?.trackException(mastraError);
688
- this.logger.error(mastraError.toString());
689
- return {
690
- threads: [],
691
- total: 0,
692
- page,
693
- perPage,
694
- hasMore: false,
695
- };
696
- }
169
+ public async getThreadsByResourceIdPaginated(args: {
170
+ resourceId: string;
171
+ page: number;
172
+ perPage: number;
173
+ }): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
174
+ return this.stores.memory.getThreadsByResourceIdPaginated(args);
697
175
  }
698
176
 
699
177
  async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
700
- try {
701
- await this.insert({
702
- tableName: TABLE_THREADS,
703
- record: thread,
704
- });
705
- return thread;
706
- } catch (error) {
707
- const mastraError = new MastraError(
708
- {
709
- id: 'STORAGE_UPSTASH_STORAGE_SAVE_THREAD_FAILED',
710
- domain: ErrorDomain.STORAGE,
711
- category: ErrorCategory.THIRD_PARTY,
712
- details: {
713
- threadId: thread.id,
714
- },
715
- },
716
- error,
717
- );
718
- this.logger?.trackException(mastraError);
719
- this.logger.error(mastraError.toString());
720
- throw mastraError;
721
- }
178
+ return this.stores.memory.saveThread({ thread });
722
179
  }
723
180
 
724
181
  async updateThread({
@@ -730,80 +187,11 @@ export class UpstashStore extends MastraStorage {
730
187
  title: string;
731
188
  metadata: Record<string, unknown>;
732
189
  }): Promise<StorageThreadType> {
733
- const thread = await this.getThreadById({ threadId: id });
734
- if (!thread) {
735
- throw new MastraError({
736
- id: 'STORAGE_UPSTASH_STORAGE_UPDATE_THREAD_FAILED',
737
- domain: ErrorDomain.STORAGE,
738
- category: ErrorCategory.USER,
739
- text: `Thread ${id} not found`,
740
- details: {
741
- threadId: id,
742
- },
743
- });
744
- }
745
-
746
- const updatedThread = {
747
- ...thread,
748
- title,
749
- metadata: {
750
- ...thread.metadata,
751
- ...metadata,
752
- },
753
- };
754
-
755
- try {
756
- await this.saveThread({ thread: updatedThread });
757
- return updatedThread;
758
- } catch (error) {
759
- throw new MastraError(
760
- {
761
- id: 'STORAGE_UPSTASH_STORAGE_UPDATE_THREAD_FAILED',
762
- domain: ErrorDomain.STORAGE,
763
- category: ErrorCategory.THIRD_PARTY,
764
- details: {
765
- threadId: id,
766
- },
767
- },
768
- error,
769
- );
770
- }
190
+ return this.stores.memory.updateThread({ id, title, metadata });
771
191
  }
772
192
 
773
193
  async deleteThread({ threadId }: { threadId: string }): Promise<void> {
774
- // Delete thread metadata and sorted set
775
- const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
776
- const threadMessagesKey = this.getThreadMessagesKey(threadId);
777
- try {
778
- const messageIds: string[] = await this.redis.zrange(threadMessagesKey, 0, -1);
779
-
780
- const pipeline = this.redis.pipeline();
781
- pipeline.del(threadKey);
782
- pipeline.del(threadMessagesKey);
783
-
784
- for (let i = 0; i < messageIds.length; i++) {
785
- const messageId = messageIds[i];
786
- const messageKey = this.getMessageKey(threadId, messageId as string);
787
- pipeline.del(messageKey);
788
- }
789
-
790
- await pipeline.exec();
791
-
792
- // Bulk delete all message keys for this thread if any remain
793
- await this.scanAndDelete(this.getMessageKey(threadId, '*'));
794
- } catch (error) {
795
- throw new MastraError(
796
- {
797
- id: 'STORAGE_UPSTASH_STORAGE_DELETE_THREAD_FAILED',
798
- domain: ErrorDomain.STORAGE,
799
- category: ErrorCategory.THIRD_PARTY,
800
- details: {
801
- threadId,
802
- },
803
- },
804
- error,
805
- );
806
- }
194
+ return this.stores.memory.deleteThread({ threadId });
807
195
  }
808
196
 
809
197
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
@@ -811,164 +199,7 @@ export class UpstashStore extends MastraStorage {
811
199
  async saveMessages(
812
200
  args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
813
201
  ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
814
- const { messages, format = 'v1' } = args;
815
- if (messages.length === 0) return [];
816
-
817
- const threadId = messages[0]?.threadId;
818
- try {
819
- if (!threadId) {
820
- throw new Error('Thread ID is required');
821
- }
822
-
823
- // Check if thread exists
824
- const thread = await this.getThreadById({ threadId });
825
- if (!thread) {
826
- throw new Error(`Thread ${threadId} not found`);
827
- }
828
- } catch (error) {
829
- throw new MastraError(
830
- {
831
- id: 'STORAGE_UPSTASH_STORAGE_SAVE_MESSAGES_INVALID_ARGS',
832
- domain: ErrorDomain.STORAGE,
833
- category: ErrorCategory.USER,
834
- },
835
- error,
836
- );
837
- }
838
-
839
- // Add an index to each message to maintain order
840
- const messagesWithIndex = messages.map((message, index) => ({
841
- ...message,
842
- _index: index,
843
- }));
844
-
845
- // Get current thread data once (all messages belong to same thread)
846
- const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
847
- const existingThread = await this.redis.get<StorageThreadType>(threadKey);
848
-
849
- try {
850
- const batchSize = 1000;
851
- for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
852
- const batch = messagesWithIndex.slice(i, i + batchSize);
853
- const pipeline = this.redis.pipeline();
854
-
855
- for (const message of batch) {
856
- const key = this.getMessageKey(message.threadId!, message.id);
857
- const createdAtScore = new Date(message.createdAt).getTime();
858
- const score = message._index !== undefined ? message._index : createdAtScore;
859
-
860
- // Check if this message id exists in another thread
861
- const existingKeyPattern = this.getMessageKey('*', message.id);
862
- const keys = await this.scanKeys(existingKeyPattern);
863
-
864
- if (keys.length > 0) {
865
- const pipeline2 = this.redis.pipeline();
866
- keys.forEach(key => pipeline2.get(key));
867
- const results = await pipeline2.exec();
868
- const existingMessages = results.filter(
869
- (msg): msg is MastraMessageV2 | MastraMessageV1 => msg !== null,
870
- ) as (MastraMessageV2 | MastraMessageV1)[];
871
- for (const existingMessage of existingMessages) {
872
- const existingMessageKey = this.getMessageKey(existingMessage.threadId!, existingMessage.id);
873
- if (existingMessage && existingMessage.threadId !== message.threadId) {
874
- pipeline.del(existingMessageKey);
875
- // Remove from old thread's sorted set
876
- pipeline.zrem(this.getThreadMessagesKey(existingMessage.threadId!), existingMessage.id);
877
- }
878
- }
879
- }
880
-
881
- // Store the message data
882
- pipeline.set(key, message);
883
-
884
- // Add to sorted set for this thread
885
- pipeline.zadd(this.getThreadMessagesKey(message.threadId!), {
886
- score,
887
- member: message.id,
888
- });
889
- }
890
-
891
- // Update the thread's updatedAt field (only in the first batch)
892
- if (i === 0 && existingThread) {
893
- const updatedThread = {
894
- ...existingThread,
895
- updatedAt: new Date(),
896
- };
897
- pipeline.set(threadKey, this.processRecord(TABLE_THREADS, updatedThread).processedRecord);
898
- }
899
-
900
- await pipeline.exec();
901
- }
902
-
903
- const list = new MessageList().add(messages, 'memory');
904
- if (format === `v2`) return list.get.all.v2();
905
- return list.get.all.v1();
906
- } catch (error) {
907
- throw new MastraError(
908
- {
909
- id: 'STORAGE_UPSTASH_STORAGE_SAVE_MESSAGES_FAILED',
910
- domain: ErrorDomain.STORAGE,
911
- category: ErrorCategory.THIRD_PARTY,
912
- details: {
913
- threadId,
914
- },
915
- },
916
- error,
917
- );
918
- }
919
- }
920
-
921
- private async _getIncludedMessages(
922
- threadId: string,
923
- selectBy: StorageGetMessagesArg['selectBy'],
924
- ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
925
- const messageIds = new Set<string>();
926
- const messageIdToThreadIds: Record<string, string> = {};
927
-
928
- // First, get specifically included messages and their context
929
- if (selectBy?.include?.length) {
930
- for (const item of selectBy.include) {
931
- messageIds.add(item.id);
932
-
933
- // Use per-include threadId if present, else fallback to main threadId
934
- const itemThreadId = item.threadId || threadId;
935
- messageIdToThreadIds[item.id] = itemThreadId;
936
- const itemThreadMessagesKey = this.getThreadMessagesKey(itemThreadId);
937
-
938
- // Get the rank of this message in the sorted set
939
- const rank = await this.redis.zrank(itemThreadMessagesKey, item.id);
940
- if (rank === null) continue;
941
-
942
- // Get previous messages if requested
943
- if (item.withPreviousMessages) {
944
- const start = Math.max(0, rank - item.withPreviousMessages);
945
- const prevIds = rank === 0 ? [] : await this.redis.zrange(itemThreadMessagesKey, start, rank - 1);
946
- prevIds.forEach(id => {
947
- messageIds.add(id as string);
948
- messageIdToThreadIds[id as string] = itemThreadId;
949
- });
950
- }
951
-
952
- // Get next messages if requested
953
- if (item.withNextMessages) {
954
- const nextIds = await this.redis.zrange(itemThreadMessagesKey, rank + 1, rank + item.withNextMessages);
955
- nextIds.forEach(id => {
956
- messageIds.add(id as string);
957
- messageIdToThreadIds[id as string] = itemThreadId;
958
- });
959
- }
960
- }
961
-
962
- const pipeline = this.redis.pipeline();
963
- Array.from(messageIds).forEach(id => {
964
- const tId = messageIdToThreadIds[id] || threadId;
965
- pipeline.get(this.getMessageKey(tId, id as string));
966
- });
967
- const results = await pipeline.exec();
968
- return results.filter(result => result !== null) as MastraMessageV2[] | MastraMessageV1[];
969
- }
970
-
971
- return [];
202
+ return this.stores.memory.saveMessages(args);
972
203
  }
973
204
 
974
205
  /**
@@ -981,97 +212,7 @@ export class UpstashStore extends MastraStorage {
981
212
  selectBy,
982
213
  format,
983
214
  }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
984
- const threadMessagesKey = this.getThreadMessagesKey(threadId);
985
- try {
986
- const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
987
- const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
988
-
989
- const messageIds = new Set<string>();
990
- const messageIdToThreadIds: Record<string, string> = {};
991
-
992
- if (limit === 0 && !selectBy?.include) {
993
- return [];
994
- }
995
-
996
- // Then get the most recent messages (or all if no limit)
997
- if (limit === Number.MAX_SAFE_INTEGER) {
998
- // Get all messages
999
- const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
1000
- allIds.forEach(id => {
1001
- messageIds.add(id as string);
1002
- messageIdToThreadIds[id as string] = threadId;
1003
- });
1004
- } else if (limit > 0) {
1005
- // Get limited number of recent messages
1006
- const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
1007
- latestIds.forEach(id => {
1008
- messageIds.add(id as string);
1009
- messageIdToThreadIds[id as string] = threadId;
1010
- });
1011
- }
1012
-
1013
- const includedMessages = await this._getIncludedMessages(threadId, selectBy);
1014
-
1015
- // Fetch all needed messages in parallel
1016
- const messages = [
1017
- ...includedMessages,
1018
- ...((
1019
- await Promise.all(
1020
- Array.from(messageIds).map(async id => {
1021
- const tId = messageIdToThreadIds[id] || threadId;
1022
- const byThreadId = await this.redis.get<MastraMessageV2 & { _index?: number }>(
1023
- this.getMessageKey(tId, id),
1024
- );
1025
- if (byThreadId) return byThreadId;
1026
-
1027
- return null;
1028
- }),
1029
- )
1030
- ).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[]),
1031
- ];
1032
-
1033
- // Sort messages by their position in the sorted set
1034
- messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
1035
-
1036
- const seen = new Set<string>();
1037
- const dedupedMessages = messages.filter(row => {
1038
- if (seen.has(row.id)) return false;
1039
- seen.add(row.id);
1040
- return true;
1041
- });
1042
-
1043
- // Remove _index before returning and handle format conversion properly
1044
- const prepared = dedupedMessages
1045
- .filter(message => message !== null && message !== undefined)
1046
- .map(message => {
1047
- const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
1048
- return messageWithoutIndex as unknown as MastraMessageV1;
1049
- });
1050
-
1051
- // For backward compatibility, return messages directly without using MessageList
1052
- // since MessageList has deduplication logic that can cause issues
1053
- if (format === 'v2') {
1054
- // Convert V1 format back to V2 format
1055
- return prepared.map(msg => ({
1056
- ...msg,
1057
- content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
1058
- })) as MastraMessageV2[];
1059
- }
1060
-
1061
- return prepared;
1062
- } catch (error) {
1063
- throw new MastraError(
1064
- {
1065
- id: 'STORAGE_UPSTASH_STORAGE_GET_MESSAGES_FAILED',
1066
- domain: ErrorDomain.STORAGE,
1067
- category: ErrorCategory.THIRD_PARTY,
1068
- details: {
1069
- threadId,
1070
- },
1071
- },
1072
- error,
1073
- );
1074
- }
215
+ return this.stores.memory.getMessages({ threadId, selectBy, format });
1075
216
  }
1076
217
 
1077
218
  public async getMessagesPaginated(
@@ -1079,94 +220,7 @@ export class UpstashStore extends MastraStorage {
1079
220
  format?: 'v1' | 'v2';
1080
221
  },
1081
222
  ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
1082
- const { threadId, selectBy, format } = args;
1083
- const { page = 0, perPage = 40, dateRange } = selectBy?.pagination || {};
1084
- const fromDate = dateRange?.start;
1085
- const toDate = dateRange?.end;
1086
- const threadMessagesKey = this.getThreadMessagesKey(threadId);
1087
- const messages: (MastraMessageV2 | MastraMessageV1)[] = [];
1088
-
1089
- try {
1090
- const includedMessages = await this._getIncludedMessages(threadId, selectBy);
1091
- messages.push(...includedMessages);
1092
-
1093
- const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
1094
- if (allMessageIds.length === 0) {
1095
- return {
1096
- messages: [],
1097
- total: 0,
1098
- page,
1099
- perPage,
1100
- hasMore: false,
1101
- };
1102
- }
1103
-
1104
- // Use pipeline to fetch all messages efficiently
1105
- const pipeline = this.redis.pipeline();
1106
- allMessageIds.forEach(id => pipeline.get(this.getMessageKey(threadId, id as string)));
1107
- const results = await pipeline.exec();
1108
-
1109
- // Process messages and apply filters - handle undefined results from pipeline
1110
- let messagesData = results.filter((msg): msg is MastraMessageV2 | MastraMessageV1 => msg !== null) as (
1111
- | MastraMessageV2
1112
- | MastraMessageV1
1113
- )[];
1114
-
1115
- // Apply date filters if provided
1116
- if (fromDate) {
1117
- messagesData = messagesData.filter(msg => msg && new Date(msg.createdAt).getTime() >= fromDate.getTime());
1118
- }
1119
-
1120
- if (toDate) {
1121
- messagesData = messagesData.filter(msg => msg && new Date(msg.createdAt).getTime() <= toDate.getTime());
1122
- }
1123
-
1124
- // Sort messages by their position in the sorted set
1125
- messagesData.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
1126
-
1127
- const total = messagesData.length;
1128
-
1129
- const start = page * perPage;
1130
- const end = start + perPage;
1131
- const hasMore = end < total;
1132
- const paginatedMessages = messagesData.slice(start, end);
1133
-
1134
- messages.push(...paginatedMessages);
1135
-
1136
- const list = new MessageList().add(messages, 'memory');
1137
- const finalMessages = (format === `v2` ? list.get.all.v2() : list.get.all.v1()) as
1138
- | MastraMessageV1[]
1139
- | MastraMessageV2[];
1140
-
1141
- return {
1142
- messages: finalMessages,
1143
- total,
1144
- page,
1145
- perPage,
1146
- hasMore,
1147
- };
1148
- } catch (error) {
1149
- const mastraError = new MastraError(
1150
- {
1151
- id: 'STORAGE_UPSTASH_STORAGE_GET_MESSAGES_PAGINATED_FAILED',
1152
- domain: ErrorDomain.STORAGE,
1153
- category: ErrorCategory.THIRD_PARTY,
1154
- details: {
1155
- threadId,
1156
- },
1157
- },
1158
- error,
1159
- );
1160
- this.logger.error(mastraError.toString());
1161
- this.logger?.trackException(mastraError);
1162
- return {
1163
- messages: [],
1164
- total: 0,
1165
- page,
1166
- perPage,
1167
- hasMore: false,
1168
- };
1169
- }
223
+ return this.stores.memory.getMessagesPaginated(args);
1170
224
  }
1171
225
 
1172
226
  async persistWorkflowSnapshot(params: {
@@ -1175,34 +229,7 @@ export class UpstashStore extends MastraStorage {
1175
229
  runId: string;
1176
230
  snapshot: WorkflowRunState;
1177
231
  }): Promise<void> {
1178
- const { namespace = 'workflows', workflowName, runId, snapshot } = params;
1179
- try {
1180
- await this.insert({
1181
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1182
- record: {
1183
- namespace,
1184
- workflow_name: workflowName,
1185
- run_id: runId,
1186
- snapshot,
1187
- createdAt: new Date(),
1188
- updatedAt: new Date(),
1189
- },
1190
- });
1191
- } catch (error) {
1192
- throw new MastraError(
1193
- {
1194
- id: 'STORAGE_UPSTASH_STORAGE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
1195
- domain: ErrorDomain.STORAGE,
1196
- category: ErrorCategory.THIRD_PARTY,
1197
- details: {
1198
- namespace,
1199
- workflowName,
1200
- runId,
1201
- },
1202
- },
1203
- error,
1204
- );
1205
- }
232
+ return this.stores.workflows.persistWorkflowSnapshot(params);
1206
233
  }
1207
234
 
1208
235
  async loadWorkflowSnapshot(params: {
@@ -1210,370 +237,56 @@ export class UpstashStore extends MastraStorage {
1210
237
  workflowName: string;
1211
238
  runId: string;
1212
239
  }): Promise<WorkflowRunState | null> {
1213
- const { namespace = 'workflows', workflowName, runId } = params;
1214
- const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
1215
- namespace,
1216
- workflow_name: workflowName,
1217
- run_id: runId,
1218
- });
1219
- try {
1220
- const data = await this.redis.get<{
1221
- namespace: string;
1222
- workflow_name: string;
1223
- run_id: string;
1224
- snapshot: WorkflowRunState;
1225
- }>(key);
1226
- if (!data) return null;
1227
- return data.snapshot;
1228
- } catch (error) {
1229
- throw new MastraError(
1230
- {
1231
- id: 'STORAGE_UPSTASH_STORAGE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
1232
- domain: ErrorDomain.STORAGE,
1233
- category: ErrorCategory.THIRD_PARTY,
1234
- details: {
1235
- namespace,
1236
- workflowName,
1237
- runId,
1238
- },
1239
- },
1240
- error,
1241
- );
1242
- }
1243
- }
1244
-
1245
- /**
1246
- * Get all evaluations with pagination and total count
1247
- * @param options Pagination and filtering options
1248
- * @returns Object with evals array and total count
1249
- */
1250
- async getEvals(
1251
- options?: {
1252
- agentName?: string;
1253
- type?: 'test' | 'live';
1254
- } & PaginationArgs,
1255
- ): Promise<PaginationInfo & { evals: EvalRow[] }> {
1256
- try {
1257
- // Default pagination parameters
1258
- const { agentName, type, page = 0, perPage = 100, dateRange } = options || {};
1259
- const fromDate = dateRange?.start;
1260
- const toDate = dateRange?.end;
1261
-
1262
- // Get all keys that match the evals table pattern using cursor-based scanning
1263
- const pattern = `${TABLE_EVALS}:*`;
1264
- const keys = await this.scanKeys(pattern);
1265
-
1266
- // Check if we have any keys before using pipeline
1267
- if (keys.length === 0) {
1268
- return {
1269
- evals: [],
1270
- total: 0,
1271
- page,
1272
- perPage,
1273
- hasMore: false,
1274
- };
1275
- }
1276
-
1277
- // Use pipeline for batch fetching to improve performance
1278
- const pipeline = this.redis.pipeline();
1279
- keys.forEach(key => pipeline.get(key));
1280
- const results = await pipeline.exec();
1281
-
1282
- // Process results and apply filters
1283
- let filteredEvals = results
1284
- .map((result: any) => result as Record<string, any> | null)
1285
- .filter((record): record is Record<string, any> => record !== null && typeof record === 'object');
1286
-
1287
- // Apply agent name filter if provided
1288
- if (agentName) {
1289
- filteredEvals = filteredEvals.filter(record => record.agent_name === agentName);
1290
- }
1291
-
1292
- // Apply type filter if provided
1293
- if (type === 'test') {
1294
- filteredEvals = filteredEvals.filter(record => {
1295
- if (!record.test_info) return false;
1296
-
1297
- try {
1298
- if (typeof record.test_info === 'string') {
1299
- const parsedTestInfo = JSON.parse(record.test_info);
1300
- return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
1301
- }
1302
- return typeof record.test_info === 'object' && 'testPath' in record.test_info;
1303
- } catch {
1304
- return false;
1305
- }
1306
- });
1307
- } else if (type === 'live') {
1308
- filteredEvals = filteredEvals.filter(record => {
1309
- if (!record.test_info) return true;
1310
-
1311
- try {
1312
- if (typeof record.test_info === 'string') {
1313
- const parsedTestInfo = JSON.parse(record.test_info);
1314
- return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
1315
- }
1316
- return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
1317
- } catch {
1318
- return true;
1319
- }
1320
- });
1321
- }
1322
-
1323
- // Apply date filters if provided
1324
- if (fromDate) {
1325
- filteredEvals = filteredEvals.filter(record => {
1326
- const createdAt = new Date(record.created_at || record.createdAt || 0);
1327
- return createdAt.getTime() >= fromDate.getTime();
1328
- });
1329
- }
1330
-
1331
- if (toDate) {
1332
- filteredEvals = filteredEvals.filter(record => {
1333
- const createdAt = new Date(record.created_at || record.createdAt || 0);
1334
- return createdAt.getTime() <= toDate.getTime();
1335
- });
1336
- }
1337
-
1338
- // Sort by creation date (newest first)
1339
- filteredEvals.sort((a, b) => {
1340
- const dateA = new Date(a.created_at || a.createdAt || 0).getTime();
1341
- const dateB = new Date(b.created_at || b.createdAt || 0).getTime();
1342
- return dateB - dateA;
1343
- });
1344
-
1345
- const total = filteredEvals.length;
1346
-
1347
- // Apply pagination
1348
- const start = page * perPage;
1349
- const end = start + perPage;
1350
- const paginatedEvals = filteredEvals.slice(start, end);
1351
- const hasMore = end < total;
1352
-
1353
- // Transform to EvalRow format
1354
- const evals = paginatedEvals.map(record => this.transformEvalRecord(record));
1355
-
1356
- return {
1357
- evals,
1358
- total,
1359
- page,
1360
- perPage,
1361
- hasMore,
1362
- };
1363
- } catch (error) {
1364
- const { page = 0, perPage = 100 } = options || {};
1365
- const mastraError = new MastraError(
1366
- {
1367
- id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_FAILED',
1368
- domain: ErrorDomain.STORAGE,
1369
- category: ErrorCategory.THIRD_PARTY,
1370
- details: {
1371
- page,
1372
- perPage,
1373
- },
1374
- },
1375
- error,
1376
- );
1377
- this.logger.error(mastraError.toString());
1378
- this.logger?.trackException(mastraError);
1379
- return {
1380
- evals: [],
1381
- total: 0,
1382
- page,
1383
- perPage,
1384
- hasMore: false,
1385
- };
1386
- }
240
+ return this.stores.workflows.loadWorkflowSnapshot(params);
1387
241
  }
1388
242
 
1389
- async getWorkflowRuns(
1390
- {
1391
- namespace,
1392
- workflowName,
1393
- fromDate,
1394
- toDate,
1395
- limit,
1396
- offset,
1397
- resourceId,
1398
- }: {
1399
- namespace: string;
1400
- workflowName?: string;
1401
- fromDate?: Date;
1402
- toDate?: Date;
1403
- limit?: number;
1404
- offset?: number;
1405
- resourceId?: string;
1406
- } = { namespace: 'workflows' },
1407
- ): Promise<WorkflowRuns> {
1408
- try {
1409
- // Get all workflow keys
1410
- let pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ':*';
1411
- if (workflowName && resourceId) {
1412
- pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
1413
- namespace,
1414
- workflow_name: workflowName,
1415
- run_id: '*',
1416
- resourceId,
1417
- });
1418
- } else if (workflowName) {
1419
- pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ':*';
1420
- } else if (resourceId) {
1421
- pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: '*', run_id: '*', resourceId });
1422
- }
1423
- const keys = await this.scanKeys(pattern);
1424
-
1425
- // Check if we have any keys before using pipeline
1426
- if (keys.length === 0) {
1427
- return { runs: [], total: 0 };
1428
- }
1429
-
1430
- // Use pipeline for batch fetching to improve performance
1431
- const pipeline = this.redis.pipeline();
1432
- keys.forEach(key => pipeline.get(key));
1433
- const results = await pipeline.exec();
1434
-
1435
- // Filter and transform results - handle undefined results
1436
- let runs = results
1437
- .map((result: any) => result as Record<string, any> | null)
1438
- .filter(
1439
- (record): record is Record<string, any> =>
1440
- record !== null && record !== undefined && typeof record === 'object' && 'workflow_name' in record,
1441
- )
1442
- // Only filter by workflowName if it was specifically requested
1443
- .filter(record => !workflowName || record.workflow_name === workflowName)
1444
- .map(w => this.parseWorkflowRun(w!))
1445
- .filter(w => {
1446
- if (fromDate && w.createdAt < fromDate) return false;
1447
- if (toDate && w.createdAt > toDate) return false;
1448
- return true;
1449
- })
1450
- .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1451
-
1452
- const total = runs.length;
1453
-
1454
- // Apply pagination if requested
1455
- if (limit !== undefined && offset !== undefined) {
1456
- runs = runs.slice(offset, offset + limit);
1457
- }
1458
-
1459
- return { runs, total };
1460
- } catch (error) {
1461
- throw new MastraError(
1462
- {
1463
- id: 'STORAGE_UPSTASH_STORAGE_GET_WORKFLOW_RUNS_FAILED',
1464
- domain: ErrorDomain.STORAGE,
1465
- category: ErrorCategory.THIRD_PARTY,
1466
- details: {
1467
- namespace,
1468
- workflowName: workflowName || '',
1469
- resourceId: resourceId || '',
1470
- },
1471
- },
1472
- error,
1473
- );
1474
- }
243
+ async getWorkflowRuns({
244
+ workflowName,
245
+ fromDate,
246
+ toDate,
247
+ limit,
248
+ offset,
249
+ resourceId,
250
+ }: {
251
+ workflowName?: string;
252
+ fromDate?: Date;
253
+ toDate?: Date;
254
+ limit?: number;
255
+ offset?: number;
256
+ resourceId?: string;
257
+ } = {}): Promise<WorkflowRuns> {
258
+ return this.stores.workflows.getWorkflowRuns({ workflowName, fromDate, toDate, limit, offset, resourceId });
1475
259
  }
1476
260
 
1477
261
  async getWorkflowRunById({
1478
- namespace = 'workflows',
1479
262
  runId,
1480
263
  workflowName,
1481
264
  }: {
1482
- namespace: string;
1483
265
  runId: string;
1484
266
  workflowName?: string;
1485
267
  }): Promise<WorkflowRun | null> {
1486
- try {
1487
- const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + '*';
1488
- const keys = await this.scanKeys(key);
1489
- const workflows = await Promise.all(
1490
- keys.map(async key => {
1491
- const data = await this.redis.get<{
1492
- workflow_name: string;
1493
- run_id: string;
1494
- snapshot: WorkflowRunState | string;
1495
- createdAt: string | Date;
1496
- updatedAt: string | Date;
1497
- resourceId: string;
1498
- }>(key);
1499
- return data;
1500
- }),
1501
- );
1502
- const data = workflows.find(w => w?.run_id === runId && w?.workflow_name === workflowName) as WorkflowRun | null;
1503
- if (!data) return null;
1504
- return this.parseWorkflowRun(data);
1505
- } catch (error) {
1506
- throw new MastraError(
1507
- {
1508
- id: 'STORAGE_UPSTASH_STORAGE_GET_WORKFLOW_RUN_BY_ID_FAILED',
1509
- domain: ErrorDomain.STORAGE,
1510
- category: ErrorCategory.THIRD_PARTY,
1511
- details: {
1512
- namespace,
1513
- runId,
1514
- workflowName: workflowName || '',
1515
- },
1516
- },
1517
- error,
1518
- );
1519
- }
268
+ return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
1520
269
  }
1521
270
 
1522
271
  async close(): Promise<void> {
1523
272
  // No explicit cleanup needed for Upstash Redis
1524
273
  }
1525
274
 
1526
- async updateMessages(_args: {
1527
- messages: Partial<Omit<MastraMessageV2, 'createdAt'>> &
1528
- {
1529
- id: string;
1530
- content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
1531
- }[];
275
+ async updateMessages(args: {
276
+ messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
277
+ id: string;
278
+ content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
279
+ })[];
1532
280
  }): Promise<MastraMessageV2[]> {
1533
- this.logger.error('updateMessages is not yet implemented in UpstashStore');
1534
- throw new Error('Method not implemented');
281
+ return this.stores.memory.updateMessages(args);
1535
282
  }
1536
283
 
1537
284
  async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
1538
- try {
1539
- const key = `${TABLE_RESOURCES}:${resourceId}`;
1540
- const data = await this.redis.get<StorageResourceType>(key);
1541
-
1542
- if (!data) {
1543
- return null;
1544
- }
1545
-
1546
- return {
1547
- ...data,
1548
- createdAt: new Date(data.createdAt),
1549
- updatedAt: new Date(data.updatedAt),
1550
- // Ensure workingMemory is always returned as a string, regardless of automatic parsing
1551
- workingMemory: typeof data.workingMemory === 'object' ? JSON.stringify(data.workingMemory) : data.workingMemory,
1552
- metadata: typeof data.metadata === 'string' ? JSON.parse(data.metadata) : data.metadata,
1553
- };
1554
- } catch (error) {
1555
- this.logger.error('Error getting resource by ID:', error);
1556
- throw error;
1557
- }
285
+ return this.stores.memory.getResourceById({ resourceId });
1558
286
  }
1559
287
 
1560
288
  async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
1561
- try {
1562
- const key = `${TABLE_RESOURCES}:${resource.id}`;
1563
- const serializedResource = {
1564
- ...resource,
1565
- metadata: JSON.stringify(resource.metadata),
1566
- createdAt: resource.createdAt.toISOString(),
1567
- updatedAt: resource.updatedAt.toISOString(),
1568
- };
1569
-
1570
- await this.redis.set(key, serializedResource);
1571
-
1572
- return resource;
1573
- } catch (error) {
1574
- this.logger.error('Error saving resource:', error);
1575
- throw error;
1576
- }
289
+ return this.stores.memory.saveResource({ resource });
1577
290
  }
1578
291
 
1579
292
  async updateResource({
@@ -1585,36 +298,50 @@ export class UpstashStore extends MastraStorage {
1585
298
  workingMemory?: string;
1586
299
  metadata?: Record<string, unknown>;
1587
300
  }): Promise<StorageResourceType> {
1588
- try {
1589
- const existingResource = await this.getResourceById({ resourceId });
301
+ return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
302
+ }
1590
303
 
1591
- if (!existingResource) {
1592
- // Create new resource if it doesn't exist
1593
- const newResource: StorageResourceType = {
1594
- id: resourceId,
1595
- workingMemory,
1596
- metadata: metadata || {},
1597
- createdAt: new Date(),
1598
- updatedAt: new Date(),
1599
- };
1600
- return this.saveResource({ resource: newResource });
1601
- }
304
+ async getScoreById({ id: _id }: { id: string }): Promise<ScoreRowData | null> {
305
+ return this.stores.scores.getScoreById({ id: _id });
306
+ }
1602
307
 
1603
- const updatedResource = {
1604
- ...existingResource,
1605
- workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
1606
- metadata: {
1607
- ...existingResource.metadata,
1608
- ...metadata,
1609
- },
1610
- updatedAt: new Date(),
1611
- };
308
+ async saveScore(score: ScoreRowData): Promise<{ score: ScoreRowData }> {
309
+ return this.stores.scores.saveScore(score);
310
+ }
311
+
312
+ async getScoresByRunId({
313
+ runId,
314
+ pagination,
315
+ }: {
316
+ runId: string;
317
+ pagination: StoragePagination;
318
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
319
+ return this.stores.scores.getScoresByRunId({ runId, pagination });
320
+ }
1612
321
 
1613
- await this.saveResource({ resource: updatedResource });
1614
- return updatedResource;
1615
- } catch (error) {
1616
- this.logger.error('Error updating resource:', error);
1617
- throw error;
1618
- }
322
+ async getScoresByEntityId({
323
+ entityId,
324
+ entityType,
325
+ pagination,
326
+ }: {
327
+ pagination: StoragePagination;
328
+ entityId: string;
329
+ entityType: string;
330
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
331
+ return this.stores.scores.getScoresByEntityId({
332
+ entityId,
333
+ entityType,
334
+ pagination,
335
+ });
336
+ }
337
+
338
+ async getScoresByScorerId({
339
+ scorerId,
340
+ pagination,
341
+ }: {
342
+ scorerId: string;
343
+ pagination: StoragePagination;
344
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
345
+ return this.stores.scores.getScoresByScorerId({ scorerId, pagination });
1619
346
  }
1620
347
  }