@hmharness/evolution 0.14.9 → 0.14.10
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reward-model.d.ts +62 -0
- package/dist/reward-model.js +184 -0
- package/package.json +1 -1
- package/dist/bench.d.ts.v2bak.v2bak +0 -44
- package/dist/bench.js.v2bak.v2bak +0 -156
- package/dist/candidates.d.ts.v2bak.v2bak +0 -114
- package/dist/candidates.js.v2bak.v2bak +0 -232
- package/dist/dataset.d.ts.v2bak.v2bak +0 -64
- package/dist/dataset.js.v2bak.v2bak +0 -184
- package/dist/evolve.d.ts.v2bak.v2bak +0 -98
- package/dist/evolve.js.v2bak.v2bak +0 -573
- package/dist/impact.d.ts.v2bak.v2bak +0 -77
- package/dist/impact.js.v2bak.v2bak +0 -214
- package/dist/index.d.ts.v2bak.v2bak +0 -16
- package/dist/index.js.v2bak.v2bak +0 -16
- package/dist/insights.d.ts.v2bak.v2bak +0 -18
- package/dist/insights.js.v2bak.v2bak +0 -80
- package/dist/knowledge.d.ts.v2bak.v2bak +0 -15
- package/dist/knowledge.js.v2bak.v2bak +0 -145
- package/dist/labels.d.ts.v2bak.v2bak +0 -20
- package/dist/labels.js.v2bak.v2bak +0 -57
- package/dist/memory.d.ts.v2bak.v2bak +0 -32
- package/dist/memory.js.v2bak.v2bak +0 -233
- package/dist/patches.d.ts.v2bak.v2bak +0 -83
- package/dist/patches.js.v2bak.v2bak +0 -253
- package/dist/radar.d.ts.v2bak.v2bak +0 -3
- package/dist/radar.js.v2bak.v2bak +0 -40
- package/dist/ranker.d.ts.v2bak.v2bak +0 -44
- package/dist/ranker.js.v2bak.v2bak +0 -55
- package/dist/readiness.d.ts.v2bak.v2bak +0 -15
- package/dist/readiness.js.v2bak.v2bak +0 -167
- package/dist/skillpayload.d.ts.v2bak.v2bak +0 -1
- package/dist/skillpayload.js.v2bak +0 -28
- package/dist/skillpayload.js.v2bak.v2bak.v2bak +0 -28
- package/dist/skills.d.ts.v2bak.v2bak +0 -54
- package/dist/skills.js.v2bak.v2bak +0 -321
- package/dist/workflows.d.ts.v2bak.v2bak +0 -28
- package/dist/workflows.js.v2bak.v2bak +0 -84
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { HumanLabel } from './labels.ts';
|
|
2
|
+
export type { HumanLabel };
|
|
3
|
+
export interface RewardFeatures {
|
|
4
|
+
/** outcome bucket: 2=ok, 1=turn-budget, 0=error */
|
|
5
|
+
outcome: number;
|
|
6
|
+
/** failed tool calls / total tool calls, 0..1 */
|
|
7
|
+
toolFailRate: number;
|
|
8
|
+
/** agent turns used, raw count */
|
|
9
|
+
turns: number;
|
|
10
|
+
/** tool calls issued, raw count */
|
|
11
|
+
toolUses: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RewardWeights {
|
|
14
|
+
/** logistic weights over [outcome, toolFailRate, log1p(turns), log1p(toolUses), bias] */
|
|
15
|
+
w: [number, number, number, number, number];
|
|
16
|
+
trainedAt: string;
|
|
17
|
+
nSamples: number;
|
|
18
|
+
trainRmse: number;
|
|
19
|
+
/** holdout RMSE when n >= 20 (80/20 split, deterministic) */
|
|
20
|
+
holdoutRmse?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Human label record: re-used from labels.ts (single source). */
|
|
23
|
+
/** Insight record fields the model joins on. */
|
|
24
|
+
export interface InsightLike {
|
|
25
|
+
session: string;
|
|
26
|
+
outcome: string;
|
|
27
|
+
turns?: number;
|
|
28
|
+
toolUses?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Join labels with insights and map to training rows. Pure - testable. */
|
|
31
|
+
export declare function trainingRows(labels: HumanLabel[], insights: InsightLike[]): Array<{
|
|
32
|
+
f: RewardFeatures;
|
|
33
|
+
y: number;
|
|
34
|
+
session: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** Fit the reward model on joined human labels. Deterministic. */
|
|
37
|
+
export declare function fitRewardModel(labels: HumanLabel[], insights: InsightLike[]): RewardWeights;
|
|
38
|
+
/** Score a run's features with fitted weights -> reward in [0,1]. */
|
|
39
|
+
export declare function scoreFeatures(w: RewardWeights['w'], f: RewardFeatures): number;
|
|
40
|
+
/** Persistence: HMH_HOME/evolution/reward-model.json. */
|
|
41
|
+
export declare function saveRewardModel(home: string, m: RewardWeights): Promise<void>;
|
|
42
|
+
export declare function loadRewardModel(home: string): Promise<RewardWeights | null>;
|
|
43
|
+
/** DPO preference pair for external fine-tuning. */
|
|
44
|
+
export interface DpoPair {
|
|
45
|
+
prompt: string;
|
|
46
|
+
chosen: string;
|
|
47
|
+
rejected: string;
|
|
48
|
+
/** provenance for auditability */
|
|
49
|
+
chosenSession: string;
|
|
50
|
+
rejectedSession: string;
|
|
51
|
+
gap: number;
|
|
52
|
+
}
|
|
53
|
+
/** Build DPO pairs from joined labels + insights. Two classes:
|
|
54
|
+
* (a) IN-BUCKET (gold): same outcome, >=2 star gap - human preference the
|
|
55
|
+
* outcome reward cannot see (4-vs-5 star distinctions);
|
|
56
|
+
* (b) CROSS-BUCKET (classic): ok-vs-degraded completions of the SAME task
|
|
57
|
+
* template (task prefix match) - the textbook chosen/rejected pair.
|
|
58
|
+
* Pure - testable. */
|
|
59
|
+
export declare function exportDpoPairs(labels: HumanLabel[], insights: InsightLike[], taskOf: (session: string) => string, answerOf: (session: string) => string, opts?: {
|
|
60
|
+
minInBucketGap?: number;
|
|
61
|
+
crossBucketPrefix?: number;
|
|
62
|
+
}): DpoPair[];
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/evolution - reward-model (V3 RL phase, ADR-0010)
|
|
3
|
+
* A LEARNED reward model over run features, fit on the human star labels -
|
|
4
|
+
* the blueprint's "Reward -> RL" step after the M11 gate opened.
|
|
5
|
+
*
|
|
6
|
+
* Design (grounded in the Agent Lightning lesson: the app defines the task
|
|
7
|
+
* and the reward; the harness already produces rollouts):
|
|
8
|
+
* - features are what the harness already measures per session: outcome
|
|
9
|
+
* bucket, tool-failure rate, turns, tool uses (all z-free, reproducible)
|
|
10
|
+
* - the model is a tiny logistic regressor (zero deps, deterministic
|
|
11
|
+
* gradient descent) predicting the HUMAN score in [0,1]
|
|
12
|
+
* - the calibrated outcome-based reward (Spearman 0.981 vs human) is the
|
|
13
|
+
* prior; the learned model adds the fine, in-bucket distinctions the
|
|
14
|
+
* outcome signal cannot see (4-star vs 5-star runs)
|
|
15
|
+
* - weights persist to HMH_HOME/evolution/reward-model.json; refit as
|
|
16
|
+
* labels accumulate
|
|
17
|
+
*
|
|
18
|
+
* What this is NOT: a fine-tuned LLM. The blueprint explicitly defers
|
|
19
|
+
* model fine-tuning; DPO pairs for an external trainer are exported
|
|
20
|
+
* separately (exportDpoPairs).
|
|
21
|
+
*/
|
|
22
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
const FEATURES = [
|
|
25
|
+
(f) => f.outcome / 2, // 0..1
|
|
26
|
+
(f) => f.toolFailRate, // 0..1
|
|
27
|
+
(f) => Math.log1p(Math.min(f.turns, 200)) / Math.log(201), // 0..1
|
|
28
|
+
(f) => Math.log1p(Math.min(f.toolUses, 200)) / Math.log(201), // 0..1
|
|
29
|
+
];
|
|
30
|
+
function x(f) {
|
|
31
|
+
return [...FEATURES.map((fn) => fn(f)), 1];
|
|
32
|
+
}
|
|
33
|
+
function sigmoid(z) {
|
|
34
|
+
return 1 / (1 + Math.exp(-z));
|
|
35
|
+
}
|
|
36
|
+
/** Deterministic 80/20 split by index (no RNG - reproducible fits). */
|
|
37
|
+
function split(rows) {
|
|
38
|
+
const train = [];
|
|
39
|
+
const hold = [];
|
|
40
|
+
rows.forEach((r, i) => (i % 5 === 4 ? hold : train).push(r));
|
|
41
|
+
return { train, hold };
|
|
42
|
+
}
|
|
43
|
+
function fitLinear(rows, epochs = 4000, lr = 0.05, l2 = 0.01) {
|
|
44
|
+
const d = 5;
|
|
45
|
+
let w = new Array(d).fill(0);
|
|
46
|
+
const xs = rows.map((r) => x(r.f));
|
|
47
|
+
if (xs.length === 0)
|
|
48
|
+
return w;
|
|
49
|
+
for (let e = 0; e < epochs; e++) {
|
|
50
|
+
const grad = new Array(d).fill(0);
|
|
51
|
+
for (let i = 0; i < rows.length; i++) {
|
|
52
|
+
let z = 0;
|
|
53
|
+
for (let k = 0; k < d; k++)
|
|
54
|
+
z += w[k] * xs[i][k];
|
|
55
|
+
const err = sigmoid(z) - rows[i].y; // logistic loss gradient
|
|
56
|
+
for (let k = 0; k < d; k++)
|
|
57
|
+
grad[k] += err * xs[i][k];
|
|
58
|
+
}
|
|
59
|
+
for (let k = 0; k < d; k++)
|
|
60
|
+
w[k] -= lr * (grad[k] / rows.length + l2 * w[k]);
|
|
61
|
+
}
|
|
62
|
+
return w;
|
|
63
|
+
}
|
|
64
|
+
function rmse(w, rows) {
|
|
65
|
+
if (rows.length === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
let s = 0;
|
|
68
|
+
for (const r of rows) {
|
|
69
|
+
let z = 0;
|
|
70
|
+
const xv = x(r.f);
|
|
71
|
+
for (let k = 0; k < w.length; k++)
|
|
72
|
+
z += w[k] * xv[k];
|
|
73
|
+
s += (sigmoid(z) - r.y) ** 2;
|
|
74
|
+
}
|
|
75
|
+
return Math.sqrt(s / rows.length);
|
|
76
|
+
}
|
|
77
|
+
/** Join labels with insights and map to training rows. Pure - testable. */
|
|
78
|
+
export function trainingRows(labels, insights) {
|
|
79
|
+
const bySession = new Map();
|
|
80
|
+
for (const i of insights)
|
|
81
|
+
if (!bySession.has(i.session))
|
|
82
|
+
bySession.set(i.session, i);
|
|
83
|
+
const rows = [];
|
|
84
|
+
for (const l of labels) {
|
|
85
|
+
const ins = bySession.get(l.session);
|
|
86
|
+
if (!ins)
|
|
87
|
+
continue;
|
|
88
|
+
const uses = Number(ins.toolUses ?? 0);
|
|
89
|
+
const outcome = ins.outcome === 'ok' ? 2 : ins.outcome === 'turn-budget' ? 1 : 0;
|
|
90
|
+
rows.push({
|
|
91
|
+
f: { outcome, toolFailRate: 0, turns: Number(ins.turns ?? 0), toolUses: uses },
|
|
92
|
+
y: l.score / 5,
|
|
93
|
+
session: l.session,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
/** Fit the reward model on joined human labels. Deterministic. */
|
|
99
|
+
export function fitRewardModel(labels, insights) {
|
|
100
|
+
const rows = trainingRows(labels, insights);
|
|
101
|
+
const { train, hold } = split(rows);
|
|
102
|
+
const w = fitLinear(train);
|
|
103
|
+
return {
|
|
104
|
+
w: w,
|
|
105
|
+
trainedAt: new Date().toISOString(),
|
|
106
|
+
nSamples: rows.length,
|
|
107
|
+
trainRmse: Number(rmse(w, train).toFixed(4)),
|
|
108
|
+
...(hold.length >= 4 ? { holdoutRmse: Number(rmse(w, hold).toFixed(4)) } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Score a run's features with fitted weights -> reward in [0,1]. */
|
|
112
|
+
export function scoreFeatures(w, f) {
|
|
113
|
+
const xv = x(f);
|
|
114
|
+
let z = 0;
|
|
115
|
+
for (let k = 0; k < w.length; k++)
|
|
116
|
+
z += w[k] * xv[k];
|
|
117
|
+
return sigmoid(z);
|
|
118
|
+
}
|
|
119
|
+
/** Persistence: HMH_HOME/evolution/reward-model.json. */
|
|
120
|
+
export async function saveRewardModel(home, m) {
|
|
121
|
+
const dir = join(home, 'evolution');
|
|
122
|
+
await mkdir(dir, { recursive: true });
|
|
123
|
+
await writeFile(join(dir, 'reward-model.json'), JSON.stringify(m, null, 2) + '\n', 'utf8');
|
|
124
|
+
}
|
|
125
|
+
export async function loadRewardModel(home) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(await readFile(join(home, 'evolution', 'reward-model.json'), 'utf8'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Build DPO pairs from joined labels + insights. Two classes:
|
|
134
|
+
* (a) IN-BUCKET (gold): same outcome, >=2 star gap - human preference the
|
|
135
|
+
* outcome reward cannot see (4-vs-5 star distinctions);
|
|
136
|
+
* (b) CROSS-BUCKET (classic): ok-vs-degraded completions of the SAME task
|
|
137
|
+
* template (task prefix match) - the textbook chosen/rejected pair.
|
|
138
|
+
* Pure - testable. */
|
|
139
|
+
export function exportDpoPairs(labels, insights, taskOf, answerOf, opts = {}) {
|
|
140
|
+
const minGap = opts.minInBucketGap ?? 0.4;
|
|
141
|
+
const prefixLen = opts.crossBucketPrefix ?? 60;
|
|
142
|
+
const rows = trainingRows(labels, insights);
|
|
143
|
+
const insOf = new Map(insights.map((i) => [i.session, i]));
|
|
144
|
+
const pairs = [];
|
|
145
|
+
for (const a of rows) {
|
|
146
|
+
for (const b of rows) {
|
|
147
|
+
if (a.session >= b.session)
|
|
148
|
+
continue;
|
|
149
|
+
const gap = a.y - b.y;
|
|
150
|
+
if (gap < minGap)
|
|
151
|
+
continue;
|
|
152
|
+
const hi = gap > 0 ? a : b;
|
|
153
|
+
const lo = gap > 0 ? b : a;
|
|
154
|
+
const hiIns = insOf.get(hi.session);
|
|
155
|
+
const loIns = insOf.get(lo.session);
|
|
156
|
+
if (!hiIns || !loIns)
|
|
157
|
+
continue;
|
|
158
|
+
const sameOutcome = hiIns.outcome === loIns.outcome;
|
|
159
|
+
let ok = false;
|
|
160
|
+
if (sameOutcome) {
|
|
161
|
+
ok = true; // in-bucket gold
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// cross-bucket: only pair completions of the SAME task template
|
|
165
|
+
// (bench templates share long prefixes) - otherwise they teach
|
|
166
|
+
// nothing transferable
|
|
167
|
+
const ta = taskOf(hi.session);
|
|
168
|
+
const tb = taskOf(lo.session);
|
|
169
|
+
ok = ta.length >= prefixLen && ta.slice(0, prefixLen) === tb.slice(0, prefixLen);
|
|
170
|
+
}
|
|
171
|
+
if (!ok)
|
|
172
|
+
continue;
|
|
173
|
+
pairs.push({
|
|
174
|
+
prompt: taskOf(hi.session),
|
|
175
|
+
chosen: answerOf(hi.session),
|
|
176
|
+
rejected: answerOf(lo.session),
|
|
177
|
+
chosenSession: hi.session,
|
|
178
|
+
rejectedSession: lo.session,
|
|
179
|
+
gap: Number(gap.toFixed(2)),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return pairs;
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/evolution",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.10",
|
|
4
4
|
"description": "hmharness evolution subsystem: persistent memory, insight capture, skill library, and the bench that gives evolution its fitness signal. First-class kernel citizen, not a plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export interface BenchCase {
|
|
2
|
-
name: string;
|
|
3
|
-
prompt: string;
|
|
4
|
-
/** All substrings (split on &&) must appear in the output, case-insensitive. */
|
|
5
|
-
expect: string[];
|
|
6
|
-
/** Output must equal this string exactly (both trimmed). */
|
|
7
|
-
expectExact?: string;
|
|
8
|
-
/** Output must match this regex (whole output, case-sensitive). */
|
|
9
|
-
expectRegex?: string;
|
|
10
|
-
/** Substrings that must NOT appear (failure-marker exclusion). */
|
|
11
|
-
expectNone?: string[];
|
|
12
|
-
/** At least ONE of these substrings must appear. */
|
|
13
|
-
expectAny?: string[];
|
|
14
|
-
/** When true the case runs through the full agent loop with tools. */
|
|
15
|
-
tools: boolean;
|
|
16
|
-
/** Holdout cases are excluded from the promotion gate and re-verify after promotion (anti-memorization, GDPevo style). */
|
|
17
|
-
holdout: boolean;
|
|
18
|
-
/** Candidate token-cost ceiling as a multiple of the baseline run. */
|
|
19
|
-
costCap?: number;
|
|
20
|
-
}
|
|
21
|
-
export interface BenchResult {
|
|
22
|
-
name: string;
|
|
23
|
-
pass: boolean;
|
|
24
|
-
detail: string;
|
|
25
|
-
}
|
|
26
|
-
export declare function matchExpect(output: string, expect: string[]): boolean;
|
|
27
|
-
/** Fence convention: when the reply is a fenced block, assertions test the
|
|
28
|
-
* fence INNER content (day-55 finding: models preserve literals verbatim
|
|
29
|
-
* inside code fences while normalizing them in prose - the fence is the
|
|
30
|
-
* reply convention that makes exactness attainable for habit-prone models).
|
|
31
|
-
* Tolerates a trailing space after the opening fence markers. */
|
|
32
|
-
export declare function fenceInner(output: string): string;
|
|
33
|
-
/** Full structured assertion: every declared mode must hold. */
|
|
34
|
-
export declare function matchCase(output: string, c: Pick<BenchCase, 'expect' | 'expectExact' | 'expectRegex' | 'expectNone' | 'expectAny'>): {
|
|
35
|
-
pass: boolean;
|
|
36
|
-
detail: string;
|
|
37
|
-
};
|
|
38
|
-
export declare function listCases(home: string): Promise<BenchCase[]>;
|
|
39
|
-
export declare function runBench(home: string, run: (c: BenchCase) => Promise<string>): Promise<{
|
|
40
|
-
results: BenchResult[];
|
|
41
|
-
passRate: number;
|
|
42
|
-
}>;
|
|
43
|
-
/** Starter cases so the evolve gate has a signal from day one. */
|
|
44
|
-
export declare function seedCases(home: string): Promise<string[]>;
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @hmharness/evolution - bench
|
|
3
|
-
* The fitness signal for self-evolution. bench/cases/*.task files are:
|
|
4
|
-
* line 1 the prompt
|
|
5
|
-
* expect: a && b substrings that must ALL appear in the final output
|
|
6
|
-
* expect-exact: "..." the output equals this string (trimmed)
|
|
7
|
-
* expect-regex: ... a regex the output must match
|
|
8
|
-
* expect-none: a && b substrings that must NOT appear
|
|
9
|
-
* expect-any: a || b one of these substrings must appear
|
|
10
|
-
* tools: loop (optional) run through the full agent loop with tools
|
|
11
|
-
* cost-cap: 1.3 (optional) candidate cost multiplier ceiling vs baseline
|
|
12
|
-
* Skill or prompt changes must keep the bench green before promotion - the
|
|
13
|
-
* evolve loop enforces this A/B (baseline vs candidate).
|
|
14
|
-
*
|
|
15
|
-
* Assertion upgrade (gate methodology): plain substring matching lets
|
|
16
|
-
* verbose-but-wrong outputs pass; the structured modes pin exact shapes,
|
|
17
|
-
* forbid failure markers, and (with cost-cap) stop a candidate that only
|
|
18
|
-
* passes by burning 3x tokens. Old files keep working - `expect:` keeps
|
|
19
|
-
* its ALL-substrings semantics.
|
|
20
|
-
*/
|
|
21
|
-
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
export function matchExpect(output, expect) {
|
|
24
|
-
const lower = output.toLowerCase();
|
|
25
|
-
return expect.every((e) => lower.includes(e.toLowerCase()));
|
|
26
|
-
}
|
|
27
|
-
/** Fence convention: when the reply is a fenced block, assertions test the
|
|
28
|
-
* fence INNER content (day-55 finding: models preserve literals verbatim
|
|
29
|
-
* inside code fences while normalizing them in prose - the fence is the
|
|
30
|
-
* reply convention that makes exactness attainable for habit-prone models).
|
|
31
|
-
* Tolerates a trailing space after the opening fence markers. */
|
|
32
|
-
export function fenceInner(output) {
|
|
33
|
-
const m = output.match(/```[a-zA-Z]*[ \t]*\r?\n([\s\S]*?)(?:\r?\n)?```/);
|
|
34
|
-
return m ? m[1] : output;
|
|
35
|
-
}
|
|
36
|
-
/** Full structured assertion: every declared mode must hold. */
|
|
37
|
-
export function matchCase(output, c) {
|
|
38
|
-
const body = fenceInner(output);
|
|
39
|
-
if (c.expectExact !== undefined && body.trim() !== c.expectExact.trim()) {
|
|
40
|
-
return { pass: false, detail: `exact mismatch: got "${body.trim().slice(0, 80)}"` };
|
|
41
|
-
}
|
|
42
|
-
if (c.expectRegex !== undefined) {
|
|
43
|
-
try {
|
|
44
|
-
if (!new RegExp(c.expectRegex).test(body))
|
|
45
|
-
return { pass: false, detail: `regex mismatch: /${c.expectRegex.slice(0, 60)}/` };
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return { pass: false, detail: `invalid regex in case: ${c.expectRegex.slice(0, 40)}` };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (c.expectNone && c.expectNone.some((e) => body.toLowerCase().includes(e.toLowerCase()))) {
|
|
52
|
-
return { pass: false, detail: `forbidden marker present: ${c.expectNone.join(' && ')}` };
|
|
53
|
-
}
|
|
54
|
-
if (c.expectAny && !c.expectAny.some((e) => body.toLowerCase().includes(e.toLowerCase()))) {
|
|
55
|
-
return { pass: false, detail: `none of the allowed markers found: ${c.expectAny.join(' || ')}` };
|
|
56
|
-
}
|
|
57
|
-
if (!matchExpect(body, c.expect)) {
|
|
58
|
-
return { pass: false, detail: `missing "${c.expect.join('" && "')}" in output` };
|
|
59
|
-
}
|
|
60
|
-
return { pass: true, detail: 'ok' };
|
|
61
|
-
}
|
|
62
|
-
export async function listCases(home) {
|
|
63
|
-
const dir = join(home, 'bench', 'cases');
|
|
64
|
-
let files;
|
|
65
|
-
try {
|
|
66
|
-
files = (await readdir(dir)).filter((f) => f.endsWith('.task'));
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
const cases = [];
|
|
72
|
-
for (const f of files.sort()) {
|
|
73
|
-
const raw = (await readFile(join(dir, f), 'utf8')).trim();
|
|
74
|
-
const lines = raw.split('\n');
|
|
75
|
-
const prompt = lines[0];
|
|
76
|
-
const pick = (key) => {
|
|
77
|
-
const l = lines.find((x) => x.startsWith(key + ':'));
|
|
78
|
-
return l ? l.slice(key.length + 1).trim() : undefined;
|
|
79
|
-
};
|
|
80
|
-
const list = (key) => {
|
|
81
|
-
const v = pick(key);
|
|
82
|
-
return v ? v.split(/&&|\|\|/).map((s) => s.trim()).filter(Boolean) : undefined;
|
|
83
|
-
};
|
|
84
|
-
const costCap = Number(pick('cost-cap'));
|
|
85
|
-
cases.push({
|
|
86
|
-
name: f.replace(/\.task$/, ''),
|
|
87
|
-
prompt,
|
|
88
|
-
expect: list('expect') ?? [],
|
|
89
|
-
expectExact: pick('expect-exact')?.replace(/^"|"$/g, ''),
|
|
90
|
-
expectRegex: pick('expect-regex'),
|
|
91
|
-
expectNone: list('expect-none'),
|
|
92
|
-
expectAny: list('expect-any'),
|
|
93
|
-
tools: pick('tools') === 'loop',
|
|
94
|
-
holdout: pick('holdout') === 'true',
|
|
95
|
-
costCap: Number.isFinite(costCap) && costCap > 0 ? costCap : undefined,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
return cases;
|
|
99
|
-
}
|
|
100
|
-
export async function runBench(home, run) {
|
|
101
|
-
const cases = await listCases(home);
|
|
102
|
-
const results = [];
|
|
103
|
-
for (const c of cases) {
|
|
104
|
-
try {
|
|
105
|
-
const out = await run(c);
|
|
106
|
-
const r = matchCase(out, c);
|
|
107
|
-
results.push({ name: c.name, pass: r.pass, detail: r.detail });
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
results.push({ name: c.name, pass: false, detail: String(err).slice(0, 120) });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const passed = results.filter((r) => r.pass).length;
|
|
114
|
-
// persist a history record: readiness's "eval-regression-stable" condition
|
|
115
|
-
// reads evolution/benches/*.json - without these records the RL gate's
|
|
116
|
-
// stability condition can never be met (day-20 audit gap)
|
|
117
|
-
try {
|
|
118
|
-
const dir = join(home, 'evolution', 'benches');
|
|
119
|
-
await mkdir(dir, { recursive: true });
|
|
120
|
-
const record = {
|
|
121
|
-
time: new Date().toISOString(),
|
|
122
|
-
total: cases.length,
|
|
123
|
-
passed,
|
|
124
|
-
passRate: cases.length === 0 ? 1 : passed / cases.length,
|
|
125
|
-
results: results.map((r) => ({ name: r.name, pass: r.pass, detail: r.detail.slice(0, 200) })),
|
|
126
|
-
};
|
|
127
|
-
await writeFile(join(dir, `${record.time.replace(/[:.]/g, '-')}.json`), JSON.stringify(record, null, 2) + '\n', 'utf8');
|
|
128
|
-
}
|
|
129
|
-
catch { /* history is best-effort; the run result still returns */ }
|
|
130
|
-
return { results, passRate: cases.length === 0 ? 1 : passed / cases.length };
|
|
131
|
-
}
|
|
132
|
-
/** Starter cases so the evolve gate has a signal from day one. */
|
|
133
|
-
export async function seedCases(home) {
|
|
134
|
-
const dir = join(home, 'bench', 'cases');
|
|
135
|
-
const written = [];
|
|
136
|
-
const seeds = [
|
|
137
|
-
['toolchain-report.task', '运行鸿蒙工具链体检,然后逐项说出一共检查了哪三个工具的名称\nexpect: hdc && hvigorw && ohpm\ntools: loop\n'],
|
|
138
|
-
['reply-determinism.task', '只回复这六个字符,不要任何其他内容:HMH-OK\nexpect: HMH-OK\n'],
|
|
139
|
-
['toolchain-ohpm-status.task', '运行鸿蒙工具链体检,然后只回答 ohpm 的状态是 OK 还是 MISSING\nexpect: OK\ntools: loop\nholdout: true\n'],
|
|
140
|
-
];
|
|
141
|
-
let existing = [];
|
|
142
|
-
try {
|
|
143
|
-
existing = await readdir(dir);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
/* first seed */
|
|
147
|
-
}
|
|
148
|
-
for (const [name, body] of seeds) {
|
|
149
|
-
if (existing.includes(name))
|
|
150
|
-
continue;
|
|
151
|
-
await mkdir(dir, { recursive: true });
|
|
152
|
-
await writeFile(join(dir, name), body, 'utf8');
|
|
153
|
-
written.push(name);
|
|
154
|
-
}
|
|
155
|
-
return written;
|
|
156
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { BenchCase } from './bench.ts';
|
|
2
|
-
export type CandidateTarget = 'prompt' | 'skill' | 'context' | 'tool_policy' | 'model_router' | 'workflow' | 'harness';
|
|
3
|
-
export declare const CANDIDATE_TARGETS: CandidateTarget[];
|
|
4
|
-
export interface EvolutionCandidate {
|
|
5
|
-
id: string;
|
|
6
|
-
target: CandidateTarget;
|
|
7
|
-
/** the version this candidate is measured against (baseline pointer) */
|
|
8
|
-
baseVersion: string;
|
|
9
|
-
candidateVersion: string;
|
|
10
|
-
hypothesis: string;
|
|
11
|
-
/** metric the experiment reports on, e.g. 'bench.passRate' */
|
|
12
|
-
expectedMetric: string;
|
|
13
|
-
/** target-specific payload: skill markdown, prompt delta, router table… */
|
|
14
|
-
payload?: string;
|
|
15
|
-
/** who proposed it - 'agent' proposals on prompt/harness need human sign-off */
|
|
16
|
-
origin?: string;
|
|
17
|
-
createdAt: string;
|
|
18
|
-
}
|
|
19
|
-
export declare function registerCandidate(home: string, cand: Omit<EvolutionCandidate, 'id' | 'createdAt'> & {
|
|
20
|
-
id?: string;
|
|
21
|
-
}): Promise<EvolutionCandidate>;
|
|
22
|
-
export declare function listCandidates(home: string): Promise<EvolutionCandidate[]>;
|
|
23
|
-
export declare function getCandidate(home: string, id: string): Promise<EvolutionCandidate | null>;
|
|
24
|
-
/** Two-proportion two-sided test (treatment vs control pass rates). */
|
|
25
|
-
export declare function twoProportionTest(control: {
|
|
26
|
-
pass: number;
|
|
27
|
-
n: number;
|
|
28
|
-
}, treatment: {
|
|
29
|
-
pass: number;
|
|
30
|
-
n: number;
|
|
31
|
-
}): {
|
|
32
|
-
z: number;
|
|
33
|
-
p: number;
|
|
34
|
-
diff: number;
|
|
35
|
-
};
|
|
36
|
-
export interface ArmResult {
|
|
37
|
-
pass: boolean;
|
|
38
|
-
/** token cost of the arm run (cost-cap honesty, same currency as estTokens) */
|
|
39
|
-
tokens: number;
|
|
40
|
-
}
|
|
41
|
-
/** Injected by the caller: run one bench case under one arm. The runner owns
|
|
42
|
-
* the actual injection point (skills block, memory config, router table…). */
|
|
43
|
-
export type ArmRunner = (c: BenchCase, arm: 'control' | 'treatment') => Promise<ArmResult>;
|
|
44
|
-
export interface ExperimentArm {
|
|
45
|
-
pass: number;
|
|
46
|
-
n: number;
|
|
47
|
-
passRate: number;
|
|
48
|
-
tokens: number;
|
|
49
|
-
/** per-case rows (day-56): which case flipped is the point of a diff */
|
|
50
|
-
results?: Array<{
|
|
51
|
-
name: string;
|
|
52
|
-
pass: boolean;
|
|
53
|
-
tokens: number;
|
|
54
|
-
}>;
|
|
55
|
-
}
|
|
56
|
-
export interface ExperimentReport {
|
|
57
|
-
candidateId: string;
|
|
58
|
-
ranAt: string;
|
|
59
|
-
cases: number;
|
|
60
|
-
control: ExperimentArm;
|
|
61
|
-
treatment: ExperimentArm;
|
|
62
|
-
diff: number;
|
|
63
|
-
z: number;
|
|
64
|
-
p: number;
|
|
65
|
-
verdict: 'promote-eligible' | 'reject' | 'needs-data';
|
|
66
|
-
reason: string;
|
|
67
|
-
}
|
|
68
|
-
/** Per-arm minimum before any verdict counts (aligned with impact.ts canary
|
|
69
|
-
* threshold: >=8 sessions, and blueprint "one success is not significance"). */
|
|
70
|
-
export declare const MIN_ARM_N = 8;
|
|
71
|
-
export declare function verdictFor(control: {
|
|
72
|
-
pass: number;
|
|
73
|
-
n: number;
|
|
74
|
-
}, treatment: {
|
|
75
|
-
pass: number;
|
|
76
|
-
n: number;
|
|
77
|
-
}): {
|
|
78
|
-
verdict: ExperimentReport['verdict'];
|
|
79
|
-
reason: string;
|
|
80
|
-
z: number;
|
|
81
|
-
p: number;
|
|
82
|
-
diff: number;
|
|
83
|
-
};
|
|
84
|
-
export declare function runCandidateExperiment(home: string, candidateId: string, opts: {
|
|
85
|
-
runCase: ArmRunner;
|
|
86
|
-
cases: BenchCase[];
|
|
87
|
-
maxCases?: number;
|
|
88
|
-
}): Promise<ExperimentReport>;
|
|
89
|
-
/** Latest experiment report for a candidate (promotion evidence). */
|
|
90
|
-
export declare function latestExperiment(home: string, candidateId: string): Promise<ExperimentReport | null>;
|
|
91
|
-
export interface ActiveVersion {
|
|
92
|
-
target: CandidateTarget;
|
|
93
|
-
version: string;
|
|
94
|
-
candidateId: string;
|
|
95
|
-
since: string;
|
|
96
|
-
reportAt?: string;
|
|
97
|
-
}
|
|
98
|
-
export declare function activeVersions(home: string): Promise<Record<string, ActiveVersion>>;
|
|
99
|
-
export interface PromoteResult {
|
|
100
|
-
ok: boolean;
|
|
101
|
-
error?: string;
|
|
102
|
-
active?: ActiveVersion;
|
|
103
|
-
}
|
|
104
|
-
/** Blueprint gates, enforced: latest report must be promote-eligible, a
|
|
105
|
-
* previous pointer is written BEFORE activation, agent-origin prompt/harness
|
|
106
|
-
* candidates need approvedByHuman. Skill promotions ride the existing
|
|
107
|
-
* promoteSkill canary channel. */
|
|
108
|
-
export declare function promoteCandidate(home: string, candidateId: string, opts?: {
|
|
109
|
-
approvedByHuman?: boolean;
|
|
110
|
-
}): Promise<PromoteResult>;
|
|
111
|
-
export declare function rollbackCandidate(home: string, target: CandidateTarget): Promise<{
|
|
112
|
-
ok: boolean;
|
|
113
|
-
error?: string;
|
|
114
|
-
}>;
|