@mastra/libsql 0.10.3-alpha.0 → 0.10.4-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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +33 -0
- package/dist/_tsup-dts-rollup.d.cts +10 -0
- package/dist/_tsup-dts-rollup.d.ts +10 -0
- package/dist/index.cjs +79 -0
- package/dist/index.js +79 -0
- package/package.json +4 -4
- package/src/storage/index.test.ts +194 -0
- package/src/storage/index.ts +107 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/libsql@0.10.
|
|
2
|
+
> @mastra/libsql@0.10.4-alpha.0 build /home/runner/work/mastra/mastra/stores/libsql
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.5.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 10773ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/libsql/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
14
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/libsql/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 11908ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
20
|
+
[32mESM[39m [1mdist/index.js [22m[32m61.63 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 1696ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m61.96 KB[39m
|
|
23
|
+
[32mCJS[39m ⚡️ Build success in 1707ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @mastra/libsql
|
|
2
2
|
|
|
3
|
+
## 0.10.4-alpha.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d8f2d19: Add updateMessages API to storage classes (only support for PG and LibSQL for now) and to memory class. Additionally allow for metadata to be saved in the content field of a message.
|
|
8
|
+
- Updated dependencies [d8f2d19]
|
|
9
|
+
- Updated dependencies [9d52b17]
|
|
10
|
+
- Updated dependencies [8ba1b51]
|
|
11
|
+
- @mastra/core@0.10.7-alpha.0
|
|
12
|
+
|
|
13
|
+
## 0.10.3
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 63f6b7d: dependencies updates:
|
|
18
|
+
- Updated dependency [`@libsql/client@^0.15.9` ↗︎](https://www.npmjs.com/package/@libsql/client/v/0.15.9) (from `^0.15.8`, in `dependencies`)
|
|
19
|
+
- Updated dependencies [63f6b7d]
|
|
20
|
+
- Updated dependencies [12a95fc]
|
|
21
|
+
- Updated dependencies [4b0f8a6]
|
|
22
|
+
- Updated dependencies [51264a5]
|
|
23
|
+
- Updated dependencies [8e6f677]
|
|
24
|
+
- Updated dependencies [d70c420]
|
|
25
|
+
- Updated dependencies [ee9af57]
|
|
26
|
+
- Updated dependencies [36f1c36]
|
|
27
|
+
- Updated dependencies [2a16996]
|
|
28
|
+
- Updated dependencies [10d352e]
|
|
29
|
+
- Updated dependencies [9589624]
|
|
30
|
+
- Updated dependencies [53d3c37]
|
|
31
|
+
- Updated dependencies [751c894]
|
|
32
|
+
- Updated dependencies [577ce3a]
|
|
33
|
+
- Updated dependencies [9260b3a]
|
|
34
|
+
- @mastra/core@0.10.6
|
|
35
|
+
|
|
3
36
|
## 0.10.3-alpha.0
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
|
@@ -6,6 +6,7 @@ import type { DescribeIndexParams } from '@mastra/core/vector';
|
|
|
6
6
|
import type { EvalRow } from '@mastra/core/storage';
|
|
7
7
|
import type { IndexStats } from '@mastra/core/vector';
|
|
8
8
|
import type { InValue } from '@libsql/client';
|
|
9
|
+
import type { MastraMessageContentV2 } from '@mastra/core/agent';
|
|
9
10
|
import type { MastraMessageV1 } from '@mastra/core/memory';
|
|
10
11
|
import type { MastraMessageV2 } from '@mastra/core/agent';
|
|
11
12
|
import { MastraStorage } from '@mastra/core/storage';
|
|
@@ -172,6 +173,15 @@ declare class LibSQLStore extends MastraStorage {
|
|
|
172
173
|
messages: MastraMessageV2[];
|
|
173
174
|
format: 'v2';
|
|
174
175
|
}): Promise<MastraMessageV2[]>;
|
|
176
|
+
updateMessages({ messages, }: {
|
|
177
|
+
messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
|
|
178
|
+
id: string;
|
|
179
|
+
content?: {
|
|
180
|
+
metadata?: MastraMessageContentV2['metadata'];
|
|
181
|
+
content?: MastraMessageContentV2['content'];
|
|
182
|
+
};
|
|
183
|
+
})[];
|
|
184
|
+
}): Promise<MastraMessageV2[]>;
|
|
175
185
|
private transformEvalRow;
|
|
176
186
|
/** @deprecated use getEvals instead */
|
|
177
187
|
getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]>;
|
|
@@ -6,6 +6,7 @@ import type { DescribeIndexParams } from '@mastra/core/vector';
|
|
|
6
6
|
import type { EvalRow } from '@mastra/core/storage';
|
|
7
7
|
import type { IndexStats } from '@mastra/core/vector';
|
|
8
8
|
import type { InValue } from '@libsql/client';
|
|
9
|
+
import type { MastraMessageContentV2 } from '@mastra/core/agent';
|
|
9
10
|
import type { MastraMessageV1 } from '@mastra/core/memory';
|
|
10
11
|
import type { MastraMessageV2 } from '@mastra/core/agent';
|
|
11
12
|
import { MastraStorage } from '@mastra/core/storage';
|
|
@@ -172,6 +173,15 @@ declare class LibSQLStore extends MastraStorage {
|
|
|
172
173
|
messages: MastraMessageV2[];
|
|
173
174
|
format: 'v2';
|
|
174
175
|
}): Promise<MastraMessageV2[]>;
|
|
176
|
+
updateMessages({ messages, }: {
|
|
177
|
+
messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
|
|
178
|
+
id: string;
|
|
179
|
+
content?: {
|
|
180
|
+
metadata?: MastraMessageContentV2['metadata'];
|
|
181
|
+
content?: MastraMessageContentV2['content'];
|
|
182
|
+
};
|
|
183
|
+
})[];
|
|
184
|
+
}): Promise<MastraMessageV2[]>;
|
|
175
185
|
private transformEvalRow;
|
|
176
186
|
/** @deprecated use getEvals instead */
|
|
177
187
|
getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]>;
|
package/dist/index.cjs
CHANGED
|
@@ -1351,6 +1351,85 @@ var LibSQLStore = class extends storage.MastraStorage {
|
|
|
1351
1351
|
throw error;
|
|
1352
1352
|
}
|
|
1353
1353
|
}
|
|
1354
|
+
async updateMessages({
|
|
1355
|
+
messages
|
|
1356
|
+
}) {
|
|
1357
|
+
if (messages.length === 0) {
|
|
1358
|
+
return [];
|
|
1359
|
+
}
|
|
1360
|
+
const messageIds = messages.map((m) => m.id);
|
|
1361
|
+
const placeholders = messageIds.map(() => "?").join(",");
|
|
1362
|
+
const selectSql = `SELECT * FROM ${storage.TABLE_MESSAGES} WHERE id IN (${placeholders})`;
|
|
1363
|
+
const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
|
|
1364
|
+
const existingMessages = existingResult.rows.map((row) => this.parseRow(row));
|
|
1365
|
+
if (existingMessages.length === 0) {
|
|
1366
|
+
return [];
|
|
1367
|
+
}
|
|
1368
|
+
const batchStatements = [];
|
|
1369
|
+
const threadIdsToUpdate = /* @__PURE__ */ new Set();
|
|
1370
|
+
const columnMapping = {
|
|
1371
|
+
threadId: "thread_id"
|
|
1372
|
+
};
|
|
1373
|
+
for (const existingMessage of existingMessages) {
|
|
1374
|
+
const updatePayload = messages.find((m) => m.id === existingMessage.id);
|
|
1375
|
+
if (!updatePayload) continue;
|
|
1376
|
+
const { id, ...fieldsToUpdate } = updatePayload;
|
|
1377
|
+
if (Object.keys(fieldsToUpdate).length === 0) continue;
|
|
1378
|
+
threadIdsToUpdate.add(existingMessage.threadId);
|
|
1379
|
+
if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
|
|
1380
|
+
threadIdsToUpdate.add(updatePayload.threadId);
|
|
1381
|
+
}
|
|
1382
|
+
const setClauses = [];
|
|
1383
|
+
const args = [];
|
|
1384
|
+
const updatableFields = { ...fieldsToUpdate };
|
|
1385
|
+
if (updatableFields.content) {
|
|
1386
|
+
const newContent = {
|
|
1387
|
+
...existingMessage.content,
|
|
1388
|
+
...updatableFields.content,
|
|
1389
|
+
// Deep merge metadata if it exists on both
|
|
1390
|
+
...existingMessage.content?.metadata && updatableFields.content.metadata ? {
|
|
1391
|
+
metadata: {
|
|
1392
|
+
...existingMessage.content.metadata,
|
|
1393
|
+
...updatableFields.content.metadata
|
|
1394
|
+
}
|
|
1395
|
+
} : {}
|
|
1396
|
+
};
|
|
1397
|
+
setClauses.push(`${utils.parseSqlIdentifier("content", "column name")} = ?`);
|
|
1398
|
+
args.push(JSON.stringify(newContent));
|
|
1399
|
+
delete updatableFields.content;
|
|
1400
|
+
}
|
|
1401
|
+
for (const key in updatableFields) {
|
|
1402
|
+
if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
|
|
1403
|
+
const dbKey = columnMapping[key] || key;
|
|
1404
|
+
setClauses.push(`${utils.parseSqlIdentifier(dbKey, "column name")} = ?`);
|
|
1405
|
+
let value = updatableFields[key];
|
|
1406
|
+
if (typeof value === "object" && value !== null) {
|
|
1407
|
+
value = JSON.stringify(value);
|
|
1408
|
+
}
|
|
1409
|
+
args.push(value);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (setClauses.length === 0) continue;
|
|
1413
|
+
args.push(id);
|
|
1414
|
+
const sql = `UPDATE ${storage.TABLE_MESSAGES} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1415
|
+
batchStatements.push({ sql, args });
|
|
1416
|
+
}
|
|
1417
|
+
if (batchStatements.length === 0) {
|
|
1418
|
+
return existingMessages;
|
|
1419
|
+
}
|
|
1420
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1421
|
+
for (const threadId of threadIdsToUpdate) {
|
|
1422
|
+
if (threadId) {
|
|
1423
|
+
batchStatements.push({
|
|
1424
|
+
sql: `UPDATE ${storage.TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
|
|
1425
|
+
args: [now, threadId]
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
await this.client.batch(batchStatements, "write");
|
|
1430
|
+
const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
|
|
1431
|
+
return updatedResult.rows.map((row) => this.parseRow(row));
|
|
1432
|
+
}
|
|
1354
1433
|
transformEvalRow(row) {
|
|
1355
1434
|
const resultValue = JSON.parse(row.result);
|
|
1356
1435
|
const testInfoValue = row.test_info ? JSON.parse(row.test_info) : void 0;
|
package/dist/index.js
CHANGED
|
@@ -1349,6 +1349,85 @@ var LibSQLStore = class extends MastraStorage {
|
|
|
1349
1349
|
throw error;
|
|
1350
1350
|
}
|
|
1351
1351
|
}
|
|
1352
|
+
async updateMessages({
|
|
1353
|
+
messages
|
|
1354
|
+
}) {
|
|
1355
|
+
if (messages.length === 0) {
|
|
1356
|
+
return [];
|
|
1357
|
+
}
|
|
1358
|
+
const messageIds = messages.map((m) => m.id);
|
|
1359
|
+
const placeholders = messageIds.map(() => "?").join(",");
|
|
1360
|
+
const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
|
|
1361
|
+
const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
|
|
1362
|
+
const existingMessages = existingResult.rows.map((row) => this.parseRow(row));
|
|
1363
|
+
if (existingMessages.length === 0) {
|
|
1364
|
+
return [];
|
|
1365
|
+
}
|
|
1366
|
+
const batchStatements = [];
|
|
1367
|
+
const threadIdsToUpdate = /* @__PURE__ */ new Set();
|
|
1368
|
+
const columnMapping = {
|
|
1369
|
+
threadId: "thread_id"
|
|
1370
|
+
};
|
|
1371
|
+
for (const existingMessage of existingMessages) {
|
|
1372
|
+
const updatePayload = messages.find((m) => m.id === existingMessage.id);
|
|
1373
|
+
if (!updatePayload) continue;
|
|
1374
|
+
const { id, ...fieldsToUpdate } = updatePayload;
|
|
1375
|
+
if (Object.keys(fieldsToUpdate).length === 0) continue;
|
|
1376
|
+
threadIdsToUpdate.add(existingMessage.threadId);
|
|
1377
|
+
if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
|
|
1378
|
+
threadIdsToUpdate.add(updatePayload.threadId);
|
|
1379
|
+
}
|
|
1380
|
+
const setClauses = [];
|
|
1381
|
+
const args = [];
|
|
1382
|
+
const updatableFields = { ...fieldsToUpdate };
|
|
1383
|
+
if (updatableFields.content) {
|
|
1384
|
+
const newContent = {
|
|
1385
|
+
...existingMessage.content,
|
|
1386
|
+
...updatableFields.content,
|
|
1387
|
+
// Deep merge metadata if it exists on both
|
|
1388
|
+
...existingMessage.content?.metadata && updatableFields.content.metadata ? {
|
|
1389
|
+
metadata: {
|
|
1390
|
+
...existingMessage.content.metadata,
|
|
1391
|
+
...updatableFields.content.metadata
|
|
1392
|
+
}
|
|
1393
|
+
} : {}
|
|
1394
|
+
};
|
|
1395
|
+
setClauses.push(`${parseSqlIdentifier("content", "column name")} = ?`);
|
|
1396
|
+
args.push(JSON.stringify(newContent));
|
|
1397
|
+
delete updatableFields.content;
|
|
1398
|
+
}
|
|
1399
|
+
for (const key in updatableFields) {
|
|
1400
|
+
if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
|
|
1401
|
+
const dbKey = columnMapping[key] || key;
|
|
1402
|
+
setClauses.push(`${parseSqlIdentifier(dbKey, "column name")} = ?`);
|
|
1403
|
+
let value = updatableFields[key];
|
|
1404
|
+
if (typeof value === "object" && value !== null) {
|
|
1405
|
+
value = JSON.stringify(value);
|
|
1406
|
+
}
|
|
1407
|
+
args.push(value);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (setClauses.length === 0) continue;
|
|
1411
|
+
args.push(id);
|
|
1412
|
+
const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1413
|
+
batchStatements.push({ sql, args });
|
|
1414
|
+
}
|
|
1415
|
+
if (batchStatements.length === 0) {
|
|
1416
|
+
return existingMessages;
|
|
1417
|
+
}
|
|
1418
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1419
|
+
for (const threadId of threadIdsToUpdate) {
|
|
1420
|
+
if (threadId) {
|
|
1421
|
+
batchStatements.push({
|
|
1422
|
+
sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
|
|
1423
|
+
args: [now, threadId]
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
await this.client.batch(batchStatements, "write");
|
|
1428
|
+
const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
|
|
1429
|
+
return updatedResult.rows.map((row) => this.parseRow(row));
|
|
1430
|
+
}
|
|
1352
1431
|
transformEvalRow(row) {
|
|
1353
1432
|
const resultValue = JSON.parse(row.result);
|
|
1354
1433
|
const testInfoValue = row.test_info ? JSON.parse(row.test_info) : void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/libsql",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4-alpha.0",
|
|
4
4
|
"description": "Libsql provider for Mastra - includes both vector and db storage capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"tsup": "^8.5.0",
|
|
30
30
|
"typescript": "^5.8.3",
|
|
31
31
|
"vitest": "^3.2.3",
|
|
32
|
-
"@internal/
|
|
33
|
-
"@internal/
|
|
34
|
-
"@mastra/core": "0.10.
|
|
32
|
+
"@internal/storage-test-utils": "0.0.9",
|
|
33
|
+
"@internal/lint": "0.0.13",
|
|
34
|
+
"@mastra/core": "0.10.7-alpha.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@mastra/core": ">=0.10.4-0 <0.11.0"
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resetRole,
|
|
9
9
|
} from '@internal/storage-test-utils';
|
|
10
10
|
import type { MastraMessageV1, StorageThreadType } from '@mastra/core';
|
|
11
|
+
import type { MastraMessageV2, MastraMessageContentV2 } from '@mastra/core/agent';
|
|
11
12
|
import { Mastra } from '@mastra/core/mastra';
|
|
12
13
|
import { TABLE_EVALS, TABLE_TRACES, TABLE_MESSAGES, TABLE_THREADS } from '@mastra/core/storage';
|
|
13
14
|
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
@@ -362,3 +363,196 @@ describe('LibSQLStore Pagination Features', () => {
|
|
|
362
363
|
});
|
|
363
364
|
});
|
|
364
365
|
});
|
|
366
|
+
|
|
367
|
+
describe('LibSQLStore updateMessages', () => {
|
|
368
|
+
let store: LibSQLStore;
|
|
369
|
+
let thread: StorageThreadType;
|
|
370
|
+
|
|
371
|
+
const createSampleMessageV2 = ({
|
|
372
|
+
threadId,
|
|
373
|
+
resourceId,
|
|
374
|
+
role = 'user',
|
|
375
|
+
content,
|
|
376
|
+
createdAt,
|
|
377
|
+
}: {
|
|
378
|
+
threadId: string;
|
|
379
|
+
resourceId?: string;
|
|
380
|
+
role?: 'user' | 'assistant';
|
|
381
|
+
content?: Partial<MastraMessageContentV2>;
|
|
382
|
+
createdAt?: Date;
|
|
383
|
+
}): MastraMessageV2 => {
|
|
384
|
+
return {
|
|
385
|
+
id: randomUUID(),
|
|
386
|
+
threadId,
|
|
387
|
+
resourceId: resourceId || thread.resourceId,
|
|
388
|
+
role,
|
|
389
|
+
createdAt: createdAt || new Date(),
|
|
390
|
+
content: {
|
|
391
|
+
format: 2,
|
|
392
|
+
parts: content?.parts || [],
|
|
393
|
+
content: content?.content || `Sample content ${randomUUID()}`,
|
|
394
|
+
...content,
|
|
395
|
+
},
|
|
396
|
+
type: 'v2',
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
beforeAll(async () => {
|
|
401
|
+
store = libsql;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
beforeEach(async () => {
|
|
405
|
+
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
406
|
+
await store.clearTable({ tableName: TABLE_THREADS });
|
|
407
|
+
const threadData = createSampleThread();
|
|
408
|
+
thread = await store.saveThread({ thread: threadData as StorageThreadType });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should update a single field of a message (e.g., role)', async () => {
|
|
412
|
+
const originalMessage = createSampleMessageV2({ threadId: thread.id, role: 'user' });
|
|
413
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
414
|
+
|
|
415
|
+
const updatedMessages = await store.updateMessages({
|
|
416
|
+
messages: [{ id: originalMessage.id, role: 'assistant' }],
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(updatedMessages).toHaveLength(1);
|
|
420
|
+
expect(updatedMessages[0].role).toBe('assistant');
|
|
421
|
+
|
|
422
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
423
|
+
expect(fromDb[0].role).toBe('assistant');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should update only the metadata within the content field, preserving other content fields', async () => {
|
|
427
|
+
const originalMessage = createSampleMessageV2({
|
|
428
|
+
threadId: thread.id,
|
|
429
|
+
content: { content: 'hello world', parts: [{ type: 'text', text: 'hello world' }] },
|
|
430
|
+
});
|
|
431
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
432
|
+
|
|
433
|
+
const newMetadata = { someKey: 'someValue' };
|
|
434
|
+
await store.updateMessages({
|
|
435
|
+
messages: [{ id: originalMessage.id, content: { metadata: newMetadata } as any }],
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
439
|
+
expect(fromDb).toHaveLength(1);
|
|
440
|
+
expect(fromDb[0].content.metadata).toEqual(newMetadata);
|
|
441
|
+
expect(fromDb[0].content.content).toBe('hello world');
|
|
442
|
+
expect(fromDb[0].content.parts).toEqual([{ type: 'text', text: 'hello world' }]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should update only the content string within the content field, preserving metadata', async () => {
|
|
446
|
+
const originalMessage = createSampleMessageV2({
|
|
447
|
+
threadId: thread.id,
|
|
448
|
+
content: { metadata: { initial: true } },
|
|
449
|
+
});
|
|
450
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
451
|
+
|
|
452
|
+
const newContentString = 'This is the new content string';
|
|
453
|
+
await store.updateMessages({
|
|
454
|
+
messages: [{ id: originalMessage.id, content: { content: newContentString } as any }],
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
458
|
+
expect(fromDb[0].content.content).toBe(newContentString);
|
|
459
|
+
expect(fromDb[0].content.metadata).toEqual({ initial: true });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should deep merge metadata, not overwrite it', async () => {
|
|
463
|
+
const originalMessage = createSampleMessageV2({
|
|
464
|
+
threadId: thread.id,
|
|
465
|
+
content: { metadata: { initial: true }, content: 'old content' },
|
|
466
|
+
});
|
|
467
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
468
|
+
|
|
469
|
+
const newMetadata = { updated: true };
|
|
470
|
+
await store.updateMessages({
|
|
471
|
+
messages: [{ id: originalMessage.id, content: { metadata: newMetadata } as any }],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
475
|
+
expect(fromDb[0].content.content).toBe('old content');
|
|
476
|
+
expect(fromDb[0].content.metadata).toEqual({ initial: true, updated: true });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should update multiple messages at once', async () => {
|
|
480
|
+
const msg1 = createSampleMessageV2({ threadId: thread.id, role: 'user' });
|
|
481
|
+
const msg2 = createSampleMessageV2({ threadId: thread.id, content: { content: 'original' } });
|
|
482
|
+
await store.saveMessages({ messages: [msg1, msg2], format: 'v2' });
|
|
483
|
+
|
|
484
|
+
await store.updateMessages({
|
|
485
|
+
messages: [
|
|
486
|
+
{ id: msg1.id, role: 'assistant' },
|
|
487
|
+
{ id: msg2.id, content: { content: 'updated' } as any },
|
|
488
|
+
],
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
492
|
+
const updatedMsg1 = fromDb.find(m => m.id === msg1.id)!;
|
|
493
|
+
const updatedMsg2 = fromDb.find(m => m.id === msg2.id)!;
|
|
494
|
+
|
|
495
|
+
expect(updatedMsg1.role).toBe('assistant');
|
|
496
|
+
expect(updatedMsg2.content.content).toBe('updated');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should update the parent thread updatedAt timestamp', async () => {
|
|
500
|
+
const originalMessage = createSampleMessageV2({ threadId: thread.id });
|
|
501
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
502
|
+
const initialThread = await store.getThreadById({ threadId: thread.id });
|
|
503
|
+
|
|
504
|
+
await new Promise(r => setTimeout(r, 10));
|
|
505
|
+
|
|
506
|
+
await store.updateMessages({ messages: [{ id: originalMessage.id, role: 'assistant' }] });
|
|
507
|
+
|
|
508
|
+
const updatedThread = await store.getThreadById({ threadId: thread.id });
|
|
509
|
+
|
|
510
|
+
expect(new Date(updatedThread!.updatedAt).getTime()).toBeGreaterThan(new Date(initialThread!.updatedAt).getTime());
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should update timestamps on both threads when moving a message', async () => {
|
|
514
|
+
const thread2 = await store.saveThread({ thread: createSampleThread() });
|
|
515
|
+
const message = createSampleMessageV2({ threadId: thread.id });
|
|
516
|
+
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
517
|
+
|
|
518
|
+
const initialThread1 = await store.getThreadById({ threadId: thread.id });
|
|
519
|
+
const initialThread2 = await store.getThreadById({ threadId: thread2.id });
|
|
520
|
+
|
|
521
|
+
await new Promise(r => setTimeout(r, 10));
|
|
522
|
+
|
|
523
|
+
await store.updateMessages({
|
|
524
|
+
messages: [{ id: message.id, threadId: thread2.id }],
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const updatedThread1 = await store.getThreadById({ threadId: thread.id });
|
|
528
|
+
const updatedThread2 = await store.getThreadById({ threadId: thread2.id });
|
|
529
|
+
|
|
530
|
+
expect(new Date(updatedThread1!.updatedAt).getTime()).toBeGreaterThan(
|
|
531
|
+
new Date(initialThread1!.updatedAt).getTime(),
|
|
532
|
+
);
|
|
533
|
+
expect(new Date(updatedThread2!.updatedAt).getTime()).toBeGreaterThan(
|
|
534
|
+
new Date(initialThread2!.updatedAt).getTime(),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// Verify the message was moved
|
|
538
|
+
const thread1Messages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
539
|
+
const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
|
|
540
|
+
expect(thread1Messages).toHaveLength(0);
|
|
541
|
+
expect(thread2Messages).toHaveLength(1);
|
|
542
|
+
expect(thread2Messages[0].id).toBe(message.id);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should not fail when trying to update a non-existent message', async () => {
|
|
546
|
+
const originalMessage = createSampleMessageV2({ threadId: thread.id });
|
|
547
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
548
|
+
|
|
549
|
+
await expect(
|
|
550
|
+
store.updateMessages({
|
|
551
|
+
messages: [{ id: randomUUID(), role: 'assistant' }],
|
|
552
|
+
}),
|
|
553
|
+
).resolves.not.toThrow();
|
|
554
|
+
|
|
555
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
556
|
+
expect(fromDb[0].role).toBe(originalMessage.role);
|
|
557
|
+
});
|
|
558
|
+
});
|
package/src/storage/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient } from '@libsql/client';
|
|
2
2
|
import type { Client, InValue } from '@libsql/client';
|
|
3
3
|
import { MessageList } from '@mastra/core/agent';
|
|
4
|
-
import type { MastraMessageV2 } from '@mastra/core/agent';
|
|
4
|
+
import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
|
|
5
5
|
import type { MetricResult, TestInfo } from '@mastra/core/eval';
|
|
6
6
|
import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
|
|
7
7
|
import {
|
|
@@ -755,6 +755,112 @@ export class LibSQLStore extends MastraStorage {
|
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
async updateMessages({
|
|
759
|
+
messages,
|
|
760
|
+
}: {
|
|
761
|
+
messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
|
|
762
|
+
id: string;
|
|
763
|
+
content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
|
|
764
|
+
})[];
|
|
765
|
+
}): Promise<MastraMessageV2[]> {
|
|
766
|
+
if (messages.length === 0) {
|
|
767
|
+
return [];
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const messageIds = messages.map(m => m.id);
|
|
771
|
+
const placeholders = messageIds.map(() => '?').join(',');
|
|
772
|
+
|
|
773
|
+
const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
|
|
774
|
+
const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
|
|
775
|
+
const existingMessages: MastraMessageV2[] = existingResult.rows.map(row => this.parseRow(row));
|
|
776
|
+
|
|
777
|
+
if (existingMessages.length === 0) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const batchStatements = [];
|
|
782
|
+
const threadIdsToUpdate = new Set<string>();
|
|
783
|
+
const columnMapping: Record<string, string> = {
|
|
784
|
+
threadId: 'thread_id',
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
for (const existingMessage of existingMessages) {
|
|
788
|
+
const updatePayload = messages.find(m => m.id === existingMessage.id);
|
|
789
|
+
if (!updatePayload) continue;
|
|
790
|
+
|
|
791
|
+
const { id, ...fieldsToUpdate } = updatePayload;
|
|
792
|
+
if (Object.keys(fieldsToUpdate).length === 0) continue;
|
|
793
|
+
|
|
794
|
+
threadIdsToUpdate.add(existingMessage.threadId!);
|
|
795
|
+
if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
|
|
796
|
+
threadIdsToUpdate.add(updatePayload.threadId);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const setClauses = [];
|
|
800
|
+
const args: InValue[] = [];
|
|
801
|
+
const updatableFields = { ...fieldsToUpdate };
|
|
802
|
+
|
|
803
|
+
// Special handling for the 'content' field to merge instead of overwrite
|
|
804
|
+
if (updatableFields.content) {
|
|
805
|
+
const newContent = {
|
|
806
|
+
...existingMessage.content,
|
|
807
|
+
...updatableFields.content,
|
|
808
|
+
// Deep merge metadata if it exists on both
|
|
809
|
+
...(existingMessage.content?.metadata && updatableFields.content.metadata
|
|
810
|
+
? {
|
|
811
|
+
metadata: {
|
|
812
|
+
...existingMessage.content.metadata,
|
|
813
|
+
...updatableFields.content.metadata,
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
: {}),
|
|
817
|
+
};
|
|
818
|
+
setClauses.push(`${parseSqlIdentifier('content', 'column name')} = ?`);
|
|
819
|
+
args.push(JSON.stringify(newContent));
|
|
820
|
+
delete updatableFields.content;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
for (const key in updatableFields) {
|
|
824
|
+
if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
|
|
825
|
+
const dbKey = columnMapping[key] || key;
|
|
826
|
+
setClauses.push(`${parseSqlIdentifier(dbKey, 'column name')} = ?`);
|
|
827
|
+
let value = updatableFields[key as keyof typeof updatableFields];
|
|
828
|
+
|
|
829
|
+
if (typeof value === 'object' && value !== null) {
|
|
830
|
+
value = JSON.stringify(value);
|
|
831
|
+
}
|
|
832
|
+
args.push(value as InValue);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (setClauses.length === 0) continue;
|
|
837
|
+
|
|
838
|
+
args.push(id);
|
|
839
|
+
|
|
840
|
+
const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(', ')} WHERE id = ?`;
|
|
841
|
+
batchStatements.push({ sql, args });
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (batchStatements.length === 0) {
|
|
845
|
+
return existingMessages;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const now = new Date().toISOString();
|
|
849
|
+
for (const threadId of threadIdsToUpdate) {
|
|
850
|
+
if (threadId) {
|
|
851
|
+
batchStatements.push({
|
|
852
|
+
sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
|
|
853
|
+
args: [now, threadId],
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
await this.client.batch(batchStatements, 'write');
|
|
859
|
+
|
|
860
|
+
const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
|
|
861
|
+
return updatedResult.rows.map(row => this.parseRow(row));
|
|
862
|
+
}
|
|
863
|
+
|
|
758
864
|
private transformEvalRow(row: Record<string, any>): EvalRow {
|
|
759
865
|
const resultValue = JSON.parse(row.result as string);
|
|
760
866
|
const testInfoValue = row.test_info ? JSON.parse(row.test_info as string) : undefined;
|