@mastra/dynamodb 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1271 @@
1
+ import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
2
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
3
+ import { MastraStorage, TABLE_TRACES, TABLE_EVALS, TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, TABLE_THREADS } from '@mastra/core/storage';
4
+ import { Entity, Service } from 'electrodb';
5
+
6
+ // src/storage/index.ts
7
+
8
+ // src/entities/utils.ts
9
+ var baseAttributes = {
10
+ createdAt: {
11
+ type: "string",
12
+ required: true,
13
+ readOnly: true,
14
+ // Convert Date to ISO string on set
15
+ set: (value) => {
16
+ if (value instanceof Date) {
17
+ return value.toISOString();
18
+ }
19
+ return value || (/* @__PURE__ */ new Date()).toISOString();
20
+ },
21
+ // Initialize with current timestamp if not provided
22
+ default: () => (/* @__PURE__ */ new Date()).toISOString()
23
+ },
24
+ updatedAt: {
25
+ type: "string",
26
+ required: true,
27
+ // Convert Date to ISO string on set
28
+ set: (value) => {
29
+ if (value instanceof Date) {
30
+ return value.toISOString();
31
+ }
32
+ return value || (/* @__PURE__ */ new Date()).toISOString();
33
+ },
34
+ // Always use current timestamp when creating/updating
35
+ default: () => (/* @__PURE__ */ new Date()).toISOString()
36
+ },
37
+ metadata: {
38
+ type: "string",
39
+ // JSON stringified
40
+ // Stringify objects on set
41
+ set: (value) => {
42
+ if (value && typeof value !== "string") {
43
+ return JSON.stringify(value);
44
+ }
45
+ return value;
46
+ },
47
+ // Parse JSON string to object on get
48
+ get: (value) => {
49
+ if (value) {
50
+ try {
51
+ return JSON.parse(value);
52
+ } catch {
53
+ return value;
54
+ }
55
+ }
56
+ return value;
57
+ }
58
+ }
59
+ };
60
+
61
+ // src/entities/eval.ts
62
+ var evalEntity = new Entity({
63
+ model: {
64
+ entity: "eval",
65
+ version: "1",
66
+ service: "mastra"
67
+ },
68
+ attributes: {
69
+ entity: {
70
+ type: "string",
71
+ required: true
72
+ },
73
+ ...baseAttributes,
74
+ input: {
75
+ type: "string",
76
+ required: true
77
+ },
78
+ output: {
79
+ type: "string",
80
+ required: true
81
+ },
82
+ result: {
83
+ type: "string",
84
+ // JSON stringified
85
+ required: true,
86
+ // Stringify object on set
87
+ set: (value) => {
88
+ if (value && typeof value !== "string") {
89
+ return JSON.stringify(value);
90
+ }
91
+ return value;
92
+ },
93
+ // Parse JSON string to object on get
94
+ get: (value) => {
95
+ if (value) {
96
+ return JSON.parse(value);
97
+ }
98
+ return value;
99
+ }
100
+ },
101
+ agent_name: {
102
+ type: "string",
103
+ required: true
104
+ },
105
+ metric_name: {
106
+ type: "string",
107
+ required: true
108
+ },
109
+ instructions: {
110
+ type: "string",
111
+ required: true
112
+ },
113
+ test_info: {
114
+ type: "string",
115
+ // JSON stringified
116
+ required: false,
117
+ // Stringify object on set
118
+ set: (value) => {
119
+ if (value && typeof value !== "string") {
120
+ return JSON.stringify(value);
121
+ }
122
+ return value;
123
+ },
124
+ // Parse JSON string to object on get
125
+ get: (value) => {
126
+ return value;
127
+ }
128
+ },
129
+ global_run_id: {
130
+ type: "string",
131
+ required: true
132
+ },
133
+ run_id: {
134
+ type: "string",
135
+ required: true
136
+ },
137
+ created_at: {
138
+ type: "string",
139
+ required: true,
140
+ // Initialize with current timestamp if not provided
141
+ default: () => (/* @__PURE__ */ new Date()).toISOString(),
142
+ // Convert Date to ISO string on set
143
+ set: (value) => {
144
+ if (value instanceof Date) {
145
+ return value.toISOString();
146
+ }
147
+ return value || (/* @__PURE__ */ new Date()).toISOString();
148
+ }
149
+ }
150
+ },
151
+ indexes: {
152
+ primary: {
153
+ pk: { field: "pk", composite: ["entity", "run_id"] },
154
+ sk: { field: "sk", composite: [] }
155
+ },
156
+ byAgent: {
157
+ index: "gsi1",
158
+ pk: { field: "gsi1pk", composite: ["entity", "agent_name"] },
159
+ sk: { field: "gsi1sk", composite: ["created_at"] }
160
+ }
161
+ }
162
+ });
163
+ var messageEntity = new Entity({
164
+ model: {
165
+ entity: "message",
166
+ version: "1",
167
+ service: "mastra"
168
+ },
169
+ attributes: {
170
+ entity: {
171
+ type: "string",
172
+ required: true
173
+ },
174
+ ...baseAttributes,
175
+ id: {
176
+ type: "string",
177
+ required: true
178
+ },
179
+ threadId: {
180
+ type: "string",
181
+ required: true
182
+ },
183
+ content: {
184
+ type: "string",
185
+ required: true,
186
+ // Stringify content object on set if it's not already a string
187
+ set: (value) => {
188
+ if (value && typeof value !== "string") {
189
+ return JSON.stringify(value);
190
+ }
191
+ return value;
192
+ },
193
+ // Parse JSON string to object on get ONLY if it looks like JSON
194
+ get: (value) => {
195
+ if (value && typeof value === "string") {
196
+ try {
197
+ if (value.startsWith("{") || value.startsWith("[")) {
198
+ return JSON.parse(value);
199
+ }
200
+ } catch {
201
+ return value;
202
+ }
203
+ }
204
+ return value;
205
+ }
206
+ },
207
+ role: {
208
+ type: "string",
209
+ required: true
210
+ },
211
+ type: {
212
+ type: "string",
213
+ default: "text"
214
+ },
215
+ resourceId: {
216
+ type: "string",
217
+ required: false
218
+ },
219
+ toolCallIds: {
220
+ type: "string",
221
+ required: false,
222
+ set: (value) => {
223
+ if (Array.isArray(value)) {
224
+ return JSON.stringify(value);
225
+ }
226
+ return value;
227
+ },
228
+ // Parse JSON string to array on get
229
+ get: (value) => {
230
+ if (value && typeof value === "string") {
231
+ try {
232
+ return JSON.parse(value);
233
+ } catch {
234
+ return value;
235
+ }
236
+ }
237
+ return value;
238
+ }
239
+ },
240
+ toolCallArgs: {
241
+ type: "string",
242
+ required: false,
243
+ set: (value) => {
244
+ if (value && typeof value !== "string") {
245
+ return JSON.stringify(value);
246
+ }
247
+ return value;
248
+ },
249
+ // Parse JSON string to object on get
250
+ get: (value) => {
251
+ if (value && typeof value === "string") {
252
+ try {
253
+ return JSON.parse(value);
254
+ } catch {
255
+ return value;
256
+ }
257
+ }
258
+ return value;
259
+ }
260
+ },
261
+ toolNames: {
262
+ type: "string",
263
+ required: false,
264
+ set: (value) => {
265
+ if (Array.isArray(value)) {
266
+ return JSON.stringify(value);
267
+ }
268
+ return value;
269
+ },
270
+ // Parse JSON string to array on get
271
+ get: (value) => {
272
+ if (value && typeof value === "string") {
273
+ try {
274
+ return JSON.parse(value);
275
+ } catch {
276
+ return value;
277
+ }
278
+ }
279
+ return value;
280
+ }
281
+ }
282
+ },
283
+ indexes: {
284
+ primary: {
285
+ pk: { field: "pk", composite: ["entity", "id"] },
286
+ sk: { field: "sk", composite: ["entity"] }
287
+ },
288
+ byThread: {
289
+ index: "gsi1",
290
+ pk: { field: "gsi1pk", composite: ["entity", "threadId"] },
291
+ sk: { field: "gsi1sk", composite: ["createdAt"] }
292
+ }
293
+ }
294
+ });
295
+ var threadEntity = new Entity({
296
+ model: {
297
+ entity: "thread",
298
+ version: "1",
299
+ service: "mastra"
300
+ },
301
+ attributes: {
302
+ entity: {
303
+ type: "string",
304
+ required: true
305
+ },
306
+ ...baseAttributes,
307
+ id: {
308
+ type: "string",
309
+ required: true
310
+ },
311
+ resourceId: {
312
+ type: "string",
313
+ required: true
314
+ },
315
+ title: {
316
+ type: "string",
317
+ required: true
318
+ },
319
+ metadata: {
320
+ type: "string",
321
+ required: false,
322
+ // Stringify metadata object on set if it's not already a string
323
+ set: (value) => {
324
+ if (value && typeof value !== "string") {
325
+ return JSON.stringify(value);
326
+ }
327
+ return value;
328
+ },
329
+ // Parse JSON string to object on get
330
+ get: (value) => {
331
+ if (value && typeof value === "string") {
332
+ try {
333
+ if (value.startsWith("{") || value.startsWith("[")) {
334
+ return JSON.parse(value);
335
+ }
336
+ } catch {
337
+ return value;
338
+ }
339
+ }
340
+ return value;
341
+ }
342
+ }
343
+ },
344
+ indexes: {
345
+ primary: {
346
+ pk: { field: "pk", composite: ["entity", "id"] },
347
+ sk: { field: "sk", composite: ["id"] }
348
+ },
349
+ byResource: {
350
+ index: "gsi1",
351
+ pk: { field: "gsi1pk", composite: ["entity", "resourceId"] },
352
+ sk: { field: "gsi1sk", composite: ["createdAt"] }
353
+ }
354
+ }
355
+ });
356
+ var traceEntity = new Entity({
357
+ model: {
358
+ entity: "trace",
359
+ version: "1",
360
+ service: "mastra"
361
+ },
362
+ attributes: {
363
+ entity: {
364
+ type: "string",
365
+ required: true
366
+ },
367
+ ...baseAttributes,
368
+ id: {
369
+ type: "string",
370
+ required: true
371
+ },
372
+ parentSpanId: {
373
+ type: "string",
374
+ required: false
375
+ },
376
+ name: {
377
+ type: "string",
378
+ required: true
379
+ },
380
+ traceId: {
381
+ type: "string",
382
+ required: true
383
+ },
384
+ scope: {
385
+ type: "string",
386
+ required: true
387
+ },
388
+ kind: {
389
+ type: "number",
390
+ required: true
391
+ },
392
+ attributes: {
393
+ type: "string",
394
+ // JSON stringified
395
+ required: false,
396
+ // Stringify object on set
397
+ set: (value) => {
398
+ if (value && typeof value !== "string") {
399
+ return JSON.stringify(value);
400
+ }
401
+ return value;
402
+ },
403
+ // Parse JSON string to object on get
404
+ get: (value) => {
405
+ return value ? JSON.parse(value) : value;
406
+ }
407
+ },
408
+ status: {
409
+ type: "string",
410
+ // JSON stringified
411
+ required: false,
412
+ // Stringify object on set
413
+ set: (value) => {
414
+ if (value && typeof value !== "string") {
415
+ return JSON.stringify(value);
416
+ }
417
+ return value;
418
+ },
419
+ // Parse JSON string to object on get
420
+ get: (value) => {
421
+ return value;
422
+ }
423
+ },
424
+ events: {
425
+ type: "string",
426
+ // JSON stringified
427
+ required: false,
428
+ // Stringify object on set
429
+ set: (value) => {
430
+ if (value && typeof value !== "string") {
431
+ return JSON.stringify(value);
432
+ }
433
+ return value;
434
+ },
435
+ // Parse JSON string to object on get
436
+ get: (value) => {
437
+ return value;
438
+ }
439
+ },
440
+ links: {
441
+ type: "string",
442
+ // JSON stringified
443
+ required: false,
444
+ // Stringify object on set
445
+ set: (value) => {
446
+ if (value && typeof value !== "string") {
447
+ return JSON.stringify(value);
448
+ }
449
+ return value;
450
+ },
451
+ // Parse JSON string to object on get
452
+ get: (value) => {
453
+ return value;
454
+ }
455
+ },
456
+ other: {
457
+ type: "string",
458
+ required: false
459
+ },
460
+ startTime: {
461
+ type: "number",
462
+ required: true
463
+ },
464
+ endTime: {
465
+ type: "number",
466
+ required: true
467
+ }
468
+ },
469
+ indexes: {
470
+ primary: {
471
+ pk: { field: "pk", composite: ["entity", "id"] },
472
+ sk: { field: "sk", composite: [] }
473
+ },
474
+ byName: {
475
+ index: "gsi1",
476
+ pk: { field: "gsi1pk", composite: ["entity", "name"] },
477
+ sk: { field: "gsi1sk", composite: ["startTime"] }
478
+ },
479
+ byScope: {
480
+ index: "gsi2",
481
+ pk: { field: "gsi2pk", composite: ["entity", "scope"] },
482
+ sk: { field: "gsi2sk", composite: ["startTime"] }
483
+ }
484
+ }
485
+ });
486
+ var workflowSnapshotEntity = new Entity({
487
+ model: {
488
+ entity: "workflow_snapshot",
489
+ version: "1",
490
+ service: "mastra"
491
+ },
492
+ attributes: {
493
+ entity: {
494
+ type: "string",
495
+ required: true
496
+ },
497
+ ...baseAttributes,
498
+ workflow_name: {
499
+ type: "string",
500
+ required: true
501
+ },
502
+ run_id: {
503
+ type: "string",
504
+ required: true
505
+ },
506
+ snapshot: {
507
+ type: "string",
508
+ // JSON stringified
509
+ required: true,
510
+ // Stringify snapshot object on set
511
+ set: (value) => {
512
+ if (value && typeof value !== "string") {
513
+ return JSON.stringify(value);
514
+ }
515
+ return value;
516
+ },
517
+ // Parse JSON string to object on get
518
+ get: (value) => {
519
+ return value ? JSON.parse(value) : value;
520
+ }
521
+ },
522
+ resourceId: {
523
+ type: "string",
524
+ required: false
525
+ }
526
+ },
527
+ indexes: {
528
+ primary: {
529
+ pk: { field: "pk", composite: ["entity", "workflow_name"] },
530
+ sk: { field: "sk", composite: ["run_id"] }
531
+ },
532
+ // GSI to allow querying by run_id efficiently without knowing the workflow_name
533
+ gsi2: {
534
+ index: "gsi2",
535
+ pk: { field: "gsi2pk", composite: ["entity", "run_id"] },
536
+ sk: { field: "gsi2sk", composite: ["workflow_name"] }
537
+ }
538
+ }
539
+ });
540
+
541
+ // src/entities/index.ts
542
+ function getElectroDbService(client, tableName) {
543
+ return new Service(
544
+ {
545
+ thread: threadEntity,
546
+ message: messageEntity,
547
+ eval: evalEntity,
548
+ trace: traceEntity,
549
+ workflowSnapshot: workflowSnapshotEntity
550
+ },
551
+ {
552
+ client,
553
+ table: tableName
554
+ }
555
+ );
556
+ }
557
+
558
+ // src/storage/index.ts
559
+ var DynamoDBStore = class extends MastraStorage {
560
+ constructor({ name, config }) {
561
+ super({ name });
562
+ this.hasInitialized = null;
563
+ if (!config.tableName || typeof config.tableName !== "string" || config.tableName.trim() === "") {
564
+ throw new Error("DynamoDBStore: config.tableName must be provided and cannot be empty.");
565
+ }
566
+ if (!/^[a-zA-Z0-9_.-]{3,255}$/.test(config.tableName)) {
567
+ throw new Error(
568
+ `DynamoDBStore: config.tableName "${config.tableName}" contains invalid characters or is not between 3 and 255 characters long.`
569
+ );
570
+ }
571
+ const dynamoClient = new DynamoDBClient({
572
+ region: config.region || "us-east-1",
573
+ endpoint: config.endpoint,
574
+ credentials: config.credentials
575
+ });
576
+ this.tableName = config.tableName;
577
+ this.client = DynamoDBDocumentClient.from(dynamoClient);
578
+ this.service = getElectroDbService(this.client, this.tableName);
579
+ }
580
+ /**
581
+ * This method is modified for DynamoDB with ElectroDB single-table design.
582
+ * It assumes the table is created and managed externally via CDK/CloudFormation.
583
+ *
584
+ * This implementation only validates that the required table exists and is accessible.
585
+ * No table creation is attempted - we simply check if we can access the table.
586
+ */
587
+ async createTable({ tableName }) {
588
+ this.logger.debug("Validating access to externally managed table", { tableName, physicalTable: this.tableName });
589
+ try {
590
+ const tableExists = await this.validateTableExists();
591
+ if (!tableExists) {
592
+ this.logger.error(
593
+ `Table ${this.tableName} does not exist or is not accessible. It should be created via CDK/CloudFormation.`
594
+ );
595
+ throw new Error(
596
+ `Table ${this.tableName} does not exist or is not accessible. Ensure it's created via CDK/CloudFormation before using this store.`
597
+ );
598
+ }
599
+ this.logger.debug(`Table ${this.tableName} exists and is accessible`);
600
+ } catch (error) {
601
+ this.logger.error("Error validating table access", { tableName: this.tableName, error });
602
+ throw error;
603
+ }
604
+ }
605
+ /**
606
+ * Validates that the required DynamoDB table exists and is accessible.
607
+ * This does not check the table structure - it assumes the table
608
+ * was created with the correct structure via CDK/CloudFormation.
609
+ */
610
+ async validateTableExists() {
611
+ try {
612
+ const command = new DescribeTableCommand({
613
+ TableName: this.tableName
614
+ });
615
+ await this.client.send(command);
616
+ return true;
617
+ } catch (error) {
618
+ if (error.name === "ResourceNotFoundException") {
619
+ return false;
620
+ }
621
+ throw error;
622
+ }
623
+ }
624
+ /**
625
+ * Initialize storage, validating the externally managed table is accessible.
626
+ * For the single-table design, we only validate once that we can access
627
+ * the table that was created via CDK/CloudFormation.
628
+ */
629
+ async init() {
630
+ if (this.hasInitialized === null) {
631
+ this.hasInitialized = this._performInitializationAndStore();
632
+ }
633
+ try {
634
+ await this.hasInitialized;
635
+ } catch (error) {
636
+ throw error;
637
+ }
638
+ }
639
+ /**
640
+ * Performs the actual table validation and stores the promise.
641
+ * Handles resetting the stored promise on failure to allow retries.
642
+ */
643
+ _performInitializationAndStore() {
644
+ return this.validateTableExists().then((exists) => {
645
+ if (!exists) {
646
+ throw new Error(
647
+ `Table ${this.tableName} does not exist or is not accessible. Ensure it's created via CDK/CloudFormation before using this store.`
648
+ );
649
+ }
650
+ return true;
651
+ }).catch((err) => {
652
+ this.hasInitialized = null;
653
+ throw err;
654
+ });
655
+ }
656
+ /**
657
+ * Clear all items from a logical "table" (entity type)
658
+ */
659
+ async clearTable({ tableName }) {
660
+ this.logger.debug("DynamoDB clearTable called", { tableName });
661
+ const entityName = this.getEntityNameForTable(tableName);
662
+ if (!entityName || !this.service.entities[entityName]) {
663
+ throw new Error(`No entity defined for ${tableName}`);
664
+ }
665
+ try {
666
+ const result = await this.service.entities[entityName].scan.go({ pages: "all" });
667
+ if (!result.data.length) {
668
+ this.logger.debug(`No records found to clear for ${tableName}`);
669
+ return;
670
+ }
671
+ this.logger.debug(`Found ${result.data.length} records to delete for ${tableName}`);
672
+ const keysToDelete = result.data.map((item) => {
673
+ const key = { entity: entityName };
674
+ switch (entityName) {
675
+ case "thread":
676
+ if (!item.id) throw new Error(`Missing required key 'id' for entity 'thread'`);
677
+ key.id = item.id;
678
+ break;
679
+ case "message":
680
+ if (!item.id) throw new Error(`Missing required key 'id' for entity 'message'`);
681
+ key.id = item.id;
682
+ break;
683
+ case "workflowSnapshot":
684
+ if (!item.workflow_name)
685
+ throw new Error(`Missing required key 'workflow_name' for entity 'workflowSnapshot'`);
686
+ if (!item.run_id) throw new Error(`Missing required key 'run_id' for entity 'workflowSnapshot'`);
687
+ key.workflow_name = item.workflow_name;
688
+ key.run_id = item.run_id;
689
+ break;
690
+ case "eval":
691
+ if (!item.run_id) throw new Error(`Missing required key 'run_id' for entity 'eval'`);
692
+ key.run_id = item.run_id;
693
+ break;
694
+ case "trace":
695
+ if (!item.id) throw new Error(`Missing required key 'id' for entity 'trace'`);
696
+ key.id = item.id;
697
+ break;
698
+ default:
699
+ this.logger.warn(`Unknown entity type encountered during clearTable: ${entityName}`);
700
+ throw new Error(`Cannot construct delete key for unknown entity type: ${entityName}`);
701
+ }
702
+ return key;
703
+ });
704
+ const batchSize = 25;
705
+ for (let i = 0; i < keysToDelete.length; i += batchSize) {
706
+ const batchKeys = keysToDelete.slice(i, i + batchSize);
707
+ await this.service.entities[entityName].delete(batchKeys).go();
708
+ }
709
+ this.logger.debug(`Successfully cleared all records for ${tableName}`);
710
+ } catch (error) {
711
+ this.logger.error("Failed to clear table", { tableName, error });
712
+ throw error;
713
+ }
714
+ }
715
+ /**
716
+ * Insert a record into the specified "table" (entity)
717
+ */
718
+ async insert({ tableName, record }) {
719
+ this.logger.debug("DynamoDB insert called", { tableName });
720
+ const entityName = this.getEntityNameForTable(tableName);
721
+ if (!entityName || !this.service.entities[entityName]) {
722
+ throw new Error(`No entity defined for ${tableName}`);
723
+ }
724
+ try {
725
+ const dataToSave = { entity: entityName, ...record };
726
+ await this.service.entities[entityName].create(dataToSave).go();
727
+ } catch (error) {
728
+ this.logger.error("Failed to insert record", { tableName, error });
729
+ throw error;
730
+ }
731
+ }
732
+ /**
733
+ * Insert multiple records as a batch
734
+ */
735
+ async batchInsert({ tableName, records }) {
736
+ this.logger.debug("DynamoDB batchInsert called", { tableName, count: records.length });
737
+ const entityName = this.getEntityNameForTable(tableName);
738
+ if (!entityName || !this.service.entities[entityName]) {
739
+ throw new Error(`No entity defined for ${tableName}`);
740
+ }
741
+ const recordsToSave = records.map((rec) => ({ entity: entityName, ...rec }));
742
+ const batchSize = 25;
743
+ const batches = [];
744
+ for (let i = 0; i < recordsToSave.length; i += batchSize) {
745
+ const batch = recordsToSave.slice(i, i + batchSize);
746
+ batches.push(batch);
747
+ }
748
+ try {
749
+ for (const batch of batches) {
750
+ for (const recordData of batch) {
751
+ if (!recordData.entity) {
752
+ this.logger.error("Missing entity property in record data for batchInsert", { recordData, tableName });
753
+ throw new Error(`Internal error: Missing entity property during batchInsert for ${tableName}`);
754
+ }
755
+ this.logger.debug("Attempting to create record in batchInsert:", { entityName, recordData });
756
+ await this.service.entities[entityName].create(recordData).go();
757
+ }
758
+ }
759
+ } catch (error) {
760
+ this.logger.error("Failed to batch insert records", { tableName, error });
761
+ throw error;
762
+ }
763
+ }
764
+ /**
765
+ * Load a record by its keys
766
+ */
767
+ async load({ tableName, keys }) {
768
+ this.logger.debug("DynamoDB load called", { tableName, keys });
769
+ const entityName = this.getEntityNameForTable(tableName);
770
+ if (!entityName || !this.service.entities[entityName]) {
771
+ throw new Error(`No entity defined for ${tableName}`);
772
+ }
773
+ try {
774
+ const keyObject = { entity: entityName, ...keys };
775
+ const result = await this.service.entities[entityName].get(keyObject).go();
776
+ if (!result.data) {
777
+ return null;
778
+ }
779
+ let data = result.data;
780
+ return data;
781
+ } catch (error) {
782
+ this.logger.error("Failed to load record", { tableName, keys, error });
783
+ throw error;
784
+ }
785
+ }
786
+ // Thread operations
787
+ async getThreadById({ threadId }) {
788
+ this.logger.debug("Getting thread by ID", { threadId });
789
+ try {
790
+ const result = await this.service.entities.thread.get({ entity: "thread", id: threadId }).go();
791
+ if (!result.data) {
792
+ return null;
793
+ }
794
+ const data = result.data;
795
+ return {
796
+ ...data
797
+ // metadata: data.metadata ? JSON.parse(data.metadata) : undefined, // REMOVED by AI
798
+ // metadata is already transformed by the entity's getter
799
+ };
800
+ } catch (error) {
801
+ this.logger.error("Failed to get thread by ID", { threadId, error });
802
+ throw error;
803
+ }
804
+ }
805
+ async getThreadsByResourceId({ resourceId }) {
806
+ this.logger.debug("Getting threads by resource ID", { resourceId });
807
+ try {
808
+ const result = await this.service.entities.thread.query.byResource({ entity: "thread", resourceId }).go();
809
+ if (!result.data.length) {
810
+ return [];
811
+ }
812
+ return result.data.map((data) => ({
813
+ ...data
814
+ // metadata: data.metadata ? JSON.parse(data.metadata) : undefined, // REMOVED by AI
815
+ // metadata is already transformed by the entity's getter
816
+ }));
817
+ } catch (error) {
818
+ this.logger.error("Failed to get threads by resource ID", { resourceId, error });
819
+ throw error;
820
+ }
821
+ }
822
+ async saveThread({ thread }) {
823
+ this.logger.debug("Saving thread", { threadId: thread.id });
824
+ const now = /* @__PURE__ */ new Date();
825
+ const threadData = {
826
+ entity: "thread",
827
+ id: thread.id,
828
+ resourceId: thread.resourceId,
829
+ title: thread.title || `Thread ${thread.id}`,
830
+ createdAt: thread.createdAt?.toISOString() || now.toISOString(),
831
+ updatedAt: now.toISOString(),
832
+ metadata: thread.metadata ? JSON.stringify(thread.metadata) : void 0
833
+ };
834
+ try {
835
+ await this.service.entities.thread.create(threadData).go();
836
+ return {
837
+ id: thread.id,
838
+ resourceId: thread.resourceId,
839
+ title: threadData.title,
840
+ createdAt: thread.createdAt || now,
841
+ updatedAt: now,
842
+ metadata: thread.metadata
843
+ };
844
+ } catch (error) {
845
+ this.logger.error("Failed to save thread", { threadId: thread.id, error });
846
+ throw error;
847
+ }
848
+ }
849
+ async updateThread({
850
+ id,
851
+ title,
852
+ metadata
853
+ }) {
854
+ this.logger.debug("Updating thread", { threadId: id });
855
+ try {
856
+ const existingThread = await this.getThreadById({ threadId: id });
857
+ if (!existingThread) {
858
+ throw new Error(`Thread not found: ${id}`);
859
+ }
860
+ const now = /* @__PURE__ */ new Date();
861
+ const updateData = {
862
+ updatedAt: now.toISOString()
863
+ };
864
+ if (title) {
865
+ updateData.title = title;
866
+ }
867
+ if (metadata) {
868
+ updateData.metadata = JSON.stringify(metadata);
869
+ }
870
+ await this.service.entities.thread.update({ entity: "thread", id }).set(updateData).go();
871
+ return {
872
+ ...existingThread,
873
+ title: title || existingThread.title,
874
+ metadata: metadata || existingThread.metadata,
875
+ updatedAt: now
876
+ };
877
+ } catch (error) {
878
+ this.logger.error("Failed to update thread", { threadId: id, error });
879
+ throw error;
880
+ }
881
+ }
882
+ async deleteThread({ threadId }) {
883
+ this.logger.debug("Deleting thread", { threadId });
884
+ try {
885
+ await this.service.entities.thread.delete({ entity: "thread", id: threadId }).go();
886
+ } catch (error) {
887
+ this.logger.error("Failed to delete thread", { threadId, error });
888
+ throw error;
889
+ }
890
+ }
891
+ // Message operations
892
+ async getMessages(args) {
893
+ const { threadId, selectBy } = args;
894
+ this.logger.debug("Getting messages", { threadId, selectBy });
895
+ try {
896
+ const query = this.service.entities.message.query.byThread({ entity: "message", threadId });
897
+ if (selectBy?.last && typeof selectBy.last === "number") {
898
+ const results2 = await query.go({ limit: selectBy.last, reverse: true });
899
+ return results2.data.map((data) => this.parseMessageData(data));
900
+ }
901
+ const results = await query.go();
902
+ return results.data.map((data) => this.parseMessageData(data));
903
+ } catch (error) {
904
+ this.logger.error("Failed to get messages", { threadId, error });
905
+ throw error;
906
+ }
907
+ }
908
+ async saveMessages({ messages }) {
909
+ this.logger.debug("Saving messages", { count: messages.length });
910
+ if (!messages.length) {
911
+ return [];
912
+ }
913
+ const messagesToSave = messages.map((msg) => {
914
+ const now = (/* @__PURE__ */ new Date()).toISOString();
915
+ return {
916
+ entity: "message",
917
+ // Add entity type
918
+ id: msg.id,
919
+ threadId: msg.threadId,
920
+ role: msg.role,
921
+ type: msg.type,
922
+ resourceId: msg.resourceId,
923
+ // Ensure complex fields are stringified if not handled by attribute setters
924
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
925
+ toolCallArgs: msg.toolCallArgs ? JSON.stringify(msg.toolCallArgs) : void 0,
926
+ toolCallIds: msg.toolCallIds ? JSON.stringify(msg.toolCallIds) : void 0,
927
+ toolNames: msg.toolNames ? JSON.stringify(msg.toolNames) : void 0,
928
+ createdAt: msg.createdAt?.toISOString() || now,
929
+ updatedAt: now
930
+ // Add updatedAt
931
+ };
932
+ });
933
+ try {
934
+ const batchSize = 25;
935
+ const batches = [];
936
+ for (let i = 0; i < messagesToSave.length; i += batchSize) {
937
+ const batch = messagesToSave.slice(i, i + batchSize);
938
+ batches.push(batch);
939
+ }
940
+ for (const batch of batches) {
941
+ for (const messageData of batch) {
942
+ if (!messageData.entity) {
943
+ this.logger.error("Missing entity property in message data for create", { messageData });
944
+ throw new Error("Internal error: Missing entity property during saveMessages");
945
+ }
946
+ await this.service.entities.message.create(messageData).go();
947
+ }
948
+ }
949
+ return messages;
950
+ } catch (error) {
951
+ this.logger.error("Failed to save messages", { error });
952
+ throw error;
953
+ }
954
+ }
955
+ // Helper function to parse message data (handle JSON fields)
956
+ parseMessageData(data) {
957
+ return {
958
+ ...data,
959
+ // Ensure dates are Date objects if needed (ElectroDB might return strings)
960
+ createdAt: data.createdAt ? new Date(data.createdAt) : void 0,
961
+ updatedAt: data.updatedAt ? new Date(data.updatedAt) : void 0
962
+ // Other fields like content, toolCallArgs etc. are assumed to be correctly
963
+ // transformed by the ElectroDB entity getters.
964
+ };
965
+ }
966
+ // Trace operations
967
+ async getTraces(args) {
968
+ const { name, scope, page, perPage } = args;
969
+ this.logger.debug("Getting traces", { name, scope, page, perPage });
970
+ try {
971
+ let query;
972
+ if (name) {
973
+ query = this.service.entities.trace.query.byName({ entity: "trace", name });
974
+ } else if (scope) {
975
+ query = this.service.entities.trace.query.byScope({ entity: "trace", scope });
976
+ } else {
977
+ this.logger.warn("Performing a scan operation on traces - consider using a more specific query");
978
+ query = this.service.entities.trace.scan;
979
+ }
980
+ let items = [];
981
+ let cursor = null;
982
+ let pagesFetched = 0;
983
+ const startPage = page > 0 ? page : 1;
984
+ do {
985
+ const results = await query.go({ cursor, limit: perPage });
986
+ pagesFetched++;
987
+ if (pagesFetched === startPage) {
988
+ items = results.data;
989
+ break;
990
+ }
991
+ cursor = results.cursor;
992
+ if (!cursor && results.data.length > 0 && pagesFetched < startPage) {
993
+ break;
994
+ }
995
+ } while (cursor && pagesFetched < startPage);
996
+ return items;
997
+ } catch (error) {
998
+ this.logger.error("Failed to get traces", { error });
999
+ throw error;
1000
+ }
1001
+ }
1002
+ async batchTraceInsert({ records }) {
1003
+ this.logger.debug("Batch inserting traces", { count: records.length });
1004
+ if (!records.length) {
1005
+ return;
1006
+ }
1007
+ try {
1008
+ const recordsToSave = records.map((rec) => ({ entity: "trace", ...rec }));
1009
+ await this.batchInsert({
1010
+ tableName: TABLE_TRACES,
1011
+ records: recordsToSave
1012
+ // Pass records with 'entity' included
1013
+ });
1014
+ } catch (error) {
1015
+ this.logger.error("Failed to batch insert traces", { error });
1016
+ throw error;
1017
+ }
1018
+ }
1019
+ // Workflow operations
1020
+ async persistWorkflowSnapshot({
1021
+ workflowName,
1022
+ runId,
1023
+ snapshot
1024
+ }) {
1025
+ this.logger.debug("Persisting workflow snapshot", { workflowName, runId });
1026
+ try {
1027
+ const resourceId = "resourceId" in snapshot ? snapshot.resourceId : void 0;
1028
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1029
+ const data = {
1030
+ entity: "workflow_snapshot",
1031
+ // Add entity type
1032
+ workflow_name: workflowName,
1033
+ run_id: runId,
1034
+ snapshot: JSON.stringify(snapshot),
1035
+ // Stringify the snapshot object
1036
+ createdAt: now,
1037
+ updatedAt: now,
1038
+ resourceId
1039
+ };
1040
+ await this.service.entities.workflowSnapshot.create(data).go();
1041
+ } catch (error) {
1042
+ this.logger.error("Failed to persist workflow snapshot", { workflowName, runId, error });
1043
+ throw error;
1044
+ }
1045
+ }
1046
+ async loadWorkflowSnapshot({
1047
+ workflowName,
1048
+ runId
1049
+ }) {
1050
+ this.logger.debug("Loading workflow snapshot", { workflowName, runId });
1051
+ try {
1052
+ const result = await this.service.entities.workflowSnapshot.get({
1053
+ entity: "workflow_snapshot",
1054
+ // Add entity type
1055
+ workflow_name: workflowName,
1056
+ run_id: runId
1057
+ }).go();
1058
+ if (!result.data?.snapshot) {
1059
+ return null;
1060
+ }
1061
+ return result.data.snapshot;
1062
+ } catch (error) {
1063
+ this.logger.error("Failed to load workflow snapshot", { workflowName, runId, error });
1064
+ throw error;
1065
+ }
1066
+ }
1067
+ async getWorkflowRuns(args) {
1068
+ this.logger.debug("Getting workflow runs", { args });
1069
+ try {
1070
+ const limit = args?.limit || 10;
1071
+ const offset = args?.offset || 0;
1072
+ let query;
1073
+ if (args?.workflowName) {
1074
+ query = this.service.entities.workflowSnapshot.query.primary({
1075
+ entity: "workflow_snapshot",
1076
+ // Add entity type
1077
+ workflow_name: args.workflowName
1078
+ });
1079
+ } else {
1080
+ this.logger.warn("Performing a scan operation on workflow snapshots - consider using a more specific query");
1081
+ query = this.service.entities.workflowSnapshot.scan;
1082
+ }
1083
+ const allMatchingSnapshots = [];
1084
+ let cursor = null;
1085
+ const DYNAMODB_PAGE_SIZE = 100;
1086
+ do {
1087
+ const pageResults = await query.go({
1088
+ limit: DYNAMODB_PAGE_SIZE,
1089
+ cursor
1090
+ });
1091
+ if (pageResults.data && pageResults.data.length > 0) {
1092
+ let pageFilteredData = pageResults.data;
1093
+ if (args?.fromDate || args?.toDate) {
1094
+ pageFilteredData = pageFilteredData.filter((snapshot) => {
1095
+ const createdAt = new Date(snapshot.createdAt);
1096
+ if (args.fromDate && createdAt < args.fromDate) {
1097
+ return false;
1098
+ }
1099
+ if (args.toDate && createdAt > args.toDate) {
1100
+ return false;
1101
+ }
1102
+ return true;
1103
+ });
1104
+ }
1105
+ if (args?.resourceId) {
1106
+ pageFilteredData = pageFilteredData.filter((snapshot) => {
1107
+ return snapshot.resourceId === args.resourceId;
1108
+ });
1109
+ }
1110
+ allMatchingSnapshots.push(...pageFilteredData);
1111
+ }
1112
+ cursor = pageResults.cursor;
1113
+ } while (cursor);
1114
+ if (!allMatchingSnapshots.length) {
1115
+ return { runs: [], total: 0 };
1116
+ }
1117
+ const total = allMatchingSnapshots.length;
1118
+ const paginatedData = allMatchingSnapshots.slice(offset, offset + limit);
1119
+ const runs = paginatedData.map((snapshot) => this.formatWorkflowRun(snapshot));
1120
+ return {
1121
+ runs,
1122
+ total
1123
+ };
1124
+ } catch (error) {
1125
+ this.logger.error("Failed to get workflow runs", { error });
1126
+ throw error;
1127
+ }
1128
+ }
1129
+ async getWorkflowRunById(args) {
1130
+ const { runId, workflowName } = args;
1131
+ this.logger.debug("Getting workflow run by ID", { runId, workflowName });
1132
+ try {
1133
+ if (workflowName) {
1134
+ this.logger.debug("WorkflowName provided, using direct GET operation.");
1135
+ const result2 = await this.service.entities.workflowSnapshot.get({
1136
+ entity: "workflow_snapshot",
1137
+ // Entity type for PK
1138
+ workflow_name: workflowName,
1139
+ run_id: runId
1140
+ }).go();
1141
+ if (!result2.data) {
1142
+ return null;
1143
+ }
1144
+ const snapshot2 = result2.data.snapshot;
1145
+ return {
1146
+ workflowName: result2.data.workflow_name,
1147
+ runId: result2.data.run_id,
1148
+ snapshot: snapshot2,
1149
+ createdAt: new Date(result2.data.createdAt),
1150
+ updatedAt: new Date(result2.data.updatedAt),
1151
+ resourceId: result2.data.resourceId
1152
+ };
1153
+ }
1154
+ this.logger.debug(
1155
+ 'WorkflowName not provided. Attempting to find workflow run by runId using GSI. Ensure GSI (e.g., "byRunId") is defined on the workflowSnapshot entity with run_id as its key and provisioned in DynamoDB.'
1156
+ );
1157
+ const result = await this.service.entities.workflowSnapshot.query.gsi2({ entity: "workflow_snapshot", run_id: runId }).go();
1158
+ const matchingRunDbItem = result.data && result.data.length > 0 ? result.data[0] : null;
1159
+ if (!matchingRunDbItem) {
1160
+ return null;
1161
+ }
1162
+ const snapshot = matchingRunDbItem.snapshot;
1163
+ return {
1164
+ workflowName: matchingRunDbItem.workflow_name,
1165
+ runId: matchingRunDbItem.run_id,
1166
+ snapshot,
1167
+ createdAt: new Date(matchingRunDbItem.createdAt),
1168
+ updatedAt: new Date(matchingRunDbItem.updatedAt),
1169
+ resourceId: matchingRunDbItem.resourceId
1170
+ };
1171
+ } catch (error) {
1172
+ this.logger.error("Failed to get workflow run by ID", { runId, workflowName, error });
1173
+ throw error;
1174
+ }
1175
+ }
1176
+ // Helper function to format workflow run
1177
+ formatWorkflowRun(snapshotData) {
1178
+ return {
1179
+ workflowName: snapshotData.workflow_name,
1180
+ runId: snapshotData.run_id,
1181
+ snapshot: snapshotData.snapshot,
1182
+ createdAt: new Date(snapshotData.createdAt),
1183
+ updatedAt: new Date(snapshotData.updatedAt),
1184
+ resourceId: snapshotData.resourceId
1185
+ };
1186
+ }
1187
+ // Helper methods for entity/table mapping
1188
+ getEntityNameForTable(tableName) {
1189
+ const mapping = {
1190
+ [TABLE_THREADS]: "thread",
1191
+ [TABLE_MESSAGES]: "message",
1192
+ [TABLE_WORKFLOW_SNAPSHOT]: "workflowSnapshot",
1193
+ [TABLE_EVALS]: "eval",
1194
+ [TABLE_TRACES]: "trace"
1195
+ };
1196
+ return mapping[tableName] || null;
1197
+ }
1198
+ // Eval operations
1199
+ async getEvalsByAgentName(agentName, type) {
1200
+ this.logger.debug("Getting evals for agent", { agentName, type });
1201
+ try {
1202
+ const query = this.service.entities.eval.query.byAgent({ entity: "eval", agent_name: agentName });
1203
+ const results = await query.go({ order: "desc", limit: 100 });
1204
+ if (!results.data.length) {
1205
+ return [];
1206
+ }
1207
+ let filteredData = results.data;
1208
+ if (type) {
1209
+ filteredData = filteredData.filter((evalRecord) => {
1210
+ try {
1211
+ const testInfo = evalRecord.test_info && typeof evalRecord.test_info === "string" ? JSON.parse(evalRecord.test_info) : void 0;
1212
+ if (type === "test" && !testInfo) {
1213
+ return false;
1214
+ }
1215
+ if (type === "live" && testInfo) {
1216
+ return false;
1217
+ }
1218
+ } catch (e) {
1219
+ this.logger.warn("Failed to parse test_info during filtering", { record: evalRecord, error: e });
1220
+ }
1221
+ return true;
1222
+ });
1223
+ }
1224
+ return filteredData.map((evalRecord) => {
1225
+ try {
1226
+ return {
1227
+ input: evalRecord.input,
1228
+ output: evalRecord.output,
1229
+ // Safely parse result and test_info
1230
+ result: evalRecord.result && typeof evalRecord.result === "string" ? JSON.parse(evalRecord.result) : void 0,
1231
+ agentName: evalRecord.agent_name,
1232
+ createdAt: evalRecord.created_at,
1233
+ // Keep as string from DDB?
1234
+ metricName: evalRecord.metric_name,
1235
+ instructions: evalRecord.instructions,
1236
+ runId: evalRecord.run_id,
1237
+ globalRunId: evalRecord.global_run_id,
1238
+ testInfo: evalRecord.test_info && typeof evalRecord.test_info === "string" ? JSON.parse(evalRecord.test_info) : void 0
1239
+ };
1240
+ } catch (parseError) {
1241
+ this.logger.error("Failed to parse eval record", { record: evalRecord, error: parseError });
1242
+ return {
1243
+ agentName: evalRecord.agent_name,
1244
+ createdAt: evalRecord.created_at,
1245
+ runId: evalRecord.run_id,
1246
+ globalRunId: evalRecord.global_run_id
1247
+ };
1248
+ }
1249
+ });
1250
+ } catch (error) {
1251
+ this.logger.error("Failed to get evals by agent name", { agentName, type, error });
1252
+ throw error;
1253
+ }
1254
+ }
1255
+ /**
1256
+ * Closes the DynamoDB client connection and cleans up resources.
1257
+ * Should be called when the store is no longer needed, e.g., at the end of tests or application shutdown.
1258
+ */
1259
+ async close() {
1260
+ this.logger.debug("Closing DynamoDB client for store:", { name: this.name });
1261
+ try {
1262
+ this.client.destroy();
1263
+ this.logger.debug("DynamoDB client closed successfully for store:", { name: this.name });
1264
+ } catch (error) {
1265
+ this.logger.error("Error closing DynamoDB client for store:", { name: this.name, error });
1266
+ throw error;
1267
+ }
1268
+ }
1269
+ };
1270
+
1271
+ export { DynamoDBStore };