@tangle-network/agent-bench 0.1.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/README.md +15 -0
- package/package.json +35 -0
- package/src/adapters.ts +60 -0
- package/src/aec-gate.mts +217 -0
- package/src/atom-humaneval.mts +197 -0
- package/src/atom-mcp-e2e.mts +223 -0
- package/src/benchmarks/_harness.ts +206 -0
- package/src/benchmarks/aec-bench.test.mts +53 -0
- package/src/benchmarks/aec-bench.ts +319 -0
- package/src/benchmarks/appworld.test.mts +45 -0
- package/src/benchmarks/appworld.ts +426 -0
- package/src/benchmarks/cad-design.ts +429 -0
- package/src/benchmarks/cadbench.ts +135 -0
- package/src/benchmarks/cadgenbench.ts +121 -0
- package/src/benchmarks/commit0.test.mts +71 -0
- package/src/benchmarks/commit0.ts +266 -0
- package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
- package/src/benchmarks/enterpriseops-gym.ts +343 -0
- package/src/benchmarks/finsearchcomp.ts +371 -0
- package/src/benchmarks/frames.ts +520 -0
- package/src/benchmarks/hotpotqa.ts +320 -0
- package/src/benchmarks/humaneval.ts +251 -0
- package/src/benchmarks/mind2web.ts +311 -0
- package/src/benchmarks/programbench.test.mts +60 -0
- package/src/benchmarks/programbench.ts +210 -0
- package/src/benchmarks/simpleqa.ts +390 -0
- package/src/benchmarks/swe-bench.ts +152 -0
- package/src/benchmarks/terminal-bench.ts +182 -0
- package/src/benchmarks/trata-hedge.ts +496 -0
- package/src/benchmarks/types.ts +58 -0
- package/src/browser/adapters/bad-design-audit.ts +153 -0
- package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
- package/src/browser/adapters/bad.ts +165 -0
- package/src/browser/agent-adapter.ts +145 -0
- package/src/browser/process-adapter.ts +146 -0
- package/src/browser/run-to-spans.ts +105 -0
- package/src/browser/run-to-spans.verify.ts +89 -0
- package/src/browser/ui-reviewer.ts +200 -0
- package/src/browser/ui-reviewer.verify.ts +94 -0
- package/src/browser/verify.ts +96 -0
- package/src/clbench-codebase-gate.mts +314 -0
- package/src/clbench-context-gate.mts +305 -0
- package/src/cloud-loop.mts +138 -0
- package/src/coding-skills/minimal-diff.md +9 -0
- package/src/coding-skills/read-before-edit.md +9 -0
- package/src/coding-skills/reproduce-first.md +10 -0
- package/src/coding-skills/run-tests-after-edit.md +9 -0
- package/src/coding-skills/trace-the-failure.md +9 -0
- package/src/commit0-env-run.mts +59 -0
- package/src/commit0-env.ts +173 -0
- package/src/commit0-gate.mts +529 -0
- package/src/commit0-prereqs.sh +48 -0
- package/src/corpus-replay.mts +300 -0
- package/src/corpus-report.mts +504 -0
- package/src/corpus.test.mts +273 -0
- package/src/corpus.ts +325 -0
- package/src/decoder-live.mts +133 -0
- package/src/directives.ts +84 -0
- package/src/diverse-gate.mjs +112 -0
- package/src/egress-probe.mts +26 -0
- package/src/eops-skills/address-every-subtask.md +5 -0
- package/src/eops-skills/exact-tools-and-args.md +5 -0
- package/src/eops-skills/full-sequence-to-goal.md +5 -0
- package/src/eops-skills/ground-every-value.md +5 -0
- package/src/eops-skills/honor-the-policies.md +5 -0
- package/src/examples/README.md +58 -0
- package/src/examples/math-demo.mts +110 -0
- package/src/examples/strategy-demo.mts +119 -0
- package/src/fleet.mts +121 -0
- package/src/gate-cli.mts +101 -0
- package/src/gate.test.mts +129 -0
- package/src/gate.ts +460 -0
- package/src/generate-eval/certify.ts +178 -0
- package/src/generate-eval/schema.ts +78 -0
- package/src/humaneval-gate.mts +204 -0
- package/src/humaneval-repair-gate.mts +143 -0
- package/src/index.ts +19 -0
- package/src/mcp-mount-probe.mts +126 -0
- package/src/profile-coordinates.ts +134 -0
- package/src/profiles.ts +128 -0
- package/src/refine-loop.test.mts +106 -0
- package/src/refine-loop.ts +106 -0
- package/src/research-gate.mts +132 -0
- package/src/research-shot.ts +134 -0
- package/src/resolve-client.ts +58 -0
- package/src/router-executor.ts +51 -0
- package/src/run-pool.ts +48 -0
- package/src/runtime-hook-recorder.ts +137 -0
- package/src/sandbox-run.ts +125 -0
- package/src/search-bench/bridge.ts +124 -0
- package/src/search-bench/export.mts +0 -0
- package/src/search-bench/parametric-check.mts +63 -0
- package/src/search-bench/profiles.ts +98 -0
- package/src/search-bench/run.mts +287 -0
- package/src/search-bench/tasks-fresh.ts +688 -0
- package/src/search-bench/tasks.ts +129 -0
- package/src/search-tool.ts +95 -0
- package/src/selector.test.mts +189 -0
- package/src/selector.ts +366 -0
- package/src/skill-sandbox-smoke.mts +100 -0
- package/src/stats.mts +90 -0
- package/src/terminal-compare.ts +519 -0
- package/src/trajectory-assemble.mjs +130 -0
- package/src/trata-gate.mts +243 -0
- package/src/trata-gepa.mts +434 -0
- package/src/worker-blender.ts +230 -0
- package/src/worker-browser.ts +102 -0
- package/src/worker-build123d.ts +143 -0
- package/src/worker-cad.ts +451 -0
- package/src/worker.ts +136 -0
- package/src/workspace-loop.mts +133 -0
package/src/selector.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployable, non-oracle selector (docs/roadmap-rsi.md Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* best-of-N only pays if you can pick the good attempt WITHOUT the judge. Today
|
|
5
|
+
* the loop's winner is judge-selected (verdict.score) — an oracle upper bound, not
|
|
6
|
+
* a deployable system. A `Selector` ranks the k candidate OUTPUTS using only
|
|
7
|
+
* trace-observable signal and never the judge verdict. The type enforces the
|
|
8
|
+
* firewall: a Selector is `(outputs: string[]) => index`; it is structurally
|
|
9
|
+
* incapable of reading `valid`/`score`, so it cannot become an oracle by accident.
|
|
10
|
+
*
|
|
11
|
+
* Scoring is OFFLINE over the corpus: each random@k RunRecord already holds k
|
|
12
|
+
* attempts each with a stored `valid`, so picking index i and reading
|
|
13
|
+
* attempts[i].valid is the judge's verdict of the pick — zero new rollouts, zero
|
|
14
|
+
* new judge calls. selector@k vs random@k (does picking beat a blind random draw?)
|
|
15
|
+
* and selector@k vs oracle@k (how much ceiling is recoverable?) fall straight out.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AttemptRecord, RunRecord } from './corpus'
|
|
19
|
+
|
|
20
|
+
/** Picks one of `outputs` by index, using the candidate text only. Never the verdict. */
|
|
21
|
+
export type Selector = (outputs: string[]) => number
|
|
22
|
+
|
|
23
|
+
/** Normalize a short free-text answer so equivalent answers cluster: lowercase,
|
|
24
|
+
* trim, collapse whitespace, strip surrounding quotes/punctuation, drop a leading
|
|
25
|
+
* article. Deliberately conservative — it clusters trivial restatements, not
|
|
26
|
+
* paraphrases (that is the selector's known ceiling, not a bug). */
|
|
27
|
+
export function normalizeAnswer(s: string): string {
|
|
28
|
+
return s
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.trim()
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.replace(/^[\s"'`([{]+|[\s"'`)\]}.,;:!?]+$/g, '')
|
|
33
|
+
.replace(/^(the|a|an)\s+/, '')
|
|
34
|
+
.trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Self-consistency (Wang 2022): pick the output that agrees with the most others
|
|
39
|
+
* after normalization. Ties break to the cluster whose first member appears
|
|
40
|
+
* earliest, and within the winning cluster to its earliest member — deterministic,
|
|
41
|
+
* so test-retest flip rate is 0 by construction (the sanity floor). Empty input is
|
|
42
|
+
* a caller bug (fail loud).
|
|
43
|
+
*/
|
|
44
|
+
export const selfConsistencySelect: Selector = (outputs) => {
|
|
45
|
+
if (outputs.length === 0) throw new Error('selfConsistencySelect: no candidate outputs')
|
|
46
|
+
const counts = new Map<string, number>()
|
|
47
|
+
for (const o of outputs) {
|
|
48
|
+
const k = normalizeAnswer(o)
|
|
49
|
+
counts.set(k, (counts.get(k) ?? 0) + 1)
|
|
50
|
+
}
|
|
51
|
+
let bestIdx = 0
|
|
52
|
+
let bestCount = -1
|
|
53
|
+
for (let i = 0; i < outputs.length; i += 1) {
|
|
54
|
+
const c = counts.get(normalizeAnswer(outputs[i] as string)) ?? 0
|
|
55
|
+
if (c > bestCount) {
|
|
56
|
+
bestCount = c
|
|
57
|
+
bestIdx = i // first member of the largest cluster encountered
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return bestIdx
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verifier-grounded selection: pick the candidate with the highest deployable-checker
|
|
65
|
+
* pass-count (ties → earliest). The pass-counts come from a checker the agent can run
|
|
66
|
+
* in production (a provided test suite), NOT from the gold answer — so unlike a
|
|
67
|
+
* judge/oracle pick this is deployable. A boolean pass/fail is the pass-count's {0,1}
|
|
68
|
+
* special case. Pure and unit-shaped: it reads only the supplied counts, never any
|
|
69
|
+
* verdict. Empty input is a caller bug (fail loud); a negative count is invalid.
|
|
70
|
+
*/
|
|
71
|
+
export function verifierGroundedSelect(passCounts: ReadonlyArray<number>): number {
|
|
72
|
+
if (passCounts.length === 0) throw new Error('verifierGroundedSelect: no candidate pass-counts')
|
|
73
|
+
let bestIdx = 0
|
|
74
|
+
let bestCount = Number.NEGATIVE_INFINITY
|
|
75
|
+
for (let i = 0; i < passCounts.length; i += 1) {
|
|
76
|
+
const c = passCounts[i] as number
|
|
77
|
+
if (!Number.isFinite(c) || c < 0) {
|
|
78
|
+
throw new Error(`verifierGroundedSelect: invalid pass-count ${c} at index ${i}`)
|
|
79
|
+
}
|
|
80
|
+
if (c > bestCount) {
|
|
81
|
+
bestCount = c
|
|
82
|
+
bestIdx = i
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return bestIdx
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One instance's offline selector outcome, all derived from stored verdicts. */
|
|
89
|
+
export interface SelectorOutcome {
|
|
90
|
+
/** usable candidates (attempts with a non-empty output) */
|
|
91
|
+
k: number
|
|
92
|
+
/** the picked attempt's stored verdict — selector@k for this instance */
|
|
93
|
+
selectorResolved: boolean
|
|
94
|
+
/** nValid / k — expected pass of a uniform-random pick (the compute control) */
|
|
95
|
+
randomExpected: number
|
|
96
|
+
/** any usable candidate passed — the oracle ceiling */
|
|
97
|
+
oracle: boolean
|
|
98
|
+
/** round-1 verdict — pass@1 */
|
|
99
|
+
blind: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const usableCandidates = (record: RunRecord): AttemptRecord[] =>
|
|
103
|
+
record.attempts.filter((a) => typeof a.output === 'string' && a.output.length > 0)
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Score a selector on one RunRecord offline. Returns null when no attempt produced
|
|
107
|
+
* an output (unscoreable — not a pass). The selector sees OUTPUTS ONLY; the stored
|
|
108
|
+
* `valid` flags are read after the pick, never passed to the selector.
|
|
109
|
+
*/
|
|
110
|
+
export function scoreSelectorOnRun(record: RunRecord, select: Selector): SelectorOutcome | null {
|
|
111
|
+
const usable = usableCandidates(record)
|
|
112
|
+
if (usable.length === 0) return null
|
|
113
|
+
const picked = select(usable.map((a) => a.output as string))
|
|
114
|
+
if (!Number.isInteger(picked) || picked < 0 || picked >= usable.length) {
|
|
115
|
+
throw new Error(`selector returned out-of-range index ${picked} for ${usable.length} candidates`)
|
|
116
|
+
}
|
|
117
|
+
const nValid = usable.filter((a) => a.valid === true).length
|
|
118
|
+
return {
|
|
119
|
+
k: usable.length,
|
|
120
|
+
selectorResolved: usable[picked]?.valid === true,
|
|
121
|
+
randomExpected: nValid / usable.length,
|
|
122
|
+
oracle: nValid > 0,
|
|
123
|
+
blind: record.blindResolved === true,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SelectorReport {
|
|
128
|
+
/** scoreable instances */
|
|
129
|
+
n: number
|
|
130
|
+
selectorRate: number
|
|
131
|
+
randomRate: number
|
|
132
|
+
oracleRate: number
|
|
133
|
+
blindRate: number
|
|
134
|
+
/** selector@k − random@k: does picking beat a blind random draw at the same k? (the gate) */
|
|
135
|
+
dVsRandom: number
|
|
136
|
+
/** selector@k − blind: total lift over one shot */
|
|
137
|
+
dVsBlind: number
|
|
138
|
+
/** oracle@k − selector@k: ceiling left on the table */
|
|
139
|
+
gapToOracle: number
|
|
140
|
+
/** instances skipped as unscoreable (no output in any attempt) */
|
|
141
|
+
skipped: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Aggregate the offline selector outcome across a corpus slice (one condition). */
|
|
145
|
+
export function summarizeSelector(records: RunRecord[], select: Selector): SelectorReport {
|
|
146
|
+
let n = 0
|
|
147
|
+
let selector = 0
|
|
148
|
+
let randomExp = 0
|
|
149
|
+
let oracle = 0
|
|
150
|
+
let blind = 0
|
|
151
|
+
let skipped = 0
|
|
152
|
+
for (const r of records) {
|
|
153
|
+
const o = scoreSelectorOnRun(r, select)
|
|
154
|
+
if (o === null) {
|
|
155
|
+
skipped += 1
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
n += 1
|
|
159
|
+
if (o.selectorResolved) selector += 1
|
|
160
|
+
randomExp += o.randomExpected
|
|
161
|
+
if (o.oracle) oracle += 1
|
|
162
|
+
if (o.blind) blind += 1
|
|
163
|
+
}
|
|
164
|
+
const rate = (x: number) => (n > 0 ? x / n : 0)
|
|
165
|
+
const selectorRate = rate(selector)
|
|
166
|
+
const randomRate = rate(randomExp)
|
|
167
|
+
const oracleRate = rate(oracle)
|
|
168
|
+
const blindRate = rate(blind)
|
|
169
|
+
return {
|
|
170
|
+
n,
|
|
171
|
+
selectorRate,
|
|
172
|
+
randomRate,
|
|
173
|
+
oracleRate,
|
|
174
|
+
blindRate,
|
|
175
|
+
dVsRandom: selectorRate - randomRate,
|
|
176
|
+
dVsBlind: selectorRate - blindRate,
|
|
177
|
+
gapToOracle: oracleRate - selectorRate,
|
|
178
|
+
skipped,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Verifier-grounded selection over a CONTINUOUS deployable-checker reward.
|
|
184
|
+
//
|
|
185
|
+
// HumanEval proved verifier-grounded selection captures the oracle ceiling on a
|
|
186
|
+
// BINARY pass/fail checker. This is the continuous-reward analog for graded
|
|
187
|
+
// domains (commit0's pytest pass-rate, aec's tests/verify.py partial credit):
|
|
188
|
+
// each attempt carries a stored `score` from a DEPLOYABLE checker the agent could
|
|
189
|
+
// re-run itself — NOT the gold answer. The selector picks argmax(score); the gate
|
|
190
|
+
// is selector-reward vs a blind random draw at equal k.
|
|
191
|
+
//
|
|
192
|
+
// Admissibility: `score` MUST be the deployable checker's reward (re-runnable
|
|
193
|
+
// without the gold — provided unit tests / a verify.py that recomputes GT). It is
|
|
194
|
+
// the same signal the selector would have in production. `valid` (the gold
|
|
195
|
+
// resolve verdict) is read AFTER selection only to MEASURE the pick's binary
|
|
196
|
+
// outcome; selection itself ranks by the checker score, never by the gold.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
const scoredAttempts = (record: RunRecord): AttemptRecord[] =>
|
|
200
|
+
record.attempts.filter((a) => typeof a.score === 'number' && Number.isFinite(a.score))
|
|
201
|
+
|
|
202
|
+
/** One instance's verifier-grounded outcome on the continuous graded reward. */
|
|
203
|
+
export interface VerifierSelectorOutcome {
|
|
204
|
+
k: number
|
|
205
|
+
/** reward of the argmax-score pick (== max score) — what a deployable best-of-k ships */
|
|
206
|
+
selectorReward: number
|
|
207
|
+
/** mean score — expected reward of a uniform-random pick (the blind compute control) */
|
|
208
|
+
randomReward: number
|
|
209
|
+
/** max score — the best-of-k ceiling */
|
|
210
|
+
oracleReward: number
|
|
211
|
+
/** round-1 score — one-shot reward */
|
|
212
|
+
blindReward: number
|
|
213
|
+
/** did the verifier-picked attempt fully resolve (binary) */
|
|
214
|
+
selectorResolved: boolean
|
|
215
|
+
oracleResolved: boolean
|
|
216
|
+
blindResolved: boolean
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Score verifier-grounded selection on one RunRecord offline. Returns null when no
|
|
221
|
+
* attempt carries a numeric checker score (unscoreable). The selector ranks by the
|
|
222
|
+
* stored `score` only; `valid` is read post-pick to report the binary outcome.
|
|
223
|
+
*/
|
|
224
|
+
export function scoreVerifierSelectorOnRun(record: RunRecord): VerifierSelectorOutcome | null {
|
|
225
|
+
const usable = scoredAttempts(record)
|
|
226
|
+
if (usable.length === 0) return null
|
|
227
|
+
const scores = usable.map((a) => a.score as number)
|
|
228
|
+
const picked = verifierGroundedSelect(scores)
|
|
229
|
+
const sum = scores.reduce((s, x) => s + x, 0)
|
|
230
|
+
const round1 = usable.find((a) => a.round === 0) ?? (usable[0] as AttemptRecord)
|
|
231
|
+
return {
|
|
232
|
+
k: usable.length,
|
|
233
|
+
selectorReward: scores[picked] as number,
|
|
234
|
+
randomReward: sum / usable.length,
|
|
235
|
+
oracleReward: Math.max(...scores),
|
|
236
|
+
blindReward: round1.score as number,
|
|
237
|
+
selectorResolved: usable[picked]?.valid === true,
|
|
238
|
+
oracleResolved: usable.some((a) => a.valid === true),
|
|
239
|
+
blindResolved: record.blindResolved === true,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Deterministic PRNG so the bootstrap CI is reproducible (no Math.random). */
|
|
244
|
+
function mulberry32(seed: number): () => number {
|
|
245
|
+
let a = seed >>> 0
|
|
246
|
+
return () => {
|
|
247
|
+
a = (a + 0x6d2b79f5) | 0
|
|
248
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
249
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
250
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Two-sided percentile bootstrap (95% CI + p) on the mean of paired per-instance
|
|
255
|
+
* deltas — the same instrument corpus-report uses, applied to the within-corpus
|
|
256
|
+
* (selector − random) reward delta. Deterministic seed: same corpus → same CI. */
|
|
257
|
+
export function bootstrapDeltaCI(
|
|
258
|
+
deltas: readonly number[],
|
|
259
|
+
resamples = 10000,
|
|
260
|
+
seed = 0xc0ffee,
|
|
261
|
+
): { mean: number; lo: number; hi: number; p: number; discordant: number } {
|
|
262
|
+
const n = deltas.length
|
|
263
|
+
if (n === 0) return { mean: 0, lo: 0, hi: 0, p: 1, discordant: 0 }
|
|
264
|
+
const rng = mulberry32(seed)
|
|
265
|
+
const means = new Array<number>(resamples)
|
|
266
|
+
for (let b = 0; b < resamples; b += 1) {
|
|
267
|
+
let s = 0
|
|
268
|
+
for (let i = 0; i < n; i += 1) s += deltas[(rng() * n) | 0] as number
|
|
269
|
+
means[b] = s / n
|
|
270
|
+
}
|
|
271
|
+
means.sort((x, y) => x - y)
|
|
272
|
+
let ge0 = 0
|
|
273
|
+
for (const m of means) if (m >= 0) ge0 += 1
|
|
274
|
+
const pOne = Math.min(ge0, resamples - ge0) / resamples
|
|
275
|
+
return {
|
|
276
|
+
mean: deltas.reduce((s, d) => s + d, 0) / n,
|
|
277
|
+
lo: means[Math.floor(0.025 * resamples)] as number,
|
|
278
|
+
hi: means[Math.floor(0.975 * resamples)] as number,
|
|
279
|
+
p: Math.min(1, 2 * pOne),
|
|
280
|
+
discordant: deltas.filter((d) => d !== 0).length,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface VerifierSelectorReport {
|
|
285
|
+
/** scoreable instances */
|
|
286
|
+
n: number
|
|
287
|
+
selectorReward: number
|
|
288
|
+
randomReward: number
|
|
289
|
+
oracleReward: number
|
|
290
|
+
blindReward: number
|
|
291
|
+
selectorResolveRate: number
|
|
292
|
+
oracleResolveRate: number
|
|
293
|
+
blindResolveRate: number
|
|
294
|
+
/** THE GATE: deployable selector reward − blind random-draw reward at equal k. */
|
|
295
|
+
rewardVsRandom: number
|
|
296
|
+
/** selector reward − one-shot reward (total best-of-k lift). */
|
|
297
|
+
rewardVsBlind: number
|
|
298
|
+
/** 95% bootstrap CI + two-sided p on the paired (selector − random) per-instance delta. */
|
|
299
|
+
ci: { mean: number; lo: number; hi: number; p: number; discordant: number }
|
|
300
|
+
/** instances skipped as unscoreable (no checker score on any attempt) */
|
|
301
|
+
skipped: number
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Aggregate verifier-grounded selection across a corpus slice (one condition). */
|
|
305
|
+
export function summarizeVerifierSelector(records: RunRecord[]): VerifierSelectorReport {
|
|
306
|
+
let n = 0
|
|
307
|
+
let selR = 0
|
|
308
|
+
let randR = 0
|
|
309
|
+
let oraR = 0
|
|
310
|
+
let blindR = 0
|
|
311
|
+
let selRes = 0
|
|
312
|
+
let oraRes = 0
|
|
313
|
+
let blindRes = 0
|
|
314
|
+
let skipped = 0
|
|
315
|
+
const deltas: number[] = []
|
|
316
|
+
for (const r of records) {
|
|
317
|
+
const o = scoreVerifierSelectorOnRun(r)
|
|
318
|
+
if (o === null) {
|
|
319
|
+
skipped += 1
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
n += 1
|
|
323
|
+
selR += o.selectorReward
|
|
324
|
+
randR += o.randomReward
|
|
325
|
+
oraR += o.oracleReward
|
|
326
|
+
blindR += o.blindReward
|
|
327
|
+
if (o.selectorResolved) selRes += 1
|
|
328
|
+
if (o.oracleResolved) oraRes += 1
|
|
329
|
+
if (o.blindResolved) blindRes += 1
|
|
330
|
+
deltas.push(o.selectorReward - o.randomReward)
|
|
331
|
+
}
|
|
332
|
+
const rate = (x: number) => (n > 0 ? x / n : 0)
|
|
333
|
+
return {
|
|
334
|
+
n,
|
|
335
|
+
selectorReward: rate(selR),
|
|
336
|
+
randomReward: rate(randR),
|
|
337
|
+
oracleReward: rate(oraR),
|
|
338
|
+
blindReward: rate(blindR),
|
|
339
|
+
selectorResolveRate: rate(selRes),
|
|
340
|
+
oracleResolveRate: rate(oraRes),
|
|
341
|
+
blindResolveRate: rate(blindRes),
|
|
342
|
+
rewardVsRandom: rate(selR) - rate(randR),
|
|
343
|
+
rewardVsBlind: rate(selR) - rate(blindR),
|
|
344
|
+
ci: bootstrapDeltaCI(deltas),
|
|
345
|
+
skipped,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Test-retest: fraction of records where two selector runs pick a DIFFERENT index.
|
|
351
|
+
* A deterministic selector against itself is 0 (the floor); a stochastic selector
|
|
352
|
+
* (or two seeds) reveals how much its decision is noise. A selector whose flips
|
|
353
|
+
* move the resolve rate is not a selector.
|
|
354
|
+
*/
|
|
355
|
+
export function flipRate(records: RunRecord[], a: Selector, b: Selector): number {
|
|
356
|
+
let n = 0
|
|
357
|
+
let flips = 0
|
|
358
|
+
for (const r of records) {
|
|
359
|
+
const usable = usableCandidates(r)
|
|
360
|
+
if (usable.length === 0) continue
|
|
361
|
+
const outs = usable.map((x) => x.output as string)
|
|
362
|
+
n += 1
|
|
363
|
+
if (a(outs) !== b(outs)) flips += 1
|
|
364
|
+
}
|
|
365
|
+
return n > 0 ? flips / n : 0
|
|
366
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof that a SKILL.md from an AgentProfile actually materializes inside the sandbox where the
|
|
3
|
+
* coding harness loads it — the "are we on the right surface" check before any benchmark.
|
|
4
|
+
* Creates one opencode box with resources.skills, then asks the IN-SESSION agent to read the
|
|
5
|
+
* skill back. PASS keys on a UNIQUE MARKER in the skill body, so a hit is unambiguously OUR
|
|
6
|
+
* skill — not opencode's bundled /nix/store skills and not the agent's prose.
|
|
7
|
+
*
|
|
8
|
+
* Two gotchas this guards against (both produced false readings during bring-up):
|
|
9
|
+
* 1. PATH: opencode's discovery dir is ~/.opencode/skill/<name>/SKILL.md (SINGULAR "skill");
|
|
10
|
+
* claude-code uses ~/.claude/skills/. resources.skills lands in the backend's own dir.
|
|
11
|
+
* 2. VIEW: a bare box.exec() WITHOUT a sessionId sees a DIFFERENT filesystem than the agent
|
|
12
|
+
* session (SDK ExecOptions docs). So we verify through the in-SESSION agent (streamPrompt),
|
|
13
|
+
* the path the bench actually runs — not a detached exec.
|
|
14
|
+
*
|
|
15
|
+
* Run: dotenvx run -f ~/company/devops/secrets/.env.keys -- pnpm exec tsx bench/src/skill-sandbox-smoke.mts
|
|
16
|
+
*/
|
|
17
|
+
import { Sandbox, defineInlineResource } from '@tangle-network/sandbox'
|
|
18
|
+
|
|
19
|
+
const must = (k: string): string => {
|
|
20
|
+
const v = process.env[k]
|
|
21
|
+
if (!v) throw new Error(`env ${k} required`)
|
|
22
|
+
return v
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const skillName = 'reproduce-first'
|
|
26
|
+
// A unique token that cannot occur in opencode's bundled skills or in plausible prose.
|
|
27
|
+
const marker = 'SKILLMAT-OK-7f3c91'
|
|
28
|
+
const skillMd = [
|
|
29
|
+
'---',
|
|
30
|
+
`name: ${skillName}`,
|
|
31
|
+
'description: Reproduce the failing test before changing any code.',
|
|
32
|
+
'---',
|
|
33
|
+
'',
|
|
34
|
+
`Materialization proof token: ${marker}`,
|
|
35
|
+
'When fixing a bug: run the failing test FIRST to observe the real error, make the smallest',
|
|
36
|
+
'change that turns it green, then re-run to confirm.',
|
|
37
|
+
].join('\n')
|
|
38
|
+
|
|
39
|
+
async function main(): Promise<void> {
|
|
40
|
+
const client = new Sandbox({
|
|
41
|
+
baseUrl: process.env.SANDBOX_BASE_URL ?? 'https://sandbox.tangle.tools',
|
|
42
|
+
apiKey: must('TANGLE_API_KEY'),
|
|
43
|
+
timeoutMs: 600_000,
|
|
44
|
+
} as never)
|
|
45
|
+
|
|
46
|
+
console.error(`[smoke] creating opencode box with resources.skills=[${skillName}]…`)
|
|
47
|
+
const box: Record<string, (...a: never[]) => unknown> & { id?: string } = (await client.create({
|
|
48
|
+
backend: {
|
|
49
|
+
type: 'opencode',
|
|
50
|
+
model: {
|
|
51
|
+
provider: process.env.WORKER_PROVIDER ?? 'openai',
|
|
52
|
+
model: process.env.WORKER_MODEL ?? 'deepseek-v4-flash',
|
|
53
|
+
baseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
|
|
54
|
+
},
|
|
55
|
+
profile: { name: 'skill-smoke', resources: { skills: [defineInlineResource(skillName, skillMd)] } },
|
|
56
|
+
},
|
|
57
|
+
} as never)) as never
|
|
58
|
+
console.error('[smoke] box id:', box.id, '— waiting for running…')
|
|
59
|
+
await box.waitFor('running' as never, { timeoutMs: 180_000 } as never)
|
|
60
|
+
console.error('[smoke] box running; asking the IN-SESSION agent to read the materialized skill back…')
|
|
61
|
+
|
|
62
|
+
// cat the skill at BOTH backend discovery dirs (opencode singular, claude-code plural) + a
|
|
63
|
+
// name-scoped find; the marker appears wherever resources.skills actually landed it.
|
|
64
|
+
const prompt =
|
|
65
|
+
'Run this exact shell command and paste its raw output verbatim, nothing else:\n' +
|
|
66
|
+
`\`cat ~/.opencode/skill/${skillName}/SKILL.md ~/.claude/skills/${skillName}/SKILL.md 2>/dev/null; ` +
|
|
67
|
+
`echo "--FIND--"; find / -name SKILL.md 2>/dev/null | grep -i ${skillName} | grep -vi /nix/store || echo NOFILE\``
|
|
68
|
+
let out = ''
|
|
69
|
+
const ac = new AbortController()
|
|
70
|
+
const timer = setTimeout(() => ac.abort(), 220_000)
|
|
71
|
+
try {
|
|
72
|
+
for await (const ev of box.streamPrompt(prompt as never, { signal: ac.signal } as never) as AsyncGenerator<unknown>) {
|
|
73
|
+
out += (typeof ev === 'string' ? ev : JSON.stringify(ev)) + '\n'
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer)
|
|
77
|
+
}
|
|
78
|
+
console.error(`[smoke] stream chars: ${out.length}`)
|
|
79
|
+
console.error('[smoke] tail of stream:\n' + out.slice(-1500))
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await box.delete?.()
|
|
83
|
+
} catch {
|
|
84
|
+
/* best-effort cleanup */
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Deterministic: PASS only if the agent read OUR unique marker out of the file — not a loose
|
|
88
|
+
// match on the skill name (which appears in the prompt/prose) or on "SKILL.md" (bundled skills).
|
|
89
|
+
const landed = out.includes(marker)
|
|
90
|
+
const path = /\.opencode\/skill\//.test(out) ? '~/.opencode/skill (opencode)' : /\.claude\/skills\//.test(out) ? '~/.claude/skills (claude-code)' : '(path not surfaced)'
|
|
91
|
+
console.error(
|
|
92
|
+
`\n[smoke] VERDICT: ${landed ? `PASS — resources.skills materialized + the in-session agent read the marker at ${path}` : 'FAIL — marker NOT read back; resources.skills did not land where the harness reads (check backend discovery dir)'}`,
|
|
93
|
+
)
|
|
94
|
+
process.exit(landed ? 0 : 2)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch((e) => {
|
|
98
|
+
console.error('[smoke] FAILED:', e instanceof Error ? (e.stack ?? e.message) : e)
|
|
99
|
+
process.exit(1)
|
|
100
|
+
})
|
package/src/stats.mts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared statistics + scheduling helpers for the gate runners.
|
|
3
|
+
*
|
|
4
|
+
* The gate runners (humaneval / commit0 / aec / clbench-context / clbench-codebase)
|
|
5
|
+
* each hand-rolled the same three primitives: a bounded-concurrency pool, the
|
|
6
|
+
* mulberry32 PRNG, and the paired-bootstrap lift. They live here once.
|
|
7
|
+
*
|
|
8
|
+
* Two pools coexist on purpose, because the gates and the batch runners want
|
|
9
|
+
* different fault contracts:
|
|
10
|
+
* - `runPool` (re-exported from ./run-pool) catches a per-item throw and yields
|
|
11
|
+
* `{ ok, error }` for THAT item — the batch never aborts. Used by the batch
|
|
12
|
+
* runners / experiment harness that aggregate partial results.
|
|
13
|
+
* - `pool` (here) lets the worker throw and propagates the rejection — the gates
|
|
14
|
+
* want a router/sandbox fault to abort the arm loudly rather than score a
|
|
15
|
+
* phantom 0. It returns `R[]` in item order. This matches the gates' existing
|
|
16
|
+
* private `pool()` exactly; it is NOT a re-export of `runPool`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export { runPool, type PoolOutcome } from './run-pool'
|
|
20
|
+
|
|
21
|
+
/** Bounded-concurrency pool: run `fn` over `items`, at most `limit` in flight,
|
|
22
|
+
* results returned in item order. A worker that throws propagates the rejection
|
|
23
|
+
* (fail-loud) — the gates rely on this so a router/judge fault aborts the arm
|
|
24
|
+
* rather than being silently scored. */
|
|
25
|
+
export async function pool<T, R>(items: T[], limit: number, fn: (item: T, idx: number) => Promise<R>): Promise<R[]> {
|
|
26
|
+
const results: R[] = new Array(items.length)
|
|
27
|
+
let next = 0
|
|
28
|
+
async function worker(): Promise<void> {
|
|
29
|
+
for (;;) {
|
|
30
|
+
const idx = next
|
|
31
|
+
next += 1
|
|
32
|
+
if (idx >= items.length) return
|
|
33
|
+
results[idx] = await fn(items[idx] as T, idx)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, () => worker()))
|
|
37
|
+
return results
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** mulberry32 — deterministic PRNG (Math.imul). Seeded once at 0x9e3779b9 by
|
|
41
|
+
* `pairedLift` so every gate's bootstrap CI is reproducible and identical. */
|
|
42
|
+
export function makeRng(seed: number): () => number {
|
|
43
|
+
let s = seed | 0
|
|
44
|
+
return () => {
|
|
45
|
+
s = (s + 0x6d2b79f5) | 0
|
|
46
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s)
|
|
47
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
48
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PairedLift {
|
|
53
|
+
point: number
|
|
54
|
+
low: number
|
|
55
|
+
high: number
|
|
56
|
+
pairs: number
|
|
57
|
+
discordant: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Paired lift = mean over tasks of (treatment − baseline) with a 95% bootstrap
|
|
61
|
+
* CI from resampling the paired tasks (mulberry32 seeded 0x9e3779b9, B=10000).
|
|
62
|
+
* Works on {0,1} arms (humaneval/aec/commit0) and continuous values (clbench
|
|
63
|
+
* rubric fractions) alike. `discordant` uses a |d| > 1e-9 tolerance, which equals
|
|
64
|
+
* the exact `d !== 0` test on integer-delta (binary) arms and avoids counting
|
|
65
|
+
* float noise as a discordant pair on continuous arms. */
|
|
66
|
+
export function pairedLift(baseline: number[], treatment: number[], bootstrapN = 10000): PairedLift {
|
|
67
|
+
if (baseline.length !== treatment.length) throw new Error('pairedLift: misaligned arms')
|
|
68
|
+
const n = baseline.length
|
|
69
|
+
if (n === 0) throw new Error('pairedLift: no pairs')
|
|
70
|
+
const deltas = baseline.map((b, i) => (treatment[i] as number) - b)
|
|
71
|
+
const mean = (a: number[]) => a.reduce((s, x) => s + x, 0) / a.length
|
|
72
|
+
const point = mean(deltas)
|
|
73
|
+
const discordant = deltas.filter((d) => Math.abs(d) > 1e-9).length
|
|
74
|
+
const rng = makeRng(0x9e3779b9)
|
|
75
|
+
const rint = (m: number) => Math.floor(rng() * m)
|
|
76
|
+
const boots: number[] = []
|
|
77
|
+
for (let b = 0; b < bootstrapN; b += 1) {
|
|
78
|
+
let acc = 0
|
|
79
|
+
for (let j = 0; j < n; j += 1) acc += deltas[rint(n)] as number
|
|
80
|
+
boots.push(acc / n)
|
|
81
|
+
}
|
|
82
|
+
boots.sort((x, y) => x - y)
|
|
83
|
+
return {
|
|
84
|
+
point,
|
|
85
|
+
low: boots[Math.floor(0.025 * bootstrapN)] ?? Number.NaN,
|
|
86
|
+
high: boots[Math.floor(0.975 * bootstrapN)] ?? Number.NaN,
|
|
87
|
+
pairs: n,
|
|
88
|
+
discordant,
|
|
89
|
+
}
|
|
90
|
+
}
|