@mastra/libsql 1.0.0-beta.8 → 1.0.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +1358 -0
  2. package/dist/docs/README.md +39 -0
  3. package/dist/docs/SKILL.md +40 -0
  4. package/dist/docs/SOURCE_MAP.json +6 -0
  5. package/dist/docs/agents/01-agent-memory.md +166 -0
  6. package/dist/docs/agents/02-networks.md +292 -0
  7. package/dist/docs/agents/03-agent-approval.md +377 -0
  8. package/dist/docs/agents/04-network-approval.md +274 -0
  9. package/dist/docs/core/01-reference.md +151 -0
  10. package/dist/docs/guides/01-ai-sdk.md +141 -0
  11. package/dist/docs/memory/01-overview.md +76 -0
  12. package/dist/docs/memory/02-storage.md +233 -0
  13. package/dist/docs/memory/03-working-memory.md +390 -0
  14. package/dist/docs/memory/04-semantic-recall.md +233 -0
  15. package/dist/docs/memory/05-memory-processors.md +318 -0
  16. package/dist/docs/memory/06-reference.md +133 -0
  17. package/dist/docs/observability/01-overview.md +64 -0
  18. package/dist/docs/observability/02-default.md +177 -0
  19. package/dist/docs/rag/01-retrieval.md +548 -0
  20. package/dist/docs/storage/01-reference.md +542 -0
  21. package/dist/docs/vectors/01-reference.md +213 -0
  22. package/dist/docs/workflows/01-snapshots.md +240 -0
  23. package/dist/index.cjs +2394 -1824
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.js +2392 -1827
  26. package/dist/index.js.map +1 -1
  27. package/dist/storage/db/index.d.ts +305 -0
  28. package/dist/storage/db/index.d.ts.map +1 -0
  29. package/dist/storage/{domains → db}/utils.d.ts +21 -13
  30. package/dist/storage/db/utils.d.ts.map +1 -0
  31. package/dist/storage/domains/agents/index.d.ts +5 -7
  32. package/dist/storage/domains/agents/index.d.ts.map +1 -1
  33. package/dist/storage/domains/memory/index.d.ts +8 -10
  34. package/dist/storage/domains/memory/index.d.ts.map +1 -1
  35. package/dist/storage/domains/observability/index.d.ts +42 -27
  36. package/dist/storage/domains/observability/index.d.ts.map +1 -1
  37. package/dist/storage/domains/scores/index.d.ts +11 -27
  38. package/dist/storage/domains/scores/index.d.ts.map +1 -1
  39. package/dist/storage/domains/workflows/index.d.ts +10 -14
  40. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  41. package/dist/storage/index.d.ts +28 -189
  42. package/dist/storage/index.d.ts.map +1 -1
  43. package/dist/vector/index.d.ts +6 -2
  44. package/dist/vector/index.d.ts.map +1 -1
  45. package/dist/vector/sql-builder.d.ts.map +1 -1
  46. package/package.json +9 -8
  47. package/dist/storage/domains/operations/index.d.ts +0 -110
  48. package/dist/storage/domains/operations/index.d.ts.map +0 -1
  49. package/dist/storage/domains/utils.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createClient } from '@libsql/client';
2
2
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
3
- import { createVectorErrorId, MastraStorage, StoreOperations, TABLE_WORKFLOW_SNAPSHOT, TABLE_SPANS, createStorageErrorId, ScoresStorage, TABLE_SCORERS, normalizePerPage, calculatePagination, transformScoreRow, WorkflowsStorage, MemoryStorage, TABLE_MESSAGES, TABLE_THREADS, TABLE_RESOURCES, ObservabilityStorage, AgentsStorage, TABLE_AGENTS, TABLE_SCHEMAS, safelyParseJSON, SPAN_SCHEMA } from '@mastra/core/storage';
3
+ import { createVectorErrorId, AgentsStorage, AGENTS_SCHEMA, TABLE_AGENTS, createStorageErrorId, normalizePerPage, calculatePagination, MemoryStorage, TABLE_SCHEMAS, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, ObservabilityStorage, SPAN_SCHEMA, TABLE_SPANS, listTracesArgsSchema, ScoresStorage, SCORERS_SCHEMA, TABLE_SCORERS, transformScoreRow, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, MastraCompositeStore, TraceStatus, getSqlType, safelyParseJSON } from '@mastra/core/storage';
4
4
  import { parseSqlIdentifier, parseFieldKey } from '@mastra/core/utils';
5
- import { MastraVector } from '@mastra/core/vector';
5
+ import { MastraVector, validateTopK, validateUpsertInput } from '@mastra/core/vector';
6
6
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
7
+ import { MastraBase } from '@mastra/core/base';
7
8
  import { MessageList } from '@mastra/core/agent';
8
9
  import { saveScorePayloadSchema } from '@mastra/core/evals';
9
10
 
@@ -241,10 +242,10 @@ var FILTER_OPERATORS = {
241
242
  };
242
243
  },
243
244
  // Element Operators
244
- $exists: (key) => {
245
+ $exists: (key, value) => {
245
246
  const jsonPath = getJsonPath(key);
246
247
  return {
247
- sql: `json_extract(metadata, ${jsonPath}) IS NOT NULL`,
248
+ sql: value === false ? `json_extract(metadata, ${jsonPath}) IS NULL` : `json_extract(metadata, ${jsonPath}) IS NOT NULL`,
248
249
  needsValue: false
249
250
  };
250
251
  },
@@ -508,7 +509,7 @@ var LibSQLVector = class extends MastraVector {
508
509
  maxRetries;
509
510
  initialBackoffMs;
510
511
  constructor({
511
- connectionUrl,
512
+ url,
512
513
  authToken,
513
514
  syncUrl,
514
515
  syncInterval,
@@ -518,14 +519,14 @@ var LibSQLVector = class extends MastraVector {
518
519
  }) {
519
520
  super({ id });
520
521
  this.turso = createClient({
521
- url: connectionUrl,
522
+ url,
522
523
  syncUrl,
523
524
  authToken,
524
525
  syncInterval
525
526
  });
526
527
  this.maxRetries = maxRetries;
527
528
  this.initialBackoffMs = initialBackoffMs;
528
- if (connectionUrl.includes(`file:`) || connectionUrl.includes(`:memory:`)) {
529
+ if (url.includes(`file:`) || url.includes(`:memory:`)) {
529
530
  this.turso.execute("PRAGMA journal_mode=WAL;").then(() => this.logger.debug("LibSQLStore: PRAGMA journal_mode=WAL set.")).catch((err) => this.logger.warn("LibSQLStore: Failed to set PRAGMA journal_mode=WAL.", err));
530
531
  this.turso.execute("PRAGMA busy_timeout = 5000;").then(() => this.logger.debug("LibSQLStore: PRAGMA busy_timeout=5000 set.")).catch((err) => this.logger.warn("LibSQLStore: Failed to set PRAGMA busy_timeout=5000.", err));
531
532
  }
@@ -537,7 +538,7 @@ var LibSQLVector = class extends MastraVector {
537
538
  try {
538
539
  return await operation();
539
540
  } catch (error) {
540
- if (error.code === "SQLITE_BUSY" || error.message && error.message.toLowerCase().includes("database is locked")) {
541
+ if (error.code === "SQLITE_BUSY" || error.code === "SQLITE_LOCKED" || error.code === "SQLITE_LOCKED_SHAREDCACHE" || error.message && error.message.toLowerCase().includes("database is locked") || error.message && error.message.toLowerCase().includes("database table is locked")) {
541
542
  attempts++;
542
543
  if (attempts >= this.maxRetries) {
543
544
  this.logger.error(
@@ -571,22 +572,14 @@ var LibSQLVector = class extends MastraVector {
571
572
  minScore = -1
572
573
  // Default to -1 to include all results (cosine similarity ranges from -1 to 1)
573
574
  }) {
574
- try {
575
- if (!Number.isInteger(topK) || topK <= 0) {
576
- throw new Error("topK must be a positive integer");
577
- }
578
- if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
579
- throw new Error("queryVector must be an array of finite numbers");
580
- }
581
- } catch (error) {
582
- throw new MastraError(
583
- {
584
- id: createVectorErrorId("LIBSQL", "QUERY", "INVALID_ARGS"),
585
- domain: ErrorDomain.STORAGE,
586
- category: ErrorCategory.USER
587
- },
588
- error
589
- );
575
+ validateTopK("LIBSQL", topK);
576
+ if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
577
+ throw new MastraError({
578
+ id: createVectorErrorId("LIBSQL", "QUERY", "INVALID_ARGS"),
579
+ domain: ErrorDomain.STORAGE,
580
+ category: ErrorCategory.USER,
581
+ details: { message: "queryVector must be an array of finite numbers" }
582
+ });
590
583
  }
591
584
  try {
592
585
  const parsedIndexName = parseSqlIdentifier(indexName, "index name");
@@ -646,6 +639,7 @@ var LibSQLVector = class extends MastraVector {
646
639
  }
647
640
  }
648
641
  async doUpsert({ indexName, vectors, metadata, ids }) {
642
+ validateUpsertInput("LIBSQL", vectors, metadata, ids);
649
643
  const tx = await this.turso.transaction("write");
650
644
  try {
651
645
  const parsedIndexName = parseSqlIdentifier(indexName, "index name");
@@ -1092,833 +1086,1347 @@ var LibSQLVector = class extends MastraVector {
1092
1086
  });
1093
1087
  }
1094
1088
  };
1095
- var AgentsLibSQL = class extends AgentsStorage {
1096
- client;
1097
- constructor({ client, operations: _ }) {
1098
- super();
1099
- this.client = client;
1100
- }
1101
- parseJson(value, fieldName) {
1102
- if (!value) return void 0;
1103
- if (typeof value !== "string") return value;
1104
- try {
1105
- return JSON.parse(value);
1106
- } catch (error) {
1107
- const details = {
1108
- value: value.length > 100 ? value.substring(0, 100) + "..." : value
1109
- };
1110
- if (fieldName) {
1111
- details.field = fieldName;
1089
+ function buildSelectColumns(tableName) {
1090
+ const schema = TABLE_SCHEMAS[tableName];
1091
+ return Object.keys(schema).map((col) => {
1092
+ const colDef = schema[col];
1093
+ const parsedCol = parseSqlIdentifier(col, "column name");
1094
+ return colDef?.type === "jsonb" ? `json(${parsedCol}) as ${parsedCol}` : parsedCol;
1095
+ }).join(", ");
1096
+ }
1097
+ function isLockError(error) {
1098
+ return error.code === "SQLITE_BUSY" || error.code === "SQLITE_LOCKED" || error.message?.toLowerCase().includes("database is locked") || error.message?.toLowerCase().includes("database table is locked") || error.message?.toLowerCase().includes("table is locked") || error.constructor.name === "SqliteError" && error.message?.toLowerCase().includes("locked");
1099
+ }
1100
+ function createExecuteWriteOperationWithRetry({
1101
+ logger,
1102
+ maxRetries,
1103
+ initialBackoffMs
1104
+ }) {
1105
+ return async function executeWriteOperationWithRetry(operationFn, operationDescription) {
1106
+ let attempts = 0;
1107
+ let backoff = initialBackoffMs;
1108
+ while (attempts < maxRetries) {
1109
+ try {
1110
+ return await operationFn();
1111
+ } catch (error) {
1112
+ logger.debug(`LibSQLStore: Error caught in retry loop for ${operationDescription}`, {
1113
+ errorType: error.constructor.name,
1114
+ errorCode: error.code,
1115
+ errorMessage: error.message,
1116
+ attempts,
1117
+ maxRetries
1118
+ });
1119
+ if (isLockError(error)) {
1120
+ attempts++;
1121
+ if (attempts >= maxRetries) {
1122
+ logger.error(
1123
+ `LibSQLStore: Operation failed after ${maxRetries} attempts due to database lock: ${error.message}`,
1124
+ { error, attempts, maxRetries }
1125
+ );
1126
+ throw error;
1127
+ }
1128
+ logger.warn(
1129
+ `LibSQLStore: Attempt ${attempts} failed due to database lock during ${operationDescription}. Retrying in ${backoff}ms...`,
1130
+ { errorMessage: error.message, attempts, backoff, maxRetries }
1131
+ );
1132
+ await new Promise((resolve) => setTimeout(resolve, backoff));
1133
+ backoff *= 2;
1134
+ } else {
1135
+ logger.error(`LibSQLStore: Non-lock error during ${operationDescription}, not retrying`, { error });
1136
+ throw error;
1137
+ }
1112
1138
  }
1113
- throw new MastraError(
1114
- {
1115
- id: createStorageErrorId("LIBSQL", "PARSE_JSON", "INVALID_JSON"),
1116
- domain: ErrorDomain.STORAGE,
1117
- category: ErrorCategory.SYSTEM,
1118
- text: `Failed to parse JSON${fieldName ? ` for field "${fieldName}"` : ""}: ${error instanceof Error ? error.message : "Unknown error"}`,
1119
- details
1120
- },
1121
- error
1122
- );
1123
1139
  }
1124
- }
1125
- parseRow(row) {
1126
- return {
1127
- id: row.id,
1128
- name: row.name,
1129
- description: row.description,
1130
- instructions: row.instructions,
1131
- model: this.parseJson(row.model, "model"),
1132
- tools: this.parseJson(row.tools, "tools"),
1133
- defaultOptions: this.parseJson(row.defaultOptions, "defaultOptions"),
1134
- workflows: this.parseJson(row.workflows, "workflows"),
1135
- agents: this.parseJson(row.agents, "agents"),
1136
- inputProcessors: this.parseJson(row.inputProcessors, "inputProcessors"),
1137
- outputProcessors: this.parseJson(row.outputProcessors, "outputProcessors"),
1138
- memory: this.parseJson(row.memory, "memory"),
1139
- scorers: this.parseJson(row.scorers, "scorers"),
1140
- metadata: this.parseJson(row.metadata, "metadata"),
1141
- createdAt: new Date(row.createdAt),
1142
- updatedAt: new Date(row.updatedAt)
1143
- };
1144
- }
1145
- async getAgentById({ id }) {
1146
- try {
1147
- const result = await this.client.execute({
1148
- sql: `SELECT * FROM "${TABLE_AGENTS}" WHERE id = ?`,
1149
- args: [id]
1150
- });
1151
- if (!result.rows || result.rows.length === 0) {
1152
- return null;
1153
- }
1154
- return this.parseRow(result.rows[0]);
1155
- } catch (error) {
1156
- throw new MastraError(
1157
- {
1158
- id: createStorageErrorId("LIBSQL", "GET_AGENT_BY_ID", "FAILED"),
1159
- domain: ErrorDomain.STORAGE,
1160
- category: ErrorCategory.THIRD_PARTY,
1161
- details: { agentId: id }
1162
- },
1163
- error
1164
- );
1140
+ throw new Error(`LibSQLStore: Unexpected exit from retry loop for ${operationDescription}`);
1141
+ };
1142
+ }
1143
+ function prepareStatement({ tableName, record }) {
1144
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1145
+ const schema = TABLE_SCHEMAS[tableName];
1146
+ const columnNames = Object.keys(record);
1147
+ const columns = columnNames.map((col) => parseSqlIdentifier(col, "column name"));
1148
+ const values = columnNames.map((col) => {
1149
+ const v = record[col];
1150
+ if (typeof v === `undefined` || v === null) {
1151
+ return null;
1165
1152
  }
1166
- }
1167
- async createAgent({ agent }) {
1168
- try {
1169
- const now = /* @__PURE__ */ new Date();
1170
- const nowIso = now.toISOString();
1171
- await this.client.execute({
1172
- sql: `INSERT INTO "${TABLE_AGENTS}" (id, name, description, instructions, model, tools, "defaultOptions", workflows, agents, "inputProcessors", "outputProcessors", memory, scorers, metadata, "createdAt", "updatedAt")
1173
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1174
- args: [
1175
- agent.id,
1176
- agent.name,
1177
- agent.description ?? null,
1178
- agent.instructions,
1179
- JSON.stringify(agent.model),
1180
- agent.tools ? JSON.stringify(agent.tools) : null,
1181
- agent.defaultOptions ? JSON.stringify(agent.defaultOptions) : null,
1182
- agent.workflows ? JSON.stringify(agent.workflows) : null,
1183
- agent.agents ? JSON.stringify(agent.agents) : null,
1184
- agent.inputProcessors ? JSON.stringify(agent.inputProcessors) : null,
1185
- agent.outputProcessors ? JSON.stringify(agent.outputProcessors) : null,
1186
- agent.memory ? JSON.stringify(agent.memory) : null,
1187
- agent.scorers ? JSON.stringify(agent.scorers) : null,
1188
- agent.metadata ? JSON.stringify(agent.metadata) : null,
1189
- nowIso,
1190
- nowIso
1191
- ]
1192
- });
1193
- return {
1194
- ...agent,
1195
- createdAt: now,
1196
- updatedAt: now
1197
- };
1198
- } catch (error) {
1199
- throw new MastraError(
1200
- {
1201
- id: createStorageErrorId("LIBSQL", "CREATE_AGENT", "FAILED"),
1202
- domain: ErrorDomain.STORAGE,
1203
- category: ErrorCategory.THIRD_PARTY,
1204
- details: { agentId: agent.id }
1205
- },
1206
- error
1207
- );
1153
+ const colDef = schema[col];
1154
+ if (colDef?.type === "jsonb") {
1155
+ return JSON.stringify(v);
1208
1156
  }
1209
- }
1210
- async updateAgent({ id, ...updates }) {
1211
- try {
1212
- const existingAgent = await this.getAgentById({ id });
1213
- if (!existingAgent) {
1214
- throw new MastraError({
1215
- id: createStorageErrorId("LIBSQL", "UPDATE_AGENT", "NOT_FOUND"),
1216
- domain: ErrorDomain.STORAGE,
1217
- category: ErrorCategory.USER,
1218
- text: `Agent ${id} not found`,
1219
- details: { agentId: id }
1220
- });
1221
- }
1222
- const setClauses = [];
1223
- const args = [];
1224
- if (updates.name !== void 0) {
1225
- setClauses.push("name = ?");
1226
- args.push(updates.name);
1227
- }
1228
- if (updates.description !== void 0) {
1229
- setClauses.push("description = ?");
1230
- args.push(updates.description);
1231
- }
1232
- if (updates.instructions !== void 0) {
1233
- setClauses.push("instructions = ?");
1234
- args.push(updates.instructions);
1235
- }
1236
- if (updates.model !== void 0) {
1237
- setClauses.push("model = ?");
1238
- args.push(JSON.stringify(updates.model));
1239
- }
1240
- if (updates.tools !== void 0) {
1241
- setClauses.push("tools = ?");
1242
- args.push(JSON.stringify(updates.tools));
1243
- }
1244
- if (updates.defaultOptions !== void 0) {
1245
- setClauses.push('"defaultOptions" = ?');
1246
- args.push(JSON.stringify(updates.defaultOptions));
1247
- }
1248
- if (updates.workflows !== void 0) {
1249
- setClauses.push("workflows = ?");
1250
- args.push(JSON.stringify(updates.workflows));
1251
- }
1252
- if (updates.agents !== void 0) {
1253
- setClauses.push("agents = ?");
1254
- args.push(JSON.stringify(updates.agents));
1255
- }
1256
- if (updates.inputProcessors !== void 0) {
1257
- setClauses.push('"inputProcessors" = ?');
1258
- args.push(JSON.stringify(updates.inputProcessors));
1259
- }
1260
- if (updates.outputProcessors !== void 0) {
1261
- setClauses.push('"outputProcessors" = ?');
1262
- args.push(JSON.stringify(updates.outputProcessors));
1263
- }
1264
- if (updates.memory !== void 0) {
1265
- setClauses.push("memory = ?");
1266
- args.push(JSON.stringify(updates.memory));
1267
- }
1268
- if (updates.scorers !== void 0) {
1269
- setClauses.push("scorers = ?");
1270
- args.push(JSON.stringify(updates.scorers));
1271
- }
1272
- if (updates.metadata !== void 0) {
1273
- const mergedMetadata = { ...existingAgent.metadata, ...updates.metadata };
1274
- setClauses.push("metadata = ?");
1275
- args.push(JSON.stringify(mergedMetadata));
1276
- }
1277
- const now = /* @__PURE__ */ new Date();
1278
- setClauses.push('"updatedAt" = ?');
1279
- args.push(now.toISOString());
1280
- args.push(id);
1281
- if (setClauses.length > 1) {
1282
- await this.client.execute({
1283
- sql: `UPDATE "${TABLE_AGENTS}" SET ${setClauses.join(", ")} WHERE id = ?`,
1284
- args
1285
- });
1286
- }
1287
- const updatedAgent = await this.getAgentById({ id });
1288
- if (!updatedAgent) {
1289
- throw new MastraError({
1290
- id: createStorageErrorId("LIBSQL", "UPDATE_AGENT", "NOT_FOUND_AFTER_UPDATE"),
1291
- domain: ErrorDomain.STORAGE,
1292
- category: ErrorCategory.SYSTEM,
1293
- text: `Agent ${id} not found after update`,
1294
- details: { agentId: id }
1295
- });
1296
- }
1297
- return updatedAgent;
1298
- } catch (error) {
1299
- if (error instanceof MastraError) {
1300
- throw error;
1301
- }
1302
- throw new MastraError(
1303
- {
1304
- id: createStorageErrorId("LIBSQL", "UPDATE_AGENT", "FAILED"),
1305
- domain: ErrorDomain.STORAGE,
1306
- category: ErrorCategory.THIRD_PARTY,
1307
- details: { agentId: id }
1308
- },
1309
- error
1310
- );
1157
+ if (v instanceof Date) {
1158
+ return v.toISOString();
1159
+ }
1160
+ return typeof v === "object" ? JSON.stringify(v) : v;
1161
+ });
1162
+ const placeholders = columnNames.map((col) => {
1163
+ const colDef = schema[col];
1164
+ return colDef?.type === "jsonb" ? "jsonb(?)" : "?";
1165
+ }).join(", ");
1166
+ return {
1167
+ sql: `INSERT OR REPLACE INTO ${parsedTableName} (${columns.join(", ")}) VALUES (${placeholders})`,
1168
+ args: values
1169
+ };
1170
+ }
1171
+ function prepareUpdateStatement({
1172
+ tableName,
1173
+ updates,
1174
+ keys
1175
+ }) {
1176
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1177
+ const schema = TABLE_SCHEMAS[tableName];
1178
+ const updateColumnNames = Object.keys(updates);
1179
+ const updateColumns = updateColumnNames.map((col) => parseSqlIdentifier(col, "column name"));
1180
+ const updateValues = updateColumnNames.map((col) => {
1181
+ const colDef = schema[col];
1182
+ const v = updates[col];
1183
+ if (colDef?.type === "jsonb") {
1184
+ return transformToSqlValue(v, true);
1311
1185
  }
1186
+ return transformToSqlValue(v, false);
1187
+ });
1188
+ const setClause = updateColumns.map((col, i) => {
1189
+ const colDef = schema[updateColumnNames[i]];
1190
+ return colDef?.type === "jsonb" ? `${col} = jsonb(?)` : `${col} = ?`;
1191
+ }).join(", ");
1192
+ const whereClause = prepareWhereClause(keys, schema);
1193
+ return {
1194
+ sql: `UPDATE ${parsedTableName} SET ${setClause}${whereClause.sql}`,
1195
+ args: [...updateValues, ...whereClause.args]
1196
+ };
1197
+ }
1198
+ function transformToSqlValue(value, forceJsonStringify = false) {
1199
+ if (typeof value === "undefined" || value === null) {
1200
+ return null;
1312
1201
  }
1313
- async deleteAgent({ id }) {
1314
- try {
1315
- await this.client.execute({
1316
- sql: `DELETE FROM "${TABLE_AGENTS}" WHERE id = ?`,
1317
- args: [id]
1318
- });
1319
- } catch (error) {
1320
- throw new MastraError(
1321
- {
1322
- id: createStorageErrorId("LIBSQL", "DELETE_AGENT", "FAILED"),
1323
- domain: ErrorDomain.STORAGE,
1324
- category: ErrorCategory.THIRD_PARTY,
1325
- details: { agentId: id }
1326
- },
1327
- error
1328
- );
1202
+ if (forceJsonStringify) {
1203
+ return JSON.stringify(value);
1204
+ }
1205
+ if (value instanceof Date) {
1206
+ return value.toISOString();
1207
+ }
1208
+ return typeof value === "object" ? JSON.stringify(value) : value;
1209
+ }
1210
+ function prepareDeleteStatement({ tableName, keys }) {
1211
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1212
+ const whereClause = prepareWhereClause(keys, TABLE_SCHEMAS[tableName]);
1213
+ return {
1214
+ sql: `DELETE FROM ${parsedTableName}${whereClause.sql}`,
1215
+ args: whereClause.args
1216
+ };
1217
+ }
1218
+ function prepareWhereClause(filters, schema) {
1219
+ const conditions = [];
1220
+ const args = [];
1221
+ for (const [columnName, filterValue] of Object.entries(filters)) {
1222
+ const column = schema[columnName];
1223
+ if (!column) {
1224
+ throw new Error(`Unknown column: ${columnName}`);
1329
1225
  }
1226
+ const parsedColumn = parseSqlIdentifier(columnName, "column name");
1227
+ const result = buildCondition2(parsedColumn, filterValue);
1228
+ conditions.push(result.condition);
1229
+ args.push(...result.args);
1330
1230
  }
1331
- async listAgents(args) {
1332
- const { page = 0, perPage: perPageInput, orderBy } = args || {};
1333
- const { field, direction } = this.parseOrderBy(orderBy);
1334
- if (page < 0) {
1231
+ return {
1232
+ sql: conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "",
1233
+ args
1234
+ };
1235
+ }
1236
+ function buildCondition2(columnName, filterValue) {
1237
+ if (filterValue === null) {
1238
+ return { condition: `${columnName} IS NULL`, args: [] };
1239
+ }
1240
+ if (typeof filterValue === "object" && filterValue !== null && ("startAt" in filterValue || "endAt" in filterValue)) {
1241
+ return buildDateRangeCondition(columnName, filterValue);
1242
+ }
1243
+ return {
1244
+ condition: `${columnName} = ?`,
1245
+ args: [transformToSqlValue(filterValue)]
1246
+ };
1247
+ }
1248
+ function buildDateRangeCondition(columnName, range) {
1249
+ const conditions = [];
1250
+ const args = [];
1251
+ if (range.startAt !== void 0) {
1252
+ conditions.push(`${columnName} >= ?`);
1253
+ args.push(transformToSqlValue(range.startAt));
1254
+ }
1255
+ if (range.endAt !== void 0) {
1256
+ conditions.push(`${columnName} <= ?`);
1257
+ args.push(transformToSqlValue(range.endAt));
1258
+ }
1259
+ if (conditions.length === 0) {
1260
+ throw new Error("Date range must specify at least startAt or endAt");
1261
+ }
1262
+ return {
1263
+ condition: conditions.join(" AND "),
1264
+ args
1265
+ };
1266
+ }
1267
+ function transformFromSqlRow({
1268
+ tableName,
1269
+ sqlRow
1270
+ }) {
1271
+ const result = {};
1272
+ const jsonColumns = new Set(
1273
+ Object.keys(TABLE_SCHEMAS[tableName]).filter((key) => TABLE_SCHEMAS[tableName][key].type === "jsonb").map((key) => key)
1274
+ );
1275
+ const dateColumns = new Set(
1276
+ Object.keys(TABLE_SCHEMAS[tableName]).filter((key) => TABLE_SCHEMAS[tableName][key].type === "timestamp").map((key) => key)
1277
+ );
1278
+ for (const [key, value] of Object.entries(sqlRow)) {
1279
+ if (value === null || value === void 0) {
1280
+ result[key] = value;
1281
+ continue;
1282
+ }
1283
+ if (dateColumns.has(key) && typeof value === "string") {
1284
+ result[key] = new Date(value);
1285
+ continue;
1286
+ }
1287
+ if (jsonColumns.has(key) && typeof value === "string") {
1288
+ result[key] = safelyParseJSON(value);
1289
+ continue;
1290
+ }
1291
+ result[key] = value;
1292
+ }
1293
+ return result;
1294
+ }
1295
+
1296
+ // src/storage/db/index.ts
1297
+ function resolveClient(config) {
1298
+ if ("client" in config) {
1299
+ return config.client;
1300
+ }
1301
+ return createClient({
1302
+ url: config.url,
1303
+ ...config.authToken ? { authToken: config.authToken } : {}
1304
+ });
1305
+ }
1306
+ var LibSQLDB = class extends MastraBase {
1307
+ client;
1308
+ maxRetries;
1309
+ initialBackoffMs;
1310
+ executeWriteOperationWithRetry;
1311
+ constructor({
1312
+ client,
1313
+ maxRetries,
1314
+ initialBackoffMs
1315
+ }) {
1316
+ super({
1317
+ component: "STORAGE",
1318
+ name: "LIBSQL_DB_LAYER"
1319
+ });
1320
+ this.client = client;
1321
+ this.maxRetries = maxRetries ?? 5;
1322
+ this.initialBackoffMs = initialBackoffMs ?? 100;
1323
+ this.executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
1324
+ logger: this.logger,
1325
+ maxRetries: this.maxRetries,
1326
+ initialBackoffMs: this.initialBackoffMs
1327
+ });
1328
+ }
1329
+ /**
1330
+ * Checks if a column exists in the specified table.
1331
+ *
1332
+ * @param table - The name of the table to check
1333
+ * @param column - The name of the column to look for
1334
+ * @returns `true` if the column exists in the table, `false` otherwise
1335
+ */
1336
+ async hasColumn(table, column) {
1337
+ const sanitizedTable = parseSqlIdentifier(table, "table name");
1338
+ const result = await this.client.execute({
1339
+ sql: `PRAGMA table_info("${sanitizedTable}")`
1340
+ });
1341
+ return result.rows?.some((row) => row.name === column);
1342
+ }
1343
+ /**
1344
+ * Internal insert implementation without retry logic.
1345
+ */
1346
+ async doInsert({
1347
+ tableName,
1348
+ record
1349
+ }) {
1350
+ await this.client.execute(
1351
+ prepareStatement({
1352
+ tableName,
1353
+ record
1354
+ })
1355
+ );
1356
+ }
1357
+ /**
1358
+ * Inserts or replaces a record in the specified table with automatic retry on lock errors.
1359
+ *
1360
+ * @param args - The insert arguments
1361
+ * @param args.tableName - The name of the table to insert into
1362
+ * @param args.record - The record to insert (key-value pairs)
1363
+ */
1364
+ insert(args) {
1365
+ return this.executeWriteOperationWithRetry(() => this.doInsert(args), `insert into table ${args.tableName}`);
1366
+ }
1367
+ /**
1368
+ * Internal update implementation without retry logic.
1369
+ */
1370
+ async doUpdate({
1371
+ tableName,
1372
+ keys,
1373
+ data
1374
+ }) {
1375
+ await this.client.execute(prepareUpdateStatement({ tableName, updates: data, keys }));
1376
+ }
1377
+ /**
1378
+ * Updates a record in the specified table with automatic retry on lock errors.
1379
+ *
1380
+ * @param args - The update arguments
1381
+ * @param args.tableName - The name of the table to update
1382
+ * @param args.keys - The key(s) identifying the record to update
1383
+ * @param args.data - The fields to update (key-value pairs)
1384
+ */
1385
+ update(args) {
1386
+ return this.executeWriteOperationWithRetry(() => this.doUpdate(args), `update table ${args.tableName}`);
1387
+ }
1388
+ /**
1389
+ * Internal batch insert implementation without retry logic.
1390
+ */
1391
+ async doBatchInsert({
1392
+ tableName,
1393
+ records
1394
+ }) {
1395
+ if (records.length === 0) return;
1396
+ const batchStatements = records.map((r) => prepareStatement({ tableName, record: r }));
1397
+ await this.client.batch(batchStatements, "write");
1398
+ }
1399
+ /**
1400
+ * Inserts multiple records in a single batch transaction with automatic retry on lock errors.
1401
+ *
1402
+ * @param args - The batch insert arguments
1403
+ * @param args.tableName - The name of the table to insert into
1404
+ * @param args.records - Array of records to insert
1405
+ * @throws {MastraError} When the batch insert fails after retries
1406
+ */
1407
+ async batchInsert(args) {
1408
+ return this.executeWriteOperationWithRetry(
1409
+ () => this.doBatchInsert(args),
1410
+ `batch insert into table ${args.tableName}`
1411
+ ).catch((error) => {
1335
1412
  throw new MastraError(
1336
1413
  {
1337
- id: createStorageErrorId("LIBSQL", "LIST_AGENTS", "INVALID_PAGE"),
1414
+ id: createStorageErrorId("LIBSQL", "BATCH_INSERT", "FAILED"),
1338
1415
  domain: ErrorDomain.STORAGE,
1339
- category: ErrorCategory.USER,
1340
- details: { page }
1416
+ category: ErrorCategory.THIRD_PARTY,
1417
+ details: {
1418
+ tableName: args.tableName
1419
+ }
1341
1420
  },
1342
- new Error("page must be >= 0")
1421
+ error
1422
+ );
1423
+ });
1424
+ }
1425
+ /**
1426
+ * Internal batch update implementation without retry logic.
1427
+ * Each record can be updated based on single or composite keys.
1428
+ */
1429
+ async doBatchUpdate({
1430
+ tableName,
1431
+ updates
1432
+ }) {
1433
+ if (updates.length === 0) return;
1434
+ const batchStatements = updates.map(
1435
+ ({ keys, data }) => prepareUpdateStatement({
1436
+ tableName,
1437
+ updates: data,
1438
+ keys
1439
+ })
1440
+ );
1441
+ await this.client.batch(batchStatements, "write");
1442
+ }
1443
+ /**
1444
+ * Updates multiple records in a single batch transaction with automatic retry on lock errors.
1445
+ * Each record can be updated based on single or composite keys.
1446
+ *
1447
+ * @param args - The batch update arguments
1448
+ * @param args.tableName - The name of the table to update
1449
+ * @param args.updates - Array of update operations, each containing keys and data
1450
+ * @throws {MastraError} When the batch update fails after retries
1451
+ */
1452
+ async batchUpdate(args) {
1453
+ return this.executeWriteOperationWithRetry(
1454
+ () => this.doBatchUpdate(args),
1455
+ `batch update in table ${args.tableName}`
1456
+ ).catch((error) => {
1457
+ throw new MastraError(
1458
+ {
1459
+ id: createStorageErrorId("LIBSQL", "BATCH_UPDATE", "FAILED"),
1460
+ domain: ErrorDomain.STORAGE,
1461
+ category: ErrorCategory.THIRD_PARTY,
1462
+ details: {
1463
+ tableName: args.tableName
1464
+ }
1465
+ },
1466
+ error
1467
+ );
1468
+ });
1469
+ }
1470
+ /**
1471
+ * Internal batch delete implementation without retry logic.
1472
+ * Each record can be deleted based on single or composite keys.
1473
+ */
1474
+ async doBatchDelete({
1475
+ tableName,
1476
+ keys
1477
+ }) {
1478
+ if (keys.length === 0) return;
1479
+ const batchStatements = keys.map(
1480
+ (keyObj) => prepareDeleteStatement({
1481
+ tableName,
1482
+ keys: keyObj
1483
+ })
1484
+ );
1485
+ await this.client.batch(batchStatements, "write");
1486
+ }
1487
+ /**
1488
+ * Deletes multiple records in a single batch transaction with automatic retry on lock errors.
1489
+ * Each record can be deleted based on single or composite keys.
1490
+ *
1491
+ * @param args - The batch delete arguments
1492
+ * @param args.tableName - The name of the table to delete from
1493
+ * @param args.keys - Array of key objects identifying records to delete
1494
+ * @throws {MastraError} When the batch delete fails after retries
1495
+ */
1496
+ async batchDelete({
1497
+ tableName,
1498
+ keys
1499
+ }) {
1500
+ return this.executeWriteOperationWithRetry(
1501
+ () => this.doBatchDelete({ tableName, keys }),
1502
+ `batch delete from table ${tableName}`
1503
+ ).catch((error) => {
1504
+ throw new MastraError(
1505
+ {
1506
+ id: createStorageErrorId("LIBSQL", "BATCH_DELETE", "FAILED"),
1507
+ domain: ErrorDomain.STORAGE,
1508
+ category: ErrorCategory.THIRD_PARTY,
1509
+ details: {
1510
+ tableName
1511
+ }
1512
+ },
1513
+ error
1514
+ );
1515
+ });
1516
+ }
1517
+ /**
1518
+ * Internal single-record delete implementation without retry logic.
1519
+ */
1520
+ async doDelete({ tableName, keys }) {
1521
+ await this.client.execute(prepareDeleteStatement({ tableName, keys }));
1522
+ }
1523
+ /**
1524
+ * Deletes a single record from the specified table with automatic retry on lock errors.
1525
+ *
1526
+ * @param args - The delete arguments
1527
+ * @param args.tableName - The name of the table to delete from
1528
+ * @param args.keys - The key(s) identifying the record to delete
1529
+ * @throws {MastraError} When the delete fails after retries
1530
+ */
1531
+ async delete(args) {
1532
+ return this.executeWriteOperationWithRetry(() => this.doDelete(args), `delete from table ${args.tableName}`).catch(
1533
+ (error) => {
1534
+ throw new MastraError(
1535
+ {
1536
+ id: createStorageErrorId("LIBSQL", "DELETE", "FAILED"),
1537
+ domain: ErrorDomain.STORAGE,
1538
+ category: ErrorCategory.THIRD_PARTY,
1539
+ details: {
1540
+ tableName: args.tableName
1541
+ }
1542
+ },
1543
+ error
1544
+ );
1545
+ }
1546
+ );
1547
+ }
1548
+ /**
1549
+ * Selects a single record from the specified table by key(s).
1550
+ * Returns the most recently created record if multiple matches exist.
1551
+ * Automatically parses JSON string values back to objects/arrays.
1552
+ *
1553
+ * @typeParam R - The expected return type of the record
1554
+ * @param args - The select arguments
1555
+ * @param args.tableName - The name of the table to select from
1556
+ * @param args.keys - The key(s) identifying the record to select
1557
+ * @returns The matching record or `null` if not found
1558
+ */
1559
+ async select({ tableName, keys }) {
1560
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1561
+ const columns = buildSelectColumns(tableName);
1562
+ const parsedKeys = Object.keys(keys).map((key) => parseSqlIdentifier(key, "column name"));
1563
+ const conditions = parsedKeys.map((key) => `${key} = ?`).join(" AND ");
1564
+ const values = Object.values(keys);
1565
+ const result = await this.client.execute({
1566
+ sql: `SELECT ${columns} FROM ${parsedTableName} WHERE ${conditions} ORDER BY createdAt DESC LIMIT 1`,
1567
+ args: values
1568
+ });
1569
+ if (!result.rows || result.rows.length === 0) {
1570
+ return null;
1571
+ }
1572
+ const row = result.rows[0];
1573
+ const parsed = Object.fromEntries(
1574
+ Object.entries(row || {}).map(([k, v]) => {
1575
+ try {
1576
+ return [k, typeof v === "string" ? v.startsWith("{") || v.startsWith("[") ? JSON.parse(v) : v : v];
1577
+ } catch {
1578
+ return [k, v];
1579
+ }
1580
+ })
1581
+ );
1582
+ return parsed;
1583
+ }
1584
+ /**
1585
+ * Selects multiple records from the specified table with optional filtering, ordering, and pagination.
1586
+ *
1587
+ * @typeParam R - The expected return type of each record
1588
+ * @param args - The select arguments
1589
+ * @param args.tableName - The name of the table to select from
1590
+ * @param args.whereClause - Optional WHERE clause with SQL string and arguments
1591
+ * @param args.orderBy - Optional ORDER BY clause (e.g., "createdAt DESC")
1592
+ * @param args.offset - Optional offset for pagination
1593
+ * @param args.limit - Optional limit for pagination
1594
+ * @param args.args - Optional additional query arguments
1595
+ * @returns Array of matching records
1596
+ */
1597
+ async selectMany({
1598
+ tableName,
1599
+ whereClause,
1600
+ orderBy,
1601
+ offset,
1602
+ limit,
1603
+ args
1604
+ }) {
1605
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1606
+ const columns = buildSelectColumns(tableName);
1607
+ let statement = `SELECT ${columns} FROM ${parsedTableName}`;
1608
+ if (whereClause?.sql) {
1609
+ statement += ` ${whereClause.sql}`;
1610
+ }
1611
+ if (orderBy) {
1612
+ statement += ` ORDER BY ${orderBy}`;
1613
+ }
1614
+ if (limit) {
1615
+ statement += ` LIMIT ${limit}`;
1616
+ }
1617
+ if (offset) {
1618
+ statement += ` OFFSET ${offset}`;
1619
+ }
1620
+ const result = await this.client.execute({
1621
+ sql: statement,
1622
+ args: [...whereClause?.args ?? [], ...args ?? []]
1623
+ });
1624
+ return (result.rows ?? []).map((row) => {
1625
+ return Object.fromEntries(
1626
+ Object.entries(row || {}).map(([k, v]) => {
1627
+ try {
1628
+ return [k, typeof v === "string" ? v.startsWith("{") || v.startsWith("[") ? JSON.parse(v) : v : v];
1629
+ } catch {
1630
+ return [k, v];
1631
+ }
1632
+ })
1343
1633
  );
1634
+ });
1635
+ }
1636
+ /**
1637
+ * Returns the total count of records matching the optional WHERE clause.
1638
+ *
1639
+ * @param args - The count arguments
1640
+ * @param args.tableName - The name of the table to count from
1641
+ * @param args.whereClause - Optional WHERE clause with SQL string and arguments
1642
+ * @returns The total count of matching records
1643
+ */
1644
+ async selectTotalCount({
1645
+ tableName,
1646
+ whereClause
1647
+ }) {
1648
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1649
+ const statement = `SELECT COUNT(*) as count FROM ${parsedTableName} ${whereClause ? `${whereClause.sql}` : ""}`;
1650
+ const result = await this.client.execute({
1651
+ sql: statement,
1652
+ args: whereClause?.args ?? []
1653
+ });
1654
+ if (!result.rows || result.rows.length === 0) {
1655
+ return 0;
1344
1656
  }
1345
- const perPage = normalizePerPage(perPageInput, 100);
1346
- const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1657
+ return result.rows[0]?.count ?? 0;
1658
+ }
1659
+ /**
1660
+ * Maps a storage column type to its SQLite equivalent.
1661
+ */
1662
+ getSqlType(type) {
1663
+ switch (type) {
1664
+ case "bigint":
1665
+ return "INTEGER";
1666
+ // SQLite uses INTEGER for all integer sizes
1667
+ case "timestamp":
1668
+ return "TEXT";
1669
+ // Store timestamps as ISO strings in SQLite
1670
+ case "float":
1671
+ return "REAL";
1672
+ // SQLite's floating point type
1673
+ case "boolean":
1674
+ return "INTEGER";
1675
+ // SQLite uses 0/1 for booleans
1676
+ case "jsonb":
1677
+ return "TEXT";
1678
+ // SQLite: column stores TEXT, we use jsonb()/json() functions for binary optimization
1679
+ default:
1680
+ return getSqlType(type);
1681
+ }
1682
+ }
1683
+ /**
1684
+ * Creates a table if it doesn't exist based on the provided schema.
1685
+ *
1686
+ * @param args - The create table arguments
1687
+ * @param args.tableName - The name of the table to create
1688
+ * @param args.schema - The schema definition for the table columns
1689
+ */
1690
+ async createTable({
1691
+ tableName,
1692
+ schema
1693
+ }) {
1347
1694
  try {
1348
- const countResult = await this.client.execute({
1349
- sql: `SELECT COUNT(*) as count FROM "${TABLE_AGENTS}"`,
1350
- args: []
1695
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1696
+ const columnDefinitions = Object.entries(schema).map(([colName, colDef]) => {
1697
+ const type = this.getSqlType(colDef.type);
1698
+ const nullable = colDef.nullable === false ? "NOT NULL" : "";
1699
+ const primaryKey = colDef.primaryKey ? "PRIMARY KEY" : "";
1700
+ return `"${colName}" ${type} ${nullable} ${primaryKey}`.trim();
1351
1701
  });
1352
- const total = Number(countResult.rows?.[0]?.count ?? 0);
1353
- if (total === 0) {
1354
- return {
1355
- agents: [],
1356
- total: 0,
1357
- page,
1358
- perPage: perPageForResponse,
1359
- hasMore: false
1360
- };
1702
+ const tableConstraints = [];
1703
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
1704
+ tableConstraints.push("UNIQUE (workflow_name, run_id)");
1705
+ }
1706
+ if (tableName === TABLE_SPANS) {
1707
+ tableConstraints.push("UNIQUE (spanId, traceId)");
1708
+ }
1709
+ const allDefinitions = [...columnDefinitions, ...tableConstraints].join(",\n ");
1710
+ const sql = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
1711
+ ${allDefinitions}
1712
+ )`;
1713
+ await this.client.execute(sql);
1714
+ this.logger.debug(`LibSQLDB: Created table ${tableName}`);
1715
+ if (tableName === TABLE_SPANS) {
1716
+ await this.migrateSpansTable();
1361
1717
  }
1362
- const limitValue = perPageInput === false ? total : perPage;
1363
- const dataResult = await this.client.execute({
1364
- sql: `SELECT * FROM "${TABLE_AGENTS}" ORDER BY "${field}" ${direction} LIMIT ? OFFSET ?`,
1365
- args: [limitValue, offset]
1366
- });
1367
- const agents = (dataResult.rows || []).map((row) => this.parseRow(row));
1368
- return {
1369
- agents,
1370
- total,
1371
- page,
1372
- perPage: perPageForResponse,
1373
- hasMore: perPageInput === false ? false : offset + perPage < total
1374
- };
1375
1718
  } catch (error) {
1719
+ if (error instanceof MastraError) {
1720
+ throw error;
1721
+ }
1376
1722
  throw new MastraError(
1377
1723
  {
1378
- id: createStorageErrorId("LIBSQL", "LIST_AGENTS", "FAILED"),
1724
+ id: createStorageErrorId("LIBSQL", "CREATE_TABLE", "FAILED"),
1379
1725
  domain: ErrorDomain.STORAGE,
1380
- category: ErrorCategory.THIRD_PARTY
1726
+ category: ErrorCategory.THIRD_PARTY,
1727
+ details: { tableName }
1381
1728
  },
1382
1729
  error
1383
1730
  );
1384
1731
  }
1385
1732
  }
1386
- };
1387
- var MemoryLibSQL = class extends MemoryStorage {
1388
- client;
1389
- operations;
1390
- constructor({ client, operations }) {
1391
- super();
1392
- this.client = client;
1393
- this.operations = operations;
1733
+ /**
1734
+ * Migrates the spans table schema from OLD_SPAN_SCHEMA to current SPAN_SCHEMA.
1735
+ * This adds new columns that don't exist in old schema and ensures required indexes exist.
1736
+ */
1737
+ async migrateSpansTable() {
1738
+ const schema = TABLE_SCHEMAS[TABLE_SPANS];
1739
+ try {
1740
+ for (const [columnName, columnDef] of Object.entries(schema)) {
1741
+ const columnExists = await this.hasColumn(TABLE_SPANS, columnName);
1742
+ if (!columnExists) {
1743
+ const sqlType = this.getSqlType(columnDef.type);
1744
+ const alterSql = `ALTER TABLE "${TABLE_SPANS}" ADD COLUMN "${columnName}" ${sqlType}`;
1745
+ await this.client.execute(alterSql);
1746
+ this.logger.debug(`LibSQLDB: Added column '${columnName}' to ${TABLE_SPANS}`);
1747
+ }
1748
+ }
1749
+ const indexExists = await this.spansUniqueIndexExists();
1750
+ if (!indexExists) {
1751
+ const duplicateInfo = await this.checkForDuplicateSpans();
1752
+ if (duplicateInfo.hasDuplicates) {
1753
+ const errorMessage = `
1754
+ ===========================================================================
1755
+ MIGRATION REQUIRED: Duplicate spans detected in ${TABLE_SPANS}
1756
+ ===========================================================================
1757
+
1758
+ Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
1759
+
1760
+ The spans table requires a unique constraint on (traceId, spanId), but your
1761
+ database contains duplicate entries that must be resolved first.
1762
+
1763
+ To fix this, run the manual migration command:
1764
+
1765
+ npx mastra migrate
1766
+
1767
+ This command will:
1768
+ 1. Remove duplicate spans (keeping the most complete/recent version)
1769
+ 2. Add the required unique constraint
1770
+
1771
+ Note: This migration may take some time for large tables.
1772
+ ===========================================================================
1773
+ `;
1774
+ throw new MastraError({
1775
+ id: createStorageErrorId("LIBSQL", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
1776
+ domain: ErrorDomain.STORAGE,
1777
+ category: ErrorCategory.USER,
1778
+ text: errorMessage
1779
+ });
1780
+ } else {
1781
+ await this.client.execute(
1782
+ `CREATE UNIQUE INDEX IF NOT EXISTS "mastra_ai_spans_spanid_traceid_idx" ON "${TABLE_SPANS}" ("spanId", "traceId")`
1783
+ );
1784
+ this.logger.debug(`LibSQLDB: Created unique index on (spanId, traceId) for ${TABLE_SPANS}`);
1785
+ }
1786
+ }
1787
+ this.logger.info(`LibSQLDB: Migration completed for ${TABLE_SPANS}`);
1788
+ } catch (error) {
1789
+ if (error instanceof MastraError) {
1790
+ throw error;
1791
+ }
1792
+ this.logger.warn(`LibSQLDB: Failed to migrate spans table ${TABLE_SPANS}:`, error);
1793
+ }
1394
1794
  }
1395
- parseRow(row) {
1396
- let content = row.content;
1795
+ /**
1796
+ * Checks if the unique index on (spanId, traceId) already exists on the spans table.
1797
+ * Used to skip deduplication when the index already exists (migration already complete).
1798
+ */
1799
+ async spansUniqueIndexExists() {
1397
1800
  try {
1398
- content = JSON.parse(row.content);
1801
+ const result = await this.client.execute(
1802
+ `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'mastra_ai_spans_spanid_traceid_idx'`
1803
+ );
1804
+ return (result.rows?.length ?? 0) > 0;
1399
1805
  } catch {
1806
+ return false;
1400
1807
  }
1401
- const result = {
1402
- id: row.id,
1403
- content,
1404
- role: row.role,
1405
- createdAt: new Date(row.createdAt),
1406
- threadId: row.thread_id,
1407
- resourceId: row.resourceId
1408
- };
1409
- if (row.type && row.type !== `v2`) result.type = row.type;
1410
- return result;
1411
1808
  }
1412
- async _getIncludedMessages({ include }) {
1413
- if (!include || include.length === 0) return null;
1414
- const unionQueries = [];
1415
- const params = [];
1416
- for (const inc of include) {
1417
- const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
1418
- unionQueries.push(
1419
- `
1420
- SELECT * FROM (
1421
- WITH target_thread AS (
1422
- SELECT thread_id FROM "${TABLE_MESSAGES}" WHERE id = ?
1423
- ),
1424
- numbered_messages AS (
1425
- SELECT
1426
- id, content, role, type, "createdAt", thread_id, "resourceId",
1427
- ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
1428
- FROM "${TABLE_MESSAGES}"
1429
- WHERE thread_id = (SELECT thread_id FROM target_thread)
1430
- ),
1431
- target_positions AS (
1432
- SELECT row_num as target_pos
1433
- FROM numbered_messages
1434
- WHERE id = ?
1435
- )
1436
- SELECT DISTINCT m.*
1437
- FROM numbered_messages m
1438
- CROSS JOIN target_positions t
1439
- WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
1440
- )
1441
- `
1442
- // Keep ASC for final sorting after fetching context
1809
+ /**
1810
+ * Checks for duplicate (traceId, spanId) combinations in the spans table.
1811
+ * Returns information about duplicates for logging/CLI purposes.
1812
+ */
1813
+ async checkForDuplicateSpans() {
1814
+ try {
1815
+ const result = await this.client.execute(`
1816
+ SELECT COUNT(*) as duplicate_count FROM (
1817
+ SELECT "spanId", "traceId"
1818
+ FROM "${TABLE_SPANS}"
1819
+ GROUP BY "spanId", "traceId"
1820
+ HAVING COUNT(*) > 1
1821
+ )
1822
+ `);
1823
+ const duplicateCount = Number(result.rows?.[0]?.duplicate_count ?? 0);
1824
+ return {
1825
+ hasDuplicates: duplicateCount > 0,
1826
+ duplicateCount
1827
+ };
1828
+ } catch (error) {
1829
+ this.logger.debug(`LibSQLDB: Could not check for duplicates: ${error}`);
1830
+ return { hasDuplicates: false, duplicateCount: 0 };
1831
+ }
1832
+ }
1833
+ /**
1834
+ * Manually run the spans migration to deduplicate and add the unique constraint.
1835
+ * This is intended to be called from the CLI when duplicates are detected.
1836
+ *
1837
+ * @returns Migration result with status and details
1838
+ */
1839
+ async migrateSpans() {
1840
+ const indexExists = await this.spansUniqueIndexExists();
1841
+ if (indexExists) {
1842
+ return {
1843
+ success: true,
1844
+ alreadyMigrated: true,
1845
+ duplicatesRemoved: 0,
1846
+ message: `Migration already complete. Unique index exists on ${TABLE_SPANS}.`
1847
+ };
1848
+ }
1849
+ const duplicateInfo = await this.checkForDuplicateSpans();
1850
+ if (duplicateInfo.hasDuplicates) {
1851
+ this.logger.info(
1852
+ `Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
1443
1853
  );
1444
- params.push(id, id, withPreviousMessages, withNextMessages);
1854
+ await this.deduplicateSpans();
1855
+ } else {
1856
+ this.logger.info(`No duplicate spans found.`);
1445
1857
  }
1446
- const finalQuery = unionQueries.join(" UNION ALL ") + ' ORDER BY "createdAt" ASC';
1447
- const includedResult = await this.client.execute({ sql: finalQuery, args: params });
1448
- const includedRows = includedResult.rows?.map((row) => this.parseRow(row));
1449
- const seen = /* @__PURE__ */ new Set();
1450
- const dedupedRows = includedRows.filter((row) => {
1451
- if (seen.has(row.id)) return false;
1452
- seen.add(row.id);
1453
- return true;
1454
- });
1455
- return dedupedRows;
1858
+ await this.client.execute(
1859
+ `CREATE UNIQUE INDEX IF NOT EXISTS "mastra_ai_spans_spanid_traceid_idx" ON "${TABLE_SPANS}" ("spanId", "traceId")`
1860
+ );
1861
+ return {
1862
+ success: true,
1863
+ alreadyMigrated: false,
1864
+ duplicatesRemoved: duplicateInfo.duplicateCount,
1865
+ message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added unique index to ${TABLE_SPANS}.` : `Migration complete. Added unique index to ${TABLE_SPANS}.`
1866
+ };
1456
1867
  }
1457
- async listMessagesById({ messageIds }) {
1458
- if (messageIds.length === 0) return { messages: [] };
1868
+ /**
1869
+ * Check migration status for the spans table.
1870
+ * Returns information about whether migration is needed.
1871
+ */
1872
+ async checkSpansMigrationStatus() {
1873
+ const indexExists = await this.spansUniqueIndexExists();
1874
+ if (indexExists) {
1875
+ return {
1876
+ needsMigration: false,
1877
+ hasDuplicates: false,
1878
+ duplicateCount: 0,
1879
+ constraintExists: true,
1880
+ tableName: TABLE_SPANS
1881
+ };
1882
+ }
1883
+ const duplicateInfo = await this.checkForDuplicateSpans();
1884
+ return {
1885
+ needsMigration: true,
1886
+ hasDuplicates: duplicateInfo.hasDuplicates,
1887
+ duplicateCount: duplicateInfo.duplicateCount,
1888
+ constraintExists: false,
1889
+ tableName: TABLE_SPANS
1890
+ };
1891
+ }
1892
+ /**
1893
+ * Deduplicates spans table by removing duplicate (spanId, traceId) combinations.
1894
+ * Keeps the "best" record for each duplicate group based on:
1895
+ * 1. Completed spans (endedAt IS NOT NULL) over incomplete ones
1896
+ * 2. Most recently updated (updatedAt DESC)
1897
+ * 3. Most recently created (createdAt DESC) as tiebreaker
1898
+ */
1899
+ async deduplicateSpans() {
1459
1900
  try {
1460
- const sql = `
1461
- SELECT
1462
- id,
1463
- content,
1464
- role,
1465
- type,
1466
- "createdAt",
1467
- thread_id,
1468
- "resourceId"
1469
- FROM "${TABLE_MESSAGES}"
1470
- WHERE id IN (${messageIds.map(() => "?").join(", ")})
1471
- ORDER BY "createdAt" DESC
1472
- `;
1473
- const result = await this.client.execute({ sql, args: messageIds });
1474
- if (!result.rows) return { messages: [] };
1475
- const list = new MessageList().add(result.rows.map(this.parseRow), "memory");
1476
- return { messages: list.get.all.db() };
1901
+ const duplicateCheck = await this.client.execute(`
1902
+ SELECT COUNT(*) as duplicate_count FROM (
1903
+ SELECT "spanId", "traceId"
1904
+ FROM "${TABLE_SPANS}"
1905
+ GROUP BY "spanId", "traceId"
1906
+ HAVING COUNT(*) > 1
1907
+ )
1908
+ `);
1909
+ const duplicateCount = Number(duplicateCheck.rows?.[0]?.duplicate_count ?? 0);
1910
+ if (duplicateCount === 0) {
1911
+ this.logger.debug(`LibSQLDB: No duplicate spans found, skipping deduplication`);
1912
+ return;
1913
+ }
1914
+ this.logger.warn(`LibSQLDB: Found ${duplicateCount} duplicate (spanId, traceId) combinations, deduplicating...`);
1915
+ const deleteResult = await this.client.execute(`
1916
+ DELETE FROM "${TABLE_SPANS}"
1917
+ WHERE rowid NOT IN (
1918
+ SELECT MIN(best_rowid) FROM (
1919
+ SELECT
1920
+ rowid as best_rowid,
1921
+ "spanId",
1922
+ "traceId",
1923
+ ROW_NUMBER() OVER (
1924
+ PARTITION BY "spanId", "traceId"
1925
+ ORDER BY
1926
+ CASE WHEN "endedAt" IS NOT NULL THEN 0 ELSE 1 END,
1927
+ "updatedAt" DESC,
1928
+ "createdAt" DESC
1929
+ ) as rn
1930
+ FROM "${TABLE_SPANS}"
1931
+ ) ranked
1932
+ WHERE rn = 1
1933
+ GROUP BY "spanId", "traceId"
1934
+ )
1935
+ AND ("spanId", "traceId") IN (
1936
+ SELECT "spanId", "traceId"
1937
+ FROM "${TABLE_SPANS}"
1938
+ GROUP BY "spanId", "traceId"
1939
+ HAVING COUNT(*) > 1
1940
+ )
1941
+ `);
1942
+ const deletedCount = deleteResult.rowsAffected ?? 0;
1943
+ this.logger.warn(`LibSQLDB: Deleted ${deletedCount} duplicate span records`);
1944
+ } catch (error) {
1945
+ this.logger.warn(`LibSQLDB: Failed to deduplicate spans:`, error);
1946
+ }
1947
+ }
1948
+ /**
1949
+ * Gets a default value for a column type (used when adding NOT NULL columns).
1950
+ */
1951
+ getDefaultValue(type) {
1952
+ switch (type) {
1953
+ case "text":
1954
+ case "uuid":
1955
+ return "DEFAULT ''";
1956
+ case "integer":
1957
+ case "bigint":
1958
+ case "float":
1959
+ return "DEFAULT 0";
1960
+ case "boolean":
1961
+ return "DEFAULT 0";
1962
+ case "jsonb":
1963
+ return "DEFAULT '{}'";
1964
+ case "timestamp":
1965
+ return "DEFAULT CURRENT_TIMESTAMP";
1966
+ default:
1967
+ return "DEFAULT ''";
1968
+ }
1969
+ }
1970
+ /**
1971
+ * Alters an existing table to add missing columns.
1972
+ * Used for schema migrations when new columns are added.
1973
+ *
1974
+ * @param args - The alter table arguments
1975
+ * @param args.tableName - The name of the table to alter
1976
+ * @param args.schema - The full schema definition for the table
1977
+ * @param args.ifNotExists - Array of column names to add if they don't exist
1978
+ */
1979
+ async alterTable({
1980
+ tableName,
1981
+ schema,
1982
+ ifNotExists
1983
+ }) {
1984
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
1985
+ try {
1986
+ const tableInfo = await this.client.execute({
1987
+ sql: `PRAGMA table_info("${parsedTableName}")`
1988
+ });
1989
+ const existingColumns = new Set((tableInfo.rows || []).map((row) => row.name?.toLowerCase()));
1990
+ for (const columnName of ifNotExists) {
1991
+ if (!existingColumns.has(columnName.toLowerCase()) && schema[columnName]) {
1992
+ const columnDef = schema[columnName];
1993
+ const sqlType = this.getSqlType(columnDef.type);
1994
+ const defaultValue = this.getDefaultValue(columnDef.type);
1995
+ const alterSql = `ALTER TABLE ${parsedTableName} ADD COLUMN "${columnName}" ${sqlType} ${defaultValue}`;
1996
+ await this.client.execute(alterSql);
1997
+ this.logger.debug(`LibSQLDB: Added column ${columnName} to table ${tableName}`);
1998
+ }
1999
+ }
1477
2000
  } catch (error) {
1478
2001
  throw new MastraError(
1479
2002
  {
1480
- id: createStorageErrorId("LIBSQL", "LIST_MESSAGES_BY_ID", "FAILED"),
2003
+ id: createStorageErrorId("LIBSQL", "ALTER_TABLE", "FAILED"),
1481
2004
  domain: ErrorDomain.STORAGE,
1482
2005
  category: ErrorCategory.THIRD_PARTY,
1483
- details: { messageIds: JSON.stringify(messageIds) }
2006
+ details: { tableName }
1484
2007
  },
1485
2008
  error
1486
2009
  );
1487
2010
  }
1488
2011
  }
1489
- async listMessages(args) {
1490
- const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
1491
- const threadIds = Array.isArray(threadId) ? threadId : [threadId];
1492
- if (threadIds.length === 0 || threadIds.some((id) => !id.trim())) {
1493
- throw new MastraError(
2012
+ /**
2013
+ * Deletes all records from the specified table.
2014
+ * Errors are logged but not thrown.
2015
+ *
2016
+ * @param args - The delete arguments
2017
+ * @param args.tableName - The name of the table to clear
2018
+ */
2019
+ async deleteData({ tableName }) {
2020
+ const parsedTableName = parseSqlIdentifier(tableName, "table name");
2021
+ try {
2022
+ await this.client.execute(`DELETE FROM ${parsedTableName}`);
2023
+ } catch (e) {
2024
+ const mastraError = new MastraError(
1494
2025
  {
1495
- id: createStorageErrorId("LIBSQL", "LIST_MESSAGES", "INVALID_THREAD_ID"),
2026
+ id: createStorageErrorId("LIBSQL", "CLEAR_TABLE", "FAILED"),
1496
2027
  domain: ErrorDomain.STORAGE,
1497
2028
  category: ErrorCategory.THIRD_PARTY,
1498
- details: { threadId: Array.isArray(threadId) ? threadId.join(",") : threadId }
2029
+ details: {
2030
+ tableName
2031
+ }
1499
2032
  },
1500
- new Error("threadId must be a non-empty string or array of non-empty strings")
2033
+ e
1501
2034
  );
2035
+ this.logger?.trackException?.(mastraError);
2036
+ this.logger?.error?.(mastraError.toString());
1502
2037
  }
1503
- if (page < 0) {
2038
+ }
2039
+ };
2040
+
2041
+ // src/storage/domains/agents/index.ts
2042
+ var AgentsLibSQL = class extends AgentsStorage {
2043
+ #db;
2044
+ constructor(config) {
2045
+ super();
2046
+ const client = resolveClient(config);
2047
+ this.#db = new LibSQLDB({ client, maxRetries: config.maxRetries, initialBackoffMs: config.initialBackoffMs });
2048
+ }
2049
+ async init() {
2050
+ await this.#db.createTable({ tableName: TABLE_AGENTS, schema: AGENTS_SCHEMA });
2051
+ }
2052
+ async dangerouslyClearAll() {
2053
+ await this.#db.deleteData({ tableName: TABLE_AGENTS });
2054
+ }
2055
+ parseJson(value, fieldName) {
2056
+ if (!value) return void 0;
2057
+ if (typeof value !== "string") return value;
2058
+ try {
2059
+ return JSON.parse(value);
2060
+ } catch (error) {
2061
+ const details = {
2062
+ value: value.length > 100 ? value.substring(0, 100) + "..." : value
2063
+ };
2064
+ if (fieldName) {
2065
+ details.field = fieldName;
2066
+ }
1504
2067
  throw new MastraError(
1505
2068
  {
1506
- id: createStorageErrorId("LIBSQL", "LIST_MESSAGES", "INVALID_PAGE"),
2069
+ id: createStorageErrorId("LIBSQL", "PARSE_JSON", "INVALID_JSON"),
1507
2070
  domain: ErrorDomain.STORAGE,
1508
- category: ErrorCategory.USER,
1509
- details: { page }
2071
+ category: ErrorCategory.SYSTEM,
2072
+ text: `Failed to parse JSON${fieldName ? ` for field "${fieldName}"` : ""}: ${error instanceof Error ? error.message : "Unknown error"}`,
2073
+ details
1510
2074
  },
1511
- new Error("page must be >= 0")
2075
+ error
1512
2076
  );
1513
2077
  }
1514
- const perPage = normalizePerPage(perPageInput, 40);
1515
- const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2078
+ }
2079
+ parseRow(row) {
2080
+ return {
2081
+ id: row.id,
2082
+ name: row.name,
2083
+ description: row.description,
2084
+ instructions: row.instructions,
2085
+ model: this.parseJson(row.model, "model"),
2086
+ tools: this.parseJson(row.tools, "tools"),
2087
+ defaultOptions: this.parseJson(row.defaultOptions, "defaultOptions"),
2088
+ workflows: this.parseJson(row.workflows, "workflows"),
2089
+ agents: this.parseJson(row.agents, "agents"),
2090
+ inputProcessors: this.parseJson(row.inputProcessors, "inputProcessors"),
2091
+ outputProcessors: this.parseJson(row.outputProcessors, "outputProcessors"),
2092
+ memory: this.parseJson(row.memory, "memory"),
2093
+ scorers: this.parseJson(row.scorers, "scorers"),
2094
+ metadata: this.parseJson(row.metadata, "metadata"),
2095
+ createdAt: new Date(row.createdAt),
2096
+ updatedAt: new Date(row.updatedAt)
2097
+ };
2098
+ }
2099
+ async getAgentById({ id }) {
1516
2100
  try {
1517
- const { field, direction } = this.parseOrderBy(orderBy, "ASC");
1518
- const orderByStatement = `ORDER BY "${field}" ${direction}`;
1519
- const threadPlaceholders = threadIds.map(() => "?").join(", ");
1520
- const conditions = [`thread_id IN (${threadPlaceholders})`];
1521
- const queryParams = [...threadIds];
1522
- if (resourceId) {
1523
- conditions.push(`"resourceId" = ?`);
1524
- queryParams.push(resourceId);
1525
- }
1526
- if (filter?.dateRange?.start) {
1527
- conditions.push(`"createdAt" >= ?`);
1528
- queryParams.push(
1529
- filter.dateRange.start instanceof Date ? filter.dateRange.start.toISOString() : filter.dateRange.start
1530
- );
1531
- }
1532
- if (filter?.dateRange?.end) {
1533
- conditions.push(`"createdAt" <= ?`);
1534
- queryParams.push(
1535
- filter.dateRange.end instanceof Date ? filter.dateRange.end.toISOString() : filter.dateRange.end
1536
- );
1537
- }
1538
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1539
- const countResult = await this.client.execute({
1540
- sql: `SELECT COUNT(*) as count FROM ${TABLE_MESSAGES} ${whereClause}`,
1541
- args: queryParams
1542
- });
1543
- const total = Number(countResult.rows?.[0]?.count ?? 0);
1544
- const limitValue = perPageInput === false ? total : perPage;
1545
- const dataResult = await this.client.execute({
1546
- sql: `SELECT id, content, role, type, "createdAt", "resourceId", "thread_id" FROM ${TABLE_MESSAGES} ${whereClause} ${orderByStatement} LIMIT ? OFFSET ?`,
1547
- args: [...queryParams, limitValue, offset]
2101
+ const result = await this.#db.select({
2102
+ tableName: TABLE_AGENTS,
2103
+ keys: { id }
1548
2104
  });
1549
- const messages = (dataResult.rows || []).map((row) => this.parseRow(row));
1550
- if (total === 0 && messages.length === 0 && (!include || include.length === 0)) {
1551
- return {
1552
- messages: [],
1553
- total: 0,
1554
- page,
1555
- perPage: perPageForResponse,
1556
- hasMore: false
1557
- };
1558
- }
1559
- const messageIds = new Set(messages.map((m) => m.id));
1560
- if (include && include.length > 0) {
1561
- const includeMessages = await this._getIncludedMessages({ include });
1562
- if (includeMessages) {
1563
- for (const includeMsg of includeMessages) {
1564
- if (!messageIds.has(includeMsg.id)) {
1565
- messages.push(includeMsg);
1566
- messageIds.add(includeMsg.id);
1567
- }
1568
- }
1569
- }
1570
- }
1571
- const list = new MessageList().add(messages, "memory");
1572
- let finalMessages = list.get.all.db();
1573
- finalMessages = finalMessages.sort((a, b) => {
1574
- const isDateField = field === "createdAt" || field === "updatedAt";
1575
- const aValue = isDateField ? new Date(a[field]).getTime() : a[field];
1576
- const bValue = isDateField ? new Date(b[field]).getTime() : b[field];
1577
- if (typeof aValue === "number" && typeof bValue === "number") {
1578
- return direction === "ASC" ? aValue - bValue : bValue - aValue;
2105
+ return result ? this.parseRow(result) : null;
2106
+ } catch (error) {
2107
+ throw new MastraError(
2108
+ {
2109
+ id: createStorageErrorId("LIBSQL", "GET_AGENT_BY_ID", "FAILED"),
2110
+ domain: ErrorDomain.STORAGE,
2111
+ category: ErrorCategory.THIRD_PARTY,
2112
+ details: { agentId: id }
2113
+ },
2114
+ error
2115
+ );
2116
+ }
2117
+ }
2118
+ async createAgent({ agent }) {
2119
+ try {
2120
+ const now = /* @__PURE__ */ new Date();
2121
+ await this.#db.insert({
2122
+ tableName: TABLE_AGENTS,
2123
+ record: {
2124
+ id: agent.id,
2125
+ name: agent.name,
2126
+ description: agent.description ?? null,
2127
+ instructions: agent.instructions,
2128
+ model: agent.model,
2129
+ tools: agent.tools ?? null,
2130
+ defaultOptions: agent.defaultOptions ?? null,
2131
+ workflows: agent.workflows ?? null,
2132
+ agents: agent.agents ?? null,
2133
+ inputProcessors: agent.inputProcessors ?? null,
2134
+ outputProcessors: agent.outputProcessors ?? null,
2135
+ memory: agent.memory ?? null,
2136
+ scorers: agent.scorers ?? null,
2137
+ metadata: agent.metadata ?? null,
2138
+ createdAt: now,
2139
+ updatedAt: now
1579
2140
  }
1580
- return direction === "ASC" ? String(aValue).localeCompare(String(bValue)) : String(bValue).localeCompare(String(aValue));
1581
2141
  });
1582
- const threadIdSet = new Set(threadIds);
1583
- const returnedThreadMessageIds = new Set(
1584
- finalMessages.filter((m) => m.threadId && threadIdSet.has(m.threadId)).map((m) => m.id)
1585
- );
1586
- const allThreadMessagesReturned = returnedThreadMessageIds.size >= total;
1587
- const hasMore = perPageInput !== false && !allThreadMessagesReturned && offset + perPage < total;
1588
2142
  return {
1589
- messages: finalMessages,
1590
- total,
1591
- page,
1592
- perPage: perPageForResponse,
1593
- hasMore
2143
+ ...agent,
2144
+ createdAt: now,
2145
+ updatedAt: now
1594
2146
  };
1595
2147
  } catch (error) {
1596
- const mastraError = new MastraError(
2148
+ throw new MastraError(
1597
2149
  {
1598
- id: createStorageErrorId("LIBSQL", "LIST_MESSAGES", "FAILED"),
2150
+ id: createStorageErrorId("LIBSQL", "CREATE_AGENT", "FAILED"),
1599
2151
  domain: ErrorDomain.STORAGE,
1600
2152
  category: ErrorCategory.THIRD_PARTY,
1601
- details: {
1602
- threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
1603
- resourceId: resourceId ?? ""
1604
- }
2153
+ details: { agentId: agent.id }
1605
2154
  },
1606
2155
  error
1607
2156
  );
1608
- this.logger?.error?.(mastraError.toString());
1609
- this.logger?.trackException?.(mastraError);
1610
- return {
1611
- messages: [],
1612
- total: 0,
1613
- page,
1614
- perPage: perPageForResponse,
1615
- hasMore: false
1616
- };
1617
2157
  }
1618
2158
  }
1619
- async saveMessages({ messages }) {
1620
- if (messages.length === 0) return { messages };
2159
+ async updateAgent({ id, ...updates }) {
1621
2160
  try {
1622
- const threadId = messages[0]?.threadId;
1623
- if (!threadId) {
1624
- throw new Error("Thread ID is required");
2161
+ const existingAgent = await this.getAgentById({ id });
2162
+ if (!existingAgent) {
2163
+ throw new MastraError({
2164
+ id: createStorageErrorId("LIBSQL", "UPDATE_AGENT", "NOT_FOUND"),
2165
+ domain: ErrorDomain.STORAGE,
2166
+ category: ErrorCategory.USER,
2167
+ text: `Agent ${id} not found`,
2168
+ details: { agentId: id }
2169
+ });
1625
2170
  }
1626
- const batchStatements = messages.map((message) => {
1627
- const time = message.createdAt || /* @__PURE__ */ new Date();
1628
- if (!message.threadId) {
1629
- throw new Error(
1630
- `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
1631
- );
1632
- }
1633
- if (!message.resourceId) {
1634
- throw new Error(
1635
- `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
1636
- );
1637
- }
1638
- return {
1639
- sql: `INSERT INTO "${TABLE_MESSAGES}" (id, thread_id, content, role, type, "createdAt", "resourceId")
1640
- VALUES (?, ?, ?, ?, ?, ?, ?)
1641
- ON CONFLICT(id) DO UPDATE SET
1642
- thread_id=excluded.thread_id,
1643
- content=excluded.content,
1644
- role=excluded.role,
1645
- type=excluded.type,
1646
- "resourceId"=excluded."resourceId"
1647
- `,
1648
- args: [
1649
- message.id,
1650
- message.threadId,
1651
- typeof message.content === "object" ? JSON.stringify(message.content) : message.content,
1652
- message.role,
1653
- message.type || "v2",
1654
- time instanceof Date ? time.toISOString() : time,
1655
- message.resourceId
1656
- ]
1657
- };
1658
- });
1659
- const now = (/* @__PURE__ */ new Date()).toISOString();
1660
- batchStatements.push({
1661
- sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
1662
- args: [now, threadId]
1663
- });
1664
- const BATCH_SIZE = 50;
1665
- const messageStatements = batchStatements.slice(0, -1);
1666
- const threadUpdateStatement = batchStatements[batchStatements.length - 1];
1667
- for (let i = 0; i < messageStatements.length; i += BATCH_SIZE) {
1668
- const batch = messageStatements.slice(i, i + BATCH_SIZE);
1669
- if (batch.length > 0) {
1670
- await this.client.batch(batch, "write");
1671
- }
2171
+ const data = {
2172
+ updatedAt: /* @__PURE__ */ new Date()
2173
+ };
2174
+ if (updates.name !== void 0) data.name = updates.name;
2175
+ if (updates.description !== void 0) data.description = updates.description;
2176
+ if (updates.instructions !== void 0) data.instructions = updates.instructions;
2177
+ if (updates.model !== void 0) data.model = updates.model;
2178
+ if (updates.tools !== void 0) data.tools = updates.tools;
2179
+ if (updates.defaultOptions !== void 0) data.defaultOptions = updates.defaultOptions;
2180
+ if (updates.workflows !== void 0) data.workflows = updates.workflows;
2181
+ if (updates.agents !== void 0) data.agents = updates.agents;
2182
+ if (updates.inputProcessors !== void 0) data.inputProcessors = updates.inputProcessors;
2183
+ if (updates.outputProcessors !== void 0) data.outputProcessors = updates.outputProcessors;
2184
+ if (updates.memory !== void 0) data.memory = updates.memory;
2185
+ if (updates.scorers !== void 0) data.scorers = updates.scorers;
2186
+ if (updates.metadata !== void 0) {
2187
+ data.metadata = { ...existingAgent.metadata, ...updates.metadata };
1672
2188
  }
1673
- if (threadUpdateStatement) {
1674
- await this.client.execute(threadUpdateStatement);
2189
+ if (Object.keys(data).length > 1) {
2190
+ await this.#db.update({
2191
+ tableName: TABLE_AGENTS,
2192
+ keys: { id },
2193
+ data
2194
+ });
2195
+ }
2196
+ const updatedAgent = await this.getAgentById({ id });
2197
+ if (!updatedAgent) {
2198
+ throw new MastraError({
2199
+ id: createStorageErrorId("LIBSQL", "UPDATE_AGENT", "NOT_FOUND_AFTER_UPDATE"),
2200
+ domain: ErrorDomain.STORAGE,
2201
+ category: ErrorCategory.SYSTEM,
2202
+ text: `Agent ${id} not found after update`,
2203
+ details: { agentId: id }
2204
+ });
1675
2205
  }
1676
- const list = new MessageList().add(messages, "memory");
1677
- return { messages: list.get.all.db() };
2206
+ return updatedAgent;
1678
2207
  } catch (error) {
2208
+ if (error instanceof MastraError) {
2209
+ throw error;
2210
+ }
1679
2211
  throw new MastraError(
1680
2212
  {
1681
- id: createStorageErrorId("LIBSQL", "SAVE_MESSAGES", "FAILED"),
2213
+ id: createStorageErrorId("LIBSQL", "UPDATE_AGENT", "FAILED"),
1682
2214
  domain: ErrorDomain.STORAGE,
1683
- category: ErrorCategory.THIRD_PARTY
2215
+ category: ErrorCategory.THIRD_PARTY,
2216
+ details: { agentId: id }
1684
2217
  },
1685
2218
  error
1686
2219
  );
1687
2220
  }
1688
2221
  }
1689
- async updateMessages({
1690
- messages
1691
- }) {
1692
- if (messages.length === 0) {
1693
- return [];
2222
+ async deleteAgent({ id }) {
2223
+ try {
2224
+ await this.#db.delete({
2225
+ tableName: TABLE_AGENTS,
2226
+ keys: { id }
2227
+ });
2228
+ } catch (error) {
2229
+ throw new MastraError(
2230
+ {
2231
+ id: createStorageErrorId("LIBSQL", "DELETE_AGENT", "FAILED"),
2232
+ domain: ErrorDomain.STORAGE,
2233
+ category: ErrorCategory.THIRD_PARTY,
2234
+ details: { agentId: id }
2235
+ },
2236
+ error
2237
+ );
1694
2238
  }
1695
- const messageIds = messages.map((m) => m.id);
1696
- const placeholders = messageIds.map(() => "?").join(",");
1697
- const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
1698
- const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
1699
- const existingMessages = existingResult.rows.map((row) => this.parseRow(row));
1700
- if (existingMessages.length === 0) {
1701
- return [];
2239
+ }
2240
+ async listAgents(args) {
2241
+ const { page = 0, perPage: perPageInput, orderBy } = args || {};
2242
+ const { field, direction } = this.parseOrderBy(orderBy);
2243
+ if (page < 0) {
2244
+ throw new MastraError(
2245
+ {
2246
+ id: createStorageErrorId("LIBSQL", "LIST_AGENTS", "INVALID_PAGE"),
2247
+ domain: ErrorDomain.STORAGE,
2248
+ category: ErrorCategory.USER,
2249
+ details: { page }
2250
+ },
2251
+ new Error("page must be >= 0")
2252
+ );
1702
2253
  }
1703
- const batchStatements = [];
1704
- const threadIdsToUpdate = /* @__PURE__ */ new Set();
1705
- const columnMapping = {
1706
- threadId: "thread_id"
1707
- };
1708
- for (const existingMessage of existingMessages) {
1709
- const updatePayload = messages.find((m) => m.id === existingMessage.id);
1710
- if (!updatePayload) continue;
1711
- const { id, ...fieldsToUpdate } = updatePayload;
1712
- if (Object.keys(fieldsToUpdate).length === 0) continue;
1713
- threadIdsToUpdate.add(existingMessage.threadId);
1714
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1715
- threadIdsToUpdate.add(updatePayload.threadId);
1716
- }
1717
- const setClauses = [];
1718
- const args = [];
1719
- const updatableFields = { ...fieldsToUpdate };
1720
- if (updatableFields.content) {
1721
- const newContent = {
1722
- ...existingMessage.content,
1723
- ...updatableFields.content,
1724
- // Deep merge metadata if it exists on both
1725
- ...existingMessage.content?.metadata && updatableFields.content.metadata ? {
1726
- metadata: {
1727
- ...existingMessage.content.metadata,
1728
- ...updatableFields.content.metadata
1729
- }
1730
- } : {}
2254
+ const perPage = normalizePerPage(perPageInput, 100);
2255
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2256
+ try {
2257
+ const total = await this.#db.selectTotalCount({ tableName: TABLE_AGENTS });
2258
+ if (total === 0) {
2259
+ return {
2260
+ agents: [],
2261
+ total: 0,
2262
+ page,
2263
+ perPage: perPageForResponse,
2264
+ hasMore: false
1731
2265
  };
1732
- setClauses.push(`${parseSqlIdentifier("content", "column name")} = ?`);
1733
- args.push(JSON.stringify(newContent));
1734
- delete updatableFields.content;
1735
2266
  }
1736
- for (const key in updatableFields) {
1737
- if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1738
- const dbKey = columnMapping[key] || key;
1739
- setClauses.push(`${parseSqlIdentifier(dbKey, "column name")} = ?`);
1740
- let value = updatableFields[key];
1741
- if (typeof value === "object" && value !== null) {
1742
- value = JSON.stringify(value);
1743
- }
1744
- args.push(value);
1745
- }
1746
- }
1747
- if (setClauses.length === 0) continue;
1748
- args.push(id);
1749
- const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(", ")} WHERE id = ?`;
1750
- batchStatements.push({ sql, args });
1751
- }
1752
- if (batchStatements.length === 0) {
1753
- return existingMessages;
2267
+ const limitValue = perPageInput === false ? total : perPage;
2268
+ const rows = await this.#db.selectMany({
2269
+ tableName: TABLE_AGENTS,
2270
+ orderBy: `"${field}" ${direction}`,
2271
+ limit: limitValue,
2272
+ offset
2273
+ });
2274
+ const agents = rows.map((row) => this.parseRow(row));
2275
+ return {
2276
+ agents,
2277
+ total,
2278
+ page,
2279
+ perPage: perPageForResponse,
2280
+ hasMore: perPageInput === false ? false : offset + perPage < total
2281
+ };
2282
+ } catch (error) {
2283
+ throw new MastraError(
2284
+ {
2285
+ id: createStorageErrorId("LIBSQL", "LIST_AGENTS", "FAILED"),
2286
+ domain: ErrorDomain.STORAGE,
2287
+ category: ErrorCategory.THIRD_PARTY
2288
+ },
2289
+ error
2290
+ );
1754
2291
  }
1755
- const now = (/* @__PURE__ */ new Date()).toISOString();
1756
- for (const threadId of threadIdsToUpdate) {
1757
- if (threadId) {
1758
- batchStatements.push({
1759
- sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
1760
- args: [now, threadId]
1761
- });
1762
- }
2292
+ }
2293
+ };
2294
+ var MemoryLibSQL = class extends MemoryStorage {
2295
+ #client;
2296
+ #db;
2297
+ constructor(config) {
2298
+ super();
2299
+ const client = resolveClient(config);
2300
+ this.#client = client;
2301
+ this.#db = new LibSQLDB({ client, maxRetries: config.maxRetries, initialBackoffMs: config.initialBackoffMs });
2302
+ }
2303
+ async init() {
2304
+ await this.#db.createTable({ tableName: TABLE_THREADS, schema: TABLE_SCHEMAS[TABLE_THREADS] });
2305
+ await this.#db.createTable({ tableName: TABLE_MESSAGES, schema: TABLE_SCHEMAS[TABLE_MESSAGES] });
2306
+ await this.#db.createTable({ tableName: TABLE_RESOURCES, schema: TABLE_SCHEMAS[TABLE_RESOURCES] });
2307
+ await this.#db.alterTable({
2308
+ tableName: TABLE_MESSAGES,
2309
+ schema: TABLE_SCHEMAS[TABLE_MESSAGES],
2310
+ ifNotExists: ["resourceId"]
2311
+ });
2312
+ }
2313
+ async dangerouslyClearAll() {
2314
+ await this.#db.deleteData({ tableName: TABLE_MESSAGES });
2315
+ await this.#db.deleteData({ tableName: TABLE_THREADS });
2316
+ await this.#db.deleteData({ tableName: TABLE_RESOURCES });
2317
+ }
2318
+ parseRow(row) {
2319
+ let content = row.content;
2320
+ try {
2321
+ content = JSON.parse(row.content);
2322
+ } catch {
1763
2323
  }
1764
- await this.client.batch(batchStatements, "write");
1765
- const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
1766
- return updatedResult.rows.map((row) => this.parseRow(row));
2324
+ const result = {
2325
+ id: row.id,
2326
+ content,
2327
+ role: row.role,
2328
+ createdAt: new Date(row.createdAt),
2329
+ threadId: row.thread_id,
2330
+ resourceId: row.resourceId
2331
+ };
2332
+ if (row.type && row.type !== `v2`) result.type = row.type;
2333
+ return result;
1767
2334
  }
1768
- async deleteMessages(messageIds) {
1769
- if (!messageIds || messageIds.length === 0) {
1770
- return;
2335
+ async _getIncludedMessages({ include }) {
2336
+ if (!include || include.length === 0) return null;
2337
+ const unionQueries = [];
2338
+ const params = [];
2339
+ for (const inc of include) {
2340
+ const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
2341
+ unionQueries.push(
2342
+ `
2343
+ SELECT * FROM (
2344
+ WITH target_thread AS (
2345
+ SELECT thread_id FROM "${TABLE_MESSAGES}" WHERE id = ?
2346
+ ),
2347
+ numbered_messages AS (
2348
+ SELECT
2349
+ id, content, role, type, "createdAt", thread_id, "resourceId",
2350
+ ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
2351
+ FROM "${TABLE_MESSAGES}"
2352
+ WHERE thread_id = (SELECT thread_id FROM target_thread)
2353
+ ),
2354
+ target_positions AS (
2355
+ SELECT row_num as target_pos
2356
+ FROM numbered_messages
2357
+ WHERE id = ?
2358
+ )
2359
+ SELECT DISTINCT m.*
2360
+ FROM numbered_messages m
2361
+ CROSS JOIN target_positions t
2362
+ WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
2363
+ )
2364
+ `
2365
+ // Keep ASC for final sorting after fetching context
2366
+ );
2367
+ params.push(id, id, withPreviousMessages, withNextMessages);
1771
2368
  }
2369
+ const finalQuery = unionQueries.join(" UNION ALL ") + ' ORDER BY "createdAt" ASC';
2370
+ const includedResult = await this.#client.execute({ sql: finalQuery, args: params });
2371
+ const includedRows = includedResult.rows?.map((row) => this.parseRow(row));
2372
+ const seen = /* @__PURE__ */ new Set();
2373
+ const dedupedRows = includedRows.filter((row) => {
2374
+ if (seen.has(row.id)) return false;
2375
+ seen.add(row.id);
2376
+ return true;
2377
+ });
2378
+ return dedupedRows;
2379
+ }
2380
+ async listMessagesById({ messageIds }) {
2381
+ if (messageIds.length === 0) return { messages: [] };
1772
2382
  try {
1773
- const BATCH_SIZE = 100;
1774
- const threadIds = /* @__PURE__ */ new Set();
1775
- const tx = await this.client.transaction("write");
1776
- try {
1777
- for (let i = 0; i < messageIds.length; i += BATCH_SIZE) {
1778
- const batch = messageIds.slice(i, i + BATCH_SIZE);
1779
- const placeholders = batch.map(() => "?").join(",");
1780
- const result = await tx.execute({
1781
- sql: `SELECT DISTINCT thread_id FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
1782
- args: batch
1783
- });
1784
- result.rows?.forEach((row) => {
1785
- if (row.thread_id) threadIds.add(row.thread_id);
1786
- });
1787
- await tx.execute({
1788
- sql: `DELETE FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
1789
- args: batch
1790
- });
1791
- }
1792
- if (threadIds.size > 0) {
1793
- const now = (/* @__PURE__ */ new Date()).toISOString();
1794
- for (const threadId of threadIds) {
1795
- await tx.execute({
1796
- sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
1797
- args: [now, threadId]
1798
- });
1799
- }
1800
- }
1801
- await tx.commit();
1802
- } catch (error) {
1803
- await tx.rollback();
1804
- throw error;
1805
- }
2383
+ const sql = `
2384
+ SELECT
2385
+ id,
2386
+ content,
2387
+ role,
2388
+ type,
2389
+ "createdAt",
2390
+ thread_id,
2391
+ "resourceId"
2392
+ FROM "${TABLE_MESSAGES}"
2393
+ WHERE id IN (${messageIds.map(() => "?").join(", ")})
2394
+ ORDER BY "createdAt" DESC
2395
+ `;
2396
+ const result = await this.#client.execute({ sql, args: messageIds });
2397
+ if (!result.rows) return { messages: [] };
2398
+ const list = new MessageList().add(result.rows.map(this.parseRow), "memory");
2399
+ return { messages: list.get.all.db() };
1806
2400
  } catch (error) {
1807
2401
  throw new MastraError(
1808
2402
  {
1809
- id: createStorageErrorId("LIBSQL", "DELETE_MESSAGES", "FAILED"),
2403
+ id: createStorageErrorId("LIBSQL", "LIST_MESSAGES_BY_ID", "FAILED"),
1810
2404
  domain: ErrorDomain.STORAGE,
1811
2405
  category: ErrorCategory.THIRD_PARTY,
1812
- details: { messageIds: messageIds.join(", ") }
2406
+ details: { messageIds: JSON.stringify(messageIds) }
1813
2407
  },
1814
2408
  error
1815
2409
  );
1816
2410
  }
1817
2411
  }
1818
- async getResourceById({ resourceId }) {
1819
- const result = await this.operations.load({
1820
- tableName: TABLE_RESOURCES,
1821
- keys: { id: resourceId }
1822
- });
1823
- if (!result) {
1824
- return null;
1825
- }
1826
- return {
1827
- ...result,
1828
- // Ensure workingMemory is always returned as a string, even if auto-parsed as JSON
1829
- workingMemory: result.workingMemory && typeof result.workingMemory === "object" ? JSON.stringify(result.workingMemory) : result.workingMemory,
1830
- metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata,
1831
- createdAt: new Date(result.createdAt),
1832
- updatedAt: new Date(result.updatedAt)
1833
- };
1834
- }
1835
- async saveResource({ resource }) {
1836
- await this.operations.insert({
1837
- tableName: TABLE_RESOURCES,
1838
- record: {
1839
- ...resource,
1840
- metadata: JSON.stringify(resource.metadata)
1841
- }
1842
- });
1843
- return resource;
1844
- }
1845
- async updateResource({
1846
- resourceId,
1847
- workingMemory,
1848
- metadata
1849
- }) {
1850
- const existingResource = await this.getResourceById({ resourceId });
1851
- if (!existingResource) {
1852
- const newResource = {
1853
- id: resourceId,
1854
- workingMemory,
1855
- metadata: metadata || {},
1856
- createdAt: /* @__PURE__ */ new Date(),
1857
- updatedAt: /* @__PURE__ */ new Date()
1858
- };
1859
- return this.saveResource({ resource: newResource });
1860
- }
1861
- const updatedResource = {
1862
- ...existingResource,
1863
- workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1864
- metadata: {
1865
- ...existingResource.metadata,
1866
- ...metadata
1867
- },
1868
- updatedAt: /* @__PURE__ */ new Date()
1869
- };
1870
- const updates = [];
1871
- const values = [];
1872
- if (workingMemory !== void 0) {
1873
- updates.push("workingMemory = ?");
1874
- values.push(workingMemory);
1875
- }
1876
- if (metadata) {
1877
- updates.push("metadata = ?");
1878
- values.push(JSON.stringify(updatedResource.metadata));
1879
- }
1880
- updates.push("updatedAt = ?");
1881
- values.push(updatedResource.updatedAt.toISOString());
1882
- values.push(resourceId);
1883
- await this.client.execute({
1884
- sql: `UPDATE ${TABLE_RESOURCES} SET ${updates.join(", ")} WHERE id = ?`,
1885
- args: values
1886
- });
1887
- return updatedResource;
1888
- }
1889
- async getThreadById({ threadId }) {
1890
- try {
1891
- const result = await this.operations.load({
1892
- tableName: TABLE_THREADS,
1893
- keys: { id: threadId }
1894
- });
1895
- if (!result) {
1896
- return null;
1897
- }
1898
- return {
1899
- ...result,
1900
- metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata,
1901
- createdAt: new Date(result.createdAt),
1902
- updatedAt: new Date(result.updatedAt)
1903
- };
1904
- } catch (error) {
2412
+ async listMessages(args) {
2413
+ const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
2414
+ const threadIds = Array.isArray(threadId) ? threadId : [threadId];
2415
+ if (threadIds.length === 0 || threadIds.some((id) => !id.trim())) {
1905
2416
  throw new MastraError(
1906
2417
  {
1907
- id: createStorageErrorId("LIBSQL", "GET_THREAD_BY_ID", "FAILED"),
2418
+ id: createStorageErrorId("LIBSQL", "LIST_MESSAGES", "INVALID_THREAD_ID"),
1908
2419
  domain: ErrorDomain.STORAGE,
1909
2420
  category: ErrorCategory.THIRD_PARTY,
1910
- details: { threadId }
2421
+ details: { threadId: Array.isArray(threadId) ? threadId.join(",") : threadId }
1911
2422
  },
1912
- error
2423
+ new Error("threadId must be a non-empty string or array of non-empty strings")
1913
2424
  );
1914
2425
  }
1915
- }
1916
- async listThreadsByResourceId(args) {
1917
- const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
1918
2426
  if (page < 0) {
1919
2427
  throw new MastraError(
1920
2428
  {
1921
- id: createStorageErrorId("LIBSQL", "LIST_THREADS_BY_RESOURCE_ID", "INVALID_PAGE"),
2429
+ id: createStorageErrorId("LIBSQL", "LIST_MESSAGES", "INVALID_PAGE"),
1922
2430
  domain: ErrorDomain.STORAGE,
1923
2431
  category: ErrorCategory.USER,
1924
2432
  details: { page }
@@ -1926,63 +2434,106 @@ var MemoryLibSQL = class extends MemoryStorage {
1926
2434
  new Error("page must be >= 0")
1927
2435
  );
1928
2436
  }
1929
- const perPage = normalizePerPage(perPageInput, 100);
2437
+ const perPage = normalizePerPage(perPageInput, 40);
1930
2438
  const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1931
- const { field, direction } = this.parseOrderBy(orderBy);
1932
2439
  try {
1933
- const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
1934
- const queryParams = [resourceId];
1935
- const mapRowToStorageThreadType = (row) => ({
1936
- id: row.id,
1937
- resourceId: row.resourceId,
1938
- title: row.title,
1939
- createdAt: new Date(row.createdAt),
1940
- // Convert string to Date
1941
- updatedAt: new Date(row.updatedAt),
1942
- // Convert string to Date
1943
- metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata
1944
- });
1945
- const countResult = await this.client.execute({
1946
- sql: `SELECT COUNT(*) as count ${baseQuery}`,
2440
+ const { field, direction } = this.parseOrderBy(orderBy, "ASC");
2441
+ const orderByStatement = `ORDER BY "${field}" ${direction}`;
2442
+ const threadPlaceholders = threadIds.map(() => "?").join(", ");
2443
+ const conditions = [`thread_id IN (${threadPlaceholders})`];
2444
+ const queryParams = [...threadIds];
2445
+ if (resourceId) {
2446
+ conditions.push(`"resourceId" = ?`);
2447
+ queryParams.push(resourceId);
2448
+ }
2449
+ if (filter?.dateRange?.start) {
2450
+ const startOp = filter.dateRange.startExclusive ? ">" : ">=";
2451
+ conditions.push(`"createdAt" ${startOp} ?`);
2452
+ queryParams.push(
2453
+ filter.dateRange.start instanceof Date ? filter.dateRange.start.toISOString() : filter.dateRange.start
2454
+ );
2455
+ }
2456
+ if (filter?.dateRange?.end) {
2457
+ const endOp = filter.dateRange.endExclusive ? "<" : "<=";
2458
+ conditions.push(`"createdAt" ${endOp} ?`);
2459
+ queryParams.push(
2460
+ filter.dateRange.end instanceof Date ? filter.dateRange.end.toISOString() : filter.dateRange.end
2461
+ );
2462
+ }
2463
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2464
+ const countResult = await this.#client.execute({
2465
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_MESSAGES} ${whereClause}`,
1947
2466
  args: queryParams
1948
2467
  });
1949
2468
  const total = Number(countResult.rows?.[0]?.count ?? 0);
1950
- if (total === 0) {
2469
+ const limitValue = perPageInput === false ? total : perPage;
2470
+ const dataResult = await this.#client.execute({
2471
+ sql: `SELECT id, content, role, type, "createdAt", "resourceId", "thread_id" FROM ${TABLE_MESSAGES} ${whereClause} ${orderByStatement} LIMIT ? OFFSET ?`,
2472
+ args: [...queryParams, limitValue, offset]
2473
+ });
2474
+ const messages = (dataResult.rows || []).map((row) => this.parseRow(row));
2475
+ if (total === 0 && messages.length === 0 && (!include || include.length === 0)) {
1951
2476
  return {
1952
- threads: [],
2477
+ messages: [],
1953
2478
  total: 0,
1954
2479
  page,
1955
2480
  perPage: perPageForResponse,
1956
2481
  hasMore: false
1957
2482
  };
1958
2483
  }
1959
- const limitValue = perPageInput === false ? total : perPage;
1960
- const dataResult = await this.client.execute({
1961
- sql: `SELECT * ${baseQuery} ORDER BY "${field}" ${direction} LIMIT ? OFFSET ?`,
1962
- args: [...queryParams, limitValue, offset]
2484
+ const messageIds = new Set(messages.map((m) => m.id));
2485
+ if (include && include.length > 0) {
2486
+ const includeMessages = await this._getIncludedMessages({ include });
2487
+ if (includeMessages) {
2488
+ for (const includeMsg of includeMessages) {
2489
+ if (!messageIds.has(includeMsg.id)) {
2490
+ messages.push(includeMsg);
2491
+ messageIds.add(includeMsg.id);
2492
+ }
2493
+ }
2494
+ }
2495
+ }
2496
+ const list = new MessageList().add(messages, "memory");
2497
+ let finalMessages = list.get.all.db();
2498
+ finalMessages = finalMessages.sort((a, b) => {
2499
+ const isDateField = field === "createdAt" || field === "updatedAt";
2500
+ const aValue = isDateField ? new Date(a[field]).getTime() : a[field];
2501
+ const bValue = isDateField ? new Date(b[field]).getTime() : b[field];
2502
+ if (typeof aValue === "number" && typeof bValue === "number") {
2503
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
2504
+ }
2505
+ return direction === "ASC" ? String(aValue).localeCompare(String(bValue)) : String(bValue).localeCompare(String(aValue));
1963
2506
  });
1964
- const threads = (dataResult.rows || []).map(mapRowToStorageThreadType);
2507
+ const threadIdSet = new Set(threadIds);
2508
+ const returnedThreadMessageIds = new Set(
2509
+ finalMessages.filter((m) => m.threadId && threadIdSet.has(m.threadId)).map((m) => m.id)
2510
+ );
2511
+ const allThreadMessagesReturned = returnedThreadMessageIds.size >= total;
2512
+ const hasMore = perPageInput !== false && !allThreadMessagesReturned && offset + perPage < total;
1965
2513
  return {
1966
- threads,
2514
+ messages: finalMessages,
1967
2515
  total,
1968
2516
  page,
1969
2517
  perPage: perPageForResponse,
1970
- hasMore: perPageInput === false ? false : offset + perPage < total
2518
+ hasMore
1971
2519
  };
1972
2520
  } catch (error) {
1973
2521
  const mastraError = new MastraError(
1974
2522
  {
1975
- id: createStorageErrorId("LIBSQL", "LIST_THREADS_BY_RESOURCE_ID", "FAILED"),
2523
+ id: createStorageErrorId("LIBSQL", "LIST_MESSAGES", "FAILED"),
1976
2524
  domain: ErrorDomain.STORAGE,
1977
2525
  category: ErrorCategory.THIRD_PARTY,
1978
- details: { resourceId }
2526
+ details: {
2527
+ threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
2528
+ resourceId: resourceId ?? ""
2529
+ }
1979
2530
  },
1980
2531
  error
1981
2532
  );
1982
- this.logger?.trackException?.(mastraError);
1983
2533
  this.logger?.error?.(mastraError.toString());
2534
+ this.logger?.trackException?.(mastraError);
1984
2535
  return {
1985
- threads: [],
2536
+ messages: [],
1986
2537
  total: 0,
1987
2538
  page,
1988
2539
  perPage: perPageForResponse,
@@ -1990,909 +2541,1132 @@ var MemoryLibSQL = class extends MemoryStorage {
1990
2541
  };
1991
2542
  }
1992
2543
  }
1993
- async saveThread({ thread }) {
2544
+ async saveMessages({ messages }) {
2545
+ if (messages.length === 0) return { messages };
1994
2546
  try {
1995
- await this.operations.insert({
1996
- tableName: TABLE_THREADS,
1997
- record: {
1998
- ...thread,
1999
- metadata: JSON.stringify(thread.metadata)
2547
+ const threadId = messages[0]?.threadId;
2548
+ if (!threadId) {
2549
+ throw new Error("Thread ID is required");
2550
+ }
2551
+ const batchStatements = messages.map((message) => {
2552
+ const time = message.createdAt || /* @__PURE__ */ new Date();
2553
+ if (!message.threadId) {
2554
+ throw new Error(
2555
+ `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
2556
+ );
2000
2557
  }
2001
- });
2002
- return thread;
2003
- } catch (error) {
2004
- const mastraError = new MastraError(
2005
- {
2006
- id: createStorageErrorId("LIBSQL", "SAVE_THREAD", "FAILED"),
2007
- domain: ErrorDomain.STORAGE,
2008
- category: ErrorCategory.THIRD_PARTY,
2009
- details: { threadId: thread.id }
2010
- },
2011
- error
2012
- );
2013
- this.logger?.trackException?.(mastraError);
2014
- this.logger?.error?.(mastraError.toString());
2015
- throw mastraError;
2016
- }
2017
- }
2018
- async updateThread({
2019
- id,
2020
- title,
2021
- metadata
2022
- }) {
2023
- const thread = await this.getThreadById({ threadId: id });
2024
- if (!thread) {
2025
- throw new MastraError({
2026
- id: createStorageErrorId("LIBSQL", "UPDATE_THREAD", "NOT_FOUND"),
2027
- domain: ErrorDomain.STORAGE,
2028
- category: ErrorCategory.USER,
2029
- text: `Thread ${id} not found`,
2030
- details: {
2031
- status: 404,
2032
- threadId: id
2558
+ if (!message.resourceId) {
2559
+ throw new Error(
2560
+ `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
2561
+ );
2033
2562
  }
2563
+ return {
2564
+ sql: `INSERT INTO "${TABLE_MESSAGES}" (id, thread_id, content, role, type, "createdAt", "resourceId")
2565
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2566
+ ON CONFLICT(id) DO UPDATE SET
2567
+ thread_id=excluded.thread_id,
2568
+ content=excluded.content,
2569
+ role=excluded.role,
2570
+ type=excluded.type,
2571
+ "resourceId"=excluded."resourceId"
2572
+ `,
2573
+ args: [
2574
+ message.id,
2575
+ message.threadId,
2576
+ typeof message.content === "object" ? JSON.stringify(message.content) : message.content,
2577
+ message.role,
2578
+ message.type || "v2",
2579
+ time instanceof Date ? time.toISOString() : time,
2580
+ message.resourceId
2581
+ ]
2582
+ };
2034
2583
  });
2035
- }
2036
- const updatedThread = {
2037
- ...thread,
2038
- title,
2039
- metadata: {
2040
- ...thread.metadata,
2041
- ...metadata
2042
- }
2043
- };
2044
- try {
2045
- await this.client.execute({
2046
- sql: `UPDATE ${TABLE_THREADS} SET title = ?, metadata = ? WHERE id = ?`,
2047
- args: [title, JSON.stringify(updatedThread.metadata), id]
2584
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2585
+ batchStatements.push({
2586
+ sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
2587
+ args: [now, threadId]
2048
2588
  });
2049
- return updatedThread;
2589
+ const BATCH_SIZE = 50;
2590
+ const messageStatements = batchStatements.slice(0, -1);
2591
+ const threadUpdateStatement = batchStatements[batchStatements.length - 1];
2592
+ for (let i = 0; i < messageStatements.length; i += BATCH_SIZE) {
2593
+ const batch = messageStatements.slice(i, i + BATCH_SIZE);
2594
+ if (batch.length > 0) {
2595
+ await this.#client.batch(batch, "write");
2596
+ }
2597
+ }
2598
+ if (threadUpdateStatement) {
2599
+ await this.#client.execute(threadUpdateStatement);
2600
+ }
2601
+ const list = new MessageList().add(messages, "memory");
2602
+ return { messages: list.get.all.db() };
2050
2603
  } catch (error) {
2051
2604
  throw new MastraError(
2052
2605
  {
2053
- id: createStorageErrorId("LIBSQL", "UPDATE_THREAD", "FAILED"),
2606
+ id: createStorageErrorId("LIBSQL", "SAVE_MESSAGES", "FAILED"),
2054
2607
  domain: ErrorDomain.STORAGE,
2055
- category: ErrorCategory.THIRD_PARTY,
2056
- text: `Failed to update thread ${id}`,
2057
- details: { threadId: id }
2608
+ category: ErrorCategory.THIRD_PARTY
2058
2609
  },
2059
2610
  error
2060
2611
  );
2061
2612
  }
2062
2613
  }
2063
- async deleteThread({ threadId }) {
2064
- try {
2065
- await this.client.execute({
2066
- sql: `DELETE FROM ${TABLE_MESSAGES} WHERE thread_id = ?`,
2067
- args: [threadId]
2068
- });
2069
- await this.client.execute({
2070
- sql: `DELETE FROM ${TABLE_THREADS} WHERE id = ?`,
2071
- args: [threadId]
2072
- });
2073
- } catch (error) {
2074
- throw new MastraError(
2075
- {
2076
- id: createStorageErrorId("LIBSQL", "DELETE_THREAD", "FAILED"),
2077
- domain: ErrorDomain.STORAGE,
2078
- category: ErrorCategory.THIRD_PARTY,
2079
- details: { threadId }
2080
- },
2081
- error
2082
- );
2614
+ async updateMessages({
2615
+ messages
2616
+ }) {
2617
+ if (messages.length === 0) {
2618
+ return [];
2083
2619
  }
2084
- }
2085
- };
2086
- function createExecuteWriteOperationWithRetry({
2087
- logger,
2088
- maxRetries,
2089
- initialBackoffMs
2090
- }) {
2091
- return async function executeWriteOperationWithRetry(operationFn, operationDescription) {
2092
- let retries = 0;
2093
- while (true) {
2094
- try {
2095
- return await operationFn();
2096
- } catch (error) {
2097
- if (error.message && (error.message.includes("SQLITE_BUSY") || error.message.includes("database is locked")) && retries < maxRetries) {
2098
- retries++;
2099
- const backoffTime = initialBackoffMs * Math.pow(2, retries - 1);
2100
- logger.warn(
2101
- `LibSQLStore: Encountered SQLITE_BUSY during ${operationDescription}. Retrying (${retries}/${maxRetries}) in ${backoffTime}ms...`
2102
- );
2103
- await new Promise((resolve) => setTimeout(resolve, backoffTime));
2104
- } else {
2105
- logger.error(`LibSQLStore: Error during ${operationDescription} after ${retries} retries: ${error}`);
2106
- throw error;
2620
+ const messageIds = messages.map((m) => m.id);
2621
+ const placeholders = messageIds.map(() => "?").join(",");
2622
+ const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
2623
+ const existingResult = await this.#client.execute({ sql: selectSql, args: messageIds });
2624
+ const existingMessages = existingResult.rows.map((row) => this.parseRow(row));
2625
+ if (existingMessages.length === 0) {
2626
+ return [];
2627
+ }
2628
+ const batchStatements = [];
2629
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
2630
+ const columnMapping = {
2631
+ threadId: "thread_id"
2632
+ };
2633
+ for (const existingMessage of existingMessages) {
2634
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
2635
+ if (!updatePayload) continue;
2636
+ const { id, ...fieldsToUpdate } = updatePayload;
2637
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
2638
+ threadIdsToUpdate.add(existingMessage.threadId);
2639
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
2640
+ threadIdsToUpdate.add(updatePayload.threadId);
2641
+ }
2642
+ const setClauses = [];
2643
+ const args = [];
2644
+ const updatableFields = { ...fieldsToUpdate };
2645
+ if (updatableFields.content) {
2646
+ const newContent = {
2647
+ ...existingMessage.content,
2648
+ ...updatableFields.content,
2649
+ // Deep merge metadata if it exists on both
2650
+ ...existingMessage.content?.metadata && updatableFields.content.metadata ? {
2651
+ metadata: {
2652
+ ...existingMessage.content.metadata,
2653
+ ...updatableFields.content.metadata
2654
+ }
2655
+ } : {}
2656
+ };
2657
+ setClauses.push(`${parseSqlIdentifier("content", "column name")} = ?`);
2658
+ args.push(JSON.stringify(newContent));
2659
+ delete updatableFields.content;
2660
+ }
2661
+ for (const key in updatableFields) {
2662
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
2663
+ const dbKey = columnMapping[key] || key;
2664
+ setClauses.push(`${parseSqlIdentifier(dbKey, "column name")} = ?`);
2665
+ let value = updatableFields[key];
2666
+ if (typeof value === "object" && value !== null) {
2667
+ value = JSON.stringify(value);
2668
+ }
2669
+ args.push(value);
2107
2670
  }
2108
2671
  }
2672
+ if (setClauses.length === 0) continue;
2673
+ args.push(id);
2674
+ const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(", ")} WHERE id = ?`;
2675
+ batchStatements.push({ sql, args });
2109
2676
  }
2110
- };
2111
- }
2112
- function prepareStatement({ tableName, record }) {
2113
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2114
- const columns = Object.keys(record).map((col) => parseSqlIdentifier(col, "column name"));
2115
- const values = Object.values(record).map((v) => {
2116
- if (typeof v === `undefined` || v === null) {
2117
- return null;
2118
- }
2119
- if (v instanceof Date) {
2120
- return v.toISOString();
2121
- }
2122
- return typeof v === "object" ? JSON.stringify(v) : v;
2123
- });
2124
- const placeholders = values.map(() => "?").join(", ");
2125
- return {
2126
- sql: `INSERT OR REPLACE INTO ${parsedTableName} (${columns.join(", ")}) VALUES (${placeholders})`,
2127
- args: values
2128
- };
2129
- }
2130
- function prepareUpdateStatement({
2131
- tableName,
2132
- updates,
2133
- keys
2134
- }) {
2135
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2136
- const schema = TABLE_SCHEMAS[tableName];
2137
- const updateColumns = Object.keys(updates).map((col) => parseSqlIdentifier(col, "column name"));
2138
- const updateValues = Object.values(updates).map(transformToSqlValue);
2139
- const setClause = updateColumns.map((col) => `${col} = ?`).join(", ");
2140
- const whereClause = prepareWhereClause(keys, schema);
2141
- return {
2142
- sql: `UPDATE ${parsedTableName} SET ${setClause}${whereClause.sql}`,
2143
- args: [...updateValues, ...whereClause.args]
2144
- };
2145
- }
2146
- function transformToSqlValue(value) {
2147
- if (typeof value === "undefined" || value === null) {
2148
- return null;
2149
- }
2150
- if (value instanceof Date) {
2151
- return value.toISOString();
2152
- }
2153
- return typeof value === "object" ? JSON.stringify(value) : value;
2154
- }
2155
- function prepareDeleteStatement({ tableName, keys }) {
2156
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2157
- const whereClause = prepareWhereClause(keys, TABLE_SCHEMAS[tableName]);
2158
- return {
2159
- sql: `DELETE FROM ${parsedTableName}${whereClause.sql}`,
2160
- args: whereClause.args
2161
- };
2162
- }
2163
- function prepareWhereClause(filters, schema) {
2164
- const conditions = [];
2165
- const args = [];
2166
- for (const [columnName, filterValue] of Object.entries(filters)) {
2167
- const column = schema[columnName];
2168
- if (!column) {
2169
- throw new Error(`Unknown column: ${columnName}`);
2170
- }
2171
- const parsedColumn = parseSqlIdentifier(columnName, "column name");
2172
- const result = buildCondition2(parsedColumn, filterValue);
2173
- conditions.push(result.condition);
2174
- args.push(...result.args);
2175
- }
2176
- return {
2177
- sql: conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "",
2178
- args
2179
- };
2180
- }
2181
- function buildCondition2(columnName, filterValue) {
2182
- if (filterValue === null) {
2183
- return { condition: `${columnName} IS NULL`, args: [] };
2184
- }
2185
- if (typeof filterValue === "object" && filterValue !== null && ("startAt" in filterValue || "endAt" in filterValue)) {
2186
- return buildDateRangeCondition(columnName, filterValue);
2187
- }
2188
- return {
2189
- condition: `${columnName} = ?`,
2190
- args: [transformToSqlValue(filterValue)]
2191
- };
2192
- }
2193
- function buildDateRangeCondition(columnName, range) {
2194
- const conditions = [];
2195
- const args = [];
2196
- if (range.startAt !== void 0) {
2197
- conditions.push(`${columnName} >= ?`);
2198
- args.push(transformToSqlValue(range.startAt));
2199
- }
2200
- if (range.endAt !== void 0) {
2201
- conditions.push(`${columnName} <= ?`);
2202
- args.push(transformToSqlValue(range.endAt));
2203
- }
2204
- if (conditions.length === 0) {
2205
- throw new Error("Date range must specify at least startAt or endAt");
2206
- }
2207
- return {
2208
- condition: conditions.join(" AND "),
2209
- args
2210
- };
2211
- }
2212
- function buildDateRangeFilter(dateRange, columnName = "createdAt") {
2213
- if (!dateRange?.start && !dateRange?.end) {
2214
- return {};
2215
- }
2216
- const filter = {};
2217
- if (dateRange.start) {
2218
- filter.startAt = new Date(dateRange.start).toISOString();
2219
- }
2220
- if (dateRange.end) {
2221
- filter.endAt = new Date(dateRange.end).toISOString();
2222
- }
2223
- return { [columnName]: filter };
2224
- }
2225
- function transformFromSqlRow({
2226
- tableName,
2227
- sqlRow
2228
- }) {
2229
- const result = {};
2230
- const jsonColumns = new Set(
2231
- Object.keys(TABLE_SCHEMAS[tableName]).filter((key) => TABLE_SCHEMAS[tableName][key].type === "jsonb").map((key) => key)
2232
- );
2233
- const dateColumns = new Set(
2234
- Object.keys(TABLE_SCHEMAS[tableName]).filter((key) => TABLE_SCHEMAS[tableName][key].type === "timestamp").map((key) => key)
2235
- );
2236
- for (const [key, value] of Object.entries(sqlRow)) {
2237
- if (value === null || value === void 0) {
2238
- result[key] = value;
2239
- continue;
2240
- }
2241
- if (dateColumns.has(key) && typeof value === "string") {
2242
- result[key] = new Date(value);
2243
- continue;
2677
+ if (batchStatements.length === 0) {
2678
+ return existingMessages;
2244
2679
  }
2245
- if (jsonColumns.has(key) && typeof value === "string") {
2246
- result[key] = safelyParseJSON(value);
2247
- continue;
2680
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2681
+ for (const threadId of threadIdsToUpdate) {
2682
+ if (threadId) {
2683
+ batchStatements.push({
2684
+ sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
2685
+ args: [now, threadId]
2686
+ });
2687
+ }
2248
2688
  }
2249
- result[key] = value;
2250
- }
2251
- return result;
2252
- }
2253
-
2254
- // src/storage/domains/observability/index.ts
2255
- var ObservabilityLibSQL = class extends ObservabilityStorage {
2256
- operations;
2257
- constructor({ operations }) {
2258
- super();
2259
- this.operations = operations;
2689
+ await this.#client.batch(batchStatements, "write");
2690
+ const updatedResult = await this.#client.execute({ sql: selectSql, args: messageIds });
2691
+ return updatedResult.rows.map((row) => this.parseRow(row));
2260
2692
  }
2261
- async createSpan(span) {
2693
+ async deleteMessages(messageIds) {
2694
+ if (!messageIds || messageIds.length === 0) {
2695
+ return;
2696
+ }
2262
2697
  try {
2263
- const now = (/* @__PURE__ */ new Date()).toISOString();
2264
- const record = {
2265
- ...span,
2266
- createdAt: now,
2267
- updatedAt: now
2268
- };
2269
- return this.operations.insert({ tableName: TABLE_SPANS, record });
2698
+ const BATCH_SIZE = 100;
2699
+ const threadIds = /* @__PURE__ */ new Set();
2700
+ const tx = await this.#client.transaction("write");
2701
+ try {
2702
+ for (let i = 0; i < messageIds.length; i += BATCH_SIZE) {
2703
+ const batch = messageIds.slice(i, i + BATCH_SIZE);
2704
+ const placeholders = batch.map(() => "?").join(",");
2705
+ const result = await tx.execute({
2706
+ sql: `SELECT DISTINCT thread_id FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
2707
+ args: batch
2708
+ });
2709
+ result.rows?.forEach((row) => {
2710
+ if (row.thread_id) threadIds.add(row.thread_id);
2711
+ });
2712
+ await tx.execute({
2713
+ sql: `DELETE FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
2714
+ args: batch
2715
+ });
2716
+ }
2717
+ if (threadIds.size > 0) {
2718
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2719
+ for (const threadId of threadIds) {
2720
+ await tx.execute({
2721
+ sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
2722
+ args: [now, threadId]
2723
+ });
2724
+ }
2725
+ }
2726
+ await tx.commit();
2727
+ } catch (error) {
2728
+ await tx.rollback();
2729
+ throw error;
2730
+ }
2270
2731
  } catch (error) {
2271
2732
  throw new MastraError(
2272
2733
  {
2273
- id: createStorageErrorId("LIBSQL", "CREATE_SPAN", "FAILED"),
2734
+ id: createStorageErrorId("LIBSQL", "DELETE_MESSAGES", "FAILED"),
2274
2735
  domain: ErrorDomain.STORAGE,
2275
- category: ErrorCategory.USER,
2276
- details: {
2277
- spanId: span.spanId,
2278
- traceId: span.traceId,
2279
- spanType: span.spanType,
2280
- spanName: span.name
2281
- }
2736
+ category: ErrorCategory.THIRD_PARTY,
2737
+ details: { messageIds: messageIds.join(", ") }
2282
2738
  },
2283
2739
  error
2284
2740
  );
2285
2741
  }
2286
2742
  }
2287
- async getTrace(traceId) {
2743
+ async getResourceById({ resourceId }) {
2744
+ const result = await this.#db.select({
2745
+ tableName: TABLE_RESOURCES,
2746
+ keys: { id: resourceId }
2747
+ });
2748
+ if (!result) {
2749
+ return null;
2750
+ }
2751
+ return {
2752
+ ...result,
2753
+ // Ensure workingMemory is always returned as a string, even if auto-parsed as JSON
2754
+ workingMemory: result.workingMemory && typeof result.workingMemory === "object" ? JSON.stringify(result.workingMemory) : result.workingMemory,
2755
+ metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata,
2756
+ createdAt: new Date(result.createdAt),
2757
+ updatedAt: new Date(result.updatedAt)
2758
+ };
2759
+ }
2760
+ async saveResource({ resource }) {
2761
+ await this.#db.insert({
2762
+ tableName: TABLE_RESOURCES,
2763
+ record: {
2764
+ ...resource
2765
+ // metadata is handled by prepareStatement which stringifies jsonb columns
2766
+ }
2767
+ });
2768
+ return resource;
2769
+ }
2770
+ async updateResource({
2771
+ resourceId,
2772
+ workingMemory,
2773
+ metadata
2774
+ }) {
2775
+ const existingResource = await this.getResourceById({ resourceId });
2776
+ if (!existingResource) {
2777
+ const newResource = {
2778
+ id: resourceId,
2779
+ workingMemory,
2780
+ metadata: metadata || {},
2781
+ createdAt: /* @__PURE__ */ new Date(),
2782
+ updatedAt: /* @__PURE__ */ new Date()
2783
+ };
2784
+ return this.saveResource({ resource: newResource });
2785
+ }
2786
+ const updatedResource = {
2787
+ ...existingResource,
2788
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
2789
+ metadata: {
2790
+ ...existingResource.metadata,
2791
+ ...metadata
2792
+ },
2793
+ updatedAt: /* @__PURE__ */ new Date()
2794
+ };
2795
+ const updates = [];
2796
+ const values = [];
2797
+ if (workingMemory !== void 0) {
2798
+ updates.push("workingMemory = ?");
2799
+ values.push(workingMemory);
2800
+ }
2801
+ if (metadata) {
2802
+ updates.push("metadata = jsonb(?)");
2803
+ values.push(JSON.stringify(updatedResource.metadata));
2804
+ }
2805
+ updates.push("updatedAt = ?");
2806
+ values.push(updatedResource.updatedAt.toISOString());
2807
+ values.push(resourceId);
2808
+ await this.#client.execute({
2809
+ sql: `UPDATE ${TABLE_RESOURCES} SET ${updates.join(", ")} WHERE id = ?`,
2810
+ args: values
2811
+ });
2812
+ return updatedResource;
2813
+ }
2814
+ async getThreadById({ threadId }) {
2288
2815
  try {
2289
- const spans = await this.operations.loadMany({
2290
- tableName: TABLE_SPANS,
2291
- whereClause: { sql: " WHERE traceId = ?", args: [traceId] },
2292
- orderBy: "startedAt DESC"
2816
+ const result = await this.#db.select({
2817
+ tableName: TABLE_THREADS,
2818
+ keys: { id: threadId }
2293
2819
  });
2294
- if (!spans || spans.length === 0) {
2820
+ if (!result) {
2295
2821
  return null;
2296
2822
  }
2297
2823
  return {
2298
- traceId,
2299
- spans: spans.map((span) => transformFromSqlRow({ tableName: TABLE_SPANS, sqlRow: span }))
2824
+ ...result,
2825
+ metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata,
2826
+ createdAt: new Date(result.createdAt),
2827
+ updatedAt: new Date(result.updatedAt)
2300
2828
  };
2301
2829
  } catch (error) {
2302
2830
  throw new MastraError(
2303
2831
  {
2304
- id: createStorageErrorId("LIBSQL", "GET_TRACE", "FAILED"),
2832
+ id: createStorageErrorId("LIBSQL", "GET_THREAD_BY_ID", "FAILED"),
2305
2833
  domain: ErrorDomain.STORAGE,
2306
- category: ErrorCategory.USER,
2307
- details: {
2308
- traceId
2309
- }
2834
+ category: ErrorCategory.THIRD_PARTY,
2835
+ details: { threadId }
2310
2836
  },
2311
2837
  error
2312
2838
  );
2313
2839
  }
2314
2840
  }
2315
- async updateSpan({
2316
- spanId,
2317
- traceId,
2318
- updates
2319
- }) {
2841
+ async listThreads(args) {
2842
+ const { page = 0, perPage: perPageInput, orderBy, filter } = args;
2320
2843
  try {
2321
- await this.operations.update({
2322
- tableName: TABLE_SPANS,
2323
- keys: { spanId, traceId },
2324
- data: { ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
2325
- });
2844
+ this.validatePaginationInput(page, perPageInput ?? 100);
2326
2845
  } catch (error) {
2327
2846
  throw new MastraError(
2328
2847
  {
2329
- id: createStorageErrorId("LIBSQL", "UPDATE_SPAN", "FAILED"),
2848
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "INVALID_PAGE"),
2330
2849
  domain: ErrorDomain.STORAGE,
2331
2850
  category: ErrorCategory.USER,
2332
- details: {
2333
- spanId,
2334
- traceId
2335
- }
2851
+ details: { page, ...perPageInput !== void 0 && { perPage: perPageInput } }
2336
2852
  },
2337
- error
2853
+ error instanceof Error ? error : new Error("Invalid pagination parameters")
2338
2854
  );
2339
2855
  }
2340
- }
2341
- async getTracesPaginated({
2342
- filters,
2343
- pagination
2344
- }) {
2345
- const page = pagination?.page ?? 0;
2346
- const perPage = pagination?.perPage ?? 10;
2347
- const { entityId, entityType, ...actualFilters } = filters || {};
2348
- const filtersWithDateRange = {
2349
- ...actualFilters,
2350
- ...buildDateRangeFilter(pagination?.dateRange, "startedAt"),
2351
- parentSpanId: null
2352
- };
2353
- const whereClause = prepareWhereClause(filtersWithDateRange, SPAN_SCHEMA);
2354
- let actualWhereClause = whereClause.sql || "";
2355
- if (entityId && entityType) {
2356
- const statement = `name = ?`;
2357
- let name = "";
2358
- if (entityType === "workflow") {
2359
- name = `workflow run: '${entityId}'`;
2360
- } else if (entityType === "agent") {
2361
- name = `agent run: '${entityId}'`;
2362
- } else {
2363
- const error = new MastraError({
2364
- id: createStorageErrorId("LIBSQL", "GET_TRACES_PAGINATED", "INVALID_ENTITY_TYPE"),
2365
- domain: ErrorDomain.STORAGE,
2366
- category: ErrorCategory.USER,
2367
- details: {
2368
- entityType
2369
- },
2370
- text: `Cannot filter by entity type: ${entityType}`
2371
- });
2372
- this.logger?.trackException(error);
2373
- throw error;
2374
- }
2375
- whereClause.args.push(name);
2376
- if (actualWhereClause) {
2377
- actualWhereClause += ` AND ${statement}`;
2378
- } else {
2379
- actualWhereClause += `WHERE ${statement}`;
2380
- }
2381
- }
2382
- const orderBy = "startedAt DESC";
2383
- let count = 0;
2856
+ const perPage = normalizePerPage(perPageInput, 100);
2384
2857
  try {
2385
- count = await this.operations.loadTotalCount({
2386
- tableName: TABLE_SPANS,
2387
- whereClause: { sql: actualWhereClause, args: whereClause.args }
2388
- });
2858
+ this.validateMetadataKeys(filter?.metadata);
2389
2859
  } catch (error) {
2390
2860
  throw new MastraError(
2391
2861
  {
2392
- id: createStorageErrorId("LIBSQL", "GET_TRACES_PAGINATED", "COUNT_FAILED"),
2862
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "INVALID_METADATA_KEY"),
2393
2863
  domain: ErrorDomain.STORAGE,
2394
- category: ErrorCategory.USER
2864
+ category: ErrorCategory.USER,
2865
+ details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
2395
2866
  },
2396
- error
2867
+ error instanceof Error ? error : new Error("Invalid metadata key")
2397
2868
  );
2398
2869
  }
2399
- if (count === 0) {
2400
- return {
2401
- pagination: {
2870
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2871
+ const { field, direction } = this.parseOrderBy(orderBy);
2872
+ try {
2873
+ const whereClauses = [];
2874
+ const queryParams = [];
2875
+ if (filter?.resourceId) {
2876
+ whereClauses.push("resourceId = ?");
2877
+ queryParams.push(filter.resourceId);
2878
+ }
2879
+ if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
2880
+ for (const [key, value] of Object.entries(filter.metadata)) {
2881
+ if (value === null) {
2882
+ whereClauses.push(`json_extract(metadata, '$.${key}') IS NULL`);
2883
+ } else if (typeof value === "boolean") {
2884
+ whereClauses.push(`json_extract(metadata, '$.${key}') = ?`);
2885
+ queryParams.push(value ? 1 : 0);
2886
+ } else if (typeof value === "number") {
2887
+ whereClauses.push(`json_extract(metadata, '$.${key}') = ?`);
2888
+ queryParams.push(value);
2889
+ } else if (typeof value === "string") {
2890
+ whereClauses.push(`json_extract(metadata, '$.${key}') = ?`);
2891
+ queryParams.push(value);
2892
+ } else {
2893
+ throw new MastraError({
2894
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "INVALID_METADATA_VALUE"),
2895
+ domain: ErrorDomain.STORAGE,
2896
+ category: ErrorCategory.USER,
2897
+ text: `Metadata filter value for key "${key}" must be a scalar type (string, number, boolean, or null), got ${typeof value}`,
2898
+ details: { key, valueType: typeof value }
2899
+ });
2900
+ }
2901
+ }
2902
+ }
2903
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
2904
+ const baseQuery = `FROM ${TABLE_THREADS} ${whereClause}`;
2905
+ const mapRowToStorageThreadType = (row) => ({
2906
+ id: row.id,
2907
+ resourceId: row.resourceId,
2908
+ title: row.title,
2909
+ createdAt: new Date(row.createdAt),
2910
+ updatedAt: new Date(row.updatedAt),
2911
+ metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata
2912
+ });
2913
+ const countResult = await this.#client.execute({
2914
+ sql: `SELECT COUNT(*) as count ${baseQuery}`,
2915
+ args: queryParams
2916
+ });
2917
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
2918
+ if (total === 0) {
2919
+ return {
2920
+ threads: [],
2402
2921
  total: 0,
2403
2922
  page,
2404
- perPage,
2923
+ perPage: perPageForResponse,
2405
2924
  hasMore: false
2406
- },
2407
- spans: []
2408
- };
2409
- }
2410
- try {
2411
- const spans = await this.operations.loadMany({
2412
- tableName: TABLE_SPANS,
2413
- whereClause: {
2414
- sql: actualWhereClause,
2415
- args: whereClause.args
2416
- },
2417
- orderBy,
2418
- offset: page * perPage,
2419
- limit: perPage
2925
+ };
2926
+ }
2927
+ const limitValue = perPageInput === false ? total : perPage;
2928
+ const dataResult = await this.#client.execute({
2929
+ sql: `SELECT ${buildSelectColumns(TABLE_THREADS)} ${baseQuery} ORDER BY "${field}" ${direction} LIMIT ? OFFSET ?`,
2930
+ args: [...queryParams, limitValue, offset]
2420
2931
  });
2932
+ const threads = (dataResult.rows || []).map(mapRowToStorageThreadType);
2421
2933
  return {
2422
- pagination: {
2423
- total: count,
2424
- page,
2425
- perPage,
2426
- hasMore: spans.length === perPage
2427
- },
2428
- spans: spans.map((span) => transformFromSqlRow({ tableName: TABLE_SPANS, sqlRow: span }))
2934
+ threads,
2935
+ total,
2936
+ page,
2937
+ perPage: perPageForResponse,
2938
+ hasMore: perPageInput === false ? false : offset + perPage < total
2429
2939
  };
2430
2940
  } catch (error) {
2431
- throw new MastraError(
2432
- {
2433
- id: createStorageErrorId("LIBSQL", "GET_TRACES_PAGINATED", "FAILED"),
2434
- domain: ErrorDomain.STORAGE,
2435
- category: ErrorCategory.USER
2436
- },
2437
- error
2438
- );
2439
- }
2440
- }
2441
- async batchCreateSpans(args) {
2442
- try {
2443
- const now = (/* @__PURE__ */ new Date()).toISOString();
2444
- return this.operations.batchInsert({
2445
- tableName: TABLE_SPANS,
2446
- records: args.records.map((record) => ({
2447
- ...record,
2448
- createdAt: now,
2449
- updatedAt: now
2450
- }))
2451
- });
2452
- } catch (error) {
2453
- throw new MastraError(
2941
+ if (error instanceof MastraError && error.category === ErrorCategory.USER) {
2942
+ throw error;
2943
+ }
2944
+ const mastraError = new MastraError(
2454
2945
  {
2455
- id: createStorageErrorId("LIBSQL", "BATCH_CREATE_SPANS", "FAILED"),
2946
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "FAILED"),
2456
2947
  domain: ErrorDomain.STORAGE,
2457
- category: ErrorCategory.USER
2948
+ category: ErrorCategory.THIRD_PARTY,
2949
+ details: {
2950
+ ...filter?.resourceId && { resourceId: filter.resourceId },
2951
+ hasMetadataFilter: !!filter?.metadata
2952
+ }
2458
2953
  },
2459
2954
  error
2460
2955
  );
2956
+ this.logger?.trackException?.(mastraError);
2957
+ this.logger?.error?.(mastraError.toString());
2958
+ return {
2959
+ threads: [],
2960
+ total: 0,
2961
+ page,
2962
+ perPage: perPageForResponse,
2963
+ hasMore: false
2964
+ };
2461
2965
  }
2462
2966
  }
2463
- async batchUpdateSpans(args) {
2967
+ async saveThread({ thread }) {
2464
2968
  try {
2465
- return this.operations.batchUpdate({
2466
- tableName: TABLE_SPANS,
2467
- updates: args.records.map((record) => ({
2468
- keys: { spanId: record.spanId, traceId: record.traceId },
2469
- data: { ...record.updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
2470
- }))
2969
+ await this.#db.insert({
2970
+ tableName: TABLE_THREADS,
2971
+ record: {
2972
+ ...thread
2973
+ // metadata is handled by prepareStatement which stringifies jsonb columns
2974
+ }
2471
2975
  });
2976
+ return thread;
2472
2977
  } catch (error) {
2473
- throw new MastraError(
2978
+ const mastraError = new MastraError(
2474
2979
  {
2475
- id: createStorageErrorId("LIBSQL", "BATCH_UPDATE_SPANS", "FAILED"),
2980
+ id: createStorageErrorId("LIBSQL", "SAVE_THREAD", "FAILED"),
2476
2981
  domain: ErrorDomain.STORAGE,
2477
- category: ErrorCategory.USER
2982
+ category: ErrorCategory.THIRD_PARTY,
2983
+ details: { threadId: thread.id }
2478
2984
  },
2479
2985
  error
2480
2986
  );
2987
+ this.logger?.trackException?.(mastraError);
2988
+ this.logger?.error?.(mastraError.toString());
2989
+ throw mastraError;
2481
2990
  }
2482
2991
  }
2483
- async batchDeleteTraces(args) {
2992
+ async updateThread({
2993
+ id,
2994
+ title,
2995
+ metadata
2996
+ }) {
2997
+ const thread = await this.getThreadById({ threadId: id });
2998
+ if (!thread) {
2999
+ throw new MastraError({
3000
+ id: createStorageErrorId("LIBSQL", "UPDATE_THREAD", "NOT_FOUND"),
3001
+ domain: ErrorDomain.STORAGE,
3002
+ category: ErrorCategory.USER,
3003
+ text: `Thread ${id} not found`,
3004
+ details: {
3005
+ status: 404,
3006
+ threadId: id
3007
+ }
3008
+ });
3009
+ }
3010
+ const updatedThread = {
3011
+ ...thread,
3012
+ title,
3013
+ metadata: {
3014
+ ...thread.metadata,
3015
+ ...metadata
3016
+ }
3017
+ };
2484
3018
  try {
2485
- const keys = args.traceIds.map((traceId) => ({ traceId }));
2486
- return this.operations.batchDelete({
2487
- tableName: TABLE_SPANS,
2488
- keys
3019
+ await this.#client.execute({
3020
+ sql: `UPDATE ${TABLE_THREADS} SET title = ?, metadata = jsonb(?) WHERE id = ?`,
3021
+ args: [title, JSON.stringify(updatedThread.metadata), id]
2489
3022
  });
3023
+ return updatedThread;
2490
3024
  } catch (error) {
2491
3025
  throw new MastraError(
2492
3026
  {
2493
- id: createStorageErrorId("LIBSQL", "BATCH_DELETE_TRACES", "FAILED"),
3027
+ id: createStorageErrorId("LIBSQL", "UPDATE_THREAD", "FAILED"),
2494
3028
  domain: ErrorDomain.STORAGE,
2495
- category: ErrorCategory.USER
3029
+ category: ErrorCategory.THIRD_PARTY,
3030
+ text: `Failed to update thread ${id}`,
3031
+ details: { threadId: id }
2496
3032
  },
2497
3033
  error
2498
3034
  );
2499
3035
  }
2500
3036
  }
2501
- };
2502
- var StoreOperationsLibSQL = class extends StoreOperations {
2503
- client;
2504
- /**
2505
- * Maximum number of retries for write operations if an SQLITE_BUSY error occurs.
2506
- * @default 5
2507
- */
2508
- maxRetries;
2509
- /**
2510
- * Initial backoff time in milliseconds for retrying write operations on SQLITE_BUSY.
2511
- * The backoff time will double with each retry (exponential backoff).
2512
- * @default 100
2513
- */
2514
- initialBackoffMs;
2515
- constructor({
2516
- client,
2517
- maxRetries,
2518
- initialBackoffMs
2519
- }) {
2520
- super();
2521
- this.client = client;
2522
- this.maxRetries = maxRetries ?? 5;
2523
- this.initialBackoffMs = initialBackoffMs ?? 100;
2524
- }
2525
- async hasColumn(table, column) {
2526
- const result = await this.client.execute({
2527
- sql: `PRAGMA table_info(${table})`
2528
- });
2529
- return (await result.rows)?.some((row) => row.name === column);
2530
- }
2531
- getCreateTableSQL(tableName, schema) {
2532
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2533
- const columns = Object.entries(schema).map(([name, col]) => {
2534
- const parsedColumnName = parseSqlIdentifier(name, "column name");
2535
- const type = this.getSqlType(col.type);
2536
- const nullable = col.nullable ? "" : "NOT NULL";
2537
- const primaryKey = col.primaryKey ? "PRIMARY KEY" : "";
2538
- return `${parsedColumnName} ${type} ${nullable} ${primaryKey}`.trim();
2539
- });
2540
- if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
2541
- const stmnt = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
2542
- ${columns.join(",\n")},
2543
- PRIMARY KEY (workflow_name, run_id)
2544
- )`;
2545
- return stmnt;
2546
- }
2547
- if (tableName === TABLE_SPANS) {
2548
- const stmnt = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
2549
- ${columns.join(",\n")},
2550
- PRIMARY KEY (traceId, spanId)
2551
- )`;
2552
- return stmnt;
2553
- }
2554
- return `CREATE TABLE IF NOT EXISTS ${parsedTableName} (${columns.join(", ")})`;
2555
- }
2556
- async createTable({
2557
- tableName,
2558
- schema
2559
- }) {
3037
+ async deleteThread({ threadId }) {
2560
3038
  try {
2561
- this.logger.debug(`Creating database table`, { tableName, operation: "schema init" });
2562
- const sql = this.getCreateTableSQL(tableName, schema);
2563
- await this.client.execute(sql);
3039
+ await this.#client.execute({
3040
+ sql: `DELETE FROM ${TABLE_MESSAGES} WHERE thread_id = ?`,
3041
+ args: [threadId]
3042
+ });
3043
+ await this.#client.execute({
3044
+ sql: `DELETE FROM ${TABLE_THREADS} WHERE id = ?`,
3045
+ args: [threadId]
3046
+ });
2564
3047
  } catch (error) {
2565
3048
  throw new MastraError(
2566
3049
  {
2567
- id: createStorageErrorId("LIBSQL", "CREATE_TABLE", "FAILED"),
3050
+ id: createStorageErrorId("LIBSQL", "DELETE_THREAD", "FAILED"),
2568
3051
  domain: ErrorDomain.STORAGE,
2569
3052
  category: ErrorCategory.THIRD_PARTY,
2570
- details: {
2571
- tableName
2572
- }
3053
+ details: { threadId }
2573
3054
  },
2574
3055
  error
2575
3056
  );
2576
3057
  }
2577
3058
  }
2578
- getSqlType(type) {
2579
- switch (type) {
2580
- case "bigint":
2581
- return "INTEGER";
2582
- // SQLite uses INTEGER for all integer sizes
2583
- case "timestamp":
2584
- return "TEXT";
2585
- // Store timestamps as ISO strings in SQLite
2586
- // jsonb falls through to base class which returns 'JSONB'
2587
- // SQLite's flexible type system treats JSONB as TEXT affinity
2588
- default:
2589
- return super.getSqlType(type);
3059
+ async cloneThread(args) {
3060
+ const { sourceThreadId, newThreadId: providedThreadId, resourceId, title, metadata, options } = args;
3061
+ const sourceThread = await this.getThreadById({ threadId: sourceThreadId });
3062
+ if (!sourceThread) {
3063
+ throw new MastraError({
3064
+ id: createStorageErrorId("LIBSQL", "CLONE_THREAD", "SOURCE_NOT_FOUND"),
3065
+ domain: ErrorDomain.STORAGE,
3066
+ category: ErrorCategory.USER,
3067
+ text: `Source thread with id ${sourceThreadId} not found`,
3068
+ details: { sourceThreadId }
3069
+ });
2590
3070
  }
2591
- }
2592
- async doInsert({
2593
- tableName,
2594
- record
2595
- }) {
2596
- await this.client.execute(
2597
- prepareStatement({
2598
- tableName,
2599
- record
2600
- })
2601
- );
2602
- }
2603
- insert(args) {
2604
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
2605
- logger: this.logger,
2606
- maxRetries: this.maxRetries,
2607
- initialBackoffMs: this.initialBackoffMs
2608
- });
2609
- return executeWriteOperationWithRetry(() => this.doInsert(args), `insert into table ${args.tableName}`);
2610
- }
2611
- async load({ tableName, keys }) {
2612
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2613
- const parsedKeys = Object.keys(keys).map((key) => parseSqlIdentifier(key, "column name"));
2614
- const conditions = parsedKeys.map((key) => `${key} = ?`).join(" AND ");
2615
- const values = Object.values(keys);
2616
- const result = await this.client.execute({
2617
- sql: `SELECT * FROM ${parsedTableName} WHERE ${conditions} ORDER BY createdAt DESC LIMIT 1`,
2618
- args: values
2619
- });
2620
- if (!result.rows || result.rows.length === 0) {
2621
- return null;
3071
+ const newThreadId = providedThreadId || crypto.randomUUID();
3072
+ const existingThread = await this.getThreadById({ threadId: newThreadId });
3073
+ if (existingThread) {
3074
+ throw new MastraError({
3075
+ id: createStorageErrorId("LIBSQL", "CLONE_THREAD", "THREAD_EXISTS"),
3076
+ domain: ErrorDomain.STORAGE,
3077
+ category: ErrorCategory.USER,
3078
+ text: `Thread with id ${newThreadId} already exists`,
3079
+ details: { newThreadId }
3080
+ });
2622
3081
  }
2623
- const row = result.rows[0];
2624
- const parsed = Object.fromEntries(
2625
- Object.entries(row || {}).map(([k, v]) => {
2626
- try {
2627
- return [k, typeof v === "string" ? v.startsWith("{") || v.startsWith("[") ? JSON.parse(v) : v : v];
2628
- } catch {
2629
- return [k, v];
3082
+ try {
3083
+ let messageQuery = `SELECT id, content, role, type, "createdAt", thread_id, "resourceId"
3084
+ FROM "${TABLE_MESSAGES}" WHERE thread_id = ?`;
3085
+ const messageParams = [sourceThreadId];
3086
+ if (options?.messageFilter?.startDate) {
3087
+ messageQuery += ` AND "createdAt" >= ?`;
3088
+ messageParams.push(
3089
+ options.messageFilter.startDate instanceof Date ? options.messageFilter.startDate.toISOString() : options.messageFilter.startDate
3090
+ );
3091
+ }
3092
+ if (options?.messageFilter?.endDate) {
3093
+ messageQuery += ` AND "createdAt" <= ?`;
3094
+ messageParams.push(
3095
+ options.messageFilter.endDate instanceof Date ? options.messageFilter.endDate.toISOString() : options.messageFilter.endDate
3096
+ );
3097
+ }
3098
+ if (options?.messageFilter?.messageIds && options.messageFilter.messageIds.length > 0) {
3099
+ messageQuery += ` AND id IN (${options.messageFilter.messageIds.map(() => "?").join(", ")})`;
3100
+ messageParams.push(...options.messageFilter.messageIds);
3101
+ }
3102
+ messageQuery += ` ORDER BY "createdAt" ASC`;
3103
+ if (options?.messageLimit && options.messageLimit > 0) {
3104
+ const limitQuery = `SELECT * FROM (${messageQuery.replace('ORDER BY "createdAt" ASC', 'ORDER BY "createdAt" DESC')} LIMIT ?) ORDER BY "createdAt" ASC`;
3105
+ messageParams.push(options.messageLimit);
3106
+ messageQuery = limitQuery;
3107
+ }
3108
+ const sourceMessagesResult = await this.#client.execute({ sql: messageQuery, args: messageParams });
3109
+ const sourceMessages = sourceMessagesResult.rows || [];
3110
+ const now = /* @__PURE__ */ new Date();
3111
+ const nowStr = now.toISOString();
3112
+ const lastMessageId = sourceMessages.length > 0 ? sourceMessages[sourceMessages.length - 1].id : void 0;
3113
+ const cloneMetadata = {
3114
+ sourceThreadId,
3115
+ clonedAt: now,
3116
+ ...lastMessageId && { lastMessageId }
3117
+ };
3118
+ const newThread = {
3119
+ id: newThreadId,
3120
+ resourceId: resourceId || sourceThread.resourceId,
3121
+ title: title || (sourceThread.title ? `Clone of ${sourceThread.title}` : void 0),
3122
+ metadata: {
3123
+ ...metadata,
3124
+ clone: cloneMetadata
3125
+ },
3126
+ createdAt: now,
3127
+ updatedAt: now
3128
+ };
3129
+ const tx = await this.#client.transaction("write");
3130
+ try {
3131
+ await tx.execute({
3132
+ sql: `INSERT INTO "${TABLE_THREADS}" (id, "resourceId", title, metadata, "createdAt", "updatedAt")
3133
+ VALUES (?, ?, ?, jsonb(?), ?, ?)`,
3134
+ args: [
3135
+ newThread.id,
3136
+ newThread.resourceId,
3137
+ newThread.title || null,
3138
+ JSON.stringify(newThread.metadata),
3139
+ nowStr,
3140
+ nowStr
3141
+ ]
3142
+ });
3143
+ const clonedMessages = [];
3144
+ const targetResourceId = resourceId || sourceThread.resourceId;
3145
+ for (const sourceMsg of sourceMessages) {
3146
+ const newMessageId = crypto.randomUUID();
3147
+ const contentStr = sourceMsg.content;
3148
+ let parsedContent;
3149
+ try {
3150
+ parsedContent = JSON.parse(contentStr);
3151
+ } catch {
3152
+ parsedContent = { format: 2, parts: [{ type: "text", text: contentStr }] };
3153
+ }
3154
+ await tx.execute({
3155
+ sql: `INSERT INTO "${TABLE_MESSAGES}" (id, thread_id, content, role, type, "createdAt", "resourceId")
3156
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
3157
+ args: [
3158
+ newMessageId,
3159
+ newThreadId,
3160
+ contentStr,
3161
+ sourceMsg.role,
3162
+ sourceMsg.type || "v2",
3163
+ sourceMsg.createdAt,
3164
+ targetResourceId
3165
+ ]
3166
+ });
3167
+ clonedMessages.push({
3168
+ id: newMessageId,
3169
+ threadId: newThreadId,
3170
+ content: parsedContent,
3171
+ role: sourceMsg.role,
3172
+ type: sourceMsg.type || void 0,
3173
+ createdAt: new Date(sourceMsg.createdAt),
3174
+ resourceId: targetResourceId
3175
+ });
2630
3176
  }
2631
- })
2632
- );
2633
- return parsed;
2634
- }
2635
- async loadMany({
2636
- tableName,
2637
- whereClause,
2638
- orderBy,
2639
- offset,
2640
- limit,
2641
- args
2642
- }) {
2643
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2644
- let statement = `SELECT * FROM ${parsedTableName}`;
2645
- if (whereClause?.sql) {
2646
- statement += `${whereClause.sql}`;
2647
- }
2648
- if (orderBy) {
2649
- statement += ` ORDER BY ${orderBy}`;
2650
- }
2651
- if (limit) {
2652
- statement += ` LIMIT ${limit}`;
2653
- }
2654
- if (offset) {
2655
- statement += ` OFFSET ${offset}`;
2656
- }
2657
- const result = await this.client.execute({
2658
- sql: statement,
2659
- args: [...whereClause?.args ?? [], ...args ?? []]
2660
- });
2661
- return result.rows;
2662
- }
2663
- async loadTotalCount({
2664
- tableName,
2665
- whereClause
2666
- }) {
2667
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
2668
- const statement = `SELECT COUNT(*) as count FROM ${parsedTableName} ${whereClause ? `${whereClause.sql}` : ""}`;
2669
- const result = await this.client.execute({
2670
- sql: statement,
2671
- args: whereClause?.args ?? []
2672
- });
2673
- if (!result.rows || result.rows.length === 0) {
2674
- return 0;
2675
- }
2676
- return result.rows[0]?.count ?? 0;
2677
- }
2678
- update(args) {
2679
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
2680
- logger: this.logger,
2681
- maxRetries: this.maxRetries,
2682
- initialBackoffMs: this.initialBackoffMs
2683
- });
2684
- return executeWriteOperationWithRetry(() => this.executeUpdate(args), `update table ${args.tableName}`);
2685
- }
2686
- async executeUpdate({
2687
- tableName,
2688
- keys,
2689
- data
2690
- }) {
2691
- await this.client.execute(prepareUpdateStatement({ tableName, updates: data, keys }));
2692
- }
2693
- async doBatchInsert({
2694
- tableName,
2695
- records
2696
- }) {
2697
- if (records.length === 0) return;
2698
- const batchStatements = records.map((r) => prepareStatement({ tableName, record: r }));
2699
- await this.client.batch(batchStatements, "write");
2700
- }
2701
- batchInsert(args) {
2702
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
2703
- logger: this.logger,
2704
- maxRetries: this.maxRetries,
2705
- initialBackoffMs: this.initialBackoffMs
2706
- });
2707
- return executeWriteOperationWithRetry(
2708
- () => this.doBatchInsert(args),
2709
- `batch insert into table ${args.tableName}`
2710
- ).catch((error) => {
3177
+ await tx.commit();
3178
+ return {
3179
+ thread: newThread,
3180
+ clonedMessages
3181
+ };
3182
+ } catch (error) {
3183
+ await tx.rollback();
3184
+ throw error;
3185
+ }
3186
+ } catch (error) {
3187
+ if (error instanceof MastraError) {
3188
+ throw error;
3189
+ }
2711
3190
  throw new MastraError(
2712
3191
  {
2713
- id: createStorageErrorId("LIBSQL", "BATCH_INSERT", "FAILED"),
3192
+ id: createStorageErrorId("LIBSQL", "CLONE_THREAD", "FAILED"),
2714
3193
  domain: ErrorDomain.STORAGE,
2715
3194
  category: ErrorCategory.THIRD_PARTY,
2716
- details: {
2717
- tableName: args.tableName
2718
- }
3195
+ details: { sourceThreadId, newThreadId }
2719
3196
  },
2720
3197
  error
2721
3198
  );
2722
- });
3199
+ }
3200
+ }
3201
+ };
3202
+ var ObservabilityLibSQL = class extends ObservabilityStorage {
3203
+ #db;
3204
+ constructor(config) {
3205
+ super();
3206
+ const client = resolveClient(config);
3207
+ this.#db = new LibSQLDB({ client, maxRetries: config.maxRetries, initialBackoffMs: config.initialBackoffMs });
3208
+ }
3209
+ async init() {
3210
+ await this.#db.createTable({ tableName: TABLE_SPANS, schema: SPAN_SCHEMA });
3211
+ }
3212
+ async dangerouslyClearAll() {
3213
+ await this.#db.deleteData({ tableName: TABLE_SPANS });
2723
3214
  }
2724
3215
  /**
2725
- * Public batch update method with retry logic
3216
+ * Manually run the spans migration to deduplicate and add the unique constraint.
3217
+ * This is intended to be called from the CLI when duplicates are detected.
3218
+ *
3219
+ * @returns Migration result with status and details
2726
3220
  */
2727
- batchUpdate(args) {
2728
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
2729
- logger: this.logger,
2730
- maxRetries: this.maxRetries,
2731
- initialBackoffMs: this.initialBackoffMs
2732
- });
2733
- return executeWriteOperationWithRetry(
2734
- () => this.executeBatchUpdate(args),
2735
- `batch update in table ${args.tableName}`
2736
- ).catch((error) => {
3221
+ async migrateSpans() {
3222
+ return this.#db.migrateSpans();
3223
+ }
3224
+ /**
3225
+ * Check migration status for the spans table.
3226
+ * Returns information about whether migration is needed.
3227
+ */
3228
+ async checkSpansMigrationStatus() {
3229
+ return this.#db.checkSpansMigrationStatus();
3230
+ }
3231
+ get tracingStrategy() {
3232
+ return {
3233
+ preferred: "batch-with-updates",
3234
+ supported: ["batch-with-updates", "insert-only"]
3235
+ };
3236
+ }
3237
+ async createSpan(args) {
3238
+ const { span } = args;
3239
+ try {
3240
+ const startedAt = span.startedAt instanceof Date ? span.startedAt.toISOString() : span.startedAt;
3241
+ const endedAt = span.endedAt instanceof Date ? span.endedAt.toISOString() : span.endedAt;
3242
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3243
+ const record = {
3244
+ ...span,
3245
+ startedAt,
3246
+ endedAt,
3247
+ createdAt: now,
3248
+ updatedAt: now
3249
+ };
3250
+ return this.#db.insert({ tableName: TABLE_SPANS, record });
3251
+ } catch (error) {
2737
3252
  throw new MastraError(
2738
3253
  {
2739
- id: createStorageErrorId("LIBSQL", "BATCH_UPDATE", "FAILED"),
3254
+ id: createStorageErrorId("LIBSQL", "CREATE_SPAN", "FAILED"),
2740
3255
  domain: ErrorDomain.STORAGE,
2741
- category: ErrorCategory.THIRD_PARTY,
3256
+ category: ErrorCategory.USER,
2742
3257
  details: {
2743
- tableName: args.tableName
3258
+ spanId: span.spanId,
3259
+ traceId: span.traceId,
3260
+ spanType: span.spanType,
3261
+ name: span.name
2744
3262
  }
2745
3263
  },
2746
3264
  error
2747
3265
  );
2748
- });
3266
+ }
2749
3267
  }
2750
- /**
2751
- * Updates multiple records in batch. Each record can be updated based on single or composite keys.
2752
- */
2753
- async executeBatchUpdate({
2754
- tableName,
2755
- updates
2756
- }) {
2757
- if (updates.length === 0) return;
2758
- const batchStatements = updates.map(
2759
- ({ keys, data }) => prepareUpdateStatement({
2760
- tableName,
2761
- updates: data,
2762
- keys
2763
- })
2764
- );
2765
- await this.client.batch(batchStatements, "write");
3268
+ async getSpan(args) {
3269
+ const { traceId, spanId } = args;
3270
+ try {
3271
+ const rows = await this.#db.selectMany({
3272
+ tableName: TABLE_SPANS,
3273
+ whereClause: { sql: " WHERE traceId = ? AND spanId = ?", args: [traceId, spanId] },
3274
+ limit: 1
3275
+ });
3276
+ if (!rows || rows.length === 0) {
3277
+ return null;
3278
+ }
3279
+ return {
3280
+ span: transformFromSqlRow({ tableName: TABLE_SPANS, sqlRow: rows[0] })
3281
+ };
3282
+ } catch (error) {
3283
+ throw new MastraError(
3284
+ {
3285
+ id: createStorageErrorId("LIBSQL", "GET_SPAN", "FAILED"),
3286
+ domain: ErrorDomain.STORAGE,
3287
+ category: ErrorCategory.USER,
3288
+ details: { traceId, spanId }
3289
+ },
3290
+ error
3291
+ );
3292
+ }
2766
3293
  }
2767
- /**
2768
- * Public batch delete method with retry logic
2769
- */
2770
- batchDelete({ tableName, keys }) {
2771
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
2772
- logger: this.logger,
2773
- maxRetries: this.maxRetries,
2774
- initialBackoffMs: this.initialBackoffMs
2775
- });
2776
- return executeWriteOperationWithRetry(
2777
- () => this.executeBatchDelete({ tableName, keys }),
2778
- `batch delete from table ${tableName}`
2779
- ).catch((error) => {
3294
+ async getRootSpan(args) {
3295
+ const { traceId } = args;
3296
+ try {
3297
+ const rows = await this.#db.selectMany({
3298
+ tableName: TABLE_SPANS,
3299
+ whereClause: { sql: " WHERE traceId = ? AND parentSpanId IS NULL", args: [traceId] },
3300
+ limit: 1
3301
+ });
3302
+ if (!rows || rows.length === 0) {
3303
+ return null;
3304
+ }
3305
+ return {
3306
+ span: transformFromSqlRow({ tableName: TABLE_SPANS, sqlRow: rows[0] })
3307
+ };
3308
+ } catch (error) {
2780
3309
  throw new MastraError(
2781
3310
  {
2782
- id: createStorageErrorId("LIBSQL", "BATCH_DELETE", "FAILED"),
3311
+ id: createStorageErrorId("LIBSQL", "GET_ROOT_SPAN", "FAILED"),
2783
3312
  domain: ErrorDomain.STORAGE,
2784
- category: ErrorCategory.THIRD_PARTY,
3313
+ category: ErrorCategory.USER,
3314
+ details: { traceId }
3315
+ },
3316
+ error
3317
+ );
3318
+ }
3319
+ }
3320
+ async getTrace(args) {
3321
+ const { traceId } = args;
3322
+ try {
3323
+ const spans = await this.#db.selectMany({
3324
+ tableName: TABLE_SPANS,
3325
+ whereClause: { sql: " WHERE traceId = ?", args: [traceId] },
3326
+ orderBy: "startedAt ASC"
3327
+ });
3328
+ if (!spans || spans.length === 0) {
3329
+ return null;
3330
+ }
3331
+ return {
3332
+ traceId,
3333
+ spans: spans.map((span) => transformFromSqlRow({ tableName: TABLE_SPANS, sqlRow: span }))
3334
+ };
3335
+ } catch (error) {
3336
+ throw new MastraError(
3337
+ {
3338
+ id: createStorageErrorId("LIBSQL", "GET_TRACE", "FAILED"),
3339
+ domain: ErrorDomain.STORAGE,
3340
+ category: ErrorCategory.USER,
2785
3341
  details: {
2786
- tableName
3342
+ traceId
2787
3343
  }
2788
3344
  },
2789
3345
  error
2790
3346
  );
2791
- });
2792
- }
2793
- /**
2794
- * Deletes multiple records in batch. Each record can be deleted based on single or composite keys.
2795
- */
2796
- async executeBatchDelete({
2797
- tableName,
2798
- keys
2799
- }) {
2800
- if (keys.length === 0) return;
2801
- const batchStatements = keys.map(
2802
- (keyObj) => prepareDeleteStatement({
2803
- tableName,
2804
- keys: keyObj
2805
- })
2806
- );
2807
- await this.client.batch(batchStatements, "write");
3347
+ }
2808
3348
  }
2809
- /**
2810
- * Alters table schema to add columns if they don't exist
2811
- * @param tableName Name of the table
2812
- * @param schema Schema of the table
2813
- * @param ifNotExists Array of column names to add if they don't exist
2814
- */
2815
- async alterTable({
2816
- tableName,
2817
- schema,
2818
- ifNotExists
2819
- }) {
2820
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
3349
+ async updateSpan(args) {
3350
+ const { traceId, spanId, updates } = args;
2821
3351
  try {
2822
- const pragmaQuery = `PRAGMA table_info(${parsedTableName})`;
2823
- const result = await this.client.execute(pragmaQuery);
2824
- const existingColumnNames = new Set(result.rows.map((row) => row.name.toLowerCase()));
2825
- for (const columnName of ifNotExists) {
2826
- if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
2827
- const columnDef = schema[columnName];
2828
- const sqlType = this.getSqlType(columnDef.type);
2829
- const nullable = columnDef.nullable === false ? "NOT NULL" : "";
2830
- const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
2831
- const alterSql = `ALTER TABLE ${parsedTableName} ADD COLUMN "${columnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
2832
- await this.client.execute(alterSql);
2833
- this.logger?.debug?.(`Added column ${columnName} to table ${parsedTableName}`);
2834
- }
3352
+ const data = { ...updates };
3353
+ if (data.endedAt instanceof Date) {
3354
+ data.endedAt = data.endedAt.toISOString();
3355
+ }
3356
+ if (data.startedAt instanceof Date) {
3357
+ data.startedAt = data.startedAt.toISOString();
2835
3358
  }
3359
+ data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3360
+ await this.#db.update({
3361
+ tableName: TABLE_SPANS,
3362
+ keys: { spanId, traceId },
3363
+ data
3364
+ });
2836
3365
  } catch (error) {
2837
3366
  throw new MastraError(
2838
3367
  {
2839
- id: createStorageErrorId("LIBSQL", "ALTER_TABLE", "FAILED"),
3368
+ id: createStorageErrorId("LIBSQL", "UPDATE_SPAN", "FAILED"),
2840
3369
  domain: ErrorDomain.STORAGE,
2841
- category: ErrorCategory.THIRD_PARTY,
3370
+ category: ErrorCategory.USER,
2842
3371
  details: {
2843
- tableName
3372
+ spanId,
3373
+ traceId
3374
+ }
3375
+ },
3376
+ error
3377
+ );
3378
+ }
3379
+ }
3380
+ async listTraces(args) {
3381
+ const { filters, pagination, orderBy } = listTracesArgsSchema.parse(args);
3382
+ const { page, perPage } = pagination;
3383
+ const tableName = parseSqlIdentifier(TABLE_SPANS, "table name");
3384
+ try {
3385
+ const conditions = ["parentSpanId IS NULL"];
3386
+ const queryArgs = [];
3387
+ if (filters) {
3388
+ if (filters.startedAt?.start) {
3389
+ conditions.push(`startedAt >= ?`);
3390
+ queryArgs.push(filters.startedAt.start.toISOString());
3391
+ }
3392
+ if (filters.startedAt?.end) {
3393
+ conditions.push(`startedAt <= ?`);
3394
+ queryArgs.push(filters.startedAt.end.toISOString());
3395
+ }
3396
+ if (filters.endedAt?.start) {
3397
+ conditions.push(`endedAt >= ?`);
3398
+ queryArgs.push(filters.endedAt.start.toISOString());
3399
+ }
3400
+ if (filters.endedAt?.end) {
3401
+ conditions.push(`endedAt <= ?`);
3402
+ queryArgs.push(filters.endedAt.end.toISOString());
3403
+ }
3404
+ if (filters.spanType !== void 0) {
3405
+ conditions.push(`spanType = ?`);
3406
+ queryArgs.push(filters.spanType);
3407
+ }
3408
+ if (filters.entityType !== void 0) {
3409
+ conditions.push(`entityType = ?`);
3410
+ queryArgs.push(filters.entityType);
3411
+ }
3412
+ if (filters.entityId !== void 0) {
3413
+ conditions.push(`entityId = ?`);
3414
+ queryArgs.push(filters.entityId);
3415
+ }
3416
+ if (filters.entityName !== void 0) {
3417
+ conditions.push(`entityName = ?`);
3418
+ queryArgs.push(filters.entityName);
3419
+ }
3420
+ if (filters.userId !== void 0) {
3421
+ conditions.push(`userId = ?`);
3422
+ queryArgs.push(filters.userId);
3423
+ }
3424
+ if (filters.organizationId !== void 0) {
3425
+ conditions.push(`organizationId = ?`);
3426
+ queryArgs.push(filters.organizationId);
3427
+ }
3428
+ if (filters.resourceId !== void 0) {
3429
+ conditions.push(`resourceId = ?`);
3430
+ queryArgs.push(filters.resourceId);
3431
+ }
3432
+ if (filters.runId !== void 0) {
3433
+ conditions.push(`runId = ?`);
3434
+ queryArgs.push(filters.runId);
3435
+ }
3436
+ if (filters.sessionId !== void 0) {
3437
+ conditions.push(`sessionId = ?`);
3438
+ queryArgs.push(filters.sessionId);
3439
+ }
3440
+ if (filters.threadId !== void 0) {
3441
+ conditions.push(`threadId = ?`);
3442
+ queryArgs.push(filters.threadId);
3443
+ }
3444
+ if (filters.requestId !== void 0) {
3445
+ conditions.push(`requestId = ?`);
3446
+ queryArgs.push(filters.requestId);
3447
+ }
3448
+ if (filters.environment !== void 0) {
3449
+ conditions.push(`environment = ?`);
3450
+ queryArgs.push(filters.environment);
3451
+ }
3452
+ if (filters.source !== void 0) {
3453
+ conditions.push(`source = ?`);
3454
+ queryArgs.push(filters.source);
3455
+ }
3456
+ if (filters.serviceName !== void 0) {
3457
+ conditions.push(`serviceName = ?`);
3458
+ queryArgs.push(filters.serviceName);
3459
+ }
3460
+ if (filters.scope != null) {
3461
+ for (const [key, value] of Object.entries(filters.scope)) {
3462
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
3463
+ throw new MastraError({
3464
+ id: createStorageErrorId("LIBSQL", "LIST_TRACES", "INVALID_FILTER_KEY"),
3465
+ domain: ErrorDomain.STORAGE,
3466
+ category: ErrorCategory.USER,
3467
+ details: { key }
3468
+ });
3469
+ }
3470
+ conditions.push(`json_extract(scope, '$.${key}') = ?`);
3471
+ queryArgs.push(typeof value === "string" ? value : JSON.stringify(value));
3472
+ }
3473
+ }
3474
+ if (filters.metadata != null) {
3475
+ for (const [key, value] of Object.entries(filters.metadata)) {
3476
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
3477
+ throw new MastraError({
3478
+ id: createStorageErrorId("LIBSQL", "LIST_TRACES", "INVALID_FILTER_KEY"),
3479
+ domain: ErrorDomain.STORAGE,
3480
+ category: ErrorCategory.USER,
3481
+ details: { key }
3482
+ });
3483
+ }
3484
+ conditions.push(`json_extract(metadata, '$.${key}') = ?`);
3485
+ queryArgs.push(typeof value === "string" ? value : JSON.stringify(value));
3486
+ }
3487
+ }
3488
+ if (filters.tags != null && filters.tags.length > 0) {
3489
+ for (const tag of filters.tags) {
3490
+ conditions.push(`EXISTS (SELECT 1 FROM json_each(${tableName}.tags) WHERE value = ?)`);
3491
+ queryArgs.push(tag);
3492
+ }
3493
+ }
3494
+ if (filters.status !== void 0) {
3495
+ switch (filters.status) {
3496
+ case TraceStatus.ERROR:
3497
+ conditions.push(`error IS NOT NULL`);
3498
+ break;
3499
+ case TraceStatus.RUNNING:
3500
+ conditions.push(`endedAt IS NULL AND error IS NULL`);
3501
+ break;
3502
+ case TraceStatus.SUCCESS:
3503
+ conditions.push(`endedAt IS NOT NULL AND error IS NULL`);
3504
+ break;
3505
+ }
3506
+ }
3507
+ if (filters.hasChildError !== void 0) {
3508
+ if (filters.hasChildError) {
3509
+ conditions.push(`EXISTS (
3510
+ SELECT 1 FROM ${tableName} c
3511
+ WHERE c.traceId = ${tableName}.traceId AND c.error IS NOT NULL
3512
+ )`);
3513
+ } else {
3514
+ conditions.push(`NOT EXISTS (
3515
+ SELECT 1 FROM ${tableName} c
3516
+ WHERE c.traceId = ${tableName}.traceId AND c.error IS NOT NULL
3517
+ )`);
2844
3518
  }
3519
+ }
3520
+ }
3521
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3522
+ const sortField = orderBy.field;
3523
+ const sortDirection = orderBy.direction;
3524
+ let orderByClause;
3525
+ if (sortField === "endedAt") {
3526
+ orderByClause = sortDirection === "DESC" ? `CASE WHEN ${sortField} IS NULL THEN 0 ELSE 1 END, ${sortField} DESC` : `CASE WHEN ${sortField} IS NULL THEN 1 ELSE 0 END, ${sortField} ASC`;
3527
+ } else {
3528
+ orderByClause = `${sortField} ${sortDirection}`;
3529
+ }
3530
+ const count = await this.#db.selectTotalCount({
3531
+ tableName: TABLE_SPANS,
3532
+ whereClause: { sql: whereClause, args: queryArgs }
3533
+ });
3534
+ if (count === 0) {
3535
+ return {
3536
+ pagination: {
3537
+ total: 0,
3538
+ page,
3539
+ perPage,
3540
+ hasMore: false
3541
+ },
3542
+ spans: []
3543
+ };
3544
+ }
3545
+ const spans = await this.#db.selectMany({
3546
+ tableName: TABLE_SPANS,
3547
+ whereClause: { sql: whereClause, args: queryArgs },
3548
+ orderBy: orderByClause,
3549
+ offset: page * perPage,
3550
+ limit: perPage
3551
+ });
3552
+ return {
3553
+ pagination: {
3554
+ total: count,
3555
+ page,
3556
+ perPage,
3557
+ hasMore: (page + 1) * perPage < count
3558
+ },
3559
+ spans: spans.map((span) => transformFromSqlRow({ tableName: TABLE_SPANS, sqlRow: span }))
3560
+ };
3561
+ } catch (error) {
3562
+ throw new MastraError(
3563
+ {
3564
+ id: createStorageErrorId("LIBSQL", "LIST_TRACES", "FAILED"),
3565
+ domain: ErrorDomain.STORAGE,
3566
+ category: ErrorCategory.USER
3567
+ },
3568
+ error
3569
+ );
3570
+ }
3571
+ }
3572
+ async batchCreateSpans(args) {
3573
+ try {
3574
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3575
+ const records = args.records.map((record) => {
3576
+ const startedAt = record.startedAt instanceof Date ? record.startedAt.toISOString() : record.startedAt;
3577
+ const endedAt = record.endedAt instanceof Date ? record.endedAt.toISOString() : record.endedAt;
3578
+ return {
3579
+ ...record,
3580
+ startedAt,
3581
+ endedAt,
3582
+ createdAt: now,
3583
+ updatedAt: now
3584
+ };
3585
+ });
3586
+ return this.#db.batchInsert({
3587
+ tableName: TABLE_SPANS,
3588
+ records
3589
+ });
3590
+ } catch (error) {
3591
+ throw new MastraError(
3592
+ {
3593
+ id: createStorageErrorId("LIBSQL", "BATCH_CREATE_SPANS", "FAILED"),
3594
+ domain: ErrorDomain.STORAGE,
3595
+ category: ErrorCategory.USER
2845
3596
  },
2846
3597
  error
2847
3598
  );
2848
3599
  }
2849
3600
  }
2850
- async clearTable({ tableName }) {
2851
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
3601
+ async batchUpdateSpans(args) {
3602
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2852
3603
  try {
2853
- await this.client.execute(`DELETE FROM ${parsedTableName}`);
2854
- } catch (e) {
2855
- const mastraError = new MastraError(
3604
+ return this.#db.batchUpdate({
3605
+ tableName: TABLE_SPANS,
3606
+ updates: args.records.map((record) => {
3607
+ const data = { ...record.updates };
3608
+ if (data.endedAt instanceof Date) {
3609
+ data.endedAt = data.endedAt.toISOString();
3610
+ }
3611
+ if (data.startedAt instanceof Date) {
3612
+ data.startedAt = data.startedAt.toISOString();
3613
+ }
3614
+ data.updatedAt = now;
3615
+ return {
3616
+ keys: { spanId: record.spanId, traceId: record.traceId },
3617
+ data
3618
+ };
3619
+ })
3620
+ });
3621
+ } catch (error) {
3622
+ throw new MastraError(
2856
3623
  {
2857
- id: createStorageErrorId("LIBSQL", "CLEAR_TABLE", "FAILED"),
3624
+ id: createStorageErrorId("LIBSQL", "BATCH_UPDATE_SPANS", "FAILED"),
2858
3625
  domain: ErrorDomain.STORAGE,
2859
- category: ErrorCategory.THIRD_PARTY,
2860
- details: {
2861
- tableName
2862
- }
3626
+ category: ErrorCategory.USER
2863
3627
  },
2864
- e
3628
+ error
2865
3629
  );
2866
- this.logger?.trackException?.(mastraError);
2867
- this.logger?.error?.(mastraError.toString());
2868
3630
  }
2869
3631
  }
2870
- async dropTable({ tableName }) {
2871
- const parsedTableName = parseSqlIdentifier(tableName, "table name");
3632
+ async batchDeleteTraces(args) {
2872
3633
  try {
2873
- await this.client.execute(`DROP TABLE IF EXISTS ${parsedTableName}`);
2874
- } catch (e) {
3634
+ const keys = args.traceIds.map((traceId) => ({ traceId }));
3635
+ return this.#db.batchDelete({
3636
+ tableName: TABLE_SPANS,
3637
+ keys
3638
+ });
3639
+ } catch (error) {
2875
3640
  throw new MastraError(
2876
3641
  {
2877
- id: createStorageErrorId("LIBSQL", "DROP_TABLE", "FAILED"),
3642
+ id: createStorageErrorId("LIBSQL", "BATCH_DELETE_TRACES", "FAILED"),
2878
3643
  domain: ErrorDomain.STORAGE,
2879
- category: ErrorCategory.THIRD_PARTY,
2880
- details: {
2881
- tableName
2882
- }
3644
+ category: ErrorCategory.USER
2883
3645
  },
2884
- e
3646
+ error
2885
3647
  );
2886
3648
  }
2887
3649
  }
2888
3650
  };
2889
3651
  var ScoresLibSQL = class extends ScoresStorage {
2890
- operations;
2891
- client;
2892
- constructor({ client, operations }) {
3652
+ #db;
3653
+ #client;
3654
+ constructor(config) {
2893
3655
  super();
2894
- this.operations = operations;
2895
- this.client = client;
3656
+ const client = resolveClient(config);
3657
+ this.#client = client;
3658
+ this.#db = new LibSQLDB({ client, maxRetries: config.maxRetries, initialBackoffMs: config.initialBackoffMs });
3659
+ }
3660
+ async init() {
3661
+ await this.#db.createTable({ tableName: TABLE_SCORERS, schema: SCORERS_SCHEMA });
3662
+ await this.#db.alterTable({
3663
+ tableName: TABLE_SCORERS,
3664
+ schema: SCORERS_SCHEMA,
3665
+ ifNotExists: ["spanId", "requestContext"]
3666
+ });
3667
+ }
3668
+ async dangerouslyClearAll() {
3669
+ await this.#db.deleteData({ tableName: TABLE_SCORERS });
2896
3670
  }
2897
3671
  async listScoresByRunId({
2898
3672
  runId,
@@ -2900,7 +3674,7 @@ var ScoresLibSQL = class extends ScoresStorage {
2900
3674
  }) {
2901
3675
  try {
2902
3676
  const { page, perPage: perPageInput } = pagination;
2903
- const countResult = await this.client.execute({
3677
+ const countResult = await this.#client.execute({
2904
3678
  sql: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE runId = ?`,
2905
3679
  args: [runId]
2906
3680
  });
@@ -2920,8 +3694,8 @@ var ScoresLibSQL = class extends ScoresStorage {
2920
3694
  const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2921
3695
  const limitValue = perPageInput === false ? total : perPage;
2922
3696
  const end = perPageInput === false ? total : start + perPage;
2923
- const result = await this.client.execute({
2924
- sql: `SELECT * FROM ${TABLE_SCORERS} WHERE runId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
3697
+ const result = await this.#client.execute({
3698
+ sql: `SELECT ${buildSelectColumns(TABLE_SCORERS)} FROM ${TABLE_SCORERS} WHERE runId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
2925
3699
  args: [runId, limitValue, start]
2926
3700
  });
2927
3701
  const scores = result.rows?.map((row) => this.transformScoreRow(row)) ?? [];
@@ -2973,7 +3747,7 @@ var ScoresLibSQL = class extends ScoresStorage {
2973
3747
  queryParams.push(source);
2974
3748
  }
2975
3749
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2976
- const countResult = await this.client.execute({
3750
+ const countResult = await this.#client.execute({
2977
3751
  sql: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} ${whereClause}`,
2978
3752
  args: queryParams
2979
3753
  });
@@ -2993,8 +3767,8 @@ var ScoresLibSQL = class extends ScoresStorage {
2993
3767
  const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2994
3768
  const limitValue = perPageInput === false ? total : perPage;
2995
3769
  const end = perPageInput === false ? total : start + perPage;
2996
- const result = await this.client.execute({
2997
- sql: `SELECT * FROM ${TABLE_SCORERS} ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
3770
+ const result = await this.#client.execute({
3771
+ sql: `SELECT ${buildSelectColumns(TABLE_SCORERS)} FROM ${TABLE_SCORERS} ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
2998
3772
  args: [...queryParams, limitValue, start]
2999
3773
  });
3000
3774
  const scores = result.rows?.map((row) => this.transformScoreRow(row)) ?? [];
@@ -3020,16 +3794,13 @@ var ScoresLibSQL = class extends ScoresStorage {
3020
3794
  }
3021
3795
  /**
3022
3796
  * LibSQL-specific score row transformation.
3023
- * Maps additionalLLMContext column to additionalContext field.
3024
3797
  */
3025
3798
  transformScoreRow(row) {
3026
- return transformScoreRow(row, {
3027
- fieldMappings: { additionalContext: "additionalLLMContext" }
3028
- });
3799
+ return transformScoreRow(row);
3029
3800
  }
3030
3801
  async getScoreById({ id }) {
3031
- const result = await this.client.execute({
3032
- sql: `SELECT * FROM ${TABLE_SCORERS} WHERE id = ?`,
3802
+ const result = await this.#client.execute({
3803
+ sql: `SELECT ${buildSelectColumns(TABLE_SCORERS)} FROM ${TABLE_SCORERS} WHERE id = ?`,
3033
3804
  args: [id]
3034
3805
  });
3035
3806
  return result.rows?.[0] ? this.transformScoreRow(result.rows[0]) : null;
@@ -3045,7 +3816,7 @@ var ScoresLibSQL = class extends ScoresStorage {
3045
3816
  domain: ErrorDomain.STORAGE,
3046
3817
  category: ErrorCategory.USER,
3047
3818
  details: {
3048
- scorer: score.scorer?.id ?? "unknown",
3819
+ scorer: typeof score.scorer?.id === "string" ? score.scorer.id : String(score.scorer?.id ?? "unknown"),
3049
3820
  entityId: score.entityId ?? "unknown",
3050
3821
  entityType: score.entityType ?? "unknown",
3051
3822
  traceId: score.traceId ?? "",
@@ -3058,7 +3829,7 @@ var ScoresLibSQL = class extends ScoresStorage {
3058
3829
  try {
3059
3830
  const id = crypto.randomUUID();
3060
3831
  const now = /* @__PURE__ */ new Date();
3061
- await this.operations.insert({
3832
+ await this.#db.insert({
3062
3833
  tableName: TABLE_SCORERS,
3063
3834
  record: {
3064
3835
  ...parsedScore,
@@ -3086,7 +3857,7 @@ var ScoresLibSQL = class extends ScoresStorage {
3086
3857
  }) {
3087
3858
  try {
3088
3859
  const { page, perPage: perPageInput } = pagination;
3089
- const countResult = await this.client.execute({
3860
+ const countResult = await this.#client.execute({
3090
3861
  sql: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE entityId = ? AND entityType = ?`,
3091
3862
  args: [entityId, entityType]
3092
3863
  });
@@ -3106,8 +3877,8 @@ var ScoresLibSQL = class extends ScoresStorage {
3106
3877
  const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
3107
3878
  const limitValue = perPageInput === false ? total : perPage;
3108
3879
  const end = perPageInput === false ? total : start + perPage;
3109
- const result = await this.client.execute({
3110
- sql: `SELECT * FROM ${TABLE_SCORERS} WHERE entityId = ? AND entityType = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
3880
+ const result = await this.#client.execute({
3881
+ sql: `SELECT ${buildSelectColumns(TABLE_SCORERS)} FROM ${TABLE_SCORERS} WHERE entityId = ? AND entityType = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
3111
3882
  args: [entityId, entityType, limitValue, start]
3112
3883
  });
3113
3884
  const scores = result.rows?.map((row) => this.transformScoreRow(row)) ?? [];
@@ -3140,15 +3911,15 @@ var ScoresLibSQL = class extends ScoresStorage {
3140
3911
  const { page, perPage: perPageInput } = pagination;
3141
3912
  const perPage = normalizePerPage(perPageInput, 100);
3142
3913
  const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
3143
- const countSQLResult = await this.client.execute({
3914
+ const countSQLResult = await this.#client.execute({
3144
3915
  sql: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE traceId = ? AND spanId = ?`,
3145
3916
  args: [traceId, spanId]
3146
3917
  });
3147
3918
  const total = Number(countSQLResult.rows?.[0]?.count ?? 0);
3148
3919
  const limitValue = perPageInput === false ? total : perPage;
3149
3920
  const end = perPageInput === false ? total : start + perPage;
3150
- const result = await this.client.execute({
3151
- sql: `SELECT * FROM ${TABLE_SCORERS} WHERE traceId = ? AND spanId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
3921
+ const result = await this.#client.execute({
3922
+ sql: `SELECT ${buildSelectColumns(TABLE_SCORERS)} FROM ${TABLE_SCORERS} WHERE traceId = ? AND spanId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
3152
3923
  args: [traceId, spanId, limitValue, start]
3153
3924
  });
3154
3925
  const scores = result.rows?.map((row) => this.transformScoreRow(row)) ?? [];
@@ -3173,56 +3944,68 @@ var ScoresLibSQL = class extends ScoresStorage {
3173
3944
  }
3174
3945
  }
3175
3946
  };
3176
- function parseWorkflowRun(row) {
3177
- let parsedSnapshot = row.snapshot;
3178
- if (typeof parsedSnapshot === "string") {
3179
- try {
3180
- parsedSnapshot = JSON.parse(row.snapshot);
3181
- } catch (e) {
3182
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
3183
- }
3184
- }
3185
- return {
3186
- workflowName: row.workflow_name,
3187
- runId: row.run_id,
3188
- snapshot: parsedSnapshot,
3189
- resourceId: row.resourceId,
3190
- createdAt: new Date(row.createdAt),
3191
- updatedAt: new Date(row.updatedAt)
3192
- };
3193
- }
3194
3947
  var WorkflowsLibSQL = class extends WorkflowsStorage {
3195
- operations;
3196
- client;
3197
- maxRetries;
3198
- initialBackoffMs;
3199
- constructor({
3200
- operations,
3201
- client,
3202
- maxRetries = 5,
3203
- initialBackoffMs = 500
3204
- }) {
3948
+ #db;
3949
+ #client;
3950
+ executeWithRetry;
3951
+ constructor(config) {
3205
3952
  super();
3206
- this.operations = operations;
3207
- this.client = client;
3208
- this.maxRetries = maxRetries;
3209
- this.initialBackoffMs = initialBackoffMs;
3953
+ const client = resolveClient(config);
3954
+ const maxRetries = config.maxRetries ?? 5;
3955
+ const initialBackoffMs = config.initialBackoffMs ?? 500;
3956
+ this.#client = client;
3957
+ this.#db = new LibSQLDB({ client, maxRetries, initialBackoffMs });
3958
+ this.executeWithRetry = createExecuteWriteOperationWithRetry({
3959
+ logger: this.logger,
3960
+ maxRetries,
3961
+ initialBackoffMs
3962
+ });
3210
3963
  this.setupPragmaSettings().catch(
3211
3964
  (err) => this.logger.warn("LibSQL Workflows: Failed to setup PRAGMA settings.", err)
3212
3965
  );
3213
3966
  }
3967
+ parseWorkflowRun(row) {
3968
+ let parsedSnapshot = row.snapshot;
3969
+ if (typeof parsedSnapshot === "string") {
3970
+ try {
3971
+ parsedSnapshot = JSON.parse(row.snapshot);
3972
+ } catch (e) {
3973
+ this.logger.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
3974
+ }
3975
+ }
3976
+ return {
3977
+ workflowName: row.workflow_name,
3978
+ runId: row.run_id,
3979
+ snapshot: parsedSnapshot,
3980
+ resourceId: row.resourceId,
3981
+ createdAt: new Date(row.createdAt),
3982
+ updatedAt: new Date(row.updatedAt)
3983
+ };
3984
+ }
3985
+ async init() {
3986
+ const schema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
3987
+ await this.#db.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema });
3988
+ await this.#db.alterTable({
3989
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
3990
+ schema,
3991
+ ifNotExists: ["resourceId"]
3992
+ });
3993
+ }
3994
+ async dangerouslyClearAll() {
3995
+ await this.#db.deleteData({ tableName: TABLE_WORKFLOW_SNAPSHOT });
3996
+ }
3214
3997
  async setupPragmaSettings() {
3215
3998
  try {
3216
- await this.client.execute("PRAGMA busy_timeout = 10000;");
3999
+ await this.#client.execute("PRAGMA busy_timeout = 10000;");
3217
4000
  this.logger.debug("LibSQL Workflows: PRAGMA busy_timeout=10000 set.");
3218
4001
  try {
3219
- await this.client.execute("PRAGMA journal_mode = WAL;");
4002
+ await this.#client.execute("PRAGMA journal_mode = WAL;");
3220
4003
  this.logger.debug("LibSQL Workflows: PRAGMA journal_mode=WAL set.");
3221
4004
  } catch {
3222
4005
  this.logger.debug("LibSQL Workflows: WAL mode not supported, using default journal mode.");
3223
4006
  }
3224
4007
  try {
3225
- await this.client.execute("PRAGMA synchronous = NORMAL;");
4008
+ await this.#client.execute("PRAGMA synchronous = NORMAL;");
3226
4009
  this.logger.debug("LibSQL Workflows: PRAGMA synchronous=NORMAL set.");
3227
4010
  } catch {
3228
4011
  this.logger.debug("LibSQL Workflows: Failed to set synchronous mode.");
@@ -3231,44 +4014,6 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3231
4014
  this.logger.warn("LibSQL Workflows: Failed to set PRAGMA settings.", err);
3232
4015
  }
3233
4016
  }
3234
- async executeWithRetry(operation) {
3235
- let attempts = 0;
3236
- let backoff = this.initialBackoffMs;
3237
- while (attempts < this.maxRetries) {
3238
- try {
3239
- return await operation();
3240
- } catch (error) {
3241
- this.logger.debug("LibSQL Workflows: Error caught in retry loop", {
3242
- errorType: error.constructor.name,
3243
- errorCode: error.code,
3244
- errorMessage: error.message,
3245
- attempts,
3246
- maxRetries: this.maxRetries
3247
- });
3248
- const isLockError = error.code === "SQLITE_BUSY" || error.code === "SQLITE_LOCKED" || error.message?.toLowerCase().includes("database is locked") || error.message?.toLowerCase().includes("database table is locked") || error.message?.toLowerCase().includes("table is locked") || error.constructor.name === "SqliteError" && error.message?.toLowerCase().includes("locked");
3249
- if (isLockError) {
3250
- attempts++;
3251
- if (attempts >= this.maxRetries) {
3252
- this.logger.error(
3253
- `LibSQL Workflows: Operation failed after ${this.maxRetries} attempts due to database lock: ${error.message}`,
3254
- { error, attempts, maxRetries: this.maxRetries }
3255
- );
3256
- throw error;
3257
- }
3258
- this.logger.warn(
3259
- `LibSQL Workflows: Attempt ${attempts} failed due to database lock. Retrying in ${backoff}ms...`,
3260
- { errorMessage: error.message, attempts, backoff, maxRetries: this.maxRetries }
3261
- );
3262
- await new Promise((resolve) => setTimeout(resolve, backoff));
3263
- backoff *= 2;
3264
- } else {
3265
- this.logger.error("LibSQL Workflows: Non-lock error occurred, not retrying", { error });
3266
- throw error;
3267
- }
3268
- }
3269
- }
3270
- throw new Error("LibSQL Workflows: Max retries reached, but no error was re-thrown from the loop.");
3271
- }
3272
4017
  async updateWorkflowResults({
3273
4018
  workflowName,
3274
4019
  runId,
@@ -3277,10 +4022,10 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3277
4022
  requestContext
3278
4023
  }) {
3279
4024
  return this.executeWithRetry(async () => {
3280
- const tx = await this.client.transaction("write");
4025
+ const tx = await this.#client.transaction("write");
3281
4026
  try {
3282
4027
  const existingSnapshotResult = await tx.execute({
3283
- sql: `SELECT snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
4028
+ sql: `SELECT json(snapshot) as snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
3284
4029
  args: [workflowName, runId]
3285
4030
  });
3286
4031
  let snapshot;
@@ -3305,9 +4050,13 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3305
4050
  }
3306
4051
  snapshot.context[stepId] = result;
3307
4052
  snapshot.requestContext = { ...snapshot.requestContext, ...requestContext };
4053
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3308
4054
  await tx.execute({
3309
- sql: `UPDATE ${TABLE_WORKFLOW_SNAPSHOT} SET snapshot = ? WHERE workflow_name = ? AND run_id = ?`,
3310
- args: [JSON.stringify(snapshot), workflowName, runId]
4055
+ sql: `INSERT INTO ${TABLE_WORKFLOW_SNAPSHOT} (workflow_name, run_id, snapshot, createdAt, updatedAt)
4056
+ VALUES (?, ?, jsonb(?), ?, ?)
4057
+ ON CONFLICT(workflow_name, run_id)
4058
+ DO UPDATE SET snapshot = excluded.snapshot, updatedAt = excluded.updatedAt`,
4059
+ args: [workflowName, runId, JSON.stringify(snapshot), now, now]
3311
4060
  });
3312
4061
  await tx.commit();
3313
4062
  return snapshot.context;
@@ -3317,7 +4066,7 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3317
4066
  }
3318
4067
  throw error;
3319
4068
  }
3320
- });
4069
+ }, "updateWorkflowResults");
3321
4070
  }
3322
4071
  async updateWorkflowState({
3323
4072
  workflowName,
@@ -3325,10 +4074,10 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3325
4074
  opts
3326
4075
  }) {
3327
4076
  return this.executeWithRetry(async () => {
3328
- const tx = await this.client.transaction("write");
4077
+ const tx = await this.#client.transaction("write");
3329
4078
  try {
3330
4079
  const existingSnapshotResult = await tx.execute({
3331
- sql: `SELECT snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
4080
+ sql: `SELECT json(snapshot) as snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
3332
4081
  args: [workflowName, runId]
3333
4082
  });
3334
4083
  if (!existingSnapshotResult.rows?.[0]) {
@@ -3343,7 +4092,7 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3343
4092
  }
3344
4093
  const updatedSnapshot = { ...snapshot, ...opts };
3345
4094
  await tx.execute({
3346
- sql: `UPDATE ${TABLE_WORKFLOW_SNAPSHOT} SET snapshot = ? WHERE workflow_name = ? AND run_id = ?`,
4095
+ sql: `UPDATE ${TABLE_WORKFLOW_SNAPSHOT} SET snapshot = jsonb(?) WHERE workflow_name = ? AND run_id = ?`,
3347
4096
  args: [JSON.stringify(updatedSnapshot), workflowName, runId]
3348
4097
  });
3349
4098
  await tx.commit();
@@ -3354,24 +4103,27 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3354
4103
  }
3355
4104
  throw error;
3356
4105
  }
3357
- });
4106
+ }, "updateWorkflowState");
3358
4107
  }
3359
4108
  async persistWorkflowSnapshot({
3360
4109
  workflowName,
3361
4110
  runId,
3362
4111
  resourceId,
3363
- snapshot
4112
+ snapshot,
4113
+ createdAt,
4114
+ updatedAt
3364
4115
  }) {
4116
+ const now = /* @__PURE__ */ new Date();
3365
4117
  const data = {
3366
4118
  workflow_name: workflowName,
3367
4119
  run_id: runId,
3368
4120
  resourceId,
3369
4121
  snapshot,
3370
- createdAt: /* @__PURE__ */ new Date(),
3371
- updatedAt: /* @__PURE__ */ new Date()
4122
+ createdAt: createdAt ?? now,
4123
+ updatedAt: updatedAt ?? now
3372
4124
  };
3373
4125
  this.logger.debug("Persisting workflow snapshot", { workflowName, runId, data });
3374
- await this.operations.insert({
4126
+ await this.#db.insert({
3375
4127
  tableName: TABLE_WORKFLOW_SNAPSHOT,
3376
4128
  record: data
3377
4129
  });
@@ -3381,7 +4133,7 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3381
4133
  runId
3382
4134
  }) {
3383
4135
  this.logger.debug("Loading workflow snapshot", { workflowName, runId });
3384
- const d = await this.operations.load({
4136
+ const d = await this.#db.select({
3385
4137
  tableName: TABLE_WORKFLOW_SNAPSHOT,
3386
4138
  keys: { workflow_name: workflowName, run_id: runId }
3387
4139
  });
@@ -3403,14 +4155,14 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3403
4155
  }
3404
4156
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3405
4157
  try {
3406
- const result = await this.client.execute({
3407
- sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC LIMIT 1`,
4158
+ const result = await this.#client.execute({
4159
+ sql: `SELECT workflow_name, run_id, resourceId, json(snapshot) as snapshot, createdAt, updatedAt FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC LIMIT 1`,
3408
4160
  args
3409
4161
  });
3410
4162
  if (!result.rows?.[0]) {
3411
4163
  return null;
3412
4164
  }
3413
- return parseWorkflowRun(result.rows[0]);
4165
+ return this.parseWorkflowRun(result.rows[0]);
3414
4166
  } catch (error) {
3415
4167
  throw new MastraError(
3416
4168
  {
@@ -3423,22 +4175,24 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3423
4175
  }
3424
4176
  }
3425
4177
  async deleteWorkflowRunById({ runId, workflowName }) {
3426
- try {
3427
- await this.client.execute({
3428
- sql: `DELETE FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
3429
- args: [workflowName, runId]
3430
- });
3431
- } catch (error) {
3432
- throw new MastraError(
3433
- {
3434
- id: createStorageErrorId("LIBSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
3435
- domain: ErrorDomain.STORAGE,
3436
- category: ErrorCategory.THIRD_PARTY,
3437
- details: { runId, workflowName }
3438
- },
3439
- error
3440
- );
3441
- }
4178
+ return this.executeWithRetry(async () => {
4179
+ try {
4180
+ await this.#client.execute({
4181
+ sql: `DELETE FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
4182
+ args: [workflowName, runId]
4183
+ });
4184
+ } catch (error) {
4185
+ throw new MastraError(
4186
+ {
4187
+ id: createStorageErrorId("LIBSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
4188
+ domain: ErrorDomain.STORAGE,
4189
+ category: ErrorCategory.THIRD_PARTY,
4190
+ details: { runId, workflowName }
4191
+ },
4192
+ error
4193
+ );
4194
+ }
4195
+ }, "deleteWorkflowRunById");
3442
4196
  }
3443
4197
  async listWorkflowRuns({
3444
4198
  workflowName,
@@ -3469,19 +4223,19 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3469
4223
  args.push(toDate.toISOString());
3470
4224
  }
3471
4225
  if (resourceId) {
3472
- const hasResourceId = await this.operations.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
4226
+ const hasResourceId = await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
3473
4227
  if (hasResourceId) {
3474
4228
  conditions.push("resourceId = ?");
3475
4229
  args.push(resourceId);
3476
4230
  } else {
3477
- console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
4231
+ this.logger.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
3478
4232
  }
3479
4233
  }
3480
4234
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3481
4235
  let total = 0;
3482
4236
  const usePagination = typeof perPage === "number" && typeof page === "number";
3483
4237
  if (usePagination) {
3484
- const countResult = await this.client.execute({
4238
+ const countResult = await this.#client.execute({
3485
4239
  sql: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
3486
4240
  args
3487
4241
  });
@@ -3489,11 +4243,11 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3489
4243
  }
3490
4244
  const normalizedPerPage = usePagination ? normalizePerPage(perPage, Number.MAX_SAFE_INTEGER) : 0;
3491
4245
  const offset = usePagination ? page * normalizedPerPage : 0;
3492
- const result = await this.client.execute({
3493
- sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC${usePagination ? ` LIMIT ? OFFSET ?` : ""}`,
4246
+ const result = await this.#client.execute({
4247
+ sql: `SELECT workflow_name, run_id, resourceId, json(snapshot) as snapshot, createdAt, updatedAt FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC${usePagination ? ` LIMIT ? OFFSET ?` : ""}`,
3494
4248
  args: usePagination ? [...args, normalizedPerPage, offset] : args
3495
4249
  });
3496
- const runs = (result.rows || []).map((row) => parseWorkflowRun(row));
4250
+ const runs = (result.rows || []).map((row) => this.parseWorkflowRun(row));
3497
4251
  return { runs, total: total || runs.length };
3498
4252
  } catch (error) {
3499
4253
  throw new MastraError(
@@ -3509,7 +4263,7 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
3509
4263
  };
3510
4264
 
3511
4265
  // src/storage/index.ts
3512
- var LibSQLStore = class extends MastraStorage {
4266
+ var LibSQLStore = class extends MastraCompositeStore {
3513
4267
  client;
3514
4268
  maxRetries;
3515
4269
  initialBackoffMs;
@@ -3536,18 +4290,17 @@ var LibSQLStore = class extends MastraStorage {
3536
4290
  } else {
3537
4291
  this.client = config.client;
3538
4292
  }
3539
- const operations = new StoreOperationsLibSQL({
4293
+ const domainConfig = {
3540
4294
  client: this.client,
3541
4295
  maxRetries: this.maxRetries,
3542
4296
  initialBackoffMs: this.initialBackoffMs
3543
- });
3544
- const scores = new ScoresLibSQL({ client: this.client, operations });
3545
- const workflows = new WorkflowsLibSQL({ client: this.client, operations });
3546
- const memory = new MemoryLibSQL({ client: this.client, operations });
3547
- const observability = new ObservabilityLibSQL({ operations });
3548
- const agents = new AgentsLibSQL({ client: this.client, operations });
4297
+ };
4298
+ const scores = new ScoresLibSQL(domainConfig);
4299
+ const workflows = new WorkflowsLibSQL(domainConfig);
4300
+ const memory = new MemoryLibSQL(domainConfig);
4301
+ const observability = new ObservabilityLibSQL(domainConfig);
4302
+ const agents = new AgentsLibSQL(domainConfig);
3549
4303
  this.stores = {
3550
- operations,
3551
4304
  scores,
3552
4305
  workflows,
3553
4306
  memory,
@@ -3555,194 +4308,6 @@ var LibSQLStore = class extends MastraStorage {
3555
4308
  agents
3556
4309
  };
3557
4310
  }
3558
- get supports() {
3559
- return {
3560
- selectByIncludeResourceScope: true,
3561
- resourceWorkingMemory: true,
3562
- hasColumn: true,
3563
- createTable: true,
3564
- deleteMessages: true,
3565
- observabilityInstance: true,
3566
- listScoresBySpan: true,
3567
- agents: true
3568
- };
3569
- }
3570
- async createTable({
3571
- tableName,
3572
- schema
3573
- }) {
3574
- await this.stores.operations.createTable({ tableName, schema });
3575
- }
3576
- /**
3577
- * Alters table schema to add columns if they don't exist
3578
- * @param tableName Name of the table
3579
- * @param schema Schema of the table
3580
- * @param ifNotExists Array of column names to add if they don't exist
3581
- */
3582
- async alterTable({
3583
- tableName,
3584
- schema,
3585
- ifNotExists
3586
- }) {
3587
- await this.stores.operations.alterTable({ tableName, schema, ifNotExists });
3588
- }
3589
- async clearTable({ tableName }) {
3590
- await this.stores.operations.clearTable({ tableName });
3591
- }
3592
- async dropTable({ tableName }) {
3593
- await this.stores.operations.dropTable({ tableName });
3594
- }
3595
- insert(args) {
3596
- return this.stores.operations.insert(args);
3597
- }
3598
- batchInsert(args) {
3599
- return this.stores.operations.batchInsert(args);
3600
- }
3601
- async load({ tableName, keys }) {
3602
- return this.stores.operations.load({ tableName, keys });
3603
- }
3604
- async getThreadById({ threadId }) {
3605
- return this.stores.memory.getThreadById({ threadId });
3606
- }
3607
- async saveThread({ thread }) {
3608
- return this.stores.memory.saveThread({ thread });
3609
- }
3610
- async updateThread({
3611
- id,
3612
- title,
3613
- metadata
3614
- }) {
3615
- return this.stores.memory.updateThread({ id, title, metadata });
3616
- }
3617
- async deleteThread({ threadId }) {
3618
- return this.stores.memory.deleteThread({ threadId });
3619
- }
3620
- async listMessagesById({ messageIds }) {
3621
- return this.stores.memory.listMessagesById({ messageIds });
3622
- }
3623
- async saveMessages(args) {
3624
- const result = await this.stores.memory.saveMessages({ messages: args.messages });
3625
- return { messages: result.messages };
3626
- }
3627
- async updateMessages({
3628
- messages
3629
- }) {
3630
- return this.stores.memory.updateMessages({ messages });
3631
- }
3632
- async deleteMessages(messageIds) {
3633
- return this.stores.memory.deleteMessages(messageIds);
3634
- }
3635
- async getScoreById({ id }) {
3636
- return this.stores.scores.getScoreById({ id });
3637
- }
3638
- async saveScore(score) {
3639
- return this.stores.scores.saveScore(score);
3640
- }
3641
- async listScoresByScorerId({
3642
- scorerId,
3643
- entityId,
3644
- entityType,
3645
- source,
3646
- pagination
3647
- }) {
3648
- return this.stores.scores.listScoresByScorerId({ scorerId, entityId, entityType, source, pagination });
3649
- }
3650
- async listScoresByRunId({
3651
- runId,
3652
- pagination
3653
- }) {
3654
- return this.stores.scores.listScoresByRunId({ runId, pagination });
3655
- }
3656
- async listScoresByEntityId({
3657
- entityId,
3658
- entityType,
3659
- pagination
3660
- }) {
3661
- return this.stores.scores.listScoresByEntityId({ entityId, entityType, pagination });
3662
- }
3663
- /**
3664
- * WORKFLOWS
3665
- */
3666
- async updateWorkflowResults({
3667
- workflowName,
3668
- runId,
3669
- stepId,
3670
- result,
3671
- requestContext
3672
- }) {
3673
- return this.stores.workflows.updateWorkflowResults({ workflowName, runId, stepId, result, requestContext });
3674
- }
3675
- async updateWorkflowState({
3676
- workflowName,
3677
- runId,
3678
- opts
3679
- }) {
3680
- return this.stores.workflows.updateWorkflowState({ workflowName, runId, opts });
3681
- }
3682
- async persistWorkflowSnapshot({
3683
- workflowName,
3684
- runId,
3685
- resourceId,
3686
- snapshot
3687
- }) {
3688
- return this.stores.workflows.persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot });
3689
- }
3690
- async loadWorkflowSnapshot({
3691
- workflowName,
3692
- runId
3693
- }) {
3694
- return this.stores.workflows.loadWorkflowSnapshot({ workflowName, runId });
3695
- }
3696
- async listWorkflowRuns(args = {}) {
3697
- return this.stores.workflows.listWorkflowRuns(args);
3698
- }
3699
- async getWorkflowRunById({
3700
- runId,
3701
- workflowName
3702
- }) {
3703
- return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
3704
- }
3705
- async deleteWorkflowRunById({ runId, workflowName }) {
3706
- return this.stores.workflows.deleteWorkflowRunById({ runId, workflowName });
3707
- }
3708
- async getResourceById({ resourceId }) {
3709
- return this.stores.memory.getResourceById({ resourceId });
3710
- }
3711
- async saveResource({ resource }) {
3712
- return this.stores.memory.saveResource({ resource });
3713
- }
3714
- async updateResource({
3715
- resourceId,
3716
- workingMemory,
3717
- metadata
3718
- }) {
3719
- return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
3720
- }
3721
- async createSpan(span) {
3722
- return this.stores.observability.createSpan(span);
3723
- }
3724
- async updateSpan(params) {
3725
- return this.stores.observability.updateSpan(params);
3726
- }
3727
- async getTrace(traceId) {
3728
- return this.stores.observability.getTrace(traceId);
3729
- }
3730
- async getTracesPaginated(args) {
3731
- return this.stores.observability.getTracesPaginated(args);
3732
- }
3733
- async listScoresBySpan({
3734
- traceId,
3735
- spanId,
3736
- pagination
3737
- }) {
3738
- return this.stores.scores.listScoresBySpan({ traceId, spanId, pagination });
3739
- }
3740
- async batchCreateSpans(args) {
3741
- return this.stores.observability.batchCreateSpans(args);
3742
- }
3743
- async batchUpdateSpans(args) {
3744
- return this.stores.observability.batchUpdateSpans(args);
3745
- }
3746
4311
  };
3747
4312
 
3748
4313
  // src/vector/prompt.ts
@@ -3844,6 +4409,6 @@ Example Complex Query:
3844
4409
  ]
3845
4410
  }`;
3846
4411
 
3847
- export { LibSQLStore as DefaultStorage, LIBSQL_PROMPT, LibSQLStore, LibSQLVector };
4412
+ export { AgentsLibSQL, LibSQLStore as DefaultStorage, LIBSQL_PROMPT, LibSQLStore, LibSQLVector, MemoryLibSQL, ObservabilityLibSQL, ScoresLibSQL, WorkflowsLibSQL };
3848
4413
  //# sourceMappingURL=index.js.map
3849
4414
  //# sourceMappingURL=index.js.map