@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.
- package/README.md +15 -0
- package/package.json +35 -0
- package/src/adapters.ts +60 -0
- package/src/aec-gate.mts +217 -0
- package/src/atom-humaneval.mts +197 -0
- package/src/atom-mcp-e2e.mts +223 -0
- package/src/benchmarks/_harness.ts +206 -0
- package/src/benchmarks/aec-bench.test.mts +53 -0
- package/src/benchmarks/aec-bench.ts +319 -0
- package/src/benchmarks/appworld.test.mts +45 -0
- package/src/benchmarks/appworld.ts +426 -0
- package/src/benchmarks/cad-design.ts +429 -0
- package/src/benchmarks/cadbench.ts +135 -0
- package/src/benchmarks/cadgenbench.ts +121 -0
- package/src/benchmarks/commit0.test.mts +71 -0
- package/src/benchmarks/commit0.ts +266 -0
- package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
- package/src/benchmarks/enterpriseops-gym.ts +343 -0
- package/src/benchmarks/finsearchcomp.ts +371 -0
- package/src/benchmarks/frames.ts +520 -0
- package/src/benchmarks/hotpotqa.ts +320 -0
- package/src/benchmarks/humaneval.ts +251 -0
- package/src/benchmarks/mind2web.ts +311 -0
- package/src/benchmarks/programbench.test.mts +60 -0
- package/src/benchmarks/programbench.ts +210 -0
- package/src/benchmarks/simpleqa.ts +390 -0
- package/src/benchmarks/swe-bench.ts +152 -0
- package/src/benchmarks/terminal-bench.ts +182 -0
- package/src/benchmarks/trata-hedge.ts +496 -0
- package/src/benchmarks/types.ts +58 -0
- package/src/browser/adapters/bad-design-audit.ts +153 -0
- package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
- package/src/browser/adapters/bad.ts +165 -0
- package/src/browser/agent-adapter.ts +145 -0
- package/src/browser/process-adapter.ts +146 -0
- package/src/browser/run-to-spans.ts +105 -0
- package/src/browser/run-to-spans.verify.ts +89 -0
- package/src/browser/ui-reviewer.ts +200 -0
- package/src/browser/ui-reviewer.verify.ts +94 -0
- package/src/browser/verify.ts +96 -0
- package/src/clbench-codebase-gate.mts +314 -0
- package/src/clbench-context-gate.mts +305 -0
- package/src/cloud-loop.mts +138 -0
- package/src/coding-skills/minimal-diff.md +9 -0
- package/src/coding-skills/read-before-edit.md +9 -0
- package/src/coding-skills/reproduce-first.md +10 -0
- package/src/coding-skills/run-tests-after-edit.md +9 -0
- package/src/coding-skills/trace-the-failure.md +9 -0
- package/src/commit0-env-run.mts +59 -0
- package/src/commit0-env.ts +173 -0
- package/src/commit0-gate.mts +529 -0
- package/src/commit0-prereqs.sh +48 -0
- package/src/corpus-replay.mts +300 -0
- package/src/corpus-report.mts +504 -0
- package/src/corpus.test.mts +273 -0
- package/src/corpus.ts +325 -0
- package/src/decoder-live.mts +133 -0
- package/src/directives.ts +84 -0
- package/src/diverse-gate.mjs +112 -0
- package/src/egress-probe.mts +26 -0
- package/src/eops-skills/address-every-subtask.md +5 -0
- package/src/eops-skills/exact-tools-and-args.md +5 -0
- package/src/eops-skills/full-sequence-to-goal.md +5 -0
- package/src/eops-skills/ground-every-value.md +5 -0
- package/src/eops-skills/honor-the-policies.md +5 -0
- package/src/examples/README.md +58 -0
- package/src/examples/math-demo.mts +110 -0
- package/src/examples/strategy-demo.mts +119 -0
- package/src/fleet.mts +121 -0
- package/src/gate-cli.mts +101 -0
- package/src/gate.test.mts +129 -0
- package/src/gate.ts +460 -0
- package/src/generate-eval/certify.ts +178 -0
- package/src/generate-eval/schema.ts +78 -0
- package/src/humaneval-gate.mts +204 -0
- package/src/humaneval-repair-gate.mts +143 -0
- package/src/index.ts +19 -0
- package/src/mcp-mount-probe.mts +126 -0
- package/src/profile-coordinates.ts +134 -0
- package/src/profiles.ts +128 -0
- package/src/refine-loop.test.mts +106 -0
- package/src/refine-loop.ts +106 -0
- package/src/research-gate.mts +132 -0
- package/src/research-shot.ts +134 -0
- package/src/resolve-client.ts +58 -0
- package/src/router-executor.ts +51 -0
- package/src/run-pool.ts +48 -0
- package/src/runtime-hook-recorder.ts +137 -0
- package/src/sandbox-run.ts +125 -0
- package/src/search-bench/bridge.ts +124 -0
- package/src/search-bench/export.mts +0 -0
- package/src/search-bench/parametric-check.mts +63 -0
- package/src/search-bench/profiles.ts +98 -0
- package/src/search-bench/run.mts +287 -0
- package/src/search-bench/tasks-fresh.ts +688 -0
- package/src/search-bench/tasks.ts +129 -0
- package/src/search-tool.ts +95 -0
- package/src/selector.test.mts +189 -0
- package/src/selector.ts +366 -0
- package/src/skill-sandbox-smoke.mts +100 -0
- package/src/stats.mts +90 -0
- package/src/terminal-compare.ts +519 -0
- package/src/trajectory-assemble.mjs +130 -0
- package/src/trata-gate.mts +243 -0
- package/src/trata-gepa.mts +434 -0
- package/src/worker-blender.ts +230 -0
- package/src/worker-browser.ts +102 -0
- package/src/worker-build123d.ts +143 -0
- package/src/worker-cad.ts +451 -0
- package/src/worker.ts +136 -0
- 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
|
+
}
|