@tangle-network/agent-bench 0.1.0 → 0.3.6

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 (317) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +39 -2
  4. package/dist/adapters.d.ts +15 -0
  5. package/dist/adapters.js +43 -0
  6. package/dist/adapters.js.map +1 -0
  7. package/dist/benchmarks/_harness.d.ts +125 -0
  8. package/dist/benchmarks/_harness.js +33 -0
  9. package/dist/benchmarks/_harness.js.map +1 -0
  10. package/dist/benchmarks/aec-bench.d.ts +27 -0
  11. package/dist/benchmarks/aec-bench.js +8 -0
  12. package/dist/benchmarks/aec-bench.js.map +1 -0
  13. package/dist/benchmarks/agentbench.d.ts +16 -0
  14. package/dist/benchmarks/agentbench.js +10 -0
  15. package/dist/benchmarks/agentbench.js.map +1 -0
  16. package/dist/benchmarks/appworld.d.ts +37 -0
  17. package/dist/benchmarks/appworld.js +14 -0
  18. package/dist/benchmarks/appworld.js.map +1 -0
  19. package/dist/benchmarks/bfcl.d.ts +18 -0
  20. package/dist/benchmarks/bfcl.js +10 -0
  21. package/dist/benchmarks/bfcl.js.map +1 -0
  22. package/dist/benchmarks/cad-design.d.ts +45 -0
  23. package/dist/benchmarks/cad-design.js +7 -0
  24. package/dist/benchmarks/cad-design.js.map +1 -0
  25. package/dist/benchmarks/cadbench.d.ts +19 -0
  26. package/dist/benchmarks/cadbench.js +8 -0
  27. package/dist/benchmarks/cadbench.js.map +1 -0
  28. package/dist/benchmarks/cadgenbench.d.ts +22 -0
  29. package/dist/benchmarks/cadgenbench.js +8 -0
  30. package/dist/benchmarks/cadgenbench.js.map +1 -0
  31. package/dist/benchmarks/commit0.d.ts +31 -0
  32. package/dist/benchmarks/commit0.js +10 -0
  33. package/dist/benchmarks/commit0.js.map +1 -0
  34. package/dist/benchmarks/crag.d.ts +14 -0
  35. package/dist/benchmarks/crag.js +9 -0
  36. package/dist/benchmarks/crag.js.map +1 -0
  37. package/dist/benchmarks/dabstep.d.ts +18 -0
  38. package/dist/benchmarks/dabstep.js +10 -0
  39. package/dist/benchmarks/dabstep.js.map +1 -0
  40. package/dist/benchmarks/enterpriseops-gym.d.ts +38 -0
  41. package/dist/benchmarks/enterpriseops-gym.js +10 -0
  42. package/dist/benchmarks/enterpriseops-gym.js.map +1 -0
  43. package/dist/benchmarks/finresearchbench.d.ts +15 -0
  44. package/dist/benchmarks/finresearchbench.js +8 -0
  45. package/dist/benchmarks/finresearchbench.js.map +1 -0
  46. package/dist/benchmarks/finsearchcomp.d.ts +49 -0
  47. package/dist/benchmarks/finsearchcomp.js +7 -0
  48. package/dist/benchmarks/finsearchcomp.js.map +1 -0
  49. package/dist/benchmarks/frames.d.ts +59 -0
  50. package/dist/benchmarks/frames.js +13 -0
  51. package/dist/benchmarks/frames.js.map +1 -0
  52. package/dist/benchmarks/hotpotqa.d.ts +48 -0
  53. package/dist/benchmarks/hotpotqa.js +15 -0
  54. package/dist/benchmarks/hotpotqa.js.map +1 -0
  55. package/dist/benchmarks/humaneval.d.ts +53 -0
  56. package/dist/benchmarks/humaneval.js +15 -0
  57. package/dist/benchmarks/humaneval.js.map +1 -0
  58. package/dist/benchmarks/mind2web.d.ts +41 -0
  59. package/dist/benchmarks/mind2web.js +9 -0
  60. package/dist/benchmarks/mind2web.js.map +1 -0
  61. package/dist/benchmarks/nomiracl.d.ts +15 -0
  62. package/dist/benchmarks/nomiracl.js +9 -0
  63. package/dist/benchmarks/nomiracl.js.map +1 -0
  64. package/dist/benchmarks/open-rag-bench.d.ts +14 -0
  65. package/dist/benchmarks/open-rag-bench.js +9 -0
  66. package/dist/benchmarks/open-rag-bench.js.map +1 -0
  67. package/dist/benchmarks/programbench.d.ts +38 -0
  68. package/dist/benchmarks/programbench.js +10 -0
  69. package/dist/benchmarks/programbench.js.map +1 -0
  70. package/dist/benchmarks/rag-shared.d.ts +42 -0
  71. package/dist/benchmarks/rag-shared.js +39 -0
  72. package/dist/benchmarks/rag-shared.js.map +1 -0
  73. package/dist/benchmarks/ragbench.d.ts +16 -0
  74. package/dist/benchmarks/ragbench.js +9 -0
  75. package/dist/benchmarks/ragbench.js.map +1 -0
  76. package/dist/benchmarks/simpleqa.d.ts +64 -0
  77. package/dist/benchmarks/simpleqa.js +11 -0
  78. package/dist/benchmarks/simpleqa.js.map +1 -0
  79. package/dist/benchmarks/swe-bench.d.ts +56 -0
  80. package/dist/benchmarks/swe-bench.js +14 -0
  81. package/dist/benchmarks/swe-bench.js.map +1 -0
  82. package/dist/benchmarks/t2-ragbench.d.ts +14 -0
  83. package/dist/benchmarks/t2-ragbench.js +9 -0
  84. package/dist/benchmarks/t2-ragbench.js.map +1 -0
  85. package/dist/benchmarks/tau-bench-shared.d.ts +26 -0
  86. package/dist/benchmarks/tau-bench-shared.js +10 -0
  87. package/dist/benchmarks/tau-bench-shared.js.map +1 -0
  88. package/dist/benchmarks/tau2-bench.d.ts +7 -0
  89. package/dist/benchmarks/tau2-bench.js +11 -0
  90. package/dist/benchmarks/tau2-bench.js.map +1 -0
  91. package/dist/benchmarks/tau3-banking.d.ts +15 -0
  92. package/dist/benchmarks/tau3-banking.js +9 -0
  93. package/dist/benchmarks/tau3-banking.js.map +1 -0
  94. package/dist/benchmarks/terminal-bench.d.ts +24 -0
  95. package/dist/benchmarks/terminal-bench.js +8 -0
  96. package/dist/benchmarks/terminal-bench.js.map +1 -0
  97. package/dist/benchmarks/toollm.d.ts +16 -0
  98. package/dist/benchmarks/toollm.js +10 -0
  99. package/dist/benchmarks/toollm.js.map +1 -0
  100. package/dist/benchmarks/trata-hedge.d.ts +32 -0
  101. package/dist/benchmarks/trata-hedge.js +7 -0
  102. package/dist/benchmarks/trata-hedge.js.map +1 -0
  103. package/dist/benchmarks/types.d.ts +107 -0
  104. package/dist/benchmarks/types.js +1 -0
  105. package/dist/benchmarks/types.js.map +1 -0
  106. package/dist/benchmarks/webarena-verified.d.ts +16 -0
  107. package/dist/benchmarks/webarena-verified.js +10 -0
  108. package/dist/benchmarks/webarena-verified.js.map +1 -0
  109. package/dist/chunk-2PVVP7GN.js +197 -0
  110. package/dist/chunk-2PVVP7GN.js.map +1 -0
  111. package/dist/chunk-2XU6OGEN.js +170 -0
  112. package/dist/chunk-2XU6OGEN.js.map +1 -0
  113. package/dist/chunk-53UPUNBZ.js +325 -0
  114. package/dist/chunk-53UPUNBZ.js.map +1 -0
  115. package/dist/chunk-5SBJCB6W.js +144 -0
  116. package/dist/chunk-5SBJCB6W.js.map +1 -0
  117. package/dist/chunk-7WSD27QQ.js +118 -0
  118. package/dist/chunk-7WSD27QQ.js.map +1 -0
  119. package/dist/chunk-C7T7WEK2.js +103 -0
  120. package/dist/chunk-C7T7WEK2.js.map +1 -0
  121. package/dist/chunk-CKUVRZ2T.js +251 -0
  122. package/dist/chunk-CKUVRZ2T.js.map +1 -0
  123. package/dist/chunk-HBSWHQNJ.js +30 -0
  124. package/dist/chunk-HBSWHQNJ.js.map +1 -0
  125. package/dist/chunk-HHXFIHXC.js +116 -0
  126. package/dist/chunk-HHXFIHXC.js.map +1 -0
  127. package/dist/chunk-IFAV6KEM.js +276 -0
  128. package/dist/chunk-IFAV6KEM.js.map +1 -0
  129. package/dist/chunk-INNOYXCP.js +387 -0
  130. package/dist/chunk-INNOYXCP.js.map +1 -0
  131. package/dist/chunk-J3KDJNX2.js +182 -0
  132. package/dist/chunk-J3KDJNX2.js.map +1 -0
  133. package/dist/chunk-JRWWGMK7.js +148 -0
  134. package/dist/chunk-JRWWGMK7.js.map +1 -0
  135. package/dist/chunk-JTHWEDEW.js +32 -0
  136. package/dist/chunk-JTHWEDEW.js.map +1 -0
  137. package/dist/chunk-KDIKRJGB.js +120 -0
  138. package/dist/chunk-KDIKRJGB.js.map +1 -0
  139. package/dist/chunk-LRRD7NAG.js +301 -0
  140. package/dist/chunk-LRRD7NAG.js.map +1 -0
  141. package/dist/chunk-ODT47UAY.js +221 -0
  142. package/dist/chunk-ODT47UAY.js.map +1 -0
  143. package/dist/chunk-PA2ZKHJC.js +230 -0
  144. package/dist/chunk-PA2ZKHJC.js.map +1 -0
  145. package/dist/chunk-PPYSEKFM.js +182 -0
  146. package/dist/chunk-PPYSEKFM.js.map +1 -0
  147. package/dist/chunk-PUIRNYI7.js +189 -0
  148. package/dist/chunk-PUIRNYI7.js.map +1 -0
  149. package/dist/chunk-R36V2VP7.js +169 -0
  150. package/dist/chunk-R36V2VP7.js.map +1 -0
  151. package/dist/chunk-R67DFVLO.js +142 -0
  152. package/dist/chunk-R67DFVLO.js.map +1 -0
  153. package/dist/chunk-SEVJPLZC.js +260 -0
  154. package/dist/chunk-SEVJPLZC.js.map +1 -0
  155. package/dist/chunk-SYDW647C.js +318 -0
  156. package/dist/chunk-SYDW647C.js.map +1 -0
  157. package/dist/chunk-TBKU5XQI.js +228 -0
  158. package/dist/chunk-TBKU5XQI.js.map +1 -0
  159. package/dist/chunk-TSWPNOYM.js +147 -0
  160. package/dist/chunk-TSWPNOYM.js.map +1 -0
  161. package/dist/chunk-UAIOHCUK.js +27 -0
  162. package/dist/chunk-UAIOHCUK.js.map +1 -0
  163. package/dist/chunk-UPAMRDX4.js +233 -0
  164. package/dist/chunk-UPAMRDX4.js.map +1 -0
  165. package/dist/chunk-VQRS7VUC.js +342 -0
  166. package/dist/chunk-VQRS7VUC.js.map +1 -0
  167. package/dist/chunk-X3BTXCJ4.js +262 -0
  168. package/dist/chunk-X3BTXCJ4.js.map +1 -0
  169. package/dist/chunk-X5YKXC6V.js +211 -0
  170. package/dist/chunk-X5YKXC6V.js.map +1 -0
  171. package/dist/chunk-Y6O2OCUO.js +130 -0
  172. package/dist/chunk-Y6O2OCUO.js.map +1 -0
  173. package/dist/chunk-YCGY7UIZ.js +208 -0
  174. package/dist/chunk-YCGY7UIZ.js.map +1 -0
  175. package/dist/chunk-Z7ML6L77.js +162 -0
  176. package/dist/chunk-Z7ML6L77.js.map +1 -0
  177. package/dist/chunk-ZEWMTR5M.js +136 -0
  178. package/dist/chunk-ZEWMTR5M.js.map +1 -0
  179. package/dist/index.d.ts +355 -0
  180. package/dist/index.js +1908 -0
  181. package/dist/index.js.map +1 -0
  182. package/fixtures/aec-bench.json +18 -0
  183. package/fixtures/agentbench-dbbench.json +22 -0
  184. package/fixtures/bfcl.json +45 -0
  185. package/fixtures/commit0.json +72 -0
  186. package/fixtures/crag.json +10 -0
  187. package/fixtures/dabstep.json +22 -0
  188. package/fixtures/enterpriseops-gym.json +103 -0
  189. package/fixtures/finresearchbench.json +21 -0
  190. package/fixtures/finsearchcomp.json +66 -0
  191. package/fixtures/frames.json +26 -0
  192. package/fixtures/hotpotqa.json +182 -0
  193. package/fixtures/nomiracl.json +26 -0
  194. package/fixtures/open-rag-bench.json +16 -0
  195. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  196. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  197. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  198. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  199. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  200. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  201. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  202. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  203. package/fixtures/programbench.json +17 -0
  204. package/fixtures/ragbench.json +21 -0
  205. package/fixtures/simpleqa.json +121 -0
  206. package/fixtures/t2-ragbench.json +13 -0
  207. package/fixtures/tau2-bench.json +16 -0
  208. package/fixtures/tau3-banking.json +16 -0
  209. package/fixtures/toollm.json +28 -0
  210. package/fixtures/webarena-verified.json +20 -0
  211. package/package.json +60 -20
  212. package/pier_agents/__init__.py +18 -0
  213. package/pier_agents/candidate_contract.py +755 -0
  214. package/pier_agents/process_boundary.py +321 -0
  215. package/pier_agents/tangle_candidate.py +907 -0
  216. package/pier_agents/workspace_boundary.py +368 -0
  217. package/scripts/appworld_driver.py +359 -0
  218. package/scripts/cadbench_prepare.py +22 -0
  219. package/scripts/cadgenbench_hard_parts.py +48 -0
  220. package/scripts/clbench_codebase_judge.py +73 -0
  221. package/scripts/commit0_judge.py +170 -0
  222. package/scripts/dabstep_judge.py +42 -0
  223. package/scripts/enterpriseops_gym_judge.py +281 -0
  224. package/scripts/programbench_judge.py +120 -0
  225. package/scripts/render-gate-chart.mjs +176 -0
  226. package/scripts/run-package-tests.mjs +56 -0
  227. package/scripts/terminate-pier-trial.mts +66 -0
  228. package/scripts/trata-hedge/README.md +56 -0
  229. package/scripts/trata-hedge/run.sh +60 -0
  230. package/scripts/trata-hedge/solve.py +83 -0
  231. package/scripts/verify-packed-consumer.mjs +235 -0
  232. package/scripts/verify-pier-agent.mts +715 -0
  233. package/scripts/verify-pier-pair.mts +74 -0
  234. package/scripts/verify-pier-recovery.mts +139 -0
  235. package/src/adapters.ts +26 -0
  236. package/src/benchmarks/_harness.test.mts +178 -0
  237. package/src/benchmarks/_harness.ts +239 -16
  238. package/src/benchmarks/agentbench.ts +163 -0
  239. package/src/benchmarks/appworld.test.mts +15 -9
  240. package/src/benchmarks/bfcl.ts +346 -0
  241. package/src/benchmarks/crag.ts +137 -0
  242. package/src/benchmarks/dabstep.test.mts +70 -0
  243. package/src/benchmarks/dabstep.ts +212 -0
  244. package/src/benchmarks/external-adapters.test.mts +150 -0
  245. package/src/benchmarks/finresearchbench.ts +269 -0
  246. package/src/benchmarks/humaneval.ts +20 -8
  247. package/src/benchmarks/nomiracl.ts +180 -0
  248. package/src/benchmarks/open-rag-bench.ts +153 -0
  249. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  250. package/src/benchmarks/rag-shared.ts +327 -0
  251. package/src/benchmarks/ragbench.ts +171 -0
  252. package/src/benchmarks/swe-bench.test.mts +61 -0
  253. package/src/benchmarks/swe-bench.ts +201 -19
  254. package/src/benchmarks/t2-ragbench.ts +166 -0
  255. package/src/benchmarks/tau-bench-shared.ts +214 -0
  256. package/src/benchmarks/tau2-bench.ts +30 -0
  257. package/src/benchmarks/tau3-banking.ts +29 -0
  258. package/src/benchmarks/terminal-bench.test.mts +33 -0
  259. package/src/benchmarks/terminal-bench.ts +23 -8
  260. package/src/benchmarks/toollm.ts +254 -0
  261. package/src/benchmarks/types.ts +42 -0
  262. package/src/benchmarks/webarena-verified.ts +200 -0
  263. package/src/commit0-prereqs.sh +0 -0
  264. package/src/coordination-mcp-container-reach.mts +181 -0
  265. package/src/decoder-live.mts +1 -1
  266. package/src/examples/README.md +103 -39
  267. package/src/examples/benchmark-matrix.mts +101 -0
  268. package/src/examples/lean-proof-gate.README.md +77 -0
  269. package/src/examples/lean-proof-gate.mts +162 -0
  270. package/src/examples/lean-verify.ts +95 -0
  271. package/src/examples/lean.Dockerfile +12 -0
  272. package/src/examples/math-demo.mts +9 -7
  273. package/src/examples/strategy-demo.mts +10 -12
  274. package/src/gate.ts +3 -2
  275. package/src/hev-eval.mts +69 -0
  276. package/src/hev-improve.mts +169 -0
  277. package/src/hev-structural.mts +688 -0
  278. package/src/index.ts +73 -0
  279. package/src/mbpp-structural.mts +662 -0
  280. package/src/pier-agent.test-fixtures.mts +19 -0
  281. package/src/pier-agent.test.mts +363 -0
  282. package/src/pier-agent.ts +657 -0
  283. package/src/pier-result-grader.mjs +30 -0
  284. package/src/pier-result-grader.test.mts +62 -0
  285. package/src/pier-result-grader.ts +108 -0
  286. package/src/pier-task-outcome.test.mts +117 -0
  287. package/src/pier-task-outcome.ts +240 -0
  288. package/src/pier-trial-controller.test.mts +412 -0
  289. package/src/pier-trial-controller.ts +858 -0
  290. package/src/pier-trial-supervisor.mjs +352 -0
  291. package/src/resolve-client.ts +25 -2
  292. package/src/run-benchmarks-cli.mts +72 -0
  293. package/src/run-benchmarks-report.ts +66 -0
  294. package/src/run-benchmarks.test.mts +231 -0
  295. package/src/run-benchmarks.ts +589 -0
  296. package/src/smoke-structural-rollout.mts +393 -0
  297. package/src/swe-bench-env.test.ts +207 -0
  298. package/src/swe-bench-env.ts +554 -0
  299. package/src/swe-jail.ts +293 -0
  300. package/src/swe-self-improve.mts +84 -0
  301. package/src/swe-structural-judge-policy.test.ts +117 -0
  302. package/src/swe-structural-judge-policy.ts +133 -0
  303. package/src/swe-structural-policy.test.ts +124 -0
  304. package/src/swe-structural-policy.ts +132 -0
  305. package/src/swe-structural-provenance.test.ts +93 -0
  306. package/src/swe-structural-provenance.ts +138 -0
  307. package/src/swe-structural.mts +1260 -0
  308. package/src/swe-temp.ts +14 -0
  309. package/src/tb-container-executor.mts +234 -0
  310. package/src/tb-container-executor.test.mts +99 -0
  311. package/src/tb-supervisor-sidecar.mts +222 -0
  312. package/src/trata-gepa.mts +1 -1
  313. package/steerers/eops-itsm-population.json +1 -0
  314. package/tb_agents/opencode_refine_agent.py +117 -0
  315. package/tb_agents/opencode_router_agent.py +406 -0
  316. package/tb_agents/opencode_supervisor_agent.py +239 -0
  317. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,589 @@
1
+ /**
2
+ * `runBenchmarks` — the bench unifier.
3
+ *
4
+ * `gate.ts` runs ONE adapter as a diverse-vs-blind research gate through the recursive atom.
5
+ * This is the other half it references: run a SUBSET of the registry's benchmarks over a MATRIX of
6
+ * agent cells (harness × model × persona), each scored by the benchmark's OWN deterministic judge,
7
+ * and return a ranked leaderboard. It is the "which harness/model/persona combination wins on which
8
+ * benchmark" question, answered over an arbitrary subset in one call.
9
+ *
10
+ * It owns no new mechanism. Each cell is one `openSandboxRun` shot (the same per-run primitive the
11
+ * SWE worker uses) driven by `resolveBenchClient` (off-box router completion OR in-box Sandbox; the
12
+ * harness rides `sandboxOverrides.backend.type`). The deliverable is the adapter's OWN parser
13
+ * (`adapter.output`), defaulting to the final answer text — so `runBenchmarks` needs no
14
+ * per-benchmark branching. Concurrency is the shared `runPool`. The number comes from
15
+ * `adapter.judge`, never a self-authored judge.
16
+ *
17
+ * Subset = the `benchmarks` and `cells` arrays plus `n`/`ids`/`split`. A benchmark whose
18
+ * `preflight()` fails (missing Docker/venv/dataset) is recorded as unavailable and skipped — the
19
+ * sweep never aborts because one bench's harness is absent.
20
+ *
21
+ * const report = await runBenchmarks({
22
+ * benchmarks: ['humaneval', 'swe-bench'],
23
+ * cells: [
24
+ * { label: 'opencode/glm-4.6', model: 'glm-4.6', harness: 'opencode' },
25
+ * { label: 'codex/gpt-5', model: 'gpt-5', harness: 'codex' },
26
+ * ],
27
+ * routerBaseUrl, routerKey, n: 20,
28
+ * })
29
+ */
30
+
31
+ import { mkdirSync, writeFileSync } from 'node:fs'
32
+ import type {
33
+ AgentProfile,
34
+ AgentRunSpec,
35
+ Deliverable,
36
+ OpenSandboxRunOptions,
37
+ } from '@tangle-network/agent-runtime/loops'
38
+ import { openSandboxRun } from '@tangle-network/agent-runtime/loops'
39
+ import type { SandboxEvent } from '@tangle-network/sandbox'
40
+ import { resolveAdapter } from './adapters'
41
+ import type { BenchmarkAdapter, BenchScore, BenchTask } from './benchmarks/types'
42
+ import { runRefineLoop } from './refine-loop'
43
+ import { resolveBenchClient } from './resolve-client'
44
+ import { runPool } from './run-pool'
45
+
46
+ /** One agent under test: a profile (prompt/tools/persona) plus the harness + model that run it. */
47
+ export interface BenchCell {
48
+ /** Leaderboard row label, e.g. `opencode/glm-4.6` or `tax-agent@v1`. */
49
+ readonly label: string
50
+ /** Model id for this cell. */
51
+ readonly model: string
52
+ /** Coding harness for the in-box path (`opencode`/`codex`/`claude-code`/`kimi-code`). Defaults to
53
+ * `profile.metadata.backendType`, then `opencode`. Ignored on the `router` transport. */
54
+ readonly harness?: string
55
+ /** Transport: `router` (off-box completion, default), `sandbox`, or a BackendType for in-box. */
56
+ readonly backend?: string
57
+ /** Web-search provider for the `router` transport (turns the leaf into a `router-tools` loop). */
58
+ readonly searchProvider?: string
59
+ /** The agent under test. Defaults to a minimal `{ name, metadata.backendType }` profile. */
60
+ readonly profile?: AgentProfile
61
+ }
62
+
63
+ /** Runs one (adapter, task, cell) shot and returns the deliverable text. The default uses
64
+ * `openSandboxRun`; tests inject a deterministic stub so the matrix runs offline. */
65
+ export type BenchShot = (input: {
66
+ readonly adapter: BenchmarkAdapter
67
+ readonly task: BenchTask
68
+ readonly cell: BenchCell
69
+ /** Prompt to hand to the worker. Defaults to `task.prompt`; looped runs pass revised prompts. */
70
+ readonly prompt?: string
71
+ /** 1-based attempt index for looped runs. */
72
+ readonly attempt?: number
73
+ readonly routerBaseUrl: string
74
+ readonly routerKey: string
75
+ readonly bridgeUrl?: string
76
+ readonly bridgeBearer?: string
77
+ readonly sandboxBaseUrl?: string
78
+ readonly timeoutMs?: number
79
+ readonly resolveClient?: typeof resolveBenchClient
80
+ }) => Promise<{ artifact: string; ok: boolean; detail?: string }>
81
+
82
+ export interface RunBenchmarksOptions {
83
+ /** Registry keys (`resolveAdapter`) — the benchmark subset to run. */
84
+ readonly benchmarks: readonly string[]
85
+ /** The agent cells to rank. */
86
+ readonly cells: readonly BenchCell[]
87
+ readonly routerBaseUrl: string
88
+ readonly routerKey: string
89
+ readonly bridgeUrl?: string
90
+ readonly bridgeBearer?: string
91
+ readonly sandboxBaseUrl?: string
92
+ /** Tasks per benchmark (the n). */
93
+ readonly n?: number
94
+ readonly ids?: string[]
95
+ readonly split?: string
96
+ /** Replicates per (benchmark × cell × task). Default 1. */
97
+ readonly reps?: number
98
+ /** Bounded concurrency across all shots. Default 4. */
99
+ readonly concurrency?: number
100
+ /** Per-shot wall-clock (ms). */
101
+ readonly timeoutMs?: number
102
+ /** Test seam: resolve the runtime transport. Defaults to `resolveBenchClient`. */
103
+ readonly resolveClient?: typeof resolveBenchClient
104
+ /** Max attempts per (benchmark × cell × task). Default 1. Attempts after the first receive
105
+ * non-answer checker feedback and the previous artifacts; the loop stops early on pass. */
106
+ readonly loopAttempts?: number
107
+ /** Self-verify each benchmark's judge against its gold artifact on the first task before spending
108
+ * model tokens; a benchmark whose judge rejects its own gold is recorded unavailable. Default true. */
109
+ readonly verifyJudge?: boolean
110
+ /** Test seam: a deterministic shot runner. Defaults to the `openSandboxRun` leaf. */
111
+ readonly runShot?: BenchShot
112
+ /** Test seam: resolve a benchmark key to an adapter. Defaults to the registry `resolveAdapter`. */
113
+ readonly resolveAdapter?: (key: string) => BenchmarkAdapter
114
+ readonly onResult?: (r: BenchCellTaskResult) => void
115
+ }
116
+
117
+ export interface BenchCellTaskResult {
118
+ readonly benchmark: string
119
+ readonly cell: string
120
+ readonly taskId: string
121
+ readonly rep: number
122
+ readonly resolved: boolean
123
+ readonly score: number
124
+ /** false = the shot threw or produced no artifact (infra/empty), excluded from the resolve
125
+ * denominator so a harness outage can't masquerade as a 0% capability result. */
126
+ readonly ok: boolean
127
+ readonly detail?: string
128
+ readonly wallMs: number
129
+ }
130
+
131
+ export interface BenchLeaderboardRow {
132
+ readonly benchmark: string
133
+ readonly cell: string
134
+ readonly n: number
135
+ readonly resolved: number
136
+ readonly errored: number
137
+ /** resolved / (n - errored). */
138
+ readonly resolveRate: number
139
+ /** Mean graded score over non-errored shots (partial credit where the judge supports it). */
140
+ readonly meanScore: number
141
+ }
142
+
143
+ export interface RunBenchmarksReport {
144
+ /** One row per (benchmark × cell), sorted by benchmark then descending resolveRate. */
145
+ readonly rows: readonly BenchLeaderboardRow[]
146
+ readonly perTask: readonly BenchCellTaskResult[]
147
+ readonly benchmarks: readonly string[]
148
+ readonly cells: readonly string[]
149
+ /** Benchmarks skipped because `preflight`/judge-self-check failed, with the reason. */
150
+ readonly unavailable: ReadonlyArray<{ readonly benchmark: string; readonly reason: string }>
151
+ }
152
+
153
+ /** Last assistant text across the common event shapes (delta accumulation, then a terminal
154
+ * `result`/`done`/`agent` snapshot). The structured-deliverable case is handled by the adapter's
155
+ * own `output.parse`; this is the research/QA fallback. */
156
+ function finalText(events: readonly SandboxEvent[]): string {
157
+ let text = ''
158
+ for (const ev of events) {
159
+ const e = ev as { type?: string; data?: Record<string, unknown> }
160
+ const data = e.data ?? {}
161
+ if (e.type === 'message.part.updated') {
162
+ const part = data.part as { type?: string; text?: string } | undefined
163
+ const partType = (data.partType as string | undefined) ?? part?.type
164
+ if (partType === 'text') {
165
+ if (typeof data.delta === 'string') text += data.delta
166
+ else if (typeof part?.text === 'string') text = part.text
167
+ }
168
+ } else if (typeof data.finalText === 'string') text = data.finalText
169
+ else if (typeof data.response === 'string') text = data.response
170
+ }
171
+ return text.trim()
172
+ }
173
+
174
+ /** The default real-agent shot: one `openSandboxRun` over the cell's harness+model, deliverable
175
+ * extracted by the adapter's parser (or final text), abortable on `timeoutMs`. */
176
+ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerBaseUrl, routerKey, bridgeUrl, bridgeBearer, sandboxBaseUrl, timeoutMs, resolveClient }) => {
177
+ const client = (resolveClient ?? resolveBenchClient)({
178
+ backend: cell.backend ?? 'router',
179
+ routerBaseUrl,
180
+ routerKey,
181
+ model: cell.model,
182
+ ...(bridgeUrl ? { bridgeUrl } : {}),
183
+ ...(bridgeBearer ? { bridgeBearer } : {}),
184
+ ...(sandboxBaseUrl ? { sandboxBaseUrl } : {}),
185
+ ...(cell.searchProvider ? { searchProvider: cell.searchProvider } : {}),
186
+ ...(timeoutMs ? { timeoutMs } : {}),
187
+ })
188
+ const harness = cell.harness ?? (cell.profile?.metadata?.backendType as string | undefined) ?? 'opencode'
189
+ const profile: AgentProfile = cell.profile ?? { name: cell.label, metadata: { backendType: harness } }
190
+ // Unique per shot: the same (adapter, task) runs concurrently across cells and reps, so the box
191
+ // name and runId must not collide.
192
+ const uniq = Math.random().toString(36).slice(2, 8)
193
+ const agentRun: AgentRunSpec<string> = {
194
+ profile,
195
+ name: cell.label,
196
+ taskToPrompt: () => '',
197
+ sandboxOverrides: {
198
+ name: `bench-${adapter.name}-${task.id}-${uniq}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
199
+ environment: 'universal',
200
+ backend: { type: harness as never, model: { provider: 'openai', model: cell.model, baseUrl: routerBaseUrl } },
201
+ },
202
+ }
203
+ const deliverable: Deliverable<string> = {
204
+ kind: 'events',
205
+ fromEvents: (events) => (adapter.output ? adapter.output.parse(events) : finalText(events)),
206
+ }
207
+ const controller = new AbortController()
208
+ const timer = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : undefined
209
+ const runOptions: OpenSandboxRunOptions = {
210
+ agentRun,
211
+ signal: controller.signal,
212
+ runId: `bench:${adapter.name}:${task.id}:${uniq}`,
213
+ scenarioId: task.id,
214
+ }
215
+ const boxSetup = adapter.boxSetup
216
+ if (boxSetup) {
217
+ runOptions.beforeStart = async ({ box, sessionId }) => {
218
+ const setup = boxSetup(task)
219
+ const sres = await box.exec(setup.command, {
220
+ timeoutMs: 300_000,
221
+ sessionId,
222
+ ...(setup.cwd ? { cwd: setup.cwd } : {}),
223
+ })
224
+ if (sres.exitCode !== 0)
225
+ throw new Error(
226
+ `boxSetup failed (exit ${sres.exitCode}): ${(sres.stderr ?? '').slice(0, 200)}`,
227
+ )
228
+ }
229
+ }
230
+ const run = await openSandboxRun(client, runOptions, deliverable)
231
+ try {
232
+ const turn = await run.start(prompt ?? task.prompt)
233
+ // Event-stream deliverable (adapter.output ?? finalText) — the FALLBACK.
234
+ let artifact = (turn.out ?? '').trim()
235
+ let boxExtractError: string | undefined
236
+ // Primary deliverable for benchmarks whose real artifact lives in the box FS
237
+ // (SWE-bench: a git diff of the agent's edits). Run the adapter's extraction
238
+ // command in the STILL-ALIVE box (valid until run.close() below) and prefer its
239
+ // stdout; the event-stream parse remains the fallback when the box yields nothing.
240
+ if (adapter.boxExtract) {
241
+ try {
242
+ const ex = adapter.boxExtract(task)
243
+ // The agent runs under a DRIVER SESSION whose workspace is a remapped
244
+ // virtual root; an exec WITHOUT that sessionId lands on the host FS and
245
+ // cannot see the agent's edits. Thread run.sessionId so the extraction
246
+ // runs in the SAME workspace the agent wrote to.
247
+ const res = await run.box.exec(ex.command, {
248
+ timeoutMs: 120_000,
249
+ sessionId: run.sessionId,
250
+ ...(ex.cwd ? { cwd: ex.cwd } : {}),
251
+ })
252
+ const boxArtifact = (res.stdout ?? '').trim()
253
+ if (res.exitCode !== 0)
254
+ boxExtractError = `exit ${res.exitCode}: ${(res.stderr ?? '').slice(0, 160)}`
255
+ else if (boxArtifact.length > 0) artifact = boxArtifact
256
+ if (process.env.BENCH_ARTIFACT_DIR) {
257
+ try {
258
+ mkdirSync(process.env.BENCH_ARTIFACT_DIR, { recursive: true })
259
+ const safe = `${adapter.name}_${task.id}_${uniq}`.replace(/[^a-zA-Z0-9_.-]/g, '_')
260
+ const map = await run.box
261
+ .exec(
262
+ 'echo "PWD:"; pwd; echo "LS:"; ls -la; echo "GITROOTS:"; find / -maxdepth 5 -type d -name .git 2>/dev/null; echo "SETTINGS:"; find / -maxdepth 8 -name global_settings.py -path "*conf*" 2>/dev/null',
263
+ { timeoutMs: 60_000, sessionId: run.sessionId },
264
+ )
265
+ .catch((e: unknown) => ({ exitCode: -1, stdout: '', stderr: String(e) }))
266
+ writeFileSync(
267
+ `${process.env.BENCH_ARTIFACT_DIR}/${safe}.exec.json`,
268
+ JSON.stringify(
269
+ { sessionId: run.sessionId, extract: res, map: { exitCode: map.exitCode, stdout: map.stdout, stderr: map.stderr } },
270
+ null,
271
+ 2,
272
+ ),
273
+ )
274
+ } catch {
275
+ // debug-only
276
+ }
277
+ }
278
+ } catch (err) {
279
+ boxExtractError = err instanceof Error ? err.message.slice(0, 160) : String(err)
280
+ }
281
+ }
282
+ const detail =
283
+ turn.readError !== undefined
284
+ ? `read: ${turn.readError.slice(0, 160)}`
285
+ : boxExtractError !== undefined
286
+ ? `boxExtract: ${boxExtractError}`
287
+ : undefined
288
+ // Debug affordance: dump the judged artifact (the exact model_patch the judge
289
+ // will score) so a scoring failure can be diagnosed off the real bytes without
290
+ // re-running the agent. Off by default; set BENCH_ARTIFACT_DIR to enable.
291
+ if (process.env.BENCH_ARTIFACT_DIR) {
292
+ try {
293
+ mkdirSync(process.env.BENCH_ARTIFACT_DIR, { recursive: true })
294
+ const safe = `${adapter.name}_${task.id}_${uniq}`.replace(/[^a-zA-Z0-9_.-]/g, '_')
295
+ writeFileSync(`${process.env.BENCH_ARTIFACT_DIR}/${safe}.patch`, artifact)
296
+ } catch {
297
+ // debug-only; never fail the shot on a dump error
298
+ }
299
+ }
300
+ return {
301
+ artifact,
302
+ ok: artifact.length > 0,
303
+ ...(detail ? { detail } : {}),
304
+ }
305
+ } finally {
306
+ if (timer) clearTimeout(timer)
307
+ await run.close()
308
+ }
309
+ }
310
+
311
+ function parseMaybeJson(value: string): unknown {
312
+ try {
313
+ return JSON.parse(value) as unknown
314
+ } catch {
315
+ return value
316
+ }
317
+ }
318
+
319
+ function redactJudgeLeak(value: unknown): unknown {
320
+ if (Array.isArray(value)) return value.map(redactJudgeLeak)
321
+ if (!value || typeof value !== 'object') return value
322
+ const out: Record<string, unknown> = {}
323
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
324
+ const normalizedKey = key.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase()
325
+ if (/(^|_)(gold|expected|reference|solution|answer)(_|$)/.test(normalizedKey)) continue
326
+ out[key] = redactJudgeLeak(child)
327
+ }
328
+ return out
329
+ }
330
+
331
+ function safeFeedback(score: BenchScore): Record<string, unknown> {
332
+ return {
333
+ resolved: score.resolved,
334
+ score: score.score,
335
+ ...(score.detail ? { detail: redactJudgeLeak(parseMaybeJson(score.detail)) } : {}),
336
+ }
337
+ }
338
+
339
+ function truncate(value: string, max = 4_000): string {
340
+ return value.length <= max ? value : `${value.slice(0, max)}\n...[truncated ${value.length - max} chars]`
341
+ }
342
+
343
+ function retryPrompt(task: BenchTask, history: ReadonlyArray<{ round: number; artifact: string }>, scores: ReadonlyMap<number, BenchScore>): string {
344
+ const attempts = history
345
+ .map((h) => {
346
+ const score = scores.get(h.round)
347
+ return [
348
+ `Attempt ${h.round}:`,
349
+ 'Artifact:',
350
+ truncate(h.artifact),
351
+ score ? `Checker feedback: ${JSON.stringify(safeFeedback(score))}` : undefined,
352
+ ]
353
+ .filter(Boolean)
354
+ .join('\n')
355
+ })
356
+ .join('\n\n')
357
+ return [
358
+ 'Retry the benchmark task. The previous artifact did not pass the checker.',
359
+ 'Use only the original task statement and supplied context. Do not invent facts.',
360
+ 'Return the corrected artifact in exactly the format requested by the original task.',
361
+ '',
362
+ 'Original task:',
363
+ task.prompt,
364
+ '',
365
+ 'Previous attempts and safe checker feedback:',
366
+ attempts,
367
+ ].join('\n')
368
+ }
369
+
370
+ async function loopedShot(
371
+ input: Parameters<BenchShot>[0],
372
+ shot: BenchShot,
373
+ attempts: number,
374
+ ): Promise<{ artifact: string; ok: boolean; detail?: string }> {
375
+ const scores = new Map<number, BenchScore>()
376
+ const result = await runRefineLoop<string>({
377
+ rounds: attempts,
378
+ prompt: (round, history) => (round === 1 ? input.task.prompt : retryPrompt(input.task, history, scores)),
379
+ runShot: async (prompt, round) => {
380
+ const out = await shot({ ...input, prompt, attempt: round })
381
+ return { artifact: out.artifact, note: out.detail }
382
+ },
383
+ judge: async (artifact, round) => {
384
+ const score = await input.adapter.judge(input.task, artifact)
385
+ scores.set(round, score)
386
+ return { valid: score.resolved, score: score.score }
387
+ },
388
+ })
389
+
390
+ const best = result.rounds.reduce((winner, candidate) => {
391
+ const a = scores.get(winner.round)
392
+ const b = scores.get(candidate.round)
393
+ if (!a) return candidate
394
+ if (!b) return winner
395
+ if (b.resolved && !a.resolved) return candidate
396
+ if (b.resolved === a.resolved && b.score > a.score) return candidate
397
+ return winner
398
+ }, result.rounds[0]!)
399
+ const bestScore = scores.get(best.round)
400
+ return {
401
+ artifact: best.artifact,
402
+ ok: best.artifact.trim().length > 0,
403
+ detail: JSON.stringify({
404
+ mode: 'refine-loop',
405
+ attempts: result.rounds.length,
406
+ selectedAttempt: best.round,
407
+ resolvedDuringLoop: result.resolved,
408
+ selectedScore: bestScore?.score ?? null,
409
+ rounds: result.rounds.map((round) => ({
410
+ attempt: round.round,
411
+ score: scores.get(round.round)?.score ?? null,
412
+ resolved: scores.get(round.round)?.resolved ?? null,
413
+ note: round.note ?? null,
414
+ })),
415
+ }),
416
+ }
417
+ }
418
+
419
+ function combineDetails(runDetail: string | undefined, scoreDetail: string | undefined): string | undefined {
420
+ if (runDetail && scoreDetail) {
421
+ return JSON.stringify({ run: parseMaybeJson(runDetail), score: parseMaybeJson(scoreDetail) })
422
+ }
423
+ return runDetail ?? scoreDetail
424
+ }
425
+
426
+ interface Job {
427
+ readonly benchmark: string
428
+ readonly adapter: BenchmarkAdapter
429
+ readonly cell: BenchCell
430
+ readonly task: BenchTask
431
+ readonly rep: number
432
+ }
433
+
434
+ /** Resolve + preflight + (optionally) self-verify each benchmark once; load its tasks. A benchmark
435
+ * whose harness is absent or whose judge rejects its own gold is recorded unavailable, not run. */
436
+ async function prepareBenchmarks(
437
+ benchmarks: readonly string[],
438
+ resolve: (key: string) => BenchmarkAdapter,
439
+ opts: Pick<RunBenchmarksOptions, 'n' | 'ids' | 'split' | 'verifyJudge'>,
440
+ ): Promise<{ ready: Array<{ benchmark: string; adapter: BenchmarkAdapter; tasks: BenchTask[] }>; unavailable: Array<{ benchmark: string; reason: string }> }> {
441
+ const ready: Array<{ benchmark: string; adapter: BenchmarkAdapter; tasks: BenchTask[] }> = []
442
+ const unavailable: Array<{ benchmark: string; reason: string }> = []
443
+ for (const benchmark of benchmarks) {
444
+ const adapter = resolve(benchmark) // throws on an unknown key — fail loud on a typo
445
+ try {
446
+ await adapter.preflight()
447
+ const tasks = await adapter.loadTasks({
448
+ ...(opts.n !== undefined ? { limit: opts.n } : {}),
449
+ ...(opts.ids ? { ids: opts.ids } : {}),
450
+ ...(opts.split ? { split: opts.split } : {}),
451
+ })
452
+ if (tasks.length === 0) {
453
+ unavailable.push({ benchmark, reason: 'loadTasks returned no tasks' })
454
+ continue
455
+ }
456
+ if (opts.verifyJudge !== false) {
457
+ const gold = await adapter.goldArtifact(tasks[0]!)
458
+ if (gold !== undefined) {
459
+ const verdict = await adapter.judge(tasks[0]!, gold)
460
+ if (!verdict.resolved) {
461
+ unavailable.push({ benchmark, reason: `judge rejected its own gold on ${tasks[0]!.id} — judge is miscalibrated` })
462
+ continue
463
+ }
464
+ }
465
+ }
466
+ ready.push({ benchmark, adapter, tasks })
467
+ } catch (err) {
468
+ unavailable.push({ benchmark, reason: err instanceof Error ? err.message : String(err) })
469
+ }
470
+ }
471
+ return { ready, unavailable }
472
+ }
473
+
474
+ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenchmarksReport> {
475
+ if (opts.benchmarks.length === 0) throw new Error('runBenchmarks: no benchmarks selected')
476
+ if (opts.cells.length === 0) throw new Error('runBenchmarks: no cells to run')
477
+ const reps = Math.max(1, opts.reps ?? 1)
478
+ const loopAttempts = Math.max(1, opts.loopAttempts ?? 1)
479
+ const shot = opts.runShot ?? openSandboxShot
480
+
481
+ const { ready, unavailable } = await prepareBenchmarks(opts.benchmarks, opts.resolveAdapter ?? resolveAdapter, opts)
482
+
483
+ const jobs: Job[] = []
484
+ for (const { benchmark, adapter, tasks } of ready)
485
+ for (const cell of opts.cells) for (const task of tasks) for (let rep = 0; rep < reps; rep += 1) jobs.push({ benchmark, adapter, cell, task, rep })
486
+
487
+ const perTask: BenchCellTaskResult[] = []
488
+ await runPool(jobs, Math.max(1, opts.concurrency ?? 4), async (job, index) => {
489
+ const startedAt = Date.now()
490
+ let result: BenchCellTaskResult
491
+ try {
492
+ const shotInput = {
493
+ adapter: job.adapter,
494
+ task: job.task,
495
+ cell: job.cell,
496
+ routerBaseUrl: opts.routerBaseUrl,
497
+ routerKey: opts.routerKey,
498
+ ...(opts.bridgeUrl ? { bridgeUrl: opts.bridgeUrl } : {}),
499
+ ...(opts.bridgeBearer ? { bridgeBearer: opts.bridgeBearer } : {}),
500
+ ...(opts.sandboxBaseUrl ? { sandboxBaseUrl: opts.sandboxBaseUrl } : {}),
501
+ ...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
502
+ ...(opts.resolveClient ? { resolveClient: opts.resolveClient } : {}),
503
+ }
504
+ const out = loopAttempts > 1 ? await loopedShot(shotInput, shot, loopAttempts) : await shot(shotInput)
505
+ const score: BenchScore = await job.adapter.judge(job.task, out.artifact)
506
+ result = {
507
+ benchmark: job.benchmark,
508
+ cell: job.cell.label,
509
+ taskId: job.task.id,
510
+ rep: job.rep,
511
+ resolved: out.ok && score.resolved,
512
+ score: out.ok ? score.score : 0,
513
+ ok: out.ok,
514
+ ...(out.detail ?? score.detail ? { detail: combineDetails(out.detail, score.detail) } : {}),
515
+ wallMs: Date.now() - startedAt,
516
+ }
517
+ } catch (err) {
518
+ // A thrown shot/judge is infra error for THIS cell-task: ok=false excludes it from the
519
+ // resolve denominator (never a silent 0% that hides a harness outage).
520
+ result = {
521
+ benchmark: job.benchmark,
522
+ cell: job.cell.label,
523
+ taskId: job.task.id,
524
+ rep: job.rep,
525
+ resolved: false,
526
+ score: 0,
527
+ ok: false,
528
+ detail: err instanceof Error ? err.message.slice(0, 200) : String(err),
529
+ wallMs: Date.now() - startedAt,
530
+ }
531
+ }
532
+ void index
533
+ perTask.push(result)
534
+ opts.onResult?.(result)
535
+ return result
536
+ })
537
+
538
+ const rows = aggregate(perTask)
539
+ return {
540
+ rows,
541
+ perTask,
542
+ benchmarks: ready.map((r) => r.benchmark),
543
+ cells: opts.cells.map((c) => c.label),
544
+ unavailable,
545
+ }
546
+ }
547
+
548
+ function aggregate(perTask: readonly BenchCellTaskResult[]): BenchLeaderboardRow[] {
549
+ const byKey = new Map<string, { benchmark: string; cell: string; n: number; resolved: number; errored: number; scoreSum: number }>()
550
+ for (const r of perTask) {
551
+ const key = `${r.benchmark}\u0000${r.cell}`
552
+ const e = byKey.get(key) ?? { benchmark: r.benchmark, cell: r.cell, n: 0, resolved: 0, errored: 0, scoreSum: 0 }
553
+ e.n += 1
554
+ if (!r.ok) e.errored += 1
555
+ else {
556
+ if (r.resolved) e.resolved += 1
557
+ e.scoreSum += r.score
558
+ }
559
+ byKey.set(key, e)
560
+ }
561
+ const rows: BenchLeaderboardRow[] = [...byKey.values()].map((e) => {
562
+ const denom = Math.max(1, e.n - e.errored)
563
+ return {
564
+ benchmark: e.benchmark,
565
+ cell: e.cell,
566
+ n: e.n,
567
+ resolved: e.resolved,
568
+ errored: e.errored,
569
+ resolveRate: e.resolved / denom,
570
+ meanScore: e.scoreSum / denom,
571
+ }
572
+ })
573
+ rows.sort((a, b) => (a.benchmark === b.benchmark ? b.resolveRate - a.resolveRate : a.benchmark < b.benchmark ? -1 : 1))
574
+ return rows
575
+ }
576
+
577
+ /** Render the leaderboard as a fixed-width table for a CLI/log. */
578
+ export function printBenchmarksReport(report: RunBenchmarksReport): string {
579
+ const lines: string[] = []
580
+ const w = Math.max(8, ...report.rows.map((r) => r.cell.length))
581
+ const b = Math.max(9, ...report.rows.map((r) => r.benchmark.length))
582
+ lines.push(`${'benchmark'.padEnd(b)} ${'cell'.padEnd(w)} resolve mean n err`)
583
+ for (const r of report.rows)
584
+ lines.push(
585
+ `${r.benchmark.padEnd(b)} ${r.cell.padEnd(w)} ${(r.resolveRate * 100).toFixed(1).padStart(6)}% ${r.meanScore.toFixed(3).padStart(5)} ${String(r.n).padStart(3)} ${String(r.errored).padStart(3)}`,
586
+ )
587
+ for (const u of report.unavailable) lines.push(`(skipped ${u.benchmark}: ${u.reason})`)
588
+ return lines.join('\n')
589
+ }