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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgresStore — Postgres-backed storage adapter for horizontal scaling.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the SQLite query function signatures from db.js but uses the `pg` Pool.
|
|
5
|
+
* Optional — only loaded when conversation.store === 'postgres' in config.
|
|
6
|
+
* Requires: npm install pg
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SCHEMA = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS agent_registry (
|
|
11
|
+
agent_id TEXT PRIMARY KEY,
|
|
12
|
+
display_name TEXT NOT NULL,
|
|
13
|
+
description TEXT,
|
|
14
|
+
system_prompt TEXT,
|
|
15
|
+
default_model TEXT,
|
|
16
|
+
default_hitl_level TEXT CHECK(default_hitl_level IN ('autonomous','cautious','standard','paranoid')),
|
|
17
|
+
allow_user_model_select INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
allow_user_hitl_config INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
tool_allowlist TEXT NOT NULL DEFAULT '*',
|
|
20
|
+
max_turns INTEGER,
|
|
21
|
+
max_tokens INTEGER,
|
|
22
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
24
|
+
seeded_from_config INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
26
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS prompt_versions (
|
|
30
|
+
id SERIAL PRIMARY KEY,
|
|
31
|
+
version TEXT NOT NULL,
|
|
32
|
+
content TEXT NOT NULL,
|
|
33
|
+
is_active INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
created_at TEXT NOT NULL,
|
|
35
|
+
activated_at TEXT,
|
|
36
|
+
notes TEXT
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
40
|
+
user_id TEXT PRIMARY KEY,
|
|
41
|
+
model TEXT,
|
|
42
|
+
hitl_level TEXT CHECK(hitl_level IN ('autonomous','cautious','standard','paranoid')),
|
|
43
|
+
updated_at TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS tool_registry (
|
|
47
|
+
tool_name TEXT PRIMARY KEY,
|
|
48
|
+
spec_json TEXT,
|
|
49
|
+
lifecycle_state TEXT NOT NULL DEFAULT 'candidate',
|
|
50
|
+
promoted_at TEXT,
|
|
51
|
+
flagged_at TEXT,
|
|
52
|
+
retired_at TEXT,
|
|
53
|
+
version TEXT DEFAULT '1.0.0',
|
|
54
|
+
replaced_by TEXT,
|
|
55
|
+
baseline_pass_rate REAL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS verifier_results (
|
|
59
|
+
id SERIAL PRIMARY KEY,
|
|
60
|
+
session_id TEXT,
|
|
61
|
+
tool_name TEXT NOT NULL,
|
|
62
|
+
verifier_name TEXT NOT NULL,
|
|
63
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('pass','warn','block')),
|
|
64
|
+
message TEXT,
|
|
65
|
+
tool_call_input TEXT,
|
|
66
|
+
tool_call_output TEXT,
|
|
67
|
+
created_at TEXT NOT NULL
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_verifier_results_tool
|
|
71
|
+
ON verifier_results(tool_name, created_at);
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
export class PostgresStore {
|
|
75
|
+
/**
|
|
76
|
+
* @param {{ connectionString: string }} pgConfig
|
|
77
|
+
*/
|
|
78
|
+
constructor(pgConfig) {
|
|
79
|
+
this._pgConfig = pgConfig;
|
|
80
|
+
this._pool = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async connect() {
|
|
84
|
+
let pg;
|
|
85
|
+
try {
|
|
86
|
+
pg = await import('pg');
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error('PostgresStore requires the "pg" package: run `npm install pg`');
|
|
89
|
+
}
|
|
90
|
+
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
91
|
+
this._pool = new Pool({ connectionString: this._pgConfig.connectionString });
|
|
92
|
+
await this._pool.query(SCHEMA);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async close() {
|
|
97
|
+
if (this._pool) {
|
|
98
|
+
await this._pool.end();
|
|
99
|
+
this._pool = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Prompt versions ───────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
async getActivePrompt() {
|
|
106
|
+
const { rows } = await this._pool.query(
|
|
107
|
+
'SELECT * FROM prompt_versions WHERE is_active = 1 LIMIT 1'
|
|
108
|
+
);
|
|
109
|
+
return rows[0] ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async insertPromptVersion(row) {
|
|
113
|
+
const { rows } = await this._pool.query(
|
|
114
|
+
`INSERT INTO prompt_versions (version, content, is_active, created_at, notes)
|
|
115
|
+
VALUES ($1, $2, 0, $3, $4) RETURNING id`,
|
|
116
|
+
[row.version, row.content, new Date().toISOString(), row.notes ?? null]
|
|
117
|
+
);
|
|
118
|
+
return rows[0]?.id ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async activatePromptVersion(id) {
|
|
122
|
+
const client = await this._pool.connect();
|
|
123
|
+
try {
|
|
124
|
+
await client.query('BEGIN');
|
|
125
|
+
await client.query('UPDATE prompt_versions SET is_active = 0, activated_at = NULL WHERE is_active = 1');
|
|
126
|
+
await client.query('UPDATE prompt_versions SET is_active = 1, activated_at = $1 WHERE id = $2',
|
|
127
|
+
[new Date().toISOString(), id]);
|
|
128
|
+
await client.query('COMMIT');
|
|
129
|
+
} catch (err) {
|
|
130
|
+
await client.query('ROLLBACK');
|
|
131
|
+
throw err;
|
|
132
|
+
} finally {
|
|
133
|
+
client.release();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── User preferences ──────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
async getUserPreferences(userId) {
|
|
140
|
+
const { rows } = await this._pool.query(
|
|
141
|
+
'SELECT * FROM user_preferences WHERE user_id = $1', [userId]
|
|
142
|
+
);
|
|
143
|
+
return rows[0] ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async upsertUserPreferences(userId, prefs) {
|
|
147
|
+
await this._pool.query(
|
|
148
|
+
`INSERT INTO user_preferences (user_id, model, hitl_level, updated_at)
|
|
149
|
+
VALUES ($1, $2, $3, $4)
|
|
150
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
|
151
|
+
model = $2, hitl_level = $3, updated_at = $4`,
|
|
152
|
+
[userId, prefs.model ?? null, prefs.hitlLevel ?? null, new Date().toISOString()]
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Tool registry (read-only from sidecar) ────────────────────────────
|
|
157
|
+
|
|
158
|
+
async getPromotedTools() {
|
|
159
|
+
const { rows } = await this._pool.query(
|
|
160
|
+
"SELECT * FROM tool_registry WHERE lifecycle_state = 'promoted' ORDER BY tool_name"
|
|
161
|
+
);
|
|
162
|
+
return rows;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Verifier results ──────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
async insertVerifierResult(row) {
|
|
168
|
+
const { rows } = await this._pool.query(
|
|
169
|
+
`INSERT INTO verifier_results
|
|
170
|
+
(session_id, tool_name, verifier_name, outcome, message, tool_call_input, tool_call_output, created_at)
|
|
171
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
|
172
|
+
[
|
|
173
|
+
row.session_id ?? null, row.tool_name, row.verifier_name, row.outcome,
|
|
174
|
+
row.message ?? null, row.tool_call_input ?? null, row.tool_call_output ?? null,
|
|
175
|
+
new Date().toISOString()
|
|
176
|
+
]
|
|
177
|
+
);
|
|
178
|
+
return rows[0]?.id ?? null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── PostgresPromptStore ────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Postgres-backed PromptStore — same interface as PromptStore in prompt-store.js.
|
|
186
|
+
* Uses an existing pg.Pool instance (created by buildSidecarContext).
|
|
187
|
+
*/
|
|
188
|
+
export class PostgresPromptStore {
|
|
189
|
+
/** @param {import('pg').Pool} pool */
|
|
190
|
+
constructor(pool) {
|
|
191
|
+
this._pool = pool;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getActivePrompt() {
|
|
195
|
+
const { rows } = await this._pool.query(
|
|
196
|
+
'SELECT * FROM prompt_versions WHERE is_active = 1 LIMIT 1'
|
|
197
|
+
);
|
|
198
|
+
const row = rows[0] ?? null;
|
|
199
|
+
return row ? row.content : '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getAllVersions() {
|
|
203
|
+
const { rows } = await this._pool.query(
|
|
204
|
+
'SELECT * FROM prompt_versions ORDER BY id DESC'
|
|
205
|
+
);
|
|
206
|
+
return rows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async createVersion(version, content, notes = null) {
|
|
210
|
+
const { rows } = await this._pool.query(
|
|
211
|
+
`INSERT INTO prompt_versions (version, content, is_active, created_at, notes)
|
|
212
|
+
VALUES ($1, $2, 0, $3, $4) RETURNING id`,
|
|
213
|
+
[version, content, new Date().toISOString(), notes]
|
|
214
|
+
);
|
|
215
|
+
return rows[0]?.id ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async activate(id) {
|
|
219
|
+
const client = await this._pool.connect();
|
|
220
|
+
try {
|
|
221
|
+
await client.query('BEGIN');
|
|
222
|
+
await client.query('UPDATE prompt_versions SET is_active = 0, activated_at = NULL WHERE is_active = 1');
|
|
223
|
+
await client.query('UPDATE prompt_versions SET is_active = 1, activated_at = $1 WHERE id = $2',
|
|
224
|
+
[new Date().toISOString(), id]);
|
|
225
|
+
await client.query('COMMIT');
|
|
226
|
+
} catch (err) {
|
|
227
|
+
await client.query('ROLLBACK');
|
|
228
|
+
throw err;
|
|
229
|
+
} finally {
|
|
230
|
+
client.release();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getVersion(id) {
|
|
235
|
+
const { rows } = await this._pool.query(
|
|
236
|
+
'SELECT * FROM prompt_versions WHERE id = $1', [id]
|
|
237
|
+
);
|
|
238
|
+
return rows[0] ?? null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── PostgresPreferenceStore ────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
const VALID_HITL_LEVELS_PG = ['autonomous', 'cautious', 'standard', 'paranoid'];
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Postgres-backed PreferenceStore — same interface as PreferenceStore in preference-store.js.
|
|
248
|
+
*/
|
|
249
|
+
export class PostgresPreferenceStore {
|
|
250
|
+
/**
|
|
251
|
+
* @param {import('pg').Pool} pool
|
|
252
|
+
* @param {object} config — forge config (for detectProvider / resolveApiKey)
|
|
253
|
+
* @param {object} [env] — environment variables
|
|
254
|
+
*/
|
|
255
|
+
constructor(pool, config = {}, env = {}) {
|
|
256
|
+
this._pool = pool;
|
|
257
|
+
this._config = config;
|
|
258
|
+
this._env = env;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getUserPreferences(userId) {
|
|
262
|
+
const { rows } = await this._pool.query(
|
|
263
|
+
'SELECT * FROM user_preferences WHERE user_id = $1', [userId]
|
|
264
|
+
);
|
|
265
|
+
const row = rows[0] ?? null;
|
|
266
|
+
if (!row) return null;
|
|
267
|
+
return { model: row.model, hitlLevel: row.hitl_level };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async setUserPreferences(userId, prefs) {
|
|
271
|
+
if (prefs.hitlLevel && !VALID_HITL_LEVELS_PG.includes(prefs.hitlLevel)) {
|
|
272
|
+
throw new Error(`Invalid hitlLevel: ${prefs.hitlLevel}. Must be one of: ${VALID_HITL_LEVELS_PG.join(', ')}`);
|
|
273
|
+
}
|
|
274
|
+
await this._pool.query(
|
|
275
|
+
`INSERT INTO user_preferences (user_id, model, hitl_level, updated_at)
|
|
276
|
+
VALUES ($1, $2, $3, $4)
|
|
277
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
|
278
|
+
model = $2, hitl_level = $3, updated_at = $4`,
|
|
279
|
+
[userId, prefs.model ?? null, prefs.hitlLevel ?? null, new Date().toISOString()]
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async resolveEffective(userId, config, env = {}) {
|
|
284
|
+
const { detectProvider, resolveApiKey } = await import('./api-client.js');
|
|
285
|
+
const userPrefs = await this.getUserPreferences(userId);
|
|
286
|
+
const model = (config.allowUserModelSelect && userPrefs?.model)
|
|
287
|
+
? userPrefs.model
|
|
288
|
+
: (config.defaultModel ?? 'claude-sonnet-4-6');
|
|
289
|
+
const hitlLevel = (config.allowUserHitlConfig && userPrefs?.hitlLevel)
|
|
290
|
+
? userPrefs.hitlLevel
|
|
291
|
+
: (config.defaultHitlLevel ?? 'cautious');
|
|
292
|
+
const provider = detectProvider(model);
|
|
293
|
+
const apiKey = resolveApiKey(provider, env);
|
|
294
|
+
return { model, hitlLevel, provider, apiKey };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── PostgresAgentRegistry ──────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Postgres-backed AgentRegistry — same interface as AgentRegistry in agent-registry.js.
|
|
302
|
+
*/
|
|
303
|
+
export class PostgresAgentRegistry {
|
|
304
|
+
/**
|
|
305
|
+
* @param {object} config — merged forge config
|
|
306
|
+
* @param {import('pg').Pool} pool
|
|
307
|
+
*/
|
|
308
|
+
constructor(config, pool) {
|
|
309
|
+
this._config = config;
|
|
310
|
+
this._pool = pool;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async resolveAgent(agentId) {
|
|
314
|
+
if (!agentId) {
|
|
315
|
+
const { rows } = await this._pool.query(
|
|
316
|
+
'SELECT * FROM agent_registry WHERE is_default = 1 AND enabled = 1 LIMIT 1'
|
|
317
|
+
);
|
|
318
|
+
return rows[0] ?? null;
|
|
319
|
+
}
|
|
320
|
+
const { rows } = await this._pool.query(
|
|
321
|
+
'SELECT * FROM agent_registry WHERE agent_id = $1', [agentId]
|
|
322
|
+
);
|
|
323
|
+
const agent = rows[0] ?? null;
|
|
324
|
+
if (!agent || !agent.enabled) return null;
|
|
325
|
+
return agent;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
filterTools(loaded, agent) {
|
|
329
|
+
if (!agent) return loaded;
|
|
330
|
+
const allowlist = agent.tool_allowlist;
|
|
331
|
+
if (!allowlist || allowlist === '*') return loaded;
|
|
332
|
+
let allowed;
|
|
333
|
+
try { allowed = JSON.parse(allowlist); } catch { return { toolRows: [], tools: [] }; }
|
|
334
|
+
if (!Array.isArray(allowed)) return { toolRows: [], tools: [] };
|
|
335
|
+
const allowSet = new Set(allowed);
|
|
336
|
+
return {
|
|
337
|
+
toolRows: loaded.toolRows.filter(r => allowSet.has(r.tool_name)),
|
|
338
|
+
tools: loaded.tools.filter(t => allowSet.has(t.name))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
buildAgentConfig(baseConfig, agent) {
|
|
343
|
+
if (!agent) return baseConfig;
|
|
344
|
+
const scoped = { ...baseConfig };
|
|
345
|
+
if (agent.default_model) scoped.defaultModel = agent.default_model;
|
|
346
|
+
if (agent.default_hitl_level) scoped.defaultHitlLevel = agent.default_hitl_level;
|
|
347
|
+
if (agent.allow_user_model_select) scoped.allowUserModelSelect = true;
|
|
348
|
+
if (agent.allow_user_hitl_config) scoped.allowUserHitlConfig = true;
|
|
349
|
+
if (agent.max_turns != null) scoped.maxTurns = agent.max_turns;
|
|
350
|
+
if (agent.max_tokens != null) scoped.maxTokens = agent.max_tokens;
|
|
351
|
+
return scoped;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async resolveSystemPrompt(agent, promptStore, config) {
|
|
355
|
+
if (agent?.system_prompt) return agent.system_prompt;
|
|
356
|
+
const active = typeof promptStore.getActivePrompt === 'function'
|
|
357
|
+
? await promptStore.getActivePrompt()
|
|
358
|
+
: null;
|
|
359
|
+
if (active) return active;
|
|
360
|
+
return config.systemPrompt || 'You are a helpful assistant.';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async getAgent(agentId) {
|
|
364
|
+
const { rows } = await this._pool.query(
|
|
365
|
+
'SELECT * FROM agent_registry WHERE agent_id = $1', [agentId]
|
|
366
|
+
);
|
|
367
|
+
return rows[0] ?? null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async getAllAgents() {
|
|
371
|
+
const { rows } = await this._pool.query(
|
|
372
|
+
'SELECT * FROM agent_registry ORDER BY display_name'
|
|
373
|
+
);
|
|
374
|
+
return rows;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async upsertAgent(row) {
|
|
378
|
+
const now = new Date().toISOString();
|
|
379
|
+
await this._pool.query(
|
|
380
|
+
`INSERT INTO agent_registry
|
|
381
|
+
(agent_id, display_name, description, system_prompt, default_model,
|
|
382
|
+
default_hitl_level, allow_user_model_select, allow_user_hitl_config,
|
|
383
|
+
tool_allowlist, max_turns, max_tokens, is_default, enabled, seeded_from_config,
|
|
384
|
+
created_at, updated_at)
|
|
385
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
|
386
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
387
|
+
display_name = $2, description = $3, system_prompt = $4, default_model = $5,
|
|
388
|
+
default_hitl_level = $6, allow_user_model_select = $7, allow_user_hitl_config = $8,
|
|
389
|
+
tool_allowlist = $9, max_turns = $10, max_tokens = $11, is_default = $12,
|
|
390
|
+
enabled = $13, seeded_from_config = $14, updated_at = $16`,
|
|
391
|
+
[
|
|
392
|
+
row.agent_id, row.display_name, row.description ?? null, row.system_prompt ?? null,
|
|
393
|
+
row.default_model ?? null, row.default_hitl_level ?? null,
|
|
394
|
+
row.allow_user_model_select ?? 0, row.allow_user_hitl_config ?? 0,
|
|
395
|
+
row.tool_allowlist ?? '*', row.max_turns ?? null, row.max_tokens ?? null,
|
|
396
|
+
row.is_default ?? 0, row.enabled ?? 1, row.seeded_from_config ?? 0, now, now
|
|
397
|
+
]
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async setDefault(agentId) {
|
|
402
|
+
const client = await this._pool.connect();
|
|
403
|
+
try {
|
|
404
|
+
await client.query('BEGIN');
|
|
405
|
+
const { rows } = await client.query(
|
|
406
|
+
'SELECT 1 FROM agent_registry WHERE agent_id = $1 AND enabled = 1', [agentId]
|
|
407
|
+
);
|
|
408
|
+
if (rows.length === 0) { await client.query('ROLLBACK'); return; }
|
|
409
|
+
await client.query('UPDATE agent_registry SET is_default = 0 WHERE is_default = 1');
|
|
410
|
+
await client.query(
|
|
411
|
+
'UPDATE agent_registry SET is_default = 1, updated_at = $1 WHERE agent_id = $2',
|
|
412
|
+
[new Date().toISOString(), agentId]
|
|
413
|
+
);
|
|
414
|
+
await client.query('COMMIT');
|
|
415
|
+
} catch (err) {
|
|
416
|
+
await client.query('ROLLBACK');
|
|
417
|
+
throw err;
|
|
418
|
+
} finally {
|
|
419
|
+
client.release();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async deleteAgent(agentId) {
|
|
424
|
+
await this._pool.query('DELETE FROM agent_registry WHERE agent_id = $1', [agentId]);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async seedFromConfig() {
|
|
428
|
+
const agents = this._config.agents;
|
|
429
|
+
if (!Array.isArray(agents) || agents.length === 0) return;
|
|
430
|
+
|
|
431
|
+
let defaultAgentId = null;
|
|
432
|
+
for (const a of agents) {
|
|
433
|
+
if (!a.id || !a.displayName) continue;
|
|
434
|
+
const existing = await this.getAgent(a.id);
|
|
435
|
+
if (existing && !existing.seeded_from_config) continue;
|
|
436
|
+
await this.upsertAgent({
|
|
437
|
+
agent_id: a.id,
|
|
438
|
+
display_name: a.displayName,
|
|
439
|
+
description: a.description ?? null,
|
|
440
|
+
system_prompt: a.systemPrompt ?? null,
|
|
441
|
+
default_model: a.defaultModel ?? null,
|
|
442
|
+
default_hitl_level: a.defaultHitlLevel ?? null,
|
|
443
|
+
allow_user_model_select: a.allowUserModelSelect ? 1 : 0,
|
|
444
|
+
allow_user_hitl_config: a.allowUserHitlConfig ? 1 : 0,
|
|
445
|
+
tool_allowlist: Array.isArray(a.toolAllowlist) ? JSON.stringify(a.toolAllowlist) : '*',
|
|
446
|
+
max_turns: a.maxTurns ?? null,
|
|
447
|
+
max_tokens: a.maxTokens ?? null,
|
|
448
|
+
is_default: 0,
|
|
449
|
+
enabled: 1,
|
|
450
|
+
seeded_from_config: 1
|
|
451
|
+
});
|
|
452
|
+
if (a.isDefault) defaultAgentId = a.id;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (defaultAgentId) {
|
|
456
|
+
await this.setDefault(defaultAgentId);
|
|
457
|
+
} else {
|
|
458
|
+
const defaultAgent = await this.resolveAgent(null);
|
|
459
|
+
if (!defaultAgent) {
|
|
460
|
+
const first = agents.find(a => a.id && a.displayName);
|
|
461
|
+
if (first) await this.setDefault(first.id);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type HitlLevel = 'autonomous' | 'cautious' | 'standard' | 'paranoid';
|
|
2
|
+
|
|
3
|
+
export interface UserPreferences {
|
|
4
|
+
model: string | null;
|
|
5
|
+
hitlLevel: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EffectiveSettings {
|
|
9
|
+
model: string;
|
|
10
|
+
hitlLevel: string;
|
|
11
|
+
provider: string;
|
|
12
|
+
apiKey: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-user model + HITL preferences with app-owner permission gates.
|
|
17
|
+
*
|
|
18
|
+
* `resolveEffective()` merges user preferences with config gates:
|
|
19
|
+
* - `allowUserModelSelect: false` → user model preference is ignored
|
|
20
|
+
* - `allowUserHitlConfig: false` → user HITL preference is ignored
|
|
21
|
+
*/
|
|
22
|
+
export class PreferenceStore {
|
|
23
|
+
constructor(db: object);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get stored preferences for a user, or `null` if none exist.
|
|
27
|
+
*/
|
|
28
|
+
getUserPreferences(userId: string): UserPreferences | null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Upsert user preferences.
|
|
32
|
+
* Throws if `hitlLevel` is not a valid value.
|
|
33
|
+
*/
|
|
34
|
+
setUserPreferences(userId: string, prefs: { model?: string; hitlLevel?: string }): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the effective model, HITL level, provider, and API key for a user.
|
|
38
|
+
* Merges user preferences with app-owner config gates.
|
|
39
|
+
*/
|
|
40
|
+
resolveEffective(userId: string, config: object, env?: Record<string, string>): EffectiveSettings;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Factory — creates a PreferenceStore backed by SQLite.
|
|
45
|
+
* For Postgres, use `buildSidecarContext`, which selects the adapter automatically.
|
|
46
|
+
*/
|
|
47
|
+
export function makePreferenceStore(config: object, db: object): PreferenceStore;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreferenceStore — per-user model + HITL preferences with permission hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* resolveEffective() merges user preferences with app-owner config gates:
|
|
5
|
+
* - allowUserModelSelect: if false, user model preference is ignored
|
|
6
|
+
* - allowUserHitlConfig: if false, user hitl preference is ignored
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getUserPreferences, upsertUserPreferences } from './db.js';
|
|
10
|
+
import { detectProvider, resolveApiKey } from './api-client.js';
|
|
11
|
+
|
|
12
|
+
const VALID_HITL_LEVELS = ['autonomous', 'cautious', 'standard', 'paranoid'];
|
|
13
|
+
|
|
14
|
+
export class PreferenceStore {
|
|
15
|
+
/** @param {import('better-sqlite3').Database} db */
|
|
16
|
+
constructor(db) {
|
|
17
|
+
this._db = db;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get stored preferences for a user.
|
|
22
|
+
* @param {string} userId
|
|
23
|
+
* @returns {{ model: string|null, hitlLevel: string|null } | null}
|
|
24
|
+
*/
|
|
25
|
+
getUserPreferences(userId) {
|
|
26
|
+
const row = getUserPreferences(this._db, userId);
|
|
27
|
+
if (!row) return null;
|
|
28
|
+
return { model: row.model, hitlLevel: row.hitl_level };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set user preferences (upsert).
|
|
33
|
+
* @param {string} userId
|
|
34
|
+
* @param {{ model?: string, hitlLevel?: string }} prefs
|
|
35
|
+
*/
|
|
36
|
+
setUserPreferences(userId, prefs) {
|
|
37
|
+
if (prefs.hitlLevel && !VALID_HITL_LEVELS.includes(prefs.hitlLevel)) {
|
|
38
|
+
throw new Error(`Invalid hitlLevel: ${prefs.hitlLevel}. Must be one of: ${VALID_HITL_LEVELS.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
upsertUserPreferences(this._db, userId, prefs);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve effective settings for a user — merges user prefs with config gates.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} userId
|
|
47
|
+
* @param {object} config — merged forge config (from mergeDefaults)
|
|
48
|
+
* @param {object} env — process.env or equivalent
|
|
49
|
+
* @returns {{ model: string, hitlLevel: string, provider: string, apiKey: string|null }}
|
|
50
|
+
*/
|
|
51
|
+
resolveEffective(userId, config, env = {}) {
|
|
52
|
+
const userPrefs = this.getUserPreferences(userId);
|
|
53
|
+
|
|
54
|
+
const model = (config.allowUserModelSelect && userPrefs?.model)
|
|
55
|
+
? userPrefs.model
|
|
56
|
+
: (config.defaultModel ?? 'claude-sonnet-4-6');
|
|
57
|
+
|
|
58
|
+
const hitlLevel = (config.allowUserHitlConfig && userPrefs?.hitlLevel)
|
|
59
|
+
? userPrefs.hitlLevel
|
|
60
|
+
: (config.defaultHitlLevel ?? 'cautious');
|
|
61
|
+
|
|
62
|
+
const provider = detectProvider(model);
|
|
63
|
+
const apiKey = resolveApiKey(provider, env);
|
|
64
|
+
|
|
65
|
+
return { model, hitlLevel, provider, apiKey };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Factory — creates a PreferenceStore backed by SQLite.
|
|
71
|
+
* For Postgres, use buildSidecarContext which selects the adapter automatically.
|
|
72
|
+
* @param {object} config
|
|
73
|
+
* @param {import('better-sqlite3').Database} db
|
|
74
|
+
* @returns {PreferenceStore}
|
|
75
|
+
*/
|
|
76
|
+
export function makePreferenceStore(config, db) {
|
|
77
|
+
return new PreferenceStore(db);
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface PromptVersion {
|
|
2
|
+
id: number;
|
|
3
|
+
version: string;
|
|
4
|
+
content: string;
|
|
5
|
+
is_active: number;
|
|
6
|
+
created_at: string;
|
|
7
|
+
activated_at: string | null;
|
|
8
|
+
notes: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Versioned system prompt management.
|
|
13
|
+
* The TUI writes prompt versions; the sidecar reads the active one per request.
|
|
14
|
+
* Hot-swap: activating a new version takes effect on the next chat request.
|
|
15
|
+
*/
|
|
16
|
+
export class PromptStore {
|
|
17
|
+
constructor(db: object);
|
|
18
|
+
|
|
19
|
+
/** Get the active prompt content, or `''` if none is active. */
|
|
20
|
+
getActivePrompt(): string;
|
|
21
|
+
|
|
22
|
+
/** Get all versions ordered by most recent first. */
|
|
23
|
+
getAllVersions(): PromptVersion[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a new prompt version (inactive by default).
|
|
27
|
+
* @returns The new version's id.
|
|
28
|
+
*/
|
|
29
|
+
createVersion(version: string, content: string, notes?: string | null): number;
|
|
30
|
+
|
|
31
|
+
/** Activate a prompt version by id (deactivates all others). */
|
|
32
|
+
activate(id: number): void;
|
|
33
|
+
|
|
34
|
+
/** Get a specific version by id, or `null` if not found. */
|
|
35
|
+
getVersion(id: number): PromptVersion | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Factory — creates a PromptStore backed by SQLite.
|
|
40
|
+
* For Postgres, use `buildSidecarContext`, which selects the adapter automatically.
|
|
41
|
+
*/
|
|
42
|
+
export function makePromptStore(config: object, db: object): PromptStore;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptStore — versioned system prompt management.
|
|
3
|
+
*
|
|
4
|
+
* TUI writes prompt versions, sidecar reads the active one per request.
|
|
5
|
+
* Hot-swap: activate a new version and the next chat request picks it up.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getActivePrompt,
|
|
10
|
+
insertPromptVersion,
|
|
11
|
+
activatePromptVersion,
|
|
12
|
+
getAllPromptVersions
|
|
13
|
+
} from './db.js';
|
|
14
|
+
|
|
15
|
+
export class PromptStore {
|
|
16
|
+
/** @param {import('better-sqlite3').Database} db */
|
|
17
|
+
constructor(db) {
|
|
18
|
+
this._db = db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get the active prompt content, or '' if none active. */
|
|
22
|
+
getActivePrompt() {
|
|
23
|
+
const row = getActivePrompt(this._db);
|
|
24
|
+
return row ? row.content : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Get all versions ordered by most recent first. */
|
|
28
|
+
getAllVersions() {
|
|
29
|
+
return getAllPromptVersions(this._db);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new prompt version (inactive by default).
|
|
34
|
+
* @returns {number} id
|
|
35
|
+
*/
|
|
36
|
+
createVersion(version, content, notes = null) {
|
|
37
|
+
return insertPromptVersion(this._db, { version, content, notes });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Activate a prompt version (deactivates all others). */
|
|
41
|
+
activate(id) {
|
|
42
|
+
activatePromptVersion(this._db, id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Get a specific version by id, or null. */
|
|
46
|
+
getVersion(id) {
|
|
47
|
+
return this._db.prepare('SELECT * FROM prompt_versions WHERE id = ?').get(id) ?? null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Factory — creates a PromptStore backed by SQLite.
|
|
53
|
+
* For Postgres, use buildSidecarContext which selects the adapter automatically.
|
|
54
|
+
* @param {object} config — merged forge config (unused for now, reserved for future adapters)
|
|
55
|
+
* @param {import('better-sqlite3').Database} db
|
|
56
|
+
* @returns {PromptStore}
|
|
57
|
+
*/
|
|
58
|
+
export function makePromptStore(config, db) {
|
|
59
|
+
return new PromptStore(db);
|
|
60
|
+
}
|