dsh-continual-evolve 0.1.1 → 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/README.md +172 -17
- package/README.zh.md +87 -10
- package/lib/apply.js +3 -1
- package/lib/approval.d.ts +25 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +99 -5
- package/lib/auto.js +165 -6
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +331 -0
- package/lib/benchmark.d.ts +84 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.js +33 -221
- package/lib/evaluate.d.ts +43 -7
- package/lib/evaluate.js +172 -43
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +128 -0
- package/lib/fate.js +342 -0
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -21
- package/lib/index.js +32 -2
- package/lib/inject.d.ts +8 -0
- package/lib/inject.js +51 -4
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/mount.js +5 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +8 -1
- package/lib/planner.js +40 -39
- package/lib/render.d.ts +1 -3
- package/lib/render.js +2 -5
- package/lib/review.d.ts +5 -2
- package/lib/review.js +27 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +37 -4
- package/lib/score.js +120 -10
- package/lib/service.d.ts +2 -2
- package/lib/service.js +5 -2
- package/lib/skill-render.d.ts +15 -0
- package/lib/skill-render.js +30 -0
- package/lib/skill.d.ts +12 -7
- package/lib/skill.js +36 -31
- package/lib/skillquality.d.ts +80 -0
- package/lib/skillquality.js +311 -0
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +28 -4
- package/lib/types.d.ts +39 -0
- package/lib/types.js +19 -0
- package/lib/usage.d.ts +32 -0
- package/lib/usage.js +84 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +51 -2
- package/lib/wrapup-command.d.ts +8 -0
- package/lib/wrapup-command.js +211 -0
- package/lib/wrapup.d.ts +215 -0
- package/lib/wrapup.js +427 -0
- package/package.json +8 -8
package/lib/command.js
CHANGED
|
@@ -2,25 +2,29 @@ import { ARCHIVED_AT_KEY } from "./types.js";
|
|
|
2
2
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
3
|
import { planWithLlm } from "./planner.js";
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
import { requireGlobalApproval } from "./approval.js";
|
|
6
7
|
import { saveHarnessState } from "./state.js";
|
|
7
|
-
import { loadLedger, mountSkill, unmountSkill } from "./mount.js";
|
|
8
|
-
import { blockEvolutionGoal, completeEvolutionGoal, goalServiceOf, goalStatusText, upsertEvolutionGoal } from "./goal.js";
|
|
9
8
|
import { appendResult, storePaths } from "./store.js";
|
|
10
|
-
import { addCase, createBenchmark, listBenchmarks, listCases, loadBenchmark, loadScoreboard, rollbackRejectedCandidate, saveScoreboard } from "./benchmark.js";
|
|
11
|
-
import { decide, decisionReport, entryFromCells } from "./score.js";
|
|
12
|
-
import { evaluateState } from "./evaluate.js";
|
|
13
9
|
import { entrySourceOf } from "./source.js";
|
|
14
10
|
import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
|
|
11
|
+
import { readBenchmarkFailures, readReviewFailures, summarizeFailures, formatFailureSummary } from "./failures.js";
|
|
12
|
+
import { executeGoalCommand } from "./goal-command.js";
|
|
13
|
+
import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
|
|
14
|
+
import { executeBenchmarkCommand } from "./benchmark-command.js";
|
|
15
|
+
import { executeWrapupCommand } from "./wrapup-command.js";
|
|
15
16
|
const USAGE = `Usage:
|
|
16
17
|
/evolve show this help and the current local store
|
|
17
18
|
/evolve list [global] list entries (add "global" for the cross-session store)
|
|
18
19
|
/evolve history [global] show applied refinements (rollback ids)
|
|
19
20
|
/evolve rollback <id> [global] deterministically revert a refinement
|
|
20
21
|
/evolve plan [msg] run the LLM planner against the current store
|
|
22
|
+
/evolve wrapup assess this session's local entries: promote reusable ones
|
|
23
|
+
to the global store (approval required), archive one-offs
|
|
21
24
|
/evolve archive <id> [global] hide an entry from injection (data kept, restorable)
|
|
22
25
|
/evolve unarchive <id> [global] restore an archived entry
|
|
23
26
|
/evolve log [tail N] show the recent plugin log (default 50 lines)
|
|
27
|
+
/evolve failures aggregated failure counts (gate + benchmark, by class)
|
|
24
28
|
/evolve export [global] <path> backup a store to a JSON file
|
|
25
29
|
/evolve import [global] <path> restore a store from an export file
|
|
26
30
|
/evolve mount <skillId> hot-mount a skill entry as a live cordis plugin
|
|
@@ -163,6 +167,22 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
163
167
|
}, { scope });
|
|
164
168
|
return success(renderResult(result));
|
|
165
169
|
}
|
|
170
|
+
case "failures": {
|
|
171
|
+
// /evolve failures — failure-signature aggregation (D1 observation):
|
|
172
|
+
// failed review-gate records + failed benchmark cells, counted by class.
|
|
173
|
+
const failed = [...readReviewFailures(engine.baseDir), ...readBenchmarkFailures(engine.baseDir)];
|
|
174
|
+
const summary = summarizeFailures(failed);
|
|
175
|
+
const parts = formatFailureSummary(summary).split("\n");
|
|
176
|
+
const recent = failed
|
|
177
|
+
.sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))
|
|
178
|
+
.slice(0, 10)
|
|
179
|
+
.map((f) => ` [${f.timestamp ?? "(benchmark)"}] ${f.kind} · ${f.source}: ${f.message.slice(0, 140)}`);
|
|
180
|
+
if (recent.length > 0) {
|
|
181
|
+
parts.push("recent 10:");
|
|
182
|
+
parts.push(...recent);
|
|
183
|
+
}
|
|
184
|
+
return success(parts.join("\n"));
|
|
185
|
+
}
|
|
166
186
|
case "log": {
|
|
167
187
|
// /evolve log [tail N] [session <sessionId>]
|
|
168
188
|
let tail = 50;
|
|
@@ -255,6 +275,8 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
255
275
|
...(instructions ? { instructions } : {}),
|
|
256
276
|
global: scope === "global",
|
|
257
277
|
signal: invocation.signal,
|
|
278
|
+
// skill-creator template facts (fallback: builtin guide).
|
|
279
|
+
skillsRoot: join(engine.baseDir, "skills"),
|
|
258
280
|
});
|
|
259
281
|
if (scope === "global" && opts.requireGlobalApproval && proposal.edits.length > 0) {
|
|
260
282
|
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan global 将应用 ${proposal.edits.length} 条编辑到跨会话 store:${proposal.summary}`);
|
|
@@ -266,22 +288,20 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
266
288
|
});
|
|
267
289
|
return success(renderResult(result));
|
|
268
290
|
}
|
|
291
|
+
case "wrapup": {
|
|
292
|
+
return await executeWrapupCommand(ctx, engine, invocation);
|
|
293
|
+
}
|
|
269
294
|
case "goal": {
|
|
270
295
|
return executeGoalCommand(ctx, invocation, rest);
|
|
271
296
|
}
|
|
272
297
|
case "mount": {
|
|
273
|
-
return executeMountCommand(ctx, engine, invocation, rest);
|
|
298
|
+
return await executeMountCommand(ctx, engine, invocation, rest);
|
|
274
299
|
}
|
|
275
300
|
case "unmount": {
|
|
276
|
-
|
|
277
|
-
if (!id) {
|
|
278
|
-
return error(`unmount requires a mount id (see /evolve mount list).`);
|
|
279
|
-
}
|
|
280
|
-
const record = await unmountSkill(ctx, engine.baseDir, id);
|
|
281
|
-
return record ? success(`unmounted ${record.id} (${record.entryId})`) : error(`no mount found for ${id}`);
|
|
301
|
+
return await executeUnmountCommand(ctx, engine, rest);
|
|
282
302
|
}
|
|
283
303
|
case "benchmark": {
|
|
284
|
-
return executeBenchmarkCommand(ctx, engine, invocation, rest, runtime);
|
|
304
|
+
return await executeBenchmarkCommand(ctx, engine, invocation, rest, runtime);
|
|
285
305
|
}
|
|
286
306
|
default:
|
|
287
307
|
return error(`unknown subcommand: ${sub}\n${USAGE}`);
|
|
@@ -291,214 +311,6 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
291
311
|
return error(cause instanceof Error ? cause.message : String(cause));
|
|
292
312
|
}
|
|
293
313
|
}
|
|
294
|
-
function executeGoalCommand(ctx, invocation, rest) {
|
|
295
|
-
const agent = invocation.agent;
|
|
296
|
-
const goals = goalServiceOf(ctx);
|
|
297
|
-
if (!goals) {
|
|
298
|
-
return error(`/evolve goal requires the goals service (load @deepseek-ai/dsh-goal)`);
|
|
299
|
-
}
|
|
300
|
-
const sub = rest[0] ?? "";
|
|
301
|
-
try {
|
|
302
|
-
if (sub === "done") {
|
|
303
|
-
const view = completeEvolutionGoal(ctx, agent);
|
|
304
|
-
return view ? success(`evolution goal completed: ${goalStatusText(view)}`) : success("(no goal to complete)");
|
|
305
|
-
}
|
|
306
|
-
if (sub === "block") {
|
|
307
|
-
const reason = rest.slice(1).join(" ") || "user requested block";
|
|
308
|
-
const view = blockEvolutionGoal(ctx, agent, reason);
|
|
309
|
-
return view ? success(`evolution goal blocked: ${goalStatusText(view)}`) : success("(no active goal to block)");
|
|
310
|
-
}
|
|
311
|
-
if (sub.length === 0) {
|
|
312
|
-
const current = goals.get(agent);
|
|
313
|
-
return current ? success(goalStatusText(current)) : success("(no evolution goal — /evolve goal <objective> to create one)");
|
|
314
|
-
}
|
|
315
|
-
const objective = rest.join(" ");
|
|
316
|
-
const view = upsertEvolutionGoal(ctx, agent, objective);
|
|
317
|
-
return success(`evolution goal ready: ${goalStatusText(view)}\n(active goal drives the review gate every round)`);
|
|
318
|
-
}
|
|
319
|
-
catch (cause) {
|
|
320
|
-
return error(cause instanceof Error ? cause.message : String(cause));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
async function executeMountCommand(ctx, engine, invocation, rest) {
|
|
324
|
-
const sub = rest[0] ?? "";
|
|
325
|
-
if (sub === "list") {
|
|
326
|
-
const ledger = loadLedger(engine.baseDir);
|
|
327
|
-
if (ledger.mounted.length === 0) {
|
|
328
|
-
return success("(no hot-mounted plugins — /evolve mount <skillId>)");
|
|
329
|
-
}
|
|
330
|
-
return success(ledger.mounted.map((m) => `- ${m.id} (${m.entryId}, v${m.version}, ${m.mountedAt})`).join("\n"));
|
|
331
|
-
}
|
|
332
|
-
const skillId = stripAngleBrackets(sub);
|
|
333
|
-
if (!skillId) {
|
|
334
|
-
return error(`mount requires a skill entry id.\nUsage: /evolve mount <skillId> | /evolve mount list`);
|
|
335
|
-
}
|
|
336
|
-
const sessionId = invocation.agent.id;
|
|
337
|
-
const local = engine.load("local", sessionId);
|
|
338
|
-
const globalState = engine.load("global", undefined);
|
|
339
|
-
const entry = local.entries.skill[skillId] ??
|
|
340
|
-
globalState.entries.skill[skillId] ??
|
|
341
|
-
Object.values(local.entries.skill).find((e) => e.id === skillId) ??
|
|
342
|
-
Object.values(globalState.entries.skill).find((e) => e.id === skillId);
|
|
343
|
-
if (!entry) {
|
|
344
|
-
return error(`skill entry ${skillId} not found in local or global store`);
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
const record = await mountSkill(ctx, engine.baseDir, entry);
|
|
348
|
-
return success(`mounted ${record.id} as ${record.entryId} (v${record.version}) — tool: skill_${record.id.replace(/_/g, "-")}`);
|
|
349
|
-
}
|
|
350
|
-
catch (cause) {
|
|
351
|
-
return error(cause instanceof Error ? cause.message : String(cause));
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
async function executeBenchmarkCommand(ctx, engine, invocation, rest, runtime) {
|
|
355
|
-
const sub = rest[0] ?? "";
|
|
356
|
-
const args = rest.slice(1);
|
|
357
|
-
const sessionId = invocation.agent.id;
|
|
358
|
-
const baseDir = engine.baseDir;
|
|
359
|
-
switch (sub) {
|
|
360
|
-
case "":
|
|
361
|
-
case "help":
|
|
362
|
-
return success(BENCHMARK_USAGE);
|
|
363
|
-
case "new": {
|
|
364
|
-
const title = args[0] ?? "";
|
|
365
|
-
if (!title) {
|
|
366
|
-
return error(`benchmark new requires a title.\n${BENCHMARK_USAGE}`);
|
|
367
|
-
}
|
|
368
|
-
const runs = args[1] !== undefined ? parsePositiveInt(args[1], "runs") : undefined;
|
|
369
|
-
const definition = createBenchmark(baseDir, { title, ...(runs !== undefined ? { runs } : {}) });
|
|
370
|
-
return success(`benchmark ${definition.id} created (runs=${definition.runs}, passThreshold=${definition.passThreshold})\nAdd cases with: /evolve benchmark add-case ${definition.id} "<title>" "<statement>" "<rubric>"`);
|
|
371
|
-
}
|
|
372
|
-
case "list": {
|
|
373
|
-
const benchmarks = listBenchmarks(baseDir);
|
|
374
|
-
if (benchmarks.length === 0) {
|
|
375
|
-
return success("(no benchmarks yet — use /evolve benchmark new <title>)");
|
|
376
|
-
}
|
|
377
|
-
const lines = benchmarks.map((b) => {
|
|
378
|
-
const cases = listCases(baseDir, b.id);
|
|
379
|
-
const board = loadScoreboard(baseDir, b.id);
|
|
380
|
-
const ref = board.reference ? ` ref=${board.reference.overall ?? "?"}` : " no-reference";
|
|
381
|
-
return `- ${b.id} (${cases.length} cases, runs=${b.runs})${ref}`;
|
|
382
|
-
});
|
|
383
|
-
return success(lines.join("\n"));
|
|
384
|
-
}
|
|
385
|
-
case "add-case": {
|
|
386
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
387
|
-
const title = args[1] ?? "";
|
|
388
|
-
const statement = args[2] ?? "";
|
|
389
|
-
const rubric = args[3] ?? "";
|
|
390
|
-
if (!bid || !title || !statement || !rubric) {
|
|
391
|
-
return error(`benchmark add-case needs <bid> <title> <statement> <rubric>.\n${BENCHMARK_USAGE}`);
|
|
392
|
-
}
|
|
393
|
-
const caseItem = addCase(baseDir, bid, title, statement, rubric, runtime.rubricKey);
|
|
394
|
-
return success(`case ${caseItem.id} added to ${bid}`);
|
|
395
|
-
}
|
|
396
|
-
case "reset": {
|
|
397
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
398
|
-
if (!bid) {
|
|
399
|
-
return error(`benchmark reset needs a <bid>.\n${BENCHMARK_USAGE}`);
|
|
400
|
-
}
|
|
401
|
-
if (!loadBenchmark(baseDir, bid)) {
|
|
402
|
-
return error(`benchmark ${bid} not found`);
|
|
403
|
-
}
|
|
404
|
-
saveScoreboard(baseDir, bid, { candidates: [], decisions: [] });
|
|
405
|
-
return success(`scoreboard reset for ${bid} — run /evolve benchmark run ${bid} to record a fresh reference`);
|
|
406
|
-
}
|
|
407
|
-
case "status": {
|
|
408
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
409
|
-
const board = loadScoreboard(baseDir, bid);
|
|
410
|
-
const lines = [];
|
|
411
|
-
if (board.reference) {
|
|
412
|
-
lines.push(`reference "${board.reference.label}": overall=${board.reference.overall ?? "?"} cells=${board.reference.cells.length}`);
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
lines.push("(no reference evaluation yet)");
|
|
416
|
-
}
|
|
417
|
-
for (const c of board.candidates) {
|
|
418
|
-
lines.push(`candidate "${c.label}": overall=${c.overall ?? "?"} cells=${c.cells.length}${c.refinementId ? ` (${c.refinementId})` : ""}`);
|
|
419
|
-
}
|
|
420
|
-
for (const d of board.decisions) {
|
|
421
|
-
lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
|
|
422
|
-
}
|
|
423
|
-
return success(lines.join("\n") || "(empty scoreboard)");
|
|
424
|
-
}
|
|
425
|
-
case "run": {
|
|
426
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
427
|
-
const candidateId = args.includes("candidate") ? stripAngleBrackets(args[args.indexOf("candidate") + 1] ?? "") : undefined;
|
|
428
|
-
const definition = loadBenchmark(baseDir, bid);
|
|
429
|
-
if (!definition) {
|
|
430
|
-
return error(`benchmark ${bid} not found`);
|
|
431
|
-
}
|
|
432
|
-
const cases = listCases(baseDir, bid);
|
|
433
|
-
if (cases.length === 0) {
|
|
434
|
-
return error(`benchmark ${bid} has no cases — use /evolve benchmark add-case`);
|
|
435
|
-
}
|
|
436
|
-
const board = loadScoreboard(baseDir, bid);
|
|
437
|
-
const label = candidateId ? `candidate:${candidateId}` : "reference";
|
|
438
|
-
if (!candidateId && board.reference) {
|
|
439
|
-
return error(`reference already evaluated (${board.reference.overall ?? "?"}); evaluate a candidate instead: /evolve benchmark run ${bid} candidate <refinementId>`);
|
|
440
|
-
}
|
|
441
|
-
const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
|
|
442
|
-
const outcome = await evaluateState(ctx, invocation.agent, {
|
|
443
|
-
cases,
|
|
444
|
-
rubricKey: runtime.rubricKey,
|
|
445
|
-
runs: definition.runs,
|
|
446
|
-
passThreshold: definition.passThreshold,
|
|
447
|
-
harnessOverview: overview,
|
|
448
|
-
label,
|
|
449
|
-
signal: invocation.signal,
|
|
450
|
-
});
|
|
451
|
-
const entry = entryFromCells(label, outcome.cells, candidateId);
|
|
452
|
-
const lines = [
|
|
453
|
-
`evaluation "${label}": ${outcome.cells.length} cells, overall=${entry.overall ?? "?"}`,
|
|
454
|
-
...Object.entries(entry.aggregate)
|
|
455
|
-
.filter(([key]) => key !== "overall")
|
|
456
|
-
.map(([key, value]) => ` ${key}: ${value ?? "?"}`),
|
|
457
|
-
];
|
|
458
|
-
if (candidateId) {
|
|
459
|
-
if (!board.reference) {
|
|
460
|
-
lines.push("(no reference yet — this run only recorded the candidate)");
|
|
461
|
-
board.candidates.push(entry);
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
const decision = decide(board.reference, entry, { passThreshold: definition.passThreshold, regressionTolerance: 0 });
|
|
465
|
-
board.candidates.push(entry);
|
|
466
|
-
board.decisions.push({
|
|
467
|
-
candidateLabel: label,
|
|
468
|
-
refinementId: candidateId,
|
|
469
|
-
accepted: decision.accepted,
|
|
470
|
-
reasons: decision.reasons,
|
|
471
|
-
createdAt: new Date().toISOString(),
|
|
472
|
-
});
|
|
473
|
-
lines.push(...decisionReport(board.reference, entry, decision));
|
|
474
|
-
if (!decision.accepted) {
|
|
475
|
-
lines.push(`Consider rolling back the candidate: /evolve rollback <${candidateId}>`);
|
|
476
|
-
if (runtime.autoRollbackOnReject) {
|
|
477
|
-
const outcome = rollbackRejectedCandidate(engine, sessionId, candidateId);
|
|
478
|
-
lines.push(outcome.message);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
board.reference = entry;
|
|
485
|
-
lines.push("reference evaluation recorded as the baseline");
|
|
486
|
-
}
|
|
487
|
-
saveScoreboard(baseDir, bid, board);
|
|
488
|
-
return success(lines.join("\n"));
|
|
489
|
-
}
|
|
490
|
-
default:
|
|
491
|
-
return error(`unknown benchmark subcommand: ${sub}\n${BENCHMARK_USAGE}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
const BENCHMARK_USAGE = `Usage:
|
|
495
|
-
/evolve benchmark new <title> create a benchmark (runs=1)
|
|
496
|
-
/evolve benchmark add-case <bid> <title> <statement> <rubric>
|
|
497
|
-
/evolve benchmark list list benchmarks + reference status
|
|
498
|
-
/evolve benchmark status <bid> show scoreboard + decisions
|
|
499
|
-
/evolve benchmark reset <bid> clear the scoreboard (fresh reference)
|
|
500
|
-
/evolve benchmark run <bid> evaluate current state as the reference
|
|
501
|
-
/evolve benchmark run <bid> candidate <refinementId> evaluate the post-refinement state and decide`;
|
|
502
314
|
function renderResult(result) {
|
|
503
315
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
504
316
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
package/lib/evaluate.d.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The evaluation matrix runner: executes every case × run as a
|
|
3
|
-
* structured-output subagent, with the provider/model frozen to the
|
|
4
|
-
* agent's own route.
|
|
5
|
-
*
|
|
2
|
+
* The evaluation matrix runner: executes every case × run as a TWO-STAGE
|
|
3
|
+
* structured-output subagent pair, with the provider/model frozen to the
|
|
4
|
+
* calling agent's own route.
|
|
5
|
+
*
|
|
6
|
+
* Stage 1 — executor: a fresh child performs the case task with its tools
|
|
7
|
+
* and records CONCRETE EVIDENCE of what it did and found. It NEVER sees the
|
|
8
|
+
* rubric (gap A1, evaluator/scorer separation): the agent under test cannot
|
|
9
|
+
* optimize its behavior toward the grading criteria, and cannot grade its
|
|
10
|
+
* own execution.
|
|
11
|
+
*
|
|
12
|
+
* Stage 2 — reviewer: an INDEPENDENT child grades the executor's evidence
|
|
13
|
+
* strictly against the rubric. The rubric plaintext is decrypted in the host
|
|
14
|
+
* and flows ONLY into this reviewer branch; the executor branch never
|
|
15
|
+
* touches it. A separate model instance grading someone else's evidence
|
|
16
|
+
* removes the self-serving bias of self-scoring.
|
|
17
|
+
*
|
|
18
|
+
* Failure-cell protocol (gap A2): a unit that cannot produce a score
|
|
19
|
+
* (decrypt error, executor crash, reviewer crash, protocol error) returns a
|
|
20
|
+
* cell with `status: "failed"` — NEVER a zero — so aggregation can exclude
|
|
21
|
+
* it and the acceptance rule can reject rounds with too many failures instead
|
|
22
|
+
* of silently averaging a 0 into the mean.
|
|
23
|
+
*
|
|
24
|
+
* Each cell carries the executor child's session id (gap A4): the score can
|
|
25
|
+
* be drilled back to the exact session transcript that produced the evidence.
|
|
6
26
|
*
|
|
7
27
|
* Uses the host-plane `subagents` service (available in every profile) with
|
|
8
28
|
* the native `outputSchema` structured-output seam: the provider validates
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
29
|
+
* each child's reply against its cell schema, so the host never parses model
|
|
30
|
+
* text for evaluations. (The workflow engine was rejected because the web
|
|
31
|
+
* profile keeps it in a per-agent isolated realm a host plugin cannot
|
|
12
32
|
* resolve.)
|
|
13
33
|
*/
|
|
14
34
|
import type { Context } from "@deepseek-ai/cordis";
|
|
@@ -32,7 +52,23 @@ export interface EvaluationOutcome {
|
|
|
32
52
|
}
|
|
33
53
|
/** How many evaluation units may run concurrently (bounded subagent fan-out). */
|
|
34
54
|
export declare const DEFAULT_EVALUATION_CONCURRENCY = 4;
|
|
55
|
+
/** Evidence handed to the reviewer is capped so the grading call stays bounded. */
|
|
56
|
+
export declare const MAX_EVIDENCE_CHARS = 8000;
|
|
35
57
|
export declare function evaluateState(ctx: Context, agent: Agent, options: EvaluateOptions): Promise<EvaluationOutcome>;
|
|
58
|
+
export interface ExecutorResult {
|
|
59
|
+
caseId: string;
|
|
60
|
+
run: number;
|
|
61
|
+
evidence: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Deterministic hash of a case's statement + rubric envelope (gap A3):
|
|
65
|
+
* a 16-char SHA-256 prefix, hex-encoded. Used to detect material changes
|
|
66
|
+
* between reference and candidate evaluation runs (see
|
|
67
|
+
* `score.flagMaterialDrift`).
|
|
68
|
+
*/
|
|
69
|
+
export declare function caseHash(caseItem: BenchmarkCase): string;
|
|
70
|
+
/** Validate a provider-validated executor result; returns undefined when malformed. */
|
|
71
|
+
export declare function normalizeExecutor(value: unknown, caseId: string, run: number): ExecutorResult | undefined;
|
|
36
72
|
/** Validate a provider-validated structured cell; returns undefined when malformed. */
|
|
37
73
|
export declare function normalizeCell(value: unknown, caseId: string, run: number, passThreshold: number): CellScore | undefined;
|
|
38
74
|
//# sourceMappingURL=evaluate.d.ts.map
|
package/lib/evaluate.js
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { mapPool } from "./pool.js";
|
|
2
3
|
import { decryptRubric, deriveKey, DEV_RUBRIC_KEY } from "./rubric.js";
|
|
3
4
|
/** Key used when the caller did not resolve one: mirrors resolveRubricKey's last-resort dev fallback. */
|
|
4
5
|
function devRubricKey() {
|
|
5
6
|
return deriveKey(DEV_RUBRIC_KEY);
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
+
/** Stage 1: the agent under test — sees the task, never the rubric. */
|
|
9
|
+
const EXECUTOR_SYSTEM_PROMPT = `You are one evaluation unit in a benchmark matrix.
|
|
8
10
|
|
|
9
|
-
You are the agent under evaluation. Perform the case task using your tools
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
You are the agent under evaluation. Perform the case task using your tools.
|
|
12
|
+
You are NOT asked to grade yourself: a separate evaluator will judge your
|
|
13
|
+
work against criteria you do not see. Instead, record CONCRETE, VERIFIABLE
|
|
14
|
+
EVIDENCE of what you did and found — the actual commands/reads you performed,
|
|
15
|
+
what the harness state contains, the exact text you produced. Evidence is
|
|
16
|
+
what your work will be scored from; vague self-assessment earns nothing.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(concrete
|
|
18
|
+
The harness guidance attached to the state under test is included in the task.
|
|
19
|
+
Your reply is structured (see the requested output schema): caseId, run, and
|
|
20
|
+
evidence (the concrete artifact/report of your execution).`;
|
|
21
|
+
/** Stage 2: the independent grader — sees the rubric, never executes the task. */
|
|
22
|
+
const REVIEWER_SYSTEM_PROMPT = `You are an independent evaluator in a benchmark matrix.
|
|
23
|
+
|
|
24
|
+
You did NOT perform the task and you cannot interact with the runtime: grade
|
|
25
|
+
strictly from the EVIDENCE produced by the agent under evaluation, against
|
|
26
|
+
the rubric. The harness guidance under test is included for context so you
|
|
27
|
+
can judge whether the evidence genuinely reflects the state under test
|
|
28
|
+
(e.g. whether the agent actually inspected the harness store).
|
|
29
|
+
|
|
30
|
+
Score YOUR JUDGMENT of the evidence against the rubric criteria: a
|
|
31
|
+
0-100 score, and passed (true iff score >= the stated threshold). Cite in
|
|
32
|
+
notes the concrete rubric criteria and the evidence that supports the score.
|
|
33
|
+
Do not inflate: grade the evidence as presented, not what the agent might
|
|
34
|
+
have meant.`;
|
|
35
|
+
const EXECUTOR_SCHEMA = {
|
|
36
|
+
type: "object",
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
properties: {
|
|
39
|
+
caseId: { type: "string" },
|
|
40
|
+
run: { type: "number" },
|
|
41
|
+
evidence: { type: "string" },
|
|
42
|
+
},
|
|
43
|
+
required: ["caseId", "run", "evidence"],
|
|
44
|
+
};
|
|
16
45
|
const CELL_SCHEMA = {
|
|
17
46
|
type: "object",
|
|
18
47
|
additionalProperties: false,
|
|
@@ -27,6 +56,8 @@ const CELL_SCHEMA = {
|
|
|
27
56
|
};
|
|
28
57
|
/** How many evaluation units may run concurrently (bounded subagent fan-out). */
|
|
29
58
|
export const DEFAULT_EVALUATION_CONCURRENCY = 4;
|
|
59
|
+
/** Evidence handed to the reviewer is capped so the grading call stays bounded. */
|
|
60
|
+
export const MAX_EVIDENCE_CHARS = 8000;
|
|
30
61
|
export async function evaluateState(ctx, agent, options) {
|
|
31
62
|
if (!agent.options.provider || !agent.options.model) {
|
|
32
63
|
throw new Error("evolve: benchmark evaluation requires a provider/model route");
|
|
@@ -44,65 +75,162 @@ export async function evaluateState(ctx, agent, options) {
|
|
|
44
75
|
const cells = await mapPool(units, DEFAULT_EVALUATION_CONCURRENCY, (unit) => runUnit(subagents, agent, options, unit.case, unit.run));
|
|
45
76
|
return { label: options.label, cells, stopReason: "completed" };
|
|
46
77
|
}
|
|
78
|
+
/** A cell that could not be produced: never a zero, excluded by aggregation. */
|
|
79
|
+
function failedCell(caseId, run, message) {
|
|
80
|
+
return { caseId, run, status: "failed", score: 0, passed: false, notes: message };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Deterministic hash of a case's statement + rubric envelope (gap A3):
|
|
84
|
+
* a 16-char SHA-256 prefix, hex-encoded. Used to detect material changes
|
|
85
|
+
* between reference and candidate evaluation runs (see
|
|
86
|
+
* `score.flagMaterialDrift`).
|
|
87
|
+
*/
|
|
88
|
+
export function caseHash(caseItem) {
|
|
89
|
+
const material = `${caseItem.statement}\n${caseItem.rubric}`;
|
|
90
|
+
return createHash("sha256").update(material).digest("hex").slice(0, 16);
|
|
91
|
+
}
|
|
47
92
|
async function runUnit(subagents, agent, options, c, run) {
|
|
93
|
+
// Gap C3: track wall-clock duration of the entire cell evaluation.
|
|
94
|
+
const cellStart = Date.now();
|
|
48
95
|
// The ONLY rubric decryption point: the envelope is opened here, in the
|
|
49
|
-
// host, and the plaintext goes
|
|
50
|
-
//
|
|
96
|
+
// host, and the plaintext goes ONLY into the reviewer prompt — the
|
|
97
|
+
// executor branch never touches it (gap A1).
|
|
51
98
|
let rubric;
|
|
52
99
|
try {
|
|
53
100
|
rubric = decryptRubric(c.rubric, options.rubricKey ?? devRubricKey());
|
|
54
101
|
}
|
|
55
102
|
catch (cause) {
|
|
56
|
-
return {
|
|
57
|
-
caseId: c.id,
|
|
58
|
-
run,
|
|
59
|
-
score: 0,
|
|
60
|
-
passed: false,
|
|
61
|
-
notes: `rubric decrypt failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
62
|
-
};
|
|
103
|
+
return { ...failedCell(c.id, run, `rubric decrypt failed: ${cause instanceof Error ? cause.message : String(cause)}`), durationMs: Date.now() - cellStart };
|
|
63
104
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"Execute the task with your tools, then produce the structured evaluation.",
|
|
73
|
-
].join("\n\n");
|
|
105
|
+
// Runtime evidence (gap A3): record actual provider/model from the host,
|
|
106
|
+
// and compute a material hash of the case for change detection.
|
|
107
|
+
const actualProvider = agent.options.provider ?? "unknown";
|
|
108
|
+
const actualModel = agent.options.model ?? "unknown";
|
|
109
|
+
const hash = caseHash(c);
|
|
110
|
+
// Stage 1: executor — task + evidence, NO rubric.
|
|
111
|
+
let evidence;
|
|
112
|
+
let sessionId;
|
|
74
113
|
try {
|
|
75
|
-
const
|
|
76
|
-
label: `${c.id} r${run}`,
|
|
77
|
-
prompt: [
|
|
114
|
+
const executorRun = await subagents.start("spawn", {
|
|
115
|
+
label: `${c.id} r${run} execute`,
|
|
116
|
+
prompt: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: [
|
|
120
|
+
EXECUTOR_SYSTEM_PROMPT,
|
|
121
|
+
"---",
|
|
122
|
+
"Your harness guidance (state under test):",
|
|
123
|
+
`<harness_overview>\n${options.harnessOverview}\n</harness_overview>`,
|
|
124
|
+
`Case ${c.id} — task (statement):\n${c.statement}`,
|
|
125
|
+
`Run ${run} of ${options.runs}.`,
|
|
126
|
+
"Execute the task with your tools, then produce the structured evidence.",
|
|
127
|
+
].join("\n\n"),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
parent: agent,
|
|
131
|
+
signal: options.signal ?? new AbortController().signal,
|
|
132
|
+
outputSchema: EXECUTOR_SCHEMA,
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const settled = await executorRun.result;
|
|
136
|
+
if (settled.stopReason !== "completed") {
|
|
137
|
+
throw new Error(`executor stopped: ${settled.stopReason ?? "unknown"}`);
|
|
138
|
+
}
|
|
139
|
+
const parsed = normalizeExecutor(settled.structured, c.id, run) ?? fromEvidenceText(settled.output, c.id, run);
|
|
140
|
+
if (!parsed) {
|
|
141
|
+
throw new Error("executor returned neither a structured value nor usable text");
|
|
142
|
+
}
|
|
143
|
+
evidence = parsed;
|
|
144
|
+
sessionId = executorRun.id || undefined;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
executorRun.dispose();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (cause) {
|
|
151
|
+
return { ...failedCell(c.id, run, `executor failed: ${cause instanceof Error ? cause.message : String(cause)}`), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
|
|
152
|
+
}
|
|
153
|
+
// Stage 2: independent reviewer — rubric + evidence, NO task execution.
|
|
154
|
+
try {
|
|
155
|
+
const reviewerRun = await subagents.start("spawn", {
|
|
156
|
+
label: `${c.id} r${run} grade`,
|
|
157
|
+
prompt: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: [
|
|
161
|
+
REVIEWER_SYSTEM_PROMPT,
|
|
162
|
+
"---",
|
|
163
|
+
"Your harness guidance (state under test):",
|
|
164
|
+
`<harness_overview>\n${options.harnessOverview}\n</harness_overview>`,
|
|
165
|
+
`Case ${c.id} — task (statement):\n${c.statement}`,
|
|
166
|
+
`Rubric — grade the evidence strictly against these criteria:\n${rubric}`,
|
|
167
|
+
`Evidence produced by the agent under evaluation:\n<evidence>\n${trimEvidence(evidence.evidence)}\n</evidence>`,
|
|
168
|
+
`Run ${run} of ${options.runs}. passThreshold = ${options.passThreshold}.`,
|
|
169
|
+
"Produce the structured score.",
|
|
170
|
+
].join("\n\n"),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
78
173
|
parent: agent,
|
|
79
174
|
signal: options.signal ?? new AbortController().signal,
|
|
80
175
|
outputSchema: CELL_SCHEMA,
|
|
81
176
|
});
|
|
82
177
|
try {
|
|
83
|
-
const settled = await
|
|
178
|
+
const settled = await reviewerRun.result;
|
|
84
179
|
if (settled.stopReason !== "completed") {
|
|
85
|
-
throw new Error(`
|
|
180
|
+
throw new Error(`reviewer stopped: ${settled.stopReason ?? "unknown"}`);
|
|
86
181
|
}
|
|
87
|
-
const
|
|
182
|
+
const cell = normalizeCell(settled.structured, c.id, run, options.passThreshold) ??
|
|
88
183
|
fromOutputText(settled.output, c.id, run, options.passThreshold);
|
|
89
|
-
if (!
|
|
90
|
-
throw new Error("
|
|
184
|
+
if (!cell) {
|
|
185
|
+
throw new Error("reviewer returned neither a structured value nor usable text");
|
|
91
186
|
}
|
|
92
|
-
return
|
|
187
|
+
return { ...cell, ...(sessionId !== undefined ? { sessionId } : {}), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
|
|
93
188
|
}
|
|
94
189
|
finally {
|
|
95
|
-
|
|
190
|
+
reviewerRun.dispose();
|
|
96
191
|
}
|
|
97
192
|
}
|
|
98
193
|
catch (cause) {
|
|
99
|
-
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
194
|
+
return { ...failedCell(c.id, run, `reviewer failed: ${cause instanceof Error ? cause.message : String(cause)}`), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Cap the evidence handed to the reviewer so the grading call stays bounded. */
|
|
198
|
+
function trimEvidence(text) {
|
|
199
|
+
if (text.length <= MAX_EVIDENCE_CHARS)
|
|
200
|
+
return text;
|
|
201
|
+
return `${text.slice(0, MAX_EVIDENCE_CHARS)}\n…[evidence truncated at ${MAX_EVIDENCE_CHARS} chars]`;
|
|
202
|
+
}
|
|
203
|
+
/** Validate a provider-validated executor result; returns undefined when malformed. */
|
|
204
|
+
export function normalizeExecutor(value, caseId, run) {
|
|
205
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
206
|
+
return undefined;
|
|
207
|
+
const record = value;
|
|
208
|
+
const evidence = typeof record["evidence"] === "string" ? record["evidence"] : "";
|
|
209
|
+
if (evidence.trim().length === 0)
|
|
210
|
+
return undefined;
|
|
211
|
+
return {
|
|
212
|
+
caseId: typeof record["caseId"] === "string" && record["caseId"].length > 0 ? record["caseId"] : caseId,
|
|
213
|
+
run: typeof record["run"] === "number" && Number.isFinite(record["run"]) ? Math.trunc(record["run"]) : run,
|
|
214
|
+
evidence,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Fallback: recover the executor result from its text blocks when no structured value arrived. */
|
|
218
|
+
function fromEvidenceText(blocks, caseId, run) {
|
|
219
|
+
if (!Array.isArray(blocks))
|
|
220
|
+
return undefined;
|
|
221
|
+
const text = blocks
|
|
222
|
+
.filter((block) => block.type === "text")
|
|
223
|
+
.map((block) => block.text ?? "")
|
|
224
|
+
.join("\n");
|
|
225
|
+
const trimmed = text.trim();
|
|
226
|
+
if (trimmed.length === 0)
|
|
227
|
+
return undefined;
|
|
228
|
+
try {
|
|
229
|
+
return normalizeExecutor(JSON.parse(trimmed), caseId, run);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Not JSON — keep the raw text as the evidence.
|
|
233
|
+
return { caseId, run, evidence: trimmed };
|
|
106
234
|
}
|
|
107
235
|
}
|
|
108
236
|
/** Validate a provider-validated structured cell; returns undefined when malformed. */
|
|
@@ -116,6 +244,7 @@ export function normalizeCell(value, caseId, run, passThreshold) {
|
|
|
116
244
|
return {
|
|
117
245
|
caseId: typeof record["caseId"] === "string" && record["caseId"].length > 0 ? record["caseId"] : caseId,
|
|
118
246
|
run: typeof record["run"] === "number" && Number.isFinite(record["run"]) ? Math.trunc(record["run"]) : run,
|
|
247
|
+
status: "ok",
|
|
119
248
|
score: Math.min(100, Math.max(0, score)),
|
|
120
249
|
passed: record["passed"] === true || score >= passThreshold,
|
|
121
250
|
notes: typeof record["notes"] === "string" ? record["notes"] : "",
|