dsh-continual-evolve 0.1.0 → 0.2.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/lib/evaluate.js CHANGED
@@ -4,15 +4,43 @@ import { decryptRubric, deriveKey, DEV_RUBRIC_KEY } from "./rubric.js";
4
4
  function devRubricKey() {
5
5
  return deriveKey(DEV_RUBRIC_KEY);
6
6
  }
7
- const EVAL_SYSTEM_PROMPT = `You are one evaluation unit in a benchmark matrix.
7
+ /** Stage 1: the agent under test sees the task, never the rubric. */
8
+ const EXECUTOR_SYSTEM_PROMPT = `You are one evaluation unit in a benchmark matrix.
8
9
 
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.
10
+ You are the agent under evaluation. Perform the case task using your tools.
11
+ You are NOT asked to grade yourself: a separate evaluator will judge your
12
+ work against criteria you do not see. Instead, record CONCRETE, VERIFIABLE
13
+ EVIDENCE of what you did and found — the actual commands/reads you performed,
14
+ what the harness state contains, the exact text you produced. Evidence is
15
+ what your work will be scored from; vague self-assessment earns nothing.
12
16
 
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).`;
17
+ The harness guidance attached to the state under test is included in the task.
18
+ Your reply is structured (see the requested output schema): caseId, run, and
19
+ evidence (the concrete artifact/report of your execution).`;
20
+ /** Stage 2: the independent grader — sees the rubric, never executes the task. */
21
+ const REVIEWER_SYSTEM_PROMPT = `You are an independent evaluator in a benchmark matrix.
22
+
23
+ You did NOT perform the task and you cannot interact with the runtime: grade
24
+ strictly from the EVIDENCE produced by the agent under evaluation, against
25
+ the rubric. The harness guidance under test is included for context so you
26
+ can judge whether the evidence genuinely reflects the state under test
27
+ (e.g. whether the agent actually inspected the harness store).
28
+
29
+ Score YOUR JUDGMENT of the evidence against the rubric criteria: a
30
+ 0-100 score, and passed (true iff score >= the stated threshold). Cite in
31
+ notes the concrete rubric criteria and the evidence that supports the score.
32
+ Do not inflate: grade the evidence as presented, not what the agent might
33
+ have meant.`;
34
+ const EXECUTOR_SCHEMA = {
35
+ type: "object",
36
+ additionalProperties: false,
37
+ properties: {
38
+ caseId: { type: "string" },
39
+ run: { type: "number" },
40
+ evidence: { type: "string" },
41
+ },
42
+ required: ["caseId", "run", "evidence"],
43
+ };
16
44
  const CELL_SCHEMA = {
17
45
  type: "object",
18
46
  additionalProperties: false,
@@ -27,6 +55,8 @@ const CELL_SCHEMA = {
27
55
  };
28
56
  /** How many evaluation units may run concurrently (bounded subagent fan-out). */
29
57
  export const DEFAULT_EVALUATION_CONCURRENCY = 4;
58
+ /** Evidence handed to the reviewer is capped so the grading call stays bounded. */
59
+ export const MAX_EVIDENCE_CHARS = 8000;
30
60
  export async function evaluateState(ctx, agent, options) {
31
61
  if (!agent.options.provider || !agent.options.model) {
32
62
  throw new Error("evolve: benchmark evaluation requires a provider/model route");
@@ -44,65 +74,148 @@ export async function evaluateState(ctx, agent, options) {
44
74
  const cells = await mapPool(units, DEFAULT_EVALUATION_CONCURRENCY, (unit) => runUnit(subagents, agent, options, unit.case, unit.run));
45
75
  return { label: options.label, cells, stopReason: "completed" };
46
76
  }
77
+ /** A cell that could not be produced: never a zero, excluded by aggregation. */
78
+ function failedCell(caseId, run, message) {
79
+ return { caseId, run, status: "failed", score: 0, passed: false, notes: message };
80
+ }
47
81
  async function runUnit(subagents, agent, options, c, run) {
48
82
  // 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.
83
+ // host, and the plaintext goes ONLY into the reviewer prompt — the
84
+ // executor branch never touches it (gap A1).
51
85
  let rubric;
52
86
  try {
53
87
  rubric = decryptRubric(c.rubric, options.rubricKey ?? devRubricKey());
54
88
  }
55
89
  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
- };
90
+ return failedCell(c.id, run, `rubric decrypt failed: ${cause instanceof Error ? cause.message : String(cause)}`);
91
+ }
92
+ // Stage 1: executor — task + evidence, NO rubric.
93
+ let evidence;
94
+ let sessionId;
95
+ try {
96
+ const executorRun = await subagents.start("spawn", {
97
+ label: `${c.id} r${run} execute`,
98
+ prompt: [
99
+ {
100
+ type: "text",
101
+ text: [
102
+ EXECUTOR_SYSTEM_PROMPT,
103
+ "---",
104
+ "Your harness guidance (state under test):",
105
+ `<harness_overview>\n${options.harnessOverview}\n</harness_overview>`,
106
+ `Case ${c.id} — task (statement):\n${c.statement}`,
107
+ `Run ${run} of ${options.runs}.`,
108
+ "Execute the task with your tools, then produce the structured evidence.",
109
+ ].join("\n\n"),
110
+ },
111
+ ],
112
+ parent: agent,
113
+ signal: options.signal ?? new AbortController().signal,
114
+ outputSchema: EXECUTOR_SCHEMA,
115
+ });
116
+ try {
117
+ const settled = await executorRun.result;
118
+ if (settled.stopReason !== "completed") {
119
+ throw new Error(`executor stopped: ${settled.stopReason ?? "unknown"}`);
120
+ }
121
+ const parsed = normalizeExecutor(settled.structured, c.id, run) ?? fromEvidenceText(settled.output, c.id, run);
122
+ if (!parsed) {
123
+ throw new Error("executor returned neither a structured value nor usable text");
124
+ }
125
+ evidence = parsed;
126
+ sessionId = executorRun.id || undefined;
127
+ }
128
+ finally {
129
+ executorRun.dispose();
130
+ }
131
+ }
132
+ catch (cause) {
133
+ return failedCell(c.id, run, `executor failed: ${cause instanceof Error ? cause.message : String(cause)}`);
63
134
  }
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");
135
+ // Stage 2: independent reviewer — rubric + evidence, NO task execution.
74
136
  try {
75
- const runObj = await subagents.start("spawn", {
76
- label: `${c.id} r${run}`,
77
- prompt: [{ type: "text", text: prompt }],
137
+ const reviewerRun = await subagents.start("spawn", {
138
+ label: `${c.id} r${run} grade`,
139
+ prompt: [
140
+ {
141
+ type: "text",
142
+ text: [
143
+ REVIEWER_SYSTEM_PROMPT,
144
+ "---",
145
+ "Your harness guidance (state under test):",
146
+ `<harness_overview>\n${options.harnessOverview}\n</harness_overview>`,
147
+ `Case ${c.id} — task (statement):\n${c.statement}`,
148
+ `Rubric — grade the evidence strictly against these criteria:\n${rubric}`,
149
+ `Evidence produced by the agent under evaluation:\n<evidence>\n${trimEvidence(evidence.evidence)}\n</evidence>`,
150
+ `Run ${run} of ${options.runs}. passThreshold = ${options.passThreshold}.`,
151
+ "Produce the structured score.",
152
+ ].join("\n\n"),
153
+ },
154
+ ],
78
155
  parent: agent,
79
156
  signal: options.signal ?? new AbortController().signal,
80
157
  outputSchema: CELL_SCHEMA,
81
158
  });
82
159
  try {
83
- const settled = await runObj.result;
160
+ const settled = await reviewerRun.result;
84
161
  if (settled.stopReason !== "completed") {
85
- throw new Error(`child stopped: ${settled.stopReason ?? "unknown"}`);
162
+ throw new Error(`reviewer stopped: ${settled.stopReason ?? "unknown"}`);
86
163
  }
87
- const parsed = normalizeCell(settled.structured, c.id, run, options.passThreshold) ??
164
+ const cell = normalizeCell(settled.structured, c.id, run, options.passThreshold) ??
88
165
  fromOutputText(settled.output, c.id, run, options.passThreshold);
89
- if (!parsed) {
90
- throw new Error("child returned neither a structured value nor usable text");
166
+ if (!cell) {
167
+ throw new Error("reviewer returned neither a structured value nor usable text");
168
+ }
169
+ if (sessionId !== undefined) {
170
+ return { ...cell, sessionId };
91
171
  }
92
- return parsed;
172
+ return cell;
93
173
  }
94
174
  finally {
95
- runObj.dispose();
175
+ reviewerRun.dispose();
96
176
  }
97
177
  }
98
178
  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
- };
179
+ return failedCell(c.id, run, `reviewer failed: ${cause instanceof Error ? cause.message : String(cause)}`);
180
+ }
181
+ }
182
+ /** Cap the evidence handed to the reviewer so the grading call stays bounded. */
183
+ function trimEvidence(text) {
184
+ if (text.length <= MAX_EVIDENCE_CHARS)
185
+ return text;
186
+ return `${text.slice(0, MAX_EVIDENCE_CHARS)}\n…[evidence truncated at ${MAX_EVIDENCE_CHARS} chars]`;
187
+ }
188
+ /** Validate a provider-validated executor result; returns undefined when malformed. */
189
+ export function normalizeExecutor(value, caseId, run) {
190
+ if (typeof value !== "object" || value === null || Array.isArray(value))
191
+ return undefined;
192
+ const record = value;
193
+ const evidence = typeof record["evidence"] === "string" ? record["evidence"] : "";
194
+ if (evidence.trim().length === 0)
195
+ return undefined;
196
+ return {
197
+ caseId: typeof record["caseId"] === "string" && record["caseId"].length > 0 ? record["caseId"] : caseId,
198
+ run: typeof record["run"] === "number" && Number.isFinite(record["run"]) ? Math.trunc(record["run"]) : run,
199
+ evidence,
200
+ };
201
+ }
202
+ /** Fallback: recover the executor result from its text blocks when no structured value arrived. */
203
+ function fromEvidenceText(blocks, caseId, run) {
204
+ if (!Array.isArray(blocks))
205
+ return undefined;
206
+ const text = blocks
207
+ .filter((block) => block.type === "text")
208
+ .map((block) => block.text ?? "")
209
+ .join("\n");
210
+ const trimmed = text.trim();
211
+ if (trimmed.length === 0)
212
+ return undefined;
213
+ try {
214
+ return normalizeExecutor(JSON.parse(trimmed), caseId, run);
215
+ }
216
+ catch {
217
+ // Not JSON — keep the raw text as the evidence.
218
+ return { caseId, run, evidence: trimmed };
106
219
  }
107
220
  }
108
221
  /** Validate a provider-validated structured cell; returns undefined when malformed. */
@@ -116,6 +229,7 @@ export function normalizeCell(value, caseId, run, passThreshold) {
116
229
  return {
117
230
  caseId: typeof record["caseId"] === "string" && record["caseId"].length > 0 ? record["caseId"] : caseId,
118
231
  run: typeof record["run"] === "number" && Number.isFinite(record["run"]) ? Math.trunc(record["run"]) : run,
232
+ status: "ok",
119
233
  score: Math.min(100, Math.max(0, score)),
120
234
  passed: record["passed"] === true || score >= passThreshold,
121
235
  notes: typeof record["notes"] === "string" ? record["notes"] : "",
package/lib/fate.d.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Gate local-fate dimension (#11 P2): the automatic review gate also decides
3
+ * what happens to a session's local entries while the session is STILL
4
+ * running — not only at wrap-up.
5
+ *
6
+ * Local entries default to orphans: a later session (not on the parentSession
7
+ * chain) never sees them, and nothing promotes or archives them — the
8
+ * exploration results effectively "die" with the session. This module gives
9
+ * the gate its own fate cadence on top of the existing review:
10
+ *
11
+ * - `promote` → the entry moves into the global store. The global store is a
12
+ * governed resource: the user is consulted FIRST (the consultSkillEdits
13
+ * pattern — never written silently), with a cooldown after a decline.
14
+ * - `archive` → hidden from injection (data stays restorable). Archives that
15
+ * would bury possibly-reusable content (not covered globally AND distilled
16
+ * from real user messages) ask the user first; covered/operational entries
17
+ * archive silently, exactly like the wrap-up command does.
18
+ * - `keep` → nothing.
19
+ *
20
+ * At compaction the gate NEVER opens a dialog (the agent is mid-compaction):
21
+ * only deterministic silent archives apply, everything governed is deferred
22
+ * with an audit record pointing at `/evolve wrapup`.
23
+ *
24
+ * Division of labor is the same as wrap-up: the mechanical audit proposes
25
+ * (listLocalCandidates + coverage guards), the LLM classifies
26
+ * (assessLocalEntries), the user approves, the code applies deterministically
27
+ * (wholePromoteProposals / splitPromoteProposals — the SAME edits the wrap-up
28
+ * command applies). Every decision lands in reviews.jsonl via the gate's
29
+ * record callback.
30
+ */
31
+ import type { Context } from "@deepseek-ai/cordis";
32
+ import type { Agent } from "@deepseek-ai/dsh-agent";
33
+ import type { HarnessState, RefinementResult } from "./types.js";
34
+ import type { EvolutionEngine } from "./service.js";
35
+ import type { AutoRefineReason } from "./review.js";
36
+ import type { AutoReviewConfig, GateState, ReviewRecord } from "./auto.js";
37
+ import { type WrapupCandidate, type WrapupItem } from "./wrapup.js";
38
+ /** Turns a declined local-fate proposal stays silent before being offered again. */
39
+ export declare const FATE_CONSULT_COOLDOWN_TURNS = 10;
40
+ /** What the gate decided to do with the session's local entries. */
41
+ export interface FatePlan {
42
+ candidates: readonly WrapupCandidate[];
43
+ /** Whole promotions that passed the deterministic global-coverage guard. */
44
+ promotable: WrapupItem[];
45
+ /** Split promotions (archive + cleaned promote payload) that passed the guard. */
46
+ splits: {
47
+ item: WrapupItem;
48
+ candidate: WrapupCandidate;
49
+ }[];
50
+ /** Archives that may proceed silently (covered globally / no real distillation source). */
51
+ silentArchives: WrapupItem[];
52
+ /** Archives that must ask the user first (uncovered + real source). */
53
+ reviewArchives: WrapupItem[];
54
+ /** Promotes blocked by the deterministic guard, with why. */
55
+ skipped: {
56
+ key: string;
57
+ reason: string;
58
+ }[];
59
+ /** Split promotions blocked by the deterministic guard, with why. */
60
+ splitSkipped: {
61
+ key: string;
62
+ reason: string;
63
+ }[];
64
+ }
65
+ /**
66
+ * Partition an assessed wrap-up classification into concrete fate actions,
67
+ * re-running the deterministic guards against the LIVE global store (state
68
+ * may have changed while the LLM call was in flight). Pure and unit-tested;
69
+ * mirrors the partition step of the wrap-up command.
70
+ */
71
+ export declare function planLocalFates(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[], globalState: HarnessState): FatePlan;
72
+ /**
73
+ * The cooldown key of a candidate set: the sorted `kind:id` list. The set is
74
+ * the unit of consultation — a declined proposal is not offered again within
75
+ * the cooldown window, and a changed set (new entries appeared) starts a
76
+ * fresh consultation.
77
+ */
78
+ export declare function fateSetKey(candidates: readonly WrapupCandidate[]): string;
79
+ /**
80
+ * Whether the local-fate dimension is due for this gate run. Turn-interval
81
+ * gates respect the fate cadence (an independent counter — goal-driven
82
+ * sessions run the review EVERY round, the fate assessment must not);
83
+ * compaction is unconditional: experiences about to be summarized away get
84
+ * their fate check regardless.
85
+ */
86
+ export declare function fateCadenceDue(state: GateState, reason: AutoRefineReason, intervalTurns: number): boolean;
87
+ export interface FateConsultResult {
88
+ approved: boolean;
89
+ asked: boolean;
90
+ reason: "nothing-to-ask" | "consented" | "declined" | "cooldown" | "unavailable" | "error";
91
+ }
92
+ /**
93
+ * Ask the user whether to execute the gate's local-fate proposal. ONE dialog
94
+ * covers every governed action (promotes, split promotions, review-required
95
+ * archives) — the gate never spams questions. Conservative on every edge:
96
+ * no question service → not approved; the question call fails → not approved;
97
+ * the same candidate set was declined within the cooldown → not asked again.
98
+ * A decline records the cooldown (the consultSkillEdits pattern).
99
+ */
100
+ export declare function consultLocalFates(ctx: Context, agent: Agent, plan: FatePlan, gate: GateState): Promise<FateConsultResult>;
101
+ export type FateApplyMode = "full" | "silent-only";
102
+ export interface FateApplyResult {
103
+ /** Human-readable lines of what was applied (for the notice and the audit record). */
104
+ applied: string[];
105
+ /** Every refinement result produced, for rollback discovery. */
106
+ results: RefinementResult[];
107
+ }
108
+ /**
109
+ * Deterministically apply the fate plan. `"full"` applies everything
110
+ * (promotes, splits, silent + review archives); `"silent-only"` applies only
111
+ * the deterministic silent archives (the compaction path — nothing governed).
112
+ * Promotes go through the SAME proposals as the wrap-up command
113
+ * (wholePromoteProposals / splitPromoteProposals), so both paths write
114
+ * identical global entries and local retirement stamps.
115
+ */
116
+ export declare function applyLocalFates(engine: EvolutionEngine, sessionId: string, plan: FatePlan, localState: HarnessState, mode: FateApplyMode): FateApplyResult;
117
+ /**
118
+ * The gate's local-fate phase. Runs after the review phase on every gate
119
+ * trigger (turn_interval / compact), subject to cadence and cooldown. All
120
+ * failures are contained and recorded — a broken fate dimension never
121
+ * disturbs the agent loop.
122
+ */
123
+ export declare function runLocalFatePhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, reason: AutoRefineReason, record: (entry: Omit<ReviewRecord, "timestamp">) => void): Promise<void>;
124
+ /** The user-visible notice after the gate applied local-fate actions. */
125
+ export declare function buildFateNotice(applied: readonly string[]): string;
126
+ //# sourceMappingURL=fate.d.ts.map