@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.d.mts +338 -387
- package/dist/index.d.ts +338 -387
- package/dist/index.js +1766 -2675
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1754 -2671
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -5
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/
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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 {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
86
|
-
updated_at TEXT
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await
|
|
99
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
async
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Message Operations
|
|
293
|
+
// ============================================================================
|
|
263
294
|
/**
|
|
264
|
-
*
|
|
295
|
+
* Add a single message
|
|
265
296
|
*/
|
|
266
|
-
async
|
|
267
|
-
await this.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
*
|
|
324
|
+
* Add multiple messages
|
|
293
325
|
*/
|
|
294
|
-
async
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
*
|
|
356
|
+
* Apply storage limit to a conversation
|
|
304
357
|
*/
|
|
305
|
-
async
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
*
|
|
375
|
+
* Get messages with optional filtering
|
|
314
376
|
*/
|
|
315
|
-
async
|
|
316
|
-
this.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
args.push(
|
|
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 (
|
|
328
|
-
|
|
329
|
-
args.push(
|
|
389
|
+
if (before) {
|
|
390
|
+
sql += " AND created_at < ?";
|
|
391
|
+
args.push(before.toISOString());
|
|
330
392
|
}
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
args.push(
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
*
|
|
436
|
+
* Clear messages for a user
|
|
366
437
|
*/
|
|
367
|
-
async
|
|
368
|
-
await this.
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
*
|
|
461
|
+
* Create a new conversation
|
|
375
462
|
*/
|
|
376
|
-
async
|
|
377
|
-
await this.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
497
|
+
* Get a conversation by ID
|
|
410
498
|
*/
|
|
411
|
-
async
|
|
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 ${
|
|
503
|
+
sql: `SELECT * FROM ${conversationsTable} WHERE id = ?`,
|
|
414
504
|
args: [id]
|
|
415
505
|
});
|
|
416
|
-
if (result.rows.length === 0)
|
|
417
|
-
|
|
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
|
|
521
|
+
* Get conversations by resource ID
|
|
421
522
|
*/
|
|
422
|
-
async
|
|
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 ${
|
|
425
|
-
args: [
|
|
527
|
+
sql: `SELECT * FROM ${conversationsTable} WHERE resource_id = ? ORDER BY updated_at DESC`,
|
|
528
|
+
args: [resourceId]
|
|
426
529
|
});
|
|
427
|
-
return result.rows.map((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
|
-
*
|
|
541
|
+
* Get conversations by user ID
|
|
431
542
|
*/
|
|
432
|
-
async
|
|
433
|
-
|
|
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 (
|
|
436
|
-
|
|
437
|
-
args.push(
|
|
554
|
+
if (options.userId) {
|
|
555
|
+
sql += " AND user_id = ?";
|
|
556
|
+
args.push(options.userId);
|
|
438
557
|
}
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
args.push(
|
|
558
|
+
if (options.resourceId) {
|
|
559
|
+
sql += " AND resource_id = ?";
|
|
560
|
+
args.push(options.resourceId);
|
|
442
561
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 (
|
|
448
|
-
|
|
449
|
-
args.push(
|
|
569
|
+
if (options.offset) {
|
|
570
|
+
sql += " OFFSET ?";
|
|
571
|
+
args.push(options.offset);
|
|
450
572
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
457
|
-
args.push(
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
635
|
+
* Get working memory
|
|
523
636
|
*/
|
|
524
|
-
async
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
*
|
|
657
|
+
* Set working memory
|
|
533
658
|
*/
|
|
534
|
-
async
|
|
535
|
-
await this.
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
*
|
|
693
|
+
* Delete working memory
|
|
542
694
|
*/
|
|
543
|
-
async
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
727
|
+
* Get workflow state by execution ID
|
|
552
728
|
*/
|
|
553
|
-
async
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
*
|
|
754
|
+
* Set workflow state
|
|
589
755
|
*/
|
|
590
|
-
async
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
*
|
|
778
|
+
* Update workflow state
|
|
603
779
|
*/
|
|
604
|
-
async
|
|
605
|
-
await this.
|
|
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
|
-
*
|
|
794
|
+
* Get suspended workflow states for a workflow
|
|
609
795
|
*/
|
|
610
|
-
async
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
*
|
|
817
|
+
* Close database connection
|
|
684
818
|
*/
|
|
685
|
-
|
|
686
|
-
|
|
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/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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, "
|
|
832
|
+
__name(this, "LibSQLObservabilityAdapter");
|
|
719
833
|
}
|
|
720
834
|
client;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
workflowExtension;
|
|
835
|
+
tablePrefix;
|
|
836
|
+
debug;
|
|
724
837
|
logger;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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.
|
|
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 =
|
|
757
|
-
url
|
|
758
|
-
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
|
-
|
|
774
|
-
if (this.
|
|
873
|
+
debugLog(message, data) {
|
|
874
|
+
if (this.debug) {
|
|
775
875
|
this.logger.debug(`${message}`, data || "");
|
|
776
876
|
}
|
|
777
877
|
}
|
|
778
878
|
/**
|
|
779
|
-
*
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
925
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
926
|
-
ON ${
|
|
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
|
-
|
|
929
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
930
|
-
ON ${
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
954
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
955
|
-
ON ${
|
|
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
|
-
|
|
958
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
959
|
-
ON ${
|
|
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
|
-
|
|
962
|
-
CREATE
|
|
963
|
-
|
|
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
|
-
|
|
966
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
967
|
-
ON ${
|
|
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
|
-
|
|
970
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
971
|
-
ON ${
|
|
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
|
-
|
|
974
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
975
|
-
ON ${
|
|
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
|
-
|
|
978
|
-
CREATE INDEX IF NOT EXISTS idx_${
|
|
979
|
-
ON ${
|
|
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
|
-
|
|
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.
|
|
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
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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.
|
|
1069
|
+
this.logger.error("Failed to add span", { error, span });
|
|
1070
|
+
throw error;
|
|
1024
1071
|
}
|
|
1025
1072
|
}
|
|
1026
1073
|
/**
|
|
1027
|
-
*
|
|
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
|
|
1039
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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 (
|
|
1065
|
-
|
|
1066
|
-
args.push(
|
|
1085
|
+
if (updates.duration !== void 0) {
|
|
1086
|
+
setClauses.push("duration = ?");
|
|
1087
|
+
args.push(updates.duration);
|
|
1067
1088
|
}
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1070
|
-
args.push(
|
|
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 (
|
|
1073
|
-
|
|
1074
|
-
args.push(
|
|
1093
|
+
if (updates.attributes !== void 0) {
|
|
1094
|
+
setClauses.push("attributes = ?");
|
|
1095
|
+
args.push(safeStringify(updates.attributes));
|
|
1075
1096
|
}
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
args.push(
|
|
1097
|
+
if (updates.events !== void 0) {
|
|
1098
|
+
setClauses.push("events = ?");
|
|
1099
|
+
args.push(safeStringify(updates.events));
|
|
1079
1100
|
}
|
|
1080
|
-
if (
|
|
1081
|
-
|
|
1082
|
-
|
|
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 (
|
|
1089
|
-
|
|
1090
|
-
args.push(limit);
|
|
1091
|
-
} else {
|
|
1092
|
-
sql += " ORDER BY m.created_at ASC";
|
|
1105
|
+
if (setClauses.length === 0) {
|
|
1106
|
+
return;
|
|
1093
1107
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1132
|
+
this.debugLog("Span updated successfully", { spanId, updates });
|
|
1116
1133
|
} catch (error) {
|
|
1117
|
-
this.
|
|
1118
|
-
throw
|
|
1134
|
+
this.logger.error("Failed to update span", { error, spanId, updates });
|
|
1135
|
+
throw error;
|
|
1119
1136
|
}
|
|
1120
1137
|
}
|
|
1121
1138
|
/**
|
|
1122
|
-
*
|
|
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
|
|
1128
|
-
await this.
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1155
|
-
* @param conversationId Conversation ID to prune messages for
|
|
1162
|
+
* Get all spans in a trace
|
|
1156
1163
|
*/
|
|
1157
|
-
async
|
|
1158
|
-
|
|
1159
|
-
const tableName = `${this.options.tablePrefix}_messages`;
|
|
1164
|
+
async getTrace(traceId) {
|
|
1165
|
+
await this.ensureInitialized();
|
|
1160
1166
|
try {
|
|
1161
|
-
const
|
|
1162
|
-
sql: `
|
|
1163
|
-
|
|
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
|
-
|
|
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.
|
|
1178
|
+
this.logger.error("Failed to get trace", { error, traceId });
|
|
1183
1179
|
throw error;
|
|
1184
1180
|
}
|
|
1185
1181
|
}
|
|
1186
1182
|
/**
|
|
1187
|
-
*
|
|
1183
|
+
* List all traces with optional entity filter
|
|
1188
1184
|
*/
|
|
1189
|
-
async
|
|
1190
|
-
await this.
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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.
|
|
1218
|
-
throw
|
|
1218
|
+
this.logger.error("Failed to list traces", { error, limit, offset, filter });
|
|
1219
|
+
throw error;
|
|
1219
1220
|
}
|
|
1220
1221
|
}
|
|
1221
1222
|
/**
|
|
1222
|
-
*
|
|
1223
|
+
* Delete old spans
|
|
1223
1224
|
*/
|
|
1224
|
-
async
|
|
1225
|
+
async deleteOldSpans(beforeTimestamp) {
|
|
1226
|
+
await this.ensureInitialized();
|
|
1225
1227
|
try {
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
-
*
|
|
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
|
|
1238
|
-
await this.
|
|
1286
|
+
async clear() {
|
|
1287
|
+
await this.ensureInitialized();
|
|
1239
1288
|
try {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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.
|
|
1275
|
-
throw
|
|
1296
|
+
this.logger.error("Failed to clear data", { error });
|
|
1297
|
+
throw error;
|
|
1276
1298
|
}
|
|
1277
1299
|
}
|
|
1278
1300
|
/**
|
|
1279
|
-
*
|
|
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
|
-
|
|
1285
|
-
|
|
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
|
-
*
|
|
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
|
|
1295
|
-
await this.
|
|
1351
|
+
async getStats() {
|
|
1352
|
+
await this.ensureInitialized();
|
|
1296
1353
|
try {
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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.
|
|
1306
|
-
throw
|
|
1376
|
+
this.logger.error("Failed to get stats", { error });
|
|
1377
|
+
throw error;
|
|
1307
1378
|
}
|
|
1308
1379
|
}
|
|
1309
1380
|
/**
|
|
1310
|
-
*
|
|
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
|
|
1317
|
-
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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: `
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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.
|
|
1426
|
+
this.debugLog("Log record saved successfully", {
|
|
1427
|
+
timestamp,
|
|
1428
|
+
traceId,
|
|
1429
|
+
spanId,
|
|
1430
|
+
severityNumber
|
|
1431
|
+
});
|
|
1363
1432
|
} catch (error) {
|
|
1364
|
-
this.
|
|
1365
|
-
throw
|
|
1433
|
+
this.logger.error("Failed to save log record", { error, logRecord });
|
|
1434
|
+
throw error;
|
|
1366
1435
|
}
|
|
1367
1436
|
}
|
|
1368
1437
|
/**
|
|
1369
|
-
* Get
|
|
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
|
|
1374
|
-
await this.
|
|
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: `
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1450
|
-
entry.events = events;
|
|
1451
|
-
return entry;
|
|
1452
|
+
return result.rows.map((row) => this.rowToLogRecord(row));
|
|
1452
1453
|
} catch (error) {
|
|
1453
|
-
this.
|
|
1454
|
-
|
|
1454
|
+
this.logger.error("Failed to get logs by trace ID", { error, traceId });
|
|
1455
|
+
throw error;
|
|
1455
1456
|
}
|
|
1456
1457
|
}
|
|
1457
1458
|
/**
|
|
1458
|
-
* Get
|
|
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
|
|
1463
|
-
await this.
|
|
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
|
-
|
|
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.
|
|
1601
|
-
throw
|
|
1475
|
+
this.logger.error("Failed to get logs by span ID", { error, spanId });
|
|
1476
|
+
throw error;
|
|
1602
1477
|
}
|
|
1603
1478
|
}
|
|
1604
1479
|
/**
|
|
1605
|
-
* Query
|
|
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
|
|
1612
|
-
await this.
|
|
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
|
-
|
|
1485
|
+
const whereClauses = [];
|
|
1625
1486
|
const args = [];
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
args.push(userId);
|
|
1487
|
+
if (filter.traceId) {
|
|
1488
|
+
whereClauses.push("trace_id = ?");
|
|
1489
|
+
args.push(filter.traceId);
|
|
1630
1490
|
}
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1633
|
-
args.push(
|
|
1491
|
+
if (filter.spanId) {
|
|
1492
|
+
whereClauses.push("span_id = ?");
|
|
1493
|
+
args.push(filter.spanId);
|
|
1634
1494
|
}
|
|
1635
|
-
if (
|
|
1636
|
-
|
|
1495
|
+
if (filter.severityNumber !== void 0) {
|
|
1496
|
+
whereClauses.push("severity_number >= ?");
|
|
1497
|
+
args.push(filter.severityNumber);
|
|
1637
1498
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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.
|
|
1658
|
-
throw
|
|
1546
|
+
this.logger.error("Failed to query logs", { error, filter });
|
|
1547
|
+
throw error;
|
|
1659
1548
|
}
|
|
1660
1549
|
}
|
|
1661
1550
|
/**
|
|
1662
|
-
*
|
|
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
|
|
1670
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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.
|
|
1701
|
-
throw
|
|
1568
|
+
this.logger.error("Failed to delete old logs", { error, beforeTimestamp });
|
|
1569
|
+
throw error;
|
|
1702
1570
|
}
|
|
1703
1571
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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.
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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.
|
|
1761
|
-
throw
|
|
1722
|
+
this.logger.error("Failed to initialize vector adapter", error);
|
|
1723
|
+
throw error;
|
|
1762
1724
|
}
|
|
1763
1725
|
}
|
|
1764
1726
|
/**
|
|
1765
|
-
*
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
)
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
2346
|
-
|
|
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
|
-
|
|
2350
|
-
|
|
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
|
-
*
|
|
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
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
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
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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
|
-
|
|
2518
|
-
const
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
id
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
-
*
|
|
2680
|
-
* @param userId User ID to filter by
|
|
2681
|
-
* @returns Query builder object
|
|
1915
|
+
* Delete a vector by ID
|
|
2682
1916
|
*/
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
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
|
-
*
|
|
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
|
|
2760
|
-
|
|
2761
|
-
if (
|
|
2762
|
-
|
|
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
|
-
*
|
|
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
|
|
2774
|
-
|
|
2775
|
-
const
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
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
|
-
*
|
|
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
|
|
2796
|
-
|
|
2797
|
-
const
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
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
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
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
|
-
*
|
|
2026
|
+
* Close the database connection
|
|
2858
2027
|
*/
|
|
2859
|
-
async
|
|
2860
|
-
|
|
2028
|
+
async close() {
|
|
2029
|
+
this.vectorCache.clear();
|
|
2030
|
+
this.logger.debug("Vector adapter closed");
|
|
2861
2031
|
try {
|
|
2862
|
-
this.
|
|
2863
|
-
|
|
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
|
|
2037
|
+
* Get statistics about the vector table and cache
|
|
2990
2038
|
*/
|
|
2991
|
-
|
|
2992
|
-
|
|
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
|
-
|
|
2077
|
+
LibSQLMemoryAdapter,
|
|
2078
|
+
LibSQLObservabilityAdapter,
|
|
2079
|
+
LibSQLVectorAdapter
|
|
2997
2080
|
};
|
|
2998
2081
|
//# sourceMappingURL=index.mjs.map
|