@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,320 @@
1
+ /**
2
+ * HotpotQA adapter (hotpotqa/hotpot_qa, config 'distractor', split 'validation').
3
+ * Multi-hop factoid QA. Worker artifact = a single free-text final answer string.
4
+ *
5
+ * Judge is the official HotpotQA / SQuAD-style metric, FULLY DETERMINISTIC — no
6
+ * LLM. Both the predicted final answer and the gold are normalized (lowercase,
7
+ * strip articles a/an/the, strip punctuation, collapse whitespace), then scored
8
+ * by exact-match (EM) and token-level F1. resolved = EM OR F1 >= HOTPOTQA_F1_PASS
9
+ * (default 0.6); score = the F1 (0..1). detail carries em + f1. This gives the
10
+ * suite a judge that needs no model tokens at all.
11
+ *
12
+ * metadata carries the gold answer + supporting_facts (the title/sent_id pairs of
13
+ * the gold supporting sentences) — a SOFT retrieval signal for the loop's critic,
14
+ * never part of the score.
15
+ *
16
+ * Requires for a live run: the bench `.venv` with `datasets` installed + network
17
+ * to Hugging Face. For offline/CI verification set HOTPOTQA_FIXTURES=1 to load the
18
+ * committed fixtures (bench/fixtures/hotpotqa.json) — no HF download.
19
+ */
20
+
21
+ import { execFile } from 'node:child_process'
22
+ import { readFile } from 'node:fs/promises'
23
+ import { join } from 'node:path'
24
+ import { fileURLToPath } from 'node:url'
25
+ import { promisify } from 'node:util'
26
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
27
+
28
+ const execFileAsync = promisify(execFile)
29
+ const BENCH_ROOT = fileURLToPath(new URL('../..', import.meta.url))
30
+ const PY = join(BENCH_ROOT, '.venv', 'bin', 'python')
31
+ const FIXTURES = join(BENCH_ROOT, 'fixtures', 'hotpotqa.json')
32
+
33
+ const DATASET = 'hotpotqa/hotpot_qa'
34
+ const DATASET_CONFIG = 'distractor'
35
+ const DATASET_SPLIT = 'validation'
36
+ const FINAL_ANSWER_SENTINEL = 'FINAL ANSWER:'
37
+
38
+ /** Default F1 pass threshold; HotpotQA's leaderboard reports EM and F1 separately. */
39
+ const DEFAULT_F1_PASS = 0.6
40
+
41
+ /** Worker contract appended to every task prompt. The judge keys off the sentinel. */
42
+ const WORKER_CONTRACT = [
43
+ '',
44
+ 'Answer this multi-hop question. Reason across the facts you need, then commit to a single short answer.',
45
+ `End your response with a single final line: \`${FINAL_ANSWER_SENTINEL} <answer>\``,
46
+ 'The answer after the sentinel must be the bare value only (no explanation on that line).',
47
+ ].join('\n')
48
+
49
+ interface SupportingFacts {
50
+ title: string[]
51
+ sent_id: number[]
52
+ }
53
+
54
+ interface HotpotRow {
55
+ id: string
56
+ question: string
57
+ answer: string
58
+ type: string
59
+ level: string
60
+ supporting_facts: SupportingFacts
61
+ }
62
+
63
+ interface HotpotMeta {
64
+ gold: string
65
+ supportingFacts: SupportingFacts
66
+ type: string
67
+ level: string
68
+ rawQuestion: string
69
+ }
70
+
71
+ /** Run the bench venv python with a script on stdin; return stdout (throws on nonzero). */
72
+ async function py(script: string, args: string[] = []): Promise<string> {
73
+ const { stdout } = await execFileAsync(PY, ['-c', script, ...args], {
74
+ maxBuffer: 1024 * 1024 * 256,
75
+ })
76
+ return stdout
77
+ }
78
+
79
+ const ARTICLES = new Set(['a', 'an', 'the'])
80
+
81
+ /**
82
+ * The official HotpotQA / SQuAD `normalize_answer`: lowercase, strip punctuation,
83
+ * drop articles (a/an/the), collapse whitespace. Token comparisons run on the
84
+ * output of this exactly as the published evaluator does.
85
+ */
86
+ export function normalizeAnswer(input: string): string {
87
+ const lower = input.toLowerCase()
88
+ // strip punctuation: keep word chars + whitespace only
89
+ const noPunct = lower.replace(/[^\w\s]/g, ' ')
90
+ const tokens = noPunct
91
+ .split(/\s+/)
92
+ .filter((t) => t.length > 0)
93
+ .filter((t) => !ARTICLES.has(t))
94
+ return tokens.join(' ').trim()
95
+ }
96
+
97
+ /** Normalized-token list (the F1 bag-of-tokens unit). */
98
+ function answerTokens(input: string): string[] {
99
+ const n = normalizeAnswer(input)
100
+ return n.length === 0 ? [] : n.split(' ')
101
+ }
102
+
103
+ /** Exact match on the normalized strings. */
104
+ export function exactMatch(prediction: string, gold: string): boolean {
105
+ return normalizeAnswer(prediction) === normalizeAnswer(gold)
106
+ }
107
+
108
+ /**
109
+ * Token-level F1, the SQuAD/HotpotQA definition. Multiset (bag) intersection of
110
+ * normalized tokens. Mirrors the published evaluator's special-case handling of
111
+ * yes/no/noanswer and empty bags: if either side is empty, F1 is 1 only when both
112
+ * are empty, else 0.
113
+ */
114
+ export function tokenF1(prediction: string, gold: string): number {
115
+ const predTokens = answerTokens(prediction)
116
+ const goldTokens = answerTokens(gold)
117
+ if (predTokens.length === 0 || goldTokens.length === 0) {
118
+ return predTokens.length === 0 && goldTokens.length === 0 ? 1 : 0
119
+ }
120
+ const goldCounts = new Map<string, number>()
121
+ for (const t of goldTokens) goldCounts.set(t, (goldCounts.get(t) ?? 0) + 1)
122
+ let common = 0
123
+ for (const t of predTokens) {
124
+ const left = goldCounts.get(t)
125
+ if (left !== undefined && left > 0) {
126
+ common += 1
127
+ goldCounts.set(t, left - 1)
128
+ }
129
+ }
130
+ if (common === 0) return 0
131
+ const precision = common / predTokens.length
132
+ const recall = common / goldTokens.length
133
+ return (2 * precision * recall) / (precision + recall)
134
+ }
135
+
136
+ /** Read the configured F1 pass threshold; fail loud on a malformed override. */
137
+ function f1PassThreshold(): number {
138
+ const raw = process.env.HOTPOTQA_F1_PASS
139
+ if (raw === undefined || raw.length === 0) return DEFAULT_F1_PASS
140
+ const v = Number(raw)
141
+ if (!Number.isFinite(v) || v < 0 || v > 1) {
142
+ throw new Error(`HOTPOTQA_F1_PASS must be a number in [0,1], got ${JSON.stringify(raw)}`)
143
+ }
144
+ return v
145
+ }
146
+
147
+ /**
148
+ * Parse the worker artifact into the final answer string.
149
+ * Order: the `FINAL ANSWER:` sentinel first (deterministic-extraction discipline);
150
+ * fall back to the trimmed last non-empty line. Returns '' when nothing is
151
+ * parseable (fail-closed — never guess), which judge() counts as resolved=false.
152
+ */
153
+ export function parseFinalAnswer(artifact: string): string {
154
+ const lines = artifact.split(/\r?\n/)
155
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
156
+ const line = lines[i] ?? ''
157
+ const idx = line.toUpperCase().indexOf(FINAL_ANSWER_SENTINEL)
158
+ if (idx !== -1) return line.slice(idx + FINAL_ANSWER_SENTINEL.length).trim()
159
+ }
160
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
161
+ const t = (lines[i] ?? '').trim()
162
+ if (t.length > 0) return t
163
+ }
164
+ return ''
165
+ }
166
+
167
+ function rowToTask(row: HotpotRow): BenchTask {
168
+ const meta: HotpotMeta = {
169
+ gold: row.answer,
170
+ supportingFacts: row.supporting_facts ?? { title: [], sent_id: [] },
171
+ type: row.type ?? '',
172
+ level: row.level ?? '',
173
+ rawQuestion: row.question,
174
+ }
175
+ return {
176
+ id: `hotpotqa-${row.id}`,
177
+ split: DATASET_SPLIT,
178
+ prompt: row.question + WORKER_CONTRACT,
179
+ metadata: meta as unknown as Record<string, unknown>,
180
+ }
181
+ }
182
+
183
+ function readMeta(task: BenchTask): HotpotMeta {
184
+ const md = task.metadata
185
+ if (!md || typeof md.gold !== 'string') {
186
+ throw new Error(`HotpotQA task ${task.id} missing metadata.gold — loadTasks did not populate it`)
187
+ }
188
+ return md as unknown as HotpotMeta
189
+ }
190
+
191
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
192
+ const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as HotpotRow[]
193
+ console.warn(
194
+ `[hotpotqa] HOTPOTQA_FIXTURES=1 — loading ${rows.length} committed fixtures from ${FIXTURES} (no HF download)`,
195
+ )
196
+ let tasks = rows.map(rowToTask)
197
+ if (opts.ids) {
198
+ const want = new Set(opts.ids)
199
+ tasks = tasks.filter((t) => want.has(t.id))
200
+ } else if (opts.limit !== undefined) {
201
+ tasks = tasks.slice(0, opts.limit)
202
+ }
203
+ return tasks
204
+ }
205
+
206
+ export function createHotpotqaAdapter(): BenchmarkAdapter {
207
+ const fixturesMode = process.env.HOTPOTQA_FIXTURES === '1'
208
+ // Validate the threshold at construction so a malformed env fails before any run.
209
+ f1PassThreshold()
210
+
211
+ return {
212
+ name: 'hotpotqa',
213
+
214
+ async preflight() {
215
+ if (fixturesMode) {
216
+ await readFile(FIXTURES, 'utf8').catch((err) => {
217
+ throw new Error(
218
+ `HOTPOTQA_FIXTURES=1 but ${FIXTURES} unreadable: ${err instanceof Error ? err.message : err}`,
219
+ )
220
+ })
221
+ return
222
+ }
223
+ try {
224
+ await py(
225
+ `from datasets import load_dataset
226
+ load_dataset(${JSON.stringify(DATASET)}, ${JSON.stringify(DATASET_CONFIG)}, split=${JSON.stringify(DATASET_SPLIT)})
227
+ print('ok')`,
228
+ )
229
+ } catch (err) {
230
+ const msg = err instanceof Error ? err.message : String(err)
231
+ throw new Error(
232
+ `hotpotqa preflight failed: ${msg}\n` +
233
+ `Fix: (1) python3 -m venv bench/.venv && bench/.venv/bin/pip install datasets ; ` +
234
+ `(2) ensure network access to Hugging Face for ${DATASET} (${DATASET_CONFIG}) ; ` +
235
+ `or set HOTPOTQA_FIXTURES=1 to run against the committed fixtures offline.`,
236
+ )
237
+ }
238
+ },
239
+
240
+ async loadTasks(opts: LoadOptions = {}) {
241
+ if (fixturesMode) return loadFixtures(opts)
242
+ const limit = opts.limit ?? 10
243
+ const script = `
244
+ import json, sys
245
+ from datasets import load_dataset
246
+ ds = load_dataset(${JSON.stringify(DATASET)}, ${JSON.stringify(DATASET_CONFIG)}, split=${JSON.stringify(DATASET_SPLIT)})
247
+ ids = set(json.loads(sys.argv[1])) if len(sys.argv) > 1 and sys.argv[1] else None
248
+ out = []
249
+ for r in ds:
250
+ rid = f"hotpotqa-{r['id']}"
251
+ if ids is not None and rid not in ids:
252
+ continue
253
+ sf = r.get('supporting_facts', {}) or {}
254
+ out.append({
255
+ "id": r["id"],
256
+ "question": r.get("question", ""),
257
+ "answer": r.get("answer", ""),
258
+ "type": str(r.get("type", "")),
259
+ "level": str(r.get("level", "")),
260
+ "supporting_facts": {
261
+ "title": list(sf.get("title", [])),
262
+ "sent_id": [int(x) for x in sf.get("sent_id", [])],
263
+ },
264
+ })
265
+ if ids is None and len(out) >= ${limit}:
266
+ break
267
+ print(json.dumps(out))
268
+ `
269
+ const stdout = await py(script, [opts.ids ? JSON.stringify(opts.ids) : ''])
270
+ const rows = JSON.parse(stdout) as HotpotRow[]
271
+ return rows.map(rowToTask)
272
+ },
273
+
274
+ async goldArtifact(task: BenchTask) {
275
+ // Gold artifact = the worker-contract serialization of the gold answer, so
276
+ // verify-judge proves gold→resolved through the SAME parse path the real
277
+ // artifact takes.
278
+ const meta = readMeta(task)
279
+ return `${FINAL_ANSWER_SENTINEL} ${meta.gold}`
280
+ },
281
+
282
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
283
+ const meta = readMeta(task)
284
+ const finalAnswer = parseFinalAnswer(artifact)
285
+
286
+ if (finalAnswer.length === 0) {
287
+ // Fail-closed: distinguish prompt-adherence failure from a wrong answer.
288
+ return {
289
+ resolved: false,
290
+ score: 0,
291
+ detail: JSON.stringify({
292
+ reason: 'no parseable answer',
293
+ em: false,
294
+ f1: 0,
295
+ normalizedGold: normalizeAnswer(meta.gold),
296
+ }),
297
+ }
298
+ }
299
+
300
+ const em = exactMatch(finalAnswer, meta.gold)
301
+ const f1 = tokenF1(finalAnswer, meta.gold)
302
+ const threshold = f1PassThreshold()
303
+ const resolved = em || f1 >= threshold
304
+ return {
305
+ resolved,
306
+ score: f1,
307
+ detail: JSON.stringify({
308
+ em,
309
+ f1,
310
+ threshold,
311
+ normalizedAnswer: normalizeAnswer(finalAnswer),
312
+ normalizedGold: normalizeAnswer(meta.gold),
313
+ type: meta.type,
314
+ level: meta.level,
315
+ supportingFacts: meta.supportingFacts,
316
+ }),
317
+ }
318
+ },
319
+ }
320
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * HumanEval adapter — the deployable-checker domain as a `BenchmarkAdapter`, so the
3
+ * gate runner (`runGate`) can A/B the STEERING regime on it: a real rollout
4
+ * through the `Supervisor` that self-corrects across rounds, vs blind
5
+ * random@k. This is the experiment `humaneval-gate.mts` names as "the next one" —
6
+ * the gate measures SELECTION over stateless single completions; this measures
7
+ * whether observe→steer (self-correction) beats blind compute at equal k.
8
+ *
9
+ * Worker artifact = the model's reply (a Python function, fenced or raw). The
10
+ * DETERMINISTIC judge runs the candidate against the task's own `test` in an
11
+ * isolated `--network=none` python container — exit 0 = pass. No gold
12
+ * `canonical_solution` is ever shown to the model; `goldArtifact` returns it only
13
+ * to self-verify the judge before spending tokens.
14
+ *
15
+ * The primitives (loader / extractor / Docker checker) live here and are reused by
16
+ * `humaneval-gate.mts` — one home, no duplication.
17
+ */
18
+
19
+ import { execFile } from 'node:child_process'
20
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
21
+ import { tmpdir } from 'node:os'
22
+ import { join } from 'node:path'
23
+ import { gunzipSync } from 'node:zlib'
24
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
25
+
26
+ const humanevalUrl = 'https://github.com/openai/human-eval/raw/master/data/HumanEval.jsonl.gz'
27
+ const dockerImage = 'python:3.12-slim'
28
+ const dockerTimeoutMs = Number(process.env.DOCKER_TIMEOUT_MS ?? 20000)
29
+
30
+ export interface HumanEvalTask {
31
+ taskId: string
32
+ prompt: string
33
+ test: string
34
+ entryPoint: string
35
+ /** Reference solution body — used ONLY to self-verify the judge, never shown to the worker. */
36
+ canonicalSolution?: string
37
+ }
38
+
39
+ /** Pull the 164-task HumanEval JSONL.gz and parse it. Fail loud on a non-OK fetch
40
+ * or a malformed line — a silently-short task set would poison the gate. `offset`
41
+ * selects a deeper slice (the later tasks are harder) so the worker has a
42
+ * correctable middle band rather than a saturated easy prefix. */
43
+ export async function loadHumanEval(limit: number, offset = 0): Promise<HumanEvalTask[]> {
44
+ const res = await fetch(humanevalUrl)
45
+ if (!res.ok) throw new Error(`HumanEval fetch HTTP ${res.status}: ${humanevalUrl}`)
46
+ const gz = Buffer.from(await res.arrayBuffer())
47
+ const text = gunzipSync(gz).toString('utf8')
48
+ const tasks: HumanEvalTask[] = []
49
+ for (const line of text.split('\n')) {
50
+ if (line.trim() === '') continue
51
+ const d = JSON.parse(line) as {
52
+ task_id?: string
53
+ prompt?: string
54
+ test?: string
55
+ entry_point?: string
56
+ canonical_solution?: string
57
+ }
58
+ if (!d.task_id || !d.prompt || !d.test || !d.entry_point) {
59
+ throw new Error(`malformed HumanEval record: ${line.slice(0, 120)}`)
60
+ }
61
+ tasks.push({
62
+ taskId: d.task_id,
63
+ prompt: d.prompt,
64
+ test: d.test,
65
+ entryPoint: d.entry_point,
66
+ ...(d.canonical_solution ? { canonicalSolution: d.canonical_solution } : {}),
67
+ })
68
+ }
69
+ if (tasks.length === 0) throw new Error('HumanEval parsed to 0 tasks')
70
+ if (offset >= tasks.length) throw new Error(`OFFSET ${offset} >= dataset size ${tasks.length}`)
71
+ return tasks.slice(offset, offset + limit)
72
+ }
73
+
74
+ const solveInstruction =
75
+ 'Complete the following Python function. Output the COMPLETE function definition (signature, docstring optional, body) inside a single ```python code block. Include any imports the function needs. Do not write tests or example calls.'
76
+
77
+ export function basePrompt(task: HumanEvalTask): string {
78
+ return `${solveInstruction}\n\n\`\`\`python\n${task.prompt}\`\`\``
79
+ }
80
+
81
+ /** Extract the function source from a model reply: prefer a fenced ```python (or
82
+ * bare ```) block, else fall back to the raw text. The deployable program adds the
83
+ * prompt header (imports + signature context), so a candidate that returns only a
84
+ * body still runs; a candidate that re-defines the function shadows the header. */
85
+ export function extractCode(reply: string): string {
86
+ const fenced = reply.match(/```(?:python|py)?\s*\n([\s\S]*?)```/i)
87
+ if (fenced && typeof fenced[1] === 'string') return fenced[1].trim()
88
+ return reply.trim()
89
+ }
90
+
91
+ /** The deployable test program: the prompt header (imports + signature/docstring the
92
+ * model was given), then the candidate (its def shadows the header's stub), then the
93
+ * task's own check() suite and the call. No gold solution anywhere. */
94
+ function buildProgram(task: HumanEvalTask, candidate: string): string {
95
+ return `${task.prompt}\n${candidate}\n\n${task.test}\n\ncheck(${task.entryPoint})\n`
96
+ }
97
+
98
+ export interface CheckResult {
99
+ /** {0,1} pass-count for this candidate (1 = the check() suite passed). */
100
+ pass: number
101
+ /** On failure: the interpreter stderr tail (traceback / failing assertion). The
102
+ * execution-grounded feedback a self-repair loop steers on; ignored by selection. */
103
+ detail?: string
104
+ }
105
+
106
+ /** Run one candidate's deployable test program in an isolated container:
107
+ * `docker run --rm --network=none -v <tmp>:/w -w /w <img> python /w/p.py`.
108
+ * Exit 0 → pass. A docker invocation error (binary missing, daemon down, image
109
+ * pull failure) is NOT a test failure — it throws so the harness fails loud rather
110
+ * than scoring every candidate 0 from a broken checker. */
111
+ let dockerRunSeq = 0
112
+
113
+ export function runChecker(task: HumanEvalTask, candidate: string): Promise<CheckResult> {
114
+ const dir = mkdtempSync(join(tmpdir(), 'hev-'))
115
+ writeFileSync(join(dir, 'p.py'), buildProgram(task, candidate))
116
+ // Unique container name so we can force-reap it regardless of the docker client's state.
117
+ const name = `hev-${process.pid}-${dockerRunSeq++}`
118
+ return new Promise<CheckResult>((resolvePromise, reject) => {
119
+ let settled = false
120
+ const cleanup = () => {
121
+ rmSync(dir, { recursive: true, force: true })
122
+ // `execFile`'s `timeout` kills the docker CLIENT, not the container — a hung
123
+ // `python` would otherwise pin a CPU forever. Force-reap by name (fire-and-forget;
124
+ // the name is unique, so no reuse race).
125
+ execFile('docker', ['rm', '-f', name], () => {})
126
+ }
127
+ const finish = (res: CheckResult) => {
128
+ if (settled) return
129
+ settled = true
130
+ clearTimeout(backstop)
131
+ cleanup()
132
+ resolvePromise(res)
133
+ }
134
+ const fail = (e: Error) => {
135
+ if (settled) return
136
+ settled = true
137
+ clearTimeout(backstop)
138
+ cleanup()
139
+ reject(e)
140
+ }
141
+ // A hung container can leave the docker client stuck forwarding SIGTERM, so the
142
+ // execFile callback never fires. This guarantees resolution (and reap) after the
143
+ // timeout, independent of the callback.
144
+ const backstop = setTimeout(() => finish({ pass: 0 }), dockerTimeoutMs + 3000)
145
+ execFile(
146
+ 'docker',
147
+ [
148
+ 'run',
149
+ '--rm',
150
+ '--name',
151
+ name,
152
+ '--network=none',
153
+ '--cpus=1',
154
+ '--memory=512m',
155
+ '-v',
156
+ `${dir}:/w:ro`,
157
+ '-w',
158
+ '/w',
159
+ dockerImage,
160
+ 'python',
161
+ '/w/p.py',
162
+ ],
163
+ { timeout: dockerTimeoutMs, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024 },
164
+ (err, _stdout, stderr) => {
165
+ if (err) {
166
+ const e = err as NodeJS.ErrnoException & { killed?: boolean; code?: number | string }
167
+ if (e.code === 'ENOENT') {
168
+ fail(new Error('docker binary not found on PATH — cannot run the deployable checker'))
169
+ return
170
+ }
171
+ if (/cannot connect to the docker daemon|is the docker daemon running|permission denied while trying to connect/i.test(stderr)) {
172
+ fail(new Error(`docker daemon unreachable: ${stderr.slice(0, 200)}`))
173
+ return
174
+ }
175
+ if (/(unable to find image|pull access denied|manifest unknown|error response from daemon).*(pull|repository|registry)/i.test(stderr)) {
176
+ fail(new Error(`docker image ${dockerImage} unavailable: ${stderr.slice(0, 200)}`))
177
+ return
178
+ }
179
+ // killed-by-timeout or a non-zero exit (assert failure / error) are genuine
180
+ // test FAILURES — score 0, do not throw. Carry the stderr tail as the
181
+ // execution-grounded failure detail (empty ⇒ timeout/SIGKILL left no output).
182
+ finish({ pass: 0, detail: (stderr || '').slice(-600) || 'timed out (no output)' })
183
+ return
184
+ }
185
+ finish({ pass: 1 })
186
+ },
187
+ )
188
+ })
189
+ }
190
+
191
+ /** A HumanEval task carries its checker inputs in metadata so the deterministic
192
+ * judge can rebuild the deployable program from a `BenchTask` alone. */
193
+ interface HumanEvalMeta extends Record<string, unknown> {
194
+ promptHeader: string
195
+ test: string
196
+ entryPoint: string
197
+ canonicalSolution?: string
198
+ }
199
+
200
+ function toBenchTask(t: HumanEvalTask): BenchTask {
201
+ const metadata: HumanEvalMeta = {
202
+ promptHeader: t.prompt,
203
+ test: t.test,
204
+ entryPoint: t.entryPoint,
205
+ ...(t.canonicalSolution ? { canonicalSolution: t.canonicalSolution } : {}),
206
+ }
207
+ return { id: t.taskId, prompt: basePrompt(t), metadata }
208
+ }
209
+
210
+ function taskFromMeta(task: BenchTask): HumanEvalTask {
211
+ const m = task.metadata as HumanEvalMeta | undefined
212
+ if (!m?.promptHeader || !m.test || !m.entryPoint) {
213
+ throw new Error(`HumanEval judge: task ${task.id} missing checker metadata`)
214
+ }
215
+ return { taskId: task.id, prompt: m.promptHeader, test: m.test, entryPoint: m.entryPoint }
216
+ }
217
+
218
+ /** The HumanEval `BenchmarkAdapter`. OFFSET (env) selects the correctable middle
219
+ * band; loadTasks honors `limit`/`ids`. The judge is the Docker deployable checker. */
220
+ export function createHumanEvalAdapter(): BenchmarkAdapter {
221
+ return {
222
+ name: 'humaneval',
223
+ async preflight() {
224
+ // The judge is the only hard dependency; it fails loud on a missing/broken
225
+ // docker, so a cheap presence check here gives an earlier, clearer signal.
226
+ await new Promise<void>((resolve, reject) => {
227
+ execFile('docker', ['version', '--format', '{{.Server.Version}}'], (err) => {
228
+ if (err) reject(new Error('HumanEval judge needs a running Docker daemon (python:3.12-slim, --network=none)'))
229
+ else resolve()
230
+ })
231
+ })
232
+ },
233
+ async loadTasks(opts?: LoadOptions) {
234
+ const offset = Number(process.env.OFFSET ?? 0)
235
+ // Pull a generous window when filtering by id, else exactly `limit` from offset.
236
+ const all = await loadHumanEval(opts?.ids ? 164 : (opts?.limit ?? 8), offset)
237
+ const picked = opts?.ids ? all.filter((t) => opts.ids?.includes(t.taskId)) : all
238
+ return picked.map(toBenchTask)
239
+ },
240
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
241
+ const { pass } = await runChecker(taskFromMeta(task), extractCode(artifact))
242
+ return { resolved: pass === 1, score: pass, detail: pass === 1 ? 'tests passed' : 'tests failed' }
243
+ },
244
+ async goldArtifact(task: BenchTask) {
245
+ const sol = (task.metadata as HumanEvalMeta | undefined)?.canonicalSolution
246
+ // The canonical solution is the function BODY; the program header supplies the
247
+ // signature, so the body alone runs against check().
248
+ return sol ? sol : undefined
249
+ },
250
+ }
251
+ }