agent-tool-forge 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/lib/agent-registry.js +170 -0
- package/lib/api-client.js +792 -0
- package/lib/api-loader.js +260 -0
- package/lib/auth.d.ts +25 -0
- package/lib/auth.js +158 -0
- package/lib/checks/check-adapter.js +172 -0
- package/lib/checks/compose.js +42 -0
- package/lib/checks/content-match.js +14 -0
- package/lib/checks/cost-budget.js +11 -0
- package/lib/checks/index.js +18 -0
- package/lib/checks/json-valid.js +15 -0
- package/lib/checks/latency.js +11 -0
- package/lib/checks/length-bounds.js +17 -0
- package/lib/checks/negative-match.js +14 -0
- package/lib/checks/no-hallucinated-numbers.js +63 -0
- package/lib/checks/non-empty.js +34 -0
- package/lib/checks/regex-match.js +12 -0
- package/lib/checks/run-checks.js +84 -0
- package/lib/checks/schema-match.js +26 -0
- package/lib/checks/tool-call-count.js +16 -0
- package/lib/checks/tool-selection.js +34 -0
- package/lib/checks/types.js +45 -0
- package/lib/comparison/compare.js +86 -0
- package/lib/comparison/format.js +104 -0
- package/lib/comparison/index.js +6 -0
- package/lib/comparison/statistics.js +59 -0
- package/lib/comparison/types.js +41 -0
- package/lib/config-schema.js +200 -0
- package/lib/config.d.ts +66 -0
- package/lib/conversation-store.d.ts +77 -0
- package/lib/conversation-store.js +443 -0
- package/lib/db.d.ts +6 -0
- package/lib/db.js +1112 -0
- package/lib/dep-check.js +99 -0
- package/lib/drift-background.js +61 -0
- package/lib/drift-monitor.js +187 -0
- package/lib/eval-runner.js +566 -0
- package/lib/fixtures/fixture-store.js +161 -0
- package/lib/fixtures/index.js +11 -0
- package/lib/forge-engine.js +982 -0
- package/lib/forge-eval-generator.js +417 -0
- package/lib/forge-file-writer.js +386 -0
- package/lib/forge-service-client.js +190 -0
- package/lib/forge-service.d.ts +4 -0
- package/lib/forge-service.js +655 -0
- package/lib/forge-verifier-generator.js +271 -0
- package/lib/handlers/admin.js +151 -0
- package/lib/handlers/agents.js +229 -0
- package/lib/handlers/chat-resume.js +334 -0
- package/lib/handlers/chat-sync.js +320 -0
- package/lib/handlers/chat.js +320 -0
- package/lib/handlers/conversations.js +92 -0
- package/lib/handlers/preferences.js +88 -0
- package/lib/handlers/tools-list.js +58 -0
- package/lib/hitl-engine.d.ts +60 -0
- package/lib/hitl-engine.js +261 -0
- package/lib/http-utils.js +92 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +141 -0
- package/lib/init.js +636 -0
- package/lib/manual-entry.js +59 -0
- package/lib/mcp-server.js +252 -0
- package/lib/output-groups.js +54 -0
- package/lib/postgres-store.d.ts +31 -0
- package/lib/postgres-store.js +465 -0
- package/lib/preference-store.d.ts +47 -0
- package/lib/preference-store.js +79 -0
- package/lib/prompt-store.d.ts +42 -0
- package/lib/prompt-store.js +60 -0
- package/lib/rate-limiter.d.ts +30 -0
- package/lib/rate-limiter.js +104 -0
- package/lib/react-engine.d.ts +110 -0
- package/lib/react-engine.js +337 -0
- package/lib/runner/cli.js +156 -0
- package/lib/runner/cost-estimator.js +71 -0
- package/lib/runner/gate.js +46 -0
- package/lib/runner/index.js +165 -0
- package/lib/sidecar.d.ts +83 -0
- package/lib/sidecar.js +161 -0
- package/lib/sse.d.ts +15 -0
- package/lib/sse.js +30 -0
- package/lib/tools-scanner.js +91 -0
- package/lib/tui.js +253 -0
- package/lib/verifier-report.js +78 -0
- package/lib/verifier-runner.js +338 -0
- package/lib/verifier-scanner.js +70 -0
- package/lib/verifier-worker-pool.js +196 -0
- package/lib/views/chat.js +340 -0
- package/lib/views/endpoints.js +203 -0
- package/lib/views/eval-run.js +206 -0
- package/lib/views/forge-agent.js +538 -0
- package/lib/views/forge.js +410 -0
- package/lib/views/main-menu.js +275 -0
- package/lib/views/mediation.js +381 -0
- package/lib/views/model-compare.js +430 -0
- package/lib/views/model-comparison.js +333 -0
- package/lib/views/onboarding.js +470 -0
- package/lib/views/performance.js +237 -0
- package/lib/views/run-evals.js +205 -0
- package/lib/views/settings.js +829 -0
- package/lib/views/tools-evals.js +514 -0
- package/lib/views/verifier-coverage.js +617 -0
- package/lib/workers/verifier-worker.js +52 -0
- package/package.json +123 -0
- package/widget/forge-chat.js +789 -0
package/lib/db.js
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite helper for forge eval history.
|
|
3
|
+
* Uses better-sqlite3 (synchronous API).
|
|
4
|
+
*
|
|
5
|
+
* Schema:
|
|
6
|
+
* eval_runs(id, tool_name, run_at, eval_type, total_cases, passed, failed, notes)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
|
|
12
|
+
const SCHEMA = `
|
|
13
|
+
CREATE TABLE IF NOT EXISTS eval_runs (
|
|
14
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
tool_name TEXT NOT NULL,
|
|
16
|
+
run_at TEXT NOT NULL,
|
|
17
|
+
eval_type TEXT DEFAULT 'unknown',
|
|
18
|
+
total_cases INTEGER DEFAULT 0,
|
|
19
|
+
passed INTEGER DEFAULT 0,
|
|
20
|
+
failed INTEGER DEFAULT 0,
|
|
21
|
+
skipped INTEGER DEFAULT 0,
|
|
22
|
+
notes TEXT
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS tool_registry (
|
|
26
|
+
tool_name TEXT PRIMARY KEY,
|
|
27
|
+
spec_json TEXT,
|
|
28
|
+
lifecycle_state TEXT NOT NULL DEFAULT 'candidate',
|
|
29
|
+
promoted_at TEXT,
|
|
30
|
+
flagged_at TEXT,
|
|
31
|
+
retired_at TEXT,
|
|
32
|
+
version TEXT DEFAULT '1.0.0',
|
|
33
|
+
replaced_by TEXT,
|
|
34
|
+
baseline_pass_rate REAL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS eval_run_cases (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
eval_run_id INTEGER NOT NULL,
|
|
40
|
+
case_id TEXT,
|
|
41
|
+
tool_name TEXT NOT NULL,
|
|
42
|
+
status TEXT NOT NULL,
|
|
43
|
+
reason TEXT,
|
|
44
|
+
tools_called TEXT,
|
|
45
|
+
latency_ms INTEGER,
|
|
46
|
+
model TEXT,
|
|
47
|
+
input_tokens INTEGER,
|
|
48
|
+
output_tokens INTEGER,
|
|
49
|
+
run_at TEXT NOT NULL,
|
|
50
|
+
FOREIGN KEY (eval_run_id) REFERENCES eval_runs(id)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS drift_alerts (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
tool_name TEXT NOT NULL,
|
|
56
|
+
detected_at TEXT NOT NULL,
|
|
57
|
+
trigger_tools TEXT,
|
|
58
|
+
baseline_rate REAL,
|
|
59
|
+
current_rate REAL,
|
|
60
|
+
delta REAL,
|
|
61
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
62
|
+
resolved_at TEXT
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS tool_generations (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
tool_name TEXT NOT NULL,
|
|
68
|
+
started_at TEXT NOT NULL,
|
|
69
|
+
completed_at TEXT,
|
|
70
|
+
generation_model TEXT,
|
|
71
|
+
eval_model TEXT,
|
|
72
|
+
phases_completed INTEGER DEFAULT 0,
|
|
73
|
+
spec_json TEXT,
|
|
74
|
+
generated_files TEXT,
|
|
75
|
+
status TEXT DEFAULT 'in_progress',
|
|
76
|
+
notes TEXT
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS model_comparisons (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
compared_at TEXT NOT NULL,
|
|
82
|
+
tool_name TEXT,
|
|
83
|
+
model_a TEXT NOT NULL,
|
|
84
|
+
model_b TEXT NOT NULL,
|
|
85
|
+
spec_a_json TEXT,
|
|
86
|
+
spec_b_json TEXT,
|
|
87
|
+
chosen_model TEXT,
|
|
88
|
+
phase TEXT
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
session_id TEXT NOT NULL,
|
|
94
|
+
stage TEXT NOT NULL,
|
|
95
|
+
role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
|
|
96
|
+
content TEXT NOT NULL,
|
|
97
|
+
created_at TEXT NOT NULL
|
|
98
|
+
);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_session
|
|
100
|
+
ON conversations(session_id, created_at);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS mcp_call_log (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
tool_name TEXT NOT NULL,
|
|
105
|
+
called_at TEXT NOT NULL,
|
|
106
|
+
input_json TEXT,
|
|
107
|
+
output_json TEXT,
|
|
108
|
+
status_code INTEGER,
|
|
109
|
+
latency_ms INTEGER,
|
|
110
|
+
error TEXT
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS prompt_versions (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
version TEXT NOT NULL,
|
|
116
|
+
content TEXT NOT NULL,
|
|
117
|
+
is_active INTEGER NOT NULL DEFAULT 0,
|
|
118
|
+
created_at TEXT NOT NULL,
|
|
119
|
+
activated_at TEXT,
|
|
120
|
+
notes TEXT
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
124
|
+
user_id TEXT PRIMARY KEY,
|
|
125
|
+
model TEXT,
|
|
126
|
+
hitl_level TEXT CHECK(hitl_level IN ('autonomous','cautious','standard','paranoid')),
|
|
127
|
+
updated_at TEXT NOT NULL
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS verifier_results (
|
|
131
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
132
|
+
session_id TEXT,
|
|
133
|
+
tool_name TEXT NOT NULL,
|
|
134
|
+
verifier_name TEXT NOT NULL,
|
|
135
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('pass','warn','block')),
|
|
136
|
+
message TEXT,
|
|
137
|
+
tool_call_input TEXT,
|
|
138
|
+
tool_call_output TEXT,
|
|
139
|
+
created_at TEXT NOT NULL
|
|
140
|
+
);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_verifier_results_tool
|
|
142
|
+
ON verifier_results(tool_name, created_at);
|
|
143
|
+
|
|
144
|
+
CREATE TABLE IF NOT EXISTS verifier_registry (
|
|
145
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
146
|
+
verifier_name TEXT NOT NULL UNIQUE,
|
|
147
|
+
display_name TEXT,
|
|
148
|
+
type TEXT NOT NULL CHECK(type IN ('schema','pattern','custom')),
|
|
149
|
+
aciru_category TEXT NOT NULL DEFAULT 'U',
|
|
150
|
+
aciru_order TEXT NOT NULL DEFAULT 'U-9999',
|
|
151
|
+
spec_json TEXT NOT NULL,
|
|
152
|
+
description TEXT,
|
|
153
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
154
|
+
role TEXT DEFAULT 'any',
|
|
155
|
+
created_at TEXT NOT NULL,
|
|
156
|
+
updated_at TEXT NOT NULL
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE TABLE IF NOT EXISTS verifier_tool_bindings (
|
|
160
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
161
|
+
verifier_name TEXT NOT NULL,
|
|
162
|
+
tool_name TEXT NOT NULL,
|
|
163
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
164
|
+
created_at TEXT NOT NULL,
|
|
165
|
+
UNIQUE(verifier_name, tool_name),
|
|
166
|
+
FOREIGN KEY (verifier_name) REFERENCES verifier_registry(verifier_name)
|
|
167
|
+
);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_vtb_tool_name
|
|
169
|
+
ON verifier_tool_bindings(tool_name, verifier_name);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS agent_registry (
|
|
172
|
+
agent_id TEXT PRIMARY KEY,
|
|
173
|
+
display_name TEXT NOT NULL,
|
|
174
|
+
description TEXT,
|
|
175
|
+
system_prompt TEXT,
|
|
176
|
+
default_model TEXT,
|
|
177
|
+
default_hitl_level TEXT CHECK(default_hitl_level IN ('autonomous','cautious','standard','paranoid')),
|
|
178
|
+
allow_user_model_select INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
allow_user_hitl_config INTEGER NOT NULL DEFAULT 0,
|
|
180
|
+
tool_allowlist TEXT NOT NULL DEFAULT '*',
|
|
181
|
+
max_turns INTEGER,
|
|
182
|
+
max_tokens INTEGER,
|
|
183
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
184
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
185
|
+
seeded_from_config INTEGER NOT NULL DEFAULT 0,
|
|
186
|
+
created_at TEXT NOT NULL,
|
|
187
|
+
updated_at TEXT NOT NULL
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
CREATE TABLE IF NOT EXISTS chat_audit (
|
|
191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
192
|
+
session_id TEXT NOT NULL,
|
|
193
|
+
user_id TEXT NOT NULL,
|
|
194
|
+
agent_id TEXT,
|
|
195
|
+
route TEXT NOT NULL,
|
|
196
|
+
status_code INTEGER NOT NULL,
|
|
197
|
+
duration_ms INTEGER NOT NULL,
|
|
198
|
+
model TEXT,
|
|
199
|
+
message_text TEXT,
|
|
200
|
+
tool_count INTEGER DEFAULT 0,
|
|
201
|
+
hitl_triggered INTEGER DEFAULT 0,
|
|
202
|
+
warnings_count INTEGER DEFAULT 0,
|
|
203
|
+
error_message TEXT,
|
|
204
|
+
created_at TEXT NOT NULL
|
|
205
|
+
);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_chat_audit_user ON chat_audit(user_id, created_at);
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_chat_audit_session ON chat_audit(session_id);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_chat_audit_status ON chat_audit(status_code, created_at);
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get (or create) a better-sqlite3 Database instance at the given path.
|
|
213
|
+
* @param {string} dbPath - Absolute or relative path to forge.db
|
|
214
|
+
* @returns {import('better-sqlite3').Database}
|
|
215
|
+
*/
|
|
216
|
+
export function getDb(dbPath) {
|
|
217
|
+
const db = new Database(dbPath);
|
|
218
|
+
db.exec(SCHEMA);
|
|
219
|
+
// Migrate: add skipped column if it doesn't exist yet
|
|
220
|
+
try {
|
|
221
|
+
db.exec('ALTER TABLE eval_runs ADD COLUMN skipped INTEGER DEFAULT 0');
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
224
|
+
}
|
|
225
|
+
// Migrate: add model column
|
|
226
|
+
try {
|
|
227
|
+
db.exec('ALTER TABLE eval_runs ADD COLUMN model TEXT');
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
230
|
+
}
|
|
231
|
+
// Migrate: add pass_rate column
|
|
232
|
+
try {
|
|
233
|
+
db.exec('ALTER TABLE eval_runs ADD COLUMN pass_rate REAL');
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
236
|
+
}
|
|
237
|
+
// Migrate: add sample_type column
|
|
238
|
+
try {
|
|
239
|
+
db.exec('ALTER TABLE eval_runs ADD COLUMN sample_type TEXT');
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
242
|
+
}
|
|
243
|
+
// Migrate: add agent_id to conversations
|
|
244
|
+
try {
|
|
245
|
+
db.exec('ALTER TABLE conversations ADD COLUMN agent_id TEXT');
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
248
|
+
}
|
|
249
|
+
// Migrate: add user_id to conversations
|
|
250
|
+
try {
|
|
251
|
+
db.exec('ALTER TABLE conversations ADD COLUMN user_id TEXT');
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
254
|
+
}
|
|
255
|
+
// Add index for user_id lookups
|
|
256
|
+
try {
|
|
257
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id, created_at)');
|
|
258
|
+
} catch { /* may already exist */ }
|
|
259
|
+
// Migrate: add input_tokens to eval_run_cases
|
|
260
|
+
try {
|
|
261
|
+
db.exec('ALTER TABLE eval_run_cases ADD COLUMN input_tokens INTEGER');
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
264
|
+
}
|
|
265
|
+
// Migrate: add output_tokens to eval_run_cases
|
|
266
|
+
try {
|
|
267
|
+
db.exec('ALTER TABLE eval_run_cases ADD COLUMN output_tokens INTEGER');
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
270
|
+
}
|
|
271
|
+
// Migrate: add role column to verifier_registry (Phase 6 — sandbox)
|
|
272
|
+
try {
|
|
273
|
+
db.exec("ALTER TABLE verifier_registry ADD COLUMN role TEXT DEFAULT 'any'");
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (!err.message.includes('duplicate column name')) throw err;
|
|
276
|
+
}
|
|
277
|
+
return db;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Return eval summary grouped by tool_name, including last run time and pass rate.
|
|
282
|
+
* @param {import('better-sqlite3').Database} db
|
|
283
|
+
* @returns {{ tool_name: string; last_run: string; total_cases: number; passed: number; failed: number; pass_rate: string }[]}
|
|
284
|
+
*/
|
|
285
|
+
export function getEvalSummary(db) {
|
|
286
|
+
const rows = db.prepare(`
|
|
287
|
+
SELECT
|
|
288
|
+
tool_name,
|
|
289
|
+
MAX(run_at) AS last_run,
|
|
290
|
+
SUM(total_cases) AS total_cases,
|
|
291
|
+
SUM(passed) AS passed,
|
|
292
|
+
SUM(failed) AS failed
|
|
293
|
+
FROM eval_runs
|
|
294
|
+
GROUP BY tool_name
|
|
295
|
+
ORDER BY last_run DESC
|
|
296
|
+
`).all();
|
|
297
|
+
|
|
298
|
+
return rows.map((r) => ({
|
|
299
|
+
...r,
|
|
300
|
+
pass_rate: r.total_cases > 0
|
|
301
|
+
? `${Math.round((r.passed / r.total_cases) * 100)}%`
|
|
302
|
+
: 'N/A'
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Insert one eval run record.
|
|
308
|
+
* @param {import('better-sqlite3').Database} db
|
|
309
|
+
* @param {{ tool_name: string; eval_type?: string; total_cases?: number; passed?: number; failed?: number; skipped?: number; notes?: string; model?: string; pass_rate?: number; sample_type?: string }} row
|
|
310
|
+
* @returns {number} lastInsertRowid
|
|
311
|
+
*/
|
|
312
|
+
export function insertEvalRun(db, row) {
|
|
313
|
+
const result = db.prepare(`
|
|
314
|
+
INSERT INTO eval_runs (tool_name, run_at, eval_type, total_cases, passed, failed, skipped, notes, model, pass_rate, sample_type)
|
|
315
|
+
VALUES (@tool_name, @run_at, @eval_type, @total_cases, @passed, @failed, @skipped, @notes, @model, @pass_rate, @sample_type)
|
|
316
|
+
`).run({
|
|
317
|
+
tool_name: row.tool_name,
|
|
318
|
+
run_at: row.run_at ?? new Date().toISOString(),
|
|
319
|
+
eval_type: row.eval_type ?? 'unknown',
|
|
320
|
+
total_cases: row.total_cases ?? 0,
|
|
321
|
+
passed: row.passed ?? 0,
|
|
322
|
+
failed: row.failed ?? 0,
|
|
323
|
+
skipped: row.skipped ?? 0,
|
|
324
|
+
notes: row.notes ?? null,
|
|
325
|
+
model: row.model ?? null,
|
|
326
|
+
pass_rate: row.pass_rate ?? null,
|
|
327
|
+
sample_type: row.sample_type ?? null
|
|
328
|
+
});
|
|
329
|
+
return Number(result.lastInsertRowid);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Insert a new tool generation record. Returns the new row id.
|
|
334
|
+
* @param {import('better-sqlite3').Database} db
|
|
335
|
+
* @param {{ tool_name: string; started_at?: string; generation_model?: string; eval_model?: string; spec_json?: string; generated_files?: string; status?: string; notes?: string }} row
|
|
336
|
+
* @returns {number} lastInsertRowid
|
|
337
|
+
*/
|
|
338
|
+
export function insertToolGeneration(db, row) {
|
|
339
|
+
const result = db.prepare(`
|
|
340
|
+
INSERT INTO tool_generations
|
|
341
|
+
(tool_name, started_at, generation_model, eval_model, spec_json, generated_files, status, notes)
|
|
342
|
+
VALUES
|
|
343
|
+
(@tool_name, @started_at, @generation_model, @eval_model, @spec_json, @generated_files, @status, @notes)
|
|
344
|
+
`).run({
|
|
345
|
+
tool_name: row.tool_name,
|
|
346
|
+
started_at: row.started_at ?? new Date().toISOString(),
|
|
347
|
+
generation_model: row.generation_model ?? null,
|
|
348
|
+
eval_model: row.eval_model ?? null,
|
|
349
|
+
spec_json: row.spec_json ?? null,
|
|
350
|
+
generated_files: row.generated_files ?? null,
|
|
351
|
+
status: row.status ?? 'in_progress',
|
|
352
|
+
notes: row.notes ?? null
|
|
353
|
+
});
|
|
354
|
+
return Number(result.lastInsertRowid);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Update fields on an existing tool generation record.
|
|
359
|
+
* @param {import('better-sqlite3').Database} db
|
|
360
|
+
* @param {number} id
|
|
361
|
+
* @param {Partial<{ completed_at: string; phases_completed: number; spec_json: string; generated_files: string; status: string; notes: string; generation_model: string; eval_model: string }>} updates
|
|
362
|
+
*/
|
|
363
|
+
export function updateToolGeneration(db, id, updates) {
|
|
364
|
+
const allowed = [
|
|
365
|
+
'completed_at', 'phases_completed', 'spec_json', 'generated_files',
|
|
366
|
+
'status', 'notes', 'generation_model', 'eval_model'
|
|
367
|
+
];
|
|
368
|
+
const fields = Object.keys(updates).filter((k) => allowed.includes(k));
|
|
369
|
+
if (fields.length === 0) return;
|
|
370
|
+
const setClauses = fields.map((f) => `${f} = @${f}`).join(', ');
|
|
371
|
+
const params = { id };
|
|
372
|
+
for (const f of fields) params[f] = updates[f];
|
|
373
|
+
db.prepare(`UPDATE tool_generations SET ${setClauses} WHERE id = @id`).run(params);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get all tool generation records ordered by started_at DESC.
|
|
378
|
+
* @param {import('better-sqlite3').Database} db
|
|
379
|
+
* @returns {object[]}
|
|
380
|
+
*/
|
|
381
|
+
export function getToolGenerations(db) {
|
|
382
|
+
return db.prepare(`
|
|
383
|
+
SELECT * FROM tool_generations ORDER BY started_at DESC
|
|
384
|
+
`).all();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Insert a model comparison record. Returns the new row id.
|
|
389
|
+
* @param {import('better-sqlite3').Database} db
|
|
390
|
+
* @param {{ compared_at?: string; tool_name?: string; model_a: string; model_b: string; spec_a_json?: string; spec_b_json?: string; chosen_model?: string; phase?: string }} row
|
|
391
|
+
* @returns {number} lastInsertRowid
|
|
392
|
+
*/
|
|
393
|
+
export function insertModelComparison(db, row) {
|
|
394
|
+
const result = db.prepare(`
|
|
395
|
+
INSERT INTO model_comparisons
|
|
396
|
+
(compared_at, tool_name, model_a, model_b, spec_a_json, spec_b_json, chosen_model, phase)
|
|
397
|
+
VALUES
|
|
398
|
+
(@compared_at, @tool_name, @model_a, @model_b, @spec_a_json, @spec_b_json, @chosen_model, @phase)
|
|
399
|
+
`).run({
|
|
400
|
+
compared_at: row.compared_at ?? new Date().toISOString(),
|
|
401
|
+
tool_name: row.tool_name ?? null,
|
|
402
|
+
model_a: row.model_a,
|
|
403
|
+
model_b: row.model_b,
|
|
404
|
+
spec_a_json: row.spec_a_json ?? null,
|
|
405
|
+
spec_b_json: row.spec_b_json ?? null,
|
|
406
|
+
chosen_model: row.chosen_model ?? null,
|
|
407
|
+
phase: row.phase ?? null
|
|
408
|
+
});
|
|
409
|
+
return Number(result.lastInsertRowid);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Get model comparisons for a specific tool, or all rows if toolName is null.
|
|
414
|
+
* @param {import('better-sqlite3').Database} db
|
|
415
|
+
* @param {string | null} toolName
|
|
416
|
+
* @returns {object[]}
|
|
417
|
+
*/
|
|
418
|
+
export function getModelComparisons(db, toolName) {
|
|
419
|
+
if (toolName == null) {
|
|
420
|
+
return db.prepare(`
|
|
421
|
+
SELECT * FROM model_comparisons ORDER BY compared_at DESC
|
|
422
|
+
`).all();
|
|
423
|
+
}
|
|
424
|
+
return db.prepare(`
|
|
425
|
+
SELECT * FROM model_comparisons WHERE tool_name = ? ORDER BY compared_at DESC
|
|
426
|
+
`).all(toolName);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── tool_registry ──────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Upsert a row in tool_registry.
|
|
433
|
+
* @param {import('better-sqlite3').Database} db
|
|
434
|
+
* @param {{ tool_name: string; spec_json?: string; lifecycle_state?: string; promoted_at?: string; flagged_at?: string; retired_at?: string; version?: string; replaced_by?: string; baseline_pass_rate?: number }} row
|
|
435
|
+
*/
|
|
436
|
+
export function upsertToolRegistry(db, row) {
|
|
437
|
+
db.prepare(`
|
|
438
|
+
INSERT INTO tool_registry (tool_name, spec_json, lifecycle_state, promoted_at, flagged_at, retired_at, version, replaced_by, baseline_pass_rate)
|
|
439
|
+
VALUES (@tool_name, @spec_json, @lifecycle_state, @promoted_at, @flagged_at, @retired_at, @version, @replaced_by, @baseline_pass_rate)
|
|
440
|
+
ON CONFLICT(tool_name) DO UPDATE SET
|
|
441
|
+
spec_json = excluded.spec_json,
|
|
442
|
+
lifecycle_state = excluded.lifecycle_state,
|
|
443
|
+
promoted_at = excluded.promoted_at,
|
|
444
|
+
flagged_at = excluded.flagged_at,
|
|
445
|
+
retired_at = excluded.retired_at,
|
|
446
|
+
version = excluded.version,
|
|
447
|
+
replaced_by = excluded.replaced_by,
|
|
448
|
+
baseline_pass_rate = excluded.baseline_pass_rate
|
|
449
|
+
`).run({
|
|
450
|
+
tool_name: row.tool_name,
|
|
451
|
+
spec_json: row.spec_json ?? null,
|
|
452
|
+
lifecycle_state: row.lifecycle_state ?? 'candidate',
|
|
453
|
+
promoted_at: row.promoted_at ?? null,
|
|
454
|
+
flagged_at: row.flagged_at ?? null,
|
|
455
|
+
retired_at: row.retired_at ?? null,
|
|
456
|
+
version: row.version ?? '1.0.0',
|
|
457
|
+
replaced_by: row.replaced_by ?? null,
|
|
458
|
+
baseline_pass_rate: row.baseline_pass_rate ?? null
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get a single tool_registry row by tool name.
|
|
464
|
+
* @param {import('better-sqlite3').Database} db
|
|
465
|
+
* @param {string} toolName
|
|
466
|
+
* @returns {object|null}
|
|
467
|
+
*/
|
|
468
|
+
export function getToolRegistry(db, toolName) {
|
|
469
|
+
return db.prepare(`SELECT * FROM tool_registry WHERE tool_name = ?`).get(toolName) ?? null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get all tool_registry rows.
|
|
474
|
+
* @param {import('better-sqlite3').Database} db
|
|
475
|
+
* @returns {object[]}
|
|
476
|
+
*/
|
|
477
|
+
export function getAllToolRegistry(db) {
|
|
478
|
+
return db.prepare(`SELECT * FROM tool_registry ORDER BY tool_name`).all();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Partially update lifecycle fields on a tool_registry row.
|
|
483
|
+
* @param {import('better-sqlite3').Database} db
|
|
484
|
+
* @param {string} toolName
|
|
485
|
+
* @param {Partial<{ lifecycle_state: string; promoted_at: string; flagged_at: string; retired_at: string; replaced_by: string; baseline_pass_rate: number }>} updates
|
|
486
|
+
*/
|
|
487
|
+
export function updateToolLifecycle(db, toolName, updates) {
|
|
488
|
+
const allowed = ['lifecycle_state', 'promoted_at', 'flagged_at', 'retired_at', 'replaced_by', 'baseline_pass_rate'];
|
|
489
|
+
const fields = Object.keys(updates).filter((k) => allowed.includes(k));
|
|
490
|
+
if (fields.length === 0) return;
|
|
491
|
+
const setClauses = fields.map((f) => `${f} = @${f}`).join(', ');
|
|
492
|
+
const params = { tool_name: toolName };
|
|
493
|
+
for (const f of fields) params[f] = updates[f];
|
|
494
|
+
db.prepare(`UPDATE tool_registry SET ${setClauses} WHERE tool_name = @tool_name`).run(params);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── eval_run_cases ─────────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Batch insert eval run case rows in a transaction.
|
|
501
|
+
* @param {import('better-sqlite3').Database} db
|
|
502
|
+
* @param {Array<{ eval_run_id: number; case_id?: string; tool_name: string; status: string; reason?: string; tools_called?: string; latency_ms?: number; model?: string }>} rows
|
|
503
|
+
*/
|
|
504
|
+
export function insertEvalRunCases(db, rows) {
|
|
505
|
+
const stmt = db.prepare(`
|
|
506
|
+
INSERT INTO eval_run_cases (eval_run_id, case_id, tool_name, status, reason, tools_called, latency_ms, model, input_tokens, output_tokens, run_at)
|
|
507
|
+
VALUES (@eval_run_id, @case_id, @tool_name, @status, @reason, @tools_called, @latency_ms, @model, @input_tokens, @output_tokens, @run_at)
|
|
508
|
+
`);
|
|
509
|
+
const now = new Date().toISOString();
|
|
510
|
+
db.transaction(() => {
|
|
511
|
+
for (const row of rows) {
|
|
512
|
+
stmt.run({
|
|
513
|
+
eval_run_id: row.eval_run_id,
|
|
514
|
+
case_id: row.case_id ?? null,
|
|
515
|
+
tool_name: row.tool_name,
|
|
516
|
+
status: row.status,
|
|
517
|
+
reason: row.reason ?? null,
|
|
518
|
+
tools_called: row.tools_called ?? null,
|
|
519
|
+
latency_ms: row.latency_ms ?? null,
|
|
520
|
+
model: row.model ?? null,
|
|
521
|
+
input_tokens: row.input_tokens ?? null,
|
|
522
|
+
output_tokens: row.output_tokens ?? null,
|
|
523
|
+
run_at: now
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Get eval run cases for a specific tool (or any tool if toolName is null), with LIMIT.
|
|
531
|
+
* @param {import('better-sqlite3').Database} db
|
|
532
|
+
* @param {string|null} toolName - null = any tool
|
|
533
|
+
* @param {number} [limit=50]
|
|
534
|
+
* @returns {object[]}
|
|
535
|
+
*/
|
|
536
|
+
export function getEvalRunCasesByTool(db, toolName, limit = 50) {
|
|
537
|
+
if (toolName == null) {
|
|
538
|
+
return db.prepare(`SELECT * FROM eval_run_cases ORDER BY run_at DESC LIMIT ?`).all(limit);
|
|
539
|
+
}
|
|
540
|
+
return db.prepare(`SELECT * FROM eval_run_cases WHERE tool_name = ? ORDER BY run_at DESC LIMIT ?`).all(toolName, limit);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ── drift_alerts ───────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Insert a drift alert record.
|
|
547
|
+
* @param {import('better-sqlite3').Database} db
|
|
548
|
+
* @param {{ tool_name: string; trigger_tools?: string; baseline_rate?: number; current_rate?: number; delta?: number }} row
|
|
549
|
+
* @returns {number} lastInsertRowid
|
|
550
|
+
*/
|
|
551
|
+
export function insertDriftAlert(db, row) {
|
|
552
|
+
const result = db.prepare(`
|
|
553
|
+
INSERT INTO drift_alerts (tool_name, detected_at, trigger_tools, baseline_rate, current_rate, delta, status)
|
|
554
|
+
VALUES (@tool_name, @detected_at, @trigger_tools, @baseline_rate, @current_rate, @delta, 'open')
|
|
555
|
+
`).run({
|
|
556
|
+
tool_name: row.tool_name,
|
|
557
|
+
detected_at: new Date().toISOString(),
|
|
558
|
+
trigger_tools: row.trigger_tools ?? null,
|
|
559
|
+
baseline_rate: row.baseline_rate ?? null,
|
|
560
|
+
current_rate: row.current_rate ?? null,
|
|
561
|
+
delta: row.delta ?? null
|
|
562
|
+
});
|
|
563
|
+
return Number(result.lastInsertRowid);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Get drift alerts — all open alerts if toolName is null, or filtered by tool.
|
|
568
|
+
* @param {import('better-sqlite3').Database} db
|
|
569
|
+
* @param {string|null} toolName - null = all open alerts
|
|
570
|
+
* @returns {object[]}
|
|
571
|
+
*/
|
|
572
|
+
export function getDriftAlerts(db, toolName) {
|
|
573
|
+
if (toolName == null) {
|
|
574
|
+
return db.prepare(`SELECT * FROM drift_alerts WHERE status = 'open' ORDER BY detected_at DESC`).all();
|
|
575
|
+
}
|
|
576
|
+
return db.prepare(`SELECT * FROM drift_alerts WHERE tool_name = ? AND status = 'open' ORDER BY detected_at DESC`).all(toolName);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Mark a drift alert as resolved.
|
|
581
|
+
* @param {import('better-sqlite3').Database} db
|
|
582
|
+
* @param {number} alertId
|
|
583
|
+
*/
|
|
584
|
+
export function resolveDriftAlert(db, alertId) {
|
|
585
|
+
db.prepare(`UPDATE drift_alerts SET status = 'resolved', resolved_at = ? WHERE id = ?`)
|
|
586
|
+
.run(new Date().toISOString(), alertId);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Mark a drift alert as dismissed (acknowledged, not fixed).
|
|
591
|
+
* @param {import('better-sqlite3').Database} db
|
|
592
|
+
* @param {number} alertId
|
|
593
|
+
*/
|
|
594
|
+
export function dismissDriftAlert(db, alertId) {
|
|
595
|
+
db.prepare(`UPDATE drift_alerts SET status = 'dismissed', resolved_at = ? WHERE id = ?`)
|
|
596
|
+
.run(new Date().toISOString(), alertId);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── performance trending ───────────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get per-tool eval run history for trending (pass_rate over time).
|
|
603
|
+
* @param {import('better-sqlite3').Database} db
|
|
604
|
+
* @param {string} toolName
|
|
605
|
+
* @param {number} [windowSize=10]
|
|
606
|
+
* @returns {object[]} rows with run_at, pass_rate, passed, total_cases
|
|
607
|
+
*/
|
|
608
|
+
export function getPerToolRunHistory(db, toolName, windowSize = 10) {
|
|
609
|
+
return db.prepare(`
|
|
610
|
+
SELECT run_at, pass_rate, passed, total_cases, model
|
|
611
|
+
FROM eval_runs
|
|
612
|
+
WHERE tool_name = ? AND total_cases > 0
|
|
613
|
+
ORDER BY run_at DESC
|
|
614
|
+
LIMIT ?
|
|
615
|
+
`).all(toolName, windowSize);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Get per-model aggregate stats for a tool's eval cases.
|
|
620
|
+
* Used by the model comparison view.
|
|
621
|
+
*
|
|
622
|
+
* @param {import('better-sqlite3').Database} db
|
|
623
|
+
* @param {string} toolName
|
|
624
|
+
* @returns {Array<{
|
|
625
|
+
* model: string,
|
|
626
|
+
* case_count: number,
|
|
627
|
+
* passed: number,
|
|
628
|
+
* avg_latency_ms: number,
|
|
629
|
+
* total_input_tokens: number,
|
|
630
|
+
* total_output_tokens: number
|
|
631
|
+
* }>}
|
|
632
|
+
*/
|
|
633
|
+
export function getModelComparisonData(db, toolName) {
|
|
634
|
+
return db.prepare(`
|
|
635
|
+
SELECT
|
|
636
|
+
model,
|
|
637
|
+
COUNT(*) AS case_count,
|
|
638
|
+
SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
639
|
+
ROUND(AVG(latency_ms)) AS avg_latency_ms,
|
|
640
|
+
SUM(COALESCE(input_tokens, 0)) AS total_input_tokens,
|
|
641
|
+
SUM(COALESCE(output_tokens, 0)) AS total_output_tokens
|
|
642
|
+
FROM eval_run_cases
|
|
643
|
+
WHERE tool_name = ? AND model IS NOT NULL AND status != 'skipped'
|
|
644
|
+
GROUP BY model
|
|
645
|
+
ORDER BY (passed * 1.0 / COUNT(*)) DESC
|
|
646
|
+
`).all(toolName);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── conversations ───────────────────────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
export function createSession() {
|
|
652
|
+
return randomUUID();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function insertConversationMessage(db, { session_id, stage, role, content }) {
|
|
656
|
+
const result = db.prepare(`
|
|
657
|
+
INSERT INTO conversations (session_id, stage, role, content, created_at)
|
|
658
|
+
VALUES (@session_id, @stage, @role, @content, @created_at)
|
|
659
|
+
`).run({
|
|
660
|
+
session_id,
|
|
661
|
+
stage,
|
|
662
|
+
role,
|
|
663
|
+
content,
|
|
664
|
+
created_at: new Date().toISOString()
|
|
665
|
+
});
|
|
666
|
+
return Number(result.lastInsertRowid);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export function getConversationHistory(db, session_id) {
|
|
670
|
+
return db.prepare(`
|
|
671
|
+
SELECT * FROM conversations
|
|
672
|
+
WHERE session_id = ?
|
|
673
|
+
ORDER BY created_at ASC
|
|
674
|
+
`).all(session_id);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function getIncompleteSessions(db) {
|
|
678
|
+
return db.prepare(`
|
|
679
|
+
SELECT
|
|
680
|
+
c.session_id,
|
|
681
|
+
c.stage,
|
|
682
|
+
MAX(c.created_at) AS last_updated
|
|
683
|
+
FROM conversations c
|
|
684
|
+
WHERE c.session_id NOT IN (
|
|
685
|
+
SELECT DISTINCT session_id
|
|
686
|
+
FROM conversations
|
|
687
|
+
WHERE role = 'system' AND content = '[COMPLETE]'
|
|
688
|
+
)
|
|
689
|
+
GROUP BY c.session_id
|
|
690
|
+
ORDER BY last_updated DESC
|
|
691
|
+
`).all();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ── mcp_call_log ───────────────────────────────────────────────────────────
|
|
695
|
+
|
|
696
|
+
export function insertMcpCallLog(db, row) {
|
|
697
|
+
const result = db.prepare(`
|
|
698
|
+
INSERT INTO mcp_call_log (tool_name, called_at, input_json, output_json, status_code, latency_ms, error)
|
|
699
|
+
VALUES (@tool_name, @called_at, @input_json, @output_json, @status_code, @latency_ms, @error)
|
|
700
|
+
`).run({
|
|
701
|
+
tool_name: row.tool_name,
|
|
702
|
+
called_at: new Date().toISOString(),
|
|
703
|
+
input_json: row.input_json ?? null,
|
|
704
|
+
output_json: row.output_json ?? null,
|
|
705
|
+
status_code: row.status_code ?? null,
|
|
706
|
+
latency_ms: row.latency_ms ?? null,
|
|
707
|
+
error: row.error ?? null
|
|
708
|
+
});
|
|
709
|
+
return Number(result.lastInsertRowid);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function getMcpCallLog(db, toolName = null, limit = 50) {
|
|
713
|
+
if (toolName == null) {
|
|
714
|
+
return db.prepare(`SELECT * FROM mcp_call_log ORDER BY id DESC LIMIT ?`).all(limit);
|
|
715
|
+
}
|
|
716
|
+
return db.prepare(`SELECT * FROM mcp_call_log WHERE tool_name = ? ORDER BY id DESC LIMIT ?`).all(toolName, limit);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── prompt_versions ─────────────────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Get the currently active prompt version.
|
|
723
|
+
* @param {import('better-sqlite3').Database} db
|
|
724
|
+
* @returns {object|null}
|
|
725
|
+
*/
|
|
726
|
+
export function getActivePrompt(db) {
|
|
727
|
+
return db.prepare(`SELECT * FROM prompt_versions WHERE is_active = 1`).get() ?? null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Insert a new prompt version (inactive by default).
|
|
732
|
+
* @param {import('better-sqlite3').Database} db
|
|
733
|
+
* @param {{ version: string; content: string; notes?: string }} row
|
|
734
|
+
* @returns {number} lastInsertRowid
|
|
735
|
+
*/
|
|
736
|
+
export function insertPromptVersion(db, row) {
|
|
737
|
+
const result = db.prepare(`
|
|
738
|
+
INSERT INTO prompt_versions (version, content, is_active, created_at, notes)
|
|
739
|
+
VALUES (@version, @content, 0, @created_at, @notes)
|
|
740
|
+
`).run({
|
|
741
|
+
version: row.version,
|
|
742
|
+
content: row.content,
|
|
743
|
+
created_at: new Date().toISOString(),
|
|
744
|
+
notes: row.notes ?? null
|
|
745
|
+
});
|
|
746
|
+
return Number(result.lastInsertRowid);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Activate a prompt version (deactivates all others in a transaction).
|
|
751
|
+
* @param {import('better-sqlite3').Database} db
|
|
752
|
+
* @param {number} id
|
|
753
|
+
*/
|
|
754
|
+
export function activatePromptVersion(db, id) {
|
|
755
|
+
db.transaction(() => {
|
|
756
|
+
db.prepare(`UPDATE prompt_versions SET is_active = 0, activated_at = NULL WHERE is_active = 1`).run();
|
|
757
|
+
db.prepare(`UPDATE prompt_versions SET is_active = 1, activated_at = ? WHERE id = ?`)
|
|
758
|
+
.run(new Date().toISOString(), id);
|
|
759
|
+
})();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Get all prompt versions ordered by created_at DESC.
|
|
764
|
+
* @param {import('better-sqlite3').Database} db
|
|
765
|
+
* @returns {object[]}
|
|
766
|
+
*/
|
|
767
|
+
export function getAllPromptVersions(db) {
|
|
768
|
+
return db.prepare(`SELECT * FROM prompt_versions ORDER BY id DESC`).all();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ── user_preferences ────────────────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Get user preferences by userId.
|
|
775
|
+
* @param {import('better-sqlite3').Database} db
|
|
776
|
+
* @param {string} userId
|
|
777
|
+
* @returns {object|null}
|
|
778
|
+
*/
|
|
779
|
+
export function getUserPreferences(db, userId) {
|
|
780
|
+
return db.prepare(`SELECT * FROM user_preferences WHERE user_id = ?`).get(userId) ?? null;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Upsert user preferences.
|
|
785
|
+
* @param {import('better-sqlite3').Database} db
|
|
786
|
+
* @param {string} userId
|
|
787
|
+
* @param {{ model?: string; hitlLevel?: string }} prefs
|
|
788
|
+
*/
|
|
789
|
+
export function upsertUserPreferences(db, userId, prefs) {
|
|
790
|
+
db.prepare(`
|
|
791
|
+
INSERT INTO user_preferences (user_id, model, hitl_level, updated_at)
|
|
792
|
+
VALUES (@user_id, @model, @hitl_level, @updated_at)
|
|
793
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
794
|
+
model = @model,
|
|
795
|
+
hitl_level = @hitl_level,
|
|
796
|
+
updated_at = @updated_at
|
|
797
|
+
`).run({
|
|
798
|
+
user_id: userId,
|
|
799
|
+
model: prefs.model ?? null,
|
|
800
|
+
hitl_level: prefs.hitlLevel ?? null,
|
|
801
|
+
updated_at: new Date().toISOString()
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ── verifier_results ────────────────────────────────────────────────────────
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Insert a verifier result record.
|
|
809
|
+
* @param {import('better-sqlite3').Database} db
|
|
810
|
+
* @param {{ session_id?: string; tool_name: string; verifier_name: string; outcome: string; message?: string; tool_call_input?: string; tool_call_output?: string }} row
|
|
811
|
+
* @returns {number} lastInsertRowid
|
|
812
|
+
*/
|
|
813
|
+
export function insertVerifierResult(db, row) {
|
|
814
|
+
const result = db.prepare(`
|
|
815
|
+
INSERT INTO verifier_results (session_id, tool_name, verifier_name, outcome, message, tool_call_input, tool_call_output, created_at)
|
|
816
|
+
VALUES (@session_id, @tool_name, @verifier_name, @outcome, @message, @tool_call_input, @tool_call_output, @created_at)
|
|
817
|
+
`).run({
|
|
818
|
+
session_id: row.session_id ?? null,
|
|
819
|
+
tool_name: row.tool_name,
|
|
820
|
+
verifier_name: row.verifier_name,
|
|
821
|
+
outcome: row.outcome,
|
|
822
|
+
message: row.message ?? null,
|
|
823
|
+
tool_call_input: row.tool_call_input ?? null,
|
|
824
|
+
tool_call_output: row.tool_call_output ?? null,
|
|
825
|
+
created_at: new Date().toISOString()
|
|
826
|
+
});
|
|
827
|
+
return Number(result.lastInsertRowid);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Get verifier results for a tool, ordered by most recent.
|
|
832
|
+
* @param {import('better-sqlite3').Database} db
|
|
833
|
+
* @param {string} toolName
|
|
834
|
+
* @param {number} [limit=50]
|
|
835
|
+
* @returns {object[]}
|
|
836
|
+
*/
|
|
837
|
+
export function getVerifierResultsByTool(db, toolName, limit = 50) {
|
|
838
|
+
return db.prepare(`
|
|
839
|
+
SELECT * FROM verifier_results
|
|
840
|
+
WHERE tool_name = ?
|
|
841
|
+
ORDER BY created_at DESC
|
|
842
|
+
LIMIT ?
|
|
843
|
+
`).all(toolName, limit);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ── verifier_registry ───────────────────────────────────────────────────────
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Upsert a verifier into the registry. Sets enabled=1 on insert/update.
|
|
850
|
+
* @param {import('better-sqlite3').Database} db
|
|
851
|
+
* @param {{ verifier_name: string; display_name?: string; type: string; aciru_category?: string; aciru_order?: string; spec_json: string; description?: string }} row
|
|
852
|
+
*/
|
|
853
|
+
export function upsertVerifier(db, row) {
|
|
854
|
+
const now = new Date().toISOString();
|
|
855
|
+
db.prepare(`
|
|
856
|
+
INSERT INTO verifier_registry (verifier_name, display_name, type, aciru_category, aciru_order, spec_json, description, enabled, created_at, updated_at)
|
|
857
|
+
VALUES (@verifier_name, @display_name, @type, @aciru_category, @aciru_order, @spec_json, @description, 1, @now, @now)
|
|
858
|
+
ON CONFLICT(verifier_name) DO UPDATE SET
|
|
859
|
+
display_name = excluded.display_name,
|
|
860
|
+
type = excluded.type,
|
|
861
|
+
aciru_category = excluded.aciru_category,
|
|
862
|
+
aciru_order = excluded.aciru_order,
|
|
863
|
+
spec_json = excluded.spec_json,
|
|
864
|
+
description = excluded.description,
|
|
865
|
+
enabled = 1,
|
|
866
|
+
updated_at = excluded.updated_at
|
|
867
|
+
`).run({
|
|
868
|
+
verifier_name: row.verifier_name,
|
|
869
|
+
display_name: row.display_name ?? null,
|
|
870
|
+
type: row.type,
|
|
871
|
+
aciru_category: row.aciru_category ?? 'U',
|
|
872
|
+
aciru_order: row.aciru_order ?? 'U-9999',
|
|
873
|
+
spec_json: row.spec_json,
|
|
874
|
+
description: row.description ?? null,
|
|
875
|
+
now
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Get a single verifier by name.
|
|
881
|
+
* @param {import('better-sqlite3').Database} db
|
|
882
|
+
* @param {string} verifierName
|
|
883
|
+
* @returns {object|null}
|
|
884
|
+
*/
|
|
885
|
+
export function getVerifier(db, verifierName) {
|
|
886
|
+
return db.prepare('SELECT * FROM verifier_registry WHERE verifier_name = ?').get(verifierName) ?? null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Get all verifiers ordered by aciru_order.
|
|
891
|
+
* @param {import('better-sqlite3').Database} db
|
|
892
|
+
* @returns {object[]}
|
|
893
|
+
*/
|
|
894
|
+
export function getAllVerifiers(db) {
|
|
895
|
+
return db.prepare('SELECT * FROM verifier_registry ORDER BY aciru_order ASC').all();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Delete a verifier and its bindings in a transaction.
|
|
900
|
+
* @param {import('better-sqlite3').Database} db
|
|
901
|
+
* @param {string} verifierName
|
|
902
|
+
*/
|
|
903
|
+
export function deleteVerifier(db, verifierName) {
|
|
904
|
+
db.transaction(() => {
|
|
905
|
+
db.prepare('DELETE FROM verifier_tool_bindings WHERE verifier_name = ?').run(verifierName);
|
|
906
|
+
db.prepare('DELETE FROM verifier_registry WHERE verifier_name = ?').run(verifierName);
|
|
907
|
+
})();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Bind a verifier to a tool. Use tool_name='*' for wildcard.
|
|
912
|
+
* @param {import('better-sqlite3').Database} db
|
|
913
|
+
* @param {{ verifier_name: string; tool_name: string }} binding
|
|
914
|
+
*/
|
|
915
|
+
export function upsertVerifierBinding(db, binding) {
|
|
916
|
+
db.prepare(`
|
|
917
|
+
INSERT OR IGNORE INTO verifier_tool_bindings (verifier_name, tool_name, enabled, created_at)
|
|
918
|
+
VALUES (@verifier_name, @tool_name, 1, @created_at)
|
|
919
|
+
`).run({
|
|
920
|
+
verifier_name: binding.verifier_name,
|
|
921
|
+
tool_name: binding.tool_name,
|
|
922
|
+
created_at: new Date().toISOString()
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Remove a verifier-tool binding.
|
|
928
|
+
* @param {import('better-sqlite3').Database} db
|
|
929
|
+
* @param {string} verifierName
|
|
930
|
+
* @param {string} toolName
|
|
931
|
+
*/
|
|
932
|
+
export function removeVerifierBinding(db, verifierName, toolName) {
|
|
933
|
+
db.prepare('DELETE FROM verifier_tool_bindings WHERE verifier_name = ? AND tool_name = ?')
|
|
934
|
+
.run(verifierName, toolName);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Get all enabled verifiers bound to a tool (includes wildcard '*' bindings).
|
|
939
|
+
* Hot-path query — called per sidecar request.
|
|
940
|
+
* @param {import('better-sqlite3').Database} db
|
|
941
|
+
* @param {string} toolName
|
|
942
|
+
* @returns {object[]}
|
|
943
|
+
*/
|
|
944
|
+
export function getVerifiersForTool(db, toolName) {
|
|
945
|
+
return db.prepare(`
|
|
946
|
+
SELECT vr.* FROM verifier_registry vr
|
|
947
|
+
INNER JOIN verifier_tool_bindings vtb ON vr.verifier_name = vtb.verifier_name
|
|
948
|
+
WHERE (vtb.tool_name = ? OR vtb.tool_name = '*')
|
|
949
|
+
AND vtb.enabled = 1 AND vr.enabled = 1
|
|
950
|
+
ORDER BY vr.aciru_order ASC
|
|
951
|
+
`).all(toolName);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Get all tool bindings for a verifier.
|
|
956
|
+
* @param {import('better-sqlite3').Database} db
|
|
957
|
+
* @param {string} verifierName
|
|
958
|
+
* @returns {object[]}
|
|
959
|
+
*/
|
|
960
|
+
export function getBindingsForVerifier(db, verifierName) {
|
|
961
|
+
return db.prepare('SELECT * FROM verifier_tool_bindings WHERE verifier_name = ?').all(verifierName);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ── agent_registry ──────────────────────────────────────────────────────────
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Upsert an agent into the registry.
|
|
968
|
+
* @param {import('better-sqlite3').Database} db
|
|
969
|
+
* @param {object} row
|
|
970
|
+
*/
|
|
971
|
+
export function upsertAgent(db, row) {
|
|
972
|
+
const now = new Date().toISOString();
|
|
973
|
+
db.prepare(`
|
|
974
|
+
INSERT INTO agent_registry (agent_id, display_name, description, system_prompt, default_model,
|
|
975
|
+
default_hitl_level, allow_user_model_select, allow_user_hitl_config, tool_allowlist,
|
|
976
|
+
max_turns, max_tokens, is_default, enabled, seeded_from_config, created_at, updated_at)
|
|
977
|
+
VALUES (@agent_id, @display_name, @description, @system_prompt, @default_model,
|
|
978
|
+
@default_hitl_level, @allow_user_model_select, @allow_user_hitl_config, @tool_allowlist,
|
|
979
|
+
@max_turns, @max_tokens, @is_default, @enabled, @seeded_from_config, @now, @now)
|
|
980
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
981
|
+
display_name = excluded.display_name,
|
|
982
|
+
description = excluded.description,
|
|
983
|
+
system_prompt = excluded.system_prompt,
|
|
984
|
+
default_model = excluded.default_model,
|
|
985
|
+
default_hitl_level = excluded.default_hitl_level,
|
|
986
|
+
allow_user_model_select = excluded.allow_user_model_select,
|
|
987
|
+
allow_user_hitl_config = excluded.allow_user_hitl_config,
|
|
988
|
+
tool_allowlist = excluded.tool_allowlist,
|
|
989
|
+
max_turns = excluded.max_turns,
|
|
990
|
+
max_tokens = excluded.max_tokens,
|
|
991
|
+
is_default = excluded.is_default,
|
|
992
|
+
enabled = excluded.enabled,
|
|
993
|
+
seeded_from_config = excluded.seeded_from_config,
|
|
994
|
+
updated_at = excluded.updated_at
|
|
995
|
+
`).run({
|
|
996
|
+
agent_id: row.agent_id,
|
|
997
|
+
display_name: row.display_name,
|
|
998
|
+
description: row.description ?? null,
|
|
999
|
+
system_prompt: row.system_prompt ?? null,
|
|
1000
|
+
default_model: row.default_model ?? null,
|
|
1001
|
+
default_hitl_level: row.default_hitl_level ?? null,
|
|
1002
|
+
allow_user_model_select: row.allow_user_model_select ?? 0,
|
|
1003
|
+
allow_user_hitl_config: row.allow_user_hitl_config ?? 0,
|
|
1004
|
+
tool_allowlist: row.tool_allowlist ?? '*',
|
|
1005
|
+
max_turns: row.max_turns ?? null,
|
|
1006
|
+
max_tokens: row.max_tokens ?? null,
|
|
1007
|
+
is_default: row.is_default ?? 0,
|
|
1008
|
+
enabled: row.enabled ?? 1,
|
|
1009
|
+
seeded_from_config: row.seeded_from_config ?? 0,
|
|
1010
|
+
now
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Get a single agent by ID.
|
|
1016
|
+
* @param {import('better-sqlite3').Database} db
|
|
1017
|
+
* @param {string} agentId
|
|
1018
|
+
* @returns {object|null}
|
|
1019
|
+
*/
|
|
1020
|
+
export function getAgent(db, agentId) {
|
|
1021
|
+
return db.prepare('SELECT * FROM agent_registry WHERE agent_id = ?').get(agentId) ?? null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Get all agents ordered by display_name.
|
|
1026
|
+
* @param {import('better-sqlite3').Database} db
|
|
1027
|
+
* @returns {object[]}
|
|
1028
|
+
*/
|
|
1029
|
+
export function getAllAgents(db) {
|
|
1030
|
+
return db.prepare('SELECT * FROM agent_registry ORDER BY display_name').all();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Get the default agent (is_default = 1 and enabled = 1).
|
|
1035
|
+
* @param {import('better-sqlite3').Database} db
|
|
1036
|
+
* @returns {object|null}
|
|
1037
|
+
*/
|
|
1038
|
+
export function getDefaultAgent(db) {
|
|
1039
|
+
return db.prepare('SELECT * FROM agent_registry WHERE is_default = 1 AND enabled = 1').get() ?? null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Set a single agent as default (clears others in a transaction).
|
|
1044
|
+
* @param {import('better-sqlite3').Database} db
|
|
1045
|
+
* @param {string} agentId
|
|
1046
|
+
*/
|
|
1047
|
+
export function setDefaultAgent(db, agentId) {
|
|
1048
|
+
db.transaction(() => {
|
|
1049
|
+
// Verify agent exists and is enabled before clearing defaults
|
|
1050
|
+
const exists = db.prepare('SELECT 1 FROM agent_registry WHERE agent_id = ? AND enabled = 1').get(agentId);
|
|
1051
|
+
if (!exists) return;
|
|
1052
|
+
db.prepare('UPDATE agent_registry SET is_default = 0 WHERE is_default = 1').run();
|
|
1053
|
+
db.prepare('UPDATE agent_registry SET is_default = 1, updated_at = ? WHERE agent_id = ?')
|
|
1054
|
+
.run(new Date().toISOString(), agentId);
|
|
1055
|
+
})();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Delete an agent by ID.
|
|
1060
|
+
* @param {import('better-sqlite3').Database} db
|
|
1061
|
+
* @param {string} agentId
|
|
1062
|
+
*/
|
|
1063
|
+
export function deleteAgent(db, agentId) {
|
|
1064
|
+
db.prepare('DELETE FROM agent_registry WHERE agent_id = ?').run(agentId);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ── chat_audit ───────────────────────────────────────────────────────────────
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Insert a chat audit record.
|
|
1071
|
+
* @param {import('better-sqlite3').Database} db
|
|
1072
|
+
* @param {{
|
|
1073
|
+
* session_id: string,
|
|
1074
|
+
* user_id: string,
|
|
1075
|
+
* agent_id?: string,
|
|
1076
|
+
* route: string,
|
|
1077
|
+
* status_code: number,
|
|
1078
|
+
* duration_ms: number,
|
|
1079
|
+
* model?: string,
|
|
1080
|
+
* message_text?: string,
|
|
1081
|
+
* tool_count?: number,
|
|
1082
|
+
* hitl_triggered?: number,
|
|
1083
|
+
* warnings_count?: number,
|
|
1084
|
+
* error_message?: string
|
|
1085
|
+
* }} row
|
|
1086
|
+
* @returns {number} lastInsertRowid
|
|
1087
|
+
*/
|
|
1088
|
+
export function insertChatAudit(db, row) {
|
|
1089
|
+
const result = db.prepare(`
|
|
1090
|
+
INSERT INTO chat_audit
|
|
1091
|
+
(session_id, user_id, agent_id, route, status_code, duration_ms,
|
|
1092
|
+
model, message_text, tool_count, hitl_triggered, warnings_count, error_message, created_at)
|
|
1093
|
+
VALUES
|
|
1094
|
+
(@session_id, @user_id, @agent_id, @route, @status_code, @duration_ms,
|
|
1095
|
+
@model, @message_text, @tool_count, @hitl_triggered, @warnings_count, @error_message, @created_at)
|
|
1096
|
+
`).run({
|
|
1097
|
+
session_id: row.session_id ?? '',
|
|
1098
|
+
user_id: row.user_id ?? 'anon',
|
|
1099
|
+
agent_id: row.agent_id ?? null,
|
|
1100
|
+
route: row.route,
|
|
1101
|
+
status_code: row.status_code,
|
|
1102
|
+
duration_ms: row.duration_ms,
|
|
1103
|
+
model: row.model ?? null,
|
|
1104
|
+
message_text: row.message_text ?? null,
|
|
1105
|
+
tool_count: row.tool_count ?? 0,
|
|
1106
|
+
hitl_triggered: row.hitl_triggered ?? 0,
|
|
1107
|
+
warnings_count: row.warnings_count ?? 0,
|
|
1108
|
+
error_message: row.error_message ?? null,
|
|
1109
|
+
created_at: new Date().toISOString()
|
|
1110
|
+
});
|
|
1111
|
+
return Number(result.lastInsertRowid);
|
|
1112
|
+
}
|