eminiar 0.11.0 → 0.13.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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +64 -10
- package/bin/em-hook +75 -2
- package/dist/cli.js +31606 -16936
- package/dist/hook-async-4MIP3VHX.js +15 -0
- package/dist/hook-chunk-77I6QQ43.js +3780 -0
- package/dist/hook-chunk-IYHVLMJ4.js +24099 -0
- package/dist/hook-chunk-MVOBVFQL.js +9843 -0
- package/dist/hook-hooks-U6GDOQSY.js +1827 -0
- package/dist/hook.js +706 -30200
- package/dist/server.js +45060 -34225
- package/dist/ui/app.global.js +70 -63
- package/dist/ui/styles.css +1832 -0
- package/eminiar.config.schema.json +450 -1
- package/hooks/hooks.json +9 -1
- package/package.json +2 -2
- package/skills/em-doctor/SKILL.md +14 -1
- package/skills/em-onboard/SKILL.md +33 -1
- package/skills/em-swarm/SKILL.md +100 -0
|
@@ -0,0 +1,1827 @@
|
|
|
1
|
+
import { createRequire as __emCreateRequire } from "node:module"; const require = __emCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
FALLBACK_MODEL,
|
|
4
|
+
agentFileRange,
|
|
5
|
+
coordinationRoot,
|
|
6
|
+
costOfCall,
|
|
7
|
+
hookLog,
|
|
8
|
+
manifestPath,
|
|
9
|
+
readManifestAt,
|
|
10
|
+
resolvePrice
|
|
11
|
+
} from "./hook-chunk-MVOBVFQL.js";
|
|
12
|
+
import {
|
|
13
|
+
adjudicateOne,
|
|
14
|
+
briefUnitsOf,
|
|
15
|
+
cacheKeyFor,
|
|
16
|
+
complexityBucket,
|
|
17
|
+
extractFeatures,
|
|
18
|
+
resolveAdjudicator,
|
|
19
|
+
spawnAsyncJob,
|
|
20
|
+
topIntent
|
|
21
|
+
} from "./hook-chunk-77I6QQ43.js";
|
|
22
|
+
import {
|
|
23
|
+
AGENT_EFFORT_SINCE,
|
|
24
|
+
KEPT_MODEL_FLAG,
|
|
25
|
+
TIERS,
|
|
26
|
+
getContext,
|
|
27
|
+
getStorage,
|
|
28
|
+
init_esm_shims,
|
|
29
|
+
keptModel,
|
|
30
|
+
tierOf
|
|
31
|
+
} from "./hook-chunk-IYHVLMJ4.js";
|
|
32
|
+
|
|
33
|
+
// src/adjudicator/hooks.ts
|
|
34
|
+
init_esm_shims();
|
|
35
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
36
|
+
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
37
|
+
|
|
38
|
+
// src/adjudicator/agent-pin.ts
|
|
39
|
+
init_esm_shims();
|
|
40
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
41
|
+
import { homedir } from "os";
|
|
42
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
43
|
+
|
|
44
|
+
// src/adjudicator/savings.ts
|
|
45
|
+
init_esm_shims();
|
|
46
|
+
|
|
47
|
+
// src/adjudicator/env.ts
|
|
48
|
+
init_esm_shims();
|
|
49
|
+
var ON = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
50
|
+
function envFlagOn(value) {
|
|
51
|
+
return value !== void 0 && ON.has(value.trim().toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
function subagentModelForced(env = process.env) {
|
|
54
|
+
return envFlagOn(env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/adjudicator/savings.ts
|
|
58
|
+
var SAVINGS_ASSUMPTIONS = Object.freeze([
|
|
59
|
+
"Token counts would have been the same on the baseline model. Smaller models can need more turns, so compare tokens per successful task by tier.",
|
|
60
|
+
"List prices, with cache pricing unchanged.",
|
|
61
|
+
"Credit only for dispatches that were followed or applied; a failed attempt that was redone is charged its full cost.",
|
|
62
|
+
"No credit for a dispatch to an agent whose model pin the advice kept (pinned, or a pin that could not be read): the adjudicator chose nothing there.",
|
|
63
|
+
"For a role with a model range the baseline is its fallback pin (the model the agent file pins), and a ranged dispatch earns credit like any followed advice.",
|
|
64
|
+
"For an agent with no `model` line, the baseline is CLAUDE_CODE_SUBAGENT_MODEL when it is set (and FORCE is off), else the main session's model.",
|
|
65
|
+
"No credit while CLAUDE_CODE_SUBAGENT_MODEL_FORCE is on, nor for a fork (it runs on the main session's model): Claude Code, not the advice, sets the model.",
|
|
66
|
+
"The adjudicator's own provider spend is subtracted.",
|
|
67
|
+
"No credit for tool advice: em_stats already counts context kept out by em_* tools. Only the tool-advice follow rate is reported.",
|
|
68
|
+
"Main-session advice yields potential savings only and is never added to net: the counterfactual cannot be observed."
|
|
69
|
+
]);
|
|
70
|
+
var SESSION_LOOKBACK_MS = 12 * 60 * 60 * 1e3;
|
|
71
|
+
function tierRank(tier) {
|
|
72
|
+
return TIERS.indexOf(tier);
|
|
73
|
+
}
|
|
74
|
+
function isPinned(model) {
|
|
75
|
+
const m = String(model ?? "").trim();
|
|
76
|
+
return m !== "" && m.toLowerCase() !== "inherit";
|
|
77
|
+
}
|
|
78
|
+
function withKeptModel(raw, kept, extra = {}) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
81
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
82
|
+
const o = { ...parsed, ...extra };
|
|
83
|
+
for (const k of Object.values(KEPT_MODEL_FLAG)) delete o[k];
|
|
84
|
+
if (kept) o[KEPT_MODEL_FLAG[kept]] = true;
|
|
85
|
+
return JSON.stringify(o);
|
|
86
|
+
} catch {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var RANGED_FLAG = "ranged";
|
|
91
|
+
var FALLBACK_PIN_FIELD = "fallbackPin";
|
|
92
|
+
var ADVISED_PICK_FIELD = "advisedPick";
|
|
93
|
+
function scoresRecord(row) {
|
|
94
|
+
const raw = row.scores;
|
|
95
|
+
let o = raw;
|
|
96
|
+
if (typeof raw === "string") {
|
|
97
|
+
try {
|
|
98
|
+
o = JSON.parse(raw);
|
|
99
|
+
} catch {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) return void 0;
|
|
104
|
+
return o;
|
|
105
|
+
}
|
|
106
|
+
function advisedPick(row) {
|
|
107
|
+
const pick = scoresRecord(row)?.[ADVISED_PICK_FIELD];
|
|
108
|
+
return typeof pick === "string" && tierRank(pick) >= 0 ? pick : void 0;
|
|
109
|
+
}
|
|
110
|
+
function envSubagentModel(env) {
|
|
111
|
+
if (envFlagOn(env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE)) return void 0;
|
|
112
|
+
const m = env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
113
|
+
return isPinned(m) ? m.trim() : void 0;
|
|
114
|
+
}
|
|
115
|
+
function resolveBaseline(db, q) {
|
|
116
|
+
if (isPinned(q.fallbackPin)) return { model: q.fallbackPin.trim(), source: "ranged" };
|
|
117
|
+
if (isPinned(q.pinned)) return { model: q.pinned.trim(), source: "pinned" };
|
|
118
|
+
const envModel = q.env ? envSubagentModel(q.env) : void 0;
|
|
119
|
+
if (envModel) return { model: envModel, source: "env" };
|
|
120
|
+
if (q.sessionId) {
|
|
121
|
+
const calls = q.calls ?? (db ? db.apiCalls({
|
|
122
|
+
sessionId: q.sessionId,
|
|
123
|
+
since: q.ts - SESSION_LOOKBACK_MS,
|
|
124
|
+
until: q.ts,
|
|
125
|
+
sidechain: false
|
|
126
|
+
}) : []);
|
|
127
|
+
let best;
|
|
128
|
+
for (const c of calls) {
|
|
129
|
+
if (c.sidechain || c.session_id !== q.sessionId || c.ts >= q.ts || !c.model) continue;
|
|
130
|
+
if (c.model.startsWith("<")) continue;
|
|
131
|
+
if (!best || c.ts >= best.ts) best = c;
|
|
132
|
+
}
|
|
133
|
+
if (best) return { model: best.model, source: "session" };
|
|
134
|
+
}
|
|
135
|
+
return { model: FALLBACK_MODEL, source: "fallback" };
|
|
136
|
+
}
|
|
137
|
+
function totalTokens(calls) {
|
|
138
|
+
let n = 0;
|
|
139
|
+
for (const c of calls)
|
|
140
|
+
n += (c.input_tokens || 0) + (c.output_tokens || 0) + (c.cache_create_5m || 0) + (c.cache_create_1h || 0) + (c.cache_read || 0);
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
function dominantModel(calls) {
|
|
144
|
+
const counts = /* @__PURE__ */ new Map();
|
|
145
|
+
let best;
|
|
146
|
+
let bestN = 0;
|
|
147
|
+
for (const c of calls) {
|
|
148
|
+
if (!c.model || c.model.startsWith("<")) continue;
|
|
149
|
+
const n = (counts.get(c.model) ?? 0) + 1;
|
|
150
|
+
counts.set(c.model, n);
|
|
151
|
+
if (n >= bestN) {
|
|
152
|
+
best = c.model;
|
|
153
|
+
bestN = n;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return best;
|
|
157
|
+
}
|
|
158
|
+
function actualCost(calls, overrides) {
|
|
159
|
+
let total = 0;
|
|
160
|
+
for (const c of calls) {
|
|
161
|
+
const p = resolvePrice(c.model, overrides);
|
|
162
|
+
if (p) total += costOfCall(c, p).total;
|
|
163
|
+
else if (typeof c.cost_usd === "number" && c.priced) total += c.cost_usd;
|
|
164
|
+
else return null;
|
|
165
|
+
}
|
|
166
|
+
return total;
|
|
167
|
+
}
|
|
168
|
+
function repriceAt(calls, model, overrides) {
|
|
169
|
+
const p = resolvePrice(model, overrides);
|
|
170
|
+
if (!p) return null;
|
|
171
|
+
let total = 0;
|
|
172
|
+
for (const c of calls) total += costOfCall(c, p).total;
|
|
173
|
+
return total;
|
|
174
|
+
}
|
|
175
|
+
function allRows(db, filter = {}, max = 2e4) {
|
|
176
|
+
const out = [];
|
|
177
|
+
for (let offset = 0; offset < max; offset += 500) {
|
|
178
|
+
const { rows, total } = db.adjudicator.list({ ...filter, limit: 500, offset });
|
|
179
|
+
out.push(...rows);
|
|
180
|
+
if (!rows.length || offset + rows.length >= total) break;
|
|
181
|
+
}
|
|
182
|
+
return out.sort((a, b) => a.ts - b.ts || a.id - b.id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/adjudicator/agent-pin.ts
|
|
186
|
+
var FORK_AGENT = "fork";
|
|
187
|
+
function isFork(agentType) {
|
|
188
|
+
return agentType === FORK_AGENT;
|
|
189
|
+
}
|
|
190
|
+
var BUILTIN = {
|
|
191
|
+
"general-purpose": void 0,
|
|
192
|
+
Explore: void 0,
|
|
193
|
+
Plan: void 0,
|
|
194
|
+
"statusline-setup": "sonnet",
|
|
195
|
+
"claude-code-guide": "haiku"
|
|
196
|
+
};
|
|
197
|
+
var MAX_FILES = 2e3;
|
|
198
|
+
var MAX_DEPTH = 8;
|
|
199
|
+
var MAX_BYTES = 256 * 1024;
|
|
200
|
+
var MAX_UP = 8;
|
|
201
|
+
var SEGMENT = /^[A-Za-z0-9_.-]+$/;
|
|
202
|
+
function homeOf(env) {
|
|
203
|
+
return env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
|
|
204
|
+
}
|
|
205
|
+
function defaultManagedDir() {
|
|
206
|
+
switch (process.platform) {
|
|
207
|
+
case "darwin":
|
|
208
|
+
return "/Library/Application Support/ClaudeCode";
|
|
209
|
+
case "linux":
|
|
210
|
+
return "/etc/claude-code";
|
|
211
|
+
case "win32":
|
|
212
|
+
return "C:\\Program Files\\ClaudeCode";
|
|
213
|
+
default:
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function scalar(fm, key) {
|
|
218
|
+
const raw = new RegExp(`^${key}:[ \\t]*(.*)$`, "m").exec(fm)?.[1]?.trim();
|
|
219
|
+
if (!raw) return void 0;
|
|
220
|
+
const q = raw[0];
|
|
221
|
+
if (q === '"' || q === "'") {
|
|
222
|
+
const end = raw.indexOf(q, 1);
|
|
223
|
+
return end > 1 ? raw.slice(1, end).trim() || void 0 : void 0;
|
|
224
|
+
}
|
|
225
|
+
return raw.replace(/\s+#.*$/, "").trim() || void 0;
|
|
226
|
+
}
|
|
227
|
+
function readDef(file) {
|
|
228
|
+
try {
|
|
229
|
+
if (statSync(file).size > MAX_BYTES) return { file };
|
|
230
|
+
const text = readFileSync(file, "utf8");
|
|
231
|
+
const fm = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
|
|
232
|
+
if (!fm) return { file };
|
|
233
|
+
const def = { file, name: scalar(fm, "name"), model: scalar(fm, "model") };
|
|
234
|
+
if (text.includes(" range=")) def.range = agentFileRange(text);
|
|
235
|
+
return def;
|
|
236
|
+
} catch {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function rangeOf(defs, model) {
|
|
241
|
+
const r = defs[0]?.range;
|
|
242
|
+
if (!r || defs.some((d) => d.range?.floor !== r.floor || d.range?.ceiling !== r.ceiling))
|
|
243
|
+
return void 0;
|
|
244
|
+
const tier = model.toLowerCase() === "opusplan" ? void 0 : tierOf(model);
|
|
245
|
+
const [lo, hi, at] = [tierRank(r.floor), tierRank(r.ceiling), tierRank(tier)];
|
|
246
|
+
return lo >= 0 && lo < hi && at >= lo && at <= hi ? { floor: r.floor, ceiling: r.ceiling } : void 0;
|
|
247
|
+
}
|
|
248
|
+
function scan(dir) {
|
|
249
|
+
const files = [];
|
|
250
|
+
let truncated = false;
|
|
251
|
+
const walk = (d, depth) => {
|
|
252
|
+
if (truncated) return;
|
|
253
|
+
let entries;
|
|
254
|
+
try {
|
|
255
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
256
|
+
} catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const e of entries) {
|
|
260
|
+
const p = join(d, e.name);
|
|
261
|
+
if (e.isDirectory()) {
|
|
262
|
+
if (depth >= MAX_DEPTH) truncated = true;
|
|
263
|
+
else walk(p, depth + 1);
|
|
264
|
+
} else if (e.name.endsWith(".md")) {
|
|
265
|
+
if (files.length >= MAX_FILES) {
|
|
266
|
+
truncated = true;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
files.push(p);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
walk(dir, 0);
|
|
274
|
+
return { files, truncated };
|
|
275
|
+
}
|
|
276
|
+
function byName(dir, type) {
|
|
277
|
+
const { files, truncated } = scan(dir);
|
|
278
|
+
const defs = [];
|
|
279
|
+
for (const f of files) {
|
|
280
|
+
const d = readDef(f);
|
|
281
|
+
if (d?.name === type) defs.push(d);
|
|
282
|
+
}
|
|
283
|
+
return { defs, truncated };
|
|
284
|
+
}
|
|
285
|
+
function shown(file, home) {
|
|
286
|
+
const rel = relative(home, file);
|
|
287
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? join("~", rel) : file;
|
|
288
|
+
}
|
|
289
|
+
function decide(defs, source, home) {
|
|
290
|
+
const pins = new Set(defs.map((d) => isPinned(d.model) ? d.model.trim() : ""));
|
|
291
|
+
const first = defs[0];
|
|
292
|
+
const file = shown(first.file, home);
|
|
293
|
+
if (pins.size > 1)
|
|
294
|
+
return {
|
|
295
|
+
status: "unknown",
|
|
296
|
+
source,
|
|
297
|
+
file,
|
|
298
|
+
reason: `${defs.length} ${source} definitions disagree on the model`
|
|
299
|
+
};
|
|
300
|
+
const model = [...pins][0];
|
|
301
|
+
if (!model) {
|
|
302
|
+
const out = {
|
|
303
|
+
status: "unpinned",
|
|
304
|
+
source,
|
|
305
|
+
file,
|
|
306
|
+
reason: `${source} agent has no model pin`
|
|
307
|
+
};
|
|
308
|
+
if (defs.every((d) => d.model?.trim().toLowerCase() === "inherit")) out.inherit = true;
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
const range = rangeOf(defs, model);
|
|
312
|
+
return range ? {
|
|
313
|
+
status: "ranged",
|
|
314
|
+
model,
|
|
315
|
+
range,
|
|
316
|
+
source,
|
|
317
|
+
file,
|
|
318
|
+
reason: `${source} agent ranges ${range.floor}..${range.ceiling} (fallback ${model})`
|
|
319
|
+
} : { status: "pinned", model, source, file, reason: `${source} agent pins model ${model}` };
|
|
320
|
+
}
|
|
321
|
+
function projectDirs(root, cwd) {
|
|
322
|
+
const top = resolve(root);
|
|
323
|
+
const inside = (d2) => {
|
|
324
|
+
const rel = relative(top, d2);
|
|
325
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
326
|
+
};
|
|
327
|
+
let d = cwd && inside(resolve(cwd)) ? resolve(cwd) : top;
|
|
328
|
+
const dirs = [];
|
|
329
|
+
for (; ; ) {
|
|
330
|
+
dirs.push(d);
|
|
331
|
+
if (d === top) break;
|
|
332
|
+
d = dirname(d);
|
|
333
|
+
}
|
|
334
|
+
if (!existsSync(join(top, ".git"))) {
|
|
335
|
+
const above = [];
|
|
336
|
+
let u = top;
|
|
337
|
+
for (let i = 0; i < MAX_UP; i++) {
|
|
338
|
+
const parent = dirname(u);
|
|
339
|
+
if (parent === u) break;
|
|
340
|
+
u = parent;
|
|
341
|
+
above.push(u);
|
|
342
|
+
if (existsSync(join(u, ".git"))) {
|
|
343
|
+
dirs.push(...above);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return dirs.map((x) => join(x, ".claude", "agents"));
|
|
349
|
+
}
|
|
350
|
+
function pluginDirs(plugin, env, home) {
|
|
351
|
+
const out = /* @__PURE__ */ new Set();
|
|
352
|
+
const pluginsRoot = env.CLAUDE_CODE_PLUGIN_CACHE_DIR?.trim() || join(home, ".claude", "plugins");
|
|
353
|
+
try {
|
|
354
|
+
const j = JSON.parse(readFileSync(join(pluginsRoot, "installed_plugins.json"), "utf8"));
|
|
355
|
+
for (const [key, val] of Object.entries(j?.plugins ?? {})) {
|
|
356
|
+
const at = key.lastIndexOf("@");
|
|
357
|
+
if ((at > 0 ? key.slice(0, at) : key) !== plugin) continue;
|
|
358
|
+
for (const e of Array.isArray(val) ? val : [val]) {
|
|
359
|
+
const p = e?.installPath;
|
|
360
|
+
if (typeof p === "string" && p) out.add(p);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
366
|
+
for (const raw of (env.CLAUDE_CODE_PLUGIN_DIRS ?? "").split(sep)) {
|
|
367
|
+
const p = raw.trim().replace(/^~(?=$|[/\\])/, home);
|
|
368
|
+
if (!p || !isAbsolute(p)) continue;
|
|
369
|
+
const name = manifest(p)?.name;
|
|
370
|
+
if ((typeof name === "string" ? name : basename(p)) === plugin) out.add(p);
|
|
371
|
+
}
|
|
372
|
+
return [...out];
|
|
373
|
+
}
|
|
374
|
+
function manifest(dir) {
|
|
375
|
+
try {
|
|
376
|
+
const m = JSON.parse(readFileSync(join(dir, ".claude-plugin", "plugin.json"), "utf8"));
|
|
377
|
+
return m && typeof m === "object" ? m : void 0;
|
|
378
|
+
} catch {
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function agentPaths(dir) {
|
|
383
|
+
const a = manifest(dir)?.agents;
|
|
384
|
+
const list = typeof a === "string" ? [a] : Array.isArray(a) ? a.filter((x) => typeof x === "string") : [];
|
|
385
|
+
return (list.length ? list : ["./agents"]).map((p) => resolve(dir, p));
|
|
386
|
+
}
|
|
387
|
+
function pluginDefs(segments, env, home) {
|
|
388
|
+
const [plugin, ...rest] = segments;
|
|
389
|
+
const agent = rest.at(-1);
|
|
390
|
+
const sub = rest.slice(0, -1);
|
|
391
|
+
const installs = pluginDirs(plugin, env, home);
|
|
392
|
+
if (!installs.length) return `plugin ${plugin} is not installed where eminiar can see it`;
|
|
393
|
+
const defs = [];
|
|
394
|
+
const matches = (d, file) => !!d && (d.name === agent || basename(file, ".md") === agent);
|
|
395
|
+
for (const install of installs) {
|
|
396
|
+
for (const p of agentPaths(install)) {
|
|
397
|
+
let isDir = false;
|
|
398
|
+
try {
|
|
399
|
+
isDir = statSync(p).isDirectory();
|
|
400
|
+
} catch {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (!isDir) {
|
|
404
|
+
if (!sub.length && p.endsWith(".md")) {
|
|
405
|
+
const d = readDef(p);
|
|
406
|
+
if (matches(d, p)) defs.push(d);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
let entries;
|
|
411
|
+
try {
|
|
412
|
+
entries = readdirSync(join(p, ...sub)).filter((n) => n.endsWith(".md"));
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
for (const n of entries) {
|
|
417
|
+
const f = join(p, ...sub, n);
|
|
418
|
+
const d = readDef(f);
|
|
419
|
+
if (matches(d, f)) defs.push(d);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { defs, truncated: false };
|
|
424
|
+
}
|
|
425
|
+
function resolveAgentPin(agentType, q) {
|
|
426
|
+
try {
|
|
427
|
+
if (isFork(agentType))
|
|
428
|
+
return {
|
|
429
|
+
status: "unpinned",
|
|
430
|
+
source: "builtin",
|
|
431
|
+
reason: "a fork runs on the main session's model"
|
|
432
|
+
};
|
|
433
|
+
const env = q.env ?? process.env;
|
|
434
|
+
const home = homeOf(env);
|
|
435
|
+
if (agentType.includes(":")) {
|
|
436
|
+
const segments = agentType.split(":");
|
|
437
|
+
if (segments.length < 2 || !segments.every((s) => SEGMENT.test(s) && s !== "." && s !== ".."))
|
|
438
|
+
return { status: "unknown", source: "plugin", reason: "malformed plugin agent name" };
|
|
439
|
+
const found = pluginDefs(segments, env, home);
|
|
440
|
+
if (typeof found === "string") return { status: "unknown", source: "plugin", reason: found };
|
|
441
|
+
if (!found.defs.length)
|
|
442
|
+
return {
|
|
443
|
+
status: "unknown",
|
|
444
|
+
source: "plugin",
|
|
445
|
+
reason: `plugin agent ${agentType} has no definition file eminiar can read`
|
|
446
|
+
};
|
|
447
|
+
return decide(found.defs, "plugin", home);
|
|
448
|
+
}
|
|
449
|
+
const managed = q.managedDir === void 0 ? defaultManagedDir() : q.managedDir;
|
|
450
|
+
const scopes = [];
|
|
451
|
+
if (managed) scopes.push(["managed", join(managed, ".claude", "agents")]);
|
|
452
|
+
for (const d of projectDirs(q.root, q.cwd)) scopes.push(["project", d]);
|
|
453
|
+
scopes.push(["user", join(home, ".claude", "agents")]);
|
|
454
|
+
let truncated = false;
|
|
455
|
+
for (const [source, dir] of scopes) {
|
|
456
|
+
const found = byName(dir, agentType);
|
|
457
|
+
if (found.defs.length) return decide(found.defs, source, home);
|
|
458
|
+
truncated ||= found.truncated;
|
|
459
|
+
}
|
|
460
|
+
if (truncated)
|
|
461
|
+
return { status: "unknown", reason: "an agents directory is too large to scan fully" };
|
|
462
|
+
if (Object.hasOwn(BUILTIN, agentType)) {
|
|
463
|
+
const model = BUILTIN[agentType];
|
|
464
|
+
return model ? { status: "pinned", model, source: "builtin", reason: `built-in agent runs on ${model}` } : { status: "unpinned", source: "builtin", reason: "built-in agent has no model pin" };
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
status: "unknown",
|
|
468
|
+
reason: `no definition of ${agentType} found (a --agents or plugin agent?)`
|
|
469
|
+
};
|
|
470
|
+
} catch (err) {
|
|
471
|
+
return { status: "unknown", reason: `agent lookup failed: ${err?.message ?? err}` };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/adjudicator/learn.ts
|
|
476
|
+
init_esm_shims();
|
|
477
|
+
var SETTLE_MS = 2 * 6e4;
|
|
478
|
+
var FINAL_AFTER_MS = 24 * 60 * 6e4;
|
|
479
|
+
var LINK_WINDOW_MS = 6e4;
|
|
480
|
+
var AGENT_IDLE_MS = 30 * 6e4;
|
|
481
|
+
var BASELINE_LOOKBACK_MS = 12 * 60 * 6e4;
|
|
482
|
+
var DEFAULT_LEARNING = Object.freeze({
|
|
483
|
+
enabled: true,
|
|
484
|
+
maxBias: 0.3,
|
|
485
|
+
minSamples: 5,
|
|
486
|
+
halfLife: 50,
|
|
487
|
+
costWeight: 0.05
|
|
488
|
+
});
|
|
489
|
+
function learningOf(explicit, config) {
|
|
490
|
+
return { ...DEFAULT_LEARNING, ...config?.adjudicator?.learning ?? {}, ...explicit ?? {} };
|
|
491
|
+
}
|
|
492
|
+
var clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
493
|
+
function decayFactor(halfLife) {
|
|
494
|
+
return halfLife > 0 && Number.isFinite(halfLife) ? 2 ** (-1 / halfLife) : 1;
|
|
495
|
+
}
|
|
496
|
+
function posterior(s, n) {
|
|
497
|
+
return (s + 2) / (n + 4);
|
|
498
|
+
}
|
|
499
|
+
function median(xs) {
|
|
500
|
+
if (!xs.length) return null;
|
|
501
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
502
|
+
const mid = s.length >> 1;
|
|
503
|
+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
504
|
+
}
|
|
505
|
+
function computeBias(row, bucketMedian, l) {
|
|
506
|
+
if (!(row.n >= l.minSamples) || row.n <= 0) return 0;
|
|
507
|
+
let b = posterior(row.s, row.n) - 0.8;
|
|
508
|
+
if (row.costMedian && row.costMedian > 0 && bucketMedian && bucketMedian > 0)
|
|
509
|
+
b -= l.costWeight * Math.log2(row.costMedian / bucketMedian);
|
|
510
|
+
return clamp(b, -l.maxBias, l.maxBias);
|
|
511
|
+
}
|
|
512
|
+
function bucketMedianOf(rows) {
|
|
513
|
+
return median(
|
|
514
|
+
rows.map((r) => r.costMedian).filter((c) => typeof c === "number" && c > 0)
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
function stepMedian(current, sample) {
|
|
518
|
+
if (current === null || !(current > 0)) return sample;
|
|
519
|
+
if (sample > current) return Math.min(sample, current * 1.2);
|
|
520
|
+
if (sample < current) return Math.max(sample, current / 1.2);
|
|
521
|
+
return current;
|
|
522
|
+
}
|
|
523
|
+
function recordSignal(db, sig, learning, now = Date.now()) {
|
|
524
|
+
if (!learning.enabled || !sig.option || !sig.bucket) return;
|
|
525
|
+
const inBucket = db.adjudicator.biasAll().filter((r) => r.dimension === sig.dimension && r.bucket === sig.bucket);
|
|
526
|
+
let target = inBucket.find((r) => r.option === sig.option);
|
|
527
|
+
if (!target) {
|
|
528
|
+
target = {
|
|
529
|
+
dimension: sig.dimension,
|
|
530
|
+
bucket: sig.bucket,
|
|
531
|
+
option: sig.option,
|
|
532
|
+
n: 0,
|
|
533
|
+
s: 0,
|
|
534
|
+
f: 0,
|
|
535
|
+
costMedian: null,
|
|
536
|
+
bias: 0,
|
|
537
|
+
updatedAt: null
|
|
538
|
+
};
|
|
539
|
+
inBucket.push(target);
|
|
540
|
+
}
|
|
541
|
+
const d = decayFactor(learning.halfLife);
|
|
542
|
+
target.n = target.n * d + 1;
|
|
543
|
+
target.s = target.s * d + (sig.success ? 1 : 0);
|
|
544
|
+
target.f = target.f * d + (sig.success ? 0 : 1);
|
|
545
|
+
if (sig.success && typeof sig.costUsd === "number" && sig.costUsd > 0)
|
|
546
|
+
target.costMedian = stepMedian(target.costMedian, sig.costUsd);
|
|
547
|
+
target.updatedAt = now;
|
|
548
|
+
const med = bucketMedianOf(inBucket);
|
|
549
|
+
for (const r of inBucket) {
|
|
550
|
+
const bias = computeBias(r, med, learning);
|
|
551
|
+
if (r !== target && bias === r.bias) continue;
|
|
552
|
+
r.bias = bias;
|
|
553
|
+
db.adjudicator.biasUpsert({ ...r, updatedAt: r === target ? now : r.updatedAt });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function biasFromRows(rows, learning, key) {
|
|
557
|
+
const out = {};
|
|
558
|
+
if (!learning.enabled) return out;
|
|
559
|
+
const tierBucket = typeof key.complexity === "number" && Number.isFinite(key.complexity) ? String(complexityBucket(key.complexity)) : void 0;
|
|
560
|
+
const roleBucket = key.intent || "none";
|
|
561
|
+
for (const [dimension, bucket] of [
|
|
562
|
+
["tier", tierBucket],
|
|
563
|
+
["role", roleBucket]
|
|
564
|
+
]) {
|
|
565
|
+
if (bucket === void 0) continue;
|
|
566
|
+
const inBucket = rows.filter((r) => r.dimension === dimension && r.bucket === bucket);
|
|
567
|
+
const med = bucketMedianOf(inBucket);
|
|
568
|
+
for (const r of inBucket) {
|
|
569
|
+
const b = computeBias(r, med, learning);
|
|
570
|
+
if (b !== 0) out[`${dimension}:${r.option}`] = b;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
function biasFor(db, learning, key) {
|
|
576
|
+
const l = learningOf(learning, void 0);
|
|
577
|
+
if (!l.enabled) return {};
|
|
578
|
+
return biasFromRows(db.adjudicator.biasAll(), l, key);
|
|
579
|
+
}
|
|
580
|
+
function parsedFeatures(row) {
|
|
581
|
+
if (!row.scores) return void 0;
|
|
582
|
+
try {
|
|
583
|
+
const s = JSON.parse(row.scores);
|
|
584
|
+
return s && typeof s === "object" && s.features && typeof s.features === "object" ? s.features : void 0;
|
|
585
|
+
} catch {
|
|
586
|
+
return void 0;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function heuristicComplexityOf(row) {
|
|
590
|
+
if (!row.scores) return void 0;
|
|
591
|
+
try {
|
|
592
|
+
const s = JSON.parse(row.scores);
|
|
593
|
+
const c = s && typeof s === "object" ? s.heuristicComplexity : void 0;
|
|
594
|
+
return typeof c === "number" && Number.isFinite(c) ? c : void 0;
|
|
595
|
+
} catch {
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function bucketsOf(row) {
|
|
600
|
+
const heuristic = heuristicComplexityOf(row);
|
|
601
|
+
const complexity = heuristic ?? (typeof row.complexity === "number" && Number.isFinite(row.complexity) ? row.complexity : void 0);
|
|
602
|
+
const tier = complexity === void 0 ? void 0 : String(complexityBucket(complexity));
|
|
603
|
+
const f = parsedFeatures(row);
|
|
604
|
+
const intent = f?.intents && typeof f.intents === "object" ? topIntent(f) : void 0;
|
|
605
|
+
return { tier, role: intent ?? "none" };
|
|
606
|
+
}
|
|
607
|
+
function diff(row, patch) {
|
|
608
|
+
const out = {};
|
|
609
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
610
|
+
if (v === void 0) continue;
|
|
611
|
+
const cur = row[k] ?? null;
|
|
612
|
+
if (cur === (v ?? null)) continue;
|
|
613
|
+
out[k] = v;
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
var CREDITED = /* @__PURE__ */ new Set(["applied", "followed"]);
|
|
618
|
+
function matchDispatches(rows, agents, windowMs = LINK_WINDOW_MS) {
|
|
619
|
+
const out = /* @__PURE__ */ new Map();
|
|
620
|
+
let R = [...rows];
|
|
621
|
+
let A = [...agents];
|
|
622
|
+
const inWindow = (r, a) => a.first >= r.ts && a.first <= r.ts + windowMs;
|
|
623
|
+
for (; ; ) {
|
|
624
|
+
const cand = new Map(R.map((r) => [r.id, A.filter((a) => inWindow(r, a))]));
|
|
625
|
+
const owners = new Map(A.map((a) => [a.id, R.filter((r) => inWindow(r, a))]));
|
|
626
|
+
const pairs = [];
|
|
627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
628
|
+
for (const start of R) {
|
|
629
|
+
if (seen.has(start.id)) continue;
|
|
630
|
+
const rowsC = [];
|
|
631
|
+
const agentsC = /* @__PURE__ */ new Set();
|
|
632
|
+
const queue = [start];
|
|
633
|
+
seen.add(start.id);
|
|
634
|
+
while (queue.length) {
|
|
635
|
+
const r = queue.pop();
|
|
636
|
+
rowsC.push(r);
|
|
637
|
+
for (const a of cand.get(r.id) ?? []) {
|
|
638
|
+
if (agentsC.has(a)) continue;
|
|
639
|
+
agentsC.add(a);
|
|
640
|
+
for (const o of owners.get(a.id) ?? [])
|
|
641
|
+
if (!seen.has(o.id)) {
|
|
642
|
+
seen.add(o.id);
|
|
643
|
+
queue.push(o);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!agentsC.size || rowsC.some((r) => !r.settled)) continue;
|
|
648
|
+
if (rowsC.length === 1 && agentsC.size === 1) {
|
|
649
|
+
pairs.push([rowsC[0], [...agentsC][0]]);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
for (const r of rowsC) {
|
|
653
|
+
if (!r.want) continue;
|
|
654
|
+
const match = (cand.get(r.id) ?? []).filter((a2) => a2.tier === r.want);
|
|
655
|
+
if (match.length !== 1) continue;
|
|
656
|
+
const a = match[0];
|
|
657
|
+
const rivals = (owners.get(a.id) ?? []).filter((o) => o !== r);
|
|
658
|
+
if (rivals.every((o) => o.want !== void 0 && o.want !== a.tier)) pairs.push([r, a]);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (!pairs.length) return out;
|
|
662
|
+
for (const [r, a] of pairs) out.set(r.id, a.id);
|
|
663
|
+
const pr = new Set(pairs.map(([r]) => r.id));
|
|
664
|
+
const pa = new Set(pairs.map(([, a]) => a.id));
|
|
665
|
+
R = R.filter((r) => !pr.has(r.id));
|
|
666
|
+
A = A.filter((a) => !pa.has(a.id));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
var Reconciler = class {
|
|
670
|
+
constructor(db, now, learning, pricing, sessionsSince) {
|
|
671
|
+
this.db = db;
|
|
672
|
+
this.now = now;
|
|
673
|
+
this.learning = learning;
|
|
674
|
+
this.pricing = pricing;
|
|
675
|
+
this.sessionsSince = sessionsSince;
|
|
676
|
+
this.latestCallTs = db.apiCallsLatestTs();
|
|
677
|
+
}
|
|
678
|
+
db;
|
|
679
|
+
now;
|
|
680
|
+
learning;
|
|
681
|
+
pricing;
|
|
682
|
+
sessionsSince;
|
|
683
|
+
result = {
|
|
684
|
+
scanned: 0,
|
|
685
|
+
updated: 0,
|
|
686
|
+
finalized: 0,
|
|
687
|
+
linked: 0,
|
|
688
|
+
learned: 0
|
|
689
|
+
};
|
|
690
|
+
sessionCalls = /* @__PURE__ */ new Map();
|
|
691
|
+
taskRows = /* @__PURE__ */ new Map();
|
|
692
|
+
runWorkers = /* @__PURE__ */ new Map();
|
|
693
|
+
promptsBySession;
|
|
694
|
+
/** Agent ids already tied to an adjudication row. */
|
|
695
|
+
taken = /* @__PURE__ */ new Set();
|
|
696
|
+
/** Row id → the agent linkAgents matched it to in this run. */
|
|
697
|
+
links = /* @__PURE__ */ new Map();
|
|
698
|
+
latestCallTs;
|
|
699
|
+
/** Calls of a session from (its oldest pending row − lookback), loaded once per run. */
|
|
700
|
+
calls(sessionId) {
|
|
701
|
+
let c = this.sessionCalls.get(sessionId);
|
|
702
|
+
if (!c) {
|
|
703
|
+
const since = (this.sessionsSince.get(sessionId) ?? this.now) - BASELINE_LOOKBACK_MS;
|
|
704
|
+
c = this.db.apiCalls({ sessionId, since });
|
|
705
|
+
this.sessionCalls.set(sessionId, c);
|
|
706
|
+
}
|
|
707
|
+
return c;
|
|
708
|
+
}
|
|
709
|
+
siblings(runId, taskId) {
|
|
710
|
+
const k = `${runId}#${taskId}`;
|
|
711
|
+
let r = this.taskRows.get(k);
|
|
712
|
+
if (!r) {
|
|
713
|
+
r = this.db.adjudicator.forTask(runId, taskId);
|
|
714
|
+
this.taskRows.set(k, r);
|
|
715
|
+
}
|
|
716
|
+
return r;
|
|
717
|
+
}
|
|
718
|
+
workers(runId) {
|
|
719
|
+
let w = this.runWorkers.get(runId);
|
|
720
|
+
if (!w) {
|
|
721
|
+
w = this.db.swarm.workers(runId);
|
|
722
|
+
this.runWorkers.set(runId, w);
|
|
723
|
+
}
|
|
724
|
+
return w;
|
|
725
|
+
}
|
|
726
|
+
prompts(sessionId, since) {
|
|
727
|
+
if (!this.promptsBySession) {
|
|
728
|
+
const m = /* @__PURE__ */ new Map();
|
|
729
|
+
for (const p of allRows(this.db, { surface: "prompt", since })) {
|
|
730
|
+
if (!p.sessionId) continue;
|
|
731
|
+
const list = m.get(p.sessionId);
|
|
732
|
+
if (list) list.push(p);
|
|
733
|
+
else m.set(p.sessionId, [p]);
|
|
734
|
+
}
|
|
735
|
+
this.promptsBySession = m;
|
|
736
|
+
}
|
|
737
|
+
return this.promptsBySession.get(sessionId) ?? [];
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Can every agent this row might own be visible? Yes once ingestion holds a call past the
|
|
741
|
+
* row's link window, or once the row is AGENT_IDLE_MS old (a session whose last agents
|
|
742
|
+
* finished inside the window has no later call; by then its transcripts are ingested).
|
|
743
|
+
*/
|
|
744
|
+
windowSeen(row) {
|
|
745
|
+
return this.latestCallTs === void 0 || this.latestCallTs >= row.ts + LINK_WINDOW_MS || this.now - row.ts >= AGENT_IDLE_MS;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Decide, once per run and jointly per session, which sidechain agent each unlinked
|
|
749
|
+
* non-swarm agent row dispatched (agentRow reads the result). Parallel Agent calls in one
|
|
750
|
+
* message are the common case, so rows are not linked greedily in order: every dispatch
|
|
751
|
+
* row of the session without an agent id competes (final, unsettled and swarm rows too),
|
|
752
|
+
* and matchDispatches links only what the evidence settles. Ambiguous rows stay unlinked.
|
|
753
|
+
*/
|
|
754
|
+
linkAgents(targets) {
|
|
755
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
756
|
+
for (const r of targets) {
|
|
757
|
+
if (r.agentId || !r.sessionId) continue;
|
|
758
|
+
const list = bySession.get(r.sessionId);
|
|
759
|
+
if (list) list.push(r);
|
|
760
|
+
else bySession.set(r.sessionId, [r]);
|
|
761
|
+
}
|
|
762
|
+
if (!bySession.size) return;
|
|
763
|
+
const minTs = Math.min(...targets.map((r) => r.ts));
|
|
764
|
+
const near = allRows(this.db, { surface: "agent", since: minTs - LINK_WINDOW_MS });
|
|
765
|
+
for (const [sessionId, mine] of bySession) {
|
|
766
|
+
const maxTs = Math.max(...mine.map((r) => r.ts));
|
|
767
|
+
const linkable = new Set(mine.map((r) => r.id));
|
|
768
|
+
const rows = near.filter((r) => r.sessionId === sessionId && !r.agentId && r.ts <= maxTs + LINK_WINDOW_MS).map((r) => ({
|
|
769
|
+
id: r.id,
|
|
770
|
+
ts: r.ts,
|
|
771
|
+
want: tierOf(r.modelFinal ?? r.modelRequested),
|
|
772
|
+
settled: this.windowSeen(r)
|
|
773
|
+
}));
|
|
774
|
+
const callsBy = /* @__PURE__ */ new Map();
|
|
775
|
+
for (const c of this.calls(sessionId)) {
|
|
776
|
+
if (!c.sidechain || !c.agent_id || this.taken.has(c.agent_id)) continue;
|
|
777
|
+
const list = callsBy.get(c.agent_id);
|
|
778
|
+
if (list) list.push(c);
|
|
779
|
+
else callsBy.set(c.agent_id, [c]);
|
|
780
|
+
}
|
|
781
|
+
const agents = [...callsBy].map(([id, cs]) => ({
|
|
782
|
+
id,
|
|
783
|
+
first: Math.min(...cs.map((c) => c.ts)),
|
|
784
|
+
tier: tierOf(dominantModel(cs))
|
|
785
|
+
}));
|
|
786
|
+
for (const [rowId, agentId] of matchDispatches(rows, agents)) {
|
|
787
|
+
this.taken.add(agentId);
|
|
788
|
+
if (linkable.has(rowId)) this.links.set(rowId, agentId);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** Seed `taken` with the agent ids already on agent / plan rows near these rows. */
|
|
793
|
+
seedTaken(since) {
|
|
794
|
+
for (const surface of ["agent", "plan"])
|
|
795
|
+
for (const r of allRows(this.db, { surface, since: since - FINAL_AFTER_MS }))
|
|
796
|
+
if (r.agentId) this.taken.add(r.agentId);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Commit a patch; on the transition to final, record the row's learning signals and keep
|
|
800
|
+
* exactly what was taught on the row (scores.taught, empty when nothing was) so a later
|
|
801
|
+
* relabel or feedback retracts that, not a lesson recomputed from the row's current state.
|
|
802
|
+
*/
|
|
803
|
+
commit(row, patch, teach) {
|
|
804
|
+
if (patch.outcome && patch.outcome !== "pending" && row.outcomeTs == null && patch.outcomeTs === void 0)
|
|
805
|
+
patch.outcomeTs = this.now;
|
|
806
|
+
const d = diff(row, patch);
|
|
807
|
+
if (!Object.keys(d).length) return;
|
|
808
|
+
let lesson;
|
|
809
|
+
if (d.final === true && !row.final) {
|
|
810
|
+
this.result.finalized++;
|
|
811
|
+
if (teach) {
|
|
812
|
+
const outcome = d.outcome ?? row.outcome;
|
|
813
|
+
lesson = this.learning.enabled && (outcome === "success" || outcome === "failure") ? this.lessonOf(row, outcome === "success", teach, d.actualCostUsd ?? row.actualCostUsd) : [];
|
|
814
|
+
const scores = withTaught(row.scores, lesson);
|
|
815
|
+
if (scores !== void 0) d.scores = scores;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.db.adjudicator.update(row.id, d);
|
|
819
|
+
this.result.updated++;
|
|
820
|
+
for (const sig of lesson ?? []) {
|
|
821
|
+
recordSignal(this.db, sig, this.learning, this.now);
|
|
822
|
+
this.result.learned++;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/** The signals a finalising swarm row teaches: the tier that ran and the role, if known. */
|
|
826
|
+
lessonOf(row, success, what, cost) {
|
|
827
|
+
const b = bucketsOf(row);
|
|
828
|
+
const out = [];
|
|
829
|
+
if (what.tier && b.tier !== void 0)
|
|
830
|
+
out.push({ dimension: "tier", bucket: b.tier, option: what.tier, success, costUsd: cost });
|
|
831
|
+
if (what.role)
|
|
832
|
+
out.push({ dimension: "role", bucket: b.role, option: what.role, success, costUsd: cost });
|
|
833
|
+
return out;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Actual model, tokens and cost of `calls`, the baseline and its cost, follow detection
|
|
837
|
+
* and the credited savings. `calls` empty leaves the token fields alone.
|
|
838
|
+
*/
|
|
839
|
+
pricePatch(row, calls, sessionId, redone, outcome) {
|
|
840
|
+
const patch = {};
|
|
841
|
+
const baseline = row.baselineModel ?? resolveBaseline(void 0, {
|
|
842
|
+
sessionId,
|
|
843
|
+
ts: row.ts,
|
|
844
|
+
calls: sessionId ? this.calls(sessionId) : []
|
|
845
|
+
}).model;
|
|
846
|
+
if (!row.baselineModel) patch.baselineModel = baseline;
|
|
847
|
+
const deferred = keptModel(row) !== void 0;
|
|
848
|
+
let action = row.action ?? null;
|
|
849
|
+
if (action === "advised" && row.modelRequested && !deferred) {
|
|
850
|
+
const parent = row.parentId ? this.db.adjudicator.get(row.parentId) : void 0;
|
|
851
|
+
const advice = advisedPick(row) ?? (parent ? advisedPick(parent) : void 0) ?? parent?.tier ?? row.tier ?? void 0;
|
|
852
|
+
const requested = tierOf(row.modelRequested) ?? row.modelRequested;
|
|
853
|
+
if (advice && requested === advice) {
|
|
854
|
+
if (advice !== (tierOf(baseline) ?? baseline)) action = "followed";
|
|
855
|
+
} else if (advice) action = "ignored";
|
|
856
|
+
if (action !== row.action) patch.action = action;
|
|
857
|
+
}
|
|
858
|
+
if (!calls.length) return patch;
|
|
859
|
+
const actual = actualCost(calls, this.pricing);
|
|
860
|
+
const base = repriceAt(calls, baseline, this.pricing);
|
|
861
|
+
patch.actualModel = dominantModel(calls) ?? null;
|
|
862
|
+
patch.actualTokens = totalTokens(calls);
|
|
863
|
+
patch.actualCostUsd = actual;
|
|
864
|
+
patch.baselineCostUsd = base;
|
|
865
|
+
let savings = null;
|
|
866
|
+
if (CREDITED.has(action ?? "") && !deferred) {
|
|
867
|
+
if (redone && outcome === "failure") savings = actual === null ? null : -actual;
|
|
868
|
+
else if (actual !== null && base !== null) savings = base - actual;
|
|
869
|
+
}
|
|
870
|
+
patch.savingsUsd = savings;
|
|
871
|
+
return patch;
|
|
872
|
+
}
|
|
873
|
+
/** Apply the redo penalty to earlier credited attempts of a task that already went final. */
|
|
874
|
+
penaliseEarlier(row, sibs) {
|
|
875
|
+
for (const e of sibs) {
|
|
876
|
+
if (e.surface !== "agent" || e.id >= row.id || !e.final) continue;
|
|
877
|
+
if (!CREDITED.has(e.action ?? "") || typeof e.actualCostUsd !== "number") continue;
|
|
878
|
+
const patch = { savingsUsd: -e.actualCostUsd };
|
|
879
|
+
const relabel = e.outcome !== "failure" && feedbackOf(e) === void 0;
|
|
880
|
+
let before = [];
|
|
881
|
+
let after = [];
|
|
882
|
+
if (relabel) {
|
|
883
|
+
patch.outcome = "failure";
|
|
884
|
+
patch.outcomeSource = "redo";
|
|
885
|
+
before = taughtLesson(this.db, e);
|
|
886
|
+
after = reconcileLesson(this.db, { ...e, outcome: "failure", outcomeSource: "redo" });
|
|
887
|
+
const scores = withTaught(e.scores, this.learning.enabled ? after : before);
|
|
888
|
+
if (scores !== void 0) patch.scores = scores;
|
|
889
|
+
}
|
|
890
|
+
const d = diff(e, patch);
|
|
891
|
+
if (Object.keys(d).length) {
|
|
892
|
+
this.db.adjudicator.update(e.id, d);
|
|
893
|
+
Object.assign(e, d);
|
|
894
|
+
this.result.updated++;
|
|
895
|
+
}
|
|
896
|
+
if (relabel)
|
|
897
|
+
this.result.learned += replaceLesson(this.db, before, after, this.learning, this.now);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
swarmRow(row) {
|
|
901
|
+
const runId = row.runId;
|
|
902
|
+
const taskId = row.taskId;
|
|
903
|
+
const run = this.db.swarm.runGet(runId);
|
|
904
|
+
const rawTask = this.db.swarm.taskGet(taskId);
|
|
905
|
+
const task = rawTask && rawTask.run_id === runId ? rawTask : void 0;
|
|
906
|
+
const sibs = this.siblings(runId, taskId);
|
|
907
|
+
const later = sibs.find((s) => s.surface === "agent" && s.id > row.id);
|
|
908
|
+
const redone = row.surface === "agent" && later !== void 0;
|
|
909
|
+
const age = this.now - row.ts;
|
|
910
|
+
let outcome = "pending";
|
|
911
|
+
let source = "swarm";
|
|
912
|
+
if (redone) {
|
|
913
|
+
outcome = "failure";
|
|
914
|
+
source = "redo";
|
|
915
|
+
} else if (task?.status === "done") outcome = "success";
|
|
916
|
+
else if (task?.status === "failed" || task?.status === "blocked") outcome = "failure";
|
|
917
|
+
else if (!task || run && run.status !== "active") outcome = "abandoned";
|
|
918
|
+
if (outcome === "pending" && age >= FINAL_AFTER_MS) {
|
|
919
|
+
outcome = "abandoned";
|
|
920
|
+
source = "timeout";
|
|
921
|
+
}
|
|
922
|
+
const workers = this.workers(runId).filter((w) => w.task_id === taskId);
|
|
923
|
+
const otherAttempts = new Set(
|
|
924
|
+
sibs.filter((s) => s.surface === "agent" && s.id !== row.id && s.agentId).map((s) => s.agentId)
|
|
925
|
+
);
|
|
926
|
+
let worker;
|
|
927
|
+
if (row.surface === "agent") {
|
|
928
|
+
worker = workers.filter(
|
|
929
|
+
(w) => !otherAttempts.has(w.agent_id) && w.spawned_at >= row.ts - 5e3 && (!later || w.spawned_at < later.ts)
|
|
930
|
+
).sort((a, b) => a.spawned_at - b.spawned_at)[0];
|
|
931
|
+
}
|
|
932
|
+
const agentId = row.agentId ?? worker?.agent_id ?? (redone ? void 0 : task?.agent_id ?? void 0);
|
|
933
|
+
if (!worker && agentId) worker = workers.find((w) => w.agent_id === agentId);
|
|
934
|
+
if (agentId) this.taken.add(agentId);
|
|
935
|
+
const sessionId = row.sessionId ?? run?.session_id ?? worker?.session_id ?? void 0;
|
|
936
|
+
const calls = agentId && sessionId ? this.calls(sessionId).filter((c) => c.agent_id === agentId && c.ts >= row.ts - 5e3) : [];
|
|
937
|
+
const patch = {
|
|
938
|
+
...this.pricePatch(row, calls, sessionId, redone, outcome),
|
|
939
|
+
outcome,
|
|
940
|
+
outcomeSource: outcome === "pending" ? row.outcomeSource : source
|
|
941
|
+
};
|
|
942
|
+
if (agentId && !row.agentId) patch.agentId = agentId;
|
|
943
|
+
const settledAt = later?.ts ?? task?.updated_at ?? row.ts;
|
|
944
|
+
const caughtUp = this.latestCallTs === void 0 || this.latestCallTs >= settledAt || calls.length > 0;
|
|
945
|
+
if (age >= FINAL_AFTER_MS || outcome !== "pending" && caughtUp) patch.final = true;
|
|
946
|
+
const teaches = row.surface === "agent" || !sibs.some((s) => s.surface === "agent");
|
|
947
|
+
const teach = teaches ? {
|
|
948
|
+
tier: tierOf(patch.actualModel ?? row.actualModel),
|
|
949
|
+
role: task?.role ?? worker?.role ?? void 0
|
|
950
|
+
} : void 0;
|
|
951
|
+
this.commit(row, patch, teach);
|
|
952
|
+
if (row.surface === "agent") this.penaliseEarlier(row, sibs);
|
|
953
|
+
}
|
|
954
|
+
agentRow(row) {
|
|
955
|
+
const sessionId = row.sessionId ?? void 0;
|
|
956
|
+
const age = this.now - row.ts;
|
|
957
|
+
const all = sessionId ? this.calls(sessionId) : [];
|
|
958
|
+
let agentId = row.agentId ?? void 0;
|
|
959
|
+
let linkedNow = false;
|
|
960
|
+
if (!agentId) {
|
|
961
|
+
agentId = this.links.get(row.id);
|
|
962
|
+
if (agentId) {
|
|
963
|
+
linkedNow = true;
|
|
964
|
+
this.result.linked++;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const calls = agentId ? all.filter((c) => c.agent_id === agentId) : [];
|
|
968
|
+
const patch = this.pricePatch(row, calls, sessionId, false, row.outcome);
|
|
969
|
+
if (linkedNow) {
|
|
970
|
+
patch.agentId = agentId;
|
|
971
|
+
patch.outcomeSource = "link";
|
|
972
|
+
}
|
|
973
|
+
const last = calls.length ? calls[calls.length - 1].ts : void 0;
|
|
974
|
+
const idle = last !== void 0 && this.now - last >= AGENT_IDLE_MS;
|
|
975
|
+
if (age >= FINAL_AFTER_MS || idle) {
|
|
976
|
+
patch.final = true;
|
|
977
|
+
if (row.outcome === "pending") patch.outcome = "n/a";
|
|
978
|
+
}
|
|
979
|
+
this.commit(row, patch);
|
|
980
|
+
}
|
|
981
|
+
promptRow(row, promptsSince) {
|
|
982
|
+
const sessionId = row.sessionId ?? void 0;
|
|
983
|
+
const age = this.now - row.ts;
|
|
984
|
+
const patch = { outcome: "n/a", outcomeSource: "window" };
|
|
985
|
+
let closed = false;
|
|
986
|
+
if (sessionId) {
|
|
987
|
+
const next = this.prompts(sessionId, promptsSince).find(
|
|
988
|
+
(p) => p.ts > row.ts || p.ts === row.ts && p.id > row.id
|
|
989
|
+
);
|
|
990
|
+
const end = next?.ts ?? Number.POSITIVE_INFINITY;
|
|
991
|
+
const calls = this.calls(sessionId).filter(
|
|
992
|
+
(c) => !c.sidechain && c.ts >= row.ts && c.ts < end
|
|
993
|
+
);
|
|
994
|
+
if (calls.length) {
|
|
995
|
+
patch.actualModel = dominantModel(calls) ?? null;
|
|
996
|
+
patch.actualTokens = totalTokens(calls);
|
|
997
|
+
patch.actualCostUsd = actualCost(calls, this.pricing);
|
|
998
|
+
}
|
|
999
|
+
if (!row.baselineModel)
|
|
1000
|
+
patch.baselineModel = resolveBaseline(void 0, {
|
|
1001
|
+
sessionId,
|
|
1002
|
+
ts: row.ts,
|
|
1003
|
+
calls: this.calls(sessionId)
|
|
1004
|
+
}).model;
|
|
1005
|
+
closed = next !== void 0 && (this.latestCallTs === void 0 || this.latestCallTs >= next.ts);
|
|
1006
|
+
}
|
|
1007
|
+
if (closed || age >= FINAL_AFTER_MS || !sessionId) patch.final = true;
|
|
1008
|
+
this.commit(row, patch);
|
|
1009
|
+
}
|
|
1010
|
+
otherRow(row) {
|
|
1011
|
+
this.commit(row, {
|
|
1012
|
+
outcome: row.outcome === "pending" ? "n/a" : row.outcome,
|
|
1013
|
+
final: true
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function reconcile(db, opts = {}) {
|
|
1018
|
+
const now = opts.now ?? Date.now();
|
|
1019
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 200, 1e4));
|
|
1020
|
+
const learning = learningOf(opts.learning, opts.config);
|
|
1021
|
+
const pricing = opts.pricing ?? opts.config?.usage?.pricing;
|
|
1022
|
+
const rows = db.adjudicator.nonFinal(limit, now - SETTLE_MS);
|
|
1023
|
+
if (!rows.length) return { scanned: 0, updated: 0, finalized: 0, linked: 0, learned: 0 };
|
|
1024
|
+
const isSwarm = (r) => (r.surface === "agent" || r.surface === "plan") && !!r.runId && typeof r.taskId === "number";
|
|
1025
|
+
const sessionsSince = /* @__PURE__ */ new Map();
|
|
1026
|
+
const note = (sid, ts) => {
|
|
1027
|
+
if (!sid) return;
|
|
1028
|
+
const cur = sessionsSince.get(sid);
|
|
1029
|
+
if (cur === void 0 || ts < cur) sessionsSince.set(sid, ts);
|
|
1030
|
+
};
|
|
1031
|
+
for (const r of rows) {
|
|
1032
|
+
note(r.sessionId, r.ts);
|
|
1033
|
+
if (isSwarm(r) && !r.sessionId) note(db.swarm.runGet(r.runId)?.session_id, r.ts);
|
|
1034
|
+
}
|
|
1035
|
+
for (const w of rows.filter(isSwarm)) {
|
|
1036
|
+
for (const wk of db.swarm.workers(w.runId))
|
|
1037
|
+
if (wk.task_id === w.taskId) note(wk.session_id, w.ts);
|
|
1038
|
+
}
|
|
1039
|
+
const minTs = Math.min(...rows.map((r) => r.ts));
|
|
1040
|
+
const rec = new Reconciler(db, now, learning, pricing, sessionsSince);
|
|
1041
|
+
rec.seedTaken(minTs);
|
|
1042
|
+
rec.result.scanned = rows.length;
|
|
1043
|
+
const swarm = rows.filter(isSwarm);
|
|
1044
|
+
const rest = rows.filter((r) => !isSwarm(r));
|
|
1045
|
+
for (const row of swarm) {
|
|
1046
|
+
try {
|
|
1047
|
+
rec.swarmRow(row);
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
rec.linkAgents(rest.filter((r) => r.surface === "agent"));
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
for (const row of rest) {
|
|
1056
|
+
try {
|
|
1057
|
+
if (row.surface === "agent") rec.agentRow(row);
|
|
1058
|
+
else if (row.surface === "prompt") rec.promptRow(row, minTs);
|
|
1059
|
+
else rec.otherRow(row);
|
|
1060
|
+
} catch {
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return rec.result;
|
|
1064
|
+
}
|
|
1065
|
+
function feedbackOf(row) {
|
|
1066
|
+
const src = row.outcomeSource ?? "";
|
|
1067
|
+
if (src.startsWith("feedback:")) {
|
|
1068
|
+
const k = src.slice("feedback:".length);
|
|
1069
|
+
return Object.hasOwn(FEEDBACK_OUTCOME, k) ? k : void 0;
|
|
1070
|
+
}
|
|
1071
|
+
if (src === "feedback")
|
|
1072
|
+
return row.outcome === "failure" ? "too_small" : row.outcome === "success" ? "good" : void 0;
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function ranAsRole(db, row) {
|
|
1076
|
+
if (!row.runId || typeof row.taskId !== "number") return void 0;
|
|
1077
|
+
const task = db.swarm.taskGet(row.taskId);
|
|
1078
|
+
if (task && task.run_id === row.runId && task.role) return task.role;
|
|
1079
|
+
if (!row.agentId) return void 0;
|
|
1080
|
+
return db.swarm.workers(row.runId).find((w) => w.agent_id === row.agentId)?.role ?? void 0;
|
|
1081
|
+
}
|
|
1082
|
+
var signalKey = (s) => `${s.dimension}\0${s.bucket}\0${s.option}`;
|
|
1083
|
+
function parseScoresObject(scores) {
|
|
1084
|
+
if (!scores) return {};
|
|
1085
|
+
try {
|
|
1086
|
+
const s = JSON.parse(scores);
|
|
1087
|
+
return s && typeof s === "object" && !Array.isArray(s) ? s : null;
|
|
1088
|
+
} catch {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function taughtOf(row) {
|
|
1093
|
+
const s = parseScoresObject(row.scores);
|
|
1094
|
+
const raw = s?.taught;
|
|
1095
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1096
|
+
const out = [];
|
|
1097
|
+
for (const t of raw) {
|
|
1098
|
+
if (!t || typeof t !== "object") continue;
|
|
1099
|
+
const e = t;
|
|
1100
|
+
if ((e.dimension === "tier" || e.dimension === "role") && typeof e.bucket === "string" && typeof e.option === "string" && typeof e.success === "boolean")
|
|
1101
|
+
out.push({ dimension: e.dimension, bucket: e.bucket, option: e.option, success: e.success });
|
|
1102
|
+
}
|
|
1103
|
+
return out;
|
|
1104
|
+
}
|
|
1105
|
+
function withTaught(scores, lesson) {
|
|
1106
|
+
const s = parseScoresObject(scores);
|
|
1107
|
+
if (!s) return void 0;
|
|
1108
|
+
const taught = lesson.map((l) => ({
|
|
1109
|
+
dimension: l.dimension,
|
|
1110
|
+
bucket: l.bucket,
|
|
1111
|
+
option: l.option,
|
|
1112
|
+
success: l.success
|
|
1113
|
+
}));
|
|
1114
|
+
return JSON.stringify({ ...s, taught });
|
|
1115
|
+
}
|
|
1116
|
+
function taughtLesson(db, row, ladder = TIERS) {
|
|
1117
|
+
const taught = taughtOf(row);
|
|
1118
|
+
if (taught) return taught;
|
|
1119
|
+
const prior = feedbackOf(row);
|
|
1120
|
+
return prior ? feedbackLesson(db, row, prior, ladder) : reconcileLesson(db, row);
|
|
1121
|
+
}
|
|
1122
|
+
function reconcileLesson(db, row) {
|
|
1123
|
+
if (!row.final || row.outcomeSource !== "swarm" && row.outcomeSource !== "redo") return [];
|
|
1124
|
+
if (row.outcome !== "success" && row.outcome !== "failure") return [];
|
|
1125
|
+
if (!row.runId || typeof row.taskId !== "number") return [];
|
|
1126
|
+
if (row.surface === "plan") {
|
|
1127
|
+
const cutoff = row.outcomeTs ?? Number.POSITIVE_INFINITY;
|
|
1128
|
+
const tracked = db.adjudicator.forTask(row.runId, row.taskId).some((s) => s.surface === "agent" && s.ts <= cutoff);
|
|
1129
|
+
if (tracked) return [];
|
|
1130
|
+
} else if (row.surface !== "agent") return [];
|
|
1131
|
+
const success = row.outcome === "success";
|
|
1132
|
+
const b = bucketsOf(row);
|
|
1133
|
+
const out = [];
|
|
1134
|
+
const tier = tierOf(row.actualModel);
|
|
1135
|
+
if (tier && b.tier !== void 0)
|
|
1136
|
+
out.push({
|
|
1137
|
+
dimension: "tier",
|
|
1138
|
+
bucket: b.tier,
|
|
1139
|
+
option: tier,
|
|
1140
|
+
success,
|
|
1141
|
+
costUsd: row.actualCostUsd
|
|
1142
|
+
});
|
|
1143
|
+
const role = ranAsRole(db, row);
|
|
1144
|
+
if (role)
|
|
1145
|
+
out.push({
|
|
1146
|
+
dimension: "role",
|
|
1147
|
+
bucket: b.role,
|
|
1148
|
+
option: role,
|
|
1149
|
+
success,
|
|
1150
|
+
costUsd: row.actualCostUsd
|
|
1151
|
+
});
|
|
1152
|
+
return out;
|
|
1153
|
+
}
|
|
1154
|
+
function feedbackLesson(db, row, feedback, ladder = TIERS) {
|
|
1155
|
+
const b = bucketsOf(row);
|
|
1156
|
+
const success = FEEDBACK_OUTCOME[feedback] === "success";
|
|
1157
|
+
const cost = row.actualCostUsd;
|
|
1158
|
+
const out = [];
|
|
1159
|
+
const tier = tierOf(row.actualModel) ?? tierOf(row.modelFinal) ?? tierOf(row.modelRequested) ?? tierOf(row.tier) ?? void 0;
|
|
1160
|
+
if (tier && b.tier !== void 0) {
|
|
1161
|
+
out.push({ dimension: "tier", bucket: b.tier, option: tier, success, costUsd: cost });
|
|
1162
|
+
if (feedback === "too_big") {
|
|
1163
|
+
const at = ladder.indexOf(tier);
|
|
1164
|
+
const lower = at > 0 ? ladder[at - 1] : void 0;
|
|
1165
|
+
if (lower) out.push({ dimension: "tier", bucket: b.tier, option: lower, success: true });
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (feedback === "good") {
|
|
1169
|
+
const role = ranAsRole(db, row) ?? row.role ?? void 0;
|
|
1170
|
+
if (role)
|
|
1171
|
+
out.push({ dimension: "role", bucket: b.role, option: role, success: true, costUsd: cost });
|
|
1172
|
+
}
|
|
1173
|
+
return out;
|
|
1174
|
+
}
|
|
1175
|
+
function adjustSignal(db, sig, dn, ds, df, learning, now) {
|
|
1176
|
+
if (!learning.enabled) return false;
|
|
1177
|
+
const inBucket = db.adjudicator.biasAll().filter((r) => r.dimension === sig.dimension && r.bucket === sig.bucket);
|
|
1178
|
+
const target = inBucket.find((r) => r.option === sig.option);
|
|
1179
|
+
if (!target) return false;
|
|
1180
|
+
target.n = Math.max(0, target.n + dn);
|
|
1181
|
+
target.s = clamp(target.s + ds, 0, target.n);
|
|
1182
|
+
target.f = clamp(target.f + df, 0, target.n);
|
|
1183
|
+
target.updatedAt = now;
|
|
1184
|
+
const med = bucketMedianOf(inBucket);
|
|
1185
|
+
for (const r of inBucket) {
|
|
1186
|
+
const bias = computeBias(r, med, learning);
|
|
1187
|
+
if (r !== target && bias === r.bias) continue;
|
|
1188
|
+
r.bias = bias;
|
|
1189
|
+
db.adjudicator.biasUpsert({ ...r, updatedAt: r === target ? now : r.updatedAt });
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
function replaceLesson(db, before, after, learning, now = Date.now()) {
|
|
1194
|
+
if (!learning.enabled) return 0;
|
|
1195
|
+
const prev = new Map(before.map((s) => [signalKey(s), s]));
|
|
1196
|
+
let changed = 0;
|
|
1197
|
+
for (const s of after) {
|
|
1198
|
+
const k = signalKey(s);
|
|
1199
|
+
const p = prev.get(k);
|
|
1200
|
+
prev.delete(k);
|
|
1201
|
+
if (p === void 0) {
|
|
1202
|
+
recordSignal(db, s, learning, now);
|
|
1203
|
+
changed++;
|
|
1204
|
+
} else if (p.success !== s.success) {
|
|
1205
|
+
if (!adjustSignal(db, s, 0, s.success ? 1 : -1, s.success ? -1 : 1, learning, now))
|
|
1206
|
+
recordSignal(db, s, learning, now);
|
|
1207
|
+
changed++;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const p of prev.values())
|
|
1211
|
+
if (adjustSignal(db, p, -1, p.success ? -1 : 0, p.success ? 0 : -1, learning, now)) changed++;
|
|
1212
|
+
return changed;
|
|
1213
|
+
}
|
|
1214
|
+
var FEEDBACK_OUTCOME = Object.freeze({
|
|
1215
|
+
good: "success",
|
|
1216
|
+
too_small: "failure",
|
|
1217
|
+
too_big: "success"
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// src/adjudicator/swarm.ts
|
|
1221
|
+
init_esm_shims();
|
|
1222
|
+
function dispatchRule(f) {
|
|
1223
|
+
if (f.fork) return { model: "fork", effort: "fork" };
|
|
1224
|
+
return {
|
|
1225
|
+
model: f.forced ? "forced" : f.ranged ? "ranged" : f.pin ? "pin" : "pass",
|
|
1226
|
+
effort: f.effortSet || f.saturated ? "informational" : "pass"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
var ROLE_NOTE_MIN = 0.7;
|
|
1230
|
+
function rangedTier(tier, range, floor, ceiling) {
|
|
1231
|
+
const r = tierRank(tier);
|
|
1232
|
+
const applyLo = tierRank(floor);
|
|
1233
|
+
const lo = Math.max(tierRank(range.floor), applyLo);
|
|
1234
|
+
const hi = Math.min(tierRank(range.ceiling), Math.max(applyLo, tierRank(ceiling)));
|
|
1235
|
+
if (r < 0 || lo < 0 || lo > hi) return void 0;
|
|
1236
|
+
return TIERS[Math.min(hi, Math.max(lo, r))];
|
|
1237
|
+
}
|
|
1238
|
+
var LADDER = { floor: TIERS[0], ceiling: TIERS[TIERS.length - 1] };
|
|
1239
|
+
function appliedTier(tier, floor, ceiling) {
|
|
1240
|
+
return rangedTier(tier, LADDER, floor, ceiling);
|
|
1241
|
+
}
|
|
1242
|
+
function rangedPasses(pick, fallback) {
|
|
1243
|
+
return !!pick && pick !== tierOf(fallback);
|
|
1244
|
+
}
|
|
1245
|
+
function rangedPart(range, pick) {
|
|
1246
|
+
const span = `range ${range.floor}\u2013${range.ceiling}`;
|
|
1247
|
+
if (rangedPasses(pick, range.fallback))
|
|
1248
|
+
return `model ${pick} (${span}, fallback ${range.fallback})`;
|
|
1249
|
+
return pick ? `fallback ${range.fallback} (${span}; matches the adjudicator's pick)` : `fallback ${range.fallback} (${span}; no pick inside apply.floor..ceiling)`;
|
|
1250
|
+
}
|
|
1251
|
+
function keptModelOf(rule) {
|
|
1252
|
+
return rule.model === "pin" ? "pin" : rule.model === "forced" ? "force" : rule.model === "fork" ? "fork" : void 0;
|
|
1253
|
+
}
|
|
1254
|
+
function pinHome(pin) {
|
|
1255
|
+
return pin.source === "project" || pin.source === void 0 ? "remove the pin in swarm.yaml" : `remove the ${pin.source} agent's model pin`;
|
|
1256
|
+
}
|
|
1257
|
+
function pinnedPart(pin, model, fix = pinHome(pin)) {
|
|
1258
|
+
if (pin.status === "unknown")
|
|
1259
|
+
return `model pin is unknown (${model ? `adjudicator would pick ${model}; ` : ""}not overridden while the agent's definition cannot be read)`;
|
|
1260
|
+
const pinned = pin.model ?? "?";
|
|
1261
|
+
if (!model || (tierOf(pinned) ?? pinned) === model)
|
|
1262
|
+
return `pinned ${pinned} (matches the adjudicator's pick)`;
|
|
1263
|
+
return `pinned ${pinned} (adjudicator would pick ${model}; ${fix} to let it choose)`;
|
|
1264
|
+
}
|
|
1265
|
+
function effortForced(env = process.env) {
|
|
1266
|
+
const v = env.CLAUDE_CODE_EFFORT_LEVEL?.trim();
|
|
1267
|
+
return !!v;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/adjudicator/hooks.ts
|
|
1271
|
+
var ADVICE_MAX = 220;
|
|
1272
|
+
var DEGRADED_MAX = 200;
|
|
1273
|
+
var HINT_WINDOW_MS = 30 * 6e4;
|
|
1274
|
+
var WARMUP_BUDGET_MS = 1e4;
|
|
1275
|
+
var RAW_TOOLS = /* @__PURE__ */ new Set(["Bash", "Read", "Grep", "Glob", "WebFetch", "WebSearch"]);
|
|
1276
|
+
var AGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
1277
|
+
var str = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
1278
|
+
function clip(s, n) {
|
|
1279
|
+
const one = s.replace(/\s+/g, " ").trim();
|
|
1280
|
+
return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
|
|
1281
|
+
}
|
|
1282
|
+
function log(line) {
|
|
1283
|
+
hookLog("info", line);
|
|
1284
|
+
}
|
|
1285
|
+
function active(cfg, input) {
|
|
1286
|
+
return cfg.enabled && input.client !== "cursor";
|
|
1287
|
+
}
|
|
1288
|
+
function docsLibraries(ctx) {
|
|
1289
|
+
const names = /* @__PURE__ */ new Set();
|
|
1290
|
+
for (const src of readManifestAt(manifestPath(ctx), ctx.root).sources) {
|
|
1291
|
+
names.add(src.name.toLowerCase());
|
|
1292
|
+
const pkg = /^(?:npm|pypi|go|crates?):(.+)$/.exec(String(src.provider ?? ""))?.[1];
|
|
1293
|
+
if (pkg) names.add(pkg.toLowerCase());
|
|
1294
|
+
}
|
|
1295
|
+
return names.size ? [...names] : void 0;
|
|
1296
|
+
}
|
|
1297
|
+
function engineOptions(db, cfg, deps) {
|
|
1298
|
+
const o = {
|
|
1299
|
+
config: cfg,
|
|
1300
|
+
store: db.adjudicator,
|
|
1301
|
+
context: "hook",
|
|
1302
|
+
log,
|
|
1303
|
+
bias: (_req, f, h) => biasFor(db, cfg.learning, { complexity: h.complexity, intent: topIntent(f) ?? "none" })
|
|
1304
|
+
};
|
|
1305
|
+
if (deps.env) o.env = deps.env;
|
|
1306
|
+
if (deps.now) o.now = deps.now;
|
|
1307
|
+
if (deps.provider !== void 0) o.provider = deps.provider;
|
|
1308
|
+
if (deps.fetch) o.deps = { fetch: deps.fetch };
|
|
1309
|
+
return o;
|
|
1310
|
+
}
|
|
1311
|
+
function adviceLine(v, f) {
|
|
1312
|
+
const c = Math.round(v.complexity);
|
|
1313
|
+
const intent = topIntent(f);
|
|
1314
|
+
const parts = [`eminiar adjudicator: complexity ${c}/4${intent ? ` (${intent})` : ""}`];
|
|
1315
|
+
if (v.tool.choice !== "none") {
|
|
1316
|
+
const tools = v.tool.entries.filter((e, i) => e.option !== "none" && (i === 0 || e.score >= 0.1)).slice(0, 2).map((e) => e.option);
|
|
1317
|
+
if (tools.length) parts.push(`start with ${tools.join(" \u2192 ")}`);
|
|
1318
|
+
}
|
|
1319
|
+
const role = v.role.choice && v.role.choice !== "none" ? `, role ${v.role.choice}` : "";
|
|
1320
|
+
parts.push(`if you delegate: ${v.tier.choice}${role}`);
|
|
1321
|
+
return clip(parts.join(" \xB7 "), ADVICE_MAX);
|
|
1322
|
+
}
|
|
1323
|
+
var HARNESS_ENVELOPE = /^\s*<(?:agent-message|task-notification|ci-monitor-event|system-reminder)[\s>]/;
|
|
1324
|
+
var LEADING_REMINDER = /^\s*<system-reminder(?:\s[^>]*)?>[\s\S]*?<\/system-reminder>/;
|
|
1325
|
+
function stripLeadingReminders(text) {
|
|
1326
|
+
let rest = text;
|
|
1327
|
+
for (let m = LEADING_REMINDER.exec(rest); m; m = LEADING_REMINDER.exec(rest)) {
|
|
1328
|
+
rest = rest.slice(m[0].length);
|
|
1329
|
+
}
|
|
1330
|
+
return rest.trim();
|
|
1331
|
+
}
|
|
1332
|
+
function isHarnessEnvelope(text) {
|
|
1333
|
+
if (typeof text !== "string" || !HARNESS_ENVELOPE.test(text)) return false;
|
|
1334
|
+
const rest = stripLeadingReminders(text);
|
|
1335
|
+
return rest === "" || HARNESS_ENVELOPE.test(rest);
|
|
1336
|
+
}
|
|
1337
|
+
async function onPrompt(ctx, db, input, sessionId, deps = {}) {
|
|
1338
|
+
try {
|
|
1339
|
+
const cfg = ctx.config.adjudicator;
|
|
1340
|
+
if (!active(cfg, input) || !cfg.surfaces.prompt) return void 0;
|
|
1341
|
+
const raw = (input.prompt ?? "").trim();
|
|
1342
|
+
if (isHarnessEnvelope(raw)) return void 0;
|
|
1343
|
+
const text = stripLeadingReminders(raw);
|
|
1344
|
+
if (text.length < cfg.minPromptChars || text.startsWith("/") || extractFeatures(text).chat) {
|
|
1345
|
+
if (cfg.surfaces.tools) closeToolHint(db, sessionId);
|
|
1346
|
+
return void 0;
|
|
1347
|
+
}
|
|
1348
|
+
const opts = engineOptions(db, cfg, deps);
|
|
1349
|
+
opts.call = cfg.promptMode === "heuristic" ? "never" : cfg.promptMode === "async" ? "async" : "inline";
|
|
1350
|
+
if (opts.call === "async") {
|
|
1351
|
+
opts.spawn = deps.spawn ?? ((job) => spawnAsyncJob(job));
|
|
1352
|
+
opts.projectRoot = ctx.root;
|
|
1353
|
+
}
|
|
1354
|
+
if (sessionId && sessionId !== "unknown") opts.row = { sessionId };
|
|
1355
|
+
const req = { key: "prompt", surface: "prompt", text };
|
|
1356
|
+
const libraries = docsLibraries(ctx);
|
|
1357
|
+
if (libraries) {
|
|
1358
|
+
const hints = { libraries };
|
|
1359
|
+
req.hints = hints;
|
|
1360
|
+
}
|
|
1361
|
+
const a = await adjudicateOne(req, opts);
|
|
1362
|
+
const out = { context: adviceLine(a.verdict, a.features) };
|
|
1363
|
+
const main = mainSessionAdvice(db, cfg, sessionId, a.verdict, (deps.now ?? Date.now)());
|
|
1364
|
+
if (main) {
|
|
1365
|
+
if (cfg.advice.mainSession === "user") out.systemMessage = main;
|
|
1366
|
+
else out.context = `${out.context}
|
|
1367
|
+
${main}`;
|
|
1368
|
+
}
|
|
1369
|
+
return out;
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
log(`adjudicator: onPrompt ${err?.message ?? String(err)}`);
|
|
1372
|
+
return void 0;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function mainSessionAdvice(db, cfg, sessionId, v, now) {
|
|
1376
|
+
if (cfg.advice.mainSession === "off" || !sessionId || sessionId === "unknown") return void 0;
|
|
1377
|
+
const key = `adjudicator:main:${sessionId}`;
|
|
1378
|
+
let st = { n: 0 };
|
|
1379
|
+
try {
|
|
1380
|
+
const raw = db.metaGet(key);
|
|
1381
|
+
if (raw) st = JSON.parse(raw);
|
|
1382
|
+
} catch {
|
|
1383
|
+
}
|
|
1384
|
+
st.n = (Number(st.n) || 0) + 1;
|
|
1385
|
+
let line;
|
|
1386
|
+
if (st.last === void 0 || st.n - st.last >= cfg.advice.cooldownPrompts) {
|
|
1387
|
+
const base = resolveBaseline(db, { sessionId, ts: now, env: {} });
|
|
1388
|
+
const current = base.source === "session" ? tierOf(base.model) : void 0;
|
|
1389
|
+
const advice = v.tier.choice;
|
|
1390
|
+
if (current && tierRank(advice) >= 0 && tierRank(current) - tierRank(advice) >= 2) {
|
|
1391
|
+
const c = Math.round(v.complexity);
|
|
1392
|
+
line = `eminiar adjudicator: this prompt looks ${c <= 1 ? "small" : "moderate"} (complexity ${c}/4); /model ${advice} would be cheaper than ${current}`;
|
|
1393
|
+
st.last = st.n;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
db.metaSet(key, JSON.stringify(st));
|
|
1397
|
+
return line;
|
|
1398
|
+
}
|
|
1399
|
+
function withModel(out, input, model) {
|
|
1400
|
+
const h = out?.hookSpecificOutput;
|
|
1401
|
+
if (h?.permissionDecision === "deny" || h?.permissionDecision === "ask") return out;
|
|
1402
|
+
return {
|
|
1403
|
+
...out,
|
|
1404
|
+
hookSpecificOutput: {
|
|
1405
|
+
...h,
|
|
1406
|
+
hookEventName: "PreToolUse",
|
|
1407
|
+
permissionDecision: "allow",
|
|
1408
|
+
updatedInput: { ...h?.updatedInput ?? input, model }
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function withNote(out, note) {
|
|
1413
|
+
if (!note) return out;
|
|
1414
|
+
const h = out?.hookSpecificOutput;
|
|
1415
|
+
const cur = h?.additionalContext;
|
|
1416
|
+
return {
|
|
1417
|
+
...out,
|
|
1418
|
+
hookSpecificOutput: {
|
|
1419
|
+
...h,
|
|
1420
|
+
hookEventName: "PreToolUse",
|
|
1421
|
+
additionalContext: cur ? `${cur}
|
|
1422
|
+
${note}` : note
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
async function afterRouting(ctx, input, spawn, out, deps = {}) {
|
|
1427
|
+
try {
|
|
1428
|
+
if (!active(ctx.config.adjudicator, input) || !input.tool_input) return out;
|
|
1429
|
+
if (AGENT_TOOLS.has(input.tool_name ?? "")) {
|
|
1430
|
+
const a = await onPreAgent(ctx, input, spawn, deps);
|
|
1431
|
+
if (!a) return out;
|
|
1432
|
+
const next = a.model ? withModel(out, input.tool_input, a.model) : out;
|
|
1433
|
+
return withNote(next, a.note);
|
|
1434
|
+
}
|
|
1435
|
+
return withNote(out, onPreTool(ctx, input, out, deps));
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
log(`adjudicator: afterRouting ${err?.message ?? String(err)}`);
|
|
1438
|
+
return out;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function clampTier(tier, floor, ceiling) {
|
|
1442
|
+
return appliedTier(tier, floor, ceiling);
|
|
1443
|
+
}
|
|
1444
|
+
function withPin(db, rowId, pin, kept, extra = {}) {
|
|
1445
|
+
try {
|
|
1446
|
+
const scores = withKeptModel(db.adjudicator.get(rowId)?.scores, kept, { pin, ...extra });
|
|
1447
|
+
return scores === void 0 ? {} : { scores };
|
|
1448
|
+
} catch {
|
|
1449
|
+
return {};
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function storedRoleConfidence(scores) {
|
|
1453
|
+
if (!scores) return void 0;
|
|
1454
|
+
try {
|
|
1455
|
+
const c = JSON.parse(scores)?.role?.confidence;
|
|
1456
|
+
if (typeof c === "number" && Number.isFinite(c)) return c;
|
|
1457
|
+
} catch {
|
|
1458
|
+
return void 0;
|
|
1459
|
+
}
|
|
1460
|
+
return briefUnitsOf(scores)?.roleConfidence;
|
|
1461
|
+
}
|
|
1462
|
+
function findTask(ctx, db, runId, taskId) {
|
|
1463
|
+
const t = db.swarm.taskGet(taskId);
|
|
1464
|
+
if (t && t.run_id === runId) return { db, task: t };
|
|
1465
|
+
const main = coordinationRoot(ctx.root);
|
|
1466
|
+
if (main === ctx.root) return void 0;
|
|
1467
|
+
const mdb = getStorage(getContext(main), { vectors: false });
|
|
1468
|
+
const m = mdb.swarm.taskGet(taskId);
|
|
1469
|
+
return m && m.run_id === runId ? { db: mdb, task: m } : void 0;
|
|
1470
|
+
}
|
|
1471
|
+
async function onPreAgent(ctx, input, spawn, deps = {}) {
|
|
1472
|
+
const cfg = ctx.config.adjudicator;
|
|
1473
|
+
if (!active(cfg, input) || !cfg.surfaces.agent) return void 0;
|
|
1474
|
+
if (!AGENT_TOOLS.has(input.tool_name ?? "")) return void 0;
|
|
1475
|
+
const ti = input.tool_input ?? {};
|
|
1476
|
+
const env = deps.env ?? process.env;
|
|
1477
|
+
const now = (deps.now ?? Date.now)();
|
|
1478
|
+
const worker = spawn?.kind === "worker" ? spawn : void 0;
|
|
1479
|
+
const agentType = str(ti.subagent_type) ?? "general-purpose";
|
|
1480
|
+
let db = getStorage(ctx, { vectors: false });
|
|
1481
|
+
let task;
|
|
1482
|
+
if (worker?.runId && typeof worker.taskId === "number") {
|
|
1483
|
+
try {
|
|
1484
|
+
const found = findTask(ctx, db, worker.runId, worker.taskId);
|
|
1485
|
+
if (found) ({ db, task } = found);
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const text = task ? `${task.title}
|
|
1490
|
+
${task.detail ?? ""}` : `${str(ti.description) ?? ""}
|
|
1491
|
+
${str(ti.prompt) ?? ""}`;
|
|
1492
|
+
if (!text.trim()) return void 0;
|
|
1493
|
+
let plan;
|
|
1494
|
+
if (task && worker?.runId) {
|
|
1495
|
+
try {
|
|
1496
|
+
plan = db.adjudicator.forTask(worker.runId, task.id).filter((r) => r.surface === "plan").pop();
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const explicit = str(ti.model);
|
|
1501
|
+
const pin = resolveAgentPin(agentType, { root: ctx.root, cwd: input.cwd, env });
|
|
1502
|
+
const pinned = pin.status === "pinned" ? pin.model : void 0;
|
|
1503
|
+
const pinnedRespected = cfg.apply.respectFrontmatter && isPinned(pinned);
|
|
1504
|
+
const pinUnknown = cfg.apply.respectFrontmatter && pin.status === "unknown";
|
|
1505
|
+
const isForced = subagentModelForced(env);
|
|
1506
|
+
const fork = isFork(agentType);
|
|
1507
|
+
const effortSet = !!str(ti.effort) || effortForced(env);
|
|
1508
|
+
const ranged = cfg.apply.respectFrontmatter && !fork && !isForced && pin.status === "ranged" && pin.range && isPinned(pin.model) ? { floor: pin.range.floor, ceiling: pin.range.ceiling, fallback: pin.model } : void 0;
|
|
1509
|
+
const facts = { fork, forced: isForced, pin: pinnedRespected || pinUnknown, effortSet };
|
|
1510
|
+
const before = dispatchRule({ ...facts, ranged: !!ranged });
|
|
1511
|
+
const applyMode = cfg.mode === "apply";
|
|
1512
|
+
const inScope = cfg.apply.scope === "all" || !!worker;
|
|
1513
|
+
const mayApply = applyMode && inScope && (before.model === "pass" || before.model === "ranged") && !explicit;
|
|
1514
|
+
const sessionId = input.session_id && input.session_id !== "unknown" ? input.session_id : void 0;
|
|
1515
|
+
const subagentEnv = before.model === "fork" || before.model === "forced" || pin.inherit ? {} : { CLAUDE_CODE_SUBAGENT_MODEL: env.CLAUDE_CODE_SUBAGENT_MODEL };
|
|
1516
|
+
const baselinePin = pin.status === "ranged" ? pin.model : pinned;
|
|
1517
|
+
const baseline = resolveBaseline(db, {
|
|
1518
|
+
pinned: before.model === "fork" ? void 0 : before.model === "forced" ? str(env.CLAUDE_CODE_SUBAGENT_MODEL) : baselinePin,
|
|
1519
|
+
fallbackPin: ranged?.fallback,
|
|
1520
|
+
env: subagentEnv,
|
|
1521
|
+
sessionId,
|
|
1522
|
+
ts: now
|
|
1523
|
+
});
|
|
1524
|
+
const row = {
|
|
1525
|
+
modelRequested: explicit ?? null,
|
|
1526
|
+
baselineModel: baseline.model
|
|
1527
|
+
};
|
|
1528
|
+
if (sessionId) row.sessionId = sessionId;
|
|
1529
|
+
if (input.tool_use_id) row.toolUseId = input.tool_use_id;
|
|
1530
|
+
if (worker?.runId && task) {
|
|
1531
|
+
row.runId = worker.runId;
|
|
1532
|
+
row.taskId = task.id;
|
|
1533
|
+
}
|
|
1534
|
+
if (plan) {
|
|
1535
|
+
row.parentId = plan.id;
|
|
1536
|
+
for (const k of ["complexity", "tier", "effort", "tool", "role", "confidence"])
|
|
1537
|
+
if (plan[k] !== void 0 && plan[k] !== null) row[k] = plan[k];
|
|
1538
|
+
}
|
|
1539
|
+
const opts = engineOptions(db, cfg, deps);
|
|
1540
|
+
opts.call = mayApply && !plan ? "inline" : "never";
|
|
1541
|
+
opts.row = row;
|
|
1542
|
+
const req = { key: "agent", surface: "agent", text };
|
|
1543
|
+
const hints = {};
|
|
1544
|
+
if (task) {
|
|
1545
|
+
hints.files = task.files.length;
|
|
1546
|
+
hints.deps = task.deps.length;
|
|
1547
|
+
if (task.role) hints.role = task.role;
|
|
1548
|
+
}
|
|
1549
|
+
const libraries = docsLibraries(ctx);
|
|
1550
|
+
if (libraries) hints.libraries = libraries;
|
|
1551
|
+
if (Object.keys(hints).length) req.hints = hints;
|
|
1552
|
+
const a = await adjudicateOne(req, opts);
|
|
1553
|
+
const v = a.verdict;
|
|
1554
|
+
const tier = plan?.tier ?? v.tier.choice;
|
|
1555
|
+
const conf = plan?.confidence ?? v.tier.confidence;
|
|
1556
|
+
const c = Math.round(plan?.complexity ?? v.complexity);
|
|
1557
|
+
const effort = plan?.effort ?? v.effort;
|
|
1558
|
+
const role = plan?.role ?? v.role.choice;
|
|
1559
|
+
const clamped = clampTier(tier, cfg.apply.floor, cfg.apply.ceiling);
|
|
1560
|
+
const units = plan ? briefUnitsOf(plan.scores) : a.units;
|
|
1561
|
+
const saturated = !!units?.saturated;
|
|
1562
|
+
const roleConf = (plan ? storedRoleConfidence(plan.scores) : void 0) ?? v.role.confidence;
|
|
1563
|
+
const pick = ranged ? rangedTier(tier, ranged, cfg.apply.floor, cfg.apply.ceiling) : void 0;
|
|
1564
|
+
const moves = !!ranged && rangedPasses(pick, ranged.fallback);
|
|
1565
|
+
const rule = dispatchRule({
|
|
1566
|
+
...facts,
|
|
1567
|
+
ranged: moves,
|
|
1568
|
+
pin: facts.pin || !!ranged && !moves,
|
|
1569
|
+
saturated
|
|
1570
|
+
});
|
|
1571
|
+
let action;
|
|
1572
|
+
let model;
|
|
1573
|
+
let modelFinal;
|
|
1574
|
+
if (!applyMode || !inScope) {
|
|
1575
|
+
action = "advised";
|
|
1576
|
+
modelFinal = explicit ?? null;
|
|
1577
|
+
} else if (fork) {
|
|
1578
|
+
action = "respected";
|
|
1579
|
+
modelFinal = null;
|
|
1580
|
+
} else if (isForced || explicit || pinnedRespected) {
|
|
1581
|
+
action = "respected";
|
|
1582
|
+
modelFinal = isForced ? str(env.CLAUDE_CODE_SUBAGENT_MODEL) ?? null : explicit ?? (isPinned(pinned) ? pinned : null);
|
|
1583
|
+
} else if (ranged) {
|
|
1584
|
+
if (moves && pick && conf >= cfg.apply.minConfidence) {
|
|
1585
|
+
action = "applied";
|
|
1586
|
+
model = pick;
|
|
1587
|
+
modelFinal = pick;
|
|
1588
|
+
} else {
|
|
1589
|
+
action = moves ? "advised" : "respected";
|
|
1590
|
+
modelFinal = ranged.fallback;
|
|
1591
|
+
}
|
|
1592
|
+
} else if (clamped && conf >= cfg.apply.minConfidence && !pinUnknown) {
|
|
1593
|
+
action = "applied";
|
|
1594
|
+
model = clamped;
|
|
1595
|
+
modelFinal = clamped;
|
|
1596
|
+
} else {
|
|
1597
|
+
action = "advised";
|
|
1598
|
+
modelFinal = null;
|
|
1599
|
+
}
|
|
1600
|
+
const kept = model ? void 0 : keptModelOf(rule);
|
|
1601
|
+
const pinDeferred = kept === "pin";
|
|
1602
|
+
const advised = ranged ? pick : clamped;
|
|
1603
|
+
if (a.rowId !== void 0) {
|
|
1604
|
+
try {
|
|
1605
|
+
db.adjudicator.update(a.rowId, {
|
|
1606
|
+
action,
|
|
1607
|
+
modelFinal,
|
|
1608
|
+
// A ranged dispatch is flagged with its fallback, the savings baseline. Every dispatch
|
|
1609
|
+
// stores the tier its note names (the ranged pick, else the verdict clamped to the apply
|
|
1610
|
+
// bounds), the model follow detection compares the call with (savings.ts advisedPick).
|
|
1611
|
+
...withPin(db, a.rowId, pin, kept, {
|
|
1612
|
+
...ranged ? { [RANGED_FLAG]: true, [FALLBACK_PIN_FIELD]: ranged.fallback } : {},
|
|
1613
|
+
...advised ? { [ADVISED_PICK_FIELD]: advised } : {}
|
|
1614
|
+
})
|
|
1615
|
+
});
|
|
1616
|
+
} catch {
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
const why = `complexity ${c}, conf ${conf.toFixed(2)}`;
|
|
1620
|
+
if (rule.model === "fork") {
|
|
1621
|
+
const note2 = `adjudicator: fork runs on the main session's model; no \`model\` or \`effort\` to pass (${why})`;
|
|
1622
|
+
const out2 = { action, note: clip(note2, ADVICE_MAX) };
|
|
1623
|
+
if (a.rowId !== void 0) out2.rowId = a.rowId;
|
|
1624
|
+
return out2;
|
|
1625
|
+
}
|
|
1626
|
+
const effortHint = rule.effort === "pass" ? `pass effort:'${effort}' (Claude Code ${AGENT_EFFORT_SINCE}+)` : saturated && !effortSet ? `effort ${effort}: informational (brief saturated)` : `suggested effort ${effort}`;
|
|
1627
|
+
const extras = [effortHint];
|
|
1628
|
+
if (role && role !== "none" && roleConf >= ROLE_NOTE_MIN) extras.push(`role ${role}`);
|
|
1629
|
+
if (applyMode && inScope && pinUnknown && !isForced && !explicit)
|
|
1630
|
+
extras.push("not applied: the agent's model pin is unknown");
|
|
1631
|
+
const span = ranged ? `range ${ranged.floor}\u2013${ranged.ceiling}, fallback ${ranged.fallback}; ` : "";
|
|
1632
|
+
let note;
|
|
1633
|
+
if (action === "applied") {
|
|
1634
|
+
note = `adjudicator: model ${model} applied (${span}${why}); ${extras.join(", ")}`;
|
|
1635
|
+
} else if (rule.model === "forced") {
|
|
1636
|
+
note = `adjudicator: CLAUDE_CODE_SUBAGENT_MODEL_FORCE sets the model${clamped ? ` (adjudicator would pick ${clamped}; ${why})` : ` (${why})`}; ${extras.join(", ")}`;
|
|
1637
|
+
} else if (rule.model === "ranged" && !explicit) {
|
|
1638
|
+
note = `adjudicator: model ${pick} would do (${span}${why}): pass model:'${pick}'; ${extras.join(", ")}`;
|
|
1639
|
+
} else if (ranged && pinDeferred && !explicit) {
|
|
1640
|
+
note = `adjudicator: ${rangedPart(ranged, pick)}; ${extras.join(", ")}`;
|
|
1641
|
+
} else if (pinDeferred && !explicit) {
|
|
1642
|
+
const part = pinnedPart(
|
|
1643
|
+
{ status: pinUnknown ? "unknown" : "pinned", model: pinned, source: pin.source },
|
|
1644
|
+
clamped,
|
|
1645
|
+
worker && pin.source === "project" ? void 0 : `remove the model pin in ${pin.file ?? "the agent's definition"}`
|
|
1646
|
+
);
|
|
1647
|
+
note = `adjudicator: ${part}; ${extras.filter((e) => !e.startsWith("not applied")).join(", ")}`;
|
|
1648
|
+
} else if (action === "advised" && !explicit && clamped && clamped !== (tierOf(baseline.model) ?? baseline.model)) {
|
|
1649
|
+
note = `adjudicator: model ${clamped} would do (${why}): pass model:'${clamped}'; ${extras.join(", ")}`;
|
|
1650
|
+
} else {
|
|
1651
|
+
note = `adjudicator: ${extras.join(", ")}`;
|
|
1652
|
+
}
|
|
1653
|
+
const out = { action, note: clip(note, ADVICE_MAX) };
|
|
1654
|
+
if (model) out.model = model;
|
|
1655
|
+
if (a.rowId !== void 0) out.rowId = a.rowId;
|
|
1656
|
+
return out;
|
|
1657
|
+
}
|
|
1658
|
+
var POST_AGENT_LOOKBACK_MS = 24 * 36e5;
|
|
1659
|
+
function agentLinkOf(input) {
|
|
1660
|
+
const r = input.tool_response;
|
|
1661
|
+
const o = r && typeof r === "object" && !Array.isArray(r) ? r : {};
|
|
1662
|
+
const resolvedModel = str(o.resolvedModel);
|
|
1663
|
+
const agentId = str(o.agentId) ?? agentFromMeta(input.transcript_path, input.tool_use_id);
|
|
1664
|
+
if (!agentId) return void 0;
|
|
1665
|
+
return resolvedModel ? { agentId, resolvedModel } : { agentId };
|
|
1666
|
+
}
|
|
1667
|
+
function agentFromMeta(transcriptPath, toolUseId) {
|
|
1668
|
+
if (!transcriptPath || !toolUseId) return void 0;
|
|
1669
|
+
const dir = join2(dirname2(transcriptPath), basename2(transcriptPath, ".jsonl"), "subagents");
|
|
1670
|
+
let names;
|
|
1671
|
+
try {
|
|
1672
|
+
names = readdirSync2(dir).filter((n) => /^agent-[A-Za-z0-9_-]+\.meta\.json$/.test(n));
|
|
1673
|
+
} catch {
|
|
1674
|
+
return void 0;
|
|
1675
|
+
}
|
|
1676
|
+
for (const n of names) {
|
|
1677
|
+
try {
|
|
1678
|
+
const m = JSON.parse(readFileSync2(join2(dir, n), "utf8"));
|
|
1679
|
+
if (m?.toolUseId === toolUseId) return n.slice("agent-".length, -".meta.json".length);
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return void 0;
|
|
1684
|
+
}
|
|
1685
|
+
function onPostAgent(ctx, db, input, deps = {}) {
|
|
1686
|
+
try {
|
|
1687
|
+
const cfg = ctx.config.adjudicator;
|
|
1688
|
+
if (!active(cfg, input) || !cfg.surfaces.agent) return void 0;
|
|
1689
|
+
if (!AGENT_TOOLS.has(input.tool_name ?? "") || !input.tool_use_id) return void 0;
|
|
1690
|
+
const link = agentLinkOf(input);
|
|
1691
|
+
if (!link) return void 0;
|
|
1692
|
+
const since = (deps.now ?? Date.now)() - POST_AGENT_LOOKBACK_MS;
|
|
1693
|
+
const dbs = [db];
|
|
1694
|
+
const main = coordinationRoot(ctx.root);
|
|
1695
|
+
if (main !== ctx.root) dbs.push(getStorage(getContext(main), { vectors: false }));
|
|
1696
|
+
for (const d of dbs) {
|
|
1697
|
+
const row = d.adjudicator.byToolUseId(input.tool_use_id, { surface: "agent", since });
|
|
1698
|
+
if (!row) continue;
|
|
1699
|
+
if (row.agentId) {
|
|
1700
|
+
if (row.agentId !== link.agentId)
|
|
1701
|
+
log(
|
|
1702
|
+
`adjudicator: row ${row.id} keeps agent ${row.agentId}, dispatch says ${link.agentId}`
|
|
1703
|
+
);
|
|
1704
|
+
return void 0;
|
|
1705
|
+
}
|
|
1706
|
+
const patch = { agentId: link.agentId };
|
|
1707
|
+
if (link.resolvedModel && !row.actualModel) patch.actualModel = link.resolvedModel;
|
|
1708
|
+
d.adjudicator.update(row.id, patch);
|
|
1709
|
+
return row.id;
|
|
1710
|
+
}
|
|
1711
|
+
return void 0;
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
log(`adjudicator: onPostAgent ${err?.message ?? String(err)}`);
|
|
1714
|
+
return void 0;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function onPreTool(ctx, input, out, deps = {}) {
|
|
1718
|
+
const cfg = ctx.config.adjudicator;
|
|
1719
|
+
if (!active(cfg, input) || !cfg.surfaces.tools) return void 0;
|
|
1720
|
+
if (!RAW_TOOLS.has(input.tool_name ?? "") || !input.session_id) return void 0;
|
|
1721
|
+
const h = out?.hookSpecificOutput;
|
|
1722
|
+
if (h?.additionalContext || h?.permissionDecision) return void 0;
|
|
1723
|
+
try {
|
|
1724
|
+
const db = getStorage(ctx, { vectors: false });
|
|
1725
|
+
const row = db.adjudicator.latestForSession(input.session_id, "prompt");
|
|
1726
|
+
if (!row || row.toolUseId || !row.tool?.startsWith("em_")) return void 0;
|
|
1727
|
+
if ((deps.now ?? Date.now)() - row.ts > HINT_WINDOW_MS) return void 0;
|
|
1728
|
+
db.adjudicator.update(row.id, { toolUseId: input.tool_use_id || "hinted" });
|
|
1729
|
+
const c = Math.round(row.complexity ?? 0);
|
|
1730
|
+
return clip(
|
|
1731
|
+
`eminiar adjudicator: this prompt was scored for ${row.tool} (complexity ${c}/4); ${input.tool_name} works, but ${row.tool} usually answers it with less context.`,
|
|
1732
|
+
ADVICE_MAX
|
|
1733
|
+
);
|
|
1734
|
+
} catch {
|
|
1735
|
+
return void 0;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function closeToolHint(db, sessionId) {
|
|
1739
|
+
try {
|
|
1740
|
+
const row = db.adjudicator.latestForSession(sessionId, "prompt");
|
|
1741
|
+
if (row && !row.toolUseId) db.adjudicator.update(row.id, { toolUseId: "superseded" });
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function onSessionStart(ctx, db, input, deps = {}) {
|
|
1746
|
+
try {
|
|
1747
|
+
const cfg = ctx.config.adjudicator;
|
|
1748
|
+
if (!active(cfg, input)) return void 0;
|
|
1749
|
+
const env = deps.env ?? process.env;
|
|
1750
|
+
const now = (deps.now ?? Date.now)();
|
|
1751
|
+
const r = resolveAdjudicator(cfg, { context: "hook", env });
|
|
1752
|
+
const issues = [];
|
|
1753
|
+
if (r.degraded === "no_key") issues.push(`no key for ${r.configured}`);
|
|
1754
|
+
else if (r.degraded === "no_consent")
|
|
1755
|
+
issues.push(`${r.configured} is remote and has no consent`);
|
|
1756
|
+
else if (r.degraded && r.degraded !== "hook_context")
|
|
1757
|
+
issues.push(
|
|
1758
|
+
`${r.configured}: ${String(r.degraded).replace(/_/g, " ")}${r.detail ? ` (${r.detail})` : ""}`
|
|
1759
|
+
);
|
|
1760
|
+
if (r.provider !== "heuristic") {
|
|
1761
|
+
const s = db.adjudicator.providerState(r.provider);
|
|
1762
|
+
if (s && s.state !== "closed" && s.retryAt !== null && s.retryAt > now)
|
|
1763
|
+
issues.push(`${r.provider} ${s.state === "slow" ? "is slow" : "breaker open"}`);
|
|
1764
|
+
}
|
|
1765
|
+
if (cfg.mode === "apply") {
|
|
1766
|
+
const sum = db.adjudicator.summary(now - 7 * 864e5);
|
|
1767
|
+
if (sum.applyIneffective > 0)
|
|
1768
|
+
issues.push(
|
|
1769
|
+
`apply did not take effect on ${sum.applyIneffective} of ${sum.applyChecked} dispatches: use advise`
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
if (r.provider === "openai-compatible" && r.preset === "ollama" && cfg.promptMode !== "heuristic")
|
|
1773
|
+
warmUp(ctx, cfg, r, deps);
|
|
1774
|
+
if (!issues.length) return void 0;
|
|
1775
|
+
return clip(
|
|
1776
|
+
`eminiar adjudicator: ${issues.join("; ")}; \`eminiar adjudicator status\` explains.`,
|
|
1777
|
+
DEGRADED_MAX
|
|
1778
|
+
);
|
|
1779
|
+
} catch {
|
|
1780
|
+
return void 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
function warmUp(ctx, cfg, r, deps) {
|
|
1784
|
+
const request = { key: "warmup", surface: "test", text: "warm up" };
|
|
1785
|
+
const job = {
|
|
1786
|
+
v: 1,
|
|
1787
|
+
projectRoot: ctx.root,
|
|
1788
|
+
request,
|
|
1789
|
+
// The same key the child computes (no hints or catalogues: just the config's ladder and weights).
|
|
1790
|
+
cacheKey: cacheKeyFor(request, r, { config: cfg }),
|
|
1791
|
+
config: cfg,
|
|
1792
|
+
context: "hook",
|
|
1793
|
+
budgetMs: WARMUP_BUDGET_MS
|
|
1794
|
+
};
|
|
1795
|
+
(deps.spawn ?? ((j) => spawnAsyncJob(j)))(job);
|
|
1796
|
+
}
|
|
1797
|
+
function onSessionEnd(ctx, db, input) {
|
|
1798
|
+
try {
|
|
1799
|
+
if (!active(ctx.config.adjudicator, input)) return;
|
|
1800
|
+
if (input.session_id) db.metaDelete(`adjudicator:main:${input.session_id}`);
|
|
1801
|
+
reconcile(db, { limit: 200, config: ctx.config });
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
log(`adjudicator: reconcile ${err?.message ?? String(err)}`);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
export {
|
|
1807
|
+
ADVICE_MAX,
|
|
1808
|
+
DEGRADED_MAX,
|
|
1809
|
+
HINT_WINDOW_MS,
|
|
1810
|
+
POST_AGENT_LOOKBACK_MS,
|
|
1811
|
+
WARMUP_BUDGET_MS,
|
|
1812
|
+
adviceLine,
|
|
1813
|
+
afterRouting,
|
|
1814
|
+
agentLinkOf,
|
|
1815
|
+
clampTier,
|
|
1816
|
+
isHarnessEnvelope,
|
|
1817
|
+
onPostAgent,
|
|
1818
|
+
onPreAgent,
|
|
1819
|
+
onPreTool,
|
|
1820
|
+
onPrompt,
|
|
1821
|
+
onSessionEnd,
|
|
1822
|
+
onSessionStart,
|
|
1823
|
+
stripLeadingReminders,
|
|
1824
|
+
withModel,
|
|
1825
|
+
withNote
|
|
1826
|
+
};
|
|
1827
|
+
//# sourceMappingURL=hook-hooks-U6GDOQSY.js.map
|