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.
Files changed (64) hide show
  1. package/README.md +172 -17
  2. package/README.zh.md +87 -10
  3. package/lib/apply.js +3 -1
  4. package/lib/approval.d.ts +25 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +99 -5
  7. package/lib/auto.js +165 -6
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +331 -0
  10. package/lib/benchmark.d.ts +84 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.js +33 -221
  13. package/lib/evaluate.d.ts +43 -7
  14. package/lib/evaluate.js +172 -43
  15. package/lib/evolve-event.d.ts +38 -0
  16. package/lib/evolve-event.js +49 -0
  17. package/lib/failures.d.ts +39 -0
  18. package/lib/failures.js +170 -0
  19. package/lib/fate.d.ts +128 -0
  20. package/lib/fate.js +342 -0
  21. package/lib/goal-command.d.ts +7 -0
  22. package/lib/goal-command.js +37 -0
  23. package/lib/index.d.ts +51 -21
  24. package/lib/index.js +32 -2
  25. package/lib/inject.d.ts +8 -0
  26. package/lib/inject.js +51 -4
  27. package/lib/llm-text.d.ts +30 -0
  28. package/lib/llm-text.js +49 -0
  29. package/lib/mount-command.d.ts +10 -0
  30. package/lib/mount-command.js +48 -0
  31. package/lib/mount.js +5 -0
  32. package/lib/plan.js +5 -0
  33. package/lib/planner.d.ts +8 -1
  34. package/lib/planner.js +40 -39
  35. package/lib/render.d.ts +1 -3
  36. package/lib/render.js +2 -5
  37. package/lib/review.d.ts +5 -2
  38. package/lib/review.js +27 -38
  39. package/lib/rollback.d.ts +1 -3
  40. package/lib/rollback.js +0 -8
  41. package/lib/score.d.ts +37 -4
  42. package/lib/score.js +120 -10
  43. package/lib/service.d.ts +2 -2
  44. package/lib/service.js +5 -2
  45. package/lib/skill-render.d.ts +15 -0
  46. package/lib/skill-render.js +30 -0
  47. package/lib/skill.d.ts +12 -7
  48. package/lib/skill.js +36 -31
  49. package/lib/skillquality.d.ts +80 -0
  50. package/lib/skillquality.js +311 -0
  51. package/lib/store.d.ts +1 -3
  52. package/lib/store.js +0 -7
  53. package/lib/tool.js +28 -4
  54. package/lib/types.d.ts +39 -0
  55. package/lib/types.js +19 -0
  56. package/lib/usage.d.ts +32 -0
  57. package/lib/usage.js +84 -0
  58. package/lib/validate.d.ts +12 -2
  59. package/lib/validate.js +51 -2
  60. package/lib/wrapup-command.d.ts +8 -0
  61. package/lib/wrapup-command.js +211 -0
  62. package/lib/wrapup.d.ts +215 -0
  63. package/lib/wrapup.js +427 -0
  64. 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
- const id = stripAngleBrackets(rest[0] ?? "");
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 fresh
3
- * structured-output subagent, with the provider/model frozen to the calling
4
- * agent's own route. Raw per-cell scores come back to the host; aggregation
5
- * and acceptance happen in code (`src/score.ts`).
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
- * the child's reply against the cell schema, so the host never parses
10
- * model text for evaluations. (The workflow engine was rejected because the
11
- * web profile keeps it in a per-agent isolated realm a host plugin cannot
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
- const EVAL_SYSTEM_PROMPT = `You are one evaluation unit in a benchmark matrix.
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
- then score your own execution strictly against the rubric. The harness
11
- guidance attached to the state under test is included in the task.
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
- Your reply is structured (see the requested output schema): caseId, run,
14
- score (0-100), passed (true iff score >= the stated threshold), and notes
15
- (concrete evidence for the score).`;
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 straight into the child prompt. The
50
- // optimizer never reaches this path.
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
- const prompt = [
65
- EVAL_SYSTEM_PROMPT,
66
- "---",
67
- "Your harness guidance (state under test):",
68
- `<harness_overview>\n${options.harnessOverview}\n</harness_overview>`,
69
- `Case ${c.id} — task (statement):\n${c.statement}`,
70
- `Rubric — score yourself strictly against these criteria:\n${rubric}`,
71
- `Run ${run} of ${options.runs}. passThreshold = ${options.passThreshold}.`,
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 runObj = await subagents.start("spawn", {
76
- label: `${c.id} r${run}`,
77
- prompt: [{ type: "text", text: 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 runObj.result;
178
+ const settled = await reviewerRun.result;
84
179
  if (settled.stopReason !== "completed") {
85
- throw new Error(`child stopped: ${settled.stopReason ?? "unknown"}`);
180
+ throw new Error(`reviewer stopped: ${settled.stopReason ?? "unknown"}`);
86
181
  }
87
- const parsed = normalizeCell(settled.structured, c.id, run, options.passThreshold) ??
182
+ const cell = normalizeCell(settled.structured, c.id, run, options.passThreshold) ??
88
183
  fromOutputText(settled.output, c.id, run, options.passThreshold);
89
- if (!parsed) {
90
- throw new Error("child returned neither a structured value nor usable text");
184
+ if (!cell) {
185
+ throw new Error("reviewer returned neither a structured value nor usable text");
91
186
  }
92
- return parsed;
187
+ return { ...cell, ...(sessionId !== undefined ? { sessionId } : {}), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
93
188
  }
94
189
  finally {
95
- runObj.dispose();
190
+ reviewerRun.dispose();
96
191
  }
97
192
  }
98
193
  catch (cause) {
99
- return {
100
- caseId: c.id,
101
- run,
102
- score: 0,
103
- passed: false,
104
- notes: `unit failed: ${cause instanceof Error ? cause.message : String(cause)}`,
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"] : "",