@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,359 @@
1
+ # AppWorld engine driver. Two subcommands the TS adapter shells out to:
2
+ # load --split S [--limit N] [--ids a,b] -> {"tasks":[{task_id,instruction}]}
3
+ # evaluate --task-id T --split S (solution code on stdin)
4
+ # -> {"success":bool,"passes":int,"fails":int}
5
+ # evaluate runs the worker's solution in a fresh AppWorld(task_id=T) world, then
6
+ # AppWorld's OWN programmatic evaluator (world.evaluate().to_dict()) reports the
7
+ # verdict. JSON is emitted as the LAST stdout line. Fail loud: any engine error is
8
+ # printed as {"error": "..."} and exits nonzero — never a fabricated verdict.
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ import time
16
+
17
+
18
+ def fail(msg: str) -> None:
19
+ print(json.dumps({"error": msg}))
20
+ sys.exit(1)
21
+
22
+
23
+ _CODE_RE = re.compile(r"```(?:python|py)?\s*\n(.*?)```", re.DOTALL)
24
+
25
+
26
+ def _extract_code(text: str) -> str:
27
+ blocks = _CODE_RE.findall(text or "")
28
+ return (blocks[-1] if blocks else "").strip()
29
+
30
+
31
+ def _router_chat(base: str, key: str, model: str, messages: list, timeout: float = 180.0):
32
+ """One router chat-completion with retry on transient/429/5xx. Returns
33
+ (content, input_tokens, output_tokens). Raises on exhausted retries."""
34
+ import httpx
35
+
36
+ url = base.rstrip("/") + "/chat/completions"
37
+ last = None
38
+ for attempt in range(4):
39
+ try:
40
+ r = httpx.post(
41
+ url,
42
+ headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
43
+ json={"model": model, "messages": messages},
44
+ timeout=timeout,
45
+ )
46
+ if r.status_code in (429, 500, 502, 503, 504):
47
+ last = f"{r.status_code}: {r.text[:160]}"
48
+ time.sleep(2**attempt)
49
+ continue
50
+ r.raise_for_status()
51
+ d = r.json()
52
+ content = (d["choices"][0]["message"].get("content") or "")
53
+ usage = d.get("usage") or {}
54
+ return content, int(usage.get("prompt_tokens", 0) or 0), int(usage.get("completion_tokens", 0) or 0)
55
+ except Exception as e: # noqa: BLE001
56
+ last = str(e)
57
+ if attempt < 3:
58
+ time.sleep(2**attempt)
59
+ continue
60
+ raise RuntimeError(f"router_chat failed after retries: {last}")
61
+ raise RuntimeError(f"router_chat exhausted: {last}")
62
+
63
+
64
+ def _build_system(directive: str, world) -> str:
65
+ sup = world.task.supervisor
66
+ apps = list(getattr(world.task, "allowed_apps", []) or [])
67
+ descs = getattr(world.task, "app_descriptions", "")
68
+ desc_str = json.dumps(descs) if isinstance(descs, (dict, list)) else str(descs)
69
+ return (
70
+ f"You are an AI agent completing a digital task for your supervisor "
71
+ f"{getattr(sup, 'first_name', '')} {getattr(sup, 'last_name', '')} "
72
+ f"(email {getattr(sup, 'email', '')}, phone {getattr(sup, 'phone_number', '')}) "
73
+ "by WRITING PYTHON that calls app APIs (the apis.<app>.<function>(...) surface).\n\n"
74
+ f"Available apps: {', '.join(apps)}.\n"
75
+ f"App descriptions: {desc_str[:1500]}\n\n"
76
+ "How to work, one step per turn:\n"
77
+ "- Discover APIs with apis.api_docs.show_api_descriptions(app_name='<app>') and "
78
+ "apis.api_docs.show_api_doc(app_name='<app>', api_name='<api>') BEFORE calling them.\n"
79
+ "- Get the supervisor's app passwords with apis.supervisor.show_account_passwords(), then log in "
80
+ "to each app you use to obtain its access_token.\n"
81
+ "- Write ONE short Python code block per turn. After it runs you SEE its OUTPUT (or error "
82
+ "traceback) — use that to decide the next step. Print intermediate values you need.\n"
83
+ "- Iterate: inspect -> authenticate -> act -> verify. Do not guess API names or arguments.\n"
84
+ "- When the task is fully done call apis.supervisor.complete_task(answer=<answer>) (include the "
85
+ "answer if the task asks a question, otherwise apis.supervisor.complete_task()).\n"
86
+ "- Reply with EXACTLY ONE fenced ```python block per turn and nothing else.\n\n"
87
+ f"{directive}"
88
+ )
89
+
90
+
91
+ def cmd_react(args) -> None:
92
+ """Multi-turn REPL agent: the model writes a python block, the engine executes
93
+ it in the PERSISTENT world, the output is fed back, and it iterates until it
94
+ completes the task or hits max-turns. Then AppWorld's own evaluator scores it.
95
+ Config (directive, model, router creds, max_turns) arrives as JSON on stdin so
96
+ the candidate directive can be arbitrarily long. The directive is the optimized
97
+ surface; the loop + contract are fixed."""
98
+ cfg = {}
99
+ raw = sys.stdin.read()
100
+ if raw.strip():
101
+ try:
102
+ cfg = json.loads(raw)
103
+ except Exception as e: # noqa: BLE001
104
+ fail(f"react config JSON parse failed: {e}")
105
+ directive = str(cfg.get("directive", ""))
106
+ model = str(cfg.get("model", "gpt-4o"))
107
+ max_turns = int(cfg.get("max_turns", 8))
108
+ router_base = str(cfg.get("router_base", "https://router.tangle.tools/v1"))
109
+ router_key = str(cfg.get("router_key") or os.environ.get("TANGLE_API_KEY", ""))
110
+ if not router_key:
111
+ fail("react: router_key/TANGLE_API_KEY required")
112
+
113
+ try:
114
+ from appworld import AppWorld
115
+ except Exception as e: # noqa: BLE001
116
+ fail(f"appworld import failed: {e}")
117
+
118
+ in_tok = 0
119
+ out_tok = 0
120
+ turns = 0
121
+ turns_log: list = []
122
+ try:
123
+ with AppWorld(
124
+ task_id=args.task_id,
125
+ experiment_name="bench-react",
126
+ raise_on_failure=False,
127
+ ) as world:
128
+ messages = [
129
+ {"role": "system", "content": _build_system(directive, world)},
130
+ {"role": "user", "content": f"Task: {world.task.instruction}"},
131
+ ]
132
+ for turn in range(max_turns):
133
+ turns = turn + 1
134
+ content, ui, uo = _router_chat(router_base, router_key, model, messages)
135
+ in_tok += ui
136
+ out_tok += uo
137
+ code = _extract_code(content)
138
+ messages.append({"role": "assistant", "content": content})
139
+ if not code:
140
+ messages.append({
141
+ "role": "user",
142
+ "content": "Reply with exactly one ```python block that makes progress, "
143
+ "or call apis.supervisor.complete_task().",
144
+ })
145
+ continue
146
+ output = world.execute(code)
147
+ turns_log.append({"code": code[:600], "output": str(output)[:600]})
148
+ messages.append({"role": "user", "content": "OUTPUT:\n" + str(output)[:4000]})
149
+ if world.task_completed():
150
+ break
151
+ evaluation = world.evaluate().to_dict()
152
+ except Exception as e: # noqa: BLE001
153
+ fail(f"react of {args.task_id} failed: {e}")
154
+
155
+ if "success" not in evaluation or "num_tests" not in evaluation:
156
+ fail(f"evaluation dict missing success/num_tests keys: {sorted(evaluation.keys())}")
157
+ passes = evaluation.get("passes", [])
158
+ failures = evaluation.get("failures", [])
159
+ n_pass = len(passes) if isinstance(passes, list) else int(passes or 0)
160
+ n_fail = len(failures) if isinstance(failures, list) else int(failures or 0)
161
+ print(
162
+ json.dumps(
163
+ {
164
+ "success": bool(evaluation["success"]),
165
+ "passes": n_pass,
166
+ "fails": n_fail,
167
+ "num_tests": int(evaluation["num_tests"]),
168
+ # Failed sub-test names — the evidence a trace analyst steers on.
169
+ "failure_names": [str(f)[:160] for f in failures][:8]
170
+ if isinstance(failures, list)
171
+ else [],
172
+ "turns": turns,
173
+ "input_tokens": in_tok,
174
+ "output_tokens": out_tok,
175
+ "transcript": "\n---\n".join(
176
+ f"CODE:\n{t['code']}\nOUTPUT:\n{t['output']}" for t in turns_log[-3:]
177
+ )[:1600],
178
+ }
179
+ )
180
+ )
181
+
182
+
183
+ def cmd_session(args) -> None:
184
+ """Dumb world shim: a persistent AppWorld session driven over stdin JSONL.
185
+ NO LLM calls here — the agent loop lives in the runtime (routerToolLoop);
186
+ this process only owns world state. One JSON object per line, both ways:
187
+ {"op":"execute","code":"..."} -> {"output":"...","task_completed":bool}
188
+ {"op":"evaluate"} -> the evaluate verdict JSON (+failure_names)
189
+ Emits {"ready":true,"instruction":...} on start; exits on stdin EOF."""
190
+ try:
191
+ from appworld import AppWorld
192
+ except Exception as e: # noqa: BLE001
193
+ fail(f"appworld import failed: {e}")
194
+ try:
195
+ with AppWorld(
196
+ task_id=args.task_id,
197
+ experiment_name="bench-session",
198
+ raise_on_failure=False,
199
+ ) as world:
200
+ print(json.dumps({"ready": True, "instruction": world.task.instruction}), flush=True)
201
+ for line in sys.stdin:
202
+ line = line.strip()
203
+ if not line:
204
+ continue
205
+ try:
206
+ cmd = json.loads(line)
207
+ except Exception as e: # noqa: BLE001
208
+ print(json.dumps({"error": f"bad command JSON: {e}"}), flush=True)
209
+ continue
210
+ op = cmd.get("op")
211
+ if op == "execute":
212
+ # An exception here is the AGENT's outcome (bad code), not an
213
+ # infra fault — feed it back, keep the world alive.
214
+ try:
215
+ output = str(world.execute(str(cmd.get("code", ""))))
216
+ except Exception as e: # noqa: BLE001
217
+ output = f"EXECUTION ERROR: {e}"
218
+ print(
219
+ json.dumps(
220
+ {"output": output[:4000], "task_completed": bool(world.task_completed())}
221
+ ),
222
+ flush=True,
223
+ )
224
+ elif op == "evaluate":
225
+ ev = world.evaluate().to_dict()
226
+ passes = ev.get("passes", [])
227
+ failures = ev.get("failures", [])
228
+ print(
229
+ json.dumps(
230
+ {
231
+ "success": bool(ev.get("success")),
232
+ "passes": len(passes) if isinstance(passes, list) else int(passes or 0),
233
+ "fails": len(failures) if isinstance(failures, list) else int(failures or 0),
234
+ "num_tests": int(ev.get("num_tests", 0)),
235
+ "failure_names": [str(f)[:160] for f in failures][:8]
236
+ if isinstance(failures, list)
237
+ else [],
238
+ }
239
+ ),
240
+ flush=True,
241
+ )
242
+ else:
243
+ print(json.dumps({"error": f"unknown op: {op}"}), flush=True)
244
+ except Exception as e: # noqa: BLE001
245
+ fail(f"session of {args.task_id} failed: {e}")
246
+
247
+
248
+ def cmd_load(args) -> None:
249
+ try:
250
+ from appworld import load_task_ids
251
+ except Exception as e: # noqa: BLE001
252
+ fail(f"appworld import failed: {e}")
253
+
254
+ try:
255
+ ids = list(load_task_ids(args.split))
256
+ except Exception as e: # noqa: BLE001
257
+ fail(f"load_task_ids({args.split}) failed: {e}")
258
+
259
+ if args.ids:
260
+ want = set(args.ids.split(","))
261
+ ids = [i for i in ids if i in want]
262
+ elif args.limit is not None:
263
+ ids = ids[: args.limit]
264
+
265
+ from appworld import AppWorld
266
+
267
+ tasks = []
268
+ for task_id in ids:
269
+ # Open each world read-only just to read the instruction; close immediately.
270
+ try:
271
+ with AppWorld(task_id=task_id, experiment_name="bench-load") as world:
272
+ tasks.append({"task_id": task_id, "instruction": world.task.instruction})
273
+ except Exception as e: # noqa: BLE001
274
+ fail(f"opening task {task_id} failed: {e}")
275
+ print(json.dumps({"tasks": tasks}))
276
+
277
+
278
+ def cmd_evaluate(args) -> None:
279
+ code = sys.stdin.read()
280
+ try:
281
+ from appworld import AppWorld
282
+ except Exception as e: # noqa: BLE001
283
+ fail(f"appworld import failed: {e}")
284
+
285
+ try:
286
+ with AppWorld(
287
+ task_id=args.task_id,
288
+ experiment_name="bench-eval",
289
+ # An API error should surface to the agent's output, not crash the world.
290
+ raise_on_failure=False,
291
+ ) as world:
292
+ if code.strip():
293
+ world.execute(code)
294
+ evaluation = world.evaluate().to_dict()
295
+ except Exception as e: # noqa: BLE001
296
+ fail(f"evaluate of {args.task_id} failed: {e}")
297
+
298
+ # TestTracker.to_dict() carries `success` (binary task-goal-completion),
299
+ # `num_tests` (the authoritative per-requirement total), and the
300
+ # `passes`/`failures` lists. `failures` (not `fails`) is the real key — read it
301
+ # directly and never substitute a default count.
302
+ if "success" not in evaluation or "num_tests" not in evaluation:
303
+ fail(f"evaluation dict missing success/num_tests keys: {sorted(evaluation.keys())}")
304
+ success = bool(evaluation["success"])
305
+ passes = evaluation.get("passes", [])
306
+ failures = evaluation.get("failures", [])
307
+ n_pass = len(passes) if isinstance(passes, list) else int(passes or 0)
308
+ n_fail = len(failures) if isinstance(failures, list) else int(failures or 0)
309
+ print(
310
+ json.dumps(
311
+ {
312
+ "success": success,
313
+ "passes": n_pass,
314
+ "fails": n_fail,
315
+ "num_tests": int(evaluation["num_tests"]),
316
+ # The failed sub-test names are the diagnosable evidence a trace
317
+ # analyst steers on — bounded so the JSON line stays small.
318
+ "failure_names": [str(f)[:160] for f in failures][:8]
319
+ if isinstance(failures, list)
320
+ else [],
321
+ }
322
+ )
323
+ )
324
+
325
+
326
+ def main() -> None:
327
+ ap = argparse.ArgumentParser(description="appworld engine driver")
328
+ sub = ap.add_subparsers(dest="cmd", required=True)
329
+
330
+ p_load = sub.add_parser("load")
331
+ p_load.add_argument("--split", required=True)
332
+ p_load.add_argument("--limit", type=int, default=None)
333
+ p_load.add_argument("--ids", default=None)
334
+
335
+ p_eval = sub.add_parser("evaluate")
336
+ p_eval.add_argument("--task-id", required=True)
337
+ p_eval.add_argument("--split", required=True)
338
+
339
+ p_react = sub.add_parser("react")
340
+ p_react.add_argument("--task-id", required=True)
341
+ p_react.add_argument("--split", required=True)
342
+
343
+ p_session = sub.add_parser("session")
344
+ p_session.add_argument("--task-id", required=True)
345
+ p_session.add_argument("--split", required=True)
346
+
347
+ args = ap.parse_args()
348
+ if args.cmd == "load":
349
+ cmd_load(args)
350
+ elif args.cmd == "evaluate":
351
+ cmd_evaluate(args)
352
+ elif args.cmd == "react":
353
+ cmd_react(args)
354
+ elif args.cmd == "session":
355
+ cmd_session(args)
356
+
357
+
358
+ if __name__ == "__main__":
359
+ main()
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch FreedomIntelligence/CADBench + flatten its nested `criteria` into a flat
3
+ bullet list, writing the cleaned JSONL the CADBench adapter reads via CADBENCH_PATH.
4
+ Usage: python3 scripts/cadbench_prepare.py [out.jsonl] (clones the HF dataset to /tmp)"""
5
+ import json, subprocess, sys, os
6
+ OUT = sys.argv[1] if len(sys.argv) > 1 else '/tmp/cadbench_clean.jsonl'
7
+ SRC = '/tmp/cadbench-ds'
8
+ if not os.path.exists(SRC):
9
+ subprocess.run(['git', 'clone', '--depth', '1', 'https://huggingface.co/datasets/FreedomIntelligence/CADBench', SRC], check=True)
10
+ def flatten(x, out):
11
+ if isinstance(x, dict):
12
+ for v in x.values(): flatten(v, out)
13
+ elif isinstance(x, list):
14
+ for v in x: flatten(v, out)
15
+ elif isinstance(x, str): out.append(x)
16
+ rows = [json.loads(l) for l in open(f'{SRC}/CADBench.jsonl', encoding='utf-8-sig') if l.strip()]
17
+ clean = []
18
+ for r in rows:
19
+ b = []; flatten(r['criteria'], b)
20
+ clean.append({'id': r['id'], 'name': r.get('name', ''), 'instruction': r['instruction'], 'type': r.get('type', ''), 'criteria': b})
21
+ open(OUT, 'w').write('\n'.join(json.dumps(c) for c in clean))
22
+ print(f'wrote {len(clean)} tasks -> {OUT}')
@@ -0,0 +1,48 @@
1
+ from build123d import *
2
+ import os, json
3
+ OUT = "/tmp/cgb-hard"; os.makedirs(OUT, exist_ok=True)
4
+ tasks = []
5
+ def save(pid, part, desc):
6
+ p = f"{OUT}/{pid}.step"; export_step(part, p)
7
+ tasks.append({"id": pid, "prompt": desc, "gtStep": p}); print(pid, "OK", round(part.volume,1))
8
+
9
+ # 1. L-bracket: base plate + vertical leg + 4 holes through base
10
+ base = Pos(0,0,4) * Box(60,40,8)
11
+ leg = Pos(-26,0,30) * Box(8,40,52)
12
+ lb = base + leg
13
+ for (x,y) in [(-10,-10),(-10,10),(20,-10),(20,10)]:
14
+ lb -= Pos(x,y,4) * Cylinder(3,12)
15
+ save("l-bracket", lb, "An L-shaped steel mounting bracket. A base plate 60 (X) by 40 (Y) by 8 (Z) units sits flat with its bottom at z=0, centered on X and Y. A vertical leg 8 (X) by 40 (Y) by 52 (Z) units rises from the base's -X edge, forming a right angle (the whole part is ~60 wide, 40 deep, 60 tall). Four vertical through-holes of radius 3 pass through the base plate at X,Y offsets (-10,-10), (-10,10), (20,-10), (20,10) from center.")
16
+
17
+ # 2. Washer: flat annulus
18
+ w = Cylinder(20,4) - Cylinder(10,4)
19
+ save("washer", w, "A flat circular washer: an annular disk of outer radius 20, inner radius 10, and thickness 4 units, centered at the origin.")
20
+
21
+ # 3. Flanged pipe: pipe + flange disk at base + 4 bolt holes in flange
22
+ pipe = Cylinder(12,50, align=(Align.CENTER,Align.CENTER,Align.MIN)) - Cylinder(8,50, align=(Align.CENTER,Align.CENTER,Align.MIN))
23
+ flange = Cylinder(24,6, align=(Align.CENTER,Align.CENTER,Align.MIN)) - Cylinder(8,6, align=(Align.CENTER,Align.CENTER,Align.MIN))
24
+ fp = pipe + flange
25
+ import math
26
+ for i in range(4):
27
+ a = math.radians(45 + i*90); fp -= Pos(18*math.cos(a),18*math.sin(a),3) * Cylinder(2.5,8)
28
+ save("flanged-pipe", fp, "A flanged pipe. A hollow cylindrical pipe of outer radius 12, inner radius 8 (bore), height 50, standing with its base at z=0. At the base is a circular flange disk of radius 24 and thickness 6, sharing the same central bore (radius 8). Four bolt holes of radius 2.5 pass vertically through the flange on a circle of radius 18 from the axis, at 45, 135, 225, 315 degrees.")
29
+
30
+ # 4. Stepped shaft: 3 stacked cylinders, decreasing radius
31
+ s = Cylinder(15,20, align=(Align.CENTER,Align.CENTER,Align.MIN))
32
+ s += Pos(0,0,20) * Cylinder(10,20, align=(Align.CENTER,Align.CENTER,Align.MIN))
33
+ s += Pos(0,0,40) * Cylinder(6,20, align=(Align.CENTER,Align.CENTER,Align.MIN))
34
+ save("stepped-shaft", s, "A stepped cylindrical shaft, coaxial along Z, base at z=0: a bottom section radius 15 height 20, a middle section radius 10 height 20, and a top section radius 6 height 20 (total height 60).")
35
+
36
+ # 5. Mounting plate: plate + 4 corner holes + center hole
37
+ mp = Box(80,60,6)
38
+ for (x,y) in [(-32,-22),(-32,22),(32,-22),(32,22)]:
39
+ mp -= Pos(x,y,0) * Cylinder(3,10)
40
+ mp -= Cylinder(8,10)
41
+ save("mounting-plate", mp, "A rectangular mounting plate 80 (X) by 60 (Y) by 6 (Z) units, centered at the origin. A central through-hole of radius 8, plus four through-holes of radius 3 at the corners, offset (+/-32, +/-22) from center.")
42
+
43
+ # 6. Hex standoff: hexagonal prism with axial hole
44
+ hexp = extrude(RegularPolygon(radius=10, side_count=6), 30) - Cylinder(5,30, align=(Align.CENTER,Align.CENTER,Align.MIN))
45
+ save("hex-standoff", hexp, "A hexagonal standoff: a regular hexagonal prism (circumradius 10, i.e. ~17.3 across flats... actually 20 across corners) extruded 30 units tall from z=0, with a coaxial cylindrical through-hole of radius 5 along its full height.")
46
+
47
+ json.dump(tasks, open(f"{OUT}/tasks.json","w"), indent=2)
48
+ print("WROTE", len(tasks), "tasks")
@@ -0,0 +1,73 @@
1
+ """Deployable-checker bridge for CL-Bench (Continual) Codebase Adaptation.
2
+
3
+ CL-Bench's `codebase_adaptation` is the ONE domain whose scorer is a deployable
4
+ checker (not an oracle): it applies the instance's provided `test_patch` and runs
5
+ the project's pytest suite inside the instance's Docker image, keying off the exit
6
+ code — exactly the SWE-bench / commit0 regime. This bridge exposes that scorer as a
7
+ standalone (instance_id, patch) -> {success,status} call so our TypeScript gate can
8
+ rank K candidate patches by a verifier the agent could legitimately run itself.
9
+
10
+ Run it with CL-Bench's OWN venv + repo root on the path (its `src.tasks...` package):
11
+
12
+ <clbench>/.venv/bin/python clbench_codebase_judge.py \
13
+ --dataset <clbench>/data/codebase_adaptation/final-dataset.jsonl \
14
+ --instance-id jazzband__tablib-534 --patch-file /tmp/candidate.patch
15
+ # invoked with cwd=<clbench> so `import src.tasks...` resolves
16
+
17
+ Prints one JSON line: {"instance_id","success","status","error"}. Fail loud — a
18
+ Docker/import failure exits non-zero with the message on stderr, never a silent 0.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import sys
26
+
27
+ from src.tasks.codebase_adaptation.evaluator import evaluate_submission
28
+
29
+
30
+ def load_instance(dataset_path: str, instance_id: str) -> dict:
31
+ with open(dataset_path, encoding="utf-8") as f:
32
+ for line in f:
33
+ line = line.strip()
34
+ if not line:
35
+ continue
36
+ row = json.loads(line)
37
+ if row.get("instance_id") == instance_id:
38
+ return row
39
+ raise SystemExit(f"instance_id not found in {dataset_path}: {instance_id}")
40
+
41
+
42
+ def main() -> None:
43
+ ap = argparse.ArgumentParser(description="CL-Bench codebase_adaptation deployable judge")
44
+ ap.add_argument("--dataset", required=True, help="path to final-dataset.jsonl")
45
+ ap.add_argument("--instance-id", required=True)
46
+ ap.add_argument("--patch-file", required=True, help="file holding the candidate unified git diff")
47
+ args = ap.parse_args()
48
+
49
+ instance = load_instance(args.dataset, args.instance_id)
50
+ with open(args.patch_file, encoding="utf-8") as f:
51
+ patch = f.read()
52
+
53
+ # evaluate_submission spins the instance's Docker image, applies test_patch + the
54
+ # candidate, runs pytest, and reports success on a clean exit. A genuine infra
55
+ # failure raises — let it propagate (non-zero exit) so the caller never reads a
56
+ # transport fault as a failed test.
57
+ result = evaluate_submission(patch, instance)
58
+ print(json.dumps({
59
+ "instance_id": args.instance_id,
60
+ "success": bool(result.success),
61
+ "status": result.status,
62
+ "error": (result.error or "")[:500],
63
+ }))
64
+
65
+
66
+ if __name__ == "__main__":
67
+ try:
68
+ main()
69
+ except SystemExit:
70
+ raise
71
+ except Exception as exc: # infra/import failure — fail loud, do not emit a fake verdict
72
+ print(f"clbench_codebase_judge: {type(exc).__name__}: {exc}", file=sys.stderr)
73
+ sys.exit(2)
@@ -0,0 +1,170 @@
1
+ # Commit0 judge driver: stage one repo at base_commit, apply the worker's diff
2
+ # (read from stdin) into src_dir, run the official commit0 pytest harness on a
3
+ # local Docker backend, and emit {"passed": int, "total": int} as the LAST stdout
4
+ # line. The TS adapter wraps this; scoring (passed+xfail)/total mirrors
5
+ # commit0.harness.evaluate. Fail loud: any harness/Docker error is printed as
6
+ # {"error": "..."} and a nonzero exit — never a fabricated pass count.
7
+
8
+ import argparse
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ from pathlib import Path
15
+
16
+
17
+ def fail(msg: str) -> None:
18
+ print(json.dumps({"error": msg}))
19
+ sys.exit(1)
20
+
21
+
22
+ def main() -> None:
23
+ ap = argparse.ArgumentParser(description="commit0 single-instance judge")
24
+ ap.add_argument("--dataset", required=True)
25
+ ap.add_argument("--split", required=True)
26
+ ap.add_argument("--instance", required=True) # e.g. commit-0/wcwidth
27
+ ap.add_argument("--src-dir", required=True)
28
+ ap.add_argument("--backend", default=os.environ.get("COMMIT0_BACKEND", "local"))
29
+ ap.add_argument("--timeout", type=int, default=int(os.environ.get("COMMIT0_TIMEOUT", "1800")))
30
+ args = ap.parse_args()
31
+
32
+ diff = sys.stdin.read()
33
+ if not diff.strip():
34
+ # Empty implementation: nothing to apply → 0 of the suite passes. Stage the
35
+ # repo anyway so `total` reflects the real test count (fail-closed, real).
36
+ diff = ""
37
+
38
+ try:
39
+ from datasets import load_dataset
40
+ from commit0.harness.setup import main as setup_main
41
+ from commit0.harness.run_pytest_ids import main as run_tests
42
+ from commit0.harness.get_pytest_ids import main as get_tests
43
+ from commit0.harness.constants import BASE_BRANCH, RUN_PYTEST_LOG_DIR
44
+ from commit0.harness.utils import get_hash_string
45
+ except Exception as e: # noqa: BLE001
46
+ fail(f"commit0 import failed: {e}")
47
+
48
+ repo_name = args.instance.split("/")[-1]
49
+ ds = load_dataset(args.dataset, split=args.split)
50
+ example = next((x for x in ds if x["instance_id"] == args.instance), None)
51
+ if example is None:
52
+ fail(f"instance not found in {args.dataset}: {args.instance}")
53
+
54
+ # run_pytest_ids wants the SPACE-joined test-id STRING (e.g. "tests/a.py::t1 ...")
55
+ # — NOT the dataset's test_dir — and keys its report dir on get_hash_string(that
56
+ # same string). Passing test_dir ran zero tests and wrote the report under a
57
+ # different hash, so the judge silently found nothing. get_tests reads the test-ids.
58
+ test_ids = [t for t in get_tests(repo_name, 0) if t.strip()]
59
+ test_ids_str = " ".join(test_ids)
60
+ # setup.main checks out the stubbed repo onto BASE_BRANCH ("commit0") and the
61
+ # worker's diff is committed there; run_pytest_ids resolves `branch` -> a git
62
+ # commit (NOT a checkout) to diff against base_commit, so it must be the
63
+ # branch the work actually lives on. The dataset short name is not a git ref.
64
+ branch = BASE_BRANCH
65
+
66
+ base_dir = tempfile.mkdtemp(prefix="commit0-base-")
67
+ try:
68
+ # Clone + checkout the stubbed base branch for just this repo.
69
+ setup_main(args.dataset, args.split, repo_name, base_dir)
70
+
71
+ repo_dir = Path(base_dir) / repo_name
72
+ if not repo_dir.exists():
73
+ fail(f"commit0 setup did not clone {repo_name} into {base_dir}")
74
+
75
+ # Apply the worker's unified diff into the cloned repo (src only). git apply
76
+ # tolerates a/ b/ prefixes; --reject is avoided so a bad hunk fails loud.
77
+ if diff:
78
+ patch_path = Path(base_dir) / "worker.diff"
79
+ patch_path.write_text(diff)
80
+ r = subprocess.run(
81
+ ["git", "apply", "--whitespace=nowarn", str(patch_path)],
82
+ cwd=str(repo_dir),
83
+ capture_output=True,
84
+ text=True,
85
+ )
86
+ if r.returncode != 0:
87
+ # Retry with -p0 (diffs sometimes lack the a/ b/ prefix).
88
+ r2 = subprocess.run(
89
+ ["git", "apply", "-p0", "--whitespace=nowarn", str(patch_path)],
90
+ cwd=str(repo_dir),
91
+ capture_output=True,
92
+ text=True,
93
+ )
94
+ if r2.returncode != 0:
95
+ fail(f"git apply failed: {r.stderr.strip()[:500]} / {r2.stderr.strip()[:500]}")
96
+ subprocess.run(["git", "add", "-A"], cwd=str(repo_dir), check=False)
97
+ subprocess.run(
98
+ ["git", "-c", "user.email=bench@local", "-c", "user.name=bench", "commit", "-m", "worker"],
99
+ cwd=str(repo_dir),
100
+ capture_output=True,
101
+ text=True,
102
+ )
103
+
104
+ # Run the official per-repo pytest harness on the requested backend.
105
+ try:
106
+ run_tests(
107
+ args.dataset,
108
+ args.split,
109
+ base_dir,
110
+ repo_name,
111
+ branch,
112
+ test_ids_str,
113
+ False, # coverage
114
+ args.backend,
115
+ args.timeout,
116
+ 1, # num_cpus
117
+ # rebuild_image is honored by the MODAL backend only (force_build).
118
+ # The local Docker execution context just creates a container from
119
+ # the existing wentingzhao/<repo>:v0 image — pre-pull/build it via
120
+ # bench/src/commit0-prereqs.sh or this run dies with ImageNotFound.
121
+ rebuild_image=os.environ.get("COMMIT0_REBUILD_IMAGE") == "1",
122
+ verbose=0,
123
+ )
124
+ except SystemExit:
125
+ # run_pytest_ids ends with sys.exit(pytest_exit_code): 0 = all pass, nonzero
126
+ # = some tests failed — the NORMAL graded/partial-credit case, NOT a harness
127
+ # error. report.json is already written; fall through and read it.
128
+ pass
129
+ except BaseException as e: # noqa: BLE001 — never exit silently on a real backend fault
130
+ if isinstance(e, KeyboardInterrupt):
131
+ raise
132
+ import traceback
133
+
134
+ hint = ""
135
+ if "ImageNotFound" in repr(e) or "No such image" in repr(e):
136
+ hint = f" | missing per-repo Docker image — run: bench/src/commit0-prereqs.sh {repo_name}"
137
+ fail(f"commit0 run_pytest_ids failed for {repo_name}: {e!r}{hint} | {traceback.format_exc()[-1200:]}")
138
+
139
+ hashed = get_hash_string(test_ids_str)
140
+ report_file = RUN_PYTEST_LOG_DIR / repo_name / branch / hashed / "report.json"
141
+ if not report_file.exists():
142
+ fail(f"commit0 wrote no report.json at {report_file}")
143
+
144
+ report = json.loads(report_file.read_text())
145
+ # pytest-json: new form has "created"+"tests"; old form is a flat list.
146
+ if isinstance(report, dict) and "created" in report:
147
+ calls = {x["nodeid"]: x.get("call", {}) for x in report.get("tests", []) if "call" in x}
148
+ outcomes = [c.get("outcome") for c in calls.values()]
149
+ else:
150
+ outcomes = [x["outcome"] for x in report if x.get("when") == "call"]
151
+
152
+ passed = sum(1 for o in outcomes if o in ("passed", "xfailed"))
153
+ total = len(outcomes)
154
+ # Fall back to the declared test-id count when the harness produced no call
155
+ # records (e.g. collection error) so `total` is never a phantom 0.
156
+ if total == 0:
157
+ # COUNT of test ids, not their character lengths (get_tests returns a flat
158
+ # list of node-id strings). Only fires on a collection error, where passed
159
+ # is already 0 — but a phantom inflated total would still mislead a reader.
160
+ total = len(test_ids)
161
+
162
+ print(json.dumps({"passed": passed, "total": total}))
163
+ finally:
164
+ import shutil
165
+
166
+ shutil.rmtree(base_dir, ignore_errors=True)
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()