@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,311 @@
1
+ /**
2
+ * Mind2Web adapter (osunlp/Multimodal-Mind2Web, split 'test_domain' by default).
3
+ * Web-agent ACTION PREDICTION: each task is one step of a real web task — given
4
+ * the natural-language goal and the page's candidate elements, the worker picks
5
+ * the SINGLE element to act on and the action (CLICK / TYPE / SELECT [+ value]).
6
+ *
7
+ * Judge is the published Mind2Web step metric, FULLY DETERMINISTIC — no LLM:
8
+ * element correct ⇔ predicted backend_node_id ∈ pos_candidates ids
9
+ * operation correct ⇔ op-type matches AND (TYPE/SELECT) the value matches
10
+ * resolved (Step-SR) ⇔ element correct AND operation correct
11
+ * score = 0.6·element + 0.4·operation (a gradient for the optimizer; the right
12
+ * element is most of the credit, mirroring Element-Acc ≫ Op as the lever)
13
+ * This is the low-noise reward a certifiable directive-lift needs: the number is a
14
+ * programmatic match against human-verified ground truth, not a judge's opinion.
15
+ *
16
+ * The worker artifact is three sentinel lines (ELEMENT / ACTION / VALUE) so the
17
+ * judge extracts deterministically; the prompt presents the candidate set as a
18
+ * choice over backend_node_id ordered by id (position uncorrelated with the answer).
19
+ *
20
+ * Each step carries the dataset's OWN page screenshot (written to a temp file by
21
+ * the loader); the worker drops it into a browser.<op> span so run-capsule's screen
22
+ * capsule turns the run into a film — the real page, not a re-rendered DOM.
23
+ *
24
+ * Requires for a live run: a python with `datasets` + `pillow` and network to
25
+ * Hugging Face. Point M2W_PYTHON at it (defaults to bench/.venv/bin/python).
26
+ */
27
+
28
+ import { execFile } from 'node:child_process'
29
+ import { mkdir } from 'node:fs/promises'
30
+ import { tmpdir } from 'node:os'
31
+ import { join } from 'node:path'
32
+ import { fileURLToPath } from 'node:url'
33
+ import { promisify } from 'node:util'
34
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
35
+
36
+ const execFileAsync = promisify(execFile)
37
+ const BENCH_ROOT = fileURLToPath(new URL('../..', import.meta.url))
38
+ const PY = process.env.M2W_PYTHON ?? join(BENCH_ROOT, '.venv', 'bin', 'python')
39
+
40
+ const DATASET = 'osunlp/Multimodal-Mind2Web'
41
+ const DEFAULT_SPLIT = process.env.M2W_SPLIT ?? 'test_domain'
42
+ const SHOTS_DIR = process.env.M2W_SHOTS ?? join(tmpdir(), 'm2w-shots')
43
+ /** Candidate-set cap presented to the worker (always keeps all pos candidates). */
44
+ const CANDIDATE_CAP = Number(process.env.M2W_CANDIDATE_CAP ?? 30)
45
+
46
+ /** The worker contract appended to every task prompt; the judge keys off these. */
47
+ const WORKER_CONTRACT = [
48
+ '',
49
+ 'Pick the SINGLE next element to act on, then end your response with EXACTLY these three lines:',
50
+ 'ELEMENT: <the [id] number of the chosen element>',
51
+ 'ACTION: <CLICK | TYPE | SELECT>',
52
+ 'VALUE: <text to type or option to select; leave empty for CLICK>',
53
+ ].join('\n')
54
+
55
+ interface RawCandidate {
56
+ id: string
57
+ label: string
58
+ }
59
+ interface Mind2WebRow {
60
+ id: string
61
+ task: string
62
+ website: string
63
+ domain: string
64
+ subdomain: string
65
+ op: string
66
+ value: string
67
+ goldIds: string[]
68
+ candidates: RawCandidate[]
69
+ screenshotPath: string
70
+ targetRepr: string
71
+ }
72
+ interface Mind2WebMeta {
73
+ task: string
74
+ website: string
75
+ domain: string
76
+ op: string
77
+ value: string
78
+ goldIds: string[]
79
+ candidateIds: string[]
80
+ screenshotPath: string
81
+ targetRepr: string
82
+ }
83
+
84
+ /** Run the bench python with a script on stdin; return stdout (throws on nonzero). */
85
+ async function py(script: string, args: string[] = []): Promise<string> {
86
+ const { stdout } = await execFileAsync(PY, ['-c', script, ...args], { maxBuffer: 1024 * 1024 * 256 })
87
+ return stdout
88
+ }
89
+
90
+ /** The loader: stream the split, skip steps with no positive candidate (fail-loud,
91
+ * never a silent score-0), cap the candidate set, save each screenshot to a file. */
92
+ const LOADER = `
93
+ import json, sys, os
94
+ from datasets import load_dataset
95
+ cfg = json.loads(sys.argv[1])
96
+ split = cfg["split"]; limit = cfg.get("limit"); cap = cfg.get("cap", 30)
97
+ shots = cfg["shotsDir"]; ids = set(cfg["ids"]) if cfg.get("ids") else None
98
+ os.makedirs(shots, exist_ok=True)
99
+ KEEP = ("aria_label","aria-label","role","type","name","placeholder","title","alt","value","text","id","class","href")
100
+ def label(tag, attr):
101
+ try:
102
+ a = json.loads(attr) if isinstance(attr, str) else (attr or {})
103
+ except Exception:
104
+ a = {}
105
+ parts = []
106
+ for k in KEEP:
107
+ v = a.get(k)
108
+ if v:
109
+ sv = str(v).replace("\\n", " ").strip()
110
+ if k == "class": sv = sv[:40]
111
+ if k == "href": sv = sv[:50]
112
+ if sv: parts.append(k + "=" + sv[:60])
113
+ return "<" + str(tag) + "> " + " ".join(parts[:6])
114
+ def cands(raw):
115
+ out = []
116
+ for c in raw or []:
117
+ try:
118
+ d = json.loads(c) if isinstance(c, str) else c
119
+ except Exception:
120
+ continue
121
+ bid = str(d.get("backend_node_id", ""))
122
+ if not bid: continue
123
+ out.append({"id": bid, "label": label(d.get("tag", "?"), d.get("attributes"))})
124
+ return out
125
+ ds = load_dataset(${JSON.stringify(DATASET)}, split=split, streaming=True)
126
+ emitted = []; skipped = 0; scanned = 0
127
+ for r in ds:
128
+ scanned += 1
129
+ if ids is not None and r["action_uid"] not in ids:
130
+ if scanned > 8000: break
131
+ continue
132
+ pos = cands(r.get("pos_candidates"))
133
+ if not pos:
134
+ skipped += 1
135
+ continue
136
+ neg = cands(r.get("neg_candidates"))
137
+ seen = set(p["id"] for p in pos); merged = list(pos)
138
+ for n in neg:
139
+ if len(merged) >= cap: break
140
+ if n["id"] in seen: continue
141
+ seen.add(n["id"]); merged.append(n)
142
+ merged.sort(key=lambda x: int(x["id"]) if x["id"].isdigit() else 0)
143
+ try:
144
+ op = json.loads(r["operation"]) if isinstance(r["operation"], str) else r["operation"]
145
+ except Exception:
146
+ op = {}
147
+ sp = os.path.join(shots, str(r["action_uid"]) + ".jpg")
148
+ try:
149
+ r["screenshot"].convert("RGB").save(sp, "JPEG", quality=70)
150
+ except Exception:
151
+ sp = ""
152
+ emitted.append({
153
+ "id": r["action_uid"], "task": r.get("confirmed_task", ""),
154
+ "website": r.get("website", ""), "domain": r.get("domain", ""), "subdomain": r.get("subdomain", ""),
155
+ "op": str(op.get("op", "")).upper(), "value": str(op.get("value", "")),
156
+ "goldIds": [p["id"] for p in pos], "candidates": merged,
157
+ "screenshotPath": sp, "targetRepr": r.get("target_action_reprs", ""),
158
+ })
159
+ if ids is None and limit is not None and len(emitted) >= limit: break
160
+ if ids is not None and len(emitted) >= len(ids): break
161
+ sys.stderr.write("[mind2web] emitted=%d skipped_empty_pos=%d scanned=%d\\n" % (len(emitted), skipped, scanned)); sys.stderr.flush()
162
+ print(json.dumps(emitted)); sys.stdout.flush()
163
+ os._exit(0)
164
+ `
165
+
166
+ function buildPrompt(row: Mind2WebRow): string {
167
+ const choices = row.candidates.map((c) => ` [${c.id}] ${c.label}`).join('\n')
168
+ return [
169
+ `Web task: ${row.task}`,
170
+ '',
171
+ 'You are taking the NEXT single action on the current web page. Choose the one element',
172
+ 'to act on from the candidates below (each line is "[id] <tag> attributes"):',
173
+ choices,
174
+ WORKER_CONTRACT,
175
+ ].join('\n')
176
+ }
177
+
178
+ function rowToTask(row: Mind2WebRow): BenchTask {
179
+ const meta: Mind2WebMeta = {
180
+ task: row.task,
181
+ website: row.website,
182
+ domain: row.domain,
183
+ op: row.op,
184
+ value: row.value,
185
+ goldIds: row.goldIds,
186
+ candidateIds: row.candidates.map((c) => c.id),
187
+ screenshotPath: row.screenshotPath,
188
+ targetRepr: row.targetRepr,
189
+ }
190
+ return {
191
+ id: `mind2web-${row.id}`,
192
+ split: DEFAULT_SPLIT,
193
+ prompt: buildPrompt(row),
194
+ metadata: meta as unknown as Record<string, unknown>,
195
+ }
196
+ }
197
+
198
+ function readMeta(task: BenchTask): Mind2WebMeta {
199
+ const md = task.metadata
200
+ if (!md || !Array.isArray((md as { goldIds?: unknown }).goldIds)) {
201
+ throw new Error(`mind2web task ${task.id} missing metadata.goldIds — loadTasks did not populate it`)
202
+ }
203
+ return md as unknown as Mind2WebMeta
204
+ }
205
+
206
+ /** Normalize a TYPE/SELECT value for comparison: lowercase, collapse whitespace. */
207
+ function normValue(s: string): string {
208
+ return s.toLowerCase().replace(/\s+/g, ' ').trim()
209
+ }
210
+
211
+ interface ParsedAction {
212
+ elementId: string
213
+ op: string
214
+ value: string
215
+ }
216
+
217
+ /** Parse the worker artifact's ELEMENT / ACTION / VALUE sentinel lines.
218
+ * Fail-closed: a missing ELEMENT or ACTION returns null (judge → resolved=false). */
219
+ export function parseAction(artifact: string): ParsedAction | null {
220
+ const elem = /ELEMENT:\s*\[?(\d+)\]?/i.exec(artifact)
221
+ const op = /ACTION:\s*(CLICK|TYPE|SELECT)/i.exec(artifact)
222
+ if (!elem?.[1] || !op?.[1]) return null
223
+ const val = /VALUE:\s*(.*)/i.exec(artifact)
224
+ return { elementId: elem[1], op: op[1].toUpperCase(), value: (val?.[1] ?? '').trim() }
225
+ }
226
+
227
+ export function createMind2WebAdapter(): BenchmarkAdapter {
228
+ const split = DEFAULT_SPLIT
229
+
230
+ return {
231
+ name: 'mind2web',
232
+
233
+ async preflight() {
234
+ try {
235
+ await py(
236
+ `import os, sys
237
+ from datasets import load_dataset
238
+ import PIL
239
+ ds = load_dataset(${JSON.stringify(DATASET)}, split=${JSON.stringify(split)}, streaming=True)
240
+ next(iter(ds))
241
+ print('ok'); sys.stdout.flush(); os._exit(0)`,
242
+ )
243
+ } catch (err) {
244
+ const msg = err instanceof Error ? err.message : String(err)
245
+ throw new Error(
246
+ `mind2web preflight failed: ${msg}\n` +
247
+ `Fix: (1) a python with datasets + pillow (python3 -m venv bench/.venv && bench/.venv/bin/pip install datasets pillow) ; ` +
248
+ `(2) network access to Hugging Face for ${DATASET} (split ${split}) ; ` +
249
+ `point M2W_PYTHON at the python if not bench/.venv.`,
250
+ )
251
+ }
252
+ },
253
+
254
+ async loadTasks(opts: LoadOptions = {}) {
255
+ const cfg = {
256
+ split: opts.split ?? split,
257
+ limit: opts.ids ? undefined : (opts.limit ?? 10),
258
+ cap: CANDIDATE_CAP,
259
+ shotsDir: SHOTS_DIR,
260
+ ids: opts.ids ? opts.ids.map((id) => id.replace(/^mind2web-/, '')) : undefined,
261
+ }
262
+ await mkdir(SHOTS_DIR, { recursive: true })
263
+ const stdout = await py(LOADER, [JSON.stringify(cfg)])
264
+ const rows = JSON.parse(stdout) as Mind2WebRow[]
265
+ return rows.map(rowToTask)
266
+ },
267
+
268
+ async goldArtifact(task: BenchTask) {
269
+ // Gold = the worker-contract serialization of the ground-truth action, so
270
+ // verify-judge proves gold→resolved through the SAME parse path the real
271
+ // artifact takes. The first accepted positive id is the canonical target.
272
+ const meta = readMeta(task)
273
+ const id = meta.goldIds[0]
274
+ if (!id) return undefined
275
+ return `ELEMENT: ${id}\nACTION: ${meta.op}\nVALUE: ${meta.value}`
276
+ },
277
+
278
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
279
+ const meta = readMeta(task)
280
+ const parsed = parseAction(artifact)
281
+ if (!parsed) {
282
+ return {
283
+ resolved: false,
284
+ score: 0,
285
+ detail: JSON.stringify({ reason: 'no parseable ELEMENT/ACTION', goldOp: meta.op, goldIds: meta.goldIds }),
286
+ }
287
+ }
288
+ const elementCorrect = meta.goldIds.includes(parsed.elementId)
289
+ const opMatch = parsed.op === meta.op
290
+ const valueMatch = meta.op === 'CLICK' ? true : normValue(parsed.value) === normValue(meta.value)
291
+ const operationCorrect = opMatch && valueMatch
292
+ const resolved = elementCorrect && operationCorrect
293
+ const score = 0.6 * (elementCorrect ? 1 : 0) + 0.4 * (operationCorrect ? 1 : 0)
294
+ return {
295
+ resolved,
296
+ score,
297
+ detail: JSON.stringify({
298
+ elementCorrect,
299
+ opMatch,
300
+ valueMatch,
301
+ predicted: parsed,
302
+ goldOp: meta.op,
303
+ goldValue: meta.value,
304
+ goldIds: meta.goldIds,
305
+ website: meta.website,
306
+ domain: meta.domain,
307
+ }),
308
+ }
309
+ },
310
+ }
311
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Offline ProgramBench adapter test. The judge needs the official `programbench`
3
+ * harness + Docker on linux/amd64, neither installed in CI, so this exercises the
4
+ * offline parts (fixtures loadTasks, the file-manifest OutputAdapter, goldArtifact)
5
+ * and asserts the harness-backed path FAILS LOUD with the documented fix — never a
6
+ * fake score. Run: PROGRAMBENCH_FIXTURES=1 npx tsx --test src/benchmarks/programbench.test.mts
7
+ */
8
+ import assert from 'node:assert/strict'
9
+ import { test } from 'node:test'
10
+ import { createProgrambenchAdapter, programbenchSubmissionOutput } from './programbench'
11
+
12
+ process.env.PROGRAMBENCH_FIXTURES = '1'
13
+
14
+ type Events = Parameters<typeof programbenchSubmissionOutput.parse>[0]
15
+ const stream = (text: string): Events => [{ data: { finalText: text } }] as unknown as Events
16
+
17
+ test('loadTasks (fixtures) yields cleanroom tasks with instance metadata + submission contract', async () => {
18
+ const a = createProgrambenchAdapter()
19
+ const tasks = await a.loadTasks({ ids: ['abishekvashok__cmatrix.5c082c6'] })
20
+ assert.equal(tasks.length, 1)
21
+ const t = tasks[0]
22
+ assert.equal(t.id, 'abishekvashok__cmatrix.5c082c6')
23
+ assert.match(t.prompt, /compile\.sh/)
24
+ assert.match(t.prompt, /path:<relative\/path>/)
25
+ const md = t.metadata as Record<string, unknown>
26
+ assert.equal(md.instanceId, 'abishekvashok__cmatrix.5c082c6')
27
+ })
28
+
29
+ test('loadTasks limit slices the fixture set', async () => {
30
+ const a = createProgrambenchAdapter()
31
+ const tasks = await a.loadTasks({ limit: 2 })
32
+ assert.equal(tasks.length, 2)
33
+ })
34
+
35
+ test('submission OutputAdapter: ```path: blocks re-serialize into the ===FILE:=== envelope the driver splits on', () => {
36
+ const out = programbenchSubmissionOutput.parse(
37
+ stream('here\n```path:compile.sh\ncc -o executable main.c\n```\n```path:main.c\nint main(){}\n```\n'),
38
+ )
39
+ assert.match(out, /===FILE:compile\.sh===\ncc -o executable main\.c/)
40
+ assert.match(out, /===FILE:main\.c===\nint main\(\)\{\}/)
41
+ })
42
+
43
+ test('submission OutputAdapter: no file block → empty (fail-closed, harness scores missing compile.sh as 0)', () => {
44
+ assert.equal(programbenchSubmissionOutput.parse(stream('I could not solve it.')), '')
45
+ })
46
+
47
+ test('goldArtifact is undefined — oracle is stripped original source, documented, not fabricated', async () => {
48
+ const a = createProgrambenchAdapter()
49
+ const [t] = await a.loadTasks({ ids: ['abishekvashok__cmatrix.5c082c6'] })
50
+ assert.equal(await a.goldArtifact(t), undefined)
51
+ })
52
+
53
+ test('preflight FAILS LOUD with the install/Docker fix when the harness is absent', async () => {
54
+ const a = createProgrambenchAdapter()
55
+ await assert.rejects(a.preflight(), (e: Error) => {
56
+ assert.match(e.message, /pip install programbench/)
57
+ assert.match(e.message, /amd64/)
58
+ return true
59
+ })
60
+ })
@@ -0,0 +1,210 @@
1
+ /**
2
+ * ProgramBench adapter (facebookresearch/ProgramBench) — cleanroom
3
+ * reverse-engineering: rebuild a black-box executable's behavior from scratch.
4
+ * The agent is given only the gold `./executable` (run-only) + stripped docs and
5
+ * must produce a `submission.tar.gz` whose `./compile.sh` builds an `./executable`
6
+ * with identical observable behavior. Judge = the official `programbench`
7
+ * harness: it extracts the submission, runs compile.sh in the per-task cleanroom
8
+ * Docker image, then runs the HIDDEN behavioral pytest suites (pulled via
9
+ * `programbench blob sync`). Score = fraction of non-ignored tests passed
10
+ * (GRADED, applying the tests.json ignore mask); resolved = all non-ignored tests
11
+ * pass. Fully deterministic — no LLM judge.
12
+ *
13
+ * OutputAdapter is stream-only, so the worker emits its codebase as fenced
14
+ * `path:`-prefixed file blocks (including compile.sh); the adapter materializes
15
+ * those into submission.tar.gz. Test execution is delegated to the real
16
+ * `programbench eval`/`info` harness — pytest scoring + the ignore mask are NOT
17
+ * reimplemented here.
18
+ *
19
+ * Requires for a live run: the bench `.venv` with `programbench` installed +
20
+ * Docker on linux/amd64 (per-task `<image>:task_cleanroom` images) + HF access for
21
+ * the hidden test blobs. For offline/CI listing set PROGRAMBENCH_FIXTURES=1 to load
22
+ * the committed instance ids (bench/fixtures/programbench.json); judging still
23
+ * needs the harness + Docker and fails loud without them — never a fabricated score.
24
+ */
25
+
26
+ import { join } from 'node:path'
27
+ import { readFile } from 'node:fs/promises'
28
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
29
+ import { benchRoot, preflightVenvImports, runVenvScriptStdin } from './_harness'
30
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
31
+
32
+ const FIXTURES = join(benchRoot, 'fixtures', 'programbench.json')
33
+
34
+ const TESTS_DATASET = 'programbench/ProgramBench-Tests'
35
+ const TREE_API = `https://huggingface.co/api/datasets/${TESTS_DATASET}/tree/main`
36
+
37
+ interface PbFixture {
38
+ instance_id: string
39
+ image_name: string
40
+ note?: string
41
+ }
42
+
43
+ interface PbMeta {
44
+ instanceId: string
45
+ imageName: string
46
+ }
47
+
48
+ const PROMPT = [
49
+ 'This is a cleanroom reverse-engineering task. You are given a compiled black-box `./executable` (run-only) and stripped documentation in the workspace.',
50
+ 'Write an ORIGINAL codebase from scratch that, when built, produces an `./executable` with byte-for-byte identical observable behavior (stdout/stderr/exit codes/filesystem effects) to the original. You may run `./executable` to probe its behavior.',
51
+ 'You MUST include a `./compile.sh` at the workspace root that builds your source into `./executable` at the workspace root.',
52
+ 'Emit your COMPLETE submission as the LAST thing in your reply: one fenced block per file, each opening fence line being exactly ```path:<relative/path>``` followed by the file contents, then a closing fence. Include compile.sh and every source file. Nothing after the final closing fence.',
53
+ ].join('\n')
54
+
55
+ function fixtureToTask(f: PbFixture): BenchTask {
56
+ const meta: PbMeta = { instanceId: f.instance_id, imageName: f.image_name }
57
+ return {
58
+ id: f.instance_id,
59
+ prompt: PROMPT,
60
+ metadata: meta as unknown as Record<string, unknown>,
61
+ }
62
+ }
63
+
64
+ function readMeta(task: BenchTask): PbMeta {
65
+ const md = task.metadata
66
+ if (!md || typeof md.instanceId !== 'string') {
67
+ throw new Error(`programbench task ${task.id} missing metadata.instanceId — loadTasks did not populate it`)
68
+ }
69
+ return md as unknown as PbMeta
70
+ }
71
+
72
+ function selectTasks(fixtures: PbFixture[], opts: LoadOptions): BenchTask[] {
73
+ let tasks = fixtures.map(fixtureToTask)
74
+ if (opts.ids) {
75
+ const want = new Set(opts.ids)
76
+ tasks = tasks.filter((t) => want.has(t.id))
77
+ } else if (opts.limit !== undefined) {
78
+ tasks = tasks.slice(0, opts.limit)
79
+ }
80
+ return tasks
81
+ }
82
+
83
+ /** `owner__repo.commit` → DockerHub image name `owner/repo` for `<image>:task_cleanroom`. */
84
+ function imageName(instanceId: string): string {
85
+ return instanceId.split('.')[0].replace('__', '/')
86
+ }
87
+
88
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
89
+ const fixtures = JSON.parse(await readFile(FIXTURES, 'utf8')) as PbFixture[]
90
+ console.warn(
91
+ `[programbench] PROGRAMBENCH_FIXTURES=1 — loading ${fixtures.length} committed instance ids from ${FIXTURES} (no HF tree fetch)`,
92
+ )
93
+ return selectTasks(fixtures, opts)
94
+ }
95
+
96
+ /** Enumerate real instance ids from the HF Tests dataset tree (one dir per
97
+ * instance). Throws on a non-OK response (fail loud). */
98
+ async function fetchInstanceIds(): Promise<PbFixture[]> {
99
+ const res = await fetch(TREE_API)
100
+ if (!res.ok) throw new Error(`programbench tree HTTP ${res.status}: ${TREE_API}`)
101
+ const tree = (await res.json()) as Array<{ path: string; type: string }>
102
+ const ids = tree.filter((e) => e.type === 'directory' && e.path.includes('__')).map((e) => e.path)
103
+ if (ids.length === 0) throw new Error(`programbench: no instance dirs in ${TESTS_DATASET} tree`)
104
+ return ids.map((id) => ({ instance_id: id, image_name: imageName(id) }))
105
+ }
106
+
107
+ /**
108
+ * Run the official programbench harness for one instance over the worker's
109
+ * file-manifest submission. The driver builds the run-dir layout (one folder
110
+ * holding submission.tar.gz), runs `programbench blob sync` + `programbench eval`,
111
+ * then parses <instance_id>.eval.json applying the tests.json ignore mask via
112
+ * `programbench info` logic. Emits {passed,total,resolved} as the last stdout line.
113
+ */
114
+ async function runHarness(meta: PbMeta, submission: string): Promise<BenchScore> {
115
+ const judge = join(benchRoot, 'scripts', 'programbench_judge.py')
116
+ let stdout: string
117
+ try {
118
+ // The submission manifest is piped to the driver's stdin via the shared
119
+ // stdin-aware runner — execFile's `input` option is not honored async and
120
+ // hangs the driver's sys.stdin.read() forever.
121
+ stdout = await runVenvScriptStdin(judge, ['--instance', meta.instanceId], submission, { cwd: benchRoot })
122
+ } catch (err) {
123
+ const e = err as { message?: string }
124
+ throw new Error(`programbench harness failed for ${meta.instanceId}: ${(e.message || String(err)).slice(0, 1500)}`)
125
+ }
126
+ const report = JSON.parse(stdout.trim().split('\n').at(-1) ?? '{}') as {
127
+ passed?: number
128
+ total?: number
129
+ resolved?: boolean
130
+ error?: string
131
+ }
132
+ if (report.error) throw new Error(`programbench harness error for ${meta.instanceId}: ${report.error}`)
133
+ if (typeof report.passed !== 'number' || typeof report.total !== 'number') {
134
+ throw new Error(`programbench judge returned no {passed,total}: ${stdout.slice(0, 400)}`)
135
+ }
136
+ const score = report.total > 0 ? report.passed / report.total : 0
137
+ return {
138
+ resolved: report.resolved ?? (report.total > 0 && report.passed === report.total),
139
+ score,
140
+ detail: JSON.stringify({ instanceId: meta.instanceId, passed: report.passed, total: report.total }),
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Parse the worker stream into the submission text the driver materializes: the
146
+ * concatenation of every ```path:<p>``` fenced block. Passed through verbatim to
147
+ * the python driver, which tars it. Empty when the worker emitted no file block
148
+ * (fail-closed → the harness scores a missing compile.sh as 0).
149
+ */
150
+ export const programbenchSubmissionOutput: OutputAdapter<string> = {
151
+ parse(events) {
152
+ let text = ''
153
+ for (const ev of events) {
154
+ const d = (ev as { data?: Record<string, unknown> })?.data
155
+ const t = d?.finalText ?? d?.text ?? d?.result
156
+ if (typeof t === 'string' && t.length > 0) text = t
157
+ }
158
+ const blocks = [...text.matchAll(/```path:([^\n`]+)\n([\s\S]*?)```/g)]
159
+ if (blocks.length === 0) return ''
160
+ // Re-serialize in the same path:<p> envelope the driver splits on.
161
+ return blocks.map((m) => `===FILE:${m[1].trim()}===\n${m[2]}`).join('\n')
162
+ },
163
+ }
164
+
165
+ export function createProgrambenchAdapter(): BenchmarkAdapter {
166
+ const fixturesMode = process.env.PROGRAMBENCH_FIXTURES === '1'
167
+
168
+ return {
169
+ name: 'programbench',
170
+ output: programbenchSubmissionOutput,
171
+
172
+ async preflight() {
173
+ await preflightVenvImports({
174
+ modules: ['programbench'],
175
+ requireDocker: true,
176
+ fix:
177
+ `Fix: (1) python3 -m venv bench/.venv && bench/.venv/bin/pip install programbench ; ` +
178
+ `(2) Docker on linux/amd64 (per-task <image>:task_cleanroom images; will NOT run on ARM) ; ` +
179
+ `(3) HF access for the hidden test blobs (the driver runs \`programbench blob sync\`). ` +
180
+ `Set PROGRAMBENCH_FIXTURES=1 to list the committed instance ids offline.`,
181
+ })
182
+ },
183
+
184
+ async loadTasks(opts: LoadOptions = {}) {
185
+ if (fixturesMode) return loadFixtures(opts)
186
+ let fixtures: PbFixture[]
187
+ try {
188
+ fixtures = await fetchInstanceIds()
189
+ } catch (err) {
190
+ console.warn(
191
+ `[programbench] live tree fetch failed (${err instanceof Error ? err.message : err}); falling back to committed fixtures at ${FIXTURES}`,
192
+ )
193
+ return loadFixtures(opts)
194
+ }
195
+ return selectTasks(fixtures, opts)
196
+ },
197
+
198
+ async goldArtifact() {
199
+ // The oracle is the original repo's source (stripped from the task image),
200
+ // not redistributable as a portable submission string. Judge correctness is
201
+ // proven by running the real harness on a real solve, not a synthetic gold.
202
+ return undefined
203
+ },
204
+
205
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
206
+ const meta = readMeta(task)
207
+ return runHarness(meta, artifact)
208
+ },
209
+ }
210
+ }