@voltagent/libsql 1.0.0-next.0 → 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,2998 +1,2081 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
- // src/index.ts
5
- import { existsSync, mkdirSync } from "fs";
6
- import { dirname } from "path";
4
+ // src/memory-v2-adapter.ts
5
+ import fs from "fs";
6
+ import path from "path";
7
7
  import { createClient } from "@libsql/client";
8
- import { safeJsonParse } from "@voltagent/core";
9
- import { safeStringify as safeStringify2 } from "@voltagent/internal/utils";
10
- import { createPinoLogger as createPinoLogger2 } from "@voltagent/logger";
11
-
12
- // src/migrations/add-suspended-status.ts
13
- async function addSuspendedStatusMigration(db, tablePrefix = "voltagent_memory") {
14
- const migrationName = "add_suspended_status_to_workflow_history";
15
- await db.execute(`
16
- CREATE TABLE IF NOT EXISTS ${tablePrefix}_migrations (
17
- id INTEGER PRIMARY KEY AUTOINCREMENT,
18
- name TEXT NOT NULL UNIQUE,
19
- applied_at TEXT DEFAULT CURRENT_TIMESTAMP
20
- )
21
- `);
22
- const result = await db.execute({
23
- sql: `SELECT * FROM ${tablePrefix}_migrations WHERE name = ?`,
24
- args: [migrationName]
25
- });
26
- if (result.rows.length > 0) {
27
- return;
28
- }
29
- try {
30
- const needsMigration = await checkIfSuspendedStatusNeeded(db, tablePrefix);
31
- if (!needsMigration) {
32
- } else {
33
- await performSuspendedStatusMigration(db, tablePrefix);
8
+ import {
9
+ AgentRegistry,
10
+ ConversationAlreadyExistsError,
11
+ ConversationNotFoundError
12
+ } from "@voltagent/core";
13
+ import { createPinoLogger } from "@voltagent/logger";
14
+ var LibSQLMemoryAdapter = class {
15
+ static {
16
+ __name(this, "LibSQLMemoryAdapter");
17
+ }
18
+ client;
19
+ storageLimit;
20
+ tablePrefix;
21
+ initialized = false;
22
+ logger;
23
+ maxRetries;
24
+ retryDelayMs;
25
+ url;
26
+ constructor(options = {}) {
27
+ this.storageLimit = options.storageLimit ?? 100;
28
+ this.tablePrefix = options.tablePrefix ?? "voltagent_memory";
29
+ this.maxRetries = options.maxRetries ?? 3;
30
+ this.retryDelayMs = options.retryDelayMs ?? 100;
31
+ this.logger = options.logger || AgentRegistry.getInstance().getGlobalLogger() || createPinoLogger({ name: "libsql-memory" });
32
+ this.url = options.url ?? "file:./.voltagent/memory.db";
33
+ if (this.url.startsWith("file:")) {
34
+ const dbPath = this.url.replace("file:", "");
35
+ const dbDir = path.dirname(dbPath);
36
+ if (dbDir && dbDir !== "." && !fs.existsSync(dbDir)) {
37
+ fs.mkdirSync(dbDir, { recursive: true });
38
+ this.logger.debug(`Created database directory: ${dbDir}`);
39
+ }
34
40
  }
35
- await db.execute({
36
- sql: `INSERT INTO ${tablePrefix}_migrations (name) VALUES (?)`,
37
- args: [migrationName]
38
- });
39
- } catch (error) {
40
- console.error(`[Migration] Failed to apply '${migrationName}':`, error);
41
- throw error;
42
- }
43
- }
44
- __name(addSuspendedStatusMigration, "addSuspendedStatusMigration");
45
- async function checkIfSuspendedStatusNeeded(db, tablePrefix) {
46
- try {
47
- const testId = `test-suspended-check-${Date.now()}`;
48
- await db.execute({
49
- sql: `
50
- INSERT INTO ${tablePrefix}_workflow_history
51
- (id, name, workflow_id, status, start_time)
52
- VALUES (?, 'test', 'test', 'suspended', datetime('now'))
53
- `,
54
- args: [testId]
55
- });
56
- await db.execute({
57
- sql: `DELETE FROM ${tablePrefix}_workflow_history WHERE id = ?`,
58
- args: [testId]
41
+ this.client = createClient({
42
+ url: this.url,
43
+ authToken: options.authToken
59
44
  });
60
- return false;
61
- } catch (error) {
62
- if (error.message?.includes("CHECK constraint failed")) {
63
- return true;
64
- }
65
- throw error;
66
- }
67
- }
68
- __name(checkIfSuspendedStatusNeeded, "checkIfSuspendedStatusNeeded");
69
- async function performSuspendedStatusMigration(db, tablePrefix) {
70
- await db.execute("BEGIN TRANSACTION");
71
- try {
72
- await db.execute(`
73
- CREATE TABLE ${tablePrefix}_workflow_history_temp (
45
+ this.logger.debug("LibSQL Memory adapter initialized", { url: this.url });
46
+ }
47
+ /**
48
+ * Execute a database operation with retry logic
49
+ */
50
+ async executeWithRetry(operation, operationName) {
51
+ let lastError;
52
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
53
+ try {
54
+ return await operation();
55
+ } catch (error) {
56
+ lastError = error;
57
+ if (error?.code === "SQLITE_BUSY" || error?.message?.includes("SQLITE_BUSY") || error?.message?.includes("database is locked")) {
58
+ const delay = this.retryDelayMs * 2 ** attempt;
59
+ this.logger.debug(
60
+ `Database busy, retrying ${operationName} (attempt ${attempt + 1}/${this.maxRetries}) after ${delay}ms`
61
+ );
62
+ await new Promise((resolve) => setTimeout(resolve, delay));
63
+ } else {
64
+ throw error;
65
+ }
66
+ }
67
+ }
68
+ this.logger.error(
69
+ `Failed to execute ${operationName} after ${this.maxRetries} attempts`,
70
+ lastError
71
+ );
72
+ throw lastError;
73
+ }
74
+ /**
75
+ * Initialize database schema
76
+ */
77
+ async initialize() {
78
+ if (this.initialized) return;
79
+ const conversationsTable = `${this.tablePrefix}_conversations`;
80
+ const messagesTable = `${this.tablePrefix}_messages`;
81
+ const usersTable = `${this.tablePrefix}_users`;
82
+ const workflowStatesTable = `${this.tablePrefix}_workflow_states`;
83
+ const isMemoryDb = this.url === ":memory:" || this.url.includes("mode=memory");
84
+ if (!isMemoryDb && (this.url.startsWith("file:") || this.url.startsWith("libsql:"))) {
85
+ try {
86
+ await this.client.execute("PRAGMA journal_mode=WAL");
87
+ this.logger.debug("Set PRAGMA journal_mode=WAL");
88
+ } catch (err) {
89
+ this.logger.debug("Failed to set PRAGMA journal_mode=WAL (non-critical)", { err });
90
+ }
91
+ }
92
+ try {
93
+ await this.client.execute("PRAGMA busy_timeout=5000");
94
+ this.logger.debug("Set PRAGMA busy_timeout=5000");
95
+ } catch (err) {
96
+ this.logger.debug("Failed to set PRAGMA busy_timeout (non-critical)", { err });
97
+ }
98
+ try {
99
+ await this.client.execute("PRAGMA foreign_keys=ON");
100
+ this.logger.debug("Set PRAGMA foreign_keys=ON");
101
+ } catch (err) {
102
+ this.logger.debug("Failed to set PRAGMA foreign_keys (non-critical)", { err });
103
+ }
104
+ this.logger.debug("Applied PRAGMA settings for better concurrency");
105
+ await this.executeWithRetry(async () => {
106
+ await this.client.batch([
107
+ // Create users table (for user-level working memory)
108
+ `CREATE TABLE IF NOT EXISTS ${usersTable} (
109
+ id TEXT PRIMARY KEY,
110
+ metadata TEXT,
111
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
112
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
113
+ )`,
114
+ // Create conversations table (matching existing structure)
115
+ `CREATE TABLE IF NOT EXISTS ${conversationsTable} (
116
+ id TEXT PRIMARY KEY,
117
+ resource_id TEXT NOT NULL,
118
+ user_id TEXT NOT NULL,
119
+ title TEXT NOT NULL,
120
+ metadata TEXT NOT NULL,
121
+ created_at TEXT NOT NULL,
122
+ updated_at TEXT NOT NULL
123
+ )`,
124
+ // Create messages table (matching existing structure)
125
+ `CREATE TABLE IF NOT EXISTS ${messagesTable} (
126
+ conversation_id TEXT NOT NULL,
127
+ message_id TEXT NOT NULL,
128
+ user_id TEXT NOT NULL,
129
+ role TEXT NOT NULL,
130
+ parts TEXT NOT NULL,
131
+ metadata TEXT,
132
+ format_version INTEGER DEFAULT 2,
133
+ created_at TEXT NOT NULL,
134
+ PRIMARY KEY (conversation_id, message_id)
135
+ )`,
136
+ // Create workflow states table
137
+ `CREATE TABLE IF NOT EXISTS ${workflowStatesTable} (
74
138
  id TEXT PRIMARY KEY,
75
- name TEXT NOT NULL,
76
139
  workflow_id TEXT NOT NULL,
77
- status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'error', 'cancelled', 'suspended')),
78
- start_time TEXT NOT NULL,
79
- end_time TEXT,
80
- input TEXT,
81
- output TEXT,
140
+ workflow_name TEXT NOT NULL,
141
+ status TEXT NOT NULL,
142
+ suspension TEXT,
82
143
  user_id TEXT,
83
144
  conversation_id TEXT,
84
145
  metadata TEXT,
85
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
86
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
87
- )
88
- `);
89
- await db.execute(`
90
- INSERT INTO ${tablePrefix}_workflow_history_temp
91
- SELECT * FROM ${tablePrefix}_workflow_history
92
- `);
93
- await db.execute(`DROP TABLE ${tablePrefix}_workflow_history`);
94
- await db.execute(`
95
- ALTER TABLE ${tablePrefix}_workflow_history_temp
96
- RENAME TO ${tablePrefix}_workflow_history
97
- `);
98
- await db.execute(
99
- `CREATE INDEX idx_${tablePrefix}_workflow_history_workflow_id ON ${tablePrefix}_workflow_history(workflow_id)`
100
- );
101
- await db.execute(
102
- `CREATE INDEX idx_${tablePrefix}_workflow_history_status ON ${tablePrefix}_workflow_history(status)`
103
- );
104
- await db.execute(
105
- `CREATE INDEX idx_${tablePrefix}_workflow_history_start_time ON ${tablePrefix}_workflow_history(start_time)`
106
- );
107
- await db.execute(
108
- `CREATE INDEX idx_${tablePrefix}_workflow_history_user_id ON ${tablePrefix}_workflow_history(user_id)`
109
- );
110
- await db.execute(
111
- `CREATE INDEX idx_${tablePrefix}_workflow_history_conversation_id ON ${tablePrefix}_workflow_history(conversation_id)`
112
- );
113
- await db.execute("COMMIT");
114
- } catch (error) {
115
- await db.execute("ROLLBACK");
116
- throw error;
146
+ created_at TEXT NOT NULL,
147
+ updated_at TEXT NOT NULL
148
+ )`,
149
+ // Create indexes for better performance
150
+ `CREATE INDEX IF NOT EXISTS idx_${conversationsTable}_user_id ON ${conversationsTable}(user_id)`,
151
+ `CREATE INDEX IF NOT EXISTS idx_${conversationsTable}_resource_id ON ${conversationsTable}(resource_id)`,
152
+ `CREATE INDEX IF NOT EXISTS idx_${messagesTable}_conversation_id ON ${messagesTable}(conversation_id)`,
153
+ `CREATE INDEX IF NOT EXISTS idx_${messagesTable}_created_at ON ${messagesTable}(created_at)`,
154
+ `CREATE INDEX IF NOT EXISTS idx_${workflowStatesTable}_workflow_id ON ${workflowStatesTable}(workflow_id)`,
155
+ `CREATE INDEX IF NOT EXISTS idx_${workflowStatesTable}_status ON ${workflowStatesTable}(status)`
156
+ ]);
157
+ }, "initialize database schema");
158
+ await this.addV2ColumnsToMessagesTable();
159
+ await this.migrateDefaultUserIds();
160
+ this.initialized = true;
161
+ this.logger.debug("Database schema initialized");
117
162
  }
118
- }
119
- __name(performSuspendedStatusMigration, "performSuspendedStatusMigration");
120
-
121
- // src/migrations/workflow-tables.ts
122
- async function createWorkflowTables(db, tablePrefix = "voltagent_memory") {
123
- await db.execute(`
124
- CREATE TABLE IF NOT EXISTS ${tablePrefix}_workflow_history (
125
- id TEXT PRIMARY KEY,
126
- name TEXT NOT NULL,
127
- workflow_id TEXT NOT NULL,
128
- status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'error', 'cancelled', 'suspended')),
129
- start_time TEXT NOT NULL,
130
- end_time TEXT,
131
- input TEXT,
132
- output TEXT,
133
- user_id TEXT,
134
- conversation_id TEXT,
135
- metadata TEXT,
136
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
137
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
138
- )
139
- `);
140
- await db.execute(
141
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_history_workflow_id ON ${tablePrefix}_workflow_history(workflow_id)`
142
- );
143
- await db.execute(
144
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_history_status ON ${tablePrefix}_workflow_history(status)`
145
- );
146
- await db.execute(
147
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_history_start_time ON ${tablePrefix}_workflow_history(start_time)`
148
- );
149
- await db.execute(
150
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_history_user_id ON ${tablePrefix}_workflow_history(user_id)`
151
- );
152
- await db.execute(
153
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_history_conversation_id ON ${tablePrefix}_workflow_history(conversation_id)`
154
- );
155
- await db.execute(`
156
- CREATE TABLE IF NOT EXISTS ${tablePrefix}_workflow_steps (
157
- id TEXT PRIMARY KEY,
158
- workflow_history_id TEXT NOT NULL,
159
- step_index INTEGER NOT NULL,
160
- step_type TEXT NOT NULL,
161
- step_name TEXT NOT NULL,
162
- step_id TEXT,
163
- status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'error', 'skipped')),
164
- start_time TEXT NOT NULL,
165
- end_time TEXT,
166
- input TEXT,
167
- output TEXT,
168
- error_message TEXT,
169
- agent_execution_id TEXT,
170
- parallel_index INTEGER,
171
- parent_step_id TEXT,
172
- metadata TEXT,
173
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
174
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
175
- )
176
- `);
177
- await db.execute(
178
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_steps_workflow_history ON ${tablePrefix}_workflow_steps(workflow_history_id)`
179
- );
180
- await db.execute(
181
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_steps_agent_execution ON ${tablePrefix}_workflow_steps(agent_execution_id)`
182
- );
183
- await db.execute(
184
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_steps_step_index ON ${tablePrefix}_workflow_steps(workflow_history_id, step_index)`
185
- );
186
- await db.execute(
187
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_steps_parallel ON ${tablePrefix}_workflow_steps(parent_step_id, parallel_index)`
188
- );
189
- await db.execute(`
190
- CREATE TABLE IF NOT EXISTS ${tablePrefix}_workflow_timeline_events (
191
- id TEXT PRIMARY KEY,
192
- workflow_history_id TEXT NOT NULL,
193
- event_id TEXT NOT NULL,
194
- name TEXT NOT NULL,
195
- type TEXT NOT NULL CHECK (type IN ('workflow', 'workflow-step')),
196
- start_time TEXT NOT NULL,
197
- end_time TEXT,
198
- status TEXT NOT NULL,
199
- level TEXT DEFAULT 'INFO',
200
- input TEXT,
201
- output TEXT,
202
- status_message TEXT,
203
- metadata TEXT,
204
- trace_id TEXT,
205
- parent_event_id TEXT,
206
- event_sequence INTEGER,
207
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
208
- )
209
- `);
210
- await db.execute(
211
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_timeline_events_workflow_history ON ${tablePrefix}_workflow_timeline_events(workflow_history_id)`
212
- );
213
- await db.execute(
214
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_timeline_events_trace ON ${tablePrefix}_workflow_timeline_events(trace_id)`
215
- );
216
- await db.execute(
217
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_timeline_events_parent ON ${tablePrefix}_workflow_timeline_events(parent_event_id)`
218
- );
219
- await db.execute(
220
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_timeline_events_type ON ${tablePrefix}_workflow_timeline_events(type)`
221
- );
222
- await db.execute(
223
- `CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_workflow_timeline_events_sequence ON ${tablePrefix}_workflow_timeline_events(event_sequence)`
224
- );
225
- const checkWorkflowIdColumn = await db.execute(`
226
- SELECT COUNT(*) as count
227
- FROM pragma_table_info('agent_history')
228
- WHERE name = 'workflow_id'
229
- `);
230
- if (checkWorkflowIdColumn.rows[0].count === 0) {
231
- await db.execute("ALTER TABLE agent_history ADD COLUMN workflow_id TEXT");
232
- }
233
- const checkWorkflowStepIdColumn = await db.execute(`
234
- SELECT COUNT(*) as count
235
- FROM pragma_table_info('agent_history')
236
- WHERE name = 'workflow_step_id'
237
- `);
238
- if (checkWorkflowStepIdColumn.rows[0].count === 0) {
239
- await db.execute("ALTER TABLE agent_history ADD COLUMN workflow_step_id TEXT");
240
- }
241
- await db.execute(
242
- "CREATE INDEX IF NOT EXISTS idx_agent_history_workflow_id ON agent_history(workflow_id)"
243
- );
244
- await db.execute(
245
- "CREATE INDEX IF NOT EXISTS idx_agent_history_workflow_step ON agent_history(workflow_step_id)"
246
- );
247
- }
248
- __name(createWorkflowTables, "createWorkflowTables");
249
-
250
- // src/workflow-extension.ts
251
- import { safeStringify } from "@voltagent/internal/utils";
252
- import { createPinoLogger } from "@voltagent/logger";
253
- var LibSQLWorkflowExtension = class {
254
- constructor(client, _tablePrefix = "voltagent_memory", logger) {
255
- this.client = client;
256
- this._tablePrefix = _tablePrefix;
257
- this.logger = logger || createPinoLogger({ name: "libsql-workflow" });
163
+ /**
164
+ * Add new columns to messages table for V2 format if they don't exist
165
+ * This allows existing tables to support both old and new message formats
166
+ */
167
+ async addV2ColumnsToMessagesTable() {
168
+ const messagesTableName = `${this.tablePrefix}_messages`;
169
+ try {
170
+ const tableInfo = await this.client.execute(`PRAGMA table_info(${messagesTableName})`);
171
+ const columns = tableInfo.rows.map((row) => row.name);
172
+ if (!columns.includes("parts")) {
173
+ try {
174
+ await this.client.execute(`ALTER TABLE ${messagesTableName} ADD COLUMN parts TEXT`);
175
+ } catch (_e) {
176
+ }
177
+ }
178
+ if (!columns.includes("metadata")) {
179
+ try {
180
+ await this.client.execute(`ALTER TABLE ${messagesTableName} ADD COLUMN metadata TEXT`);
181
+ } catch (_e) {
182
+ }
183
+ }
184
+ if (!columns.includes("format_version")) {
185
+ try {
186
+ await this.client.execute(
187
+ `ALTER TABLE ${messagesTableName} ADD COLUMN format_version INTEGER DEFAULT 2`
188
+ );
189
+ } catch (_e) {
190
+ }
191
+ }
192
+ if (!columns.includes("user_id")) {
193
+ try {
194
+ await this.client.execute(
195
+ `ALTER TABLE ${messagesTableName} ADD COLUMN user_id TEXT NOT NULL DEFAULT 'default'`
196
+ );
197
+ } catch (_e) {
198
+ }
199
+ }
200
+ const contentInfo = tableInfo.rows.find((row) => row.name === "content");
201
+ if (contentInfo && contentInfo.notnull === 1) {
202
+ try {
203
+ await this.client.execute(
204
+ `ALTER TABLE ${messagesTableName} ADD COLUMN content_temp TEXT`
205
+ );
206
+ await this.client.execute(
207
+ `UPDATE ${messagesTableName} SET content_temp = content WHERE content IS NOT NULL`
208
+ );
209
+ try {
210
+ await this.client.execute(`ALTER TABLE ${messagesTableName} DROP COLUMN content`);
211
+ await this.client.execute(
212
+ `ALTER TABLE ${messagesTableName} RENAME COLUMN content_temp TO content`
213
+ );
214
+ } catch (_) {
215
+ }
216
+ } catch (_) {
217
+ }
218
+ }
219
+ const typeInfo = tableInfo.rows.find((row) => row.name === "type");
220
+ if (typeInfo && typeInfo.notnull === 1) {
221
+ try {
222
+ await this.client.execute(`ALTER TABLE ${messagesTableName} ADD COLUMN type_temp TEXT`);
223
+ await this.client.execute(
224
+ `UPDATE ${messagesTableName} SET type_temp = type WHERE type IS NOT NULL`
225
+ );
226
+ try {
227
+ await this.client.execute(`ALTER TABLE ${messagesTableName} DROP COLUMN type`);
228
+ await this.client.execute(
229
+ `ALTER TABLE ${messagesTableName} RENAME COLUMN type_temp TO type`
230
+ );
231
+ } catch (_) {
232
+ }
233
+ } catch (_) {
234
+ }
235
+ }
236
+ } catch (_) {
237
+ }
258
238
  }
259
- static {
260
- __name(this, "LibSQLWorkflowExtension");
239
+ /**
240
+ * Migrate default user_id values in messages table
241
+ * Updates messages with user_id='default' to use the actual user_id from their conversation
242
+ */
243
+ async migrateDefaultUserIds() {
244
+ const messagesTableName = `${this.tablePrefix}_messages`;
245
+ const conversationsTableName = `${this.tablePrefix}_conversations`;
246
+ try {
247
+ const checkResult = await this.client.execute({
248
+ sql: `SELECT COUNT(*) as count FROM ${messagesTableName} WHERE user_id = 'default'`,
249
+ args: []
250
+ });
251
+ const defaultCount = checkResult.rows[0]?.count || 0;
252
+ if (defaultCount === 0) {
253
+ return;
254
+ }
255
+ this.logger.debug(`Found ${defaultCount} messages with default user_id, starting migration`);
256
+ await this.executeWithRetry(async () => {
257
+ const result = await this.client.execute({
258
+ sql: `UPDATE ${messagesTableName}
259
+ SET user_id = (
260
+ SELECT c.user_id
261
+ FROM ${conversationsTableName} c
262
+ WHERE c.id = ${messagesTableName}.conversation_id
263
+ )
264
+ WHERE user_id = 'default'
265
+ AND EXISTS (
266
+ SELECT 1
267
+ FROM ${conversationsTableName} c
268
+ WHERE c.id = ${messagesTableName}.conversation_id
269
+ )`,
270
+ args: []
271
+ });
272
+ const updatedCount = result.rowsAffected || 0;
273
+ this.logger.info(
274
+ `Successfully migrated ${updatedCount} messages from default user_id to actual user_ids`
275
+ );
276
+ const remainingResult = await this.client.execute({
277
+ sql: `SELECT COUNT(*) as count FROM ${messagesTableName} WHERE user_id = 'default'`,
278
+ args: []
279
+ });
280
+ const remainingCount = remainingResult.rows[0]?.count || 0;
281
+ if (remainingCount > 0) {
282
+ this.logger.warn(
283
+ `${remainingCount} messages still have default user_id (possibly orphaned messages without valid conversations)`
284
+ );
285
+ }
286
+ }, "migrate default user_ids");
287
+ } catch (error) {
288
+ this.logger.error("Failed to migrate default user_ids", error);
289
+ }
261
290
  }
262
- logger;
291
+ // ============================================================================
292
+ // Message Operations
293
+ // ============================================================================
263
294
  /**
264
- * Store a workflow history entry
295
+ * Add a single message
265
296
  */
266
- async storeWorkflowHistory(entry) {
267
- await this.client.execute({
268
- sql: `
269
- INSERT INTO ${this._tablePrefix}_workflow_history (
270
- id, name, workflow_id, status, start_time, end_time,
271
- input, output, user_id, conversation_id, metadata, created_at, updated_at
272
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
273
- `,
274
- args: [
275
- entry.id,
276
- entry.workflowName,
277
- entry.workflowId,
278
- entry.status,
279
- entry.startTime.toISOString(),
280
- entry.endTime?.toISOString() || null,
281
- safeStringify(entry.input),
282
- entry.output ? safeStringify(entry.output) : null,
283
- entry.userId || null,
284
- entry.conversationId || null,
285
- entry.metadata ? safeStringify(entry.metadata) : null,
286
- entry.createdAt?.toISOString() || (/* @__PURE__ */ new Date()).toISOString(),
287
- entry.updatedAt?.toISOString() || (/* @__PURE__ */ new Date()).toISOString()
288
- ]
289
- });
297
+ async addMessage(message, userId, conversationId) {
298
+ await this.initialize();
299
+ const messagesTable = `${this.tablePrefix}_messages`;
300
+ const conversation = await this.getConversation(conversationId);
301
+ if (!conversation) {
302
+ throw new ConversationNotFoundError(conversationId);
303
+ }
304
+ await this.executeWithRetry(async () => {
305
+ await this.client.execute({
306
+ sql: `INSERT INTO ${messagesTable} (conversation_id, message_id, user_id, role, parts, metadata, format_version, created_at)
307
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
308
+ args: [
309
+ conversationId,
310
+ message.id,
311
+ userId,
312
+ message.role,
313
+ JSON.stringify(message.parts),
314
+ message.metadata ? JSON.stringify(message.metadata) : null,
315
+ 2,
316
+ // format_version
317
+ (/* @__PURE__ */ new Date()).toISOString()
318
+ ]
319
+ });
320
+ }, "add message");
321
+ await this.applyStorageLimit(conversationId);
290
322
  }
291
323
  /**
292
- * Get a workflow history entry by ID
324
+ * Add multiple messages
293
325
  */
294
- async getWorkflowHistory(id) {
295
- const result = await this.client.execute({
296
- sql: `SELECT * FROM ${this._tablePrefix}_workflow_history WHERE id = ?`,
297
- args: [id]
298
- });
299
- if (result.rows.length === 0) return null;
300
- return this.parseWorkflowHistoryRow(result.rows[0]);
326
+ async addMessages(messages, userId, conversationId) {
327
+ await this.initialize();
328
+ const messagesTable = `${this.tablePrefix}_messages`;
329
+ const conversation = await this.getConversation(conversationId);
330
+ if (!conversation) {
331
+ throw new ConversationNotFoundError(conversationId);
332
+ }
333
+ const now = (/* @__PURE__ */ new Date()).toISOString();
334
+ await this.executeWithRetry(async () => {
335
+ await this.client.batch(
336
+ messages.map((message) => ({
337
+ sql: `INSERT INTO ${messagesTable} (conversation_id, message_id, user_id, role, parts, metadata, format_version, created_at)
338
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
339
+ args: [
340
+ conversationId,
341
+ message.id,
342
+ userId,
343
+ message.role,
344
+ JSON.stringify(message.parts),
345
+ message.metadata ? JSON.stringify(message.metadata) : null,
346
+ 2,
347
+ // format_version
348
+ now
349
+ ]
350
+ }))
351
+ );
352
+ }, "add batch messages");
353
+ await this.applyStorageLimit(conversationId);
301
354
  }
302
355
  /**
303
- * Get all workflow history entries for a specific workflow
356
+ * Apply storage limit to a conversation
304
357
  */
305
- async getWorkflowHistoryByWorkflowId(workflowId) {
306
- const result = await this.client.execute({
307
- sql: `SELECT * FROM ${this._tablePrefix}_workflow_history WHERE workflow_id = ? ORDER BY start_time DESC`,
308
- args: [workflowId]
309
- });
310
- return result.rows.map((row) => this.parseWorkflowHistoryRow(row));
358
+ async applyStorageLimit(conversationId) {
359
+ const messagesTable = `${this.tablePrefix}_messages`;
360
+ await this.executeWithRetry(async () => {
361
+ await this.client.execute({
362
+ sql: `DELETE FROM ${messagesTable}
363
+ WHERE conversation_id = ?
364
+ AND message_id NOT IN (
365
+ SELECT message_id FROM ${messagesTable}
366
+ WHERE conversation_id = ?
367
+ ORDER BY created_at DESC
368
+ LIMIT ?
369
+ )`,
370
+ args: [conversationId, conversationId, this.storageLimit]
371
+ });
372
+ }, "apply storage limit");
311
373
  }
312
374
  /**
313
- * Update a workflow history entry
375
+ * Get messages with optional filtering
314
376
  */
315
- async updateWorkflowHistory(id, updates) {
316
- this.logger.trace(`Updating workflow history ${id}`, {
317
- status: updates.status,
318
- hasMetadata: !!updates.metadata,
319
- hasSuspension: !!updates.metadata?.suspension
320
- });
321
- const setClauses = [];
322
- const args = [];
323
- if (updates.status !== void 0) {
324
- setClauses.push("status = ?");
325
- args.push(updates.status);
377
+ async getMessages(userId, conversationId, options) {
378
+ await this.initialize();
379
+ const messagesTable = `${this.tablePrefix}_messages`;
380
+ const { limit = this.storageLimit, before, after, roles } = options || {};
381
+ let sql = `SELECT * FROM ${messagesTable}
382
+ WHERE conversation_id = ? AND user_id = ?`;
383
+ const args = [conversationId, userId];
384
+ if (roles && roles.length > 0) {
385
+ const placeholders = roles.map(() => "?").join(",");
386
+ sql += ` AND role IN (${placeholders})`;
387
+ args.push(...roles);
326
388
  }
327
- if (updates.endTime !== void 0) {
328
- setClauses.push("end_time = ?");
329
- args.push(updates.endTime.toISOString());
389
+ if (before) {
390
+ sql += " AND created_at < ?";
391
+ args.push(before.toISOString());
330
392
  }
331
- if (updates.output !== void 0) {
332
- setClauses.push("output = ?");
333
- args.push(safeStringify(updates.output));
334
- }
335
- if (updates.userId !== void 0) {
336
- setClauses.push("user_id = ?");
337
- args.push(updates.userId);
338
- }
339
- if (updates.conversationId !== void 0) {
340
- setClauses.push("conversation_id = ?");
341
- args.push(updates.conversationId);
342
- }
343
- if (updates.metadata !== void 0) {
344
- setClauses.push("metadata = ?");
345
- const metadataJson = safeStringify(updates.metadata);
346
- args.push(metadataJson);
347
- this.logger.trace(`Setting metadata for ${id}:`, { metadata: metadataJson });
393
+ if (after) {
394
+ sql += " AND created_at > ?";
395
+ args.push(after.toISOString());
348
396
  }
349
- setClauses.push("updated_at = ?");
350
- args.push((/* @__PURE__ */ new Date()).toISOString());
351
- args.push(id);
352
- const sql = `UPDATE ${this._tablePrefix}_workflow_history SET ${setClauses.join(", ")} WHERE id = ?`;
353
- this.logger.trace("Executing SQL:", { sql, args });
354
- try {
355
- const result = await this.client.execute({ sql, args });
356
- this.logger.trace(
357
- `Successfully updated workflow history ${id}, rows affected: ${result.rowsAffected}`
358
- );
359
- } catch (error) {
360
- this.logger.error(`Failed to update workflow history ${id}:`, { error });
361
- throw error;
397
+ sql += " ORDER BY created_at ASC";
398
+ if (limit && limit > 0) {
399
+ sql += " LIMIT ?";
400
+ args.push(limit);
362
401
  }
402
+ const result = await this.client.execute({ sql, args });
403
+ return result.rows.map((row) => {
404
+ let parts;
405
+ if (row.parts !== void 0 && row.parts !== null) {
406
+ try {
407
+ parts = JSON.parse(row.parts);
408
+ } catch {
409
+ parts = [];
410
+ }
411
+ } else if (row.content !== void 0 && row.content !== null) {
412
+ try {
413
+ const content = JSON.parse(row.content);
414
+ if (typeof content === "string") {
415
+ parts = [{ type: "text", text: content }];
416
+ } else if (Array.isArray(content)) {
417
+ parts = content;
418
+ } else {
419
+ parts = [];
420
+ }
421
+ } catch {
422
+ parts = [{ type: "text", text: row.content }];
423
+ }
424
+ } else {
425
+ parts = [];
426
+ }
427
+ return {
428
+ id: row.message_id,
429
+ role: row.role,
430
+ parts,
431
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
432
+ };
433
+ });
363
434
  }
364
435
  /**
365
- * Delete a workflow history entry
436
+ * Clear messages for a user
366
437
  */
367
- async deleteWorkflowHistory(id) {
368
- await this.client.execute({
369
- sql: `DELETE FROM ${this._tablePrefix}_workflow_history WHERE id = ?`,
370
- args: [id]
371
- });
438
+ async clearMessages(userId, conversationId) {
439
+ await this.initialize();
440
+ const messagesTable = `${this.tablePrefix}_messages`;
441
+ const conversationsTable = `${this.tablePrefix}_conversations`;
442
+ if (conversationId) {
443
+ await this.client.execute({
444
+ sql: `DELETE FROM ${messagesTable} WHERE conversation_id = ? AND user_id = ?`,
445
+ args: [conversationId, userId]
446
+ });
447
+ } else {
448
+ await this.client.execute({
449
+ sql: `DELETE FROM ${messagesTable}
450
+ WHERE conversation_id IN (
451
+ SELECT id FROM ${conversationsTable} WHERE user_id = ?
452
+ )`,
453
+ args: [userId]
454
+ });
455
+ }
372
456
  }
457
+ // ============================================================================
458
+ // Conversation Operations
459
+ // ============================================================================
373
460
  /**
374
- * Store a workflow step entry
461
+ * Create a new conversation
375
462
  */
376
- async storeWorkflowStep(step) {
377
- await this.client.execute({
378
- sql: `
379
- INSERT INTO ${this._tablePrefix}_workflow_steps (
380
- id, workflow_history_id, step_index, step_type, step_name, step_id,
381
- status, start_time, end_time, input, output, error_message,
382
- agent_execution_id, parallel_index, parent_step_id, metadata,
383
- created_at, updated_at
384
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
385
- `,
386
- args: [
387
- step.id,
388
- step.workflowHistoryId,
389
- step.stepIndex,
390
- step.stepType,
391
- step.stepName,
392
- step.stepId || null,
393
- step.status,
394
- step.startTime.toISOString(),
395
- step.endTime?.toISOString() || null,
396
- step.input ? safeStringify(step.input) : null,
397
- step.output ? safeStringify(step.output) : null,
398
- step.error ? safeStringify(step.error) : null,
399
- step.agentExecutionId || null,
400
- step.parallelIndex || null,
401
- step.parallelParentStepId || null,
402
- step.metadata ? safeStringify(step.metadata) : null,
403
- step.createdAt?.toISOString() || (/* @__PURE__ */ new Date()).toISOString(),
404
- step.updatedAt?.toISOString() || (/* @__PURE__ */ new Date()).toISOString()
405
- ]
406
- });
463
+ async createConversation(input) {
464
+ await this.initialize();
465
+ const conversationsTable = `${this.tablePrefix}_conversations`;
466
+ const existing = await this.getConversation(input.id);
467
+ if (existing) {
468
+ throw new ConversationAlreadyExistsError(input.id);
469
+ }
470
+ const now = (/* @__PURE__ */ new Date()).toISOString();
471
+ await this.executeWithRetry(async () => {
472
+ await this.client.execute({
473
+ sql: `INSERT INTO ${conversationsTable} (id, resource_id, user_id, title, metadata, created_at, updated_at)
474
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
475
+ args: [
476
+ input.id,
477
+ input.resourceId,
478
+ input.userId,
479
+ input.title,
480
+ JSON.stringify(input.metadata || {}),
481
+ now,
482
+ now
483
+ ]
484
+ });
485
+ }, "create conversation");
486
+ return {
487
+ id: input.id,
488
+ userId: input.userId,
489
+ resourceId: input.resourceId,
490
+ title: input.title,
491
+ metadata: input.metadata || {},
492
+ createdAt: now,
493
+ updatedAt: now
494
+ };
407
495
  }
408
496
  /**
409
- * Get a workflow step by ID
497
+ * Get a conversation by ID
410
498
  */
411
- async getWorkflowStep(id) {
499
+ async getConversation(id) {
500
+ await this.initialize();
501
+ const conversationsTable = `${this.tablePrefix}_conversations`;
412
502
  const result = await this.client.execute({
413
- sql: `SELECT * FROM ${this._tablePrefix}_workflow_steps WHERE id = ?`,
503
+ sql: `SELECT * FROM ${conversationsTable} WHERE id = ?`,
414
504
  args: [id]
415
505
  });
416
- if (result.rows.length === 0) return null;
417
- return this.parseWorkflowStepRow(result.rows[0]);
506
+ if (result.rows.length === 0) {
507
+ return null;
508
+ }
509
+ const row = result.rows[0];
510
+ return {
511
+ id: row.id,
512
+ userId: row.user_id,
513
+ resourceId: row.resource_id,
514
+ title: row.title,
515
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
516
+ createdAt: row.created_at,
517
+ updatedAt: row.updated_at
518
+ };
418
519
  }
419
520
  /**
420
- * Get all workflow steps for a specific workflow history
521
+ * Get conversations by resource ID
421
522
  */
422
- async getWorkflowSteps(workflowHistoryId) {
523
+ async getConversations(resourceId) {
524
+ await this.initialize();
525
+ const conversationsTable = `${this.tablePrefix}_conversations`;
423
526
  const result = await this.client.execute({
424
- sql: `SELECT * FROM ${this._tablePrefix}_workflow_steps WHERE workflow_history_id = ? ORDER BY step_index ASC`,
425
- args: [workflowHistoryId]
527
+ sql: `SELECT * FROM ${conversationsTable} WHERE resource_id = ? ORDER BY updated_at DESC`,
528
+ args: [resourceId]
426
529
  });
427
- return result.rows.map((row) => this.parseWorkflowStepRow(row));
530
+ return result.rows.map((row) => ({
531
+ id: row.id,
532
+ userId: row.user_id,
533
+ resourceId: row.resource_id,
534
+ title: row.title,
535
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
536
+ createdAt: row.created_at,
537
+ updatedAt: row.updated_at
538
+ }));
428
539
  }
429
540
  /**
430
- * Update a workflow step
541
+ * Get conversations by user ID
431
542
  */
432
- async updateWorkflowStep(id, updates) {
433
- const setClauses = [];
543
+ async getConversationsByUserId(userId, options) {
544
+ return this.queryConversations({ ...options, userId });
545
+ }
546
+ /**
547
+ * Query conversations with filters
548
+ */
549
+ async queryConversations(options) {
550
+ await this.initialize();
551
+ const conversationsTable = `${this.tablePrefix}_conversations`;
552
+ let sql = `SELECT * FROM ${conversationsTable} WHERE 1=1`;
434
553
  const args = [];
435
- if (updates.status !== void 0) {
436
- setClauses.push("status = ?");
437
- args.push(updates.status);
554
+ if (options.userId) {
555
+ sql += " AND user_id = ?";
556
+ args.push(options.userId);
438
557
  }
439
- if (updates.endTime !== void 0) {
440
- setClauses.push("end_time = ?");
441
- args.push(updates.endTime.toISOString());
558
+ if (options.resourceId) {
559
+ sql += " AND resource_id = ?";
560
+ args.push(options.resourceId);
442
561
  }
443
- if (updates.output !== void 0) {
444
- setClauses.push("output = ?");
445
- args.push(safeStringify(updates.output));
562
+ const orderBy = options.orderBy || "updated_at";
563
+ const orderDirection = options.orderDirection || "DESC";
564
+ sql += ` ORDER BY ${orderBy} ${orderDirection}`;
565
+ if (options.limit) {
566
+ sql += " LIMIT ?";
567
+ args.push(options.limit);
446
568
  }
447
- if (updates.error !== void 0) {
448
- setClauses.push("error_message = ?");
449
- args.push(safeStringify(updates.error));
569
+ if (options.offset) {
570
+ sql += " OFFSET ?";
571
+ args.push(options.offset);
450
572
  }
451
- if (updates.agentExecutionId !== void 0) {
452
- setClauses.push("agent_execution_id = ?");
453
- args.push(updates.agentExecutionId);
573
+ const result = await this.client.execute({ sql, args });
574
+ return result.rows.map((row) => ({
575
+ id: row.id,
576
+ userId: row.user_id,
577
+ resourceId: row.resource_id,
578
+ title: row.title,
579
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
580
+ createdAt: row.created_at,
581
+ updatedAt: row.updated_at
582
+ }));
583
+ }
584
+ /**
585
+ * Update a conversation
586
+ */
587
+ async updateConversation(id, updates) {
588
+ await this.initialize();
589
+ const conversationsTable = `${this.tablePrefix}_conversations`;
590
+ const conversation = await this.getConversation(id);
591
+ if (!conversation) {
592
+ throw new ConversationNotFoundError(id);
593
+ }
594
+ const now = (/* @__PURE__ */ new Date()).toISOString();
595
+ const fieldsToUpdate = ["updated_at = ?"];
596
+ const args = [now];
597
+ if (updates.title !== void 0) {
598
+ fieldsToUpdate.push("title = ?");
599
+ args.push(updates.title);
600
+ }
601
+ if (updates.resourceId !== void 0) {
602
+ fieldsToUpdate.push("resource_id = ?");
603
+ args.push(updates.resourceId);
454
604
  }
455
605
  if (updates.metadata !== void 0) {
456
- setClauses.push("metadata = ?");
457
- args.push(safeStringify(updates.metadata));
606
+ fieldsToUpdate.push("metadata = ?");
607
+ args.push(JSON.stringify(updates.metadata));
458
608
  }
459
- setClauses.push("updated_at = ?");
460
- args.push((/* @__PURE__ */ new Date()).toISOString());
461
609
  args.push(id);
462
610
  await this.client.execute({
463
- sql: `UPDATE ${this._tablePrefix}_workflow_steps SET ${setClauses.join(", ")} WHERE id = ?`,
611
+ sql: `UPDATE ${conversationsTable} SET ${fieldsToUpdate.join(", ")} WHERE id = ?`,
464
612
  args
465
613
  });
614
+ const updated = await this.getConversation(id);
615
+ if (!updated) {
616
+ throw new Error(`Conversation not found after update: ${id}`);
617
+ }
618
+ return updated;
466
619
  }
467
620
  /**
468
- * Delete a workflow step
469
- */
470
- async deleteWorkflowStep(id) {
471
- await this.client.execute({
472
- sql: `DELETE FROM ${this._tablePrefix}_workflow_steps WHERE id = ?`,
473
- args: [id]
474
- });
475
- }
476
- /**
477
- * Store a workflow timeline event
621
+ * Delete a conversation
478
622
  */
479
- async storeWorkflowTimelineEvent(event) {
623
+ async deleteConversation(id) {
624
+ await this.initialize();
625
+ const conversationsTable = `${this.tablePrefix}_conversations`;
480
626
  await this.client.execute({
481
- sql: `
482
- INSERT INTO ${this._tablePrefix}_workflow_timeline_events (
483
- id, workflow_history_id, event_id, name, type,
484
- start_time, end_time, status, level, input, output,
485
- status_message, metadata, trace_id, parent_event_id, event_sequence, created_at
486
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
487
- `,
488
- args: [
489
- event.id,
490
- event.workflowHistoryId,
491
- event.eventId,
492
- event.name,
493
- event.type,
494
- event.startTime,
495
- event.endTime || null,
496
- event.status,
497
- event.level || "INFO",
498
- event.input ? safeStringify(event.input) : null,
499
- event.output ? safeStringify(event.output) : null,
500
- event.statusMessage ? safeStringify(event.statusMessage) : null,
501
- event.metadata ? safeStringify(event.metadata) : null,
502
- event.traceId || null,
503
- event.parentEventId || null,
504
- event.eventSequence || null,
505
- // Event sequence for ordering
506
- event.createdAt.toISOString()
507
- ]
508
- });
509
- }
510
- /**
511
- * Get a workflow timeline event by ID
512
- */
513
- async getWorkflowTimelineEvent(id) {
514
- const result = await this.client.execute({
515
- sql: `SELECT * FROM ${this._tablePrefix}_workflow_timeline_events WHERE id = ?`,
627
+ sql: `DELETE FROM ${conversationsTable} WHERE id = ?`,
516
628
  args: [id]
517
629
  });
518
- if (result.rows.length === 0) return null;
519
- return this.parseWorkflowTimelineEventRow(result.rows[0]);
520
630
  }
631
+ // ============================================================================
632
+ // Working Memory Operations
633
+ // ============================================================================
521
634
  /**
522
- * Get all workflow timeline events for a specific workflow history
635
+ * Get working memory
523
636
  */
524
- async getWorkflowTimelineEvents(workflowHistoryId) {
525
- const result = await this.client.execute({
526
- sql: `SELECT * FROM ${this._tablePrefix}_workflow_timeline_events WHERE workflow_history_id = ? ORDER BY event_sequence ASC, start_time ASC`,
527
- args: [workflowHistoryId]
528
- });
529
- return result.rows.map((row) => this.parseWorkflowTimelineEventRow(row));
637
+ async getWorkingMemory(params) {
638
+ await this.initialize();
639
+ if (params.scope === "conversation" && params.conversationId) {
640
+ const conversation = await this.getConversation(params.conversationId);
641
+ return conversation?.metadata?.workingMemory || null;
642
+ }
643
+ if (params.scope === "user" && params.userId) {
644
+ const usersTable = `${this.tablePrefix}_users`;
645
+ const result = await this.client.execute({
646
+ sql: `SELECT metadata FROM ${usersTable} WHERE id = ?`,
647
+ args: [params.userId]
648
+ });
649
+ if (result.rows.length > 0) {
650
+ const metadata = result.rows[0].metadata ? JSON.parse(result.rows[0].metadata) : {};
651
+ return metadata.workingMemory || null;
652
+ }
653
+ }
654
+ return null;
530
655
  }
531
656
  /**
532
- * Delete a workflow timeline event
657
+ * Set working memory
533
658
  */
534
- async deleteWorkflowTimelineEvent(id) {
535
- await this.client.execute({
536
- sql: `DELETE FROM ${this._tablePrefix}_workflow_timeline_events WHERE id = ?`,
537
- args: [id]
538
- });
659
+ async setWorkingMemory(params) {
660
+ await this.initialize();
661
+ if (params.scope === "conversation" && params.conversationId) {
662
+ const conversation = await this.getConversation(params.conversationId);
663
+ if (!conversation) {
664
+ throw new ConversationNotFoundError(params.conversationId);
665
+ }
666
+ const metadata = conversation.metadata || {};
667
+ metadata.workingMemory = params.content;
668
+ await this.updateConversation(params.conversationId, { metadata });
669
+ }
670
+ if (params.scope === "user" && params.userId) {
671
+ const usersTable = `${this.tablePrefix}_users`;
672
+ const now = (/* @__PURE__ */ new Date()).toISOString();
673
+ const result = await this.client.execute({
674
+ sql: `SELECT metadata FROM ${usersTable} WHERE id = ?`,
675
+ args: [params.userId]
676
+ });
677
+ if (result.rows.length > 0) {
678
+ const metadata = result.rows[0].metadata ? JSON.parse(result.rows[0].metadata) : {};
679
+ metadata.workingMemory = params.content;
680
+ await this.client.execute({
681
+ sql: `UPDATE ${usersTable} SET metadata = ?, updated_at = ? WHERE id = ?`,
682
+ args: [JSON.stringify(metadata), now, params.userId]
683
+ });
684
+ } else {
685
+ await this.client.execute({
686
+ sql: `INSERT INTO ${usersTable} (id, metadata, created_at, updated_at) VALUES (?, ?, ?, ?)`,
687
+ args: [params.userId, JSON.stringify({ workingMemory: params.content }), now, now]
688
+ });
689
+ }
690
+ }
539
691
  }
540
692
  /**
541
- * Get all workflow IDs
693
+ * Delete working memory
542
694
  */
543
- async getAllWorkflowIds() {
544
- const result = await this.client.execute({
545
- sql: `SELECT DISTINCT workflow_id FROM ${this._tablePrefix}_workflow_history`,
546
- args: []
547
- });
548
- return result.rows.map((row) => row.workflow_id);
695
+ async deleteWorkingMemory(params) {
696
+ await this.initialize();
697
+ if (params.scope === "conversation" && params.conversationId) {
698
+ const conversation = await this.getConversation(params.conversationId);
699
+ if (conversation?.metadata?.workingMemory) {
700
+ const metadata = { ...conversation.metadata };
701
+ delete metadata.workingMemory;
702
+ await this.updateConversation(params.conversationId, { metadata });
703
+ }
704
+ }
705
+ if (params.scope === "user" && params.userId) {
706
+ const usersTable = `${this.tablePrefix}_users`;
707
+ const result = await this.client.execute({
708
+ sql: `SELECT metadata FROM ${usersTable} WHERE id = ?`,
709
+ args: [params.userId]
710
+ });
711
+ if (result.rows.length > 0 && result.rows[0].metadata) {
712
+ const metadata = JSON.parse(result.rows[0].metadata);
713
+ if (metadata.workingMemory) {
714
+ delete metadata.workingMemory;
715
+ await this.client.execute({
716
+ sql: `UPDATE ${usersTable} SET metadata = ?, updated_at = ? WHERE id = ?`,
717
+ args: [JSON.stringify(metadata), (/* @__PURE__ */ new Date()).toISOString(), params.userId]
718
+ });
719
+ }
720
+ }
721
+ }
549
722
  }
723
+ // ============================================================================
724
+ // Workflow State Operations
725
+ // ============================================================================
550
726
  /**
551
- * Get workflow statistics
727
+ * Get workflow state by execution ID
552
728
  */
553
- async getWorkflowStats(workflowId) {
729
+ async getWorkflowState(executionId) {
730
+ await this.initialize();
731
+ const workflowStatesTable = `${this.tablePrefix}_workflow_states`;
554
732
  const result = await this.client.execute({
555
- sql: `
556
- SELECT
557
- COUNT(*) as total_executions,
558
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful_executions,
559
- SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as failed_executions,
560
- AVG(CASE WHEN end_time IS NOT NULL THEN
561
- (julianday(end_time) - julianday(start_time)) * 24 * 60 * 60 * 1000
562
- ELSE NULL END) as avg_duration_ms,
563
- MAX(start_time) as last_execution_time
564
- FROM ${this._tablePrefix}_workflow_history
565
- WHERE workflow_id = ?
566
- `,
567
- args: [workflowId]
733
+ sql: `SELECT * FROM ${workflowStatesTable} WHERE id = ?`,
734
+ args: [executionId]
568
735
  });
569
736
  if (result.rows.length === 0) {
570
- return {
571
- totalExecutions: 0,
572
- successfulExecutions: 0,
573
- failedExecutions: 0,
574
- averageExecutionTime: 0,
575
- lastExecutionTime: void 0
576
- };
737
+ return null;
577
738
  }
578
739
  const row = result.rows[0];
579
740
  return {
580
- totalExecutions: Number(row.total_executions) || 0,
581
- successfulExecutions: Number(row.successful_executions) || 0,
582
- failedExecutions: Number(row.failed_executions) || 0,
583
- averageExecutionTime: Number(row.avg_duration_ms) || 0,
584
- lastExecutionTime: row.last_execution_time ? new Date(row.last_execution_time) : void 0
585
- };
586
- }
741
+ id: row.id,
742
+ workflowId: row.workflow_id,
743
+ workflowName: row.workflow_name,
744
+ status: row.status,
745
+ suspension: row.suspension ? JSON.parse(row.suspension) : void 0,
746
+ userId: row.user_id,
747
+ conversationId: row.conversation_id,
748
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
749
+ createdAt: new Date(row.created_at),
750
+ updatedAt: new Date(row.updated_at)
751
+ };
752
+ }
587
753
  /**
588
- * Get workflow history with all related data (steps and events)
754
+ * Set workflow state
589
755
  */
590
- async getWorkflowHistoryWithStepsAndEvents(id) {
591
- const history = await this.getWorkflowHistory(id);
592
- if (!history) return null;
593
- const [steps, events] = await Promise.all([
594
- this.getWorkflowSteps(id),
595
- this.getWorkflowTimelineEvents(id)
596
- ]);
597
- history.steps = steps;
598
- history.events = events;
599
- return history;
756
+ async setWorkflowState(executionId, state) {
757
+ await this.initialize();
758
+ const workflowStatesTable = `${this.tablePrefix}_workflow_states`;
759
+ await this.client.execute({
760
+ sql: `INSERT OR REPLACE INTO ${workflowStatesTable}
761
+ (id, workflow_id, workflow_name, status, suspension, user_id, conversation_id, metadata, created_at, updated_at)
762
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
763
+ args: [
764
+ executionId,
765
+ state.workflowId,
766
+ state.workflowName,
767
+ state.status,
768
+ state.suspension ? JSON.stringify(state.suspension) : null,
769
+ state.userId || null,
770
+ state.conversationId || null,
771
+ state.metadata ? JSON.stringify(state.metadata) : null,
772
+ state.createdAt.toISOString(),
773
+ state.updatedAt.toISOString()
774
+ ]
775
+ });
600
776
  }
601
777
  /**
602
- * Delete workflow history and all related data
778
+ * Update workflow state
603
779
  */
604
- async deleteWorkflowHistoryWithRelated(id) {
605
- await this.deleteWorkflowHistory(id);
780
+ async updateWorkflowState(executionId, updates) {
781
+ await this.initialize();
782
+ const existing = await this.getWorkflowState(executionId);
783
+ if (!existing) {
784
+ throw new Error(`Workflow state ${executionId} not found`);
785
+ }
786
+ const updated = {
787
+ ...existing,
788
+ ...updates,
789
+ updatedAt: /* @__PURE__ */ new Date()
790
+ };
791
+ await this.setWorkflowState(executionId, updated);
606
792
  }
607
793
  /**
608
- * Clean up old workflow histories
794
+ * Get suspended workflow states for a workflow
609
795
  */
610
- async cleanupOldWorkflowHistories(workflowId, maxEntries) {
611
- const countResult = await this.client.execute({
612
- sql: `SELECT COUNT(*) as count FROM ${this._tablePrefix}_workflow_history WHERE workflow_id = ?`,
796
+ async getSuspendedWorkflowStates(workflowId) {
797
+ await this.initialize();
798
+ const workflowStatesTable = `${this.tablePrefix}_workflow_states`;
799
+ const result = await this.client.execute({
800
+ sql: `SELECT * FROM ${workflowStatesTable} WHERE workflow_id = ? AND status = 'suspended' ORDER BY created_at DESC`,
613
801
  args: [workflowId]
614
802
  });
615
- const currentCount = Number(countResult.rows[0].count);
616
- if (currentCount <= maxEntries) return 0;
617
- const deleteCount = currentCount - maxEntries;
618
- const deleteResult = await this.client.execute({
619
- sql: `
620
- DELETE FROM ${this._tablePrefix}_workflow_history
621
- WHERE workflow_id = ?
622
- AND id IN (
623
- SELECT id FROM ${this._tablePrefix}_workflow_history
624
- WHERE workflow_id = ?
625
- ORDER BY start_time ASC
626
- LIMIT ?
627
- )
628
- `,
629
- args: [workflowId, workflowId, deleteCount]
630
- });
631
- return deleteResult.rowsAffected;
632
- }
633
- /**
634
- * Parse workflow history row from database
635
- */
636
- parseWorkflowHistoryRow(row) {
637
- return {
803
+ return result.rows.map((row) => ({
638
804
  id: row.id,
639
- workflowName: row.name,
640
805
  workflowId: row.workflow_id,
641
- status: row.status,
642
- startTime: new Date(row.start_time),
643
- endTime: row.end_time ? new Date(row.end_time) : void 0,
644
- input: row.input ? JSON.parse(row.input) : null,
645
- output: row.output ? JSON.parse(row.output) : void 0,
806
+ workflowName: row.workflow_name,
807
+ status: "suspended",
808
+ suspension: row.suspension ? JSON.parse(row.suspension) : void 0,
646
809
  userId: row.user_id,
647
810
  conversationId: row.conversation_id,
648
811
  metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
649
- steps: [],
650
- // Will be loaded separately if needed
651
- events: [],
652
- // Will be loaded separately if needed
653
- createdAt: new Date(row.created_at),
654
- updatedAt: new Date(row.updated_at)
655
- };
656
- }
657
- /**
658
- * Parse workflow step row from database
659
- */
660
- parseWorkflowStepRow(row) {
661
- return {
662
- id: row.id,
663
- workflowHistoryId: row.workflow_history_id,
664
- stepIndex: Number(row.step_index),
665
- stepType: row.step_type,
666
- stepName: row.step_name,
667
- stepId: row.step_id || void 0,
668
- status: row.status,
669
- startTime: new Date(row.start_time),
670
- endTime: row.end_time ? new Date(row.end_time) : void 0,
671
- input: row.input ? JSON.parse(row.input) : void 0,
672
- output: row.output ? JSON.parse(row.output) : void 0,
673
- error: row.error_message ? JSON.parse(row.error_message) : void 0,
674
- agentExecutionId: row.agent_execution_id || void 0,
675
- parallelIndex: row.parallel_index ? Number(row.parallel_index) : void 0,
676
- parallelParentStepId: row.parent_step_id || void 0,
677
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
678
812
  createdAt: new Date(row.created_at),
679
813
  updatedAt: new Date(row.updated_at)
680
- };
814
+ }));
681
815
  }
682
816
  /**
683
- * Parse workflow timeline event row from database
817
+ * Close database connection
684
818
  */
685
- parseWorkflowTimelineEventRow(row) {
686
- return {
687
- id: row.id,
688
- workflowHistoryId: row.workflow_history_id,
689
- eventId: row.event_id,
690
- name: row.name,
691
- type: row.type,
692
- startTime: row.start_time,
693
- endTime: row.end_time ? row.end_time : void 0,
694
- status: row.status,
695
- level: row.level || void 0,
696
- input: row.input ? JSON.parse(row.input) : void 0,
697
- output: row.output ? JSON.parse(row.output) : void 0,
698
- statusMessage: row.status_message ? JSON.parse(row.status_message) : void 0,
699
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
700
- traceId: row.trace_id || void 0,
701
- parentEventId: row.parent_event_id || void 0,
702
- eventSequence: Number(row.event_sequence),
703
- createdAt: new Date(row.created_at)
704
- };
819
+ async close() {
820
+ this.logger.debug("Closing LibSQL Memory adapter");
705
821
  }
706
822
  };
707
823
 
708
- // src/index.ts
709
- async function debugDelay() {
710
- const min = 0;
711
- const max = 0;
712
- const delay = Math.floor(Math.random() * (max - min + 1)) + min;
713
- return new Promise((resolve) => setTimeout(resolve, delay));
714
- }
715
- __name(debugDelay, "debugDelay");
716
- var LibSQLStorage = class {
824
+ // src/observability-adapter.ts
825
+ import { existsSync, mkdirSync } from "fs";
826
+ import { dirname } from "path";
827
+ import { createClient as createClient2 } from "@libsql/client";
828
+ import { safeStringify } from "@voltagent/internal/utils";
829
+ import { createPinoLogger as createPinoLogger2 } from "@voltagent/logger";
830
+ var LibSQLObservabilityAdapter = class {
717
831
  static {
718
- __name(this, "LibSQLStorage");
832
+ __name(this, "LibSQLObservabilityAdapter");
719
833
  }
720
834
  client;
721
- options;
722
- initialized;
723
- workflowExtension;
835
+ tablePrefix;
836
+ debug;
724
837
  logger;
725
- retryAttempts;
726
- baseDelayMs;
727
- /**
728
- * Create a new LibSQL storage
729
- * @param options Configuration options
730
- */
731
- constructor(options) {
732
- this.logger = options.logger || createPinoLogger2({ name: "libsql-storage" });
733
- this.retryAttempts = options.retryAttempts ?? 3;
734
- this.baseDelayMs = options.baseDelayMs ?? 50;
735
- this.options = {
736
- storageLimit: options.storageLimit || 100,
737
- tablePrefix: options.tablePrefix || "voltagent_memory",
738
- debug: options.debug || false,
739
- url: options.url || "file:./.voltagent/memory.db",
740
- authToken: options.authToken,
741
- retryAttempts: this.retryAttempts,
742
- baseDelayMs: this.baseDelayMs
743
- };
744
- if (this.options.url.startsWith("file:") && !this.options.url.includes(":memory:")) {
745
- const filePath = this.options.url.substring(5);
838
+ initialized;
839
+ maxSpansPerQuery;
840
+ constructor(options = {}) {
841
+ this.logger = options.logger || createPinoLogger2({ name: "libsql-observability" });
842
+ this.tablePrefix = options.tablePrefix || "observability";
843
+ this.debug = options.debug || false;
844
+ this.maxSpansPerQuery = options.maxSpansPerQuery || 1e3;
845
+ const url = options.url || "file:./.voltagent/observability.db";
846
+ if (url.startsWith("file:") && !url.includes(":memory:")) {
847
+ const filePath = url.substring(5);
746
848
  const dir = dirname(filePath);
747
849
  if (dir && dir !== "." && !existsSync(dir)) {
748
850
  try {
749
851
  mkdirSync(dir, { recursive: true });
750
- this.debug("Created directory for database", { dir });
852
+ this.debugLog("Created directory for database", { dir });
751
853
  } catch (error) {
752
854
  this.logger.warn("Failed to create directory for database", { dir, error });
753
855
  }
754
856
  }
755
857
  }
756
- this.client = createClient({
757
- url: this.options.url,
758
- authToken: this.options.authToken
858
+ this.client = createClient2({
859
+ url,
860
+ authToken: options.authToken
861
+ });
862
+ this.debugLog("LibSQL observability adapter initialized with options", {
863
+ url,
864
+ tablePrefix: this.tablePrefix,
865
+ debug: this.debug,
866
+ maxSpansPerQuery: this.maxSpansPerQuery
759
867
  });
760
- this.debug("LibSQL storage provider initialized with options", this.options);
761
- this.workflowExtension = new LibSQLWorkflowExtension(
762
- this.client,
763
- this.options.tablePrefix,
764
- this.logger
765
- );
766
868
  this.initialized = this.initializeDatabase();
767
869
  }
768
870
  /**
769
871
  * Log a debug message if debug is enabled
770
- * @param message Message to log
771
- * @param data Additional data to log
772
872
  */
773
- debug(message, data) {
774
- if (this.options?.debug) {
873
+ debugLog(message, data) {
874
+ if (this.debug) {
775
875
  this.logger.debug(`${message}`, data || "");
776
876
  }
777
877
  }
778
878
  /**
779
- * Calculate delay with jitter for better load distribution
780
- * @param attempt Current retry attempt number
781
- * @returns Delay in milliseconds
782
- */
783
- calculateRetryDelay(attempt) {
784
- const exponentialDelay = this.baseDelayMs * 2 ** (attempt - 1);
785
- const jitterFactor = 0.2 + Math.random() * 0.2;
786
- const delayWithJitter = exponentialDelay * (1 + jitterFactor);
787
- return Math.min(delayWithJitter, 2e3);
788
- }
789
- /**
790
- * Execute a database operation with retry strategy
791
- * Implements jittered exponential backoff
792
- * @param operationFn The operation function to execute
793
- * @param operationName Operation name for logging
794
- * @returns The result of the operation
795
- */
796
- async executeWithRetryStrategy(operationFn, operationName) {
797
- let attempt = 0;
798
- while (attempt < this.retryAttempts) {
799
- attempt++;
800
- try {
801
- return await operationFn();
802
- } catch (error) {
803
- const isBusyError = error.message && (error.message.includes("SQLITE_BUSY") || error.message.includes("database is locked") || error.code === "SQLITE_BUSY");
804
- if (!isBusyError || attempt >= this.retryAttempts) {
805
- this.debug(`Operation failed: ${operationName}`, {
806
- attempt,
807
- error: error.message
808
- });
809
- throw error;
810
- }
811
- const delay = this.calculateRetryDelay(attempt);
812
- this.debug(`Retrying ${operationName}`, {
813
- attempt,
814
- remainingAttempts: this.retryAttempts - attempt,
815
- delay
816
- });
817
- await new Promise((resolve) => setTimeout(resolve, delay));
818
- }
819
- }
820
- throw new Error(`Max retry attempts (${this.retryAttempts}) exceeded for ${operationName}`);
821
- }
822
- /**
823
- * Initialize workflow tables
824
- */
825
- async initializeWorkflowTables() {
826
- try {
827
- await createWorkflowTables(this.client, this.options.tablePrefix);
828
- this.debug("Workflow tables initialized successfully");
829
- await addSuspendedStatusMigration(this.client, this.options.tablePrefix);
830
- this.debug("Workflow migrations applied successfully");
831
- } catch (error) {
832
- this.debug("Error initializing workflow tables:", error);
833
- }
834
- }
835
- /**
836
- * Initialize the database tables
837
- * @returns Promise that resolves when initialization is complete
879
+ * Initialize database tables for observability
838
880
  */
839
881
  async initializeDatabase() {
840
- if (this.options.url.startsWith("file:") || this.options.url.includes(":memory:")) {
841
- try {
842
- await this.client.execute("PRAGMA journal_mode=WAL;");
843
- this.debug("PRAGMA journal_mode=WAL set.");
844
- } catch (err) {
845
- this.debug("Failed to set PRAGMA journal_mode=WAL.", err);
846
- }
847
- try {
848
- await this.client.execute("PRAGMA busy_timeout = 5000;");
849
- this.debug("PRAGMA busy_timeout=5000 set.");
850
- } catch (err) {
851
- this.debug("Failed to set PRAGMA busy_timeout.", err);
852
- }
853
- }
854
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
855
- await this.client.execute(`
856
- CREATE TABLE IF NOT EXISTS ${conversationsTableName} (
857
- id TEXT PRIMARY KEY,
858
- resource_id TEXT NOT NULL,
859
- user_id TEXT NOT NULL,
860
- title TEXT NOT NULL,
861
- metadata TEXT NOT NULL,
862
- created_at TEXT NOT NULL,
863
- updated_at TEXT NOT NULL
882
+ try {
883
+ await this.client.execute(`
884
+ CREATE TABLE IF NOT EXISTS ${this.tablePrefix}_spans (
885
+ span_id TEXT PRIMARY KEY,
886
+ trace_id TEXT NOT NULL,
887
+ parent_span_id TEXT,
888
+ entity_id TEXT,
889
+ entity_type TEXT,
890
+ name TEXT NOT NULL,
891
+ kind INTEGER DEFAULT 0,
892
+ start_time TEXT NOT NULL,
893
+ end_time TEXT,
894
+ duration REAL,
895
+ status_code INTEGER DEFAULT 0,
896
+ status_message TEXT,
897
+ attributes TEXT,
898
+ events TEXT,
899
+ links TEXT,
900
+ resource TEXT,
901
+ instrumentation_scope TEXT,
902
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
903
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
864
904
  )
865
905
  `);
866
- const messagesTableName = `${this.options.tablePrefix}_messages`;
867
- await this.client.execute(`
868
- CREATE TABLE IF NOT EXISTS ${messagesTableName} (
869
- conversation_id TEXT NOT NULL,
870
- message_id TEXT NOT NULL,
871
- role TEXT NOT NULL,
872
- content TEXT NOT NULL,
873
- type TEXT NOT NULL,
874
- created_at TEXT NOT NULL,
875
- PRIMARY KEY (conversation_id, message_id)
876
- )
906
+ await this.client.execute(`
907
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_spans_trace_id
908
+ ON ${this.tablePrefix}_spans(trace_id)
877
909
  `);
878
- const historyTableName = `${this.options.tablePrefix}_agent_history`;
879
- await this.client.execute(`
880
- CREATE TABLE IF NOT EXISTS ${historyTableName} (
881
- id TEXT PRIMARY KEY,
882
- agent_id TEXT NOT NULL,
883
- timestamp TEXT NOT NULL,
884
- status TEXT,
885
- input TEXT,
886
- output TEXT,
887
- usage TEXT,
888
- metadata TEXT,
889
- userId TEXT,
890
- conversationId TEXT
891
- )
910
+ await this.client.execute(`
911
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_spans_parent_span_id
912
+ ON ${this.tablePrefix}_spans(parent_span_id)
892
913
  `);
893
- const historyStepsTableName = `${this.options.tablePrefix}_agent_history_steps`;
894
- await this.client.execute(`
895
- CREATE TABLE IF NOT EXISTS ${historyStepsTableName} (
896
- key TEXT PRIMARY KEY,
897
- value TEXT NOT NULL,
898
- history_id TEXT NOT NULL,
899
- agent_id TEXT
900
- )
914
+ await this.client.execute(`
915
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_spans_start_time
916
+ ON ${this.tablePrefix}_spans(start_time)
901
917
  `);
902
- const timelineEventsTableName = `${this.options.tablePrefix}_agent_history_timeline_events`;
903
- await this.client.execute(`
904
- CREATE TABLE IF NOT EXISTS ${timelineEventsTableName} (
905
- id TEXT PRIMARY KEY,
906
- history_id TEXT NOT NULL,
907
- agent_id TEXT,
908
- event_type TEXT NOT NULL,
909
- event_name TEXT NOT NULL,
910
- start_time TEXT NOT NULL,
911
- end_time TEXT,
912
- status TEXT,
913
- status_message TEXT,
914
- level TEXT,
915
- version TEXT,
916
- parent_event_id TEXT,
917
- tags TEXT,
918
- input TEXT,
919
- output TEXT,
920
- error TEXT,
921
- metadata TEXT
922
- )
918
+ await this.client.execute(`
919
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_spans_name
920
+ ON ${this.tablePrefix}_spans(name)
923
921
  `);
924
- await this.client.execute(`
925
- CREATE INDEX IF NOT EXISTS idx_${messagesTableName}_lookup
926
- ON ${messagesTableName}(conversation_id, created_at)
922
+ await this.client.execute(`
923
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_spans_entity_id
924
+ ON ${this.tablePrefix}_spans(entity_id)
927
925
  `);
928
- await this.client.execute(`
929
- CREATE INDEX IF NOT EXISTS idx_${conversationsTableName}_resource
930
- ON ${conversationsTableName}(resource_id)
926
+ await this.client.execute(`
927
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_spans_entity_type
928
+ ON ${this.tablePrefix}_spans(entity_type)
931
929
  `);
932
- try {
933
- const tableInfo = await this.client.execute(`PRAGMA table_info(${conversationsTableName})`);
934
- const hasUserIdColumn = tableInfo.rows.some((row) => row.name === "user_id");
935
- if (hasUserIdColumn) {
936
- await this.client.execute(`
937
- CREATE INDEX IF NOT EXISTS idx_${conversationsTableName}_user
938
- ON ${conversationsTableName}(user_id)
939
- `);
940
- }
941
- } catch (error) {
942
- this.debug("Error creating user_id index, will be created after migration:", error);
943
- }
944
- await this.client.execute(`
945
- CREATE INDEX IF NOT EXISTS idx_${historyStepsTableName}_history_id
946
- ON ${historyStepsTableName}(history_id)
930
+ await this.client.execute(`
931
+ CREATE TABLE IF NOT EXISTS ${this.tablePrefix}_traces (
932
+ trace_id TEXT PRIMARY KEY,
933
+ root_span_id TEXT,
934
+ entity_id TEXT,
935
+ entity_type TEXT,
936
+ start_time TEXT NOT NULL,
937
+ end_time TEXT,
938
+ span_count INTEGER DEFAULT 1,
939
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
940
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
941
+ )
947
942
  `);
948
- await this.initializeWorkflowTables();
949
- await this.client.execute(`
950
- CREATE INDEX IF NOT EXISTS idx_${historyTableName}_agent_id
951
- ON ${historyTableName}(agent_id)
943
+ await this.client.execute(`
944
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_traces_start_time
945
+ ON ${this.tablePrefix}_traces(start_time DESC)
952
946
  `);
953
- await this.client.execute(`
954
- CREATE INDEX IF NOT EXISTS idx_${historyStepsTableName}_agent_id
955
- ON ${historyStepsTableName}(agent_id)
947
+ await this.client.execute(`
948
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_traces_entity_id
949
+ ON ${this.tablePrefix}_traces(entity_id)
956
950
  `);
957
- await this.client.execute(`
958
- CREATE INDEX IF NOT EXISTS idx_${timelineEventsTableName}_history_id
959
- ON ${timelineEventsTableName}(history_id)
951
+ await this.client.execute(`
952
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_traces_entity_type
953
+ ON ${this.tablePrefix}_traces(entity_type)
960
954
  `);
961
- await this.client.execute(`
962
- CREATE INDEX IF NOT EXISTS idx_${timelineEventsTableName}_agent_id
963
- ON ${timelineEventsTableName}(agent_id)
955
+ await this.client.execute(`
956
+ CREATE TABLE IF NOT EXISTS ${this.tablePrefix}_logs (
957
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
958
+ timestamp TEXT NOT NULL,
959
+ trace_id TEXT,
960
+ span_id TEXT,
961
+ trace_flags INTEGER,
962
+ severity_number INTEGER,
963
+ severity_text TEXT,
964
+ body TEXT NOT NULL,
965
+ attributes TEXT,
966
+ resource TEXT,
967
+ instrumentation_scope TEXT,
968
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
969
+ )
964
970
  `);
965
- await this.client.execute(`
966
- CREATE INDEX IF NOT EXISTS idx_${timelineEventsTableName}_event_type
967
- ON ${timelineEventsTableName}(event_type)
971
+ await this.client.execute(`
972
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_logs_trace_id
973
+ ON ${this.tablePrefix}_logs(trace_id)
968
974
  `);
969
- await this.client.execute(`
970
- CREATE INDEX IF NOT EXISTS idx_${timelineEventsTableName}_event_name
971
- ON ${timelineEventsTableName}(event_name)
975
+ await this.client.execute(`
976
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_logs_span_id
977
+ ON ${this.tablePrefix}_logs(span_id)
972
978
  `);
973
- await this.client.execute(`
974
- CREATE INDEX IF NOT EXISTS idx_${timelineEventsTableName}_parent_event_id
975
- ON ${timelineEventsTableName}(parent_event_id)
979
+ await this.client.execute(`
980
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_logs_timestamp
981
+ ON ${this.tablePrefix}_logs(timestamp DESC)
976
982
  `);
977
- await this.client.execute(`
978
- CREATE INDEX IF NOT EXISTS idx_${timelineEventsTableName}_status
979
- ON ${timelineEventsTableName}(status)
983
+ await this.client.execute(`
984
+ CREATE INDEX IF NOT EXISTS idx_${this.tablePrefix}_logs_severity
985
+ ON ${this.tablePrefix}_logs(severity_number)
980
986
  `);
981
- this.debug("Database initialized successfully");
982
- try {
983
- const migrationResult = await this.migrateConversationSchema({
984
- createBackup: true,
985
- deleteBackupAfterSuccess: true
986
- });
987
- if (migrationResult.success) {
988
- if ((migrationResult.migratedCount || 0) > 0) {
989
- this.logger.info(
990
- `${migrationResult.migratedCount} conversation records successfully migrated`
991
- );
992
- }
993
- } else {
994
- this.logger.error("Conversation migration error:", migrationResult.error);
995
- }
996
- } catch (error) {
997
- this.debug("Error migrating conversation schema:", error);
998
- }
999
- try {
1000
- const migrationResult = await this.migrateAgentHistorySchema();
1001
- if (!migrationResult.success) {
1002
- this.logger.error("Agent history schema migration error:", migrationResult.error);
1003
- }
987
+ this.debugLog("Database tables initialized successfully");
1004
988
  } catch (error) {
1005
- this.debug("Error migrating agent history schema:", error);
989
+ this.logger.error("Failed to initialize database tables", { error });
990
+ throw error;
1006
991
  }
992
+ }
993
+ /**
994
+ * Ensure database is initialized before operations
995
+ */
996
+ async ensureInitialized() {
997
+ await this.initialized;
998
+ }
999
+ /**
1000
+ * Add a span to the database
1001
+ */
1002
+ async addSpan(span) {
1003
+ await this.ensureInitialized();
1007
1004
  try {
1008
- const result = await this.migrateAgentHistoryData({
1009
- restoreFromBackup: false
1010
- });
1011
- if (result.success) {
1012
- if ((result.migratedCount || 0) > 0) {
1013
- this.logger.info(`${result.migratedCount} records successfully migrated`);
1014
- }
1015
- } else {
1016
- this.logger.error("Migration error:", result.error);
1017
- const restoreResult = await this.migrateAgentHistoryData({});
1018
- if (restoreResult.success) {
1019
- this.logger.info("Successfully restored from backup");
1005
+ const entityId = span.attributes?.["entity.id"] || null;
1006
+ const entityType = span.attributes?.["entity.type"] || null;
1007
+ await this.client.batch([
1008
+ // Insert the span with entity columns
1009
+ {
1010
+ sql: `
1011
+ INSERT INTO ${this.tablePrefix}_spans (
1012
+ span_id, trace_id, parent_span_id, entity_id, entity_type, name, kind,
1013
+ start_time, end_time, duration,
1014
+ status_code, status_message,
1015
+ attributes, events, links,
1016
+ resource, instrumentation_scope
1017
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1018
+ `,
1019
+ args: [
1020
+ span.spanId,
1021
+ span.traceId,
1022
+ span.parentSpanId || null,
1023
+ entityId,
1024
+ entityType,
1025
+ span.name,
1026
+ span.kind,
1027
+ span.startTime,
1028
+ span.endTime || null,
1029
+ span.duration || null,
1030
+ span.status.code,
1031
+ span.status.message || null,
1032
+ safeStringify(span.attributes),
1033
+ safeStringify(span.events),
1034
+ span.links ? safeStringify(span.links) : null,
1035
+ span.resource ? safeStringify(span.resource) : null,
1036
+ span.instrumentationScope ? safeStringify(span.instrumentationScope) : null
1037
+ ]
1038
+ },
1039
+ // Update or insert trace metadata with entity columns
1040
+ {
1041
+ sql: `
1042
+ INSERT INTO ${this.tablePrefix}_traces (
1043
+ trace_id, root_span_id, entity_id, entity_type, start_time, end_time, span_count
1044
+ ) VALUES (?, ?, ?, ?, ?, ?, 1)
1045
+ ON CONFLICT(trace_id) DO UPDATE SET
1046
+ span_count = span_count + 1,
1047
+ entity_id = COALESCE(excluded.entity_id, entity_id),
1048
+ entity_type = COALESCE(excluded.entity_type, entity_type),
1049
+ start_time = MIN(start_time, excluded.start_time),
1050
+ end_time = MAX(COALESCE(end_time, excluded.end_time), excluded.end_time),
1051
+ updated_at = CURRENT_TIMESTAMP
1052
+ `,
1053
+ args: [
1054
+ span.traceId,
1055
+ span.parentSpanId ? null : span.spanId,
1056
+ // Root span if no parent
1057
+ entityId,
1058
+ entityType,
1059
+ span.startTime,
1060
+ span.endTime || null
1061
+ ]
1020
1062
  }
1021
- }
1063
+ ]);
1064
+ this.debugLog("Span added successfully", {
1065
+ spanId: span.spanId,
1066
+ traceId: span.traceId
1067
+ });
1022
1068
  } catch (error) {
1023
- this.debug("Error initializing database:", error);
1069
+ this.logger.error("Failed to add span", { error, span });
1070
+ throw error;
1024
1071
  }
1025
1072
  }
1026
1073
  /**
1027
- * Generate a unique ID for a message
1028
- * @returns Unique ID
1029
- */
1030
- generateId() {
1031
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
1032
- }
1033
- /**
1034
- * Get messages with filtering options
1035
- * @param options Filtering options
1036
- * @returns Filtered messages
1074
+ * Update an existing span
1037
1075
  */
1038
- async getMessages(options = {}) {
1039
- await this.initialized;
1040
- await debugDelay();
1041
- const {
1042
- userId = "default",
1043
- conversationId = "default",
1044
- limit,
1045
- before,
1046
- after,
1047
- role,
1048
- types
1049
- } = options;
1050
- const messagesTableName = `${this.options.tablePrefix}_messages`;
1051
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
1076
+ async updateSpan(spanId, updates) {
1077
+ await this.ensureInitialized();
1052
1078
  try {
1053
- let sql = `
1054
- SELECT m.message_id, m.role, m.content, m.type, m.created_at, m.conversation_id
1055
- FROM ${messagesTableName} m
1056
- `;
1079
+ const setClauses = [];
1057
1080
  const args = [];
1058
- const conditions = [];
1059
- if (userId !== "default") {
1060
- sql += ` INNER JOIN ${conversationsTableName} c ON m.conversation_id = c.id`;
1061
- conditions.push("c.user_id = ?");
1062
- args.push(userId);
1081
+ if (updates.endTime !== void 0) {
1082
+ setClauses.push("end_time = ?");
1083
+ args.push(updates.endTime);
1063
1084
  }
1064
- if (conversationId !== "default") {
1065
- conditions.push("m.conversation_id = ?");
1066
- args.push(conversationId);
1085
+ if (updates.duration !== void 0) {
1086
+ setClauses.push("duration = ?");
1087
+ args.push(updates.duration);
1067
1088
  }
1068
- if (before) {
1069
- conditions.push("m.created_at < ?");
1070
- args.push(new Date(before).toISOString());
1089
+ if (updates.status !== void 0) {
1090
+ setClauses.push("status_code = ?, status_message = ?");
1091
+ args.push(updates.status.code, updates.status.message || null);
1071
1092
  }
1072
- if (after) {
1073
- conditions.push("m.created_at > ?");
1074
- args.push(new Date(after).toISOString());
1093
+ if (updates.attributes !== void 0) {
1094
+ setClauses.push("attributes = ?");
1095
+ args.push(safeStringify(updates.attributes));
1075
1096
  }
1076
- if (role) {
1077
- conditions.push("m.role = ?");
1078
- args.push(role);
1097
+ if (updates.events !== void 0) {
1098
+ setClauses.push("events = ?");
1099
+ args.push(safeStringify(updates.events));
1079
1100
  }
1080
- if (types) {
1081
- const placeholders = types.map(() => "?").join(", ");
1082
- conditions.push(`m.type IN (${placeholders})`);
1083
- args.push(...types);
1084
- }
1085
- if (conditions.length > 0) {
1086
- sql += ` WHERE ${conditions.join(" AND ")}`;
1101
+ if (updates.links !== void 0) {
1102
+ setClauses.push("links = ?");
1103
+ args.push(safeStringify(updates.links));
1087
1104
  }
1088
- if (limit && limit > 0) {
1089
- sql += " ORDER BY m.created_at DESC LIMIT ?";
1090
- args.push(limit);
1091
- } else {
1092
- sql += " ORDER BY m.created_at ASC";
1105
+ if (setClauses.length === 0) {
1106
+ return;
1093
1107
  }
1094
- const result = await this.client.execute({
1095
- sql,
1108
+ setClauses.push("updated_at = CURRENT_TIMESTAMP");
1109
+ args.push(spanId);
1110
+ await this.client.execute({
1111
+ sql: `
1112
+ UPDATE ${this.tablePrefix}_spans
1113
+ SET ${setClauses.join(", ")}
1114
+ WHERE span_id = ?
1115
+ `,
1096
1116
  args
1097
1117
  });
1098
- const messages = result.rows.map((row) => {
1099
- let content = row.content;
1100
- const parsedContent = safeJsonParse(content);
1101
- if (parsedContent !== null) {
1102
- content = parsedContent;
1118
+ if (updates.endTime) {
1119
+ const span = await this.getSpan(spanId);
1120
+ if (span) {
1121
+ await this.client.execute({
1122
+ sql: `
1123
+ UPDATE ${this.tablePrefix}_traces
1124
+ SET end_time = MAX(COALESCE(end_time, ?), ?),
1125
+ updated_at = CURRENT_TIMESTAMP
1126
+ WHERE trace_id = ?
1127
+ `,
1128
+ args: [updates.endTime, updates.endTime, span.traceId]
1129
+ });
1103
1130
  }
1104
- return {
1105
- id: row.message_id,
1106
- role: row.role,
1107
- content,
1108
- type: row.type,
1109
- createdAt: row.created_at
1110
- };
1111
- });
1112
- if (limit && limit > 0) {
1113
- return messages.reverse();
1114
1131
  }
1115
- return messages;
1132
+ this.debugLog("Span updated successfully", { spanId, updates });
1116
1133
  } catch (error) {
1117
- this.debug("Error getting messages:", error);
1118
- throw new Error("Failed to get messages from LibSQL database");
1134
+ this.logger.error("Failed to update span", { error, spanId, updates });
1135
+ throw error;
1119
1136
  }
1120
1137
  }
1121
1138
  /**
1122
- * Add a message to the conversation history
1123
- * @param message Message to add
1124
- * @param userId User identifier (optional, defaults to "default")
1125
- * @param conversationId Conversation identifier (optional, defaults to "default")
1139
+ * Get a span by ID
1126
1140
  */
1127
- async addMessage(message, conversationId = "default") {
1128
- await this.initialized;
1129
- await debugDelay();
1130
- const tableName = `${this.options.tablePrefix}_messages`;
1131
- const contentString = safeStringify2(message.content);
1132
- await this.executeWithRetryStrategy(async () => {
1133
- await this.client.execute({
1134
- sql: `INSERT INTO ${tableName} (conversation_id, message_id, role, content, type, created_at)
1135
- VALUES (?, ?, ?, ?, ?, ?)`,
1136
- args: [
1137
- conversationId,
1138
- message.id,
1139
- message.role,
1140
- contentString,
1141
- message.type,
1142
- message.createdAt
1143
- ]
1141
+ async getSpan(spanId) {
1142
+ await this.ensureInitialized();
1143
+ try {
1144
+ const result = await this.client.execute({
1145
+ sql: `
1146
+ SELECT * FROM ${this.tablePrefix}_spans
1147
+ WHERE span_id = ?
1148
+ `,
1149
+ args: [spanId]
1144
1150
  });
1145
- this.debug("Message added successfully", { conversationId, messageId: message.id });
1146
- try {
1147
- await this.pruneOldMessages(conversationId);
1148
- } catch (pruneError) {
1149
- this.debug("Error pruning old messages:", pruneError);
1151
+ if (result.rows.length === 0) {
1152
+ return null;
1150
1153
  }
1151
- }, `addMessage[${message.id}]`);
1154
+ const row = result.rows[0];
1155
+ return this.rowToSpan(row);
1156
+ } catch (error) {
1157
+ this.logger.error("Failed to get span", { error, spanId });
1158
+ throw error;
1159
+ }
1152
1160
  }
1153
1161
  /**
1154
- * Prune old messages to respect storage limit
1155
- * @param conversationId Conversation ID to prune messages for
1162
+ * Get all spans in a trace
1156
1163
  */
1157
- async pruneOldMessages(conversationId) {
1158
- const limit = this.options.storageLimit || 100;
1159
- const tableName = `${this.options.tablePrefix}_messages`;
1164
+ async getTrace(traceId) {
1165
+ await this.ensureInitialized();
1160
1166
  try {
1161
- const countResult = await this.client.execute({
1162
- sql: `SELECT COUNT(*) as count FROM ${tableName} WHERE conversation_id = ?`,
1163
- args: [conversationId]
1167
+ const result = await this.client.execute({
1168
+ sql: `
1169
+ SELECT * FROM ${this.tablePrefix}_spans
1170
+ WHERE trace_id = ?
1171
+ ORDER BY start_time ASC
1172
+ LIMIT ?
1173
+ `,
1174
+ args: [traceId, this.maxSpansPerQuery]
1164
1175
  });
1165
- const messageCount = countResult.rows[0]?.count;
1166
- if (messageCount > limit) {
1167
- const deleteCount = messageCount - limit;
1168
- await this.client.execute({
1169
- sql: `DELETE FROM ${tableName}
1170
- WHERE conversation_id = ?
1171
- AND message_id IN (
1172
- SELECT message_id FROM ${tableName}
1173
- WHERE conversation_id = ?
1174
- ORDER BY created_at ASC
1175
- LIMIT ?
1176
- )`,
1177
- args: [conversationId, conversationId, deleteCount]
1178
- });
1179
- this.debug(`Pruned ${deleteCount} old messages for conversation ${conversationId}`);
1180
- }
1176
+ return result.rows.map((row) => this.rowToSpan(row));
1181
1177
  } catch (error) {
1182
- this.debug("Error pruning old messages:", error);
1178
+ this.logger.error("Failed to get trace", { error, traceId });
1183
1179
  throw error;
1184
1180
  }
1185
1181
  }
1186
1182
  /**
1187
- * Clear messages from memory
1183
+ * List all traces with optional entity filter
1188
1184
  */
1189
- async clearMessages(options) {
1190
- await this.initialized;
1191
- await debugDelay();
1192
- const { userId, conversationId } = options;
1193
- const messagesTableName = `${this.options.tablePrefix}_messages`;
1194
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
1185
+ async listTraces(limit = 100, offset = 0, filter) {
1186
+ await this.ensureInitialized();
1195
1187
  try {
1196
- if (conversationId) {
1197
- await this.client.execute({
1198
- sql: `DELETE FROM ${messagesTableName}
1199
- WHERE conversation_id = ?
1200
- AND conversation_id IN (
1201
- SELECT id FROM ${conversationsTableName} WHERE user_id = ?
1202
- )`,
1203
- args: [conversationId, userId]
1204
- });
1205
- this.debug(`Cleared messages for conversation ${conversationId} for user ${userId}`);
1188
+ let sql;
1189
+ let args = [];
1190
+ const conditions = [];
1191
+ if (filter?.entityId) {
1192
+ conditions.push("entity_id = ?");
1193
+ args.push(filter.entityId);
1194
+ }
1195
+ if (filter?.entityType) {
1196
+ conditions.push("entity_type = ?");
1197
+ args.push(filter.entityType);
1198
+ }
1199
+ if (conditions.length > 0) {
1200
+ sql = `
1201
+ SELECT trace_id FROM ${this.tablePrefix}_traces
1202
+ WHERE ${conditions.join(" AND ")}
1203
+ ORDER BY start_time DESC
1204
+ LIMIT ? OFFSET ?
1205
+ `;
1206
+ args.push(limit, offset);
1206
1207
  } else {
1207
- await this.client.execute({
1208
- sql: `DELETE FROM ${messagesTableName}
1209
- WHERE conversation_id IN (
1210
- SELECT id FROM ${conversationsTableName} WHERE user_id = ?
1211
- )`,
1212
- args: [userId]
1213
- });
1214
- this.debug(`Cleared all messages for user ${userId}`);
1208
+ sql = `
1209
+ SELECT trace_id FROM ${this.tablePrefix}_traces
1210
+ ORDER BY start_time DESC
1211
+ LIMIT ? OFFSET ?
1212
+ `;
1213
+ args = [limit, offset];
1215
1214
  }
1215
+ const result = await this.client.execute({ sql, args });
1216
+ return result.rows.map((row) => row.trace_id);
1216
1217
  } catch (error) {
1217
- this.debug("Error clearing messages:", error);
1218
- throw new Error("Failed to clear messages from LibSQL database");
1218
+ this.logger.error("Failed to list traces", { error, limit, offset, filter });
1219
+ throw error;
1219
1220
  }
1220
1221
  }
1221
1222
  /**
1222
- * Close the database connection
1223
+ * Delete old spans
1223
1224
  */
1224
- async close() {
1225
+ async deleteOldSpans(beforeTimestamp) {
1226
+ await this.ensureInitialized();
1225
1227
  try {
1226
- await this.initialized;
1227
- } catch {
1228
+ const beforeDate = new Date(beforeTimestamp).toISOString();
1229
+ const tracesResult = await this.client.execute({
1230
+ sql: `
1231
+ SELECT DISTINCT trace_id FROM ${this.tablePrefix}_spans
1232
+ WHERE start_time < ?
1233
+ `,
1234
+ args: [beforeDate]
1235
+ });
1236
+ const affectedTraceIds = tracesResult.rows.map((row) => row.trace_id);
1237
+ const deleteResult = await this.client.execute({
1238
+ sql: `
1239
+ DELETE FROM ${this.tablePrefix}_spans
1240
+ WHERE start_time < ?
1241
+ `,
1242
+ args: [beforeDate]
1243
+ });
1244
+ if (affectedTraceIds.length > 0) {
1245
+ for (const traceId of affectedTraceIds) {
1246
+ const countResult = await this.client.execute({
1247
+ sql: `
1248
+ SELECT COUNT(*) as count FROM ${this.tablePrefix}_spans
1249
+ WHERE trace_id = ?
1250
+ `,
1251
+ args: [traceId]
1252
+ });
1253
+ const count = countResult.rows[0].count;
1254
+ if (count === 0) {
1255
+ await this.client.execute({
1256
+ sql: `
1257
+ DELETE FROM ${this.tablePrefix}_traces
1258
+ WHERE trace_id = ?
1259
+ `,
1260
+ args: [traceId]
1261
+ });
1262
+ } else {
1263
+ await this.client.execute({
1264
+ sql: `
1265
+ UPDATE ${this.tablePrefix}_traces
1266
+ SET span_count = ?,
1267
+ updated_at = CURRENT_TIMESTAMP
1268
+ WHERE trace_id = ?
1269
+ `,
1270
+ args: [count, traceId]
1271
+ });
1272
+ }
1273
+ }
1274
+ }
1275
+ const deletedCount = deleteResult.rowsAffected || 0;
1276
+ this.debugLog("Old spans deleted", { deletedCount, beforeDate });
1277
+ return deletedCount;
1278
+ } catch (error) {
1279
+ this.logger.error("Failed to delete old spans", { error, beforeTimestamp });
1280
+ throw error;
1228
1281
  }
1229
- this.client.close();
1230
1282
  }
1231
1283
  /**
1232
- * Add or update a history entry
1233
- * @param key Entry ID
1234
- * @param value Entry data
1235
- * @param agentId Agent ID for filtering
1284
+ * Clear all spans, traces, and logs
1236
1285
  */
1237
- async addHistoryEntry(key, value, agentId) {
1238
- await this.initialized;
1286
+ async clear() {
1287
+ await this.ensureInitialized();
1239
1288
  try {
1240
- const tableName = `${this.options.tablePrefix}_agent_history`;
1241
- const inputJSON = value.input ? safeStringify2(value.input) : null;
1242
- const outputJSON = value.output ? safeStringify2(value.output) : null;
1243
- const usageJSON = value.usage ? safeStringify2(value.usage) : null;
1244
- const metadataJSON = value.metadata ? safeStringify2(value.metadata) : null;
1245
- await this.client.execute({
1246
- sql: `INSERT OR REPLACE INTO ${tableName}
1247
- (id, agent_id, timestamp, status, input, output, usage, metadata, userId, conversationId)
1248
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1249
- args: [
1250
- key,
1251
- // id
1252
- agentId,
1253
- // agent_id
1254
- value.timestamp ? value.timestamp.toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
1255
- // timestamp
1256
- value.status || null,
1257
- // status
1258
- inputJSON,
1259
- // input
1260
- outputJSON,
1261
- // output
1262
- usageJSON,
1263
- // usage
1264
- metadataJSON,
1265
- // metadata
1266
- value.userId || null,
1267
- // userId
1268
- value.conversationId || null
1269
- // conversationId
1270
- ]
1271
- });
1272
- this.debug(`Set agent_history entry with ID ${key} for agent ${agentId}`);
1289
+ await this.client.batch([
1290
+ { sql: `DELETE FROM ${this.tablePrefix}_spans`, args: [] },
1291
+ { sql: `DELETE FROM ${this.tablePrefix}_traces`, args: [] },
1292
+ { sql: `DELETE FROM ${this.tablePrefix}_logs`, args: [] }
1293
+ ]);
1294
+ this.debugLog("All spans, traces, and logs cleared");
1273
1295
  } catch (error) {
1274
- this.debug("Error setting agent_history entry:", error);
1275
- throw new Error("Failed to set value in agent_history");
1296
+ this.logger.error("Failed to clear data", { error });
1297
+ throw error;
1276
1298
  }
1277
1299
  }
1278
1300
  /**
1279
- * Update an existing history entry
1280
- * @param key Entry ID
1281
- * @param value Updated entry data
1282
- * @param agentId Agent ID for filtering
1301
+ * Convert a database row to an ObservabilitySpan
1283
1302
  */
1284
- async updateHistoryEntry(key, value, agentId) {
1285
- return this.addHistoryEntry(key, value, agentId);
1303
+ rowToSpan(row) {
1304
+ const span = {
1305
+ traceId: row.trace_id,
1306
+ spanId: row.span_id,
1307
+ name: row.name,
1308
+ kind: row.kind,
1309
+ startTime: row.start_time,
1310
+ status: {
1311
+ code: row.status_code
1312
+ },
1313
+ attributes: row.attributes ? JSON.parse(row.attributes) : {},
1314
+ events: row.events ? JSON.parse(row.events) : []
1315
+ };
1316
+ if (row.parent_span_id !== null) {
1317
+ span.parentSpanId = row.parent_span_id;
1318
+ }
1319
+ if (row.end_time !== null) {
1320
+ span.endTime = row.end_time;
1321
+ }
1322
+ if (row.duration !== null) {
1323
+ span.duration = row.duration;
1324
+ }
1325
+ if (row.status_message !== null) {
1326
+ span.status.message = row.status_message;
1327
+ }
1328
+ if (row.links && row.links !== "null") {
1329
+ const links = JSON.parse(row.links);
1330
+ if (links && links.length > 0) {
1331
+ span.links = links;
1332
+ }
1333
+ }
1334
+ if (row.resource && row.resource !== "null") {
1335
+ const resource = JSON.parse(row.resource);
1336
+ if (resource && Object.keys(resource).length > 0) {
1337
+ span.resource = resource;
1338
+ }
1339
+ }
1340
+ if (row.instrumentation_scope && row.instrumentation_scope !== "null") {
1341
+ const scope = JSON.parse(row.instrumentation_scope);
1342
+ if (scope) {
1343
+ span.instrumentationScope = scope;
1344
+ }
1345
+ }
1346
+ return span;
1286
1347
  }
1287
1348
  /**
1288
- * Add a history step
1289
- * @param key Step ID
1290
- * @param value Step data
1291
- * @param historyId Related history entry ID
1292
- * @param agentId Agent ID for filtering
1349
+ * Get statistics about stored spans
1293
1350
  */
1294
- async addHistoryStep(key, value, historyId, agentId) {
1295
- await this.initialized;
1351
+ async getStats() {
1352
+ await this.ensureInitialized();
1296
1353
  try {
1297
- const tableName = `${this.options.tablePrefix}_agent_history_steps`;
1298
- const serializedValue = safeStringify2(value);
1299
- await this.client.execute({
1300
- sql: `INSERT OR REPLACE INTO ${tableName} (key, value, history_id, agent_id) VALUES (?, ?, ?, ?)`,
1301
- args: [key, serializedValue, historyId, agentId]
1302
- });
1303
- this.debug(`Set agent_history_steps:${key} for history ${historyId} and agent ${agentId}`);
1354
+ const [spanCountResult, traceCountResult, timeRangeResult] = await Promise.all([
1355
+ this.client.execute(`SELECT COUNT(*) as count FROM ${this.tablePrefix}_spans`),
1356
+ this.client.execute(`SELECT COUNT(*) as count FROM ${this.tablePrefix}_traces`),
1357
+ this.client.execute(`
1358
+ SELECT
1359
+ MIN(start_time) as oldest,
1360
+ MAX(start_time) as newest
1361
+ FROM ${this.tablePrefix}_spans
1362
+ `)
1363
+ ]);
1364
+ const stats = {
1365
+ spanCount: spanCountResult.rows[0].count,
1366
+ traceCount: traceCountResult.rows[0].count
1367
+ };
1368
+ if (timeRangeResult.rows[0].oldest) {
1369
+ stats.oldestSpan = new Date(timeRangeResult.rows[0].oldest);
1370
+ }
1371
+ if (timeRangeResult.rows[0].newest) {
1372
+ stats.newestSpan = new Date(timeRangeResult.rows[0].newest);
1373
+ }
1374
+ return stats;
1304
1375
  } catch (error) {
1305
- this.debug(`Error setting agent_history_steps:${key}`, error);
1306
- throw new Error("Failed to set value in agent_history_steps");
1376
+ this.logger.error("Failed to get stats", { error });
1377
+ throw error;
1307
1378
  }
1308
1379
  }
1309
1380
  /**
1310
- * Update a history step
1311
- * @param key Step ID
1312
- * @param value Updated step data
1313
- * @param historyId Related history entry ID
1314
- * @param agentId Agent ID for filtering
1381
+ * Save a log record to the database
1315
1382
  */
1316
- async updateHistoryStep(key, value, historyId, agentId) {
1317
- return this.addHistoryStep(key, value, historyId, agentId);
1318
- }
1319
- /**
1320
- * Add a timeline event
1321
- * @param key Event ID (UUID)
1322
- * @param value Timeline event data
1323
- * @param historyId Related history entry ID
1324
- * @param agentId Agent ID for filtering
1325
- */
1326
- async addTimelineEvent(key, value, historyId, agentId) {
1327
- await this.initialized;
1383
+ async saveLogRecord(logRecord) {
1384
+ await this.ensureInitialized();
1328
1385
  try {
1329
- const tableName = `${this.options.tablePrefix}_agent_history_timeline_events`;
1330
- const inputJSON = value.input ? safeStringify2(value.input) : null;
1331
- const outputJSON = value.output ? safeStringify2(value.output) : null;
1332
- const statusMessageJSON = value.statusMessage ? safeStringify2(value.statusMessage) : null;
1333
- const metadataJSON = value.metadata ? safeStringify2(value.metadata) : null;
1334
- const tagsJSON = value.tags ? safeStringify2(value.tags) : null;
1386
+ let timestamp;
1387
+ if (Array.isArray(logRecord.hrTime)) {
1388
+ const timeMs = logRecord.hrTime[0] * 1e3 + logRecord.hrTime[1] / 1e6;
1389
+ timestamp = new Date(timeMs).toISOString();
1390
+ } else if (logRecord.timestamp) {
1391
+ timestamp = typeof logRecord.timestamp === "string" ? logRecord.timestamp : new Date(logRecord.timestamp).toISOString();
1392
+ } else {
1393
+ timestamp = (/* @__PURE__ */ new Date()).toISOString();
1394
+ }
1395
+ const spanContext = logRecord.spanContext || {};
1396
+ const traceId = spanContext.traceId || null;
1397
+ const spanId = spanContext.spanId || null;
1398
+ const traceFlags = spanContext.traceFlags ?? null;
1399
+ const severityNumber = logRecord.severityNumber ?? null;
1400
+ const severityText = logRecord.severityText || null;
1401
+ const body = typeof logRecord.body === "string" ? logRecord.body : safeStringify(logRecord.body);
1402
+ const attributes = logRecord.attributes ? safeStringify(logRecord.attributes) : null;
1403
+ const resource = logRecord.resource?.attributes ? safeStringify(logRecord.resource.attributes) : null;
1404
+ const instrumentationScope = logRecord.instrumentationLibrary || logRecord.instrumentationScope ? safeStringify(logRecord.instrumentationLibrary || logRecord.instrumentationScope) : null;
1335
1405
  await this.client.execute({
1336
- sql: `INSERT OR REPLACE INTO ${tableName}
1337
- (id, history_id, agent_id, event_type, event_name,
1338
- start_time, end_time, status, status_message, level,
1339
- version, parent_event_id, tags,
1340
- input, output, error, metadata)
1341
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1406
+ sql: `
1407
+ INSERT INTO ${this.tablePrefix}_logs (
1408
+ timestamp, trace_id, span_id, trace_flags,
1409
+ severity_number, severity_text, body,
1410
+ attributes, resource, instrumentation_scope
1411
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1412
+ `,
1342
1413
  args: [
1343
- key,
1344
- historyId,
1345
- agentId,
1346
- value.type,
1347
- value.name,
1348
- value.startTime,
1349
- value.endTime || null,
1350
- value.status || null,
1351
- statusMessageJSON || null,
1352
- value.level || "INFO",
1353
- value.version || null,
1354
- value.parentEventId || null,
1355
- tagsJSON,
1356
- inputJSON,
1357
- outputJSON,
1358
- statusMessageJSON,
1359
- metadataJSON
1414
+ timestamp,
1415
+ traceId,
1416
+ spanId,
1417
+ traceFlags,
1418
+ severityNumber,
1419
+ severityText,
1420
+ body,
1421
+ attributes,
1422
+ resource,
1423
+ instrumentationScope
1360
1424
  ]
1361
1425
  });
1362
- this.debug(`Added timeline event ${key} for history ${historyId}`);
1426
+ this.debugLog("Log record saved successfully", {
1427
+ timestamp,
1428
+ traceId,
1429
+ spanId,
1430
+ severityNumber
1431
+ });
1363
1432
  } catch (error) {
1364
- this.debug("Error adding timeline event:", error);
1365
- throw new Error("Failed to add timeline event");
1433
+ this.logger.error("Failed to save log record", { error, logRecord });
1434
+ throw error;
1366
1435
  }
1367
1436
  }
1368
1437
  /**
1369
- * Get a history entry by ID
1370
- * @param key Entry ID
1371
- * @returns The history entry or undefined if not found
1438
+ * Get logs by trace ID
1372
1439
  */
1373
- async getHistoryEntry(key) {
1374
- await this.initialized;
1440
+ async getLogsByTraceId(traceId) {
1441
+ await this.ensureInitialized();
1375
1442
  try {
1376
- const tableName = `${this.options.tablePrefix}_agent_history`;
1377
1443
  const result = await this.client.execute({
1378
- sql: `SELECT id, agent_id, timestamp, status, input, output, usage, metadata, userId, conversationId
1379
- FROM ${tableName} WHERE id = ?`,
1380
- args: [key]
1381
- });
1382
- if (result.rows.length === 0) {
1383
- this.debug(`History entry with ID ${key} not found`);
1384
- return void 0;
1385
- }
1386
- const row = result.rows[0];
1387
- const entry = {
1388
- id: row.id,
1389
- _agentId: row.agent_id,
1390
- // Keep _agentId for compatibility
1391
- timestamp: new Date(row.timestamp),
1392
- status: row.status,
1393
- input: row.input ? safeJsonParse(row.input) : null,
1394
- output: row.output ? safeJsonParse(row.output) : null,
1395
- usage: row.usage ? safeJsonParse(row.usage) : null,
1396
- metadata: row.metadata ? safeJsonParse(row.metadata) : null,
1397
- userId: row.userId,
1398
- conversationId: row.conversationId
1399
- };
1400
- this.debug(`Got history entry with ID ${key}`);
1401
- const stepsTableName = `${this.options.tablePrefix}_agent_history_steps`;
1402
- const stepsResult = await this.client.execute({
1403
- sql: `SELECT value FROM ${stepsTableName} WHERE history_id = ? AND agent_id = ?`,
1404
- args: [key, entry._agentId]
1405
- });
1406
- const steps = stepsResult.rows.map((row2) => {
1407
- const step = safeJsonParse(row2.value);
1408
- return {
1409
- type: step.type,
1410
- name: step.name,
1411
- content: step.content,
1412
- arguments: step.arguments
1413
- };
1414
- });
1415
- const timelineEventsTableName = `${this.options.tablePrefix}_agent_history_timeline_events`;
1416
- const timelineEventsResult = await this.client.execute({
1417
- sql: `SELECT id, event_type, event_name, start_time, end_time,
1418
- status, status_message, level, version,
1419
- parent_event_id, tags, input, output, error, metadata
1420
- FROM ${timelineEventsTableName}
1421
- WHERE history_id = ? AND agent_id = ?`,
1422
- args: [key, entry._agentId]
1423
- });
1424
- const events = timelineEventsResult.rows.map((row2) => {
1425
- const input = row2.input ? safeJsonParse(row2.input) : void 0;
1426
- const output = row2.output ? safeJsonParse(row2.output) : void 0;
1427
- const error = row2.error ? safeJsonParse(row2.error) : void 0;
1428
- const statusMessage = row2.status_message ? safeJsonParse(row2.status_message) : void 0;
1429
- const metadata = row2.metadata ? safeJsonParse(row2.metadata) : void 0;
1430
- const tags = row2.tags ? safeJsonParse(row2.tags) : void 0;
1431
- return {
1432
- id: row2.id,
1433
- type: row2.event_type,
1434
- name: row2.event_name,
1435
- startTime: row2.start_time,
1436
- endTime: row2.end_time,
1437
- status: row2.status,
1438
- statusMessage,
1439
- level: row2.level,
1440
- version: row2.version,
1441
- parentEventId: row2.parent_event_id,
1442
- tags,
1443
- input,
1444
- output,
1445
- error: statusMessage ? statusMessage : error,
1446
- metadata
1447
- };
1444
+ sql: `
1445
+ SELECT * FROM ${this.tablePrefix}_logs
1446
+ WHERE trace_id = ?
1447
+ ORDER BY timestamp DESC
1448
+ LIMIT ?
1449
+ `,
1450
+ args: [traceId, this.maxSpansPerQuery]
1448
1451
  });
1449
- entry.steps = steps;
1450
- entry.events = events;
1451
- return entry;
1452
+ return result.rows.map((row) => this.rowToLogRecord(row));
1452
1453
  } catch (error) {
1453
- this.debug(`Error getting history entry with ID ${key}`, error);
1454
- return void 0;
1454
+ this.logger.error("Failed to get logs by trace ID", { error, traceId });
1455
+ throw error;
1455
1456
  }
1456
1457
  }
1457
1458
  /**
1458
- * Get a history step by ID
1459
- * @param key Step ID
1460
- * @returns The history step or undefined if not found
1459
+ * Get logs by span ID
1461
1460
  */
1462
- async getHistoryStep(key) {
1463
- await this.initialized;
1464
- try {
1465
- const tableName = `${this.options.tablePrefix}_agent_history_steps`;
1466
- const result = await this.client.execute({
1467
- sql: `SELECT value FROM ${tableName} WHERE key = ?`,
1468
- args: [key]
1469
- });
1470
- if (result.rows.length === 0) {
1471
- this.debug(`History step with ID ${key} not found`);
1472
- return void 0;
1473
- }
1474
- const value = safeJsonParse(result.rows[0].value);
1475
- this.debug(`Got history step with ID ${key}`);
1476
- return value;
1477
- } catch (error) {
1478
- this.debug(`Error getting history step with ID ${key}`, error);
1479
- return void 0;
1480
- }
1481
- }
1482
- async createConversation(conversation) {
1483
- await this.initialized;
1484
- await debugDelay();
1485
- const now = (/* @__PURE__ */ new Date()).toISOString();
1486
- const metadataString = safeStringify2(conversation.metadata);
1487
- const tableName = `${this.options.tablePrefix}_conversations`;
1488
- return await this.executeWithRetryStrategy(async () => {
1489
- await this.client.execute({
1490
- sql: `INSERT INTO ${tableName} (id, resource_id, user_id, title, metadata, created_at, updated_at)
1491
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
1492
- args: [
1493
- conversation.id,
1494
- conversation.resourceId,
1495
- conversation.userId,
1496
- conversation.title,
1497
- metadataString,
1498
- now,
1499
- now
1500
- ]
1501
- });
1502
- return {
1503
- id: conversation.id,
1504
- resourceId: conversation.resourceId,
1505
- userId: conversation.userId,
1506
- title: conversation.title,
1507
- metadata: conversation.metadata,
1508
- createdAt: now,
1509
- updatedAt: now
1510
- };
1511
- }, `createConversation[${conversation.id}]`);
1512
- }
1513
- async getConversation(id) {
1514
- await this.initialized;
1515
- await debugDelay();
1516
- const tableName = `${this.options.tablePrefix}_conversations`;
1517
- try {
1518
- const result = await this.client.execute({
1519
- sql: `SELECT * FROM ${tableName} WHERE id = ?`,
1520
- args: [id]
1521
- });
1522
- if (result.rows.length === 0) {
1523
- return null;
1524
- }
1525
- const row = result.rows[0];
1526
- return {
1527
- id: row.id,
1528
- resourceId: row.resource_id,
1529
- userId: row.user_id,
1530
- title: row.title,
1531
- metadata: row.metadata ? safeJsonParse(row.metadata) : {},
1532
- createdAt: row.created_at,
1533
- updatedAt: row.updated_at
1534
- };
1535
- } catch (error) {
1536
- this.debug("Error getting conversation:", error);
1537
- throw new Error("Failed to get conversation from LibSQL database");
1538
- }
1539
- }
1540
- async getConversations(resourceId) {
1541
- await this.initialized;
1542
- await debugDelay();
1543
- const tableName = `${this.options.tablePrefix}_conversations`;
1544
- try {
1545
- const result = await this.client.execute({
1546
- sql: `SELECT * FROM ${tableName} WHERE resource_id = ? ORDER BY updated_at DESC`,
1547
- args: [resourceId]
1548
- });
1549
- return result.rows.map((row) => ({
1550
- id: row.id,
1551
- resourceId: row.resource_id,
1552
- userId: row.user_id,
1553
- title: row.title,
1554
- metadata: safeJsonParse(row.metadata),
1555
- createdAt: row.created_at,
1556
- updatedAt: row.updated_at
1557
- }));
1558
- } catch (error) {
1559
- this.debug("Error getting conversations:", error);
1560
- throw new Error("Failed to get conversations from LibSQL database");
1561
- }
1562
- }
1563
- async getConversationsByUserId(userId, options = {}) {
1564
- await this.initialized;
1565
- await debugDelay();
1566
- const {
1567
- resourceId,
1568
- limit = 50,
1569
- offset = 0,
1570
- orderBy = "updated_at",
1571
- orderDirection = "DESC"
1572
- } = options;
1573
- const tableName = `${this.options.tablePrefix}_conversations`;
1461
+ async getLogsBySpanId(spanId) {
1462
+ await this.ensureInitialized();
1574
1463
  try {
1575
- let sql = `SELECT * FROM ${tableName} WHERE user_id = ?`;
1576
- const args = [userId];
1577
- if (resourceId) {
1578
- sql += " AND resource_id = ?";
1579
- args.push(resourceId);
1580
- }
1581
- sql += ` ORDER BY ${orderBy} ${orderDirection}`;
1582
- if (limit > 0) {
1583
- sql += " LIMIT ? OFFSET ?";
1584
- args.push(limit, offset);
1585
- }
1586
1464
  const result = await this.client.execute({
1587
- sql,
1588
- args
1465
+ sql: `
1466
+ SELECT * FROM ${this.tablePrefix}_logs
1467
+ WHERE span_id = ?
1468
+ ORDER BY timestamp DESC
1469
+ LIMIT ?
1470
+ `,
1471
+ args: [spanId, this.maxSpansPerQuery]
1589
1472
  });
1590
- return result.rows.map((row) => ({
1591
- id: row.id,
1592
- resourceId: row.resource_id,
1593
- userId: row.user_id,
1594
- title: row.title,
1595
- metadata: safeJsonParse(row.metadata),
1596
- createdAt: row.created_at,
1597
- updatedAt: row.updated_at
1598
- }));
1473
+ return result.rows.map((row) => this.rowToLogRecord(row));
1599
1474
  } catch (error) {
1600
- this.debug("Error getting conversations by user ID:", error);
1601
- throw new Error("Failed to get conversations by user ID from LibSQL database");
1475
+ this.logger.error("Failed to get logs by span ID", { error, spanId });
1476
+ throw error;
1602
1477
  }
1603
1478
  }
1604
1479
  /**
1605
- * Query conversations with filtering and pagination options
1606
- *
1607
- * @param options Query options for filtering and pagination
1608
- * @returns Promise that resolves to an array of conversations matching the criteria
1609
- * @see {@link https://voltagent.dev/docs/agents/memory/libsql#querying-conversations | Querying Conversations}
1480
+ * Query logs with flexible filtering
1610
1481
  */
1611
- async queryConversations(options) {
1612
- await this.initialized;
1613
- await debugDelay();
1614
- const {
1615
- userId,
1616
- resourceId,
1617
- limit = 50,
1618
- offset = 0,
1619
- orderBy = "updated_at",
1620
- orderDirection = "DESC"
1621
- } = options;
1622
- const tableName = `${this.options.tablePrefix}_conversations`;
1482
+ async queryLogs(filter) {
1483
+ await this.ensureInitialized();
1623
1484
  try {
1624
- let sql = `SELECT * FROM ${tableName}`;
1485
+ const whereClauses = [];
1625
1486
  const args = [];
1626
- const conditions = [];
1627
- if (userId) {
1628
- conditions.push("user_id = ?");
1629
- args.push(userId);
1487
+ if (filter.traceId) {
1488
+ whereClauses.push("trace_id = ?");
1489
+ args.push(filter.traceId);
1630
1490
  }
1631
- if (resourceId) {
1632
- conditions.push("resource_id = ?");
1633
- args.push(resourceId);
1491
+ if (filter.spanId) {
1492
+ whereClauses.push("span_id = ?");
1493
+ args.push(filter.spanId);
1634
1494
  }
1635
- if (conditions.length > 0) {
1636
- sql += ` WHERE ${conditions.join(" AND ")}`;
1495
+ if (filter.severityNumber !== void 0) {
1496
+ whereClauses.push("severity_number >= ?");
1497
+ args.push(filter.severityNumber);
1637
1498
  }
1638
- sql += ` ORDER BY ${orderBy} ${orderDirection}`;
1639
- if (limit > 0) {
1640
- sql += " LIMIT ? OFFSET ?";
1641
- args.push(limit, offset);
1499
+ if (filter.severityText) {
1500
+ whereClauses.push("severity_text = ?");
1501
+ args.push(filter.severityText);
1502
+ }
1503
+ if (filter.instrumentationScope) {
1504
+ whereClauses.push("instrumentation_scope LIKE ?");
1505
+ args.push(`%${filter.instrumentationScope}%`);
1506
+ }
1507
+ if (filter.startTimeMin !== void 0) {
1508
+ const minTime = new Date(filter.startTimeMin).toISOString();
1509
+ whereClauses.push("timestamp >= ?");
1510
+ args.push(minTime);
1642
1511
  }
1512
+ if (filter.startTimeMax !== void 0) {
1513
+ const maxTime = new Date(filter.startTimeMax).toISOString();
1514
+ whereClauses.push("timestamp <= ?");
1515
+ args.push(maxTime);
1516
+ }
1517
+ if (filter.bodyContains) {
1518
+ whereClauses.push("body LIKE ?");
1519
+ args.push(`%${filter.bodyContains}%`);
1520
+ }
1521
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
1522
+ const limit = filter.limit || this.maxSpansPerQuery;
1523
+ args.push(limit);
1643
1524
  const result = await this.client.execute({
1644
- sql,
1525
+ sql: `
1526
+ SELECT * FROM ${this.tablePrefix}_logs
1527
+ ${whereClause}
1528
+ ORDER BY timestamp DESC
1529
+ LIMIT ?
1530
+ `,
1645
1531
  args
1646
1532
  });
1647
- return result.rows.map((row) => ({
1648
- id: row.id,
1649
- resourceId: row.resource_id,
1650
- userId: row.user_id,
1651
- title: row.title,
1652
- metadata: safeJsonParse(row.metadata),
1653
- createdAt: row.created_at,
1654
- updatedAt: row.updated_at
1655
- }));
1533
+ const logs = result.rows.map((row) => this.rowToLogRecord(row));
1534
+ if (filter.attributeKey) {
1535
+ const key = filter.attributeKey;
1536
+ return logs.filter((log) => {
1537
+ if (!log.attributes) return false;
1538
+ if (filter.attributeValue !== void 0) {
1539
+ return log.attributes[key] === filter.attributeValue;
1540
+ }
1541
+ return key in log.attributes;
1542
+ });
1543
+ }
1544
+ return logs;
1656
1545
  } catch (error) {
1657
- this.debug("Error querying conversations:", error);
1658
- throw new Error("Failed to query conversations from LibSQL database");
1546
+ this.logger.error("Failed to query logs", { error, filter });
1547
+ throw error;
1659
1548
  }
1660
1549
  }
1661
1550
  /**
1662
- * Get messages for a specific conversation with pagination support
1663
- *
1664
- * @param conversationId The unique identifier of the conversation to retrieve messages from
1665
- * @param options Optional pagination and filtering options
1666
- * @returns Promise that resolves to an array of messages in chronological order (oldest first)
1667
- * @see {@link https://voltagent.dev/docs/agents/memory/libsql#conversation-messages | Getting Conversation Messages}
1551
+ * Delete old logs
1668
1552
  */
1669
- async getConversationMessages(conversationId, options = {}) {
1670
- await this.initialized;
1671
- await debugDelay();
1672
- const { limit = 100, offset = 0 } = options;
1673
- const tableName = `${this.options.tablePrefix}_messages`;
1553
+ async deleteOldLogs(beforeTimestamp) {
1554
+ await this.ensureInitialized();
1674
1555
  try {
1675
- let sql = `SELECT * FROM ${tableName} WHERE conversation_id = ? ORDER BY created_at ASC`;
1676
- const args = [conversationId];
1677
- if (limit > 0) {
1678
- sql += " LIMIT ? OFFSET ?";
1679
- args.push(limit, offset);
1680
- }
1556
+ const beforeDate = new Date(beforeTimestamp).toISOString();
1681
1557
  const result = await this.client.execute({
1682
- sql,
1683
- args
1684
- });
1685
- return result.rows.map((row) => {
1686
- let content = row.content;
1687
- const parsedContent = safeJsonParse(content);
1688
- if (parsedContent !== null) {
1689
- content = parsedContent;
1690
- }
1691
- return {
1692
- id: row.message_id,
1693
- role: row.role,
1694
- content,
1695
- type: row.type,
1696
- createdAt: row.created_at
1697
- };
1558
+ sql: `
1559
+ DELETE FROM ${this.tablePrefix}_logs
1560
+ WHERE timestamp < ?
1561
+ `,
1562
+ args: [beforeDate]
1698
1563
  });
1564
+ const deletedCount = result.rowsAffected || 0;
1565
+ this.debugLog("Old logs deleted", { deletedCount, beforeDate });
1566
+ return deletedCount;
1699
1567
  } catch (error) {
1700
- this.debug("Error getting conversation messages:", error);
1701
- throw new Error("Failed to get conversation messages from LibSQL database");
1568
+ this.logger.error("Failed to delete old logs", { error, beforeTimestamp });
1569
+ throw error;
1702
1570
  }
1703
1571
  }
1704
- async updateConversation(id, updates) {
1705
- await this.initialized;
1706
- await debugDelay();
1707
- const tableName = `${this.options.tablePrefix}_conversations`;
1708
- const now = (/* @__PURE__ */ new Date()).toISOString();
1709
- try {
1710
- const updatesList = [];
1711
- const args = [];
1712
- if (updates.resourceId !== void 0) {
1713
- updatesList.push("resource_id = ?");
1714
- args.push(updates.resourceId);
1715
- }
1716
- if (updates.userId !== void 0) {
1717
- updatesList.push("user_id = ?");
1718
- args.push(updates.userId);
1572
+ /**
1573
+ * Convert a database row to an ObservabilityLogRecord
1574
+ */
1575
+ rowToLogRecord(row) {
1576
+ const log = {
1577
+ timestamp: row.timestamp,
1578
+ body: (() => {
1579
+ try {
1580
+ const bodyStr = row.body;
1581
+ if (bodyStr.startsWith("{") || bodyStr.startsWith("[")) {
1582
+ return JSON.parse(bodyStr);
1583
+ }
1584
+ } catch {
1585
+ }
1586
+ return row.body;
1587
+ })()
1588
+ };
1589
+ if (row.trace_id !== null) {
1590
+ log.traceId = row.trace_id;
1591
+ }
1592
+ if (row.span_id !== null) {
1593
+ log.spanId = row.span_id;
1594
+ }
1595
+ if (row.trace_flags !== null) {
1596
+ log.traceFlags = row.trace_flags;
1597
+ }
1598
+ if (row.severity_number !== null) {
1599
+ log.severityNumber = row.severity_number;
1600
+ }
1601
+ if (row.severity_text !== null) {
1602
+ log.severityText = row.severity_text;
1603
+ }
1604
+ if (row.attributes && row.attributes !== "null") {
1605
+ try {
1606
+ const attributes = JSON.parse(row.attributes);
1607
+ if (attributes && Object.keys(attributes).length > 0) {
1608
+ log.attributes = attributes;
1609
+ }
1610
+ } catch {
1719
1611
  }
1720
- if (updates.title !== void 0) {
1721
- updatesList.push("title = ?");
1722
- args.push(updates.title);
1612
+ }
1613
+ if (row.resource && row.resource !== "null") {
1614
+ try {
1615
+ const resource = JSON.parse(row.resource);
1616
+ if (resource && Object.keys(resource).length > 0) {
1617
+ log.resource = resource;
1618
+ }
1619
+ } catch {
1723
1620
  }
1724
- if (updates.metadata !== void 0) {
1725
- updatesList.push("metadata = ?");
1726
- args.push(safeStringify2(updates.metadata));
1621
+ }
1622
+ if (row.instrumentation_scope && row.instrumentation_scope !== "null") {
1623
+ try {
1624
+ const scope = JSON.parse(row.instrumentation_scope);
1625
+ if (scope) {
1626
+ log.instrumentationScope = scope;
1627
+ }
1628
+ } catch {
1727
1629
  }
1728
- updatesList.push("updated_at = ?");
1729
- args.push(now);
1730
- args.push(id);
1731
- await this.client.execute({
1732
- sql: `UPDATE ${tableName} SET ${updatesList.join(", ")} WHERE id = ?`,
1733
- args
1734
- });
1735
- const updated = await this.getConversation(id);
1736
- if (!updated) {
1737
- throw new Error("Conversation not found after update");
1630
+ }
1631
+ return log;
1632
+ }
1633
+ /**
1634
+ * Close the database connection
1635
+ */
1636
+ async close() {
1637
+ this.debugLog("LibSQL observability adapter closed");
1638
+ }
1639
+ };
1640
+
1641
+ // src/vector-adapter.ts
1642
+ import fs2 from "fs";
1643
+ import path2 from "path";
1644
+ import { createClient as createClient3 } from "@libsql/client";
1645
+ import {
1646
+ cosineSimilarity
1647
+ } from "@voltagent/core";
1648
+ import { createPinoLogger as createPinoLogger3 } from "@voltagent/logger";
1649
+ var LibSQLVectorAdapter = class {
1650
+ static {
1651
+ __name(this, "LibSQLVectorAdapter");
1652
+ }
1653
+ client;
1654
+ tablePrefix;
1655
+ maxVectorDimensions;
1656
+ cacheSize;
1657
+ batchSize;
1658
+ debug;
1659
+ logger;
1660
+ maxRetries;
1661
+ retryDelayMs;
1662
+ url;
1663
+ initialized = false;
1664
+ vectorCache;
1665
+ dimensions = null;
1666
+ constructor(options = {}) {
1667
+ this.tablePrefix = options.tablePrefix ?? "voltagent";
1668
+ this.maxVectorDimensions = options.maxVectorDimensions ?? 1536;
1669
+ this.cacheSize = options.cacheSize ?? 100;
1670
+ this.batchSize = options.batchSize ?? 100;
1671
+ this.maxRetries = options.maxRetries ?? 3;
1672
+ this.retryDelayMs = options.retryDelayMs ?? 100;
1673
+ this.debug = options.debug ?? false;
1674
+ this.logger = options.logger ?? createPinoLogger3({
1675
+ name: "libsql-vector-adapter",
1676
+ level: this.debug ? "debug" : "info"
1677
+ });
1678
+ const requestedUrl = options.url ?? "file:./.voltagent/memory.db";
1679
+ if (requestedUrl === ":memory:" || requestedUrl === "file::memory:" || requestedUrl.startsWith("file::memory:")) {
1680
+ this.url = ":memory:";
1681
+ } else {
1682
+ this.url = requestedUrl;
1683
+ }
1684
+ if (this.url.startsWith("file:") && !this.url.startsWith("file::memory:")) {
1685
+ const dbPath = this.url.replace("file:", "");
1686
+ const dbDir = path2.dirname(dbPath);
1687
+ if (!fs2.existsSync(dbDir)) {
1688
+ fs2.mkdirSync(dbDir, { recursive: true });
1738
1689
  }
1739
- return updated;
1740
- } catch (error) {
1741
- this.debug("Error updating conversation:", error);
1742
- throw new Error("Failed to update conversation in LibSQL database");
1743
1690
  }
1691
+ this.client = createClient3({
1692
+ url: this.url,
1693
+ authToken: options.authToken
1694
+ });
1695
+ this.vectorCache = /* @__PURE__ */ new Map();
1744
1696
  }
1745
- async deleteConversation(id) {
1746
- await this.initialized;
1747
- await debugDelay();
1748
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
1749
- const messagesTableName = `${this.options.tablePrefix}_messages`;
1697
+ /**
1698
+ * Initialize the database schema
1699
+ */
1700
+ async initialize() {
1701
+ if (this.initialized) return;
1702
+ const tableName = `${this.tablePrefix}_vectors`;
1750
1703
  try {
1751
- await this.client.execute({
1752
- sql: `DELETE FROM ${messagesTableName} WHERE conversation_id = ?`,
1753
- args: [id]
1754
- });
1755
- await this.client.execute({
1756
- sql: `DELETE FROM ${conversationsTableName} WHERE id = ?`,
1757
- args: [id]
1758
- });
1704
+ await this.client.executeMultiple(`
1705
+ BEGIN;
1706
+ CREATE TABLE IF NOT EXISTS ${tableName} (
1707
+ id TEXT PRIMARY KEY,
1708
+ vector BLOB NOT NULL,
1709
+ dimensions INTEGER NOT NULL,
1710
+ metadata TEXT,
1711
+ content TEXT,
1712
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1713
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1714
+ );
1715
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_created ON ${tableName}(created_at);
1716
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_dimensions ON ${tableName}(dimensions);
1717
+ COMMIT;
1718
+ `);
1719
+ this.initialized = true;
1720
+ this.logger.debug("Vector adapter initialized");
1759
1721
  } catch (error) {
1760
- this.debug("Error deleting conversation:", error);
1761
- throw new Error("Failed to delete conversation from LibSQL database");
1722
+ this.logger.error("Failed to initialize vector adapter", error);
1723
+ throw error;
1762
1724
  }
1763
1725
  }
1764
1726
  /**
1765
- * Get all history entries for an agent with pagination
1766
- * @param agentId Agent ID
1767
- * @param page Page number (0-based)
1768
- * @param limit Number of entries per page
1769
- * @returns Object with entries array and total count
1727
+ * Serialize a vector to binary format
1770
1728
  */
1771
- async getAllHistoryEntriesByAgent(agentId, page, limit) {
1772
- await this.initialized;
1773
- try {
1774
- const tableName = `${this.options.tablePrefix}_agent_history`;
1775
- const offset = page * limit;
1776
- const countResult = await this.client.execute({
1777
- sql: `SELECT COUNT(*) as total FROM ${tableName} WHERE agent_id = ?`,
1778
- args: [agentId]
1779
- });
1780
- const total = Number(countResult.rows[0].total);
1781
- const result = await this.client.execute({
1782
- sql: `SELECT id, agent_id, timestamp, status, input, output, usage, metadata, userId, conversationId
1783
- FROM ${tableName} WHERE agent_id = ?
1784
- ORDER BY timestamp DESC
1785
- LIMIT ? OFFSET ?`,
1786
- args: [agentId, limit, offset]
1787
- });
1788
- const entries = result.rows.map((row) => ({
1789
- id: row.id,
1790
- _agentId: row.agent_id,
1791
- // Keep _agentId for compatibility
1792
- timestamp: new Date(row.timestamp),
1793
- status: row.status,
1794
- input: row.input ? safeJsonParse(row.input) : null,
1795
- output: row.output ? safeJsonParse(row.output) : null,
1796
- usage: row.usage ? safeJsonParse(row.usage) : null,
1797
- metadata: row.metadata ? safeJsonParse(row.metadata) : null,
1798
- userId: row.userId,
1799
- conversationId: row.conversationId
1800
- }));
1801
- this.debug(`Got all history entries for agent ${agentId} (${entries.length} items)`);
1802
- const completeEntries = await Promise.all(
1803
- entries.map(async (entry) => {
1804
- const stepsTableName = `${this.options.tablePrefix}_agent_history_steps`;
1805
- const stepsResult = await this.client.execute({
1806
- sql: `SELECT value FROM ${stepsTableName} WHERE history_id = ? AND agent_id = ?`,
1807
- args: [entry.id, agentId]
1808
- });
1809
- const steps = stepsResult.rows.map((row) => {
1810
- const step = safeJsonParse(row.value);
1811
- return {
1812
- type: step.type,
1813
- name: step.name,
1814
- content: step.content,
1815
- arguments: step.arguments
1816
- };
1817
- });
1818
- const timelineEventsTableName = `${this.options.tablePrefix}_agent_history_timeline_events`;
1819
- const timelineEventsResult = await this.client.execute({
1820
- sql: `SELECT id, event_type, event_name, start_time, end_time,
1821
- status, status_message, level, version,
1822
- parent_event_id, tags, input, output, error, metadata
1823
- FROM ${timelineEventsTableName}
1824
- WHERE history_id = ? AND agent_id = ?`,
1825
- args: [entry.id, agentId]
1826
- });
1827
- const events = timelineEventsResult.rows.map((row) => {
1828
- const input = row.input ? safeJsonParse(row.input) : void 0;
1829
- const output = row.output ? safeJsonParse(row.output) : void 0;
1830
- const error = row.error ? safeJsonParse(row.error) : void 0;
1831
- const statusMessage = row.status_message ? safeJsonParse(row.status_message) : void 0;
1832
- const metadata = row.metadata ? safeJsonParse(row.metadata) : void 0;
1833
- const tags = row.tags ? safeJsonParse(row.tags) : void 0;
1834
- return {
1835
- id: row.id,
1836
- type: row.event_type,
1837
- name: row.event_name,
1838
- startTime: row.start_time,
1839
- endTime: row.end_time,
1840
- status: row.status,
1841
- statusMessage,
1842
- level: row.level,
1843
- version: row.version,
1844
- parentEventId: row.parent_event_id,
1845
- tags,
1846
- input,
1847
- output,
1848
- error: statusMessage ? statusMessage : error,
1849
- metadata
1850
- };
1851
- });
1852
- entry.steps = steps;
1853
- entry.events = events;
1854
- return entry;
1855
- })
1856
- );
1857
- return {
1858
- entries: completeEntries,
1859
- total
1860
- };
1861
- } catch (error) {
1862
- this.debug(`Error getting history entries for agent ${agentId}`, error);
1863
- return {
1864
- entries: [],
1865
- total: 0
1866
- };
1729
+ serializeVector(vector) {
1730
+ const buffer = Buffer.allocUnsafe(vector.length * 4);
1731
+ for (let i = 0; i < vector.length; i++) {
1732
+ buffer.writeFloatLE(vector[i], i * 4);
1867
1733
  }
1734
+ return buffer;
1868
1735
  }
1869
1736
  /**
1870
- * Migrates agent history data from old structure to new structure.
1871
- * If migration fails, it can be rolled back using the backup mechanism.
1872
- *
1873
- * Old database structure:
1874
- * CREATE TABLE voltagent_memory_agent_history (
1875
- * key TEXT PRIMARY KEY,
1876
- * value TEXT NOT NULL,
1877
- * agent_id TEXT
1878
- * );
1737
+ * Deserialize a vector from binary format
1879
1738
  */
1880
- async migrateAgentHistoryData(options = {}) {
1881
- const {
1882
- createBackup = true,
1883
- restoreFromBackup = false,
1884
- deleteBackupAfterSuccess = false
1885
- } = options;
1886
- const oldTableName = `${this.options.tablePrefix}_agent_history`;
1887
- const oldTableBackup = `${oldTableName}_backup`;
1888
- const timelineEventsTableName = `${this.options.tablePrefix}_agent_history_timeline_events`;
1889
- try {
1890
- this.debug("Starting agent history migration...");
1891
- const flagCheck = await this.checkMigrationFlag("agent_history_data_migration");
1892
- if (flagCheck.alreadyCompleted) {
1893
- return { success: true, migratedCount: 0 };
1894
- }
1895
- if (restoreFromBackup) {
1896
- this.debug("Starting restoration from backup...");
1897
- const backupCheck = await this.client.execute({
1898
- sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
1899
- args: [oldTableBackup]
1900
- });
1901
- if (backupCheck.rows.length === 0) {
1902
- throw new Error("No backup found to restore");
1903
- }
1904
- await this.client.execute("BEGIN TRANSACTION;");
1905
- await this.client.execute(`DROP TABLE IF EXISTS ${oldTableName};`);
1906
- await this.client.execute(`ALTER TABLE ${oldTableBackup} RENAME TO ${oldTableName};`);
1907
- await this.client.execute("COMMIT;");
1908
- this.debug("Restoration from backup completed successfully");
1909
- return {
1910
- success: true,
1911
- backupCreated: false
1912
- };
1913
- }
1914
- const tableInfoQuery = await this.client.execute(`PRAGMA table_info(${oldTableName})`);
1915
- if (tableInfoQuery.rows.length === 0) {
1916
- this.debug(`${oldTableName} table not found, migration not needed`);
1917
- return {
1918
- success: true,
1919
- migratedCount: 0
1920
- };
1921
- }
1922
- const hasValueColumn = tableInfoQuery.rows.some((row) => row.name === "value");
1923
- if (!hasValueColumn) {
1924
- this.debug("Table is already in new format, migration not needed");
1925
- return {
1926
- success: true,
1927
- migratedCount: 0
1928
- };
1929
- }
1930
- if (createBackup) {
1931
- this.debug("Creating backup...");
1932
- const backupCheck = await this.client.execute({
1933
- sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
1934
- args: [oldTableBackup]
1935
- });
1936
- if (backupCheck.rows.length > 0) {
1937
- await this.client.execute(`DROP TABLE IF EXISTS ${oldTableBackup};`);
1739
+ deserializeVector(buffer) {
1740
+ let bytes;
1741
+ if (buffer instanceof Buffer) {
1742
+ bytes = buffer;
1743
+ } else if (buffer instanceof ArrayBuffer) {
1744
+ bytes = Buffer.from(buffer);
1745
+ } else {
1746
+ bytes = Buffer.from(buffer);
1747
+ }
1748
+ const vector = [];
1749
+ for (let i = 0; i < bytes.length; i += 4) {
1750
+ vector.push(bytes.readFloatLE(i));
1751
+ }
1752
+ return vector;
1753
+ }
1754
+ /**
1755
+ * Execute a database operation with retries
1756
+ */
1757
+ async executeWithRetry(operation, context) {
1758
+ let lastError;
1759
+ let delay = this.retryDelayMs;
1760
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
1761
+ try {
1762
+ return await operation();
1763
+ } catch (error) {
1764
+ lastError = error;
1765
+ this.logger.warn(`Operation failed (attempt ${attempt}): ${context}`, error);
1766
+ if (attempt < this.maxRetries) {
1767
+ await new Promise((resolve) => setTimeout(resolve, delay));
1768
+ delay *= 2;
1938
1769
  }
1939
- await this.client.execute(
1940
- `CREATE TABLE ${oldTableBackup} AS SELECT * FROM ${oldTableName};`
1941
- );
1942
- this.debug("Backup created successfully");
1943
1770
  }
1944
- const oldFormatData = await this.client.execute({
1945
- sql: `SELECT key, value, agent_id FROM ${oldTableName}`
1771
+ }
1772
+ this.logger.error(`Operation failed after ${this.maxRetries} attempts: ${context}`, lastError);
1773
+ throw lastError;
1774
+ }
1775
+ /**
1776
+ * Store a vector with associated metadata
1777
+ */
1778
+ async store(id, vector, metadata) {
1779
+ await this.initialize();
1780
+ if (!Array.isArray(vector) || vector.length === 0) {
1781
+ throw new Error("Vector must be a non-empty array");
1782
+ }
1783
+ if (vector.length > this.maxVectorDimensions) {
1784
+ throw new Error(
1785
+ `Vector dimensions (${vector.length}) exceed maximum (${this.maxVectorDimensions})`
1786
+ );
1787
+ }
1788
+ if (this.dimensions === null) {
1789
+ this.dimensions = vector.length;
1790
+ } else if (vector.length !== this.dimensions) {
1791
+ throw new Error(
1792
+ `Vector dimension mismatch. Expected ${this.dimensions}, got ${vector.length}`
1793
+ );
1794
+ }
1795
+ const tableName = `${this.tablePrefix}_vectors`;
1796
+ const serializedVector = this.serializeVector(vector);
1797
+ const metadataJson = metadata ? JSON.stringify(metadata) : null;
1798
+ await this.executeWithRetry(async () => {
1799
+ await this.client.execute({
1800
+ sql: `
1801
+ INSERT OR REPLACE INTO ${tableName}
1802
+ (id, vector, dimensions, metadata, updated_at)
1803
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
1804
+ `,
1805
+ args: [id, serializedVector, vector.length, metadataJson]
1946
1806
  });
1947
- if (oldFormatData.rows.length === 0) {
1948
- this.debug("No data found to migrate");
1949
- return {
1950
- success: true,
1951
- migratedCount: 0,
1952
- backupCreated: createBackup
1953
- };
1954
- }
1955
- const tempTableName = `${oldTableName}_temp`;
1956
- await this.client.execute(`
1957
- CREATE TABLE ${tempTableName} (
1958
- id TEXT PRIMARY KEY,
1959
- agent_id TEXT NOT NULL,
1960
- timestamp TEXT NOT NULL,
1961
- status TEXT,
1962
- input TEXT,
1963
- output TEXT,
1964
- usage TEXT,
1965
- metadata TEXT
1966
- )
1967
- `);
1968
- await this.client.execute("BEGIN TRANSACTION;");
1969
- let migratedCount = 0;
1970
- const migratedIds = /* @__PURE__ */ new Set();
1971
- for (const row of oldFormatData.rows) {
1972
- const key = row.key;
1973
- const agentId = row.agent_id;
1974
- const valueStr = row.value;
1975
- try {
1976
- const valueObj = safeJsonParse(valueStr);
1977
- const id = valueObj.id || key;
1978
- if (migratedIds.has(id)) {
1979
- continue;
1807
+ }, `store vector ${id}`);
1808
+ if (this.vectorCache.size >= this.cacheSize) {
1809
+ const firstKey = this.vectorCache.keys().next().value;
1810
+ if (firstKey) this.vectorCache.delete(firstKey);
1811
+ }
1812
+ this.vectorCache.set(id, { id, vector, metadata });
1813
+ this.logger.debug(`Vector stored: ${id} (${vector.length} dimensions)`);
1814
+ }
1815
+ /**
1816
+ * Store multiple vectors in batch
1817
+ */
1818
+ async storeBatch(items) {
1819
+ await this.initialize();
1820
+ if (items.length === 0) return;
1821
+ const tableName = `${this.tablePrefix}_vectors`;
1822
+ for (let i = 0; i < items.length; i += this.batchSize) {
1823
+ const batch = items.slice(i, i + this.batchSize);
1824
+ await this.executeWithRetry(async () => {
1825
+ const stmts = [];
1826
+ for (const item of batch) {
1827
+ if (!Array.isArray(item.vector) || item.vector.length === 0) {
1828
+ throw new Error("Vector must be a non-empty array");
1980
1829
  }
1981
- migratedIds.add(id);
1982
- migratedCount++;
1983
- const inputJSON = valueObj.input ? safeStringify2(valueObj.input) : null;
1984
- const outputJSON = valueObj.output ? safeStringify2(valueObj.output) : null;
1985
- const usageJSON = valueObj.usage ? safeStringify2(valueObj.usage) : null;
1986
- await this.client.execute({
1987
- sql: `INSERT INTO ${tempTableName}
1988
- (id, agent_id, timestamp, status, input, output, usage, metadata)
1989
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1990
- args: [
1991
- id,
1992
- valueObj._agentId || agentId,
1993
- valueObj.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1994
- valueObj.status || null,
1995
- inputJSON,
1996
- outputJSON,
1997
- usageJSON,
1998
- null
1999
- ]
2000
- });
2001
- let input = "";
2002
- if (Array.isArray(valueObj.events)) {
2003
- for (const event of valueObj.events) {
2004
- try {
2005
- if (event.affectedNodeId?.startsWith("message_")) {
2006
- input = event.data.input;
2007
- continue;
2008
- }
2009
- const eventId = event.id || this.generateId();
2010
- const eventType = event.type || "unknown";
2011
- let eventName = event.name || "unknown";
2012
- const startTime = event.timestamp || event.startTime || (/* @__PURE__ */ new Date()).toISOString();
2013
- const endTime = event.updatedAt || event.endTime || startTime;
2014
- let status = event.status || event.data?.status || null;
2015
- let inputData = null;
2016
- if (event.input) {
2017
- inputData = safeStringify2({ input: event.input });
2018
- } else if (event.data?.input) {
2019
- inputData = safeStringify2({ input: event.data.input });
2020
- } else if (input) {
2021
- inputData = safeStringify2({ input });
2022
- }
2023
- input = "";
2024
- let metadata = null;
2025
- if (event.metadata) {
2026
- metadata = safeStringify2(event.metadata);
2027
- } else if (event.data) {
2028
- metadata = safeStringify2({
2029
- id: event.affectedNodeId?.split("_").pop(),
2030
- agentId: event.data?.metadata?.sourceAgentId,
2031
- ...event.data
2032
- });
2033
- }
2034
- if (eventType === "agent") {
2035
- if (eventName === "start") {
2036
- eventName = "agent:start";
2037
- status = "running";
2038
- } else if (eventName === "finished") {
2039
- if (event.data.status === "error") {
2040
- eventName = "agent:error";
2041
- } else {
2042
- eventName = "agent:success";
2043
- }
2044
- }
2045
- await this.client.execute({
2046
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2047
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2048
- status, status_message, level, version, parent_event_id,
2049
- tags, input, output, error, metadata)
2050
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2051
- args: [
2052
- eventId,
2053
- id,
2054
- valueObj._agentId || agentId,
2055
- eventType,
2056
- eventName,
2057
- startTime,
2058
- endTime,
2059
- // @ts-ignore
2060
- status,
2061
- eventName === "agent:error" ? event.data.error.message : null,
2062
- event.level || "INFO",
2063
- event.version || null,
2064
- event.parentEventId || null,
2065
- null,
2066
- // tags
2067
- inputData,
2068
- event.data.output ? safeStringify2(event.data.output) : null,
2069
- eventName === "agent:error" ? safeStringify2(event.data.error) : null,
2070
- metadata
2071
- ]
2072
- });
2073
- } else if (eventType === "memory") {
2074
- if (eventName === "memory:saveMessage") {
2075
- await this.client.execute({
2076
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2077
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2078
- status, status_message, level, version, parent_event_id,
2079
- tags, input, output, error, metadata)
2080
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2081
- args: [
2082
- eventId,
2083
- id,
2084
- valueObj._agentId || agentId,
2085
- eventType,
2086
- "memory:write_start",
2087
- startTime,
2088
- null,
2089
- // no endTime
2090
- "running",
2091
- event.statusMessage || null,
2092
- event.level || "INFO",
2093
- event.version || null,
2094
- event.parentEventId || null,
2095
- null,
2096
- // tags
2097
- inputData,
2098
- null,
2099
- // no output
2100
- null,
2101
- // no error
2102
- safeStringify2({
2103
- id: "memory",
2104
- agentId: event.affectedNodeId?.split("_").pop()
2105
- })
2106
- ]
2107
- });
2108
- await this.client.execute({
2109
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2110
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2111
- status, status_message, level, version, parent_event_id,
2112
- tags, input, output, error, metadata)
2113
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2114
- args: [
2115
- this.generateId(),
2116
- // New ID
2117
- id,
2118
- valueObj._agentId || agentId,
2119
- eventType,
2120
- "memory:write_success",
2121
- endTime,
2122
- // End time
2123
- endTime,
2124
- "completed",
2125
- event.statusMessage || null,
2126
- event.level || "INFO",
2127
- event.version || null,
2128
- eventId,
2129
- // Parent event ID
2130
- null,
2131
- // tags
2132
- inputData,
2133
- event.data.output ? safeStringify2(event.data.output) : null,
2134
- event.error ? safeStringify2(event.error) : null,
2135
- safeStringify2({
2136
- id: "memory",
2137
- agentId: event.affectedNodeId?.split("_").pop()
2138
- })
2139
- ]
2140
- });
2141
- } else if (eventName === "memory:getMessages") {
2142
- await this.client.execute({
2143
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2144
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2145
- status, status_message, level, version, parent_event_id,
2146
- tags, input, output, error, metadata)
2147
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2148
- args: [
2149
- eventId,
2150
- id,
2151
- valueObj._agentId || agentId,
2152
- eventType,
2153
- "memory:read_start",
2154
- startTime,
2155
- null,
2156
- // no endTime
2157
- "running",
2158
- event.statusMessage || null,
2159
- event.level || "INFO",
2160
- event.version || null,
2161
- event.parentEventId || null,
2162
- null,
2163
- // tags
2164
- inputData,
2165
- null,
2166
- // no output
2167
- null,
2168
- // no error
2169
- safeStringify2({
2170
- id: "memory",
2171
- agentId: event.affectedNodeId?.split("_").pop()
2172
- })
2173
- ]
2174
- });
2175
- await this.client.execute({
2176
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2177
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2178
- status, status_message, level, version, parent_event_id,
2179
- tags, input, output, error, metadata)
2180
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2181
- args: [
2182
- this.generateId(),
2183
- // New ID
2184
- id,
2185
- valueObj._agentId || agentId,
2186
- eventType,
2187
- "memory:read_success",
2188
- endTime,
2189
- // End time
2190
- endTime,
2191
- status,
2192
- event.statusMessage || null,
2193
- event.level || "INFO",
2194
- event.version || null,
2195
- eventId,
2196
- // Parent event ID
2197
- null,
2198
- // tags
2199
- inputData,
2200
- event.data.output ? safeStringify2(event.data.output) : null,
2201
- event.error ? safeStringify2(event.error) : null,
2202
- safeStringify2({
2203
- id: "memory",
2204
- agentId: event.affectedNodeId?.split("_").pop()
2205
- })
2206
- ]
2207
- });
2208
- } else {
2209
- await this.client.execute({
2210
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2211
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2212
- status, status_message, level, version, parent_event_id,
2213
- tags, input, output, error, metadata)
2214
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2215
- args: [
2216
- eventId,
2217
- id,
2218
- valueObj._agentId || agentId,
2219
- eventType,
2220
- eventName,
2221
- startTime,
2222
- endTime,
2223
- status,
2224
- event.statusMessage || null,
2225
- event.level || "INFO",
2226
- event.version || null,
2227
- event.parentEventId || null,
2228
- null,
2229
- // tags
2230
- inputData,
2231
- event.output ? safeStringify2(event.output) : null,
2232
- event.error ? safeStringify2(event.error) : null,
2233
- metadata
2234
- ]
2235
- });
2236
- }
2237
- } else if (eventType === "tool") {
2238
- if (eventName === "tool_working") {
2239
- await this.client.execute({
2240
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2241
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2242
- status, status_message, level, version, parent_event_id,
2243
- tags, input, output, error, metadata)
2244
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2245
- args: [
2246
- eventId,
2247
- id,
2248
- valueObj._agentId || agentId,
2249
- eventType,
2250
- "tool:start",
2251
- startTime,
2252
- null,
2253
- // no endTime
2254
- "running",
2255
- event.statusMessage || null,
2256
- event.level || "INFO",
2257
- event.version || null,
2258
- event.parentEventId || null,
2259
- null,
2260
- // tags
2261
- inputData,
2262
- null,
2263
- // no output
2264
- null,
2265
- // no error
2266
- safeStringify2({
2267
- id: event.affectedNodeId?.split("_").pop(),
2268
- agentId: event.data?.metadata?.sourceAgentId,
2269
- displayName: event.data.metadata.toolName
2270
- })
2271
- ]
2272
- });
2273
- await this.client.execute({
2274
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2275
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2276
- status, status_message, level, version, parent_event_id,
2277
- tags, input, output, error, metadata)
2278
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2279
- args: [
2280
- this.generateId(),
2281
- // New ID
2282
- id,
2283
- valueObj._agentId || agentId,
2284
- eventType,
2285
- "tool:success",
2286
- endTime,
2287
- // End time
2288
- endTime,
2289
- "completed",
2290
- event.statusMessage || null,
2291
- event.level || "INFO",
2292
- event.version || null,
2293
- eventId,
2294
- // Parent event ID
2295
- null,
2296
- // tags
2297
- inputData,
2298
- event.data.output ? safeStringify2(event.data.output) : null,
2299
- event.error ? safeStringify2(event.error) : null,
2300
- safeStringify2({
2301
- id: event.affectedNodeId?.split("_").pop(),
2302
- agentId: event.data?.metadata?.sourceAgentId,
2303
- displayName: event.data.metadata.toolName
2304
- })
2305
- ]
2306
- });
2307
- }
2308
- } else {
2309
- await this.client.execute({
2310
- sql: `INSERT OR REPLACE INTO ${timelineEventsTableName}
2311
- (id, history_id, agent_id, event_type, event_name, start_time, end_time,
2312
- status, status_message, level, version, parent_event_id,
2313
- tags, input, output, error, metadata)
2314
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2315
- args: [
2316
- eventId,
2317
- id,
2318
- valueObj._agentId || agentId,
2319
- eventType,
2320
- eventName,
2321
- startTime,
2322
- endTime,
2323
- status,
2324
- event.statusMessage || null,
2325
- event.level || "INFO",
2326
- event.version || null,
2327
- event.parentEventId || null,
2328
- null,
2329
- // tags
2330
- inputData,
2331
- event.output ? safeStringify2(event.output) : null,
2332
- event.error ? safeStringify2(event.error) : null,
2333
- safeStringify2({
2334
- id: eventType === "retriever" ? "retriever" : event.type,
2335
- agentId: event.affectedNodeId?.split("_").pop()
2336
- })
2337
- ]
2338
- });
2339
- }
2340
- } catch (error) {
2341
- this.debug("Error processing event:", error);
2342
- }
2343
- }
1830
+ if (this.dimensions === null) {
1831
+ this.dimensions = item.vector.length;
1832
+ } else if (item.vector.length !== this.dimensions) {
1833
+ throw new Error(
1834
+ `Vector dimension mismatch. Expected ${this.dimensions}, got ${item.vector.length}`
1835
+ );
2344
1836
  }
2345
- } catch (error) {
2346
- this.debug(`Error processing record with ID ${key}:`, error);
1837
+ const serializedVector = this.serializeVector(item.vector);
1838
+ const metadataJson = item.metadata ? JSON.stringify(item.metadata) : null;
1839
+ const content = item.content ?? null;
1840
+ stmts.push({
1841
+ sql: `INSERT OR REPLACE INTO ${tableName} (id, vector, dimensions, metadata, content, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
1842
+ args: [item.id, serializedVector, item.vector.length, metadataJson, content]
1843
+ });
2347
1844
  }
2348
- }
2349
- await this.client.execute(`DROP TABLE ${oldTableName};`);
2350
- await this.client.execute(`ALTER TABLE ${tempTableName} RENAME TO ${oldTableName};`);
2351
- await this.client.execute(`
2352
- CREATE INDEX IF NOT EXISTS idx_${oldTableName}_agent_id
2353
- ON ${oldTableName}(agent_id)
2354
- `);
2355
- await this.client.execute("COMMIT;");
2356
- this.debug(`Total ${migratedCount} records successfully migrated`);
2357
- if (createBackup && deleteBackupAfterSuccess) {
2358
- await this.client.execute(`DROP TABLE IF EXISTS ${oldTableBackup};`);
2359
- this.debug("Unnecessary backup deleted");
2360
- }
2361
- await this.setMigrationFlag("agent_history_data_migration", migratedCount);
2362
- return {
2363
- success: true,
2364
- migratedCount,
2365
- backupCreated: createBackup && !deleteBackupAfterSuccess
2366
- };
2367
- } catch (error) {
2368
- await this.client.execute("ROLLBACK;");
2369
- this.debug("Error occurred while migrating agent history data:", error);
2370
- return {
2371
- success: false,
2372
- error: error instanceof Error ? error : new Error(String(error)),
2373
- backupCreated: options.createBackup
2374
- };
1845
+ await this.client.batch(stmts, "write");
1846
+ }, `storeBatch ${batch.length} vectors`);
1847
+ this.logger.debug(`Batch of ${batch.length} vectors stored`);
2375
1848
  }
2376
1849
  }
2377
1850
  /**
2378
- * Migrate conversation schema to add user_id and update messages table
2379
- *
2380
- * ⚠️ **CRITICAL WARNING: DESTRUCTIVE OPERATION** ⚠️
2381
- *
2382
- * This method performs a DESTRUCTIVE schema migration that:
2383
- * - DROPS and recreates existing tables
2384
- * - Creates temporary tables during migration
2385
- * - Modifies the primary key structure of the messages table
2386
- * - Can cause DATA LOSS if interrupted or if errors occur
2387
- *
2388
- * **IMPORTANT SAFETY REQUIREMENTS:**
2389
- * - 🛑 STOP all application instances before running this migration
2390
- * - 🛑 Ensure NO concurrent database operations are running
2391
- * - 🛑 Take a full database backup before running (independent of built-in backup)
2392
- * - 🛑 Test the migration on a copy of production data first
2393
- * - 🛑 Plan for downtime during migration execution
2394
- *
2395
- * **What this migration does:**
2396
- * 1. Creates backup tables (if createBackup=true)
2397
- * 2. Creates temporary tables with new schema
2398
- * 3. Migrates data from old tables to new schema
2399
- * 4. DROPS original tables
2400
- * 5. Renames temporary tables to original names
2401
- * 6. All operations are wrapped in a transaction for atomicity
2402
- *
2403
- * @param options Migration configuration options
2404
- * @param options.createBackup Whether to create backup tables before migration (default: true, HIGHLY RECOMMENDED)
2405
- * @param options.restoreFromBackup Whether to restore from existing backup instead of migrating (default: false)
2406
- * @param options.deleteBackupAfterSuccess Whether to delete backup tables after successful migration (default: false)
2407
- *
2408
- * @returns Promise resolving to migration result with success status, migrated count, and backup info
2409
- *
2410
- * @example
2411
- * ```typescript
2412
- * // RECOMMENDED: Run with backup creation (default)
2413
- * const result = await storage.migrateConversationSchema({
2414
- * createBackup: true,
2415
- * deleteBackupAfterSuccess: false // Keep backup for safety
2416
- * });
2417
- *
2418
- * if (result.success) {
2419
- * console.log(`Migrated ${result.migratedCount} conversations successfully`);
2420
- * } else {
2421
- * console.error('Migration failed:', result.error);
2422
- * // Consider restoring from backup
2423
- * }
2424
- *
2425
- * // If migration fails, restore from backup:
2426
- * const restoreResult = await storage.migrateConversationSchema({
2427
- * restoreFromBackup: true
2428
- * });
2429
- * ```
2430
- *
2431
- * @throws {Error} If migration fails and transaction is rolled back
2432
- *
2433
- * @since This migration is typically only needed when upgrading from older schema versions
1851
+ * Search for similar vectors using cosine similarity
2434
1852
  */
2435
- async migrateConversationSchema(options = {}) {
2436
- const {
2437
- createBackup = true,
2438
- restoreFromBackup = false,
2439
- deleteBackupAfterSuccess = false
2440
- } = options;
2441
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
2442
- const messagesTableName = `${this.options.tablePrefix}_messages`;
2443
- const conversationsBackupName = `${conversationsTableName}_backup`;
2444
- const messagesBackupName = `${messagesTableName}_backup`;
2445
- try {
2446
- this.debug("Starting conversation schema migration...");
2447
- const flagCheck = await this.checkMigrationFlag("conversation_schema_migration");
2448
- if (flagCheck.alreadyCompleted) {
2449
- return { success: true, migratedCount: 0 };
2450
- }
2451
- if (restoreFromBackup) {
2452
- this.debug("Starting restoration from backup...");
2453
- const convBackupCheck = await this.client.execute({
2454
- sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
2455
- args: [conversationsBackupName]
2456
- });
2457
- const msgBackupCheck = await this.client.execute({
2458
- sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
2459
- args: [messagesBackupName]
2460
- });
2461
- if (convBackupCheck.rows.length === 0 || msgBackupCheck.rows.length === 0) {
2462
- throw new Error("No backup found to restore");
2463
- }
2464
- await this.client.execute("BEGIN TRANSACTION;");
2465
- await this.client.execute(`DROP TABLE IF EXISTS ${conversationsTableName};`);
2466
- await this.client.execute(`DROP TABLE IF EXISTS ${messagesTableName};`);
2467
- await this.client.execute(
2468
- `ALTER TABLE ${conversationsBackupName} RENAME TO ${conversationsTableName};`
2469
- );
2470
- await this.client.execute(
2471
- `ALTER TABLE ${messagesBackupName} RENAME TO ${messagesTableName};`
2472
- );
2473
- await this.client.execute("COMMIT;");
2474
- this.debug("Restoration from backup completed successfully");
2475
- return { success: true, backupCreated: false };
2476
- }
2477
- const convTableInfo = await this.client.execute(
2478
- `PRAGMA table_info(${conversationsTableName})`
1853
+ async search(queryVector, options) {
1854
+ await this.initialize();
1855
+ const { limit = 10, threshold = 0, filter } = options || {};
1856
+ if (this.dimensions !== null && queryVector.length !== this.dimensions) {
1857
+ throw new Error(
1858
+ `Query vector dimension mismatch. Expected ${this.dimensions}, got ${queryVector.length}`
2479
1859
  );
2480
- const msgTableInfo = await this.client.execute(`PRAGMA table_info(${messagesTableName})`);
2481
- const hasUserIdInConversations = convTableInfo.rows.some((row) => row.name === "user_id");
2482
- const hasUserIdInMessages = msgTableInfo.rows.some((row) => row.name === "user_id");
2483
- if (hasUserIdInConversations && !hasUserIdInMessages) {
2484
- this.debug("Tables are already in new format, migration not needed");
2485
- return { success: true, migratedCount: 0 };
2486
- }
2487
- if (convTableInfo.rows.length === 0 && msgTableInfo.rows.length === 0) {
2488
- this.debug("Tables don't exist, migration not needed");
2489
- return { success: true, migratedCount: 0 };
2490
- }
2491
- if (createBackup) {
2492
- this.debug("Creating backups...");
2493
- await this.client.execute(`DROP TABLE IF EXISTS ${conversationsBackupName};`);
2494
- await this.client.execute(`DROP TABLE IF EXISTS ${messagesBackupName};`);
2495
- if (convTableInfo.rows.length > 0) {
2496
- await this.client.execute(
2497
- `CREATE TABLE ${conversationsBackupName} AS SELECT * FROM ${conversationsTableName};`
2498
- );
2499
- }
2500
- if (msgTableInfo.rows.length > 0) {
2501
- await this.client.execute(
2502
- `CREATE TABLE ${messagesBackupName} AS SELECT * FROM ${messagesTableName};`
2503
- );
2504
- }
2505
- this.debug("Backups created successfully");
2506
- }
2507
- let conversationData = [];
2508
- let messageData = [];
2509
- if (convTableInfo.rows.length > 0) {
2510
- const convResult = await this.client.execute(`SELECT * FROM ${conversationsTableName}`);
2511
- conversationData = convResult.rows;
2512
- }
2513
- if (msgTableInfo.rows.length > 0) {
2514
- const msgResult = await this.client.execute(`SELECT * FROM ${messagesTableName}`);
2515
- messageData = msgResult.rows;
1860
+ }
1861
+ const tableName = `${this.tablePrefix}_vectors`;
1862
+ let query = `SELECT id, vector, dimensions, metadata, content FROM ${tableName}`;
1863
+ const args = [];
1864
+ if (this.dimensions !== null) {
1865
+ query += " WHERE dimensions = ?";
1866
+ args.push(this.dimensions);
1867
+ }
1868
+ const result = await this.executeWithRetry(
1869
+ async () => await this.client.execute({ sql: query, args }),
1870
+ "search vectors"
1871
+ );
1872
+ const searchResults = [];
1873
+ for (const row of result.rows) {
1874
+ const id = row.id;
1875
+ const vectorBlob = row.vector;
1876
+ const metadataJson = row.metadata;
1877
+ const content = row.content ?? void 0;
1878
+ const metadata = metadataJson ? JSON.parse(metadataJson) : void 0;
1879
+ if (filter && !this.matchesFilter(metadata, filter)) {
1880
+ continue;
2516
1881
  }
2517
- await this.client.execute("BEGIN TRANSACTION;");
2518
- const tempConversationsTable = `${conversationsTableName}_temp`;
2519
- const tempMessagesTable = `${messagesTableName}_temp`;
2520
- await this.client.execute(`
2521
- CREATE TABLE ${tempConversationsTable} (
2522
- id TEXT PRIMARY KEY,
2523
- resource_id TEXT NOT NULL,
2524
- user_id TEXT NOT NULL,
2525
- title TEXT NOT NULL,
2526
- metadata TEXT NOT NULL,
2527
- created_at TEXT NOT NULL,
2528
- updated_at TEXT NOT NULL
2529
- )
2530
- `);
2531
- await this.client.execute(`
2532
- CREATE TABLE ${tempMessagesTable} (
2533
- conversation_id TEXT NOT NULL,
2534
- message_id TEXT NOT NULL,
2535
- role TEXT NOT NULL,
2536
- content TEXT NOT NULL,
2537
- type TEXT NOT NULL,
2538
- created_at TEXT NOT NULL,
2539
- PRIMARY KEY (conversation_id, message_id)
2540
- )
2541
- `);
2542
- let migratedCount = 0;
2543
- const createdConversations = /* @__PURE__ */ new Set();
2544
- for (const row of messageData) {
2545
- const conversationId = row.conversation_id;
2546
- let userId = "default";
2547
- if (hasUserIdInMessages && row.user_id) {
2548
- userId = row.user_id;
2549
- }
2550
- if (!createdConversations.has(conversationId)) {
2551
- const existingConversation = conversationData.find((conv) => conv.id === conversationId);
2552
- if (existingConversation) {
2553
- let convUserId = userId;
2554
- if (hasUserIdInConversations && existingConversation.user_id) {
2555
- convUserId = existingConversation.user_id;
2556
- }
2557
- await this.client.execute({
2558
- sql: `INSERT INTO ${tempConversationsTable}
2559
- (id, resource_id, user_id, title, metadata, created_at, updated_at)
2560
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
2561
- args: [
2562
- existingConversation.id,
2563
- existingConversation.resource_id,
2564
- convUserId,
2565
- existingConversation.title,
2566
- existingConversation.metadata,
2567
- existingConversation.created_at,
2568
- existingConversation.updated_at
2569
- ]
2570
- });
2571
- } else {
2572
- const now = (/* @__PURE__ */ new Date()).toISOString();
2573
- await this.client.execute({
2574
- sql: `INSERT INTO ${tempConversationsTable}
2575
- (id, resource_id, user_id, title, metadata, created_at, updated_at)
2576
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
2577
- args: [
2578
- conversationId,
2579
- "default",
2580
- // Default resource_id for auto-created conversations
2581
- userId,
2582
- "Migrated Conversation",
2583
- // Default title
2584
- safeStringify2({}),
2585
- // Empty metadata
2586
- now,
2587
- now
2588
- ]
2589
- });
2590
- }
2591
- createdConversations.add(conversationId);
2592
- migratedCount++;
2593
- }
2594
- await this.client.execute({
2595
- sql: `INSERT INTO ${tempMessagesTable}
2596
- (conversation_id, message_id, role, content, type, created_at)
2597
- VALUES (?, ?, ?, ?, ?, ?)`,
2598
- args: [
2599
- row.conversation_id,
2600
- row.message_id,
2601
- row.role,
2602
- row.content,
2603
- row.type,
2604
- row.created_at
2605
- ]
1882
+ const vector = this.deserializeVector(vectorBlob);
1883
+ const similarity = cosineSimilarity(queryVector, vector);
1884
+ const score = (similarity + 1) / 2;
1885
+ if (score >= threshold) {
1886
+ searchResults.push({
1887
+ id,
1888
+ vector,
1889
+ metadata,
1890
+ content,
1891
+ score,
1892
+ distance: 1 - similarity
1893
+ // Convert to distance metric
2606
1894
  });
2607
1895
  }
2608
- for (const row of conversationData) {
2609
- const conversationId = row.id;
2610
- if (!createdConversations.has(conversationId)) {
2611
- let userId = "default";
2612
- if (hasUserIdInConversations && row.user_id) {
2613
- userId = row.user_id;
2614
- }
2615
- await this.client.execute({
2616
- sql: `INSERT INTO ${tempConversationsTable}
2617
- (id, resource_id, user_id, title, metadata, created_at, updated_at)
2618
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
2619
- args: [
2620
- row.id,
2621
- row.resource_id,
2622
- userId,
2623
- row.title,
2624
- row.metadata,
2625
- row.created_at,
2626
- row.updated_at
2627
- ]
2628
- });
2629
- migratedCount++;
2630
- }
2631
- }
2632
- await this.client.execute(`DROP TABLE IF EXISTS ${conversationsTableName};`);
2633
- await this.client.execute(`DROP TABLE IF EXISTS ${messagesTableName};`);
2634
- await this.client.execute(
2635
- `ALTER TABLE ${tempConversationsTable} RENAME TO ${conversationsTableName};`
2636
- );
2637
- await this.client.execute(`ALTER TABLE ${tempMessagesTable} RENAME TO ${messagesTableName};`);
2638
- await this.client.execute(`
2639
- CREATE INDEX IF NOT EXISTS idx_${messagesTableName}_lookup
2640
- ON ${messagesTableName}(conversation_id, created_at)
2641
- `);
2642
- await this.client.execute(`
2643
- CREATE INDEX IF NOT EXISTS idx_${conversationsTableName}_resource
2644
- ON ${conversationsTableName}(resource_id)
2645
- `);
2646
- await this.client.execute(`
2647
- CREATE INDEX IF NOT EXISTS idx_${conversationsTableName}_user
2648
- ON ${conversationsTableName}(user_id)
2649
- `);
2650
- await this.client.execute("COMMIT;");
2651
- if (deleteBackupAfterSuccess) {
2652
- await this.client.execute(`DROP TABLE IF EXISTS ${conversationsBackupName};`);
2653
- await this.client.execute(`DROP TABLE IF EXISTS ${messagesBackupName};`);
2654
- }
2655
- await this.setMigrationFlag("conversation_schema_migration", migratedCount);
2656
- this.debug(
2657
- `Conversation schema migration completed successfully. Migrated ${migratedCount} conversations.`
2658
- );
2659
- return {
2660
- success: true,
2661
- migratedCount,
2662
- backupCreated: createBackup
2663
- };
2664
- } catch (error) {
2665
- this.debug("Error during conversation schema migration:", error);
2666
- try {
2667
- await this.client.execute("ROLLBACK;");
2668
- } catch (rollbackError) {
2669
- this.debug("Error rolling back transaction:", rollbackError);
1896
+ }
1897
+ searchResults.sort((a, b) => b.score - a.score);
1898
+ return searchResults.slice(0, limit);
1899
+ }
1900
+ /**
1901
+ * Check if metadata matches the filter criteria
1902
+ */
1903
+ matchesFilter(metadata, filter) {
1904
+ if (!metadata) {
1905
+ return false;
1906
+ }
1907
+ for (const [key, value] of Object.entries(filter)) {
1908
+ if (metadata[key] !== value) {
1909
+ return false;
2670
1910
  }
2671
- return {
2672
- success: false,
2673
- error,
2674
- backupCreated: createBackup
2675
- };
2676
1911
  }
1912
+ return true;
2677
1913
  }
2678
1914
  /**
2679
- * Get conversations for a user with a fluent query builder interface
2680
- * @param userId User ID to filter by
2681
- * @returns Query builder object
1915
+ * Delete a vector by ID
2682
1916
  */
2683
- getUserConversations(userId) {
2684
- return {
2685
- /**
2686
- * Limit the number of results
2687
- * @param count Number of conversations to return
2688
- * @returns Query builder
2689
- */
2690
- limit: /* @__PURE__ */ __name((count) => ({
2691
- /**
2692
- * Order results by a specific field
2693
- * @param field Field to order by
2694
- * @param direction Sort direction
2695
- * @returns Query builder
2696
- */
2697
- orderBy: /* @__PURE__ */ __name((field = "updated_at", direction = "DESC") => ({
2698
- /**
2699
- * Execute the query and return results
2700
- * @returns Promise of conversations
2701
- */
2702
- execute: /* @__PURE__ */ __name(() => this.getConversationsByUserId(userId, {
2703
- limit: count,
2704
- orderBy: field,
2705
- orderDirection: direction
2706
- }), "execute")
2707
- }), "orderBy"),
2708
- /**
2709
- * Execute the query with default ordering
2710
- * @returns Promise of conversations
2711
- */
2712
- execute: /* @__PURE__ */ __name(() => this.getConversationsByUserId(userId, { limit: count }), "execute")
2713
- }), "limit"),
2714
- /**
2715
- * Order results by a specific field
2716
- * @param field Field to order by
2717
- * @param direction Sort direction
2718
- * @returns Query builder
2719
- */
2720
- orderBy: /* @__PURE__ */ __name((field = "updated_at", direction = "DESC") => ({
2721
- /**
2722
- * Limit the number of results
2723
- * @param count Number of conversations to return
2724
- * @returns Query builder
2725
- */
2726
- limit: /* @__PURE__ */ __name((count) => ({
2727
- /**
2728
- * Execute the query and return results
2729
- * @returns Promise of conversations
2730
- */
2731
- execute: /* @__PURE__ */ __name(() => this.getConversationsByUserId(userId, {
2732
- limit: count,
2733
- orderBy: field,
2734
- orderDirection: direction
2735
- }), "execute")
2736
- }), "limit"),
2737
- /**
2738
- * Execute the query without limit
2739
- * @returns Promise of conversations
2740
- */
2741
- execute: /* @__PURE__ */ __name(() => this.getConversationsByUserId(userId, {
2742
- orderBy: field,
2743
- orderDirection: direction
2744
- }), "execute")
2745
- }), "orderBy"),
2746
- /**
2747
- * Execute the query with default options
2748
- * @returns Promise of conversations
2749
- */
2750
- execute: /* @__PURE__ */ __name(() => this.getConversationsByUserId(userId), "execute")
2751
- };
1917
+ async delete(id) {
1918
+ await this.initialize();
1919
+ const tableName = `${this.tablePrefix}_vectors`;
1920
+ await this.executeWithRetry(async () => {
1921
+ await this.client.execute({
1922
+ sql: `DELETE FROM ${tableName} WHERE id = ?`,
1923
+ args: [id]
1924
+ });
1925
+ }, `delete vector ${id}`);
1926
+ this.vectorCache.delete(id);
1927
+ this.logger.debug(`Vector deleted: ${id}`);
2752
1928
  }
2753
1929
  /**
2754
- * Get conversation by ID and ensure it belongs to the specified user
2755
- * @param conversationId Conversation ID
2756
- * @param userId User ID to validate ownership
2757
- * @returns Conversation or null
1930
+ * Delete multiple vectors by IDs
2758
1931
  */
2759
- async getUserConversation(conversationId, userId) {
2760
- const conversation = await this.getConversation(conversationId);
2761
- if (!conversation || conversation.userId !== userId) {
2762
- return null;
1932
+ async deleteBatch(ids) {
1933
+ await this.initialize();
1934
+ if (ids.length === 0) return;
1935
+ const tableName = `${this.tablePrefix}_vectors`;
1936
+ for (let i = 0; i < ids.length; i += this.batchSize) {
1937
+ const batch = ids.slice(i, i + this.batchSize);
1938
+ const placeholders = batch.map(() => "?").join(",");
1939
+ await this.executeWithRetry(async () => {
1940
+ await this.client.execute({
1941
+ sql: `DELETE FROM ${tableName} WHERE id IN (${placeholders})`,
1942
+ args: batch
1943
+ });
1944
+ }, `deleteBatch ${batch.length} vectors`);
1945
+ for (const id of batch) {
1946
+ this.vectorCache.delete(id);
1947
+ }
1948
+ this.logger.debug(`Batch of ${batch.length} vectors deleted`);
2763
1949
  }
2764
- return conversation;
2765
1950
  }
2766
1951
  /**
2767
- * Get paginated conversations for a user
2768
- * @param userId User ID
2769
- * @param page Page number (1-based)
2770
- * @param pageSize Number of items per page
2771
- * @returns Object with conversations and pagination info
1952
+ * Clear all vectors
2772
1953
  */
2773
- async getPaginatedUserConversations(userId, page = 1, pageSize = 10) {
2774
- const offset = (page - 1) * pageSize;
2775
- const conversations = await this.getConversationsByUserId(userId, {
2776
- limit: pageSize + 1,
2777
- offset,
2778
- orderBy: "updated_at",
2779
- orderDirection: "DESC"
2780
- });
2781
- const hasMore = conversations.length > pageSize;
2782
- const results = hasMore ? conversations.slice(0, pageSize) : conversations;
2783
- return {
2784
- conversations: results,
2785
- page,
2786
- pageSize,
2787
- hasMore
2788
- };
1954
+ async clear() {
1955
+ await this.initialize();
1956
+ const tableName = `${this.tablePrefix}_vectors`;
1957
+ await this.executeWithRetry(async () => {
1958
+ await this.client.execute(`DELETE FROM ${tableName}`);
1959
+ }, "clear all vectors");
1960
+ this.vectorCache.clear();
1961
+ this.dimensions = null;
1962
+ this.logger.debug("All vectors cleared");
2789
1963
  }
2790
1964
  /**
2791
- * Check and create migration flag table, return if migration already completed
2792
- * @param migrationType Type of migration to check
2793
- * @returns Object with completion status and details
1965
+ * Get total count of stored vectors
2794
1966
  */
2795
- async checkMigrationFlag(migrationType) {
2796
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
2797
- const migrationFlagTable = `${conversationsTableName}_migration_flags`;
2798
- try {
2799
- const result = await this.client.execute({
2800
- sql: `SELECT * FROM ${migrationFlagTable} WHERE migration_type = ?`,
2801
- args: [migrationType]
2802
- });
2803
- if (result.rows.length > 0) {
2804
- const migrationFlag = result.rows[0];
2805
- this.debug(`${migrationType} migration already completed`);
2806
- this.debug(`Migration completed on: ${migrationFlag.completed_at}`);
2807
- this.debug(`Migrated ${migrationFlag.migrated_count || 0} records previously`);
1967
+ async count() {
1968
+ await this.initialize();
1969
+ const tableName = `${this.tablePrefix}_vectors`;
1970
+ const result = await this.executeWithRetry(
1971
+ async () => await this.client.execute(`SELECT COUNT(*) as count FROM ${tableName}`),
1972
+ "count vectors"
1973
+ );
1974
+ const raw = result.rows[0]?.count;
1975
+ if (typeof raw === "bigint") return Number(raw);
1976
+ if (typeof raw === "string") return Number.parseInt(raw, 10) || 0;
1977
+ return raw ?? 0;
1978
+ }
1979
+ /**
1980
+ * Get a specific vector by ID
1981
+ */
1982
+ async get(id) {
1983
+ await this.initialize();
1984
+ if (this.vectorCache.has(id)) {
1985
+ const cached = this.vectorCache.get(id);
1986
+ if (cached) {
2808
1987
  return {
2809
- alreadyCompleted: true,
2810
- migrationCount: migrationFlag.migrated_count,
2811
- completedAt: migrationFlag.completed_at
1988
+ ...cached,
1989
+ vector: [...cached.vector],
1990
+ // Return a copy
1991
+ metadata: cached.metadata ? { ...cached.metadata } : void 0
2812
1992
  };
2813
1993
  }
2814
- this.debug("Migration flags table found, but no migration flag exists yet");
2815
- return { alreadyCompleted: false };
2816
- } catch (flagError) {
2817
- this.debug("Migration flag table not found, creating it...");
2818
- this.debug("Original error:", flagError);
2819
- try {
2820
- await this.client.execute(`
2821
- CREATE TABLE IF NOT EXISTS ${migrationFlagTable} (
2822
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2823
- migration_type TEXT NOT NULL UNIQUE,
2824
- completed_at TEXT NOT NULL DEFAULT (datetime('now')),
2825
- migrated_count INTEGER DEFAULT 0,
2826
- metadata TEXT DEFAULT '{}'
2827
- )
2828
- `);
2829
- this.debug("Migration flags table created successfully");
2830
- } catch (createError) {
2831
- this.debug("Failed to create migration flags table:", createError);
2832
- }
2833
- return { alreadyCompleted: false };
2834
1994
  }
2835
- }
2836
- /**
2837
- * Set migration flag after successful completion
2838
- * @param migrationType Type of migration completed
2839
- * @param migratedCount Number of records migrated
2840
- */
2841
- async setMigrationFlag(migrationType, migratedCount) {
2842
- try {
2843
- const conversationsTableName = `${this.options.tablePrefix}_conversations`;
2844
- const migrationFlagTable = `${conversationsTableName}_migration_flags`;
2845
- await this.client.execute({
2846
- sql: `INSERT OR REPLACE INTO ${migrationFlagTable}
2847
- (migration_type, completed_at, migrated_count)
2848
- VALUES (?, datetime('now'), ?)`,
2849
- args: [migrationType, migratedCount]
2850
- });
2851
- this.debug("Migration flag set successfully");
2852
- } catch (flagSetError) {
2853
- this.debug("Could not set migration flag (non-critical):", flagSetError);
1995
+ const tableName = `${this.tablePrefix}_vectors`;
1996
+ const result = await this.executeWithRetry(
1997
+ async () => await this.client.execute({
1998
+ sql: `SELECT id, vector, metadata, content FROM ${tableName} WHERE id = ?`,
1999
+ args: [id]
2000
+ }),
2001
+ `get vector ${id}`
2002
+ );
2003
+ if (result.rows.length === 0) {
2004
+ return null;
2005
+ }
2006
+ const row = result.rows[0];
2007
+ const vectorBlob = row.vector;
2008
+ const metadataJson = row.metadata;
2009
+ const content = row.content;
2010
+ const vector = this.deserializeVector(vectorBlob);
2011
+ const metadata = metadataJson ? JSON.parse(metadataJson) : void 0;
2012
+ const item = {
2013
+ id,
2014
+ vector,
2015
+ metadata,
2016
+ content: content ?? void 0
2017
+ };
2018
+ if (this.vectorCache.size >= this.cacheSize) {
2019
+ const firstKey = this.vectorCache.keys().next().value;
2020
+ if (firstKey) this.vectorCache.delete(firstKey);
2854
2021
  }
2022
+ this.vectorCache.set(id, item);
2023
+ return item;
2855
2024
  }
2856
2025
  /**
2857
- * Migrate agent history schema to add userId and conversationId columns
2026
+ * Close the database connection
2858
2027
  */
2859
- async migrateAgentHistorySchema() {
2860
- const historyTableName = `${this.options.tablePrefix}_agent_history`;
2028
+ async close() {
2029
+ this.vectorCache.clear();
2030
+ this.logger.debug("Vector adapter closed");
2861
2031
  try {
2862
- this.debug("Starting agent history schema migration...");
2863
- const flagCheck = await this.checkMigrationFlag("agent_history_schema_migration");
2864
- if (flagCheck.alreadyCompleted) {
2865
- return { success: true };
2866
- }
2867
- const tableInfo = await this.client.execute(`PRAGMA table_info(${historyTableName})`);
2868
- if (tableInfo.rows.length === 0) {
2869
- this.debug("Agent history table doesn't exist, migration not needed");
2870
- return { success: true };
2871
- }
2872
- const hasUserIdColumn = tableInfo.rows.some((row) => row.name === "userId");
2873
- const hasConversationIdColumn = tableInfo.rows.some((row) => row.name === "conversationId");
2874
- if (hasUserIdColumn && hasConversationIdColumn) {
2875
- this.debug("Both userId and conversationId columns already exist, skipping migration");
2876
- await this.setMigrationFlag("agent_history_schema_migration", 0);
2877
- return { success: true };
2878
- }
2879
- if (!hasUserIdColumn) {
2880
- await this.client.execute(`ALTER TABLE ${historyTableName} ADD COLUMN userId TEXT`);
2881
- this.debug("Added userId column to agent history table");
2882
- }
2883
- if (!hasConversationIdColumn) {
2884
- await this.client.execute(`ALTER TABLE ${historyTableName} ADD COLUMN conversationId TEXT`);
2885
- this.debug("Added conversationId column to agent history table");
2886
- }
2887
- if (!hasUserIdColumn) {
2888
- await this.client.execute(`
2889
- CREATE INDEX IF NOT EXISTS idx_${historyTableName}_userId
2890
- ON ${historyTableName}(userId)
2891
- `);
2892
- }
2893
- if (!hasConversationIdColumn) {
2894
- await this.client.execute(`
2895
- CREATE INDEX IF NOT EXISTS idx_${historyTableName}_conversationId
2896
- ON ${historyTableName}(conversationId)
2897
- `);
2898
- }
2899
- await this.setMigrationFlag("agent_history_schema_migration", 0);
2900
- this.debug("Agent history schema migration completed successfully");
2901
- return { success: true };
2902
- } catch (error) {
2903
- this.debug("Error during agent history schema migration:", error);
2904
- return {
2905
- success: false,
2906
- error
2907
- };
2032
+ this.client?.close?.();
2033
+ } catch {
2908
2034
  }
2909
2035
  }
2910
- // ===== WorkflowMemory Interface Implementation =====
2911
- // Delegate all workflow operations to the workflow extension
2912
- async storeWorkflowHistory(entry) {
2913
- await this.initialized;
2914
- return this.workflowExtension.storeWorkflowHistory(entry);
2915
- }
2916
- async getWorkflowHistory(id) {
2917
- await this.initialized;
2918
- return this.workflowExtension.getWorkflowHistory(id);
2919
- }
2920
- async getWorkflowHistoryByWorkflowId(workflowId) {
2921
- await this.initialized;
2922
- return this.workflowExtension.getWorkflowHistoryByWorkflowId(workflowId);
2923
- }
2924
- async updateWorkflowHistory(id, updates) {
2925
- await this.initialized;
2926
- return this.workflowExtension.updateWorkflowHistory(id, updates);
2927
- }
2928
- async deleteWorkflowHistory(id) {
2929
- await this.initialized;
2930
- return this.workflowExtension.deleteWorkflowHistory(id);
2931
- }
2932
- async storeWorkflowStep(step) {
2933
- await this.initialized;
2934
- return this.workflowExtension.storeWorkflowStep(step);
2935
- }
2936
- async getWorkflowStep(id) {
2937
- await this.initialized;
2938
- return this.workflowExtension.getWorkflowStep(id);
2939
- }
2940
- async getWorkflowSteps(workflowHistoryId) {
2941
- await this.initialized;
2942
- return this.workflowExtension.getWorkflowSteps(workflowHistoryId);
2943
- }
2944
- async updateWorkflowStep(id, updates) {
2945
- await this.initialized;
2946
- return this.workflowExtension.updateWorkflowStep(id, updates);
2947
- }
2948
- async deleteWorkflowStep(id) {
2949
- await this.initialized;
2950
- return this.workflowExtension.deleteWorkflowStep(id);
2951
- }
2952
- async storeWorkflowTimelineEvent(event) {
2953
- await this.initialized;
2954
- return this.workflowExtension.storeWorkflowTimelineEvent(event);
2955
- }
2956
- async getWorkflowTimelineEvent(id) {
2957
- await this.initialized;
2958
- return this.workflowExtension.getWorkflowTimelineEvent(id);
2959
- }
2960
- async getWorkflowTimelineEvents(workflowHistoryId) {
2961
- await this.initialized;
2962
- return this.workflowExtension.getWorkflowTimelineEvents(workflowHistoryId);
2963
- }
2964
- async deleteWorkflowTimelineEvent(id) {
2965
- await this.initialized;
2966
- return this.workflowExtension.deleteWorkflowTimelineEvent(id);
2967
- }
2968
- async getAllWorkflowIds() {
2969
- await this.initialized;
2970
- return this.workflowExtension.getAllWorkflowIds();
2971
- }
2972
- async getWorkflowStats(workflowId) {
2973
- await this.initialized;
2974
- return this.workflowExtension.getWorkflowStats(workflowId);
2975
- }
2976
- async getWorkflowHistoryWithStepsAndEvents(id) {
2977
- await this.initialized;
2978
- return this.workflowExtension.getWorkflowHistoryWithStepsAndEvents(id);
2979
- }
2980
- async deleteWorkflowHistoryWithRelated(id) {
2981
- await this.initialized;
2982
- return this.workflowExtension.deleteWorkflowHistoryWithRelated(id);
2983
- }
2984
- async cleanupOldWorkflowHistories(workflowId, maxEntries) {
2985
- await this.initialized;
2986
- return this.workflowExtension.cleanupOldWorkflowHistories(workflowId, maxEntries);
2987
- }
2988
2036
  /**
2989
- * Get the workflow extension for advanced workflow operations
2037
+ * Get statistics about the vector table and cache
2990
2038
  */
2991
- getWorkflowExtension() {
2992
- return this.workflowExtension;
2039
+ async getStats() {
2040
+ await this.initialize();
2041
+ const tableName = `${this.tablePrefix}_vectors`;
2042
+ const [countResult, sizeResult] = await Promise.all([
2043
+ this.executeWithRetry(
2044
+ async () => await this.client.execute(
2045
+ `SELECT COUNT(*) as count, MAX(dimensions) as dims FROM ${tableName}`
2046
+ ),
2047
+ "getStats count"
2048
+ ),
2049
+ // Approximate table size by summing blob/text lengths
2050
+ this.executeWithRetry(
2051
+ async () => await this.client.execute({
2052
+ sql: `SELECT
2053
+ COALESCE(SUM(LENGTH(id)),0) +
2054
+ COALESCE(SUM(LENGTH(vector)),0) +
2055
+ COALESCE(SUM(LENGTH(metadata)),0) +
2056
+ COALESCE(SUM(LENGTH(content)),0) AS size
2057
+ FROM ${tableName}`
2058
+ }),
2059
+ "getStats size"
2060
+ )
2061
+ ]);
2062
+ const row1 = countResult.rows[0];
2063
+ const row2 = sizeResult.rows[0];
2064
+ const countRaw = row1?.count;
2065
+ const dimsRaw = row1?.dims;
2066
+ const sizeRaw = row2?.size;
2067
+ const normalize = /* @__PURE__ */ __name((v) => typeof v === "bigint" ? Number(v) : typeof v === "string" ? Number.parseInt(v, 10) || 0 : v ?? 0, "normalize");
2068
+ return {
2069
+ count: normalize(countRaw),
2070
+ dimensions: dimsRaw != null ? normalize(dimsRaw) : this.dimensions,
2071
+ cacheSize: this.vectorCache.size,
2072
+ tableSizeBytes: normalize(sizeRaw)
2073
+ };
2993
2074
  }
2994
2075
  };
2995
2076
  export {
2996
- LibSQLStorage
2077
+ LibSQLMemoryAdapter,
2078
+ LibSQLObservabilityAdapter,
2079
+ LibSQLVectorAdapter
2997
2080
  };
2998
2081
  //# sourceMappingURL=index.mjs.map