@miller-tech/uap 1.108.1 → 1.109.1
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/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +23 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/self-harness.d.ts +26 -3
- package/dist/cli/self-harness.d.ts.map +1 -1
- package/dist/cli/self-harness.js +151 -6
- package/dist/cli/self-harness.js.map +1 -1
- package/dist/dashboard/savings.js +1 -1
- package/dist/dashboard/savings.js.map +1 -1
- package/dist/self-harness/index.d.ts +9 -5
- package/dist/self-harness/index.d.ts.map +1 -1
- package/dist/self-harness/index.js +9 -5
- package/dist/self-harness/index.js.map +1 -1
- package/dist/self-harness/profile.d.ts +22 -0
- package/dist/self-harness/profile.d.ts.map +1 -1
- package/dist/self-harness/profile.js +22 -1
- package/dist/self-harness/profile.js.map +1 -1
- package/dist/self-harness/run.d.ts +72 -0
- package/dist/self-harness/run.d.ts.map +1 -0
- package/dist/self-harness/run.js +83 -0
- package/dist/self-harness/run.js.map +1 -0
- package/dist/self-harness/validate.d.ts +72 -0
- package/dist/self-harness/validate.d.ts.map +1 -0
- package/dist/self-harness/validate.js +138 -0
- package/dist/self-harness/validate.js.map +1 -0
- package/docs/design/SELF_HARNESS.md +13 -6
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/web/dashboard.html +12 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Harness — the REAL validator (Stage 3), wiring the orchestrator's injected
|
|
3
|
+
* `Validator` seam to the existing paired benchmark (`benchmarks/paired/`).
|
|
4
|
+
*
|
|
5
|
+
* For a candidate `env` Mod the physical A/B is a *server-side* change: the two
|
|
6
|
+
* arms differ only in the inference server's launch env, so they cannot run in a
|
|
7
|
+
* single paired pass. This validator therefore:
|
|
8
|
+
*
|
|
9
|
+
* 1. runs the suite as the `baseline` arm (current harness);
|
|
10
|
+
* 2. physically applies the Mod to the env file + restarts the server;
|
|
11
|
+
* 3. runs the suite again as the `candidate` arm;
|
|
12
|
+
* 4. REVERTS the env file + restarts (each Mod is validated in isolation — §7);
|
|
13
|
+
* 5. pairs the two arms cell-for-cell (`taskId#seed`) and runs `analyze` to get
|
|
14
|
+
* the same paired statistics `uap bench paired` ships.
|
|
15
|
+
*
|
|
16
|
+
* The held-out suite (disjoint tasks) runs the same way to catch overfitting. The
|
|
17
|
+
* suite-arm runner and the server restart are INJECTED so the loop is testable
|
|
18
|
+
* without a live llama server.
|
|
19
|
+
*
|
|
20
|
+
* `scaffold` / `middleware` Mods are NOT auto-validated here — their physical A/B
|
|
21
|
+
* is a prompt/proxy change that the autonomous env loop deliberately does not
|
|
22
|
+
* own; they route through the human-gated pending queue (§9). Passing one returns
|
|
23
|
+
* a null (no-lift) comparison so `decide` rejects it, never silently applying it.
|
|
24
|
+
*
|
|
25
|
+
* See docs/design/SELF_HARNESS.md §4, §7, §11.
|
|
26
|
+
*/
|
|
27
|
+
import { runPaired } from '../benchmarks/paired/runner.js';
|
|
28
|
+
import { analyze } from '../benchmarks/paired/report.js';
|
|
29
|
+
import { makeFullCondition, } from '../benchmarks/paired/types.js';
|
|
30
|
+
import { applyEnvModToFile } from './profile.js';
|
|
31
|
+
import { invertMod, describeMod } from './mods.js';
|
|
32
|
+
const BASELINE_LABEL = 'baseline';
|
|
33
|
+
const CANDIDATE_LABEL = 'candidate';
|
|
34
|
+
/**
|
|
35
|
+
* Build a real, live-suite `Validator` for the orchestrator. Only `env` Mods are
|
|
36
|
+
* validated against the server; other kinds return a null comparison (reject).
|
|
37
|
+
*/
|
|
38
|
+
export function buildValidator(deps) {
|
|
39
|
+
const components = deps.components ?? makeFullCondition().components;
|
|
40
|
+
const log = deps.log ?? (() => { });
|
|
41
|
+
const runArm = deps.runArm ?? defaultArmRunner(deps, components);
|
|
42
|
+
return async (mod) => {
|
|
43
|
+
if (mod.kind !== 'env') {
|
|
44
|
+
log(` ${describeMod(mod)} is ${mod.kind}, not env — routed to human gate, no auto-validation`);
|
|
45
|
+
return { validation: nullComparison(), heldout: null };
|
|
46
|
+
}
|
|
47
|
+
const envMod = mod;
|
|
48
|
+
const runSuite = async (dir) => {
|
|
49
|
+
// Arm 1 — baseline (current server state).
|
|
50
|
+
const baseRecs = await runArm(dir, BASELINE_LABEL, components);
|
|
51
|
+
// Apply the Mod + restart so the candidate arm sees the new env.
|
|
52
|
+
applyEnvModToFile(deps.envPath, envMod);
|
|
53
|
+
await deps.restart();
|
|
54
|
+
let candRecs;
|
|
55
|
+
try {
|
|
56
|
+
candRecs = await runArm(dir, CANDIDATE_LABEL, components);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
// Always revert to leave the server clean regardless of the arm outcome.
|
|
60
|
+
applyEnvModToFile(deps.envPath, invertMod(envMod));
|
|
61
|
+
await deps.restart();
|
|
62
|
+
}
|
|
63
|
+
return compare([...baseRecs, ...candRecs], deps.analyzeOpts);
|
|
64
|
+
};
|
|
65
|
+
log(` validating on ${deps.suiteDir}${deps.heldoutDir ? ` + held-out ${deps.heldoutDir}` : ''}`);
|
|
66
|
+
const validation = await runSuite(deps.suiteDir);
|
|
67
|
+
const heldout = deps.heldoutDir ? await runSuite(deps.heldoutDir) : null;
|
|
68
|
+
return { validation, heldout };
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Default arm runner: a single-condition `runPaired` over the suite. */
|
|
72
|
+
function defaultArmRunner(deps, _components) {
|
|
73
|
+
const adapter = deps.adapter;
|
|
74
|
+
if (!adapter) {
|
|
75
|
+
// Defer the error until first use so tests that inject runArm never hit it.
|
|
76
|
+
return async () => {
|
|
77
|
+
throw new Error('buildValidator: default runArm requires an `adapter` in deps');
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return async (suiteDir, label, components) => {
|
|
81
|
+
const { loadSuite } = await import('../benchmarks/paired/suite.js');
|
|
82
|
+
const { mkdtempSync } = await import('fs');
|
|
83
|
+
const { tmpdir } = await import('os');
|
|
84
|
+
const { join } = await import('path');
|
|
85
|
+
const tasks = loadSuite(suiteDir);
|
|
86
|
+
const condition = { label, components };
|
|
87
|
+
const workRoot = mkdtempSync(join(tmpdir(), 'sh-validate-'));
|
|
88
|
+
const out = await runPaired({
|
|
89
|
+
tasks,
|
|
90
|
+
conditions: [condition],
|
|
91
|
+
adapter,
|
|
92
|
+
model: deps.model ?? 'unknown',
|
|
93
|
+
epochs: deps.epochs ?? 5,
|
|
94
|
+
concurrency: deps.concurrency ?? 4,
|
|
95
|
+
workRoot,
|
|
96
|
+
}, suiteDir, new Date().toISOString());
|
|
97
|
+
return out.records;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Wrap combined baseline+candidate records and analyze into a Comparison. */
|
|
101
|
+
export function compare(records, analyzeOpts) {
|
|
102
|
+
const report = analyze({
|
|
103
|
+
records,
|
|
104
|
+
model: records[0]?.model ?? 'unknown',
|
|
105
|
+
adapter: records[0]?.adapter ?? 'unknown',
|
|
106
|
+
epochs: countSeeds(records),
|
|
107
|
+
startedAt: '',
|
|
108
|
+
finishedAt: '',
|
|
109
|
+
}, { ...analyzeOpts, baselineLabel: BASELINE_LABEL });
|
|
110
|
+
const cmp = report.comparisons.find((c) => c.label === CANDIDATE_LABEL);
|
|
111
|
+
if (!cmp) {
|
|
112
|
+
throw new Error(`compare: no '${CANDIDATE_LABEL}' comparison produced (labels: ${report.comparisons
|
|
113
|
+
.map((c) => c.label)
|
|
114
|
+
.join(', ')})`);
|
|
115
|
+
}
|
|
116
|
+
return cmp;
|
|
117
|
+
}
|
|
118
|
+
function countSeeds(records) {
|
|
119
|
+
return new Set(records.map((r) => r.seed)).size || 1;
|
|
120
|
+
}
|
|
121
|
+
/** A no-lift comparison (Δ=0, not significant) so `decide` rejects. */
|
|
122
|
+
export function nullComparison() {
|
|
123
|
+
return {
|
|
124
|
+
label: CANDIDATE_LABEL,
|
|
125
|
+
baseline: BASELINE_LABEL,
|
|
126
|
+
correctness: {
|
|
127
|
+
baselineRate: 0,
|
|
128
|
+
treatmentRate: 0,
|
|
129
|
+
delta: { meanDelta: 0, ci: { lower: 0, upper: 0 }, pValue: 1, n: 0, significant: false },
|
|
130
|
+
mcnemar: { bothCorrect: 0, onlyTreatment: 0, onlyBaseline: 0, bothWrong: 0, netGain: 0, pValue: 1, n: 0 },
|
|
131
|
+
verdict: 'tie',
|
|
132
|
+
},
|
|
133
|
+
metrics: {},
|
|
134
|
+
metricVerdicts: {},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export { BASELINE_LABEL, CANDIDATE_LABEL };
|
|
138
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/self-harness/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,9 +60,10 @@ mine→propose→validate→accept and persists results. Humans do both today.
|
|
|
60
60
|
key=(model_family, failure_sig) (env + scaffold snapshot)
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
New code lives under `src/self-harness/`: `orchestrator.ts
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
New code lives under `src/self-harness/`: `orchestrator.ts` (one iteration), `validate.ts` (the real
|
|
64
|
+
paired-bench validator), `run.ts` (the committing loop + versioned snapshot), `mine.ts`, `propose.ts`,
|
|
65
|
+
`mods.ts` (the DSL), `transfer.ts`, plus the `uap self-harness` CLI. It **reuses** `benchmarks/paired/`
|
|
66
|
+
for validation and the HALO exporter for mining — no reimplementation.
|
|
66
67
|
|
|
67
68
|
## 4. The Modification DSL — bounded, typed, reversible
|
|
68
69
|
|
|
@@ -171,9 +172,15 @@ Controls:
|
|
|
171
172
|
- **P0 — Plumbing (small).** Promote the failure-mode logs to structured HALO spans; add the held-out
|
|
172
173
|
regression suite; pin a stable `signature` hashing. *Exit:* `uap harness analyze` emits a typed
|
|
173
174
|
`WeaknessReport`.
|
|
174
|
-
- **P1 — Closed loop, env
|
|
175
|
-
mine→propose→validate→
|
|
176
|
-
|
|
175
|
+
- **P1 — Closed loop, env DSL (Option A). [BUILT]** `uap self-harness run` orchestrates
|
|
176
|
+
mine→propose→validate→decide over `env` Mods, reusing `benchmarks/paired/`. The real validator
|
|
177
|
+
(`validate.ts`) runs a baseline arm, physically toggles the env knob + restarts the server, runs a
|
|
178
|
+
candidate arm, reverts, and pairs the two into a `Comparison`; held-out runs the same way. `--apply`
|
|
179
|
+
commits accepted Mods to the env file, restarts once, and writes a **versioned profile snapshot**
|
|
180
|
+
(`run.ts`, `profile.ts`) + append-only history; without it the run is a pure dry-run that touches
|
|
181
|
+
nothing. *Exit met:* one autonomous iteration accepts a real env Mod (e.g. re-discovers
|
|
182
|
+
`LLAMA_N_PREDICT=4096`) with paired stats. `scaffold`/`middleware` Mods are not auto-validated here —
|
|
183
|
+
they route through the human-gated pending queue (§9).
|
|
177
184
|
- **P2 — Middleware Mods (Option B).** Add the `middleware` Mod class + the `toolcall-path-normalizer`.
|
|
178
185
|
*Exit:* the loop proposes+validates the normalizer and measurably cuts path-garbling on the medium
|
|
179
186
|
suite (the ceiling manual fixes couldn't crack).
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/web/dashboard.html
CHANGED
|
@@ -1164,10 +1164,18 @@
|
|
|
1164
1164
|
`<strong style="color:#58a6ff;font-size:18px">${fmt(sv.totalTokensSaved)}</strong> tokens · ` +
|
|
1165
1165
|
`<strong style="color:#3fb950;font-size:18px">$${(sv.totalCostSavedUsd||0).toFixed(2)}</strong> saved across all UAP influences`);
|
|
1166
1166
|
if (!sv.influences.length) { setHTML('savings-table', '<div class="empty">No savings data</div>'); return; }
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1167
|
+
// Unmeasured influences (e.g. routing with no UAP-driven calls yet) render an
|
|
1168
|
+
// explicit dimmed "—" rather than a real-looking $0, so idle != broken.
|
|
1169
|
+
const idleCell = '<span style="color:var(--fg3)" title="not active / no data recorded yet">\u2014</span>';
|
|
1170
|
+
const rows = sv.influences.map(i => {
|
|
1171
|
+
const idle = i.quality === 'unmeasured';
|
|
1172
|
+
const tok = idle ? idleCell : fmt(i.tokensSaved);
|
|
1173
|
+
const cost = idle ? idleCell : `<span style="color:#3fb950">$${(i.costSavedUsd||0).toFixed(4)}</span>`;
|
|
1174
|
+
return `<tr${idle ? ' style="opacity:.55"' : ''}><td>${esc(i.influence)}</td>` +
|
|
1175
|
+
`<td style="text-align:right">${tok}</td>` +
|
|
1176
|
+
`<td style="text-align:right">${cost}</td>` +
|
|
1177
|
+
`<td>${badge(i.quality)}</td><td style="color:var(--fg3);font-size:11px">${esc(i.detail||'')}</td></tr>`;
|
|
1178
|
+
}).join('');
|
|
1171
1179
|
setHTML('savings-table',
|
|
1172
1180
|
`<table><thead><tr><th>Influence</th><th style="text-align:right">Tokens saved</th>` +
|
|
1173
1181
|
`<th style="text-align:right">Cost saved</th><th>Quality</th><th>Detail</th></tr></thead><tbody>${rows}</tbody></table>`);
|