@tangle-network/agent-bench 0.1.0 → 0.3.5

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 (139) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +26 -1
  4. package/fixtures/aec-bench.json +18 -0
  5. package/fixtures/agentbench-dbbench.json +22 -0
  6. package/fixtures/bfcl.json +45 -0
  7. package/fixtures/commit0.json +72 -0
  8. package/fixtures/crag.json +10 -0
  9. package/fixtures/dabstep.json +22 -0
  10. package/fixtures/enterpriseops-gym.json +103 -0
  11. package/fixtures/finresearchbench.json +21 -0
  12. package/fixtures/finsearchcomp.json +66 -0
  13. package/fixtures/frames.json +26 -0
  14. package/fixtures/hotpotqa.json +182 -0
  15. package/fixtures/nomiracl.json +26 -0
  16. package/fixtures/open-rag-bench.json +16 -0
  17. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  18. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  19. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  20. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  21. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  22. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  23. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  24. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  25. package/fixtures/programbench.json +17 -0
  26. package/fixtures/ragbench.json +21 -0
  27. package/fixtures/simpleqa.json +121 -0
  28. package/fixtures/t2-ragbench.json +13 -0
  29. package/fixtures/tau2-bench.json +16 -0
  30. package/fixtures/tau3-banking.json +16 -0
  31. package/fixtures/toollm.json +28 -0
  32. package/fixtures/webarena-verified.json +20 -0
  33. package/package.json +39 -15
  34. package/pier_agents/__init__.py +18 -0
  35. package/pier_agents/candidate_contract.py +755 -0
  36. package/pier_agents/process_boundary.py +321 -0
  37. package/pier_agents/tangle_candidate.py +907 -0
  38. package/pier_agents/workspace_boundary.py +368 -0
  39. package/scripts/appworld_driver.py +359 -0
  40. package/scripts/cadbench_prepare.py +22 -0
  41. package/scripts/cadgenbench_hard_parts.py +48 -0
  42. package/scripts/clbench_codebase_judge.py +73 -0
  43. package/scripts/commit0_judge.py +170 -0
  44. package/scripts/dabstep_judge.py +42 -0
  45. package/scripts/enterpriseops_gym_judge.py +281 -0
  46. package/scripts/programbench_judge.py +120 -0
  47. package/scripts/render-gate-chart.mjs +176 -0
  48. package/scripts/run-package-tests.mjs +56 -0
  49. package/scripts/terminate-pier-trial.mts +66 -0
  50. package/scripts/trata-hedge/README.md +56 -0
  51. package/scripts/trata-hedge/run.sh +60 -0
  52. package/scripts/trata-hedge/solve.py +83 -0
  53. package/scripts/verify-packed-consumer.mjs +224 -0
  54. package/scripts/verify-pier-agent.mts +715 -0
  55. package/scripts/verify-pier-pair.mts +74 -0
  56. package/scripts/verify-pier-recovery.mts +139 -0
  57. package/src/adapters.ts +26 -0
  58. package/src/benchmarks/_harness.test.mts +178 -0
  59. package/src/benchmarks/_harness.ts +239 -16
  60. package/src/benchmarks/agentbench.ts +163 -0
  61. package/src/benchmarks/appworld.test.mts +15 -9
  62. package/src/benchmarks/bfcl.ts +346 -0
  63. package/src/benchmarks/crag.ts +137 -0
  64. package/src/benchmarks/dabstep.test.mts +70 -0
  65. package/src/benchmarks/dabstep.ts +212 -0
  66. package/src/benchmarks/external-adapters.test.mts +150 -0
  67. package/src/benchmarks/finresearchbench.ts +269 -0
  68. package/src/benchmarks/humaneval.ts +20 -8
  69. package/src/benchmarks/nomiracl.ts +180 -0
  70. package/src/benchmarks/open-rag-bench.ts +153 -0
  71. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  72. package/src/benchmarks/rag-shared.ts +327 -0
  73. package/src/benchmarks/ragbench.ts +171 -0
  74. package/src/benchmarks/swe-bench.test.mts +61 -0
  75. package/src/benchmarks/swe-bench.ts +201 -19
  76. package/src/benchmarks/t2-ragbench.ts +166 -0
  77. package/src/benchmarks/tau-bench-shared.ts +214 -0
  78. package/src/benchmarks/tau2-bench.ts +30 -0
  79. package/src/benchmarks/tau3-banking.ts +29 -0
  80. package/src/benchmarks/terminal-bench.test.mts +33 -0
  81. package/src/benchmarks/terminal-bench.ts +23 -8
  82. package/src/benchmarks/toollm.ts +254 -0
  83. package/src/benchmarks/types.ts +42 -0
  84. package/src/benchmarks/webarena-verified.ts +200 -0
  85. package/src/commit0-prereqs.sh +0 -0
  86. package/src/coordination-mcp-container-reach.mts +181 -0
  87. package/src/decoder-live.mts +1 -1
  88. package/src/examples/README.md +103 -39
  89. package/src/examples/benchmark-matrix.mts +101 -0
  90. package/src/examples/lean-proof-gate.README.md +77 -0
  91. package/src/examples/lean-proof-gate.mts +162 -0
  92. package/src/examples/lean-verify.ts +95 -0
  93. package/src/examples/lean.Dockerfile +12 -0
  94. package/src/examples/math-demo.mts +9 -7
  95. package/src/examples/strategy-demo.mts +10 -12
  96. package/src/gate.ts +3 -2
  97. package/src/hev-eval.mts +69 -0
  98. package/src/hev-improve.mts +169 -0
  99. package/src/hev-structural.mts +688 -0
  100. package/src/index.ts +73 -0
  101. package/src/mbpp-structural.mts +662 -0
  102. package/src/pier-agent.test-fixtures.mts +19 -0
  103. package/src/pier-agent.test.mts +363 -0
  104. package/src/pier-agent.ts +657 -0
  105. package/src/pier-result-grader.mjs +30 -0
  106. package/src/pier-result-grader.test.mts +62 -0
  107. package/src/pier-result-grader.ts +108 -0
  108. package/src/pier-task-outcome.test.mts +117 -0
  109. package/src/pier-task-outcome.ts +240 -0
  110. package/src/pier-trial-controller.test.mts +412 -0
  111. package/src/pier-trial-controller.ts +858 -0
  112. package/src/pier-trial-supervisor.mjs +352 -0
  113. package/src/resolve-client.ts +25 -2
  114. package/src/run-benchmarks-cli.mts +72 -0
  115. package/src/run-benchmarks-report.ts +66 -0
  116. package/src/run-benchmarks.test.mts +231 -0
  117. package/src/run-benchmarks.ts +589 -0
  118. package/src/smoke-structural-rollout.mts +393 -0
  119. package/src/swe-bench-env.test.ts +207 -0
  120. package/src/swe-bench-env.ts +554 -0
  121. package/src/swe-jail.ts +293 -0
  122. package/src/swe-self-improve.mts +84 -0
  123. package/src/swe-structural-judge-policy.test.ts +117 -0
  124. package/src/swe-structural-judge-policy.ts +133 -0
  125. package/src/swe-structural-policy.test.ts +124 -0
  126. package/src/swe-structural-policy.ts +132 -0
  127. package/src/swe-structural-provenance.test.ts +93 -0
  128. package/src/swe-structural-provenance.ts +138 -0
  129. package/src/swe-structural.mts +1260 -0
  130. package/src/swe-temp.ts +14 -0
  131. package/src/tb-container-executor.mts +234 -0
  132. package/src/tb-container-executor.test.mts +99 -0
  133. package/src/tb-supervisor-sidecar.mts +222 -0
  134. package/src/trata-gepa.mts +1 -1
  135. package/steerers/eops-itsm-population.json +1 -0
  136. package/tb_agents/opencode_refine_agent.py +117 -0
  137. package/tb_agents/opencode_router_agent.py +406 -0
  138. package/tb_agents/opencode_supervisor_agent.py +239 -0
  139. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,662 @@
1
+ /**
2
+ * MBPP STRUCTURAL lever — the transfer test for the HumanEval result in
3
+ * `hev-structural.mts`: best-of-k selection + self-repair grounded ONLY on
4
+ * VISIBLE checks, graded on HIDDEN tests the harness never shows the model.
5
+ *
6
+ * MBPP (sanitized, 427 tasks) has no docstring examples; the standard protocol
7
+ * shows the model test_list[0] (it pins the function name/signature). So:
8
+ * VISIBLE = test_list[0] (+ TESTGEN model-written asserts, generated from the
9
+ * description BEFORE any candidate exists)
10
+ * HIDDEN = test_list[1:] (with test_imports) — the grading suite
11
+ * A task with <2 asserts cannot split visible/hidden and is dropped at load.
12
+ *
13
+ * Architecture is hev-structural.mts verbatim (kept self-contained on purpose —
14
+ * the HumanEval rig is frozen post-verification): Phase A (harness: sample →
15
+ * visible-check select → visible-grounded repair, all decisions locked) then
16
+ * Phase B (hidden grading); per-call NONCE sentinels on both judges; global
17
+ * docker semaphore; in-container timeout + exit reaper; incremental OUT jsonl;
18
+ * per-task error rows with >15% abort; paired bootstrap + exact sign test.
19
+ *
20
+ * CALIBRATE=1: reference solutions through both judges (hidden self-check must
21
+ * be ~100%; failures listed — some MBPP references are known-defective).
22
+ *
23
+ * TANGLE_API_KEY=… WORKER_MODEL=meta-llama/Meta-Llama-3-8B-Instruct-Lite \
24
+ * ROUTER_BASE=https://api.together.xyz/v1 MBPP_JSON=/abs/sanitized-mbpp.json \
25
+ * N=427 K=5 REPAIRS=2 TESTGEN=6 TEMPERATURE=0.8 OUT=/abs/rows.jsonl \
26
+ * tsx src/mbpp-structural.mts
27
+ */
28
+ import { execFile, execFileSync } from 'node:child_process'
29
+ import { randomBytes } from 'node:crypto'
30
+ import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
31
+ import { tmpdir } from 'node:os'
32
+ import { join } from 'node:path'
33
+ import { extractCode } from './benchmarks/humaneval'
34
+ import { type PairedLift, pairedLift, pool } from './stats.mts'
35
+
36
+ const dockerImage = 'python:3.12-slim'
37
+ const dockerTimeoutMs = Number(process.env.DOCKER_TIMEOUT_MS ?? 20000)
38
+
39
+ function must(name: string): string {
40
+ const v = process.env[name]
41
+ if (!v) throw new Error(`env ${name} is required`)
42
+ return v
43
+ }
44
+
45
+ // ---------- dataset ----------
46
+
47
+ export interface MbppTask {
48
+ taskId: number
49
+ description: string
50
+ /** test_list[0] — shown to the model; pins the function name/signature */
51
+ shownAssert: string
52
+ /** test_list[1:] — the hidden grading suite */
53
+ hiddenAsserts: string[]
54
+ testImports: string[]
55
+ entryPoint: string
56
+ /** reference solution — judge self-check only, never shown to the model */
57
+ referenceCode: string
58
+ }
59
+
60
+ interface RawMbpp {
61
+ task_id: number
62
+ prompt: string
63
+ code: string
64
+ test_imports?: string[]
65
+ test_list: string[]
66
+ }
67
+
68
+ /** Load sanitized MBPP, sorted by task_id. Drops (and counts) tasks that cannot
69
+ * split visible/hidden (<2 asserts) or whose entry point cannot be resolved.
70
+ * Entry point = the FIRST called name in test_list[0] that also has a
71
+ * `def <name>` in the reference code — `assert set(f(...)) == …` must resolve
72
+ * to f, not set. */
73
+ export function loadMbpp(limit: number, offset = 0): { tasks: MbppTask[]; droppedShort: number[]; droppedEntry: number[] } {
74
+ const path = process.env.MBPP_JSON
75
+ if (!path) throw new Error('env MBPP_JSON is required (path to sanitized-mbpp.json)')
76
+ const raw = JSON.parse(readFileSync(path, 'utf8')) as RawMbpp[]
77
+ raw.sort((a, b) => a.task_id - b.task_id)
78
+ const tasks: MbppTask[] = []
79
+ const droppedShort: number[] = []
80
+ const droppedEntry: number[] = []
81
+ for (const d of raw) {
82
+ if (!d.test_list || d.test_list.length < 2) {
83
+ droppedShort.push(d.task_id)
84
+ continue
85
+ }
86
+ const shown = d.test_list[0] as string
87
+ const calls = [...shown.matchAll(/(\w+)\s*\(/g)].map((m) => m[1] as string)
88
+ const entry = calls.find((c) => new RegExp(`def\\s+${c}\\s*\\(`).test(d.code))
89
+ if (!entry) {
90
+ droppedEntry.push(d.task_id)
91
+ continue
92
+ }
93
+ tasks.push({
94
+ taskId: d.task_id,
95
+ description: d.prompt,
96
+ shownAssert: shown,
97
+ hiddenAsserts: d.test_list.slice(1),
98
+ testImports: d.test_imports ?? [],
99
+ entryPoint: entry,
100
+ referenceCode: d.code,
101
+ })
102
+ }
103
+ if (offset >= tasks.length) throw new Error(`OFFSET ${offset} >= usable dataset size ${tasks.length}`)
104
+ return { tasks: tasks.slice(offset, offset + limit), droppedShort, droppedEntry }
105
+ }
106
+
107
+ const solveInstruction =
108
+ 'Write a Python function for the following task. Output the COMPLETE function definition (plus any imports it needs) inside a single ```python code block. Do not write tests or example calls.'
109
+
110
+ function basePrompt(task: MbppTask): string {
111
+ return `${solveInstruction}\n\nTask: ${task.description}\nYour function must satisfy this example test:\n\`\`\`python\n${task.shownAssert}\n\`\`\``
112
+ }
113
+
114
+ // ---------- docker semaphore + jailed runner (mirrors hev-structural) ----------
115
+
116
+ let dockerSlots = 6
117
+ let dockerInFlight = 0
118
+ const dockerWaiters: Array<() => void> = []
119
+ async function withDockerSlot<T>(fn: () => Promise<T>): Promise<T> {
120
+ if (dockerInFlight >= dockerSlots) await new Promise<void>((r) => dockerWaiters.push(r))
121
+ dockerInFlight += 1
122
+ try {
123
+ return await fn()
124
+ } finally {
125
+ dockerInFlight -= 1
126
+ dockerWaiters.shift()?.()
127
+ }
128
+ }
129
+
130
+ const containerPrefix = `mbpps-${process.pid}`
131
+ let containerSeq = 0
132
+
133
+ function reapContainers(): void {
134
+ try {
135
+ const ids = execFileSync('docker', ['ps', '-aq', '--filter', `name=${containerPrefix}`], { timeout: 10000 }).toString().trim()
136
+ if (ids) execFileSync('docker', ['rm', '-f', ...ids.split('\n')], { timeout: 15000 })
137
+ } catch {
138
+ /* reaper is best-effort by design */
139
+ }
140
+ }
141
+ process.on('SIGINT', () => {
142
+ reapContainers()
143
+ process.exit(130)
144
+ })
145
+ process.on('SIGTERM', () => {
146
+ reapContainers()
147
+ process.exit(143)
148
+ })
149
+
150
+ interface JailResult {
151
+ exitCode: number
152
+ stdout: string
153
+ stderr: string
154
+ }
155
+
156
+ function runJailed(program: string): Promise<JailResult> {
157
+ return withDockerSlot(
158
+ () =>
159
+ new Promise<JailResult>((resolvePromise, reject) => {
160
+ const dir = mkdtempSync(join(tmpdir(), 'mbpps-'))
161
+ writeFileSync(join(dir, 'p.py'), program)
162
+ const name = `${containerPrefix}-${containerSeq++}`
163
+ let settled = false
164
+ const cleanup = () => {
165
+ rmSync(dir, { recursive: true, force: true })
166
+ execFile('docker', ['rm', '-f', name], () => {})
167
+ }
168
+ const finish = (res: JailResult) => {
169
+ if (settled) return
170
+ settled = true
171
+ clearTimeout(backstop)
172
+ cleanup()
173
+ resolvePromise(res)
174
+ }
175
+ const fail = (e: Error) => {
176
+ if (settled) return
177
+ settled = true
178
+ clearTimeout(backstop)
179
+ cleanup()
180
+ reject(e)
181
+ }
182
+ const backstop = setTimeout(() => finish({ exitCode: 124, stdout: '', stderr: 'backstop timeout (no output)' }), dockerTimeoutMs + 5000)
183
+ const inContainerSecs = Math.ceil(dockerTimeoutMs / 1000) + 2
184
+ execFile(
185
+ 'docker',
186
+ [
187
+ 'run', '--rm', '--name', name, '--network=none', '--cpus=1', '--memory=512m',
188
+ '-v', `${dir}:/w:ro`, '-w', '/w', dockerImage,
189
+ 'timeout', '-s', 'KILL', String(inContainerSecs), 'python', '/w/p.py',
190
+ ],
191
+ { timeout: dockerTimeoutMs + 3000, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024 },
192
+ (err, stdout, stderr) => {
193
+ if (err) {
194
+ const e = err as NodeJS.ErrnoException & { code?: number | string }
195
+ if (e.code === 'ENOENT') return fail(new Error('docker binary not found on PATH'))
196
+ const se = stderr ?? ''
197
+ if (/cannot connect to the docker daemon|is the docker daemon running|permission denied while trying to connect/i.test(se)) {
198
+ return fail(new Error(`docker daemon unreachable: ${se.slice(0, 200)}`))
199
+ }
200
+ if (/(unable to find image|pull access denied|manifest unknown|error response from daemon).*(pull|repository|registry)/i.test(se)) {
201
+ return fail(new Error(`docker image ${dockerImage} unavailable: ${se.slice(0, 200)}`))
202
+ }
203
+ const code = typeof e.code === 'number' ? e.code : 1
204
+ return finish({ exitCode: code, stdout: stdout ?? '', stderr: se })
205
+ }
206
+ finish({ exitCode: 0, stdout: stdout ?? '', stderr: stderr ?? '' })
207
+ },
208
+ )
209
+ }),
210
+ )
211
+ }
212
+
213
+ // ---------- the visible-check judge (Phase A's ONLY signal) ----------
214
+
215
+ export interface VisibleResult {
216
+ /** total visible checks run: shown assert + generated asserts (-1 = crashed) */
217
+ attempted: number
218
+ failed: number
219
+ failureOutput: string
220
+ pass: boolean
221
+ sAttempted?: number
222
+ sFailed?: number
223
+ gAttempted?: number
224
+ gFailed?: number
225
+ }
226
+
227
+ /** Each visible assert runs INDIVIDUALLY in try/except so one malformed line
228
+ * cannot zero the rest; per-assert failure text is the repair feedback. The
229
+ * hidden asserts (test_list[1:]) never appear here. */
230
+ function buildVisibleProgram(task: MbppTask, candidate: string, nonce: string, genTests: string[]): string {
231
+ const shownB64 = Buffer.from(JSON.stringify([task.shownAssert]), 'utf8').toString('base64')
232
+ const genB64 = Buffer.from(JSON.stringify(genTests), 'utf8').toString('base64')
233
+ return `${task.testImports.join('\n')}\n${candidate}\n
234
+ import base64 as _b64, json as _json, sys as _sys
235
+ _shown = _json.loads(_b64.b64decode("${shownB64}").decode("utf8"))
236
+ _gen = _json.loads(_b64.b64decode("${genB64}").decode("utf8"))
237
+ _out = []
238
+ def _run(_tests):
239
+ _att, _fail = 0, 0
240
+ for _t in _tests:
241
+ _att += 1
242
+ try:
243
+ exec(_t, dict(globals()))
244
+ except Exception as _e:
245
+ _fail += 1
246
+ _out.append("CHECK FAILED: %s -> %s: %s" % (_t.strip()[:200], type(_e).__name__, str(_e)[:200]))
247
+ return _att, _fail
248
+ _s_att, _s_fail = _run(_shown)
249
+ _g_att, _g_fail = _run(_gen)
250
+ _att, _fail = _s_att + _g_att, _s_fail + _g_fail
251
+ print("VISIBLE-${nonce} attempted=%d failed=%d satt=%d sfail=%d gatt=%d gfail=%d" % (_att, _fail, _s_att, _s_fail, _g_att, _g_fail))
252
+ _sys.stdout.write("\\n".join(_out)[-1500:])
253
+ _sys.exit(0 if _att > 0 and _fail == 0 else 1)
254
+ `
255
+ }
256
+
257
+ export async function runVisibleJudge(task: MbppTask, candidate: string, genTests: string[]): Promise<VisibleResult> {
258
+ const nonce = randomBytes(8).toString('hex')
259
+ const r = await runJailed(buildVisibleProgram(task, candidate, nonce, genTests))
260
+ const summary = new RegExp(`VISIBLE-${nonce} attempted=(\\d+) failed=(\\d+) satt=(\\d+) sfail=(\\d+) gatt=(\\d+) gfail=(\\d+)`).exec(r.stdout)
261
+ if (!summary) {
262
+ const detail = (r.stderr || r.stdout).slice(-1500) || 'timed out (no output)'
263
+ return { attempted: -1, failed: -1, failureOutput: detail, pass: false }
264
+ }
265
+ const attempted = Number(summary[1])
266
+ const failed = Number(summary[2])
267
+ const failureOutput = r.stdout.replace(summary[0], '').slice(-1500)
268
+ return {
269
+ attempted,
270
+ failed,
271
+ failureOutput,
272
+ pass: attempted > 0 && failed === 0,
273
+ sAttempted: Number(summary[3]),
274
+ sFailed: Number(summary[4]),
275
+ gAttempted: Number(summary[5]),
276
+ gFailed: Number(summary[6]),
277
+ }
278
+ }
279
+
280
+ function visibleScore(h: VisibleResult): number {
281
+ if (h.attempted <= 0) return h.attempted === 0 ? 0 : -1
282
+ // The shown assert is OFFICIAL (printed in the model's prompt); generated asserts
283
+ // are the model's own guesses and run ~70% wrong on MBPP's one-sentence specs
284
+ // (measured on the pilot: 71/102 failed on officially-passing code). Rank by the
285
+ // official signal first; guesses only break ties — otherwise 6 noisy guesses
286
+ // outvote the one reliable check and selection goes NEGATIVE.
287
+ const sA = h.sAttempted ?? 0
288
+ const gA = h.gAttempted ?? 0
289
+ const sFrac = sA > 0 ? (sA - (h.sFailed ?? 0)) / sA : 0
290
+ const gFrac = gA > 0 ? (gA - (h.gFailed ?? 0)) / gA : 0
291
+ return sFrac + 0.001 * gFrac
292
+ }
293
+
294
+ // ---------- the hidden judge (Phase B / calibration ONLY) ----------
295
+
296
+ function buildHiddenProgram(task: MbppTask, candidate: string, nonce: string): string {
297
+ return `${task.testImports.join('\n')}\n${candidate}\n\n${task.hiddenAsserts.join('\n')}\nprint("HIDDEN-${nonce} PASS")\n`
298
+ }
299
+
300
+ async function runHiddenJudge(task: MbppTask, candidate: string): Promise<{ pass: number; detail?: string }> {
301
+ const nonce = randomBytes(8).toString('hex')
302
+ const r = await runJailed(buildHiddenProgram(task, candidate, nonce))
303
+ if (r.exitCode === 0 && r.stdout.includes(`HIDDEN-${nonce} PASS`)) return { pass: 1 }
304
+ return { pass: 0, detail: (r.stderr || r.stdout).slice(-600) || 'timed out (no output)' }
305
+ }
306
+
307
+ // ---------- model client (mirrors hev-structural) ----------
308
+
309
+ interface ClientCfg {
310
+ base: string
311
+ key: string
312
+ model: string
313
+ maxTokens: number
314
+ temperature: number
315
+ }
316
+
317
+ interface Completion {
318
+ content: string
319
+ attempts: number
320
+ tokensIn: number
321
+ tokensOut: number
322
+ }
323
+
324
+ async function complete(cfg: ClientCfg, messages: Array<{ role: string; content: string }>): Promise<Completion> {
325
+ let lastErr = ''
326
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
327
+ if (attempt > 1) await new Promise((r) => setTimeout(r, 2000 * 2 ** attempt))
328
+ const ctl = new AbortController()
329
+ const timer = setTimeout(() => ctl.abort(), 240_000)
330
+ try {
331
+ const res = await fetch(`${cfg.base}/chat/completions`, {
332
+ method: 'POST',
333
+ headers: { Authorization: `Bearer ${cfg.key}`, 'Content-Type': 'application/json' },
334
+ body: JSON.stringify({ model: cfg.model, max_tokens: cfg.maxTokens, temperature: cfg.temperature, messages }),
335
+ signal: ctl.signal,
336
+ })
337
+ if (!res.ok) {
338
+ lastErr = `HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`
339
+ continue
340
+ }
341
+ const d = (await res.json()) as {
342
+ choices?: Array<{ message?: { content?: string } }>
343
+ usage?: { prompt_tokens?: number; completion_tokens?: number }
344
+ }
345
+ const content = d.choices?.[0]?.message?.content ?? ''
346
+ if (content.trim() === '') {
347
+ lastErr = 'empty content'
348
+ continue
349
+ }
350
+ return { content, attempts: attempt, tokensIn: d.usage?.prompt_tokens ?? 0, tokensOut: d.usage?.completion_tokens ?? 0 }
351
+ } catch (e) {
352
+ lastErr = e instanceof Error ? e.message : String(e)
353
+ } finally {
354
+ clearTimeout(timer)
355
+ }
356
+ }
357
+ throw new Error(`completion failed after retries: ${lastErr}`)
358
+ }
359
+
360
+ function extractRepairCode(reply: string): string {
361
+ const fences = [...reply.matchAll(/```(?:python|py)?\s*\n([\s\S]*?)```/gi)].map((m) => (m[1] ?? '').trim())
362
+ for (let i = fences.length - 1; i >= 0; i -= 1) {
363
+ if (/(^|\n)\s*def\s+\w+/.test(fences[i] as string)) return fences[i] as string
364
+ }
365
+ return extractCode(reply)
366
+ }
367
+
368
+ // ---------- TESTGEN (mirrors hev-structural; description + shown assert only) ----------
369
+
370
+ const testGenInstruction = (count: number, entry: string) =>
371
+ `Read the following task description and example test. Write exactly ${count} single-line assert statements that test the function \`${entry}\`, based ONLY on the described behavior. Match the EXACT output type and format the example test shows (if it expects a string, expect a string; if a tuple, a tuple). Each assert must be one physical line of the form \`assert ${entry}(...) == expected\` (or a True/False check). Do NOT implement the function. Do NOT repeat the example test verbatim if you can test other cases too. Output ONLY the assert lines inside a single \`\`\`python code block.`
372
+
373
+ async function generateTests(cfg: ClientCfg, task: MbppTask, count: number): Promise<{ tests: string[]; completion: Completion }> {
374
+ const c = await complete(cfg, [
375
+ { role: 'user', content: `${testGenInstruction(count, task.entryPoint)}\n\nTask: ${task.description}\nExample test:\n\`\`\`python\n${task.shownAssert}\n\`\`\`` },
376
+ ])
377
+ const block = extractCode(c.content)
378
+ const balanced = (s: string) => {
379
+ let d = 0
380
+ for (const ch of s) {
381
+ if (ch === '(' || ch === '[' || ch === '{') d += 1
382
+ else if (ch === ')' || ch === ']' || ch === '}') d -= 1
383
+ if (d < 0) return false
384
+ }
385
+ return d === 0
386
+ }
387
+ const tests = block
388
+ .split('\n')
389
+ .map((l) => l.trim())
390
+ .filter((l) => l.startsWith('assert ') && l.includes(task.entryPoint) && balanced(l))
391
+ .slice(0, count)
392
+ return { tests, completion: c }
393
+ }
394
+
395
+ // ---------- Phase A: the harness (sees ONLY visible information) ----------
396
+
397
+ interface HarnessOutcome {
398
+ taskId: number
399
+ samples: string[]
400
+ visible: VisibleResult[]
401
+ selectedIdx: number
402
+ repairs: Array<{ code: string; visible: VisibleResult }>
403
+ finalCode: string
404
+ repairStop: string
405
+ genTests: string[]
406
+ llmCalls: number
407
+ llmAttempts: number
408
+ tokensIn: number
409
+ tokensOut: number
410
+ }
411
+
412
+ async function runHarnessForTask(cfg: ClientCfg, task: MbppTask, k: number, maxRepairs: number, testGen: number): Promise<HarnessOutcome> {
413
+ let llmCalls = 0
414
+ let llmAttempts = 0
415
+ let tokensIn = 0
416
+ let tokensOut = 0
417
+ const track = (c: Completion) => {
418
+ llmCalls += 1
419
+ llmAttempts += c.attempts
420
+ tokensIn += c.tokensIn
421
+ tokensOut += c.tokensOut
422
+ }
423
+
424
+ let genTests: string[] = []
425
+ if (testGen > 0) {
426
+ const g = await generateTests(cfg, task, testGen)
427
+ track(g.completion)
428
+ genTests = g.tests
429
+ }
430
+
431
+ const samples: string[] = []
432
+ for (let i = 0; i < k; i += 1) {
433
+ const c = await complete(cfg, [{ role: 'user', content: basePrompt(task) }])
434
+ track(c)
435
+ samples.push(extractCode(c.content))
436
+ }
437
+ const visible: VisibleResult[] = []
438
+ for (const s of samples) visible.push(await runVisibleJudge(task, s, genTests))
439
+
440
+ let selectedIdx = 0
441
+ for (let i = 1; i < k; i += 1) {
442
+ if (visibleScore(visible[i] as VisibleResult) > visibleScore(visible[selectedIdx] as VisibleResult)) selectedIdx = i
443
+ }
444
+
445
+ const selVisible = visible[selectedIdx] as VisibleResult
446
+ let best = { code: samples[selectedIdx] as string, visible: selVisible }
447
+ const repairs: HarnessOutcome['repairs'] = []
448
+ let repairStop = 'already-passing'
449
+ if (!selVisible.pass) {
450
+ if (selVisible.attempted === 0) {
451
+ repairStop = 'no-signal'
452
+ } else {
453
+ repairStop = 'rounds-exhausted'
454
+ let current = best
455
+ for (let r = 0; r < maxRepairs; r += 1) {
456
+ const repairPrompt = [
457
+ 'Your Python function failed some of its checks.',
458
+ 'The task:',
459
+ task.description,
460
+ 'It must satisfy this example test:',
461
+ '```python',
462
+ task.shownAssert,
463
+ '```',
464
+ 'Your current attempt:',
465
+ '```python',
466
+ current.code,
467
+ '```',
468
+ 'Result of running the checks against your attempt:',
469
+ '```',
470
+ current.visible.failureOutput.trim() || '(the code crashed before the checks could run)',
471
+ '```',
472
+ 'Fix the function so the checks pass. Output the COMPLETE corrected function definition inside a single ```python code block. Do not write tests or example calls.',
473
+ ].join('\n')
474
+ const c = await complete(cfg, [{ role: 'user', content: repairPrompt }])
475
+ track(c)
476
+ const code = extractRepairCode(c.content)
477
+ const h = await runVisibleJudge(task, code, genTests)
478
+ repairs.push({ code, visible: h })
479
+ if (visibleScore(h) > visibleScore(current.visible)) current = { code, visible: h }
480
+ if (visibleScore(current.visible) > visibleScore(best.visible)) best = current
481
+ if (h.pass) {
482
+ repairStop = 'repaired-pass'
483
+ break
484
+ }
485
+ }
486
+ }
487
+ }
488
+
489
+ return { taskId: task.taskId, samples, visible, selectedIdx, repairs, finalCode: best.code, repairStop, genTests, llmCalls, llmAttempts, tokensIn, tokensOut }
490
+ }
491
+
492
+ // ---------- statistics (mirrors hev-structural) ----------
493
+
494
+ function signTestP(deltas: number[]): { pos: number; neg: number; p: number } {
495
+ const pos = deltas.filter((d) => d > 1e-9).length
496
+ const neg = deltas.filter((d) => d < -1e-9).length
497
+ const m = pos + neg
498
+ if (m === 0) return { pos, neg, p: 1 }
499
+ const logC: number[] = [0]
500
+ for (let i = 1; i <= m; i += 1) logC.push((logC[i - 1] as number) + Math.log(m - i + 1) - Math.log(i))
501
+ const pmf = (x: number) => Math.exp((logC[x] as number) - m * Math.LN2)
502
+ const extreme = Math.max(pos, neg)
503
+ let p = 0
504
+ for (let x = extreme; x <= m; x += 1) p += pmf(x)
505
+ p *= 2
506
+ if (pos === neg) p = 1
507
+ return { pos, neg, p: Math.min(1, p) }
508
+ }
509
+
510
+ // ---------- calibration mode ----------
511
+
512
+ async function calibrate(tasks: MbppTask[]): Promise<void> {
513
+ console.log(`=== CALIBRATION · MBPP reference solutions vs both judges · n=${tasks.length} ===`)
514
+ const rows = await pool(tasks, 16, async (t) => {
515
+ const hidden = await runHiddenJudge(t, t.referenceCode)
516
+ const visible = await runVisibleJudge(t, t.referenceCode, [])
517
+ return { id: t.taskId, hidden: hidden.pass, hiddenDetail: hidden.detail, vAttempted: visible.attempted, vFailed: visible.failed, visiblePass: visible.pass }
518
+ })
519
+ const hiddenPass = rows.filter((r) => r.hidden === 1)
520
+ const visibleFail = rows.filter((r) => !r.visiblePass)
521
+ console.log(` hidden judge self-check: ${hiddenPass.length}/${rows.length} reference solutions pass (must be ~100%)`)
522
+ if (hiddenPass.length < rows.length) {
523
+ for (const r of rows.filter((x) => x.hidden !== 1)) console.log(` hidden FAIL ${r.id}: ${(r.hiddenDetail ?? '').replace(/\n/g, ' | ').slice(0, 160)}`)
524
+ }
525
+ console.log(` visible-check false-fail on reference: ${visibleFail.length}/${rows.length}`)
526
+ if (visibleFail.length > 0) console.log(` visible false-fail ids: ${visibleFail.map((r) => `${r.id}(${r.vFailed}/${r.vAttempted})`).join(', ')}`)
527
+ }
528
+
529
+ // ---------- main ----------
530
+
531
+ const pct = (x: number) => `${(x * 100).toFixed(1)}%`
532
+ const pp = (x: number) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}pp`
533
+
534
+ interface GradedRow extends HarnessOutcome {
535
+ hiddenSamples: number[]
536
+ hiddenFinal: number
537
+ }
538
+
539
+ async function main(): Promise<void> {
540
+ const n = Number(process.env.N ?? 427)
541
+ const k = Number(process.env.K ?? 5)
542
+ const maxRepairs = Number(process.env.REPAIRS ?? 2)
543
+ const offset = Number(process.env.OFFSET ?? 0)
544
+ const temperature = Number(process.env.TEMPERATURE ?? 0.8)
545
+ const model = process.env.WORKER_MODEL ?? 'meta-llama/Meta-Llama-3-8B-Instruct-Lite'
546
+ const base = process.env.ROUTER_BASE ?? 'https://api.together.xyz/v1'
547
+ const solveConc = Number(process.env.CONCURRENCY ?? 6)
548
+ dockerSlots = Number(process.env.DOCKER_CONCURRENCY ?? 6)
549
+ const testGen = Number(process.env.TESTGEN ?? 0)
550
+ const out = process.env.OUT
551
+
552
+ const { tasks, droppedShort, droppedEntry } = loadMbpp(n, offset)
553
+ console.log(`loaded ${tasks.length} MBPP task(s); dropped ${droppedShort.length} (<2 asserts: ${droppedShort.join(',') || '-'}), ${droppedEntry.length} (entry unresolved: ${droppedEntry.join(',') || '-'})`)
554
+
555
+ if (process.env.CALIBRATE === '1') {
556
+ await calibrate(tasks)
557
+ return
558
+ }
559
+
560
+ const cfg: ClientCfg = { base, key: must('TANGLE_API_KEY'), model, maxTokens: Number(process.env.MAX_TOKENS ?? 2500), temperature }
561
+
562
+ console.log(`=== MBPP STRUCTURAL lever · visible=test_list[0]+gen · hidden=test_list[1:] · n=${tasks.length} k=${k} repairs<=${maxRepairs} temp=${temperature} testgen=${testGen} ===`)
563
+ console.log(` model=${model} base=${base} llm-conc=${solveConc} docker-conc=${dockerSlots} (global semaphore)`)
564
+
565
+ let done = 0
566
+ let errCount = 0
567
+ const outcomes = await pool(tasks, solveConc, async (task): Promise<HarnessOutcome | { taskId: number; error: string }> => {
568
+ try {
569
+ const o = await runHarnessForTask(cfg, task, k, maxRepairs, testGen)
570
+ done += 1
571
+ if (out) appendFileSync(`${out}.phaseA`, `${JSON.stringify({ model, temperature, k, maxRepairs, ...o })}\n`)
572
+ process.stderr.write(
573
+ ` [A ${done}/${tasks.length}] mbpp/${o.taskId}: sel=${o.selectedIdx} visible=${o.visible.map((h) => visibleScore(h).toFixed(2)).join('/')} repairs=${o.repairs.length} stop=${o.repairStop}\n`,
574
+ )
575
+ return o
576
+ } catch (e) {
577
+ errCount += 1
578
+ const error = e instanceof Error ? e.message : String(e)
579
+ if (out) appendFileSync(`${out}.phaseA`, `${JSON.stringify({ model, taskId: task.taskId, error })}\n`)
580
+ process.stderr.write(` [A ERROR] mbpp/${task.taskId}: ${error.slice(0, 160)}\n`)
581
+ if (errCount > Math.max(3, 0.15 * tasks.length)) throw new Error(`aborting: ${errCount} task errors — harness-level fault, not task noise (last: ${error})`)
582
+ return { taskId: task.taskId, error }
583
+ }
584
+ })
585
+
586
+ const okOutcomes = outcomes.filter((o): o is HarnessOutcome => !('error' in o))
587
+ const okTasks = okOutcomes.map((o) => tasks.find((t) => t.taskId === o.taskId) as MbppTask)
588
+ if (errCount > 0) console.log(` WARNING: ${errCount}/${tasks.length} task(s) errored in Phase A — excluded from stats, recorded in ${out ?? '(no OUT set)'}.phaseA`)
589
+
590
+ console.log(`\n▶ Phase B: hidden grading (${okOutcomes.length} tasks × ${k} samples + finals)`)
591
+ const graded: GradedRow[] = await pool(okOutcomes, 16, async (o, ti) => {
592
+ const task = okTasks[ti] as MbppTask
593
+ const hiddenSamples: number[] = []
594
+ for (const s of o.samples) hiddenSamples.push((await runHiddenJudge(task, s)).pass)
595
+ const finalIsSelected = o.finalCode === o.samples[o.selectedIdx]
596
+ const hiddenFinal = finalIsSelected ? (hiddenSamples[o.selectedIdx] as number) : (await runHiddenJudge(task, o.finalCode)).pass
597
+ const g: GradedRow = { ...o, hiddenSamples, hiddenFinal }
598
+ if (out) appendFileSync(out, `${JSON.stringify({ model, temperature, k, maxRepairs, ...g })}\n`)
599
+ return g
600
+ })
601
+ if (out) console.log(` raw rows appended to ${out} (phase-A rows incl. errors: ${out}.phaseA)`)
602
+
603
+ const blind1First = graded.map((g) => g.hiddenSamples[0] as number)
604
+ const blind1Mean = graded.map((g) => g.hiddenSamples.reduce((s, x) => s + x, 0) / g.hiddenSamples.length)
605
+ const selected = graded.map((g) => g.hiddenSamples[g.selectedIdx] as number)
606
+ const repaired = graded.map((g) => g.hiddenFinal)
607
+ const oracleK = graded.map((g) => (g.hiddenSamples.some((x) => x === 1) ? 1 : 0))
608
+ const covered = graded.map((g) => (g.visible.some((h) => h.attempted > 0) ? 1 : 0))
609
+ const rate = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length
610
+ const llmCallsTotal = graded.reduce((s, g) => s + g.llmCalls, 0)
611
+ const llmAttemptsTotal = graded.reduce((s, g) => s + g.llmAttempts, 0)
612
+ const tokensInTotal = graded.reduce((s, g) => s + g.tokensIn, 0)
613
+ const tokensOutTotal = graded.reduce((s, g) => s + g.tokensOut, 0)
614
+ const repairFired = graded.filter((g) => g.repairs.length > 0)
615
+
616
+ console.log(`\n${'='.repeat(78)}`)
617
+ console.log(`RESULTS · MBPP structural lever · n=${graded.length} · k=${k} · repairs<=${maxRepairs} · temp=${temperature} · ${model}`)
618
+ console.log('='.repeat(78))
619
+ console.log(` visible-check coverage ${pct(rate(covered))} of tasks (shown assert always present)`)
620
+ console.log(` blind pass@1 (mean of k) ${pct(rate(blind1Mean))} [PRIMARY baseline — ${k}-rep estimator]`)
621
+ console.log(` blind pass@1 (first sample) ${pct(rate(blind1First))} [single-rep reference]`)
622
+ console.log(` selected@1 (visible argmax) ${pct(rate(selected))}`)
623
+ console.log(` repaired@1 (full harness) ${pct(rate(repaired))}`)
624
+ console.log(` oracle pass@${k} (ceiling) ${pct(rate(oracleK))}`)
625
+ console.log(
626
+ ` compute: ${llmCallsTotal} llm calls (${llmAttemptsTotal} incl. retries) = ${(llmCallsTotal / graded.length).toFixed(2)}/task; tokens in/out ${tokensInTotal}/${tokensOutTotal} (blind@1 spends 1 call/task)`,
627
+ )
628
+ console.log(` repair fired on ${repairFired.length}/${graded.length} tasks (stop: ${['already-passing', 'no-signal', 'repaired-pass', 'rounds-exhausted'].map((s) => `${s}=${graded.filter((g) => g.repairStop === s).length}`).join(', ')})`)
629
+
630
+ const row = (label: string, baseline: number[], treatment: number[]) => {
631
+ const l = pairedLift(baseline, treatment)
632
+ const st = signTestP(baseline.map((b, i) => (treatment[i] as number) - b))
633
+ console.log(
634
+ ` ${label.padEnd(36)} ${pp(l.point).padStart(7)} CI [${pp(l.low)}, ${pp(l.high)}] sign-test p=${st.p < 0.001 ? st.p.toExponential(1) : st.p.toFixed(3)} (+${st.pos}/−${st.neg}) (pairs ${l.pairs})`,
635
+ )
636
+ return { l, st }
637
+ }
638
+
639
+ console.log(`\n PAIRED LIFTS vs blind pass@1 (mean-of-${k}) · 95% bootstrap CI (B=10000) + exact sign test:`)
640
+ const sel = row('selected@1 − blind@1 (selection)', blind1Mean, selected)
641
+ const rep = row('repaired@1 − blind@1 (full harness)', blind1Mean, repaired)
642
+ row('repaired@1 − selected@1 (repair)', selected, repaired)
643
+ row(`oracle@${k} − repaired@1 (unrealized)`, repaired, oracleK)
644
+
645
+ const coveredIdx = graded.map((_, i) => i).filter((i) => covered[i] === 1)
646
+ if (coveredIdx.length > 0 && coveredIdx.length < graded.length) {
647
+ const pick = (xs: number[]) => coveredIdx.map((i) => xs[i] as number)
648
+ console.log(`\n COVERED-ONLY subgroup (n=${coveredIdx.length}):`)
649
+ row(' selected@1 − blind@1', pick(blind1Mean), pick(selected))
650
+ row(' repaired@1 − blind@1', pick(blind1Mean), pick(repaired))
651
+ }
652
+
653
+ const verdict = (name: string, r: { l: PairedLift; st: { p: number } }) =>
654
+ `${name}: ${pp(r.l.point)} — ${r.l.low > 0 && r.st.p < 0.05 ? 'POSITIVE (CI excludes 0 AND sign-test p<0.05)' : r.l.high < 0 && r.st.p < 0.05 ? 'NEGATIVE' : 'n.s.'}`
655
+ console.log(`\n VERDICT: ${verdict('full harness', rep)}; ${verdict('selection alone', sel)}`)
656
+ }
657
+
658
+ main().catch((e) => {
659
+ reapContainers()
660
+ console.error(`mbpp-structural: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`)
661
+ process.exit(1)
662
+ })
@@ -0,0 +1,19 @@
1
+ import type { StagedPierCandidateExecution } from './pier-agent'
2
+
3
+ export function createStagedPierCandidateExecutionFixture(
4
+ executionId: string,
5
+ ): StagedPierCandidateExecution {
6
+ const directory = `/tmp/agent-bench-pier-test/${encodeURIComponent(executionId)}`
7
+ return {
8
+ executionId,
9
+ directory,
10
+ taskDirectory: `${directory}/task`,
11
+ candidateDirectory: `${directory}/candidate`,
12
+ profileDirectory: `${directory}/profile`,
13
+ planPath: `${directory}/execution-plan.json`,
14
+ receiptPath: `${directory}/materialization-receipt.json`,
15
+ agentArgs: ['--agent', 'pier_agents.tangle_candidate:TangleCandidateAgent'],
16
+ evaluatorEnv: {},
17
+ attemptArgs: ['--n-attempts', '1', '--max-retries', '0'],
18
+ }
19
+ }