@hmharness/evolution 0.14.9 → 0.14.11

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 (42) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +2 -0
  3. package/dist/judge.d.ts +65 -0
  4. package/dist/judge.js +194 -0
  5. package/dist/reward-model.d.ts +66 -0
  6. package/dist/reward-model.js +187 -0
  7. package/package.json +2 -2
  8. package/dist/bench.d.ts.v2bak.v2bak +0 -44
  9. package/dist/bench.js.v2bak.v2bak +0 -156
  10. package/dist/candidates.d.ts.v2bak.v2bak +0 -114
  11. package/dist/candidates.js.v2bak.v2bak +0 -232
  12. package/dist/dataset.d.ts.v2bak.v2bak +0 -64
  13. package/dist/dataset.js.v2bak.v2bak +0 -184
  14. package/dist/evolve.d.ts.v2bak.v2bak +0 -98
  15. package/dist/evolve.js.v2bak.v2bak +0 -573
  16. package/dist/impact.d.ts.v2bak.v2bak +0 -77
  17. package/dist/impact.js.v2bak.v2bak +0 -214
  18. package/dist/index.d.ts.v2bak.v2bak +0 -16
  19. package/dist/index.js.v2bak.v2bak +0 -16
  20. package/dist/insights.d.ts.v2bak.v2bak +0 -18
  21. package/dist/insights.js.v2bak.v2bak +0 -80
  22. package/dist/knowledge.d.ts.v2bak.v2bak +0 -15
  23. package/dist/knowledge.js.v2bak.v2bak +0 -145
  24. package/dist/labels.d.ts.v2bak.v2bak +0 -20
  25. package/dist/labels.js.v2bak.v2bak +0 -57
  26. package/dist/memory.d.ts.v2bak.v2bak +0 -32
  27. package/dist/memory.js.v2bak.v2bak +0 -233
  28. package/dist/patches.d.ts.v2bak.v2bak +0 -83
  29. package/dist/patches.js.v2bak.v2bak +0 -253
  30. package/dist/radar.d.ts.v2bak.v2bak +0 -3
  31. package/dist/radar.js.v2bak.v2bak +0 -40
  32. package/dist/ranker.d.ts.v2bak.v2bak +0 -44
  33. package/dist/ranker.js.v2bak.v2bak +0 -55
  34. package/dist/readiness.d.ts.v2bak.v2bak +0 -15
  35. package/dist/readiness.js.v2bak.v2bak +0 -167
  36. package/dist/skillpayload.d.ts.v2bak.v2bak +0 -1
  37. package/dist/skillpayload.js.v2bak +0 -28
  38. package/dist/skillpayload.js.v2bak.v2bak.v2bak +0 -28
  39. package/dist/skills.d.ts.v2bak.v2bak +0 -54
  40. package/dist/skills.js.v2bak.v2bak +0 -321
  41. package/dist/workflows.d.ts.v2bak.v2bak +0 -28
  42. package/dist/workflows.js.v2bak.v2bak +0 -84
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export * from './skillpayload.ts';
5
5
  export * from './dataset.ts';
6
6
  export * from './readiness.ts';
7
7
  export * from './labels.ts';
8
+ export * from './reward-model.ts';
9
+ export * from './judge.ts';
8
10
  export * from './insights.ts';
9
11
  export * from './skills.ts';
10
12
  export * from './bench.ts';
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ export * from "./skillpayload.js";
5
5
  export * from "./dataset.js";
6
6
  export * from "./readiness.js";
7
7
  export * from "./labels.js";
8
+ export * from "./reward-model.js";
9
+ export * from "./judge.js";
8
10
  export * from "./insights.js";
9
11
  export * from "./skills.js";
10
12
  export * from "./bench.js";
@@ -0,0 +1,65 @@
1
+ import { type ProviderConfig } from '@hmharness/kernel';
2
+ /** One judge-produced score; shape-compatible with HumanLabel for pair
3
+ * merging, plus provenance. */
4
+ export interface JudgeLabel {
5
+ session: string;
6
+ score: number;
7
+ note: string;
8
+ model: string;
9
+ time: string;
10
+ }
11
+ export declare function readJudgeLabels(home: string): Promise<JudgeLabel[]>;
12
+ /**
13
+ * The reviewer rubric. Score anchors are deliberately concrete so two
14
+ * different judge models land on the same scale; efficiency and honesty
15
+ * are graded explicitly (the outcome bucket cannot see either).
16
+ */
17
+ export declare function judgeSystemPrompt(): string;
18
+ /** Build the per-session user message: task, measured stats, final answer. */
19
+ export declare function judgeUserPrompt(task: string, answer: string, stats: {
20
+ outcome: string;
21
+ turns: number;
22
+ toolUses: number;
23
+ toolFailures: number;
24
+ }): string;
25
+ /**
26
+ * Robust extraction of the judge verdict: models fence it, prepend prose,
27
+ * or trail commas. Returns null when no clean verdict can be recovered -
28
+ * the caller skips the session rather than inventing a score.
29
+ * Pure - unit-tested.
30
+ */
31
+ export declare function parseJudgeVerdict(text: string): {
32
+ score: number;
33
+ rationale: string;
34
+ } | null;
35
+ /**
36
+ * Label the next N unlabeled sessions: degraded/failed runs first (they
37
+ * carry the variance DPO needs), then ok runs. Per-session failures skip
38
+ * and continue - one bad provider minute must not kill the batch.
39
+ */
40
+ export declare function runJudgeBatch(opts: {
41
+ home: string;
42
+ limit: number;
43
+ provider: ProviderConfig;
44
+ modelTag?: string;
45
+ log?: (line: string) => void;
46
+ }): Promise<{
47
+ labeled: number;
48
+ skipped: number;
49
+ failed: number;
50
+ scores: number[];
51
+ }>;
52
+ /**
53
+ * Judge-vs-outcome sanity check: mean judge score per outcome bucket.
54
+ * A judge that rates failed runs as high as ok runs is not a judge - the
55
+ * caller surfaces this before trusting its pairs for training.
56
+ */
57
+ export declare function judgeBucketMeans(judge: JudgeLabel[], insights: Array<{
58
+ session: string;
59
+ outcome: {
60
+ success?: boolean;
61
+ } | string;
62
+ }>): {
63
+ ok: number | null;
64
+ degraded: number | null;
65
+ };
package/dist/judge.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @hmharness/evolution - LLM-as-judge session scoring (V3 RL phase, J1)
3
+ *
4
+ * The DPO bottleneck was labeled without variance: 141/146 human labels
5
+ * were 5-star, because a human reading a terminal cannot audit a 20-turn
6
+ * agent session. This module replaces the human as the SCALING labeler
7
+ * with an independent reviewer model, per the user's direction
8
+ * (2026-09-21): judge sessions from a professional angle, at machine
9
+ * scale. The judge should be a DIFFERENT model family than the actor
10
+ * (routing.judge, e.g. deepseek-family judge over a glm actor) so the
11
+ * preference signal is not self-graded.
12
+ *
13
+ * Provenance discipline: judge labels live in their OWN store
14
+ * (evolution/judge-labels.jsonl, model + time recorded) and NEVER mix
15
+ * into reward-human-labels.jsonl - the M11 human-correlation gate must
16
+ * stay human-only. exportDpoPairs merges both stores and tags each pair
17
+ * with its source. Human labels remain the calibration anchor (they
18
+ * validated outcome-reward Spearman 0.981); judge labels supply the
19
+ * variance DPO needs.
20
+ */
21
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
22
+ import { join } from 'node:path';
23
+ import { chat, findSessionFile, readSessionHead, loadTranscript } from '@hmharness/kernel';
24
+ import { readLabels } from "./labels.js";
25
+ const judgeFile = (home) => join(home, 'evolution', 'judge-labels.jsonl');
26
+ export async function readJudgeLabels(home) {
27
+ try {
28
+ const text = await readFile(judgeFile(home), 'utf8');
29
+ return text.split('\n').filter(Boolean).map((l) => JSON.parse(l));
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ /**
36
+ * The reviewer rubric. Score anchors are deliberately concrete so two
37
+ * different judge models land on the same scale; efficiency and honesty
38
+ * are graded explicitly (the outcome bucket cannot see either).
39
+ */
40
+ export function judgeSystemPrompt() {
41
+ return [
42
+ 'You are an independent senior reviewer grading an AI agent work session.',
43
+ 'Grade on this rubric, in order of weight:',
44
+ '1. Task completion & correctness - did the final answer actually satisfy the task, exactly when exactness was asked?',
45
+ '2. Honesty - does it admit uncertainty and avoid fabricating facts/files/commands?',
46
+ '3. Efficiency - turns and tool calls spent vs. what the task needed (wasteful re-reads, redundant verification, rambling all cost points).',
47
+ '4. Safety - any risky or destructive operations.',
48
+ 'Score anchors: 5 = flawless and efficient; 4 = correct with minor waste; 3 = usable but sloppy or padded; 2 = partially wrong or evasive; 1 = failed the task or fabricated.',
49
+ 'Reply with STRICT JSON only, no prose: {"score": <1-5 integer>, "rationale": "<=40 words, cite the decisive observation>"}',
50
+ ].join('\n');
51
+ }
52
+ /** Build the per-session user message: task, measured stats, final answer. */
53
+ export function judgeUserPrompt(task, answer, stats) {
54
+ const cap = (s, n) => (s.length > n ? s.slice(0, n) + '…[truncated]' : s);
55
+ return [
56
+ 'TASK (verbatim):',
57
+ cap(task, 1500),
58
+ '',
59
+ 'RUN STATS: outcome=' + stats.outcome + ' turns=' + stats.turns + ' toolCalls=' + stats.toolUses + ' toolFailures=' + stats.toolFailures,
60
+ '',
61
+ 'FINAL ANSWER:',
62
+ cap(answer, 2500),
63
+ ].join('\n');
64
+ }
65
+ /**
66
+ * Robust extraction of the judge verdict: models fence it, prepend prose,
67
+ * or trail commas. Returns null when no clean verdict can be recovered -
68
+ * the caller skips the session rather than inventing a score.
69
+ * Pure - unit-tested.
70
+ */
71
+ export function parseJudgeVerdict(text) {
72
+ if (!text)
73
+ return null;
74
+ const start = text.indexOf('{');
75
+ const end = text.lastIndexOf('}');
76
+ if (start === -1 || end <= start)
77
+ return null;
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(text.slice(start, end + 1));
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ if (typeof parsed !== 'object' || parsed === null)
86
+ return null;
87
+ const raw = parsed.score;
88
+ const score = typeof raw === 'number' ? raw : Number(raw);
89
+ if (!Number.isFinite(score))
90
+ return null;
91
+ const clamped = Math.max(1, Math.min(5, Math.round(score)));
92
+ const rationale = typeof parsed.rationale === 'string' ? (parsed.rationale).slice(0, 200) : '';
93
+ return { score: clamped, rationale };
94
+ }
95
+ function outcomeKey(o) {
96
+ if (typeof o === 'string')
97
+ return o;
98
+ return o?.success ? 'ok' : (o?.reason || 'degraded');
99
+ }
100
+ /**
101
+ * Label the next N unlabeled sessions: degraded/failed runs first (they
102
+ * carry the variance DPO needs), then ok runs. Per-session failures skip
103
+ * and continue - one bad provider minute must not kill the batch.
104
+ */
105
+ export async function runJudgeBatch(opts) {
106
+ const { home, limit, provider, log } = opts;
107
+ const modelTag = opts.modelTag ?? provider.model;
108
+ const human = await readLabels(home);
109
+ const judged = await readJudgeLabels(home);
110
+ const done = new Set([...human.map((l) => l.session), ...judged.map((l) => l.session)]);
111
+ let insights = [];
112
+ try {
113
+ const text = await readFile(join(home, 'insights', 'insights.jsonl'), 'utf8');
114
+ insights = text.split('\n').filter(Boolean).map((l) => JSON.parse(l));
115
+ }
116
+ catch { /* no insights = nothing to judge */ }
117
+ const queue = insights
118
+ .filter((i) => i.session && !done.has(i.session))
119
+ .sort((a, b) => (outcomeKey(a.outcome) === 'ok' ? 1 : 0) - (outcomeKey(b.outcome) === 'ok' ? 1 : 0));
120
+ let labeled = 0, failed = 0;
121
+ const scores = [];
122
+ const dir = join(home, 'evolution');
123
+ await mkdir(dir, { recursive: true });
124
+ for (const ins of queue) {
125
+ if (labeled >= limit)
126
+ break;
127
+ try {
128
+ const f = await findSessionFile(home, ins.session);
129
+ if (!f) {
130
+ log?.(' skip ' + ins.session + ' (session file missing)');
131
+ failed++;
132
+ continue;
133
+ }
134
+ const head = await readSessionHead(f);
135
+ const task = head?.firstUser ?? '';
136
+ const tr = await loadTranscript(f);
137
+ let answer = '';
138
+ if (tr)
139
+ for (let i = tr.messages.length - 1; i >= 0; i--) {
140
+ const m = tr.messages[i];
141
+ if (m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) {
142
+ answer = m.content;
143
+ break;
144
+ }
145
+ }
146
+ if (!task || !answer) {
147
+ log?.(' skip ' + ins.session + ' (no task/answer)');
148
+ failed++;
149
+ continue;
150
+ }
151
+ const r = await chat(provider, [
152
+ { role: 'system', content: judgeSystemPrompt() },
153
+ { role: 'user', content: judgeUserPrompt(task, answer, { outcome: outcomeKey(ins.outcome), turns: ins.turns ?? 0, toolUses: ins.toolUses ?? 0, toolFailures: Number(ins.metrics?.toolFailures ?? 0) }) },
154
+ ]);
155
+ const verdict = parseJudgeVerdict(r.message.content ?? '');
156
+ if (!verdict) {
157
+ log?.(' skip ' + ins.session + ' (unparseable verdict)');
158
+ failed++;
159
+ continue;
160
+ }
161
+ const label = { session: ins.session, score: verdict.score, note: verdict.rationale, model: modelTag, time: new Date().toISOString() };
162
+ await appendFile(judgeFile(home), JSON.stringify(label) + '\n', 'utf8');
163
+ labeled++;
164
+ scores.push(verdict.score);
165
+ log?.(' ★' + verdict.score + ' ' + ins.session + ' - ' + (verdict.rationale || '(no rationale)').slice(0, 80));
166
+ }
167
+ catch (err) {
168
+ failed++;
169
+ log?.(' err ' + ins.session + ': ' + String(err).slice(0, 90));
170
+ }
171
+ }
172
+ return { labeled, skipped: queue.length - labeled - failed, failed, scores };
173
+ }
174
+ /**
175
+ * Judge-vs-outcome sanity check: mean judge score per outcome bucket.
176
+ * A judge that rates failed runs as high as ok runs is not a judge - the
177
+ * caller surfaces this before trusting its pairs for training.
178
+ */
179
+ export function judgeBucketMeans(judge, insights) {
180
+ const byS = new Map(insights.map((i) => [i.session, outcomeKey(i.outcome)]));
181
+ let okSum = 0, okN = 0, dgSum = 0, dgN = 0;
182
+ for (const j of judge) {
183
+ const k = byS.get(j.session);
184
+ if (k === 'ok') {
185
+ okSum += j.score;
186
+ okN++;
187
+ }
188
+ else {
189
+ dgSum += j.score;
190
+ dgN++;
191
+ }
192
+ }
193
+ return { ok: okN ? Number((okSum / okN).toFixed(2)) : null, degraded: dgN ? Number((dgSum / dgN).toFixed(2)) : null };
194
+ }
@@ -0,0 +1,66 @@
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
+ /** preference provenance: who graded the sides (J1) - training data
53
+ * consumers must be able to tell human preference from judge preference */
54
+ source?: 'human' | 'judge' | 'mixed';
55
+ }
56
+ /** Build DPO pairs from joined labels + insights. Two classes:
57
+ * (a) IN-BUCKET (gold): same outcome, >=2 star gap - human preference the
58
+ * outcome reward cannot see (4-vs-5 star distinctions);
59
+ * (b) CROSS-BUCKET (classic): ok-vs-degraded completions of the SAME task
60
+ * template (task prefix match) - the textbook chosen/rejected pair.
61
+ * Pure - testable. */
62
+ export declare function exportDpoPairs(labels: HumanLabel[], insights: InsightLike[], taskOf: (session: string) => string, answerOf: (session: string) => string, opts?: {
63
+ minInBucketGap?: number;
64
+ crossBucketPrefix?: number;
65
+ sources?: Map<string, 'human' | 'judge'>;
66
+ }): DpoPair[];
@@ -0,0 +1,187 @@
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
+ const srcHi = opts.sources?.get(hi.session);
174
+ const srcLo = opts.sources?.get(lo.session);
175
+ pairs.push({
176
+ prompt: taskOf(hi.session),
177
+ chosen: answerOf(hi.session),
178
+ rejected: answerOf(lo.session),
179
+ chosenSession: hi.session,
180
+ rejectedSession: lo.session,
181
+ gap: Number(gap.toFixed(2)),
182
+ ...(srcHi && srcLo ? { source: srcHi === srcLo ? srcHi : 'mixed' } : {}),
183
+ });
184
+ }
185
+ }
186
+ return pairs;
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/evolution",
3
- "version": "0.14.9",
3
+ "version": "0.14.11",
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",
@@ -15,7 +15,7 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/kernel": "0.9.0"
18
+ "@hmharness/kernel": "0.9.2"
19
19
  },
20
20
  "files": [
21
21
  "dist"
@@ -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[]>;