@tangle-network/agent-bench 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * BlenderLLM / CADBench worker. The deliverable for a CADBench task is a Blender
3
+ * `bpy` Python script that builds the described 3D model. We author it via the
4
+ * router, execute it headless in Blender (Cycles CPU, no GPU), auto-frame the
5
+ * produced geometry, and render N standardized views — the images the CADBench
6
+ * criteria judge scores. The authoring directive is the GEPA-optimizable surface.
7
+ *
8
+ * Requires `blender` + `xvfb-run` on PATH (apt blender 4.x). No GPU: Cycles CPU
9
+ * with denoising off (the apt build ships without OpenImageDenoise).
10
+ */
11
+
12
+ import { execFile } from 'node:child_process'
13
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
14
+ import { tmpdir } from 'node:os'
15
+ import { join } from 'node:path'
16
+ import { promisify } from 'node:util'
17
+ import type { Span } from '@tangle-network/agent-eval'
18
+ import type { BenchTask } from './benchmarks/types'
19
+ import { DEFAULT_BLENDER_DIRECTIVE } from './directives'
20
+ import { runRefineLoop } from './refine-loop'
21
+ import { routerChatWithUsage } from '@tangle-network/agent-runtime/loops'
22
+
23
+ export { DEFAULT_BLENDER_DIRECTIVE } from './directives'
24
+
25
+ const execFileAsync = promisify(execFile)
26
+
27
+ async function runLocal(cmd: string, args: string[], cwd: string, timeoutMs = 180_000): Promise<{ code: number; stdout: string; stderr: string }> {
28
+ try {
29
+ const { stdout, stderr } = await execFileAsync(cmd, args, { cwd, maxBuffer: 1 << 26, timeout: timeoutMs })
30
+ return { code: 0, stdout, stderr }
31
+ } catch (err) {
32
+ const e = err as { code?: number; stdout?: string; stderr?: string; message?: string }
33
+ return { code: typeof e.code === 'number' ? e.code : 1, stdout: e.stdout ?? '', stderr: e.stderr ?? e.message ?? String(err) }
34
+ }
35
+ }
36
+
37
+
38
+ /** Strip markdown fences so we keep just the Python. */
39
+ function extractPy(text: string): string {
40
+ const fence = /```(?:python|py)?\s*\n([\s\S]*?)```/i.exec(text)
41
+ return (fence ? fence[1] : text).trim()
42
+ }
43
+
44
+ /**
45
+ * The standardized Blender runner (written to a temp file per run). It clears the
46
+ * scene, executes the agent's bpy script, auto-frames the produced meshes, sets
47
+ * up neutral lighting, and renders N azimuth views with Cycles CPU.
48
+ */
49
+ const RUNNER_PY = `
50
+ import bpy, sys, math, mathutils, traceback, os
51
+ agent_script, outdir, nviews = sys.argv[-3], sys.argv[-2], int(sys.argv[-1])
52
+ bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()
53
+ ok=True
54
+ try:
55
+ g={'bpy':bpy,'math':math,'mathutils':mathutils,'__name__':'__main__'}
56
+ exec(compile(open(agent_script).read(), agent_script, 'exec'), g)
57
+ except Exception as e:
58
+ traceback.print_exc(); print('AGENT_SCRIPT_ERROR:'+repr(e)); ok=False
59
+ meshes=[o for o in bpy.context.scene.objects if o.type=='MESH']
60
+ if not meshes:
61
+ print('NO_MESH'); sys.exit(0 if ok else 3)
62
+ mn=[1e18]*3; mx=[-1e18]*3
63
+ for o in meshes:
64
+ for c in o.bound_box:
65
+ w=o.matrix_world @ mathutils.Vector(c)
66
+ for i in range(3): mn[i]=min(mn[i],w[i]); mx[i]=max(mx[i],w[i])
67
+ center=mathutils.Vector(((mn[0]+mx[0])/2,(mn[1]+mx[1])/2,(mn[2]+mx[2])/2))
68
+ size=max(mx[i]-mn[i] for i in range(3)) or 1.0
69
+ # standardize: drop any agent-added cameras/lights
70
+ for o in list(bpy.context.scene.objects):
71
+ if o.type in ('CAMERA','LIGHT'): bpy.data.objects.remove(o, do_unlink=True)
72
+ w=bpy.context.scene.world or bpy.data.worlds.new('W'); bpy.context.scene.world=w
73
+ w.use_nodes=True
74
+ try: w.node_tree.nodes['Background'].inputs[1].default_value=0.6
75
+ except Exception: pass
76
+ bpy.ops.object.light_add(type='SUN'); sun=bpy.context.object; sun.data.energy=4.0; sun.rotation_euler=mathutils.Euler((0.6,0.2,0.4))
77
+ bpy.ops.object.camera_add(); cam=bpy.context.object; bpy.context.scene.camera=cam
78
+ sc=bpy.context.scene
79
+ sc.render.engine='CYCLES'; sc.cycles.samples=20; sc.cycles.device='CPU'; sc.cycles.use_denoising=False
80
+ sc.render.resolution_x=640; sc.render.resolution_y=640; sc.render.film_transparent=False
81
+ dist=size*2.4
82
+ el=math.radians(58)
83
+ for v in range(nviews):
84
+ az=math.radians(40 + v*360.0/nviews)
85
+ cam.location=center+mathutils.Vector((math.cos(az)*math.sin(el), math.sin(az)*math.sin(el), math.cos(el)))*dist
86
+ d=(center-cam.location); cam.rotation_euler=d.to_track_quat('-Z','Y').to_euler()
87
+ sc.render.filepath=os.path.join(outdir, 'view_%d.png'%v); bpy.ops.render.render(write_still=True)
88
+ print('RENDER_DONE')
89
+ `.trim()
90
+
91
+ /** Execute a bpy script headless + render N standardized views — no authoring.
92
+ * Used by the CADBench judge to render an artifact before vision-scoring it. */
93
+ export async function renderBpy(script: string, opts: { views?: number } = {}): Promise<{ built: boolean; renders: string[]; error?: string }> {
94
+ const views = Math.max(1, opts.views ?? 4)
95
+ const dir = await mkdtemp(join(tmpdir(), 'blender-judge-'))
96
+ const runnerPath = join(dir, 'runner.py')
97
+ const scriptPath = join(dir, 'model.py')
98
+ try {
99
+ await writeFile(runnerPath, RUNNER_PY)
100
+ await writeFile(scriptPath, script)
101
+ const run = await runLocal('xvfb-run', ['-a', 'blender', '--background', '--python', runnerPath, '--', scriptPath, dir, String(views)], dir)
102
+ const out = `${run.stdout}\n${run.stderr}`
103
+ const built = /RENDER_DONE/.test(out)
104
+ if (!built) return { built: false, renders: [], error: (/(AGENT_SCRIPT_ERROR:.*|NO_MESH)/.exec(out)?.[0] ?? out.trim().slice(-400)) }
105
+ const renders: string[] = []
106
+ for (let v = 0; v < views; v++) {
107
+ const buf = await readFile(join(dir, `view_${v}.png`)).catch(() => undefined)
108
+ if (buf) renders.push(`data:image/png;base64,${buf.toString('base64')}`)
109
+ }
110
+ return { built: renders.length > 0, renders }
111
+ } finally {
112
+ await rm(dir, { recursive: true, force: true }).catch(() => {})
113
+ }
114
+ }
115
+
116
+ export interface BlenderLocalConfig {
117
+ routerBaseUrl: string
118
+ routerKey: string
119
+ model: string
120
+ rounds?: number
121
+ /** N standardized views to render (CADBench uses 4). Default 4. */
122
+ views?: number
123
+ /** The bpy authoring directive — the GEPA-optimizable surface. */
124
+ directive?: string
125
+ }
126
+
127
+ export interface BlenderShot {
128
+ /** The bpy script the agent wrote — the artifact. */
129
+ artifact: string
130
+ /** Rendered view PNGs as data URIs (the images the criteria judge scores). */
131
+ renders: string[]
132
+ trace: Span[]
133
+ usage: { input: number; output: number }
134
+ ok: boolean
135
+ /** True if the script executed and produced at least one mesh. */
136
+ built: boolean
137
+ detail?: string
138
+ }
139
+
140
+ /**
141
+ * Author a bpy script for the task via the router, execute + render it headless
142
+ * in Blender, refine on execution errors across rounds. Returns the script, the
143
+ * rendered views, a screenshot-rich trace, and real token usage.
144
+ */
145
+ export async function solveBlenderLocal(task: BenchTask, cfg: BlenderLocalConfig): Promise<BlenderShot> {
146
+ const rounds = Math.max(1, cfg.rounds ?? 2)
147
+ const views = Math.max(1, cfg.views ?? 4)
148
+ const directive = cfg.directive ?? DEFAULT_BLENDER_DIRECTIVE
149
+ const trace: Span[] = []
150
+ const runId = `cadbench-${task.id}`
151
+ let ts = Date.now()
152
+ const tick = () => (ts += 1)
153
+ const usage = { input: 0, output: 0 }
154
+ // Carried across rounds in closures (the round Artifact is the bpy script; the
155
+ // render PNGs + built flag + lastErr persist outside the loop). usage is REAL.
156
+ let renders: string[] = []
157
+ let built = false
158
+ let lastErr = ''
159
+
160
+ trace.push({ spanId: 's-brief', runId, kind: 'llm', name: 'brief', model: cfg.model, messages: [{ role: 'user', content: task.prompt }], startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
161
+
162
+ // Migrated onto runRefineLoop: the mkdtemp dir (with the runner.py written once
163
+ // in setup) is the Ctx; built (RENDER_DONE + ≥1 collected view) is the early-stop,
164
+ // modeled as a judge so default-decide stops the loop. The round-2+ steer carries
165
+ // lastErr + the prior script verbatim.
166
+ const res = await runRefineLoop<string, string>({
167
+ rounds,
168
+ setup: async () => {
169
+ const dir = await mkdtemp(join(tmpdir(), 'blender-'))
170
+ await writeFile(join(dir, 'runner.py'), RUNNER_PY)
171
+ return dir
172
+ },
173
+ prompt: (round, history) =>
174
+ round === 1
175
+ ? task.prompt
176
+ : `Your previous bpy script failed:\n${lastErr}\n\nPrevious script:\n${history[history.length - 1]?.artifact ?? ''}\n\nFix it so it runs under \`blender --background --python\` and builds the object as mesh(es). Brief:\n${task.prompt}`,
177
+ runShot: async (user, round, dir) => {
178
+ const runnerPath = join(dir, 'runner.py')
179
+ const scriptPath = join(dir, 'model.py')
180
+ const { content, usage: u } = await routerChatWithUsage(
181
+ cfg,
182
+ [
183
+ { role: 'system', content: directive },
184
+ { role: 'user', content: user },
185
+ ],
186
+ { temperature: 0.3 },
187
+ )
188
+ if (u) {
189
+ usage.input += u.input
190
+ usage.output += u.output
191
+ }
192
+ const script = extractPy(content)
193
+ trace.push({ spanId: `s-author-${round}`, runId, kind: 'llm', name: `author r${round}`, model: cfg.model, messages: [{ role: 'user', content: round === 1 ? task.prompt : 'refine' }], output: content.slice(0, 600), startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
194
+ trace.push({ spanId: `s-write-${round}`, runId, kind: 'tool', name: 'write_file', toolName: 'create_file', args: { path: 'model.py', content: script }, startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
195
+
196
+ await writeFile(scriptPath, script)
197
+ const run = await runLocal('xvfb-run', ['-a', 'blender', '--background', '--python', runnerPath, '--', scriptPath, dir, String(views)], dir)
198
+ const out = `${run.stdout}\n${run.stderr}`
199
+ built = /RENDER_DONE/.test(out)
200
+ lastErr = built ? '' : (/(AGENT_SCRIPT_ERROR:.*|NO_MESH)/.exec(out)?.[0] ?? out.trim().slice(-800))
201
+ trace.push({ spanId: `s-blender-${round}`, runId, kind: 'tool', name: `blender r${round}`, toolName: 'shell.exec', args: 'blender --background --python runner.py model.py', result: (built ? 'RENDER_DONE' : lastErr).slice(0, 1500), startedAt: tick(), endedAt: tick(), status: built ? 'ok' : 'error', error: built ? undefined : `exit ${run.code}` } as Span)
202
+
203
+ if (built) {
204
+ const collected: string[] = []
205
+ for (let v = 0; v < views; v++) {
206
+ const buf = await readFile(join(dir, `view_${v}.png`)).catch(() => undefined)
207
+ if (buf) collected.push(`data:image/png;base64,${buf.toString('base64')}`)
208
+ }
209
+ renders = collected
210
+ // first view carries the screen span (run-capsule reveal)
211
+ trace.push({ spanId: `s-render-${round}`, runId, kind: 'tool', name: 'render', toolName: 'render.screenshot', args: { action: 'rendered model', url: 'view_0.png' }, attributes: collected[0] ? { screenshot: collected[0] } : {}, startedAt: tick(), endedAt: tick(), status: collected.length ? 'ok' : 'error', error: collected.length ? undefined : 'render produced no image' } as Span)
212
+ built = collected.length > 0
213
+ }
214
+ return { artifact: script }
215
+ },
216
+ judge: async () => ({ valid: built }),
217
+ teardown: (dir) => rm(dir, { recursive: true, force: true }).then(() => {}, () => {}),
218
+ })
219
+
220
+ const script = res.final.artifact
221
+ return {
222
+ artifact: script,
223
+ renders,
224
+ trace,
225
+ usage,
226
+ ok: script.trim().length > 0,
227
+ built,
228
+ detail: built ? `built + rendered ${renders.length} views` : `did not build in ${rounds} rounds${lastErr ? `; last: ${lastErr.slice(0, 140)}` : ''}`,
229
+ }
230
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Mind2Web browser worker: one shot = the model, under the candidate authoring
3
+ * DIRECTIVE, reads the task + candidate elements and commits to a single next
4
+ * action (ELEMENT / ACTION / VALUE). The artifact is scored by the deterministic
5
+ * mind2web judge (element-match + op/value match); the produced trace carries the
6
+ * step's REAL page screenshot so run-capsule's screen capsule turns the run into a
7
+ * film — the actual page the agent acted on, not a re-rendered DOM.
8
+ *
9
+ * `directive` is the GEPA-optimizable surface: a minimal element-selection
10
+ * instruction with headroom for the optimizer to learn the discrimination moves
11
+ * (prefer the element whose role/label matches the goal step, disambiguate
12
+ * look-alikes) that move the deterministic Step-SR gate.
13
+ *
14
+ * Reports REAL token usage so the campaign's backend-integrity guard sees a real
15
+ * backend, never a fabricated zero.
16
+ */
17
+
18
+ import { readFile } from 'node:fs/promises'
19
+ import type { Span } from '@tangle-network/agent-eval'
20
+ import type { BenchTask } from './benchmarks/types'
21
+ import { routerChatWithUsage } from '@tangle-network/agent-runtime/loops'
22
+
23
+ export interface BrowserLocalConfig {
24
+ routerBaseUrl: string
25
+ routerKey: string
26
+ model: string
27
+ /** The element-selection directive (system prompt) — the GEPA-optimizable surface. */
28
+ directive?: string
29
+ }
30
+
31
+ export interface BrowserShot {
32
+ /** The ELEMENT/ACTION/VALUE the model produced — the artifact the judge scores. */
33
+ artifact: string
34
+ trace: Span[]
35
+ usage?: { input: number; output: number }
36
+ ok: boolean
37
+ detail?: string
38
+ }
39
+
40
+ /**
41
+ * The hand-written baseline element-selection directive — the SURFACE the GEPA
42
+ * loop optimizes. Deliberately minimal: states the job and the output contract,
43
+ * not HOW to discriminate the right element, leaving headroom for the optimizer.
44
+ */
45
+ export const DEFAULT_MIND2WEB_DIRECTIVE =
46
+ 'You are a web agent choosing the NEXT UI action toward the task goal. Read the task and the candidate elements, then pick the single element that makes progress and the correct action (CLICK, or TYPE/SELECT with a value). Respond with ONLY the three required lines.'
47
+
48
+ interface Mind2WebMetaView {
49
+ task?: string
50
+ website?: string
51
+ screenshotPath?: string
52
+ }
53
+
54
+ /**
55
+ * One element-selection shot under `directive`. Returns the artifact, REAL token
56
+ * usage, and a screenshot-rich trace (task → chosen action → a browser.<op> span
57
+ * carrying the page screenshot) for run-capsule.
58
+ */
59
+ export async function solveBrowserLocal(task: BenchTask, cfg: BrowserLocalConfig): Promise<BrowserShot> {
60
+ const directive = cfg.directive ?? DEFAULT_MIND2WEB_DIRECTIVE
61
+ const meta = (task.metadata ?? {}) as Mind2WebMetaView
62
+ const runId = `m2w-${task.id}`
63
+ let ts = Date.now()
64
+ const tick = () => (ts += 1)
65
+ const trace: Span[] = []
66
+ const goal = meta.task ?? task.prompt
67
+
68
+ trace.push({ spanId: 's-task', runId, kind: 'llm', name: 'web task', model: cfg.model, messages: [{ role: 'user', content: goal }], startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
69
+
70
+ const { content, usage } = await routerChatWithUsage(cfg, [
71
+ { role: 'system', content: directive },
72
+ { role: 'user', content: task.prompt },
73
+ ])
74
+ const artifact = content.trim()
75
+
76
+ const elementId = /ELEMENT:\s*\[?(\d+)\]?/i.exec(artifact)?.[1] ?? ''
77
+ const op = (/ACTION:\s*(CLICK|TYPE|SELECT)/i.exec(artifact)?.[1] ?? 'CLICK').toUpperCase()
78
+ const value = (/VALUE:\s*(.*)/i.exec(artifact)?.[1] ?? '').trim()
79
+
80
+ trace.push({ spanId: 's-reply', runId, kind: 'llm', name: 'choose action', model: cfg.model, messages: [{ role: 'user', content: goal }], output: artifact.slice(0, 400), startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
81
+
82
+ let screenshot: string | undefined
83
+ if (typeof meta.screenshotPath === 'string' && meta.screenshotPath) {
84
+ const buf = await readFile(meta.screenshotPath).catch(() => undefined)
85
+ if (buf) screenshot = `data:image/jpeg;base64,${buf.toString('base64')}`
86
+ }
87
+ const label = `${op}${value ? ` "${value.slice(0, 40)}"` : ''} element #${elementId}`
88
+ trace.push({
89
+ spanId: 's-act',
90
+ runId,
91
+ kind: 'tool',
92
+ name: `browser ${op.toLowerCase()}`,
93
+ toolName: `browser.${op.toLowerCase()}`,
94
+ args: { action: label, url: meta.website ? `https://${meta.website}` : undefined, selector: `[backend_node_id="${elementId}"]` },
95
+ attributes: screenshot ? { screenshot } : {},
96
+ startedAt: tick(),
97
+ endedAt: tick(),
98
+ status: 'ok',
99
+ } as Span)
100
+
101
+ return { artifact, trace, usage, ok: artifact.length > 0, detail: label }
102
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * CADGenBench worker. The deliverable is a STEP B-rep solid (output.step). We
3
+ * author a build123d (Python on the OpenCascade kernel) script via the router,
4
+ * execute it in the CADGenBench venv, and read back the produced output.step —
5
+ * exactly the reference baseline's contract. The artifact returned IS the STEP
6
+ * text, which the CADGenBench geometric scorer grades against the ground truth.
7
+ *
8
+ * The build123d authoring directive is the GEPA-optimizable surface; the
9
+ * build123d API cheat sheet (shipped in the cadgenbench package) is appended as
10
+ * fixed reference context. Requires the CADGenBench venv (CADGENBENCH_VENV) +
11
+ * its clone (CADGENBENCH_DIR for the cheat sheet).
12
+ */
13
+
14
+ import { execFile } from 'node:child_process'
15
+ import { existsSync, readFileSync } from 'node:fs'
16
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
17
+ import { tmpdir } from 'node:os'
18
+ import { join } from 'node:path'
19
+ import { promisify } from 'node:util'
20
+ import type { Span } from '@tangle-network/agent-eval'
21
+ import type { BenchTask } from './benchmarks/types'
22
+ import { DEFAULT_BUILD123D_DIRECTIVE } from './directives'
23
+ import { runRefineLoop } from './refine-loop'
24
+ import { routerChatWithUsage } from '@tangle-network/agent-runtime/loops'
25
+
26
+ export { DEFAULT_BUILD123D_DIRECTIVE } from './directives'
27
+
28
+ const execFileAsync = promisify(execFile)
29
+
30
+ export const CGB_VENV_PY = process.env.CADGENBENCH_VENV ?? '/tmp/cgb-venv/bin/python'
31
+ export const CGB_DIR = process.env.CADGENBENCH_DIR ?? '/tmp/cadgenbench'
32
+
33
+ async function runLocal(cmd: string, args: string[], cwd: string, timeoutMs = 120_000): Promise<{ code: number; stdout: string; stderr: string }> {
34
+ try {
35
+ const { stdout, stderr } = await execFileAsync(cmd, args, { cwd, maxBuffer: 1 << 26, timeout: timeoutMs })
36
+ return { code: 0, stdout, stderr }
37
+ } catch (err) {
38
+ const e = err as { code?: number; stdout?: string; stderr?: string; message?: string }
39
+ return { code: typeof e.code === 'number' ? e.code : 1, stdout: e.stdout ?? '', stderr: e.stderr ?? e.message ?? String(err) }
40
+ }
41
+ }
42
+
43
+ function extractPy(text: string): string {
44
+ const fence = /```(?:python|py)?\s*\n([\s\S]*?)```/i.exec(text)
45
+ return (fence ? fence[1] : text).trim()
46
+ }
47
+
48
+ let _cheat: string | null = null
49
+ function cheatSheet(): string {
50
+ if (_cheat != null) return _cheat
51
+ const p = join(CGB_DIR, 'src/cadgenbench/baseline/build123d_cheat_sheet.md')
52
+ _cheat = existsSync(p) ? readFileSync(p, 'utf8').slice(0, 12000) : ''
53
+ return _cheat
54
+ }
55
+
56
+ export interface Build123dConfig {
57
+ routerBaseUrl: string
58
+ routerKey: string
59
+ model: string
60
+ rounds?: number
61
+ /** The build123d authoring directive — the GEPA-optimizable surface. */
62
+ directive?: string
63
+ }
64
+
65
+ export interface Build123dShot {
66
+ /** The produced STEP text (the artifact the CADGenBench scorer grades). */
67
+ artifact: string
68
+ /** The Python source the agent wrote. */
69
+ source: string
70
+ trace: Span[]
71
+ usage: { input: number; output: number }
72
+ ok: boolean
73
+ built: boolean
74
+ detail?: string
75
+ }
76
+
77
+ /** Author a build123d script via the router, execute it in the CADGenBench venv,
78
+ * read back output.step. Refine on execution error / missing STEP. */
79
+ export async function solveBuild123dLocal(task: BenchTask, cfg: Build123dConfig): Promise<Build123dShot> {
80
+ const rounds = Math.max(1, cfg.rounds ?? 2)
81
+ const directive = cfg.directive ?? DEFAULT_BUILD123D_DIRECTIVE
82
+ const sys = `${directive}\n\nbuild123d API reference:\n${cheatSheet()}`
83
+ const trace: Span[] = []
84
+ const runId = `cadgenbench-${task.id}`
85
+ let ts = Date.now()
86
+ const tick = () => (ts += 1)
87
+ const usage = { input: 0, output: 0 }
88
+ // Carried across rounds in closures (the round Artifact is the Python source; the
89
+ // STEP text + built flag + lastErr persist outside the loop). usage is REAL.
90
+ let step = ''
91
+ let built = false
92
+ let lastErr = ''
93
+
94
+ trace.push({ spanId: 's-brief', runId, kind: 'llm', name: 'brief', model: cfg.model, messages: [{ role: 'user', content: task.prompt }], startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
95
+
96
+ // Migrated onto runRefineLoop: the mkdtemp scratch dir is the Ctx; built (STEP
97
+ // produced) is the early-stop, modeled as a judge so default-decide stops the
98
+ // loop. The round-2+ steer carries lastErr + the prior source verbatim.
99
+ const res = await runRefineLoop<string, string>({
100
+ rounds,
101
+ setup: () => mkdtemp(join(tmpdir(), 'b123d-')),
102
+ prompt: (round, history) =>
103
+ round === 1
104
+ ? task.prompt
105
+ : `Your previous build123d script failed:\n${lastErr}\n\nPrevious script:\n${history[history.length - 1]?.artifact ?? ''}\n\nFix it so it runs in python and writes a valid output.step. Brief:\n${task.prompt}`,
106
+ runShot: async (user, round, dir) => {
107
+ const scriptPath = join(dir, 'build.py')
108
+ const stepPath = join(dir, 'output.step')
109
+ const { content, usage: u } = await routerChatWithUsage(cfg, [
110
+ { role: 'system', content: sys },
111
+ { role: 'user', content: user },
112
+ ])
113
+ if (u) {
114
+ usage.input += u.input
115
+ usage.output += u.output
116
+ }
117
+ const source = extractPy(content)
118
+ trace.push({ spanId: `s-author-${round}`, runId, kind: 'llm', name: `author r${round}`, model: cfg.model, messages: [{ role: 'user', content: round === 1 ? task.prompt : 'refine' }], output: content.slice(0, 600), startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
119
+ trace.push({ spanId: `s-write-${round}`, runId, kind: 'tool', name: 'write_file', toolName: 'create_file', args: { path: 'build.py', content: source }, startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
120
+
121
+ await writeFile(scriptPath, source)
122
+ const run = await runLocal(CGB_VENV_PY, [scriptPath], dir)
123
+ const got = existsSync(stepPath) ? await readFile(stepPath, 'utf8').catch(() => '') : ''
124
+ built = got.includes('ISO-10303-21') && got.length > 200
125
+ lastErr = built ? '' : `${run.stdout}\n${run.stderr}`.trim().slice(-800) || 'no output.step written'
126
+ if (built) step = got
127
+ trace.push({ spanId: `s-exec-${round}`, runId, kind: 'tool', name: `build123d r${round}`, toolName: 'shell.exec', args: 'python build.py', result: (built ? 'wrote output.step' : lastErr).slice(0, 1500), startedAt: tick(), endedAt: tick(), status: built ? 'ok' : 'error', error: built ? undefined : `exit ${run.code}` } as Span)
128
+ return { artifact: source }
129
+ },
130
+ judge: async () => ({ valid: built }),
131
+ teardown: (dir) => rm(dir, { recursive: true, force: true }).then(() => {}, () => {}),
132
+ })
133
+
134
+ return {
135
+ artifact: step,
136
+ source: res.final.artifact,
137
+ trace,
138
+ usage,
139
+ ok: res.final.artifact.trim().length > 0,
140
+ built,
141
+ detail: built ? 'exported output.step' : `did not produce a STEP in ${rounds} rounds${lastErr ? `; last: ${lastErr.slice(0, 140)}` : ''}`,
142
+ }
143
+ }