@agenttrace-io/sdk 0.1.9
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/benchmark.d.ts +79 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +324 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/index.d.ts +358 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1169 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/001-initial.d.ts +5 -0
- package/dist/migrations/001-initial.d.ts.map +1 -0
- package/dist/migrations/001-initial.js +86 -0
- package/dist/migrations/001-initial.js.map +1 -0
- package/dist/migrations/002-scores.d.ts +5 -0
- package/dist/migrations/002-scores.d.ts.map +1 -0
- package/dist/migrations/002-scores.js +17 -0
- package/dist/migrations/002-scores.js.map +1 -0
- package/dist/migrations/003-alerts.d.ts +5 -0
- package/dist/migrations/003-alerts.d.ts.map +1 -0
- package/dist/migrations/003-alerts.js +27 -0
- package/dist/migrations/003-alerts.js.map +1 -0
- package/dist/migrations/004-trace-context.d.ts +5 -0
- package/dist/migrations/004-trace-context.d.ts.map +1 -0
- package/dist/migrations/004-trace-context.js +16 -0
- package/dist/migrations/004-trace-context.js.map +1 -0
- package/dist/migrations/005-agent-usage.d.ts +5 -0
- package/dist/migrations/005-agent-usage.d.ts.map +1 -0
- package/dist/migrations/005-agent-usage.js +27 -0
- package/dist/migrations/005-agent-usage.js.map +1 -0
- package/dist/migrations/005-webhooks.d.ts +5 -0
- package/dist/migrations/005-webhooks.d.ts.map +1 -0
- package/dist/migrations/005-webhooks.js +33 -0
- package/dist/migrations/005-webhooks.js.map +1 -0
- package/dist/migrations/006-api-keys.d.ts +5 -0
- package/dist/migrations/006-api-keys.d.ts.map +1 -0
- package/dist/migrations/006-api-keys.js +27 -0
- package/dist/migrations/006-api-keys.js.map +1 -0
- package/dist/migrations.d.ts +29 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +107 -0
- package/dist/migrations.js.map +1 -0
- package/dist/rate-limiter.d.ts +34 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +74 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/self-track.d.ts +42 -0
- package/dist/self-track.d.ts.map +1 -0
- package/dist/self-track.js +288 -0
- package/dist/self-track.js.map +1 -0
- package/dist/storage.d.ts +149 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +1479 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +323 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
package/dist/storage.js
ADDED
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentTrace -- SQLite Storage Layer
|
|
3
|
+
* Local storage for agent traces with zero cloud dependency
|
|
4
|
+
*/
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
7
|
+
import { statSync } from 'node:fs';
|
|
8
|
+
export class TraceStorage {
|
|
9
|
+
db;
|
|
10
|
+
dbPath;
|
|
11
|
+
_droppedTraces = 0;
|
|
12
|
+
tenantId;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
static _connections = new Map();
|
|
15
|
+
constructor(dbPath = './agenttrace.db', tenantId) {
|
|
16
|
+
this.dbPath = dbPath;
|
|
17
|
+
this.tenantId = tenantId || '';
|
|
18
|
+
const existing = TraceStorage._connections.get(dbPath);
|
|
19
|
+
if (existing) {
|
|
20
|
+
existing.refCount++;
|
|
21
|
+
this.db = existing.db;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const db = new Database(dbPath);
|
|
25
|
+
db.pragma('journal_mode = WAL');
|
|
26
|
+
db.pragma('foreign_keys = ON');
|
|
27
|
+
this.db = db;
|
|
28
|
+
TraceStorage._connections.set(dbPath, { db, refCount: 1 });
|
|
29
|
+
this.initSchema();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
initSchema() {
|
|
33
|
+
this.db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
tenant_id TEXT,
|
|
37
|
+
name TEXT NOT NULL,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
39
|
+
trace_count INTEGER DEFAULT 0,
|
|
40
|
+
total_prompt_tokens INTEGER DEFAULT 0,
|
|
41
|
+
total_completion_tokens INTEGER DEFAULT 0,
|
|
42
|
+
total_tokens INTEGER DEFAULT 0,
|
|
43
|
+
total_tool_calls INTEGER DEFAULT 0,
|
|
44
|
+
total_latency_ms INTEGER DEFAULT 0,
|
|
45
|
+
total_cost_usd REAL DEFAULT 0,
|
|
46
|
+
error_count INTEGER DEFAULT 0,
|
|
47
|
+
started_at INTEGER NOT NULL,
|
|
48
|
+
completed_at INTEGER,
|
|
49
|
+
metadata TEXT DEFAULT '{}',
|
|
50
|
+
created_at INTEGER NOT NULL,
|
|
51
|
+
updated_at INTEGER NOT NULL
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
tenant_id TEXT,
|
|
57
|
+
run_id TEXT NOT NULL,
|
|
58
|
+
name TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL,
|
|
60
|
+
input TEXT,
|
|
61
|
+
output TEXT,
|
|
62
|
+
prompt_tokens INTEGER DEFAULT 0,
|
|
63
|
+
completion_tokens INTEGER DEFAULT 0,
|
|
64
|
+
total_tokens INTEGER DEFAULT 0,
|
|
65
|
+
model TEXT,
|
|
66
|
+
provider TEXT,
|
|
67
|
+
latency_ms INTEGER DEFAULT 0,
|
|
68
|
+
cost_usd REAL DEFAULT 0,
|
|
69
|
+
error TEXT,
|
|
70
|
+
metadata TEXT DEFAULT '{}',
|
|
71
|
+
parent_id TEXT,
|
|
72
|
+
created_at INTEGER NOT NULL,
|
|
73
|
+
updated_at INTEGER NOT NULL,
|
|
74
|
+
FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
trace_id TEXT NOT NULL,
|
|
80
|
+
name TEXT NOT NULL,
|
|
81
|
+
input TEXT,
|
|
82
|
+
output TEXT,
|
|
83
|
+
latency_ms INTEGER DEFAULT 0,
|
|
84
|
+
success INTEGER DEFAULT 1,
|
|
85
|
+
error TEXT,
|
|
86
|
+
timestamp INTEGER NOT NULL,
|
|
87
|
+
FOREIGN KEY (trace_id) REFERENCES traces(id) ON DELETE CASCADE
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_traces_run_id ON traces(run_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_traces_status ON traces(status);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_traces_created_at ON traces(created_at);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_traces_cost ON traces(cost_usd);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_traces_parent_id ON traces(parent_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_trace_id ON tool_calls(trace_id);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(name);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS scores (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
trace_id TEXT NOT NULL REFERENCES traces(id),
|
|
101
|
+
name TEXT NOT NULL,
|
|
102
|
+
value REAL NOT NULL,
|
|
103
|
+
created_at INTEGER NOT NULL
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_scores_trace_id ON scores(trace_id);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_scores_name ON scores(name);
|
|
108
|
+
|
|
109
|
+
CREATE TABLE IF NOT EXISTS alerts (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
name TEXT UNIQUE NOT NULL,
|
|
112
|
+
config TEXT NOT NULL,
|
|
113
|
+
created_at INTEGER NOT NULL
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE TABLE IF NOT EXISTS alert_history (
|
|
117
|
+
id TEXT PRIMARY KEY,
|
|
118
|
+
alert_name TEXT NOT NULL,
|
|
119
|
+
triggered_at INTEGER NOT NULL,
|
|
120
|
+
stats TEXT NOT NULL,
|
|
121
|
+
delivered INTEGER DEFAULT 0,
|
|
122
|
+
error TEXT,
|
|
123
|
+
created_at INTEGER NOT NULL
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_alerts_name ON alerts(name);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_alert_history_alert_name ON alert_history(alert_name);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_alert_history_triggered_at ON alert_history(triggered_at);
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS trace_links (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
source_trace_id TEXT NOT NULL,
|
|
133
|
+
target_trace_id TEXT NOT NULL,
|
|
134
|
+
relation TEXT NOT NULL DEFAULT 'related',
|
|
135
|
+
created_at INTEGER NOT NULL,
|
|
136
|
+
FOREIGN KEY (source_trace_id) REFERENCES traces(id) ON DELETE CASCADE,
|
|
137
|
+
FOREIGN KEY (target_trace_id) REFERENCES traces(id) ON DELETE CASCADE
|
|
138
|
+
);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_trace_links_source ON trace_links(source_trace_id);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_trace_links_target ON trace_links(target_trace_id);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS agent_usage (
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
tenant_id TEXT,
|
|
145
|
+
agent_name TEXT NOT NULL,
|
|
146
|
+
agent_type TEXT,
|
|
147
|
+
session_id TEXT,
|
|
148
|
+
action TEXT NOT NULL,
|
|
149
|
+
target TEXT,
|
|
150
|
+
tokens_used INTEGER DEFAULT 0,
|
|
151
|
+
cost_usd REAL DEFAULT 0,
|
|
152
|
+
duration_ms INTEGER DEFAULT 0,
|
|
153
|
+
status TEXT DEFAULT 'success',
|
|
154
|
+
metadata TEXT DEFAULT '{}',
|
|
155
|
+
created_at INTEGER NOT NULL
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_agent_usage_agent_name ON agent_usage(agent_name);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_agent_usage_session_id ON agent_usage(session_id);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_agent_usage_action ON agent_usage(action);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_agent_usage_status ON agent_usage(status);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_agent_usage_created_at ON agent_usage(created_at);
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
165
|
+
id TEXT PRIMARY KEY,
|
|
166
|
+
url TEXT NOT NULL,
|
|
167
|
+
secret TEXT,
|
|
168
|
+
events TEXT NOT NULL,
|
|
169
|
+
enabled INTEGER DEFAULT 1,
|
|
170
|
+
created_at INTEGER NOT NULL,
|
|
171
|
+
last_triggered_at INTEGER,
|
|
172
|
+
failure_count INTEGER DEFAULT 0
|
|
173
|
+
);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_enabled ON webhooks(enabled);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_created_at ON webhooks(created_at);
|
|
176
|
+
|
|
177
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
178
|
+
id TEXT PRIMARY KEY,
|
|
179
|
+
name TEXT NOT NULL,
|
|
180
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
181
|
+
key_preview TEXT NOT NULL,
|
|
182
|
+
permissions TEXT NOT NULL DEFAULT '["read","write"]',
|
|
183
|
+
created_at INTEGER NOT NULL,
|
|
184
|
+
last_used_at INTEGER,
|
|
185
|
+
enabled INTEGER DEFAULT 1
|
|
186
|
+
);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_created_at ON api_keys(created_at);
|
|
188
|
+
|
|
189
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
name TEXT NOT NULL,
|
|
192
|
+
api_key TEXT NOT NULL UNIQUE,
|
|
193
|
+
created_at INTEGER NOT NULL
|
|
194
|
+
);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_projects_api_key ON projects(api_key);
|
|
196
|
+
|
|
197
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
198
|
+
key TEXT PRIMARY KEY,
|
|
199
|
+
value TEXT NOT NULL
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE TABLE IF NOT EXISTS version (
|
|
203
|
+
key TEXT PRIMARY KEY,
|
|
204
|
+
value TEXT NOT NULL
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE TABLE IF NOT EXISTS budgets (
|
|
208
|
+
agent_name TEXT PRIMARY KEY,
|
|
209
|
+
max_tokens_per_day INTEGER DEFAULT 0,
|
|
210
|
+
max_cost_per_day REAL DEFAULT 0,
|
|
211
|
+
created_at INTEGER NOT NULL
|
|
212
|
+
);
|
|
213
|
+
`);
|
|
214
|
+
// Migration tracking
|
|
215
|
+
const version = this.db
|
|
216
|
+
.prepare('SELECT value FROM version WHERE key = ?')
|
|
217
|
+
.get('schema_version');
|
|
218
|
+
if (!version) {
|
|
219
|
+
this.db.prepare('INSERT INTO version (key, value) VALUES (?, ?)').run('schema_version', '3');
|
|
220
|
+
}
|
|
221
|
+
// v2+ migration for multi-agent tracing (parent_id + trace_links)
|
|
222
|
+
const verRow = this.db
|
|
223
|
+
.prepare('SELECT value FROM version WHERE key = ?')
|
|
224
|
+
.get('schema_version');
|
|
225
|
+
const schemaVer = verRow ? parseInt(String(verRow.value), 10) : 0;
|
|
226
|
+
if (schemaVer < 2) {
|
|
227
|
+
try {
|
|
228
|
+
this.db.exec('ALTER TABLE traces ADD COLUMN parent_id TEXT');
|
|
229
|
+
}
|
|
230
|
+
catch (_) {
|
|
231
|
+
/* column may already exist (e.g. partial migration) */
|
|
232
|
+
}
|
|
233
|
+
this.db.exec(`
|
|
234
|
+
CREATE TABLE IF NOT EXISTS trace_links (
|
|
235
|
+
id TEXT PRIMARY KEY,
|
|
236
|
+
source_trace_id TEXT NOT NULL,
|
|
237
|
+
target_trace_id TEXT NOT NULL,
|
|
238
|
+
relation TEXT NOT NULL DEFAULT 'related',
|
|
239
|
+
created_at INTEGER NOT NULL,
|
|
240
|
+
FOREIGN KEY (source_trace_id) REFERENCES traces(id) ON DELETE CASCADE,
|
|
241
|
+
FOREIGN KEY (target_trace_id) REFERENCES traces(id) ON DELETE CASCADE
|
|
242
|
+
);
|
|
243
|
+
CREATE INDEX IF NOT EXISTS idx_trace_links_source ON trace_links(source_trace_id);
|
|
244
|
+
CREATE INDEX IF NOT EXISTS idx_trace_links_target ON trace_links(target_trace_id);
|
|
245
|
+
`);
|
|
246
|
+
this.db
|
|
247
|
+
.prepare('INSERT OR REPLACE INTO version (key, value) VALUES (?, ?)')
|
|
248
|
+
.run('schema_version', '2');
|
|
249
|
+
}
|
|
250
|
+
// v3+ migration for multi-tenant (projects table + tenant_id on agent_usage)
|
|
251
|
+
if (schemaVer < 3) {
|
|
252
|
+
this.db.exec(`
|
|
253
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
254
|
+
id TEXT PRIMARY KEY,
|
|
255
|
+
name TEXT NOT NULL,
|
|
256
|
+
api_key TEXT NOT NULL UNIQUE,
|
|
257
|
+
created_at INTEGER NOT NULL
|
|
258
|
+
);
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_projects_api_key ON projects(api_key);
|
|
260
|
+
`);
|
|
261
|
+
try {
|
|
262
|
+
this.db.exec('ALTER TABLE agent_usage ADD COLUMN tenant_id TEXT');
|
|
263
|
+
}
|
|
264
|
+
catch (_) {
|
|
265
|
+
/* column may already exist */
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN tenant_id TEXT');
|
|
269
|
+
}
|
|
270
|
+
catch (_) {
|
|
271
|
+
/* column may already exist */
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
this.db.exec('ALTER TABLE traces ADD COLUMN tenant_id TEXT');
|
|
275
|
+
}
|
|
276
|
+
catch (_) {
|
|
277
|
+
/* column may already exist */
|
|
278
|
+
}
|
|
279
|
+
this.db
|
|
280
|
+
.prepare('INSERT OR REPLACE INTO version (key, value) VALUES (?, ?)')
|
|
281
|
+
.run('schema_version', '3');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ---- Run operations ----
|
|
285
|
+
createRun(run) {
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const stmt = this.db.prepare(`
|
|
288
|
+
INSERT INTO runs (id, tenant_id, name, status, started_at, metadata, created_at, updated_at)
|
|
289
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
290
|
+
`);
|
|
291
|
+
stmt.run(run.id, run.tenantId || null, run.name, 'running', run.startedAt, JSON.stringify(run.metadata || {}), now, now);
|
|
292
|
+
return this.getRun(run.id);
|
|
293
|
+
}
|
|
294
|
+
getRun(id) {
|
|
295
|
+
const row = this.db.prepare('SELECT * FROM runs WHERE id = ?').get(id);
|
|
296
|
+
if (!row)
|
|
297
|
+
return null;
|
|
298
|
+
return this.rowToRun(row);
|
|
299
|
+
}
|
|
300
|
+
getRuns(limit = 100) {
|
|
301
|
+
let sql = 'SELECT * FROM runs WHERE 1=1';
|
|
302
|
+
const params = [];
|
|
303
|
+
const tenantId = this.tenantId;
|
|
304
|
+
if (tenantId) {
|
|
305
|
+
sql += ' AND tenant_id = ?';
|
|
306
|
+
params.push(tenantId);
|
|
307
|
+
}
|
|
308
|
+
sql += ' ORDER BY started_at DESC LIMIT ?';
|
|
309
|
+
params.push(limit);
|
|
310
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
311
|
+
return rows.map((r) => this.rowToRun(r));
|
|
312
|
+
}
|
|
313
|
+
completeRun(id, status) {
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
this.db
|
|
316
|
+
.prepare(`
|
|
317
|
+
UPDATE runs SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?
|
|
318
|
+
`)
|
|
319
|
+
.run(status, now, now, id);
|
|
320
|
+
}
|
|
321
|
+
updateRunStats(runId, tokens, toolCalls, latencyMs, costUsd) {
|
|
322
|
+
this.db
|
|
323
|
+
.prepare(`
|
|
324
|
+
UPDATE runs SET
|
|
325
|
+
total_prompt_tokens = total_prompt_tokens + ?,
|
|
326
|
+
total_completion_tokens = total_completion_tokens + ?,
|
|
327
|
+
total_tokens = total_tokens + ?,
|
|
328
|
+
total_tool_calls = total_tool_calls + ?,
|
|
329
|
+
total_latency_ms = total_latency_ms + ?,
|
|
330
|
+
total_cost_usd = total_cost_usd + ?,
|
|
331
|
+
trace_count = trace_count + 1,
|
|
332
|
+
updated_at = ?
|
|
333
|
+
WHERE id = ?
|
|
334
|
+
`)
|
|
335
|
+
.run(tokens.promptTokens, tokens.completionTokens, tokens.totalTokens, toolCalls, latencyMs, costUsd, Date.now(), runId);
|
|
336
|
+
}
|
|
337
|
+
// ---- Trace operations ----
|
|
338
|
+
createTrace(trace) {
|
|
339
|
+
this.db.transaction(() => {
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
const stmt = this.db.prepare(`
|
|
342
|
+
INSERT INTO traces (id, tenant_id, run_id, name, status, input, output, prompt_tokens, completion_tokens, total_tokens, model, provider, latency_ms, cost_usd, error, metadata, parent_id, created_at, updated_at)
|
|
343
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
344
|
+
`);
|
|
345
|
+
stmt.run(trace.id, trace.tenantId || null, trace.runId, trace.name, trace.status, JSON.stringify(trace.input), JSON.stringify(trace.output), trace.tokens.promptTokens, trace.tokens.completionTokens, trace.tokens.totalTokens, trace.tokens.model || null, trace.tokens.provider || null, trace.latencyMs, trace.costUsd, trace.error || null, JSON.stringify(trace.metadata), trace.parentId || null, now, now);
|
|
346
|
+
// Insert tool calls
|
|
347
|
+
const toolStmt = this.db.prepare(`
|
|
348
|
+
INSERT INTO tool_calls (id, trace_id, name, input, output, latency_ms, success, error, timestamp)
|
|
349
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
350
|
+
`);
|
|
351
|
+
for (const tc of trace.toolCalls) {
|
|
352
|
+
toolStmt.run(tc.id, trace.id, tc.name, JSON.stringify(tc.input), JSON.stringify(tc.output), tc.latencyMs, tc.success ? 1 : 0, tc.error || null, tc.timestamp);
|
|
353
|
+
}
|
|
354
|
+
// Update run stats
|
|
355
|
+
this.updateRunStats(trace.runId, trace.tokens, trace.toolCalls.length, trace.latencyMs, trace.costUsd);
|
|
356
|
+
})();
|
|
357
|
+
return this.getTrace(trace.id);
|
|
358
|
+
}
|
|
359
|
+
getTrace(id) {
|
|
360
|
+
const row = this.db.prepare('SELECT * FROM traces WHERE id = ?').get(id);
|
|
361
|
+
if (!row)
|
|
362
|
+
return null;
|
|
363
|
+
return this.rowToTrace(row);
|
|
364
|
+
}
|
|
365
|
+
getTraces(filter = {}) {
|
|
366
|
+
let sql = 'SELECT * FROM traces WHERE 1=1';
|
|
367
|
+
const params = [];
|
|
368
|
+
const tenantId = this.tenantId;
|
|
369
|
+
if (tenantId) {
|
|
370
|
+
sql += ' AND tenant_id = ?';
|
|
371
|
+
params.push(tenantId);
|
|
372
|
+
}
|
|
373
|
+
if (filter.runId) {
|
|
374
|
+
sql += ' AND run_id = ?';
|
|
375
|
+
params.push(filter.runId);
|
|
376
|
+
}
|
|
377
|
+
if (filter.status?.length) {
|
|
378
|
+
sql += ` AND status IN (${filter.status.map(() => '?').join(',')})`;
|
|
379
|
+
params.push(...filter.status);
|
|
380
|
+
}
|
|
381
|
+
if (filter.name) {
|
|
382
|
+
sql += ' AND name LIKE ?';
|
|
383
|
+
params.push(`%${filter.name}%`);
|
|
384
|
+
}
|
|
385
|
+
if (filter.fromDate) {
|
|
386
|
+
sql += ' AND created_at >= ?';
|
|
387
|
+
params.push(filter.fromDate);
|
|
388
|
+
}
|
|
389
|
+
if (filter.toDate) {
|
|
390
|
+
sql += ' AND created_at <= ?';
|
|
391
|
+
params.push(filter.toDate);
|
|
392
|
+
}
|
|
393
|
+
if (filter.minCost !== undefined) {
|
|
394
|
+
sql += ' AND cost_usd >= ?';
|
|
395
|
+
params.push(filter.minCost);
|
|
396
|
+
}
|
|
397
|
+
if (filter.maxCost !== undefined) {
|
|
398
|
+
sql += ' AND cost_usd <= ?';
|
|
399
|
+
params.push(filter.maxCost);
|
|
400
|
+
}
|
|
401
|
+
if (filter.minLatency !== undefined) {
|
|
402
|
+
sql += ' AND latency_ms >= ?';
|
|
403
|
+
params.push(filter.minLatency);
|
|
404
|
+
}
|
|
405
|
+
if (filter.maxLatency !== undefined) {
|
|
406
|
+
sql += ' AND latency_ms <= ?';
|
|
407
|
+
params.push(filter.maxLatency);
|
|
408
|
+
}
|
|
409
|
+
sql += ' ORDER BY created_at DESC';
|
|
410
|
+
if (filter.limit) {
|
|
411
|
+
sql += ' LIMIT ?';
|
|
412
|
+
params.push(filter.limit);
|
|
413
|
+
}
|
|
414
|
+
if (filter.offset) {
|
|
415
|
+
sql += ' OFFSET ?';
|
|
416
|
+
params.push(filter.offset);
|
|
417
|
+
}
|
|
418
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
419
|
+
return rows.map((r) => this.rowToTrace(r));
|
|
420
|
+
}
|
|
421
|
+
// ---- Score operations (for evaluation framework) ----
|
|
422
|
+
createScore(id, traceId, name, value) {
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
this.db
|
|
425
|
+
.prepare(`
|
|
426
|
+
INSERT INTO scores (id, trace_id, name, value, created_at)
|
|
427
|
+
VALUES (?, ?, ?, ?, ?)
|
|
428
|
+
`)
|
|
429
|
+
.run(id, traceId, name, value, now);
|
|
430
|
+
}
|
|
431
|
+
getScores(traceId) {
|
|
432
|
+
let sql = 'SELECT * FROM scores';
|
|
433
|
+
const params = [];
|
|
434
|
+
if (traceId) {
|
|
435
|
+
sql += ' WHERE trace_id = ?';
|
|
436
|
+
params.push(traceId);
|
|
437
|
+
}
|
|
438
|
+
sql += ' ORDER BY created_at DESC';
|
|
439
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
440
|
+
return rows.map((r) => {
|
|
441
|
+
const rec = r;
|
|
442
|
+
return {
|
|
443
|
+
id: rec.id,
|
|
444
|
+
traceId: rec.trace_id,
|
|
445
|
+
name: rec.name,
|
|
446
|
+
value: Number(rec.value),
|
|
447
|
+
createdAt: Number(rec.created_at),
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// ---- Alert operations (v0.2 alerting & webhooks) ----
|
|
452
|
+
saveAlert(name, config) {
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
const id = name;
|
|
455
|
+
this.db
|
|
456
|
+
.prepare(`
|
|
457
|
+
INSERT INTO alerts (id, name, config, created_at)
|
|
458
|
+
VALUES (?, ?, ?, ?)
|
|
459
|
+
ON CONFLICT(name) DO UPDATE SET config = excluded.config
|
|
460
|
+
`)
|
|
461
|
+
.run(id, name, JSON.stringify(config), now);
|
|
462
|
+
}
|
|
463
|
+
getStoredAlerts() {
|
|
464
|
+
const rows = this.db
|
|
465
|
+
.prepare('SELECT * FROM alerts ORDER BY created_at DESC')
|
|
466
|
+
.all();
|
|
467
|
+
return rows.map((r) => {
|
|
468
|
+
const rec = r;
|
|
469
|
+
return {
|
|
470
|
+
name: rec.name,
|
|
471
|
+
config: JSON.parse(rec.config || '{}'),
|
|
472
|
+
createdAt: Number(rec.created_at),
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
insertAlertHistory(entry) {
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
this.db
|
|
479
|
+
.prepare(`
|
|
480
|
+
INSERT INTO alert_history (id, alert_name, triggered_at, stats, delivered, error, created_at)
|
|
481
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
482
|
+
`)
|
|
483
|
+
.run(entry.id, entry.alertName, entry.triggeredAt, JSON.stringify(entry.stats || {}), entry.delivered ? 1 : 0, entry.error || null, now);
|
|
484
|
+
}
|
|
485
|
+
getAlertHistory() {
|
|
486
|
+
const rows = this.db
|
|
487
|
+
.prepare('SELECT * FROM alert_history ORDER BY triggered_at DESC')
|
|
488
|
+
.all();
|
|
489
|
+
return rows.map((r) => {
|
|
490
|
+
const rec = r;
|
|
491
|
+
return {
|
|
492
|
+
id: rec.id,
|
|
493
|
+
alertName: rec.alert_name,
|
|
494
|
+
triggeredAt: Number(rec.triggered_at),
|
|
495
|
+
stats: JSON.parse(rec.stats || '{}'),
|
|
496
|
+
delivered: rec.delivered === 1,
|
|
497
|
+
error: rec.error || undefined,
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
// ---- Multi-agent tracing (parent/child + links) v0.2 ----
|
|
502
|
+
setTraceParent(traceId, parentId) {
|
|
503
|
+
this.db
|
|
504
|
+
.prepare('UPDATE traces SET parent_id = ?, updated_at = ? WHERE id = ?')
|
|
505
|
+
.run(parentId, Date.now(), traceId);
|
|
506
|
+
}
|
|
507
|
+
getTraceParentId(traceId) {
|
|
508
|
+
const row = this.db
|
|
509
|
+
.prepare('SELECT parent_id FROM traces WHERE id = ?')
|
|
510
|
+
.get(traceId);
|
|
511
|
+
const rec = row;
|
|
512
|
+
return rec && rec.parent_id ? String(rec.parent_id) : null;
|
|
513
|
+
}
|
|
514
|
+
getChildTraceIds(parentId) {
|
|
515
|
+
const rows = this.db
|
|
516
|
+
.prepare('SELECT id FROM traces WHERE parent_id = ? ORDER BY created_at ASC')
|
|
517
|
+
.all(parentId);
|
|
518
|
+
return rows.map((r) => String(r.id));
|
|
519
|
+
}
|
|
520
|
+
getLinkedTraceIds(traceId) {
|
|
521
|
+
const rows = this.db
|
|
522
|
+
.prepare(`
|
|
523
|
+
SELECT DISTINCT target_trace_id AS id FROM trace_links WHERE source_trace_id = ? AND relation = 'related'
|
|
524
|
+
UNION
|
|
525
|
+
SELECT DISTINCT source_trace_id AS id FROM trace_links WHERE target_trace_id = ? AND relation = 'related'
|
|
526
|
+
`)
|
|
527
|
+
.all(traceId, traceId);
|
|
528
|
+
return rows
|
|
529
|
+
.map((r) => String(r.id))
|
|
530
|
+
.filter((id) => id !== traceId);
|
|
531
|
+
}
|
|
532
|
+
linkTraces(traceIds) {
|
|
533
|
+
if (!traceIds || traceIds.length < 2)
|
|
534
|
+
return;
|
|
535
|
+
const now = Date.now();
|
|
536
|
+
const stmt = this.db.prepare(`
|
|
537
|
+
INSERT OR IGNORE INTO trace_links (id, source_trace_id, target_trace_id, relation, created_at)
|
|
538
|
+
VALUES (?, ?, ?, 'related', ?)
|
|
539
|
+
`);
|
|
540
|
+
for (let i = 0; i < traceIds.length; i++) {
|
|
541
|
+
for (let j = i + 1; j < traceIds.length; j++) {
|
|
542
|
+
stmt.run(randomUUID(), traceIds[i], traceIds[j], now);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
getTraceTree(traceId) {
|
|
547
|
+
// Walk up parent chain to find ultimate root (cycle safe)
|
|
548
|
+
let rootId = traceId;
|
|
549
|
+
const upSeen = new Set();
|
|
550
|
+
while (true) {
|
|
551
|
+
if (upSeen.has(rootId))
|
|
552
|
+
break;
|
|
553
|
+
upSeen.add(rootId);
|
|
554
|
+
const p = this.getTraceParentId(rootId);
|
|
555
|
+
if (!p)
|
|
556
|
+
break;
|
|
557
|
+
rootId = p;
|
|
558
|
+
}
|
|
559
|
+
const visited = new Set();
|
|
560
|
+
const build = (id) => {
|
|
561
|
+
if (visited.has(id))
|
|
562
|
+
return null;
|
|
563
|
+
visited.add(id);
|
|
564
|
+
const trace = this.getTrace(id);
|
|
565
|
+
if (!trace)
|
|
566
|
+
return null;
|
|
567
|
+
const childIdSet = new Set();
|
|
568
|
+
for (const c of this.getChildTraceIds(id))
|
|
569
|
+
childIdSet.add(c);
|
|
570
|
+
for (const l of this.getLinkedTraceIds(id))
|
|
571
|
+
childIdSet.add(l);
|
|
572
|
+
const children = [];
|
|
573
|
+
// sort for deterministic tree order
|
|
574
|
+
const sortedChildren = Array.from(childIdSet).sort();
|
|
575
|
+
for (const cid of sortedChildren) {
|
|
576
|
+
const node = build(cid);
|
|
577
|
+
if (node)
|
|
578
|
+
children.push(node);
|
|
579
|
+
}
|
|
580
|
+
return { trace, children };
|
|
581
|
+
};
|
|
582
|
+
const rootNode = build(rootId);
|
|
583
|
+
if (rootNode)
|
|
584
|
+
return rootNode;
|
|
585
|
+
// fallback for the requested id itself
|
|
586
|
+
const t = this.getTrace(traceId);
|
|
587
|
+
if (!t) {
|
|
588
|
+
throw new Error(`Trace ${traceId} not found`);
|
|
589
|
+
}
|
|
590
|
+
return { trace: t, children: [] };
|
|
591
|
+
}
|
|
592
|
+
// ---- Agent usage tracking ----
|
|
593
|
+
recordAgentUsage(params) {
|
|
594
|
+
this.db
|
|
595
|
+
.prepare(`
|
|
596
|
+
INSERT INTO agent_usage (id, tenant_id, agent_name, agent_type, session_id, action, target, tokens_used, cost_usd, duration_ms, status, metadata, created_at)
|
|
597
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
598
|
+
`)
|
|
599
|
+
.run(params.id, params.tenantId || null, params.agentName, params.agentType || null, params.sessionId || null, params.action, params.target || null, params.tokensUsed ?? 0, params.costUsd ?? 0, params.durationMs ?? 0, params.status || 'success', JSON.stringify(params.metadata || {}), params.createdAt || Date.now());
|
|
600
|
+
}
|
|
601
|
+
getAgentUsage(filter = {}, tenantId) {
|
|
602
|
+
let sql = 'SELECT * FROM agent_usage WHERE 1=1';
|
|
603
|
+
const params = [];
|
|
604
|
+
const effTenant = tenantId || this.tenantId;
|
|
605
|
+
if (effTenant) {
|
|
606
|
+
sql += ' AND tenant_id = ?';
|
|
607
|
+
params.push(effTenant);
|
|
608
|
+
}
|
|
609
|
+
if (filter.agentName) {
|
|
610
|
+
sql += ' AND agent_name = ?';
|
|
611
|
+
params.push(filter.agentName);
|
|
612
|
+
}
|
|
613
|
+
if (filter.agentType) {
|
|
614
|
+
sql += ' AND agent_type = ?';
|
|
615
|
+
params.push(filter.agentType);
|
|
616
|
+
}
|
|
617
|
+
if (filter.action) {
|
|
618
|
+
sql += ' AND action = ?';
|
|
619
|
+
params.push(filter.action);
|
|
620
|
+
}
|
|
621
|
+
if (filter.status) {
|
|
622
|
+
if (Array.isArray(filter.status)) {
|
|
623
|
+
sql += ` AND status IN (${filter.status.map(() => '?').join(',')})`;
|
|
624
|
+
params.push(...filter.status);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
sql += ' AND status = ?';
|
|
628
|
+
params.push(filter.status);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (filter.fromDate) {
|
|
632
|
+
sql += ' AND created_at >= ?';
|
|
633
|
+
params.push(filter.fromDate);
|
|
634
|
+
}
|
|
635
|
+
if (filter.toDate) {
|
|
636
|
+
sql += ' AND created_at <= ?';
|
|
637
|
+
params.push(filter.toDate);
|
|
638
|
+
}
|
|
639
|
+
sql += ' ORDER BY created_at DESC';
|
|
640
|
+
if (filter.limit) {
|
|
641
|
+
sql += ' LIMIT ?';
|
|
642
|
+
params.push(filter.limit);
|
|
643
|
+
}
|
|
644
|
+
if (filter.offset) {
|
|
645
|
+
sql += ' OFFSET ?';
|
|
646
|
+
params.push(filter.offset);
|
|
647
|
+
}
|
|
648
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
649
|
+
return rows.map((r) => this.rowToAgentUsage(r));
|
|
650
|
+
}
|
|
651
|
+
getUsageStats(agentName, fromDate, toDate, tenantId) {
|
|
652
|
+
const whereParts = [];
|
|
653
|
+
const params = [];
|
|
654
|
+
const effTenant = tenantId || this.tenantId;
|
|
655
|
+
if (effTenant) {
|
|
656
|
+
whereParts.push('tenant_id = ?');
|
|
657
|
+
params.push(effTenant);
|
|
658
|
+
}
|
|
659
|
+
if (agentName) {
|
|
660
|
+
whereParts.push('agent_name = ?');
|
|
661
|
+
params.push(agentName);
|
|
662
|
+
}
|
|
663
|
+
if (fromDate) {
|
|
664
|
+
whereParts.push('created_at >= ?');
|
|
665
|
+
params.push(fromDate);
|
|
666
|
+
}
|
|
667
|
+
if (toDate) {
|
|
668
|
+
whereParts.push('created_at <= ?');
|
|
669
|
+
params.push(toDate);
|
|
670
|
+
}
|
|
671
|
+
const where = whereParts.length ? ` WHERE ${whereParts.join(' AND ')}` : '';
|
|
672
|
+
const totalActionsRow = this.db
|
|
673
|
+
.prepare(`SELECT COUNT(*) as c FROM agent_usage${where}`)
|
|
674
|
+
.get(...params);
|
|
675
|
+
const totalActions = totalActionsRow ? totalActionsRow.c : 0;
|
|
676
|
+
const totalAgentsRow = this.db
|
|
677
|
+
.prepare(`SELECT COUNT(DISTINCT agent_name) as c FROM agent_usage${where}`)
|
|
678
|
+
.get(...params);
|
|
679
|
+
const totalAgents = totalAgentsRow ? totalAgentsRow.c : 0;
|
|
680
|
+
const totals = this.db
|
|
681
|
+
.prepare(`SELECT
|
|
682
|
+
COALESCE(SUM(tokens_used), 0) as tokens,
|
|
683
|
+
COALESCE(SUM(cost_usd), 0) as cost,
|
|
684
|
+
COALESCE(AVG(duration_ms), 0) as avgDur
|
|
685
|
+
FROM agent_usage${where}`)
|
|
686
|
+
.get(...params);
|
|
687
|
+
const totalTokens = totals ? Number(totals.tokens) : 0;
|
|
688
|
+
const totalCostUsd = totals ? Number(totals.cost) : 0;
|
|
689
|
+
const avgDurationMs = totals ? Number(totals.avgDur) : 0;
|
|
690
|
+
const byTypeRows = this.db
|
|
691
|
+
.prepare(`SELECT action, COUNT(*) as count FROM agent_usage${where} GROUP BY action ORDER BY count DESC`)
|
|
692
|
+
.all(...params);
|
|
693
|
+
const actionsByType = {};
|
|
694
|
+
for (const row of byTypeRows) {
|
|
695
|
+
const rec = row;
|
|
696
|
+
actionsByType[String(rec.action)] = Number(rec.count);
|
|
697
|
+
}
|
|
698
|
+
const topRows = this.db
|
|
699
|
+
.prepare(`SELECT
|
|
700
|
+
agent_name,
|
|
701
|
+
COUNT(*) as actions,
|
|
702
|
+
COALESCE(SUM(tokens_used), 0) as tokens,
|
|
703
|
+
COALESCE(SUM(cost_usd), 0) as cost
|
|
704
|
+
FROM agent_usage${where}
|
|
705
|
+
GROUP BY agent_name
|
|
706
|
+
ORDER BY actions DESC, cost DESC
|
|
707
|
+
LIMIT 10`)
|
|
708
|
+
.all(...params);
|
|
709
|
+
const topAgents = topRows.map((r) => {
|
|
710
|
+
const rec = r;
|
|
711
|
+
return {
|
|
712
|
+
agentName: String(rec.agent_name),
|
|
713
|
+
actions: Number(rec.actions),
|
|
714
|
+
tokens: Number(rec.tokens),
|
|
715
|
+
costUsd: Number(rec.cost),
|
|
716
|
+
};
|
|
717
|
+
});
|
|
718
|
+
return {
|
|
719
|
+
totalAgents,
|
|
720
|
+
totalActions,
|
|
721
|
+
totalTokens,
|
|
722
|
+
totalCostUsd,
|
|
723
|
+
avgDurationMs,
|
|
724
|
+
actionsByType,
|
|
725
|
+
topAgents,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
getActiveAgents() {
|
|
729
|
+
const rows = this.db
|
|
730
|
+
.prepare(`
|
|
731
|
+
SELECT agent_name, MAX(created_at) as last_ts, COUNT(*) as total
|
|
732
|
+
FROM agent_usage
|
|
733
|
+
GROUP BY agent_name
|
|
734
|
+
ORDER BY last_ts DESC
|
|
735
|
+
`)
|
|
736
|
+
.all();
|
|
737
|
+
return rows.map((r) => {
|
|
738
|
+
const rec = r;
|
|
739
|
+
const lastTs = Number(rec.last_ts) || Date.now();
|
|
740
|
+
return {
|
|
741
|
+
agentName: String(rec.agent_name),
|
|
742
|
+
lastActive: new Date(lastTs).toISOString(),
|
|
743
|
+
totalActions: Number(rec.total),
|
|
744
|
+
};
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
// ---- Agent usage query helpers for CLI (who / sessions / cost) ----
|
|
748
|
+
getAgentWho(filter = {}) {
|
|
749
|
+
const f = {};
|
|
750
|
+
if (filter.agentType)
|
|
751
|
+
f.agentType = filter.agentType;
|
|
752
|
+
if (filter.activeOnly) {
|
|
753
|
+
f.fromDate = Date.now() - 30 * 60 * 1000;
|
|
754
|
+
}
|
|
755
|
+
const recs = this.getAgentUsage({ ...f, limit: 20000 });
|
|
756
|
+
const map = new Map();
|
|
757
|
+
for (const r of recs) {
|
|
758
|
+
let g = map.get(r.agentName);
|
|
759
|
+
if (!g) {
|
|
760
|
+
g = {
|
|
761
|
+
type: r.agentType,
|
|
762
|
+
lastAction: r.action,
|
|
763
|
+
lastTs: r.createdAt,
|
|
764
|
+
actions: 0,
|
|
765
|
+
tokens: 0,
|
|
766
|
+
cost: 0,
|
|
767
|
+
};
|
|
768
|
+
map.set(r.agentName, g);
|
|
769
|
+
}
|
|
770
|
+
if (r.agentType && !g.type)
|
|
771
|
+
g.type = r.agentType;
|
|
772
|
+
if (r.createdAt >= g.lastTs) {
|
|
773
|
+
g.lastTs = r.createdAt;
|
|
774
|
+
g.lastAction = r.action;
|
|
775
|
+
if (r.sessionId)
|
|
776
|
+
g.lastSession = r.sessionId;
|
|
777
|
+
}
|
|
778
|
+
g.actions += 1;
|
|
779
|
+
g.tokens += r.tokensUsed || 0;
|
|
780
|
+
g.cost += r.costUsd || 0;
|
|
781
|
+
}
|
|
782
|
+
const list = Array.from(map.entries()).map(([name, g]) => ({
|
|
783
|
+
agentName: name,
|
|
784
|
+
agentType: g.type,
|
|
785
|
+
sessionId: g.lastSession,
|
|
786
|
+
lastAction: g.lastAction,
|
|
787
|
+
actions: g.actions,
|
|
788
|
+
tokens: g.tokens,
|
|
789
|
+
costUsd: g.cost,
|
|
790
|
+
lastActive: g.lastTs,
|
|
791
|
+
}));
|
|
792
|
+
list.sort((a, b) => b.lastActive - a.lastActive);
|
|
793
|
+
const lim = filter.limit && filter.limit > 0 ? filter.limit : 100;
|
|
794
|
+
return list.slice(0, lim).map(({ lastActive: _lastActive, ...rest }) => rest);
|
|
795
|
+
}
|
|
796
|
+
getAgentSessions(filter = {}) {
|
|
797
|
+
const f = {};
|
|
798
|
+
if (filter.agentName)
|
|
799
|
+
f.agentName = filter.agentName;
|
|
800
|
+
if (filter.activeOnly) {
|
|
801
|
+
f.fromDate = Date.now() - 30 * 60 * 1000;
|
|
802
|
+
}
|
|
803
|
+
const recs = this.getAgentUsage({ ...f, limit: 20000 });
|
|
804
|
+
const groups = new Map();
|
|
805
|
+
for (const r of recs) {
|
|
806
|
+
const sid = r.sessionId || '';
|
|
807
|
+
const key = `${r.agentName}::${sid}`;
|
|
808
|
+
let g = groups.get(key);
|
|
809
|
+
if (!g) {
|
|
810
|
+
g = {
|
|
811
|
+
agentName: r.agentName,
|
|
812
|
+
ts: [],
|
|
813
|
+
actions: 0,
|
|
814
|
+
tokens: 0,
|
|
815
|
+
cost: 0,
|
|
816
|
+
statuses: [],
|
|
817
|
+
lastStatus: r.status,
|
|
818
|
+
lastTs: r.createdAt,
|
|
819
|
+
};
|
|
820
|
+
groups.set(key, g);
|
|
821
|
+
}
|
|
822
|
+
g.ts.push(r.createdAt);
|
|
823
|
+
g.actions += 1;
|
|
824
|
+
g.tokens += r.tokensUsed || 0;
|
|
825
|
+
g.cost += r.costUsd || 0;
|
|
826
|
+
g.statuses.push(r.status);
|
|
827
|
+
if (r.createdAt >= g.lastTs) {
|
|
828
|
+
g.lastTs = r.createdAt;
|
|
829
|
+
g.lastStatus = r.status;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
let list = Array.from(groups.entries()).map(([key, g]) => {
|
|
833
|
+
const sorted = [...g.ts].sort((a, b) => a - b);
|
|
834
|
+
const startedAt = sorted[0] ?? g.lastTs;
|
|
835
|
+
const last = sorted[sorted.length - 1] ?? g.lastTs;
|
|
836
|
+
const dur = Math.max(0, last - startedAt);
|
|
837
|
+
// session status: use last action's status, or 'failure' if any bad
|
|
838
|
+
let status = g.lastStatus;
|
|
839
|
+
if (g.statuses.some((s) => s === 'failure' || s === 'timeout')) {
|
|
840
|
+
status = g.statuses.includes('failure') ? 'failure' : 'timeout';
|
|
841
|
+
}
|
|
842
|
+
const sid = key.split('::')[1] || 'n/a';
|
|
843
|
+
return {
|
|
844
|
+
sessionId: sid,
|
|
845
|
+
agentName: g.agentName,
|
|
846
|
+
startedAt,
|
|
847
|
+
durationMs: dur,
|
|
848
|
+
actions: g.actions,
|
|
849
|
+
tokens: g.tokens,
|
|
850
|
+
costUsd: g.cost,
|
|
851
|
+
status,
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
if (filter.activeOnly) {
|
|
855
|
+
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
856
|
+
list = list.filter((s) => s.startedAt + s.durationMs >= cutoff || s.startedAt >= cutoff);
|
|
857
|
+
}
|
|
858
|
+
list.sort((a, b) => b.startedAt - a.startedAt);
|
|
859
|
+
const lim = filter.limit && filter.limit > 0 ? filter.limit : 100;
|
|
860
|
+
return list.slice(0, lim);
|
|
861
|
+
}
|
|
862
|
+
getAgentCostSummary(filter = {}) {
|
|
863
|
+
const recs = this.getAgentUsage({
|
|
864
|
+
agentName: filter.agentName,
|
|
865
|
+
fromDate: filter.fromDate,
|
|
866
|
+
toDate: filter.toDate,
|
|
867
|
+
limit: 100000,
|
|
868
|
+
});
|
|
869
|
+
let totalCostUsd = 0;
|
|
870
|
+
const costByAgent = {};
|
|
871
|
+
const costByModel = {};
|
|
872
|
+
for (const r of recs) {
|
|
873
|
+
const c = r.costUsd || 0;
|
|
874
|
+
totalCostUsd += c;
|
|
875
|
+
costByAgent[r.agentName] = (costByAgent[r.agentName] || 0) + c;
|
|
876
|
+
const meta = r.metadata || {};
|
|
877
|
+
const model = typeof meta.model === 'string'
|
|
878
|
+
? meta.model
|
|
879
|
+
: 'unknown';
|
|
880
|
+
costByModel[model] = (costByModel[model] || 0) + c;
|
|
881
|
+
}
|
|
882
|
+
return { totalCostUsd, costByAgent, costByModel };
|
|
883
|
+
}
|
|
884
|
+
// ---- API Key management (stored hashed with SHA-256) ----
|
|
885
|
+
// ---- Stats ----
|
|
886
|
+
getStats(tenantId) {
|
|
887
|
+
const where = tenantId ? ' WHERE tenant_id = ?' : '';
|
|
888
|
+
const traceWhere = tenantId ? ' WHERE tenant_id = ?' : '';
|
|
889
|
+
const params = tenantId ? [tenantId] : [];
|
|
890
|
+
const totalRuns = this.db
|
|
891
|
+
.prepare('SELECT COUNT(*) as c FROM runs' + where)
|
|
892
|
+
.get(...params);
|
|
893
|
+
const totalTraces = this.db
|
|
894
|
+
.prepare('SELECT COUNT(*) as c FROM traces' + traceWhere)
|
|
895
|
+
.get(...params);
|
|
896
|
+
const successCount = this.db
|
|
897
|
+
.prepare('SELECT COUNT(*) as c FROM traces' +
|
|
898
|
+
traceWhere +
|
|
899
|
+
(tenantId ? ' AND' : ' WHERE') +
|
|
900
|
+
" status = 'success'")
|
|
901
|
+
.get(...params);
|
|
902
|
+
const avgLatency = this.db
|
|
903
|
+
.prepare('SELECT AVG(latency_ms) as v FROM traces' + traceWhere)
|
|
904
|
+
.get(...params);
|
|
905
|
+
const totalCost = this.db
|
|
906
|
+
.prepare('SELECT SUM(cost_usd) as v FROM traces' + traceWhere)
|
|
907
|
+
.get(...params);
|
|
908
|
+
const totalTokens = this.db
|
|
909
|
+
.prepare('SELECT SUM(total_tokens) as v FROM traces' + traceWhere)
|
|
910
|
+
.get(...params);
|
|
911
|
+
const topTools = this.db
|
|
912
|
+
.prepare(`
|
|
913
|
+
SELECT tc.name, COUNT(*) as count, AVG(tc.latency_ms) as avgLatencyMs
|
|
914
|
+
FROM tool_calls tc
|
|
915
|
+
INNER JOIN traces t ON tc.trace_id = t.id${tenantId ? ' WHERE t.tenant_id = ?' : ''}
|
|
916
|
+
GROUP BY tc.name ORDER BY count DESC LIMIT 10
|
|
917
|
+
`)
|
|
918
|
+
.all(...(tenantId ? [tenantId] : []));
|
|
919
|
+
const topToolsMapped = topTools.map((t) => {
|
|
920
|
+
const rec = t;
|
|
921
|
+
return {
|
|
922
|
+
name: rec.name,
|
|
923
|
+
count: Number(rec.count),
|
|
924
|
+
avgLatencyMs: Number(rec.avgLatencyMs),
|
|
925
|
+
};
|
|
926
|
+
});
|
|
927
|
+
const topErrors = this.db
|
|
928
|
+
.prepare(`
|
|
929
|
+
SELECT error, COUNT(*) as count FROM traces
|
|
930
|
+
WHERE error IS NOT NULL AND error != ''${tenantId ? ' AND tenant_id = ?' : ''}
|
|
931
|
+
GROUP BY error ORDER BY count DESC LIMIT 10
|
|
932
|
+
`)
|
|
933
|
+
.all(...(tenantId ? [tenantId] : []));
|
|
934
|
+
const topErrorsMapped = topErrors.map((e) => {
|
|
935
|
+
const rec = e;
|
|
936
|
+
return { error: rec.error, count: Number(rec.count) };
|
|
937
|
+
});
|
|
938
|
+
const costByModelRows = this.db
|
|
939
|
+
.prepare(`
|
|
940
|
+
SELECT COALESCE(model, 'unknown') as model, SUM(cost_usd) as cost
|
|
941
|
+
FROM traces${traceWhere} GROUP BY model
|
|
942
|
+
`)
|
|
943
|
+
.all(...params);
|
|
944
|
+
const costByModel = {};
|
|
945
|
+
for (const r of costByModelRows) {
|
|
946
|
+
const rec = r;
|
|
947
|
+
costByModel[rec.model] = Number(rec.cost) || 0;
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
totalRuns: totalRuns.c,
|
|
951
|
+
totalTraces: totalTraces.c,
|
|
952
|
+
successRate: totalTraces.c > 0 ? successCount.c / totalTraces.c : 0,
|
|
953
|
+
avgLatencyMs: avgLatency.v || 0,
|
|
954
|
+
totalCostUsd: totalCost.v || 0,
|
|
955
|
+
costByModel,
|
|
956
|
+
totalTokens: totalTokens.v || 0,
|
|
957
|
+
avgTokensPerTrace: totalTraces.c > 0 ? totalTokens.v / totalTraces.c : 0,
|
|
958
|
+
topTools: topToolsMapped,
|
|
959
|
+
topErrors: topErrorsMapped,
|
|
960
|
+
droppedTraces: this._droppedTraces || 0,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
getCostBreakdown(runId, tenantId) {
|
|
964
|
+
const whereParts = [];
|
|
965
|
+
const params = [];
|
|
966
|
+
if (runId) {
|
|
967
|
+
whereParts.push('run_id = ?');
|
|
968
|
+
params.push(runId);
|
|
969
|
+
}
|
|
970
|
+
if (tenantId) {
|
|
971
|
+
whereParts.push('tenant_id = ?');
|
|
972
|
+
params.push(tenantId);
|
|
973
|
+
}
|
|
974
|
+
const where = whereParts.length ? ' WHERE ' + whereParts.join(' AND ') : '';
|
|
975
|
+
const totalCost = this.db
|
|
976
|
+
.prepare(`SELECT SUM(cost_usd) as v FROM traces${where}`)
|
|
977
|
+
.get(...params);
|
|
978
|
+
const costByModelRows = this.db
|
|
979
|
+
.prepare(`SELECT COALESCE(model, 'unknown') as model, SUM(cost_usd) as cost FROM traces${where} GROUP BY model`)
|
|
980
|
+
.all(...params);
|
|
981
|
+
const costByModel = {};
|
|
982
|
+
for (const r of costByModelRows) {
|
|
983
|
+
const rec = r;
|
|
984
|
+
costByModel[rec.model] = Number(rec.cost) || 0;
|
|
985
|
+
}
|
|
986
|
+
const byDayRows = this.db
|
|
987
|
+
.prepare(`SELECT strftime('%Y-%m-%d', created_at / 1000, 'unixepoch') as day, SUM(cost_usd) as cost FROM traces${where} GROUP BY day ORDER BY day`)
|
|
988
|
+
.all(...params);
|
|
989
|
+
const costByDay = {};
|
|
990
|
+
for (const r of byDayRows) {
|
|
991
|
+
const rec = r;
|
|
992
|
+
if (rec.day) {
|
|
993
|
+
costByDay[rec.day] = Number(rec.cost) || 0;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
totalCostUsd: totalCost.v || 0,
|
|
998
|
+
costByModel,
|
|
999
|
+
costByDay,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
// ---- Cleanup ----
|
|
1003
|
+
cleanup(maxTraces = 10000) {
|
|
1004
|
+
let deleted = 0;
|
|
1005
|
+
const txn = this.db.transaction(() => {
|
|
1006
|
+
const count = this.db.prepare('SELECT COUNT(*) as c FROM traces').get();
|
|
1007
|
+
if (count.c <= maxTraces)
|
|
1008
|
+
return;
|
|
1009
|
+
deleted = count.c - maxTraces;
|
|
1010
|
+
this.db
|
|
1011
|
+
.prepare(`
|
|
1012
|
+
DELETE FROM traces WHERE id IN (
|
|
1013
|
+
SELECT id FROM traces ORDER BY created_at ASC LIMIT ?
|
|
1014
|
+
)
|
|
1015
|
+
`)
|
|
1016
|
+
.run(deleted);
|
|
1017
|
+
});
|
|
1018
|
+
txn();
|
|
1019
|
+
return deleted;
|
|
1020
|
+
}
|
|
1021
|
+
cleanupOldTraces(before) {
|
|
1022
|
+
if (!before || before <= 0)
|
|
1023
|
+
return 0;
|
|
1024
|
+
// Clean dependents that do not have ON DELETE CASCADE (scores, trace_links)
|
|
1025
|
+
this.db
|
|
1026
|
+
.prepare(`DELETE FROM scores WHERE trace_id IN (SELECT id FROM traces WHERE created_at < ?)`)
|
|
1027
|
+
.run(before);
|
|
1028
|
+
this.db
|
|
1029
|
+
.prepare(`
|
|
1030
|
+
DELETE FROM trace_links
|
|
1031
|
+
WHERE source_trace_id IN (SELECT id FROM traces WHERE created_at < ?)
|
|
1032
|
+
OR target_trace_id IN (SELECT id FROM traces WHERE created_at < ?)
|
|
1033
|
+
`)
|
|
1034
|
+
.run(before, before);
|
|
1035
|
+
const res = this.db.prepare('DELETE FROM traces WHERE created_at < ?').run(before);
|
|
1036
|
+
return res.changes ?? 0;
|
|
1037
|
+
}
|
|
1038
|
+
cleanupOldRuns(before) {
|
|
1039
|
+
if (!before || before <= 0)
|
|
1040
|
+
return 0;
|
|
1041
|
+
// CASCADE will delete associated traces + their tool_calls
|
|
1042
|
+
const res = this.db.prepare('DELETE FROM runs WHERE started_at < ?').run(before);
|
|
1043
|
+
return res.changes ?? 0;
|
|
1044
|
+
}
|
|
1045
|
+
cleanupOldAgentUsage(before) {
|
|
1046
|
+
if (!before || before <= 0)
|
|
1047
|
+
return 0;
|
|
1048
|
+
const res = this.db.prepare('DELETE FROM agent_usage WHERE created_at < ?').run(before);
|
|
1049
|
+
return res.changes ?? 0;
|
|
1050
|
+
}
|
|
1051
|
+
// ---- Retention policy (persisted) ----
|
|
1052
|
+
getSetting(key) {
|
|
1053
|
+
const row = this.db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
|
|
1054
|
+
return row && typeof row.value === 'string' ? row.value : null;
|
|
1055
|
+
}
|
|
1056
|
+
setSetting(key, value) {
|
|
1057
|
+
this.db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(key, value);
|
|
1058
|
+
}
|
|
1059
|
+
getRetentionPolicy() {
|
|
1060
|
+
const rdStr = this.getSetting('retentionDays');
|
|
1061
|
+
const ciStr = this.getSetting('cleanupIntervalHours');
|
|
1062
|
+
return {
|
|
1063
|
+
retentionDays: rdStr !== null ? Math.max(0, parseInt(rdStr, 10) || 0) : 30,
|
|
1064
|
+
cleanupIntervalHours: ciStr !== null ? Math.max(1, parseInt(ciStr, 10) || 24) : 24,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
setRetentionPolicy(retentionDays, cleanupIntervalHours) {
|
|
1068
|
+
const days = Math.max(0, Math.floor(Number(retentionDays) || 0));
|
|
1069
|
+
this.setSetting('retentionDays', String(days));
|
|
1070
|
+
if (cleanupIntervalHours !== undefined) {
|
|
1071
|
+
const hrs = Math.max(1, Math.floor(Number(cleanupIntervalHours) || 24));
|
|
1072
|
+
this.setSetting('cleanupIntervalHours', String(hrs));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// ---- Health / Integrity ----
|
|
1076
|
+
getHealthInfo() {
|
|
1077
|
+
const traceCountRow = this.db.prepare('SELECT COUNT(*) as c FROM traces').get();
|
|
1078
|
+
const traceCount = traceCountRow ? traceCountRow.c : 0;
|
|
1079
|
+
const dbSize = this.getDbSize();
|
|
1080
|
+
const integrity = this.checkIntegrity();
|
|
1081
|
+
return {
|
|
1082
|
+
dbPath: this.dbPath,
|
|
1083
|
+
traceCount,
|
|
1084
|
+
dbSize,
|
|
1085
|
+
integrity,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
getDbSize() {
|
|
1089
|
+
if (!this.dbPath || this.dbPath === ':memory:') {
|
|
1090
|
+
return 0;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
return statSync(this.dbPath).size;
|
|
1094
|
+
}
|
|
1095
|
+
catch (_) {
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
checkIntegrity() {
|
|
1100
|
+
const required = [
|
|
1101
|
+
'runs',
|
|
1102
|
+
'traces',
|
|
1103
|
+
'tool_calls',
|
|
1104
|
+
'scores',
|
|
1105
|
+
'alerts',
|
|
1106
|
+
'alert_history',
|
|
1107
|
+
'trace_links',
|
|
1108
|
+
'agent_usage',
|
|
1109
|
+
'webhooks',
|
|
1110
|
+
'api_keys',
|
|
1111
|
+
'version',
|
|
1112
|
+
];
|
|
1113
|
+
const existingRows = this.db
|
|
1114
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
1115
|
+
.all();
|
|
1116
|
+
const existing = new Set(existingRows.map((r) => r.name));
|
|
1117
|
+
const missing = required.filter((t) => !existing.has(t));
|
|
1118
|
+
const tablesExist = missing.length === 0;
|
|
1119
|
+
// PRAGMA integrity_check
|
|
1120
|
+
let pragmaMsg = '';
|
|
1121
|
+
try {
|
|
1122
|
+
const res = this.db.pragma('integrity_check');
|
|
1123
|
+
const ok = Array.isArray(res) && res.length === 1 && res[0]?.integrity_check === 'ok';
|
|
1124
|
+
if (!ok) {
|
|
1125
|
+
pragmaMsg = res && res[0] ? String(res[0].integrity_check) : 'failed';
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
catch (e) {
|
|
1129
|
+
pragmaMsg = String(e);
|
|
1130
|
+
}
|
|
1131
|
+
let orphanCount = 0;
|
|
1132
|
+
const orphanDetails = [];
|
|
1133
|
+
if (tablesExist) {
|
|
1134
|
+
const ot = this.db
|
|
1135
|
+
.prepare('SELECT COUNT(*) as c FROM traces WHERE run_id NOT IN (SELECT id FROM runs)')
|
|
1136
|
+
.get();
|
|
1137
|
+
if (ot && ot.c > 0) {
|
|
1138
|
+
orphanCount += ot.c;
|
|
1139
|
+
orphanDetails.push(`traces without run: ${ot.c}`);
|
|
1140
|
+
}
|
|
1141
|
+
const otc = this.db
|
|
1142
|
+
.prepare('SELECT COUNT(*) as c FROM tool_calls WHERE trace_id NOT IN (SELECT id FROM traces)')
|
|
1143
|
+
.get();
|
|
1144
|
+
if (otc && otc.c > 0) {
|
|
1145
|
+
orphanCount += otc.c;
|
|
1146
|
+
orphanDetails.push(`tool_calls without trace: ${otc.c}`);
|
|
1147
|
+
}
|
|
1148
|
+
const osc = this.db
|
|
1149
|
+
.prepare('SELECT COUNT(*) as c FROM scores WHERE trace_id NOT IN (SELECT id FROM traces)')
|
|
1150
|
+
.get();
|
|
1151
|
+
if (osc && osc.c > 0) {
|
|
1152
|
+
orphanCount += osc.c;
|
|
1153
|
+
orphanDetails.push(`scores without trace: ${osc.c}`);
|
|
1154
|
+
}
|
|
1155
|
+
const ol = this.db
|
|
1156
|
+
.prepare(`
|
|
1157
|
+
SELECT COUNT(*) as c FROM trace_links
|
|
1158
|
+
WHERE source_trace_id NOT IN (SELECT id FROM traces)
|
|
1159
|
+
OR target_trace_id NOT IN (SELECT id FROM traces)
|
|
1160
|
+
`)
|
|
1161
|
+
.get();
|
|
1162
|
+
if (ol && ol.c > 0) {
|
|
1163
|
+
orphanCount += ol.c;
|
|
1164
|
+
orphanDetails.push(`trace_links orphaned: ${ol.c}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const noOrphans = orphanCount === 0;
|
|
1168
|
+
const detailsParts = [];
|
|
1169
|
+
if (pragmaMsg)
|
|
1170
|
+
detailsParts.push(`pragma: ${pragmaMsg}`);
|
|
1171
|
+
if (orphanDetails.length)
|
|
1172
|
+
detailsParts.push(...orphanDetails);
|
|
1173
|
+
const details = detailsParts.length ? detailsParts.join('; ') : undefined;
|
|
1174
|
+
return {
|
|
1175
|
+
tablesExist,
|
|
1176
|
+
noOrphans,
|
|
1177
|
+
details,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
// ---- Helpers ----
|
|
1181
|
+
rowToRun(row) {
|
|
1182
|
+
const r = row;
|
|
1183
|
+
return {
|
|
1184
|
+
id: r.id,
|
|
1185
|
+
tenantId: r.tenant_id || undefined,
|
|
1186
|
+
name: r.name,
|
|
1187
|
+
status: r.status,
|
|
1188
|
+
traceCount: Number(r.trace_count),
|
|
1189
|
+
totalTokens: {
|
|
1190
|
+
promptTokens: Number(r.total_prompt_tokens),
|
|
1191
|
+
completionTokens: Number(r.total_completion_tokens),
|
|
1192
|
+
totalTokens: Number(r.total_tokens),
|
|
1193
|
+
},
|
|
1194
|
+
totalToolCalls: Number(r.total_tool_calls),
|
|
1195
|
+
totalLatencyMs: Number(r.total_latency_ms),
|
|
1196
|
+
totalCostUsd: Number(r.total_cost_usd),
|
|
1197
|
+
errorCount: Number(r.error_count),
|
|
1198
|
+
startedAt: Number(r.started_at),
|
|
1199
|
+
completedAt: r.completed_at != null ? Number(r.completed_at) : undefined,
|
|
1200
|
+
metadata: JSON.parse(r.metadata || '{}'),
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
safeJsonParse(val) {
|
|
1204
|
+
if (val == null || val === '' || val === 'null')
|
|
1205
|
+
return null;
|
|
1206
|
+
try {
|
|
1207
|
+
return JSON.parse(val);
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return val;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
rowToTrace(row) {
|
|
1214
|
+
const r = row;
|
|
1215
|
+
const toolCallsRows = this.db
|
|
1216
|
+
.prepare('SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY timestamp')
|
|
1217
|
+
.all(r.id);
|
|
1218
|
+
return {
|
|
1219
|
+
id: r.id,
|
|
1220
|
+
runId: r.run_id,
|
|
1221
|
+
name: r.name,
|
|
1222
|
+
status: r.status,
|
|
1223
|
+
input: this.safeJsonParse(r.input),
|
|
1224
|
+
output: this.safeJsonParse(r.output),
|
|
1225
|
+
tokens: {
|
|
1226
|
+
promptTokens: Number(r.prompt_tokens),
|
|
1227
|
+
completionTokens: Number(r.completion_tokens),
|
|
1228
|
+
totalTokens: Number(r.total_tokens),
|
|
1229
|
+
model: r.model,
|
|
1230
|
+
provider: r.provider,
|
|
1231
|
+
},
|
|
1232
|
+
toolCalls: toolCallsRows.map((tc) => {
|
|
1233
|
+
const t = tc;
|
|
1234
|
+
return {
|
|
1235
|
+
id: t.id,
|
|
1236
|
+
name: t.name,
|
|
1237
|
+
input: this.safeJsonParse(t.input),
|
|
1238
|
+
output: this.safeJsonParse(t.output),
|
|
1239
|
+
latencyMs: Number(t.latency_ms),
|
|
1240
|
+
success: t.success === 1,
|
|
1241
|
+
error: t.error,
|
|
1242
|
+
timestamp: Number(t.timestamp),
|
|
1243
|
+
};
|
|
1244
|
+
}),
|
|
1245
|
+
latencyMs: Number(r.latency_ms),
|
|
1246
|
+
costUsd: Number(r.cost_usd),
|
|
1247
|
+
error: r.error,
|
|
1248
|
+
metadata: JSON.parse(r.metadata || '{}'),
|
|
1249
|
+
parentId: r.parent_id || undefined,
|
|
1250
|
+
tenantId: r.tenant_id || undefined,
|
|
1251
|
+
createdAt: Number(r.created_at),
|
|
1252
|
+
updatedAt: Number(r.updated_at),
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
rowToAgentUsage(row) {
|
|
1256
|
+
const r = row;
|
|
1257
|
+
return {
|
|
1258
|
+
id: r.id,
|
|
1259
|
+
tenantId: r.tenant_id || undefined,
|
|
1260
|
+
agentName: r.agent_name,
|
|
1261
|
+
agentType: r.agent_type || undefined,
|
|
1262
|
+
sessionId: r.session_id || undefined,
|
|
1263
|
+
action: r.action,
|
|
1264
|
+
target: r.target || undefined,
|
|
1265
|
+
tokensUsed: Number(r.tokens_used) || 0,
|
|
1266
|
+
costUsd: Number(r.cost_usd) || 0,
|
|
1267
|
+
durationMs: Number(r.duration_ms) || 0,
|
|
1268
|
+
status: r.status || 'success',
|
|
1269
|
+
metadata: JSON.parse(r.metadata || '{}'),
|
|
1270
|
+
createdAt: Number(r.created_at),
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
// ── Webhook Management ──────────────────────────────────────────
|
|
1274
|
+
registerWebhook(url, events, secret) {
|
|
1275
|
+
const id = randomUUID();
|
|
1276
|
+
this.db
|
|
1277
|
+
.prepare(`
|
|
1278
|
+
INSERT INTO webhooks (id, url, secret, events, enabled, created_at)
|
|
1279
|
+
VALUES (?, ?, ?, ?, 1, ?)
|
|
1280
|
+
`)
|
|
1281
|
+
.run(id, url, secret || null, JSON.stringify(events), Date.now());
|
|
1282
|
+
return id;
|
|
1283
|
+
}
|
|
1284
|
+
getWebhooks() {
|
|
1285
|
+
const rows = this.db
|
|
1286
|
+
.prepare(`
|
|
1287
|
+
SELECT id, url, secret, events, enabled, created_at, last_triggered_at, failure_count
|
|
1288
|
+
FROM webhooks ORDER BY created_at DESC
|
|
1289
|
+
`)
|
|
1290
|
+
.all();
|
|
1291
|
+
return rows.map((r) => ({
|
|
1292
|
+
id: r.id,
|
|
1293
|
+
url: r.url,
|
|
1294
|
+
secret: r.secret || undefined,
|
|
1295
|
+
events: JSON.parse(r.events || '[]'),
|
|
1296
|
+
enabled: !!r.enabled,
|
|
1297
|
+
createdAt: Number(r.created_at),
|
|
1298
|
+
lastTriggeredAt: r.last_triggered_at || undefined,
|
|
1299
|
+
failureCount: Number(r.failure_count) || 0,
|
|
1300
|
+
}));
|
|
1301
|
+
}
|
|
1302
|
+
deleteWebhook(id) {
|
|
1303
|
+
this.db.prepare(`DELETE FROM webhooks WHERE id = ?`).run(id);
|
|
1304
|
+
}
|
|
1305
|
+
updateWebhookLastTriggered(id) {
|
|
1306
|
+
this.db
|
|
1307
|
+
.prepare(`
|
|
1308
|
+
UPDATE webhooks SET last_triggered_at = ? WHERE id = ?
|
|
1309
|
+
`)
|
|
1310
|
+
.run(Date.now(), id);
|
|
1311
|
+
}
|
|
1312
|
+
incrementWebhookFailures(id) {
|
|
1313
|
+
this.db
|
|
1314
|
+
.prepare(`
|
|
1315
|
+
UPDATE webhooks SET failure_count = failure_count + 1 WHERE id = ?
|
|
1316
|
+
`)
|
|
1317
|
+
.run(id);
|
|
1318
|
+
}
|
|
1319
|
+
resetWebhookFailures(id) {
|
|
1320
|
+
this.db
|
|
1321
|
+
.prepare(`
|
|
1322
|
+
UPDATE webhooks SET failure_count = 0 WHERE id = ?
|
|
1323
|
+
`)
|
|
1324
|
+
.run(id);
|
|
1325
|
+
}
|
|
1326
|
+
getEnabledWebhooksForEvent(event) {
|
|
1327
|
+
const rows = this.db
|
|
1328
|
+
.prepare(`
|
|
1329
|
+
SELECT id, url, secret, events, enabled, created_at, last_triggered_at, failure_count
|
|
1330
|
+
FROM webhooks WHERE enabled = 1
|
|
1331
|
+
`)
|
|
1332
|
+
.all();
|
|
1333
|
+
return rows
|
|
1334
|
+
.map((r) => ({
|
|
1335
|
+
id: r.id,
|
|
1336
|
+
url: r.url,
|
|
1337
|
+
secret: r.secret || undefined,
|
|
1338
|
+
events: JSON.parse(r.events || '[]'),
|
|
1339
|
+
enabled: !!r.enabled,
|
|
1340
|
+
createdAt: Number(r.created_at),
|
|
1341
|
+
lastTriggeredAt: r.last_triggered_at || undefined,
|
|
1342
|
+
failureCount: Number(r.failure_count) || 0,
|
|
1343
|
+
}))
|
|
1344
|
+
.filter((w) => w.events.includes(event));
|
|
1345
|
+
}
|
|
1346
|
+
// ── API Key Management ──────────────────────────────────────────
|
|
1347
|
+
createApiKey(name, permissions = ['read', 'write'], key) {
|
|
1348
|
+
const id = randomUUID();
|
|
1349
|
+
const secretKey = key || randomBytes(32).toString('hex');
|
|
1350
|
+
const keyHash = createHash('sha256').update(secretKey).digest('hex');
|
|
1351
|
+
const now = Date.now();
|
|
1352
|
+
const keyPreview = secretKey.slice(0, 8) + '****';
|
|
1353
|
+
this.db
|
|
1354
|
+
.prepare(`
|
|
1355
|
+
INSERT INTO api_keys (id, name, key_hash, key_preview, permissions, created_at, enabled)
|
|
1356
|
+
VALUES (?, ?, ?, ?, ?, ?, 1)
|
|
1357
|
+
`)
|
|
1358
|
+
.run(id, name, keyHash, keyPreview, JSON.stringify(permissions), now);
|
|
1359
|
+
return { id, name, key: secretKey, preview: keyPreview, createdAt: now };
|
|
1360
|
+
}
|
|
1361
|
+
getApiKeys() {
|
|
1362
|
+
const rows = this.db
|
|
1363
|
+
.prepare(`
|
|
1364
|
+
SELECT id, name, created_at, last_used_at, enabled
|
|
1365
|
+
FROM api_keys ORDER BY created_at DESC
|
|
1366
|
+
`)
|
|
1367
|
+
.all();
|
|
1368
|
+
return rows.map((r) => ({
|
|
1369
|
+
id: r.id,
|
|
1370
|
+
name: r.name,
|
|
1371
|
+
createdAt: Number(r.created_at),
|
|
1372
|
+
lastUsedAt: r.last_used_at || null,
|
|
1373
|
+
enabled: !!r.enabled,
|
|
1374
|
+
}));
|
|
1375
|
+
}
|
|
1376
|
+
validateApiKey(key) {
|
|
1377
|
+
const keyHash = createHash('sha256').update(key).digest('hex');
|
|
1378
|
+
const row = this.db
|
|
1379
|
+
.prepare(`
|
|
1380
|
+
SELECT id, permissions, enabled FROM api_keys WHERE key_hash = ?
|
|
1381
|
+
`)
|
|
1382
|
+
.get(keyHash);
|
|
1383
|
+
if (!row || !row.enabled)
|
|
1384
|
+
return { valid: false, permissions: [] };
|
|
1385
|
+
this.db
|
|
1386
|
+
.prepare(`UPDATE api_keys SET last_used_at = ? WHERE key_hash = ?`)
|
|
1387
|
+
.run(Date.now(), keyHash);
|
|
1388
|
+
return { valid: true, permissions: JSON.parse(row.permissions || '[]') };
|
|
1389
|
+
}
|
|
1390
|
+
revokeApiKey(id) {
|
|
1391
|
+
this.db.prepare(`DELETE FROM api_keys WHERE id = ?`).run(id);
|
|
1392
|
+
}
|
|
1393
|
+
// ── Storage Stats ───────────────────────────────────────────────
|
|
1394
|
+
getStorageStats() {
|
|
1395
|
+
let totalSizeBytes = 0;
|
|
1396
|
+
try {
|
|
1397
|
+
totalSizeBytes = statSync(this.dbPath).size;
|
|
1398
|
+
}
|
|
1399
|
+
catch (_) {
|
|
1400
|
+
// DB file may be :memory: or not yet created
|
|
1401
|
+
}
|
|
1402
|
+
const traceCount = this.db.prepare(`SELECT COUNT(*) as c FROM traces`).get();
|
|
1403
|
+
const runCount = this.db.prepare(`SELECT COUNT(*) as c FROM runs`).get();
|
|
1404
|
+
const oldest = this.db.prepare(`SELECT MIN(created_at) as v FROM traces`).get();
|
|
1405
|
+
const newest = this.db.prepare(`SELECT MAX(created_at) as v FROM traces`).get();
|
|
1406
|
+
return {
|
|
1407
|
+
totalSizeBytes,
|
|
1408
|
+
traceCount: traceCount.c,
|
|
1409
|
+
runCount: runCount.c,
|
|
1410
|
+
oldestTrace: oldest.v,
|
|
1411
|
+
newestTrace: newest.v,
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
// ── Project / Multi-tenant CRUD ─────────────────────────────────
|
|
1415
|
+
createProject(name) {
|
|
1416
|
+
const id = randomUUID();
|
|
1417
|
+
const apiKey = `at_${randomBytes(24).toString('hex')}`;
|
|
1418
|
+
const now = Date.now();
|
|
1419
|
+
this.db
|
|
1420
|
+
.prepare('INSERT INTO projects (id, name, api_key, created_at) VALUES (?, ?, ?, ?)')
|
|
1421
|
+
.run(id, name, apiKey, now);
|
|
1422
|
+
return { id, name, apiKey, createdAt: now };
|
|
1423
|
+
}
|
|
1424
|
+
getProject(apiKey) {
|
|
1425
|
+
const row = this.db
|
|
1426
|
+
.prepare('SELECT id, name, api_key, created_at FROM projects WHERE api_key = ?')
|
|
1427
|
+
.get(apiKey);
|
|
1428
|
+
if (!row)
|
|
1429
|
+
return null;
|
|
1430
|
+
return {
|
|
1431
|
+
id: row.id,
|
|
1432
|
+
name: row.name,
|
|
1433
|
+
apiKey: row.api_key,
|
|
1434
|
+
createdAt: row.created_at,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
getProjectById(id) {
|
|
1438
|
+
const row = this.db
|
|
1439
|
+
.prepare('SELECT id, name, api_key, created_at FROM projects WHERE id = ?')
|
|
1440
|
+
.get(id);
|
|
1441
|
+
if (!row)
|
|
1442
|
+
return null;
|
|
1443
|
+
return {
|
|
1444
|
+
id: row.id,
|
|
1445
|
+
name: row.name,
|
|
1446
|
+
apiKey: row.api_key,
|
|
1447
|
+
createdAt: row.created_at,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
listProjects() {
|
|
1451
|
+
const rows = this.db
|
|
1452
|
+
.prepare('SELECT id, name, api_key, created_at FROM projects ORDER BY created_at DESC')
|
|
1453
|
+
.all();
|
|
1454
|
+
return rows.map((r) => ({
|
|
1455
|
+
id: r.id,
|
|
1456
|
+
name: r.name,
|
|
1457
|
+
apiKey: r.api_key,
|
|
1458
|
+
createdAt: r.created_at,
|
|
1459
|
+
}));
|
|
1460
|
+
}
|
|
1461
|
+
deleteProject(id) {
|
|
1462
|
+
const res = this.db.prepare('DELETE FROM projects WHERE id = ?').run(id);
|
|
1463
|
+
return (res.changes ?? 0) > 0;
|
|
1464
|
+
}
|
|
1465
|
+
close() {
|
|
1466
|
+
const entry = TraceStorage._connections.get(this.dbPath);
|
|
1467
|
+
if (entry) {
|
|
1468
|
+
entry.refCount--;
|
|
1469
|
+
if (entry.refCount <= 0) {
|
|
1470
|
+
entry.db.close();
|
|
1471
|
+
TraceStorage._connections.delete(this.dbPath);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
this.db.close();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
//# sourceMappingURL=storage.js.map
|