@mastra/lance 0.2.9 → 0.2.11-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.
@@ -1,1000 +0,0 @@
1
- import type { Connection } from '@lancedb/lancedb';
2
- import { MessageList } from '@mastra/core/agent';
3
- import type { MastraMessageContentV2 } from '@mastra/core/agent';
4
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
5
- import type { MastraMessageV1, MastraMessageV2, StorageThreadType } from '@mastra/core/memory';
6
- import {
7
- MemoryStorage,
8
- resolveMessageLimit,
9
- TABLE_MESSAGES,
10
- TABLE_RESOURCES,
11
- TABLE_THREADS,
12
- } from '@mastra/core/storage';
13
- import type { PaginationInfo, StorageGetMessagesArg, StorageResourceType } from '@mastra/core/storage';
14
- import type { StoreOperationsLance } from '../operations';
15
- import { getTableSchema, processResultWithTypeConversion } from '../utils';
16
-
17
- export class StoreMemoryLance extends MemoryStorage {
18
- private client: Connection;
19
- private operations: StoreOperationsLance;
20
- constructor({ client, operations }: { client: Connection; operations: StoreOperationsLance }) {
21
- super();
22
- this.client = client;
23
- this.operations = operations;
24
- }
25
-
26
- async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
27
- try {
28
- const thread = await this.operations.load({ tableName: TABLE_THREADS, keys: { id: threadId } });
29
-
30
- if (!thread) {
31
- return null;
32
- }
33
-
34
- return {
35
- ...thread,
36
- createdAt: new Date(thread.createdAt),
37
- updatedAt: new Date(thread.updatedAt),
38
- };
39
- } catch (error: any) {
40
- throw new MastraError(
41
- {
42
- id: 'LANCE_STORE_GET_THREAD_BY_ID_FAILED',
43
- domain: ErrorDomain.STORAGE,
44
- category: ErrorCategory.THIRD_PARTY,
45
- },
46
- error,
47
- );
48
- }
49
- }
50
-
51
- async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
52
- try {
53
- const table = await this.client.openTable(TABLE_THREADS);
54
- // fetches all threads with the given resourceId
55
- const query = table.query().where(`\`resourceId\` = '${resourceId}'`);
56
-
57
- const records = await query.toArray();
58
- return processResultWithTypeConversion(
59
- records,
60
- await getTableSchema({ tableName: TABLE_THREADS, client: this.client }),
61
- ) as StorageThreadType[];
62
- } catch (error: any) {
63
- throw new MastraError(
64
- {
65
- id: 'LANCE_STORE_GET_THREADS_BY_RESOURCE_ID_FAILED',
66
- domain: ErrorDomain.STORAGE,
67
- category: ErrorCategory.THIRD_PARTY,
68
- },
69
- error,
70
- );
71
- }
72
- }
73
-
74
- /**
75
- * Saves a thread to the database. This function doesn't overwrite existing threads.
76
- * @param thread - The thread to save
77
- * @returns The saved thread
78
- */
79
- async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
80
- try {
81
- const record = { ...thread, metadata: JSON.stringify(thread.metadata) };
82
- const table = await this.client.openTable(TABLE_THREADS);
83
- await table.add([record], { mode: 'append' });
84
-
85
- return thread;
86
- } catch (error: any) {
87
- throw new MastraError(
88
- {
89
- id: 'LANCE_STORE_SAVE_THREAD_FAILED',
90
- domain: ErrorDomain.STORAGE,
91
- category: ErrorCategory.THIRD_PARTY,
92
- },
93
- error,
94
- );
95
- }
96
- }
97
-
98
- async updateThread({
99
- id,
100
- title,
101
- metadata,
102
- }: {
103
- id: string;
104
- title: string;
105
- metadata: Record<string, unknown>;
106
- }): Promise<StorageThreadType> {
107
- const maxRetries = 5;
108
-
109
- for (let attempt = 0; attempt < maxRetries; attempt++) {
110
- try {
111
- // Get current state atomically
112
- const current = await this.getThreadById({ threadId: id });
113
- if (!current) {
114
- throw new Error(`Thread with id ${id} not found`);
115
- }
116
-
117
- // Merge metadata
118
- const mergedMetadata = { ...current.metadata, ...metadata };
119
-
120
- // Update atomically
121
- const record = {
122
- id,
123
- title,
124
- metadata: JSON.stringify(mergedMetadata),
125
- updatedAt: new Date().getTime(),
126
- };
127
-
128
- const table = await this.client.openTable(TABLE_THREADS);
129
- await table.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([record]);
130
-
131
- const updatedThread = await this.getThreadById({ threadId: id });
132
- if (!updatedThread) {
133
- throw new Error(`Failed to retrieve updated thread ${id}`);
134
- }
135
- return updatedThread;
136
- } catch (error: any) {
137
- if (error.message?.includes('Commit conflict') && attempt < maxRetries - 1) {
138
- // Wait with exponential backoff before retrying
139
- const delay = Math.pow(2, attempt) * 10; // 10ms, 20ms, 40ms
140
- await new Promise(resolve => setTimeout(resolve, delay));
141
- continue;
142
- }
143
-
144
- // If it's not a commit conflict or we've exhausted retries, throw the error
145
- throw new MastraError(
146
- {
147
- id: 'LANCE_STORE_UPDATE_THREAD_FAILED',
148
- domain: ErrorDomain.STORAGE,
149
- category: ErrorCategory.THIRD_PARTY,
150
- },
151
- error,
152
- );
153
- }
154
- }
155
-
156
- // This should never be reached, but just in case
157
- throw new MastraError(
158
- {
159
- id: 'LANCE_STORE_UPDATE_THREAD_FAILED',
160
- domain: ErrorDomain.STORAGE,
161
- category: ErrorCategory.THIRD_PARTY,
162
- },
163
- new Error('All retries exhausted'),
164
- );
165
- }
166
-
167
- async deleteThread({ threadId }: { threadId: string }): Promise<void> {
168
- try {
169
- // Delete the thread
170
- const table = await this.client.openTable(TABLE_THREADS);
171
- await table.delete(`id = '${threadId}'`);
172
-
173
- // Delete all messages with the matching thread_id
174
- const messagesTable = await this.client.openTable(TABLE_MESSAGES);
175
- await messagesTable.delete(`thread_id = '${threadId}'`);
176
- } catch (error: any) {
177
- throw new MastraError(
178
- {
179
- id: 'LANCE_STORE_DELETE_THREAD_FAILED',
180
- domain: ErrorDomain.STORAGE,
181
- category: ErrorCategory.THIRD_PARTY,
182
- },
183
- error,
184
- );
185
- }
186
- }
187
-
188
- private normalizeMessage(message: any): MastraMessageV1 | MastraMessageV2 {
189
- const { thread_id, ...rest } = message;
190
- return {
191
- ...rest,
192
- threadId: thread_id,
193
- content:
194
- typeof message.content === 'string'
195
- ? (() => {
196
- try {
197
- return JSON.parse(message.content);
198
- } catch {
199
- return message.content;
200
- }
201
- })()
202
- : message.content,
203
- };
204
- }
205
-
206
- public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
207
- public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
208
- public async getMessages({
209
- threadId,
210
- resourceId,
211
- selectBy,
212
- format,
213
- threadConfig,
214
- }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
215
- try {
216
- if (threadConfig) {
217
- throw new Error('ThreadConfig is not supported by LanceDB storage');
218
- }
219
- const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
220
- const table = await this.client.openTable(TABLE_MESSAGES);
221
-
222
- let allRecords: any[] = [];
223
-
224
- // Handle selectBy.include for cross-thread context retrieval
225
- if (selectBy?.include && selectBy.include.length > 0) {
226
- // Get all unique thread IDs from include items
227
- const threadIds = [...new Set(selectBy.include.map(item => item.threadId))];
228
-
229
- // Fetch all messages from all relevant threads
230
- for (const threadId of threadIds) {
231
- const threadQuery = table.query().where(`thread_id = '${threadId}'`);
232
- let threadRecords = await threadQuery.toArray();
233
- allRecords.push(...threadRecords);
234
- }
235
- } else {
236
- // Regular single-thread query
237
- let query = table.query().where(`\`thread_id\` = '${threadId}'`);
238
- allRecords = await query.toArray();
239
- }
240
-
241
- // Sort the records chronologically
242
- allRecords.sort((a, b) => {
243
- const dateA = new Date(a.createdAt).getTime();
244
- const dateB = new Date(b.createdAt).getTime();
245
- return dateA - dateB; // Ascending order
246
- });
247
-
248
- // Process the include.withPreviousMessages and include.withNextMessages if specified
249
- if (selectBy?.include && selectBy.include.length > 0) {
250
- allRecords = this.processMessagesWithContext(allRecords, selectBy.include);
251
- }
252
-
253
- // If we're fetching the last N messages, take only the last N after sorting
254
- if (limit !== Number.MAX_SAFE_INTEGER) {
255
- allRecords = allRecords.slice(-limit);
256
- }
257
-
258
- const messages = processResultWithTypeConversion(
259
- allRecords,
260
- await getTableSchema({ tableName: TABLE_MESSAGES, client: this.client }),
261
- );
262
-
263
- const list = new MessageList({ threadId, resourceId }).add(messages.map(this.normalizeMessage), 'memory');
264
- if (format === 'v2') return list.get.all.v2();
265
- return list.get.all.v1();
266
- } catch (error: any) {
267
- throw new MastraError(
268
- {
269
- id: 'LANCE_STORE_GET_MESSAGES_FAILED',
270
- domain: ErrorDomain.STORAGE,
271
- category: ErrorCategory.THIRD_PARTY,
272
- },
273
- error,
274
- );
275
- }
276
- }
277
-
278
- public async getMessagesById({
279
- messageIds,
280
- format,
281
- }: {
282
- messageIds: string[];
283
- format: 'v1';
284
- }): Promise<MastraMessageV1[]>;
285
- public async getMessagesById({
286
- messageIds,
287
- format,
288
- }: {
289
- messageIds: string[];
290
- format?: 'v2';
291
- }): Promise<MastraMessageV2[]>;
292
- public async getMessagesById({
293
- messageIds,
294
- format,
295
- }: {
296
- messageIds: string[];
297
- format?: 'v1' | 'v2';
298
- }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
299
- if (messageIds.length === 0) return [];
300
- try {
301
- const table = await this.client.openTable(TABLE_MESSAGES);
302
-
303
- const quotedIds = messageIds.map(id => `'${id}'`).join(', ');
304
- const allRecords = await table.query().where(`id IN (${quotedIds})`).toArray();
305
-
306
- const messages = processResultWithTypeConversion(
307
- allRecords,
308
- await getTableSchema({ tableName: TABLE_MESSAGES, client: this.client }),
309
- );
310
-
311
- const list = new MessageList().add(messages.map(this.normalizeMessage), 'memory');
312
- if (format === `v1`) return list.get.all.v1();
313
- return list.get.all.v2();
314
- } catch (error: any) {
315
- throw new MastraError(
316
- {
317
- id: 'LANCE_STORE_GET_MESSAGES_BY_ID_FAILED',
318
- domain: ErrorDomain.STORAGE,
319
- category: ErrorCategory.THIRD_PARTY,
320
- details: {
321
- messageIds: JSON.stringify(messageIds),
322
- },
323
- },
324
- error,
325
- );
326
- }
327
- }
328
-
329
- async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
330
- async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
331
- async saveMessages(
332
- args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
333
- ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
334
- try {
335
- const { messages, format = 'v1' } = args;
336
- if (messages.length === 0) {
337
- return [];
338
- }
339
-
340
- const threadId = messages[0]?.threadId;
341
-
342
- if (!threadId) {
343
- throw new Error('Thread ID is required');
344
- }
345
-
346
- // Validate all messages before saving
347
- for (const message of messages) {
348
- if (!message.id) {
349
- throw new Error('Message ID is required');
350
- }
351
- if (!message.threadId) {
352
- throw new Error('Thread ID is required for all messages');
353
- }
354
- if (message.resourceId === null || message.resourceId === undefined) {
355
- throw new Error('Resource ID cannot be null or undefined');
356
- }
357
- if (!message.content) {
358
- throw new Error('Message content is required');
359
- }
360
- }
361
-
362
- const transformedMessages = messages.map((message: MastraMessageV2 | MastraMessageV1) => {
363
- const { threadId, type, ...rest } = message;
364
- return {
365
- ...rest,
366
- thread_id: threadId,
367
- type: type ?? 'v2',
368
- content: JSON.stringify(message.content),
369
- };
370
- });
371
-
372
- const table = await this.client.openTable(TABLE_MESSAGES);
373
- await table.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(transformedMessages);
374
-
375
- // Update the thread's updatedAt timestamp
376
- const threadsTable = await this.client.openTable(TABLE_THREADS);
377
- const currentTime = new Date().getTime();
378
- const updateRecord = { id: threadId, updatedAt: currentTime };
379
- await threadsTable.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([updateRecord]);
380
-
381
- const list = new MessageList().add(messages, 'memory');
382
- if (format === `v2`) return list.get.all.v2();
383
- return list.get.all.v1();
384
- } catch (error: any) {
385
- throw new MastraError(
386
- {
387
- id: 'LANCE_STORE_SAVE_MESSAGES_FAILED',
388
- domain: ErrorDomain.STORAGE,
389
- category: ErrorCategory.THIRD_PARTY,
390
- },
391
- error,
392
- );
393
- }
394
- }
395
-
396
- async getThreadsByResourceIdPaginated(args: {
397
- resourceId: string;
398
- page?: number;
399
- perPage?: number;
400
- }): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
401
- try {
402
- const { resourceId, page = 0, perPage = 10 } = args;
403
- const table = await this.client.openTable(TABLE_THREADS);
404
-
405
- // Get total count
406
- const total = await table.countRows(`\`resourceId\` = '${resourceId}'`);
407
-
408
- // Get paginated results
409
- const query = table.query().where(`\`resourceId\` = '${resourceId}'`);
410
- const offset = page * perPage;
411
- query.limit(perPage);
412
- if (offset > 0) {
413
- query.offset(offset);
414
- }
415
-
416
- const records = await query.toArray();
417
-
418
- // Sort by updatedAt descending (most recent first)
419
- records.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
420
-
421
- const schema = await getTableSchema({ tableName: TABLE_THREADS, client: this.client });
422
- const threads = records.map(record => processResultWithTypeConversion(record, schema)) as StorageThreadType[];
423
-
424
- return {
425
- threads,
426
- total,
427
- page,
428
- perPage,
429
- hasMore: total > (page + 1) * perPage,
430
- };
431
- } catch (error: any) {
432
- throw new MastraError(
433
- {
434
- id: 'LANCE_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
435
- domain: ErrorDomain.STORAGE,
436
- category: ErrorCategory.THIRD_PARTY,
437
- },
438
- error,
439
- );
440
- }
441
- }
442
-
443
- /**
444
- * Processes messages to include context messages based on withPreviousMessages and withNextMessages
445
- * @param records - The sorted array of records to process
446
- * @param include - The array of include specifications with context parameters
447
- * @returns The processed array with context messages included
448
- */
449
- private processMessagesWithContext(
450
- records: any[],
451
- include: { id: string; withPreviousMessages?: number; withNextMessages?: number }[],
452
- ): any[] {
453
- const messagesWithContext = include.filter(item => item.withPreviousMessages || item.withNextMessages);
454
-
455
- if (messagesWithContext.length === 0) {
456
- return records;
457
- }
458
-
459
- // Create a map of message id to index in the sorted array for quick lookup
460
- const messageIndexMap = new Map<string, number>();
461
- records.forEach((message, index) => {
462
- messageIndexMap.set(message.id, index);
463
- });
464
-
465
- // Keep track of additional indices to include
466
- const additionalIndices = new Set<number>();
467
-
468
- for (const item of messagesWithContext) {
469
- const messageIndex = messageIndexMap.get(item.id);
470
-
471
- if (messageIndex !== undefined) {
472
- // Add previous messages if requested
473
- if (item.withPreviousMessages) {
474
- const startIdx = Math.max(0, messageIndex - item.withPreviousMessages);
475
- for (let i = startIdx; i < messageIndex; i++) {
476
- additionalIndices.add(i);
477
- }
478
- }
479
-
480
- // Add next messages if requested
481
- if (item.withNextMessages) {
482
- const endIdx = Math.min(records.length - 1, messageIndex + item.withNextMessages);
483
- for (let i = messageIndex + 1; i <= endIdx; i++) {
484
- additionalIndices.add(i);
485
- }
486
- }
487
- }
488
- }
489
-
490
- // If we need to include additional messages, create a new set of records
491
- if (additionalIndices.size === 0) {
492
- return records;
493
- }
494
-
495
- // Get IDs of the records that matched the original query
496
- const originalMatchIds = new Set(include.map(item => item.id));
497
-
498
- // Create a set of all indices we need to include
499
- const allIndices = new Set<number>();
500
-
501
- // Add indices of originally matched messages
502
- records.forEach((record, index) => {
503
- if (originalMatchIds.has(record.id)) {
504
- allIndices.add(index);
505
- }
506
- });
507
-
508
- // Add the additional context message indices
509
- additionalIndices.forEach(index => {
510
- allIndices.add(index);
511
- });
512
-
513
- // Create a new filtered array with only the required messages
514
- // while maintaining chronological order
515
- return Array.from(allIndices)
516
- .sort((a, b) => a - b)
517
- .map(index => records[index]);
518
- }
519
-
520
- async getMessagesPaginated(
521
- args: StorageGetMessagesArg & { format?: 'v1' | 'v2' },
522
- ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
523
- try {
524
- const { threadId, resourceId, selectBy, format = 'v1' } = args;
525
-
526
- if (!threadId) {
527
- throw new Error('Thread ID is required for getMessagesPaginated');
528
- }
529
-
530
- // Extract pagination and dateRange from selectBy.pagination
531
- const page = selectBy?.pagination?.page ?? 0;
532
- const perPage = selectBy?.pagination?.perPage ?? 10;
533
- const dateRange = selectBy?.pagination?.dateRange;
534
- const fromDate = dateRange?.start;
535
- const toDate = dateRange?.end;
536
-
537
- const table = await this.client.openTable(TABLE_MESSAGES);
538
- const messages: any[] = [];
539
-
540
- // Handle selectBy.include first (before pagination)
541
- if (selectBy?.include && Array.isArray(selectBy.include)) {
542
- // Get all unique thread IDs from include items
543
- const threadIds = [...new Set(selectBy.include.map(item => item.threadId))];
544
-
545
- // Fetch all messages from all relevant threads
546
- const allThreadMessages: any[] = [];
547
- for (const threadId of threadIds) {
548
- const threadQuery = table.query().where(`thread_id = '${threadId}'`);
549
- let threadRecords = await threadQuery.toArray();
550
-
551
- // Apply date filtering in JS for context
552
- if (fromDate) threadRecords = threadRecords.filter(m => m.createdAt >= fromDate.getTime());
553
- if (toDate) threadRecords = threadRecords.filter(m => m.createdAt <= toDate.getTime());
554
-
555
- allThreadMessages.push(...threadRecords);
556
- }
557
-
558
- // Sort all messages by createdAt
559
- allThreadMessages.sort((a, b) => a.createdAt - b.createdAt);
560
-
561
- // Apply processMessagesWithContext to the combined array
562
- const contextMessages = this.processMessagesWithContext(allThreadMessages, selectBy.include);
563
- messages.push(...contextMessages);
564
- }
565
-
566
- // Build query conditions for the main thread
567
- const conditions: string[] = [`thread_id = '${threadId}'`];
568
- if (resourceId) {
569
- conditions.push(`\`resourceId\` = '${resourceId}'`);
570
- }
571
- if (fromDate) {
572
- conditions.push(`\`createdAt\` >= ${fromDate.getTime()}`);
573
- }
574
- if (toDate) {
575
- conditions.push(`\`createdAt\` <= ${toDate.getTime()}`);
576
- }
577
-
578
- // Get total count (excluding already included messages)
579
- let total = 0;
580
- if (conditions.length > 0) {
581
- total = await table.countRows(conditions.join(' AND '));
582
- } else {
583
- total = await table.countRows();
584
- }
585
-
586
- // If no messages and no included messages, return empty result
587
- if (total === 0 && messages.length === 0) {
588
- return {
589
- messages: [],
590
- total: 0,
591
- page,
592
- perPage,
593
- hasMore: false,
594
- };
595
- }
596
-
597
- // Fetch paginated messages (excluding already included ones)
598
- const excludeIds = messages.map(m => m.id);
599
- let selectedMessages: any[] = [];
600
-
601
- if (selectBy?.last && selectBy.last > 0) {
602
- // Handle selectBy.last: get last N messages for the main thread
603
- const query = table.query();
604
- if (conditions.length > 0) {
605
- query.where(conditions.join(' AND '));
606
- }
607
- let records = await query.toArray();
608
- records = records.sort((a, b) => a.createdAt - b.createdAt);
609
-
610
- // Exclude already included messages
611
- if (excludeIds.length > 0) {
612
- records = records.filter(m => !excludeIds.includes(m.id));
613
- }
614
-
615
- selectedMessages = records.slice(-selectBy.last);
616
- } else {
617
- // Regular pagination
618
- const query = table.query();
619
- if (conditions.length > 0) {
620
- query.where(conditions.join(' AND '));
621
- }
622
- let records = await query.toArray();
623
- records = records.sort((a, b) => a.createdAt - b.createdAt);
624
-
625
- // Exclude already included messages
626
- if (excludeIds.length > 0) {
627
- records = records.filter(m => !excludeIds.includes(m.id));
628
- }
629
-
630
- selectedMessages = records.slice(page * perPage, (page + 1) * perPage);
631
- }
632
-
633
- // Merge all messages and deduplicate
634
- const allMessages = [...messages, ...selectedMessages];
635
- const seen = new Set();
636
- const dedupedMessages = allMessages.filter(m => {
637
- const key = `${m.id}:${m.thread_id}`;
638
- if (seen.has(key)) return false;
639
- seen.add(key);
640
- return true;
641
- });
642
-
643
- // Convert to correct format (v1/v2)
644
- const formattedMessages = dedupedMessages.map((msg: any) => {
645
- const { thread_id, ...rest } = msg;
646
- return {
647
- ...rest,
648
- threadId: thread_id,
649
- content:
650
- typeof msg.content === 'string'
651
- ? (() => {
652
- try {
653
- return JSON.parse(msg.content);
654
- } catch {
655
- return msg.content;
656
- }
657
- })()
658
- : msg.content,
659
- };
660
- });
661
-
662
- const list = new MessageList().add(formattedMessages, 'memory');
663
- return {
664
- messages: format === 'v2' ? list.get.all.v2() : list.get.all.v1(),
665
- total: total, // Total should be the count of messages matching the filters
666
- page,
667
- perPage,
668
- hasMore: total > (page + 1) * perPage,
669
- };
670
- } catch (error: any) {
671
- throw new MastraError(
672
- {
673
- id: 'LANCE_STORE_GET_MESSAGES_PAGINATED_FAILED',
674
- domain: ErrorDomain.STORAGE,
675
- category: ErrorCategory.THIRD_PARTY,
676
- },
677
- error,
678
- );
679
- }
680
- }
681
-
682
- /**
683
- * Parse message data from LanceDB record format to MastraMessageV2 format
684
- */
685
- private parseMessageData(data: any): MastraMessageV2 {
686
- const { thread_id, ...rest } = data;
687
- return {
688
- ...rest,
689
- threadId: thread_id,
690
- content:
691
- typeof data.content === 'string'
692
- ? (() => {
693
- try {
694
- return JSON.parse(data.content);
695
- } catch {
696
- return data.content;
697
- }
698
- })()
699
- : data.content,
700
- createdAt: new Date(data.createdAt),
701
- updatedAt: new Date(data.updatedAt),
702
- } as MastraMessageV2;
703
- }
704
-
705
- async updateMessages(args: {
706
- messages: Partial<Omit<MastraMessageV2, 'createdAt'>> &
707
- {
708
- id: string;
709
- content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
710
- }[];
711
- }): Promise<MastraMessageV2[]> {
712
- const { messages } = args;
713
- this.logger.debug('Updating messages', { count: messages.length });
714
-
715
- if (!messages.length) {
716
- return [];
717
- }
718
-
719
- const updatedMessages: MastraMessageV2[] = [];
720
- const affectedThreadIds = new Set<string>();
721
-
722
- try {
723
- for (const updateData of messages) {
724
- const { id, ...updates } = updateData;
725
-
726
- // Get the existing message
727
- const existingMessage = await this.operations.load({ tableName: TABLE_MESSAGES, keys: { id } });
728
- if (!existingMessage) {
729
- this.logger.warn('Message not found for update', { id });
730
- continue;
731
- }
732
-
733
- const existingMsg = this.parseMessageData(existingMessage);
734
- const originalThreadId = existingMsg.threadId;
735
- affectedThreadIds.add(originalThreadId!);
736
-
737
- // Prepare the update payload
738
- const updatePayload: any = {};
739
-
740
- // Handle basic field updates
741
- if ('role' in updates && updates.role !== undefined) updatePayload.role = updates.role;
742
- if ('type' in updates && updates.type !== undefined) updatePayload.type = updates.type;
743
- if ('resourceId' in updates && updates.resourceId !== undefined) updatePayload.resourceId = updates.resourceId;
744
- if ('threadId' in updates && updates.threadId !== undefined && updates.threadId !== null) {
745
- updatePayload.thread_id = updates.threadId;
746
- affectedThreadIds.add(updates.threadId as string);
747
- }
748
-
749
- // Handle content updates
750
- if (updates.content) {
751
- const existingContent = existingMsg.content;
752
- let newContent = { ...existingContent };
753
-
754
- // Deep merge metadata if provided
755
- if (updates.content.metadata !== undefined) {
756
- newContent.metadata = {
757
- ...(existingContent.metadata || {}),
758
- ...(updates.content.metadata || {}),
759
- };
760
- }
761
-
762
- // Update content string if provided
763
- if (updates.content.content !== undefined) {
764
- newContent.content = updates.content.content;
765
- }
766
-
767
- // Update parts if provided (only if it exists in the content type)
768
- if ('parts' in updates.content && updates.content.parts !== undefined) {
769
- (newContent as any).parts = updates.content.parts;
770
- }
771
-
772
- updatePayload.content = JSON.stringify(newContent);
773
- }
774
-
775
- // Update the message using merge insert
776
- await this.operations.insert({ tableName: TABLE_MESSAGES, record: { id, ...updatePayload } });
777
-
778
- // Get the updated message
779
- const updatedMessage = await this.operations.load({ tableName: TABLE_MESSAGES, keys: { id } });
780
- if (updatedMessage) {
781
- updatedMessages.push(this.parseMessageData(updatedMessage));
782
- }
783
- }
784
-
785
- // Update timestamps for all affected threads
786
- for (const threadId of affectedThreadIds) {
787
- await this.operations.insert({
788
- tableName: TABLE_THREADS,
789
- record: { id: threadId, updatedAt: Date.now() },
790
- });
791
- }
792
-
793
- return updatedMessages;
794
- } catch (error: any) {
795
- throw new MastraError(
796
- {
797
- id: 'LANCE_STORE_UPDATE_MESSAGES_FAILED',
798
- domain: ErrorDomain.STORAGE,
799
- category: ErrorCategory.THIRD_PARTY,
800
- details: { count: messages.length },
801
- },
802
- error,
803
- );
804
- }
805
- }
806
-
807
- async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
808
- try {
809
- const resource = await this.operations.load({ tableName: TABLE_RESOURCES, keys: { id: resourceId } });
810
-
811
- if (!resource) {
812
- return null;
813
- }
814
-
815
- // Handle date conversion - LanceDB stores timestamps as numbers
816
- let createdAt: Date;
817
- let updatedAt: Date;
818
-
819
- // Convert ISO strings back to Date objects with error handling
820
- try {
821
- // If createdAt is already a Date object, use it directly
822
- if (resource.createdAt instanceof Date) {
823
- createdAt = resource.createdAt;
824
- } else if (typeof resource.createdAt === 'string') {
825
- // If it's an ISO string, parse it
826
- createdAt = new Date(resource.createdAt);
827
- } else if (typeof resource.createdAt === 'number') {
828
- // If it's a timestamp, convert it to Date
829
- createdAt = new Date(resource.createdAt);
830
- } else {
831
- // If it's null or undefined, use current date
832
- createdAt = new Date();
833
- }
834
- if (isNaN(createdAt.getTime())) {
835
- createdAt = new Date(); // Fallback to current date if invalid
836
- }
837
- } catch {
838
- createdAt = new Date(); // Fallback to current date if conversion fails
839
- }
840
-
841
- try {
842
- // If updatedAt is already a Date object, use it directly
843
- if (resource.updatedAt instanceof Date) {
844
- updatedAt = resource.updatedAt;
845
- } else if (typeof resource.updatedAt === 'string') {
846
- // If it's an ISO string, parse it
847
- updatedAt = new Date(resource.updatedAt);
848
- } else if (typeof resource.updatedAt === 'number') {
849
- // If it's a timestamp, convert it to Date
850
- updatedAt = new Date(resource.updatedAt);
851
- } else {
852
- // If it's null or undefined, use current date
853
- updatedAt = new Date();
854
- }
855
- if (isNaN(updatedAt.getTime())) {
856
- updatedAt = new Date(); // Fallback to current date if invalid
857
- }
858
- } catch {
859
- updatedAt = new Date(); // Fallback to current date if conversion fails
860
- }
861
-
862
- // Handle workingMemory - return undefined for null/undefined, empty string for empty string
863
- let workingMemory = resource.workingMemory;
864
- if (workingMemory === null || workingMemory === undefined) {
865
- workingMemory = undefined;
866
- } else if (workingMemory === '') {
867
- workingMemory = ''; // Return empty string for empty strings to match test expectations
868
- } else if (typeof workingMemory === 'object') {
869
- workingMemory = JSON.stringify(workingMemory);
870
- }
871
-
872
- // Handle metadata - return undefined for empty strings, parse JSON safely
873
- let metadata = resource.metadata;
874
- if (metadata === '' || metadata === null || metadata === undefined) {
875
- metadata = undefined;
876
- } else if (typeof metadata === 'string') {
877
- try {
878
- metadata = JSON.parse(metadata);
879
- } catch {
880
- // If JSON parsing fails, return the original string
881
- metadata = metadata;
882
- }
883
- }
884
-
885
- return {
886
- ...resource,
887
- createdAt,
888
- updatedAt,
889
- workingMemory,
890
- metadata,
891
- } as StorageResourceType;
892
- } catch (error: any) {
893
- throw new MastraError(
894
- {
895
- id: 'LANCE_STORE_GET_RESOURCE_BY_ID_FAILED',
896
- domain: ErrorDomain.STORAGE,
897
- category: ErrorCategory.THIRD_PARTY,
898
- },
899
- error,
900
- );
901
- }
902
- }
903
-
904
- async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
905
- try {
906
- const record = {
907
- ...resource,
908
- metadata: resource.metadata ? JSON.stringify(resource.metadata) : '',
909
- createdAt: resource.createdAt.getTime(), // Store as timestamp (milliseconds)
910
- updatedAt: resource.updatedAt.getTime(), // Store as timestamp (milliseconds)
911
- };
912
-
913
- const table = await this.client.openTable(TABLE_RESOURCES);
914
- await table.add([record], { mode: 'append' });
915
-
916
- return resource;
917
- } catch (error: any) {
918
- throw new MastraError(
919
- {
920
- id: 'LANCE_STORE_SAVE_RESOURCE_FAILED',
921
- domain: ErrorDomain.STORAGE,
922
- category: ErrorCategory.THIRD_PARTY,
923
- },
924
- error,
925
- );
926
- }
927
- }
928
-
929
- async updateResource({
930
- resourceId,
931
- workingMemory,
932
- metadata,
933
- }: {
934
- resourceId: string;
935
- workingMemory?: string;
936
- metadata?: Record<string, unknown>;
937
- }): Promise<StorageResourceType> {
938
- const maxRetries = 3;
939
-
940
- for (let attempt = 0; attempt < maxRetries; attempt++) {
941
- try {
942
- const existingResource = await this.getResourceById({ resourceId });
943
-
944
- if (!existingResource) {
945
- // Create new resource if it doesn't exist
946
- const newResource: StorageResourceType = {
947
- id: resourceId,
948
- workingMemory,
949
- metadata: metadata || {},
950
- createdAt: new Date(),
951
- updatedAt: new Date(),
952
- };
953
- return this.saveResource({ resource: newResource });
954
- }
955
-
956
- const updatedResource = {
957
- ...existingResource,
958
- workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
959
- metadata: {
960
- ...existingResource.metadata,
961
- ...metadata,
962
- },
963
- updatedAt: new Date(),
964
- };
965
-
966
- const record = {
967
- id: resourceId,
968
- workingMemory: updatedResource.workingMemory || '',
969
- metadata: updatedResource.metadata ? JSON.stringify(updatedResource.metadata) : '',
970
- updatedAt: updatedResource.updatedAt.getTime(), // Store as timestamp (milliseconds)
971
- };
972
-
973
- const table = await this.client.openTable(TABLE_RESOURCES);
974
- await table.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([record]);
975
-
976
- return updatedResource;
977
- } catch (error: any) {
978
- if (error.message?.includes('Commit conflict') && attempt < maxRetries - 1) {
979
- // Wait with exponential backoff before retrying
980
- const delay = Math.pow(2, attempt) * 10; // 10ms, 20ms, 40ms
981
- await new Promise(resolve => setTimeout(resolve, delay));
982
- continue;
983
- }
984
-
985
- // If it's not a commit conflict or we've exhausted retries, throw the error
986
- throw new MastraError(
987
- {
988
- id: 'LANCE_STORE_UPDATE_RESOURCE_FAILED',
989
- domain: ErrorDomain.STORAGE,
990
- category: ErrorCategory.THIRD_PARTY,
991
- },
992
- error,
993
- );
994
- }
995
- }
996
-
997
- // This should never be reached, but TypeScript requires it
998
- throw new Error('Unexpected end of retry loop');
999
- }
1000
- }