@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.
Files changed (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Offline replay / score substrate over the learning-flywheel corpus
3
+ * (docs/learning-flywheel.md, layer 1: "an offline replay + reward-model layer so
4
+ * the controller space can be searched WITHOUT a live rollout per candidate").
5
+ *
6
+ * Two jobs, both READ-ONLY against the corpus:
7
+ *
8
+ * 1. REPLAY - for each RunRecord, take the recorded FINAL output and either
9
+ * (a) recompute the structural verdict from the stored per-attempt verdicts
10
+ * (default, zero model tokens) - proves the corpus is structurally
11
+ * replayable: the stored `resolved` matches the stored attempt verdicts; or
12
+ * (b) re-run the benchmark's OWN judge on that final output (--judge) and
13
+ * compare to the recorded `resolved` - proves the corpus is FAITHFULLY
14
+ * replayable and surfaces judge nondeterminism. For router-LLM judges
15
+ * (finsearchcomp) this costs JUDGE calls but NOT agent rollouts - that is
16
+ * the entire point of the offline layer. Deterministic judges (hotpotqa)
17
+ * cost nothing.
18
+ *
19
+ * 2. scoreCandidateOffline - the typed seam a future offline controller-search
20
+ * (GEPA / meta-harness over the corpus) consumes to score an ALTERNATIVE
21
+ * controller's output against a recorded instance without re-running the agent.
22
+ *
23
+ * Run:
24
+ * tsx src/corpus-replay.mts [corpusPath] [BENCH=finsearchcomp] [--judge]
25
+ * tsx src/corpus-replay.mts [corpusPath] --selector [--condition=random]
26
+ *
27
+ * Fail-loud: an empty or unreadable corpus is an error, never a silent zero.
28
+ */
29
+
30
+ import { readFile } from 'node:fs/promises'
31
+ import { argv } from 'node:process'
32
+ import { fileURLToPath } from 'node:url'
33
+ import type { BenchmarkAdapter, BenchScore, BenchTask } from './benchmarks/types'
34
+ import { ADAPTERS } from './adapters'
35
+ import type { RunRecord } from './corpus'
36
+ import { selfConsistencySelect, summarizeSelector, summarizeVerifierSelector } from './selector'
37
+
38
+ /** The benchmark's own judge - the EXTERNAL, write-only anchor (learning-flywheel.md). */
39
+ export type Judge = (task: BenchTask, artifact: string) => Promise<BenchScore>
40
+
41
+ /**
42
+ * The recorded FINAL output of a controller-run: the last attempt that actually
43
+ * produced an output. Returns undefined when no attempt produced one (every round
44
+ * errored / blank) - the caller treats that as an unreplayable record, not a pass.
45
+ */
46
+ export function finalOutput(record: RunRecord): string | undefined {
47
+ for (let i = record.attempts.length - 1; i >= 0; i -= 1) {
48
+ const out = record.attempts[i]?.output
49
+ if (typeof out === 'string' && out.length > 0) return out
50
+ }
51
+ return undefined
52
+ }
53
+
54
+ /** The stored structural verdict for the final attempt (its `valid` flag). */
55
+ function storedFinalValid(record: RunRecord): boolean {
56
+ for (let i = record.attempts.length - 1; i >= 0; i -= 1) {
57
+ const a = record.attempts[i]
58
+ if (a?.output !== undefined && a.output.length > 0) return a.valid === true
59
+ }
60
+ return false
61
+ }
62
+
63
+ /**
64
+ * The OFFLINE SCORING SEAM. Score a counterfactual controller output against a
65
+ * recorded instance by replaying the benchmark's own judge on it - NOT by
66
+ * re-running the agent. This is how a future optimizer (GEPA / meta-harness over
67
+ * the corpus) prices a candidate steer cheaply: it proposes an alternative final
68
+ * answer for the recorded instance, calls this, and reads back {resolved, score}
69
+ * at the cost of one judge call (zero for deterministic judges, zero agent
70
+ * rollouts) instead of a full live k-attempt rollout.
71
+ *
72
+ * The judge is the EXTERNAL anchor: it scores the candidate output and never sees
73
+ * the recorded outcome, so the score stays write-only (no oracle leakage).
74
+ *
75
+ * `record` supplies the instanceId the caller must resolve to a BenchTask (the
76
+ * judge needs the task's gold/judge-template metadata); pass the SAME judge that
77
+ * was used to score the corpus so offline and online scores are comparable.
78
+ */
79
+ export async function scoreCandidateOffline(
80
+ record: RunRecord,
81
+ candidateOutput: string,
82
+ judge: Judge,
83
+ task: BenchTask,
84
+ ): Promise<{ resolved: boolean; score: number }> {
85
+ if (task.id !== record.instanceId) {
86
+ throw new Error(
87
+ `scoreCandidateOffline: task ${JSON.stringify(task.id)} does not match record.instanceId ${JSON.stringify(record.instanceId)}`,
88
+ )
89
+ }
90
+ const verdict = await judge(task, candidateOutput)
91
+ return { resolved: verdict.resolved === true, score: verdict.score }
92
+ }
93
+
94
+ /** Parse the corpus JSONL; fail loud on empty/unreadable. */
95
+ async function readCorpus(corpusPath: string): Promise<RunRecord[]> {
96
+ let raw: string
97
+ try {
98
+ raw = await readFile(corpusPath, 'utf8')
99
+ } catch (err) {
100
+ throw new Error(
101
+ `corpus-replay: cannot read corpus ${corpusPath}: ${err instanceof Error ? err.message : String(err)}\n` +
102
+ `Run a bench loop first (it appends RunRecords), or pass an existing corpus path.`,
103
+ )
104
+ }
105
+ const lines = raw.split('\n').filter((l) => l.trim().length > 0)
106
+ if (lines.length === 0) throw new Error(`corpus-replay: corpus ${corpusPath} is empty - nothing to replay`)
107
+ return lines.map((l, i) => {
108
+ try {
109
+ return JSON.parse(l) as RunRecord
110
+ } catch (err) {
111
+ throw new Error(`corpus-replay: corpus ${corpusPath} line ${i + 1} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`)
112
+ }
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Resolve every recorded instanceId for one benchmark to its BenchTask in a single
118
+ * loadTasks call (the judge needs the task's gold/judge metadata). Fail loud when a
119
+ * recorded instance is absent from the live/fixture dataset - that is corpus/dataset
120
+ * drift, not a replayable record.
121
+ */
122
+ async function loadTaskMap(adapter: BenchmarkAdapter, ids: string[]): Promise<Map<string, BenchTask>> {
123
+ const tasks = await adapter.loadTasks({ ids })
124
+ const byId = new Map<string, BenchTask>()
125
+ for (const t of tasks) byId.set(t.id, t)
126
+ return byId
127
+ }
128
+
129
+ async function main(): Promise<void> {
130
+ const args = process.argv.slice(2)
131
+ const reJudge = args.includes('--judge')
132
+ const positional = args.filter((a) => !a.startsWith('--'))
133
+ const corpusPath = positional[0] ?? '/home/drew/code/agent-runtime/bench/corpus/finsearch.jsonl'
134
+ const benchOverride = positional[1] ?? process.env.BENCH
135
+
136
+ const records = await readCorpus(corpusPath)
137
+
138
+ // Group by benchmark so each adapter judges only its own records.
139
+ const byBench = new Map<string, RunRecord[]>()
140
+ for (const r of records) {
141
+ const bench = benchOverride ?? r.benchmark
142
+ const list = byBench.get(bench)
143
+ if (list) list.push(r)
144
+ else byBench.set(bench, [r])
145
+ }
146
+
147
+ // --selector[=METHOD]: score a deployable, non-oracle selector OFFLINE over the
148
+ // corpus. Each condition-run's k attempts are the candidates; the pick's stored
149
+ // verdict is read AFTER selection (zero new calls). Default to the random@k
150
+ // condition (independent attempts = the honest "can a selector beat a blind draw?").
151
+ // --selector / --selector=self-consistency → cluster by OUTPUT TEXT (Wang 2022);
152
+ // picks blind to the checker. The right baseline for free-text answers.
153
+ // --selector=verifier → rank by each attempt's DEPLOYABLE-CHECKER score (commit0
154
+ // pytest pass-rate / aec verify.py partial credit), reported on the CONTINUOUS
155
+ // graded reward with a paired bootstrap CI. The right selector for graded domains
156
+ // where text doesn't cluster (numeric JSON, diffs).
157
+ const selectorArg = args.find((a) => a === '--selector' || a.startsWith('--selector='))
158
+ if (selectorArg) {
159
+ const method = selectorArg.includes('=') ? selectorArg.slice('--selector='.length) : 'self-consistency'
160
+ if (method !== 'self-consistency' && method !== 'verifier') {
161
+ throw new Error(`corpus-replay --selector: unknown method "${method}" (use self-consistency | verifier)`)
162
+ }
163
+ const condArg = args.find((a) => a.startsWith('--condition='))
164
+ const condFilter = condArg ? condArg.slice('--condition='.length) : 'random'
165
+ const pct = (x: number) => `${(x * 100).toFixed(1)}%`
166
+ const pp = (x: number) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}pp`
167
+ let any = false
168
+ for (const [bench, recs] of byBench) {
169
+ const slice = recs.filter((r) => r.condition.includes(condFilter))
170
+ if (slice.length === 0) continue
171
+ any = true
172
+ if (method === 'verifier') {
173
+ const rep = summarizeVerifierSelector(slice)
174
+ const sig = rep.ci.p < 0.05 ? 'SIGNIFICANT' : 'n.s.'
175
+ console.log(
176
+ `\n[${bench}] selector=verifier-grounded (continuous graded reward) · condition~="${condFilter}" · n=${rep.n}` +
177
+ (rep.skipped > 0 ? ` (${rep.skipped} unscoreable)` : ''),
178
+ )
179
+ console.log(` blind (one-shot reward): ${pct(rep.blindReward)}`)
180
+ console.log(` random@k (mean-of-k reward): ${pct(rep.randomReward)} ← blind compute control`)
181
+ console.log(` selector@k (verifier-pick): ${pct(rep.selectorReward)}`)
182
+ console.log(` oracle@k (max-of-k ceiling): ${pct(rep.oracleReward)}`)
183
+ console.log(` resolve rates — blind ${pct(rep.blindResolveRate)} · selector ${pct(rep.selectorResolveRate)} · oracle ${pct(rep.oracleResolveRate)}`)
184
+ console.log(
185
+ ` ► selector − random: ${pp(rep.rewardVsRandom)} CI[${pp(rep.ci.lo)}, ${pp(rep.ci.hi)}] p=${rep.ci.p.toFixed(3)} (${sig}, ${rep.ci.discordant}/${rep.n} discordant)` +
186
+ ` ← THE GATE: does a deployable checker beat a blind draw at equal k?`,
187
+ )
188
+ continue
189
+ }
190
+ const rep = summarizeSelector(slice, selfConsistencySelect)
191
+ console.log(
192
+ `\n[${bench}] selector=self-consistency · condition~="${condFilter}" · n=${rep.n}` +
193
+ (rep.skipped > 0 ? ` (${rep.skipped} unscoreable)` : ''),
194
+ )
195
+ console.log(` blind (pass@1): ${pct(rep.blindRate)}`)
196
+ console.log(` random@k (control): ${pct(rep.randomRate)}`)
197
+ console.log(` selector@k (deployable): ${pct(rep.selectorRate)}`)
198
+ console.log(` oracle@k (ceiling): ${pct(rep.oracleRate)}`)
199
+ console.log(` ► selector − random: ${pp(rep.dVsRandom)} ← Phase-1 gate: does picking beat a blind draw at equal k?`)
200
+ console.log(` ► oracle − selector: ${pp(rep.gapToOracle)} (ceiling left on the table)`)
201
+ }
202
+ if (!any) {
203
+ const conds = [...new Set(records.map((r) => r.condition))]
204
+ throw new Error(
205
+ `corpus-replay --selector: no records match condition~="${condFilter}". Available conditions: ${conds.join(', ') || '(none)'}`,
206
+ )
207
+ }
208
+ return
209
+ }
210
+
211
+ let totalRecords = 0
212
+ let totalReplayable = 0
213
+ let totalJudged = 0
214
+ let totalAgree = 0
215
+
216
+ for (const [bench, recs] of byBench) {
217
+ const factory = ADAPTERS[bench]
218
+ if (!factory) {
219
+ throw new Error(
220
+ `corpus-replay: no adapter for benchmark ${JSON.stringify(bench)} (have: ${Object.keys(ADAPTERS).join(', ')}). ` +
221
+ `Add it to ADAPTERS or pass the correct BENCH override.`,
222
+ )
223
+ }
224
+ const adapter = factory()
225
+
226
+ // Replayable = has a recorded final output to re-score. Records where every
227
+ // round errored / went blank carry no output and cannot be offline-scored.
228
+ const replayable = recs.filter((r) => finalOutput(r) !== undefined)
229
+ totalRecords += recs.length
230
+ totalReplayable += replayable.length
231
+
232
+ if (!reJudge) {
233
+ // Default: recompute the STRUCTURAL verdict from stored attempt verdicts and
234
+ // compare to record.resolved. Zero model tokens; proves the corpus's stored
235
+ // outcome is internally consistent with its stored per-attempt verdicts.
236
+ let structAgree = 0
237
+ for (const r of replayable) {
238
+ if (storedFinalValid(r) === (r.resolved === true)) structAgree += 1
239
+ }
240
+ const rate = replayable.length > 0 ? structAgree / replayable.length : 0
241
+ console.log(
242
+ `[${bench}] structural replay: replayable n=${replayable.length}/${recs.length} ` +
243
+ `stored-verdict<->resolved agreement ${(rate * 100).toFixed(1)}% (${structAgree}/${replayable.length})`,
244
+ )
245
+ totalJudged += replayable.length
246
+ totalAgree += structAgree
247
+ continue
248
+ }
249
+
250
+ // --judge: preflight, resolve recorded instances -> BenchTask, re-run the judge
251
+ // on each recorded final output, compare the replayed verdict to record.resolved.
252
+ await adapter.preflight()
253
+ const ids = [...new Set(replayable.map((r) => r.instanceId))]
254
+ const taskMap = await loadTaskMap(adapter, ids)
255
+
256
+ let agree = 0
257
+ let judged = 0
258
+ let missing = 0
259
+ for (const r of replayable) {
260
+ const out = finalOutput(r)
261
+ if (out === undefined) continue // unreachable (replayable filter), kept explicit
262
+ const task = taskMap.get(r.instanceId)
263
+ if (!task) {
264
+ missing += 1
265
+ console.warn(`[${bench}] instance ${r.instanceId} not in dataset - corpus/dataset drift; skipping`)
266
+ continue
267
+ }
268
+ const verdict = await adapter.judge(task, out)
269
+ judged += 1
270
+ if ((verdict.resolved === true) === (r.resolved === true)) agree += 1
271
+ }
272
+ const rate = judged > 0 ? agree / judged : 0
273
+ console.log(
274
+ `[${bench}] judge replay: re-judged n=${judged}/${replayable.length} ` +
275
+ `replayed<->recorded agreement ${(rate * 100).toFixed(1)}% (${agree}/${judged})` +
276
+ (missing > 0 ? ` [${missing} instance(s) absent from dataset]` : ''),
277
+ )
278
+ totalJudged += judged
279
+ totalAgree += agree
280
+ }
281
+
282
+ const overall = totalJudged > 0 ? totalAgree / totalJudged : 0
283
+ console.log('')
284
+ console.log(
285
+ `replayable n=${totalReplayable}/${totalRecords} ` +
286
+ `${reJudge ? 'replayed<->recorded' : 'stored-verdict<->resolved'} agreement ${(overall * 100).toFixed(1)}% ` +
287
+ `(${totalAgree}/${totalJudged})${reJudge ? '' : ' [--judge to re-run the real judge]'}`,
288
+ )
289
+ console.log(
290
+ 'note: this is the offline replay/score substrate the controller-search ' +
291
+ '(GEPA / meta-harness over the corpus) consumes - scoreCandidateOffline() lets it ' +
292
+ 'price a candidate steer with one judge call (zero agent rollouts) against a recorded instance.',
293
+ )
294
+ }
295
+
296
+ // Run the CLI only when invoked directly (tsx src/corpus-replay.mts ...), so the
297
+ // scoreCandidateOffline seam can be imported as a library without firing main().
298
+ if (argv[1] && fileURLToPath(import.meta.url) === argv[1]) {
299
+ await main()
300
+ }