@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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAD sandbox worker: one shot = a fresh 'universal' Tangle sandbox where a
|
|
3
|
+
* coding agent authors OpenSCAD source for the task's brief. The sandbox's OWN
|
|
4
|
+
* Nix-profile toolchain is the gate — `openscad` compiles + exports the STL and
|
|
5
|
+
* renders a PNG inside the box; we read both back. The artifact (the `.scad`) is
|
|
6
|
+
* scored by the cad-design judge; the produced trace (brief → code → compile →
|
|
7
|
+
* render screenshot) drives run-capsule's conversation/code/terminal/screen
|
|
8
|
+
* capsules.
|
|
9
|
+
*
|
|
10
|
+
* This is the "full rounded run": real sandbox, the real CAD kernel as the
|
|
11
|
+
* verifiable reward, real geometry, and a screenshot-rich trace for the video.
|
|
12
|
+
* Requires `openscad` + `xvfb-run` in the sandbox profile (universal has both).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFile } from 'node:child_process'
|
|
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 { acquireSandbox, routerChatWithUsage } from '@tangle-network/agent-runtime/loops'
|
|
21
|
+
import { Sandbox } from '@tangle-network/sandbox'
|
|
22
|
+
import type { Span } from '@tangle-network/agent-eval'
|
|
23
|
+
import type { BenchTask } from './benchmarks/types'
|
|
24
|
+
import { DEFAULT_CAD_DIRECTIVE, DEFAULT_CAD_SANDBOX_DIRECTIVE } from './directives'
|
|
25
|
+
import { runRefineLoop } from './refine-loop'
|
|
26
|
+
|
|
27
|
+
export { DEFAULT_CAD_DIRECTIVE } from './directives'
|
|
28
|
+
|
|
29
|
+
export interface CadWorkerConfig {
|
|
30
|
+
sandboxBaseUrl: string
|
|
31
|
+
sandboxKey: string
|
|
32
|
+
routerBaseUrl: string
|
|
33
|
+
routerKey: string
|
|
34
|
+
model: string
|
|
35
|
+
provider?: string
|
|
36
|
+
timeoutMs?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CadShotResult {
|
|
40
|
+
/** The OpenSCAD source the agent wrote — the artifact the judge scores. */
|
|
41
|
+
artifact: string
|
|
42
|
+
/** Trace of the run for run-capsule: brief → code → compile → render. */
|
|
43
|
+
trace: Span[]
|
|
44
|
+
ok: boolean
|
|
45
|
+
detail?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SCAD_PATH = '/work/model.scad'
|
|
49
|
+
const STL_PATH = '/work/model.stl'
|
|
50
|
+
const PNG_PATH = '/work/model.png'
|
|
51
|
+
|
|
52
|
+
/** The acquired sandbox instance — the per-task execution Ctx for solveCadRefine. */
|
|
53
|
+
type SandboxBox = Awaited<ReturnType<typeof acquireSandbox>>
|
|
54
|
+
|
|
55
|
+
const randomSuffix = () => Math.random().toString(36).slice(2, 10)
|
|
56
|
+
|
|
57
|
+
/** Strip markdown fences / prose so we keep just the OpenSCAD source. */
|
|
58
|
+
function extractScad(text: string): string {
|
|
59
|
+
const fence = /```(?:openscad|scad|c|cpp)?\s*\n([\s\S]*?)```/i.exec(text)
|
|
60
|
+
return (fence ? fence[1] : text).trim()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const execFileAsync = promisify(execFile)
|
|
64
|
+
|
|
65
|
+
/** Run a local command, returning exit code + streams (never throws on nonzero —
|
|
66
|
+
* the geometry gate is the arbiter, so a failed compile still yields its error). */
|
|
67
|
+
async function runLocal(
|
|
68
|
+
cmd: string,
|
|
69
|
+
args: string[],
|
|
70
|
+
cwd: string,
|
|
71
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout, stderr } = await execFileAsync(cmd, args, { cwd, maxBuffer: 1 << 26, timeout: 120_000 })
|
|
74
|
+
return { code: 0, stdout, stderr }
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const e = err as { code?: number; stdout?: string; stderr?: string; message?: string }
|
|
77
|
+
return { code: e.code ?? 1, stdout: e.stdout ?? '', stderr: e.stderr ?? e.message ?? String(err) }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CadLocalConfig {
|
|
82
|
+
routerBaseUrl: string
|
|
83
|
+
routerKey: string
|
|
84
|
+
model: string
|
|
85
|
+
rounds?: number
|
|
86
|
+
/** The authoring directive (system prompt) — the GEPA-optimizable surface. */
|
|
87
|
+
directive?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CadLocalShot {
|
|
91
|
+
/** Final OpenSCAD source — the artifact the judge scores. */
|
|
92
|
+
artifact: string
|
|
93
|
+
/** First-round source (the blind shot, before any refine). */
|
|
94
|
+
round1Artifact: string
|
|
95
|
+
trace: Span[]
|
|
96
|
+
/** Per-compiling-round render PNGs as data URIs (run-capsule reveal/orbit). */
|
|
97
|
+
renders: string[]
|
|
98
|
+
usage: { input: number; output: number }
|
|
99
|
+
ok: boolean
|
|
100
|
+
detail?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* LOCAL CAD refine loop — the staging-independent twin of `solveCadRefine`. The
|
|
105
|
+
* model authors the `.scad` via the router under `directive`, the LOCAL openscad
|
|
106
|
+
* kernel (`xvfb-run -a openscad`) gates + renders each round, and compiler
|
|
107
|
+
* feedback drives the next round. Returns the final source, per-round renders, a
|
|
108
|
+
* screenshot-rich trace, and REAL token usage. This is the seam the GEPA loop
|
|
109
|
+
* drives: `directive` is the surface, the deterministic geometry gate is the reward.
|
|
110
|
+
*/
|
|
111
|
+
export async function solveCadRefineLocal(task: BenchTask, cfg: CadLocalConfig): Promise<CadLocalShot> {
|
|
112
|
+
const rounds = Math.max(1, cfg.rounds ?? 3)
|
|
113
|
+
const directive = cfg.directive ?? DEFAULT_CAD_DIRECTIVE
|
|
114
|
+
const trace: Span[] = []
|
|
115
|
+
const renders: string[] = []
|
|
116
|
+
const runId = `cad-${task.id}`
|
|
117
|
+
let ts = Date.now()
|
|
118
|
+
const tick = () => (ts += 1)
|
|
119
|
+
const usage = { input: 0, output: 0 }
|
|
120
|
+
// Carried across rounds in closures (the round Artifact is the .scad source; the
|
|
121
|
+
// lastErr steer + resolved early-stop persist outside the loop). usage is REAL.
|
|
122
|
+
let lastErr = ''
|
|
123
|
+
let resolved = false
|
|
124
|
+
|
|
125
|
+
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)
|
|
126
|
+
|
|
127
|
+
// Migrated onto runRefineLoop: the mkdtemp dir is the Ctx; resolved (compiles AND
|
|
128
|
+
// has geometry) is the early-stop, modeled as a judge so default-decide stops the
|
|
129
|
+
// loop. The round-2+ steer carries lastErr + the prior source verbatim.
|
|
130
|
+
const res = await runRefineLoop<string, string>({
|
|
131
|
+
rounds,
|
|
132
|
+
setup: () => mkdtemp(join(tmpdir(), 'cad-local-')),
|
|
133
|
+
prompt: (round, history) =>
|
|
134
|
+
round === 1
|
|
135
|
+
? task.prompt
|
|
136
|
+
: `Your previous OpenSCAD had this problem:\n${lastErr}\n\nHere is the previous source:\n${history[history.length - 1]?.artifact ?? ''}\n\nFix it so it compiles AND better matches the brief:\n${task.prompt}`,
|
|
137
|
+
runShot: async (user, round, dir) => {
|
|
138
|
+
const scadPath = join(dir, 'model.scad')
|
|
139
|
+
const stlPath = join(dir, 'model.stl')
|
|
140
|
+
const pngPath = join(dir, 'model.png')
|
|
141
|
+
const { content, usage: u } = await routerChatWithUsage(cfg, [
|
|
142
|
+
{ role: 'system', content: directive },
|
|
143
|
+
{ role: 'user', content: user },
|
|
144
|
+
])
|
|
145
|
+
if (u) {
|
|
146
|
+
usage.input += u.input
|
|
147
|
+
usage.output += u.output
|
|
148
|
+
}
|
|
149
|
+
const scad = extractScad(content)
|
|
150
|
+
trace.push({ spanId: `s-reply-${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)
|
|
151
|
+
trace.push({ spanId: `s-write-${round}`, runId, kind: 'tool', name: 'write_file', toolName: 'create_file', args: { path: 'model.scad', content: scad }, startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
|
|
152
|
+
|
|
153
|
+
await writeFile(scadPath, scad)
|
|
154
|
+
const compile = await runLocal('xvfb-run', ['-a', 'openscad', '-o', stlPath, scadPath], dir)
|
|
155
|
+
const compileOk = compile.code === 0
|
|
156
|
+
lastErr = compileOk ? '' : `${compile.stdout}\n${compile.stderr}`.trim().slice(0, 800)
|
|
157
|
+
trace.push({ spanId: `s-compile-${round}`, runId, kind: 'tool', name: `openscad r${round}`, toolName: 'shell.exec', args: 'openscad -o model.stl model.scad', result: (compileOk ? compile.stderr : lastErr).slice(0, 1500) || 'ok', startedAt: tick(), endedAt: tick(), status: compileOk ? 'ok' : 'error', error: compileOk ? undefined : `exit ${compile.code}` } as Span)
|
|
158
|
+
|
|
159
|
+
let screenshot: string | undefined
|
|
160
|
+
if (compileOk) {
|
|
161
|
+
// Full CGAL --render (not preview): clean coplanar faces, no z-fighting
|
|
162
|
+
// speckle on the window/door cutouts. Dark Tomorrow-Night palette to match
|
|
163
|
+
// the film; --autocenter --viewall frames any model regardless of its
|
|
164
|
+
// dimensions (so the same command flatters every task in the set).
|
|
165
|
+
const render = await runLocal('xvfb-run', ['-a', 'openscad', '-o', pngPath, '--render', '--imgsize=1280,960', '--colorscheme=Tomorrow Night', '--projection=perspective', '--autocenter', '--viewall', '--camera=0,0,0,60,0,25,0', scadPath], dir)
|
|
166
|
+
if (render.code === 0) {
|
|
167
|
+
const buf = await readFile(pngPath).catch(() => undefined)
|
|
168
|
+
if (buf) {
|
|
169
|
+
screenshot = `data:image/png;base64,${buf.toString('base64')}`
|
|
170
|
+
renders.push(screenshot)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const stl = await readFile(stlPath, 'utf8').catch(() => '')
|
|
174
|
+
resolved = stl.length > 0 && /facet normal/.test(stl)
|
|
175
|
+
}
|
|
176
|
+
trace.push({ spanId: `s-render-${round}`, runId, kind: 'tool', name: `render r${round}`, toolName: 'render.screenshot', args: { action: `rendered round ${round}`, url: 'model.png' }, attributes: screenshot ? { screenshot } : {}, startedAt: tick(), endedAt: tick(), status: screenshot ? 'ok' : 'error', error: screenshot ? undefined : (compileOk ? 'render produced no image' : 'skipped — did not compile') } as Span)
|
|
177
|
+
return { artifact: scad }
|
|
178
|
+
},
|
|
179
|
+
judge: async () => ({ valid: resolved }),
|
|
180
|
+
teardown: (dir) => rm(dir, { recursive: true, force: true }).then(() => {}, () => {}),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
artifact: res.final.artifact,
|
|
185
|
+
round1Artifact: res.blind.artifact,
|
|
186
|
+
trace,
|
|
187
|
+
renders,
|
|
188
|
+
usage,
|
|
189
|
+
ok: res.final.artifact.trim().length > 0,
|
|
190
|
+
detail: resolved ? `compiled in ≤${rounds} rounds` : `did not compile cleanly in ${rounds} rounds${lastErr ? `; last: ${lastErr.slice(0, 120)}` : ''}`,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface CadRefineConfig extends CadWorkerConfig {
|
|
195
|
+
/** Max author→gate→refine rounds. Default 3. */
|
|
196
|
+
rounds?: number
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Orchestrated CAD refine loop: a BARE universal sandbox is the CAD compute +
|
|
201
|
+
* render environment (reliable to provision); the model authors the `.scad` via
|
|
202
|
+
* the router, the box's own openscad gates + renders each round, and the
|
|
203
|
+
* compiler/geometry feedback drives the next round until it passes or rounds run
|
|
204
|
+
* out. This is the "design in a loop" path — every round's render is captured,
|
|
205
|
+
* so the trace shows the model improving the house across attempts.
|
|
206
|
+
*/
|
|
207
|
+
export async function solveCadRefine(task: BenchTask, cfg: CadRefineConfig): Promise<CadShotResult> {
|
|
208
|
+
const rounds = cfg.rounds ?? 3
|
|
209
|
+
const client = new Sandbox({ baseUrl: cfg.sandboxBaseUrl, apiKey: cfg.sandboxKey })
|
|
210
|
+
const t0 = Date.now()
|
|
211
|
+
const trace: Span[] = []
|
|
212
|
+
const runId = `cad-${task.id}`
|
|
213
|
+
let ts = t0
|
|
214
|
+
const tick = () => (ts += 1)
|
|
215
|
+
// The authoring system prompt for the orchestrated sandbox path — kept verbatim,
|
|
216
|
+
// distinct from DEFAULT_CAD_DIRECTIVE (the local path's GEPA surface).
|
|
217
|
+
const sys = DEFAULT_CAD_SANDBOX_DIRECTIVE
|
|
218
|
+
// Carried across rounds in closures (the round Artifact is the .scad source; the
|
|
219
|
+
// lastErr steer + resolved early-stop persist outside the loop).
|
|
220
|
+
let lastErr = ''
|
|
221
|
+
let resolved = false
|
|
222
|
+
|
|
223
|
+
// Migrated onto runRefineLoop: the universal sandbox box is the Ctx (acquired once,
|
|
224
|
+
// /work created in setup, torn down in teardown). resolved (compiles AND has
|
|
225
|
+
// geometry) is the early-stop, modeled as a judge so default-decide stops the loop.
|
|
226
|
+
// The round-2+ steer carries lastErr + the prior source verbatim.
|
|
227
|
+
const res = await runRefineLoop<string, SandboxBox>({
|
|
228
|
+
rounds,
|
|
229
|
+
setup: async () => {
|
|
230
|
+
const box = await acquireSandbox(client, {
|
|
231
|
+
name: `cad-${task.id}-${randomSuffix()}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
|
|
232
|
+
environment: 'universal',
|
|
233
|
+
})
|
|
234
|
+
// If init fails AFTER acquire, reap the box here — setup throwing before it
|
|
235
|
+
// returns the Ctx means runRefineLoop's teardown never runs, so an unguarded
|
|
236
|
+
// mkdir failure would leak the sandbox (the pre-migration finally deleted it).
|
|
237
|
+
try {
|
|
238
|
+
await box.exec('mkdir -p /work', { timeoutMs: 30_000 })
|
|
239
|
+
// The brief frames the title card (understood_task).
|
|
240
|
+
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)
|
|
241
|
+
return box
|
|
242
|
+
} catch (err) {
|
|
243
|
+
try {
|
|
244
|
+
await box.delete?.()
|
|
245
|
+
} catch {
|
|
246
|
+
// platform reaps on expiry
|
|
247
|
+
}
|
|
248
|
+
throw err
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
prompt: (round, history) =>
|
|
252
|
+
round === 1
|
|
253
|
+
? task.prompt
|
|
254
|
+
: `Your previous OpenSCAD had this problem:\n${lastErr}\n\nHere is the previous source:\n${history[history.length - 1]?.artifact ?? ''}\n\nFix it so it compiles AND better matches the brief:\n${task.prompt}`,
|
|
255
|
+
runShot: async (user, round, box) => {
|
|
256
|
+
const { content: reply } = await routerChatWithUsage(cfg, [
|
|
257
|
+
{ role: 'system', content: sys },
|
|
258
|
+
{ role: 'user', content: user },
|
|
259
|
+
])
|
|
260
|
+
const scad = extractScad(reply)
|
|
261
|
+
trace.push({ spanId: `s-reply-${round}`, runId, kind: 'llm', name: `author r${round}`, model: cfg.model, messages: [{ role: 'user', content: round === 1 ? task.prompt : 'refine' }], output: reply.slice(0, 600), startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
|
|
262
|
+
trace.push({ spanId: `s-write-${round}`, runId, kind: 'tool', name: 'write_file', toolName: 'create_file', args: { path: 'model.scad', content: scad }, startedAt: tick(), endedAt: tick(), status: 'ok' } as Span)
|
|
263
|
+
|
|
264
|
+
await box.fs.write(SCAD_PATH, scad)
|
|
265
|
+
const compile = await box.exec(`xvfb-run -a openscad -o ${STL_PATH} ${SCAD_PATH}`, { timeoutMs: 120_000 })
|
|
266
|
+
const compileOk = compile.exitCode === 0
|
|
267
|
+
lastErr = compileOk ? '' : `${compile.stdout}\n${compile.stderr}`.trim().slice(0, 800)
|
|
268
|
+
trace.push({ spanId: `s-compile-${round}`, runId, kind: 'tool', name: `openscad r${round}`, toolName: 'shell.exec', args: 'openscad -o model.stl model.scad', result: (compileOk ? compile.stderr : lastErr).slice(0, 1500) || 'ok', startedAt: tick(), endedAt: tick(), status: compileOk ? 'ok' : 'error', error: compileOk ? undefined : `exit ${compile.exitCode}` } as Span)
|
|
269
|
+
|
|
270
|
+
let screenshot: string | undefined
|
|
271
|
+
if (compileOk) {
|
|
272
|
+
const render = await box.exec(`xvfb-run -a openscad -o ${PNG_PATH} --imgsize=1100,850 --camera=40,30,40,55,0,25,260 --colorscheme=Tomorrow ${SCAD_PATH}`, { timeoutMs: 120_000 })
|
|
273
|
+
if (render.exitCode === 0) {
|
|
274
|
+
const localPng = join(tmpdir(), `cad-${task.id}-r${round}-${randomSuffix()}.png`)
|
|
275
|
+
await box.fs.download(PNG_PATH, localPng).catch(() => undefined)
|
|
276
|
+
const buf = await readFile(localPng).catch(() => undefined)
|
|
277
|
+
if (buf) screenshot = `data:image/png;base64,${buf.toString('base64')}`
|
|
278
|
+
}
|
|
279
|
+
// Geometry gate: read STL back + check it's non-trivial (the adapter judge
|
|
280
|
+
// does the full spec scoring; here we just decide whether to stop refining).
|
|
281
|
+
const stl = await box.fs.read(STL_PATH).catch(() => '')
|
|
282
|
+
resolved = stl.length > 0 && /facet normal/.test(stl)
|
|
283
|
+
}
|
|
284
|
+
trace.push({ spanId: `s-render-${round}`, runId, kind: 'tool', name: `render r${round}`, toolName: 'render.screenshot', args: { action: `rendered round ${round}`, url: 'model.png' }, attributes: screenshot ? { screenshot } : {}, startedAt: tick(), endedAt: tick(), status: screenshot ? 'ok' : 'error', error: screenshot ? undefined : (compileOk ? 'render produced no image' : 'skipped — did not compile') } as Span)
|
|
285
|
+
return { artifact: scad }
|
|
286
|
+
},
|
|
287
|
+
judge: async () => ({ valid: resolved }),
|
|
288
|
+
teardown: async (box) => {
|
|
289
|
+
try {
|
|
290
|
+
await box.delete()
|
|
291
|
+
} catch {
|
|
292
|
+
// staging reaps on expiry
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const artifact = res.final.artifact
|
|
298
|
+
return { artifact, trace, ok: artifact.trim().length > 0, detail: resolved ? `resolved in ≤${rounds} rounds` : `did not fully resolve in ${rounds} rounds${lastErr ? `; last: ${lastErr.slice(0, 120)}` : ''}` }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Run one CAD authoring shot in a real sandbox, gating with the box's own
|
|
302
|
+
* openscad and capturing a screenshot-rich trace. */
|
|
303
|
+
export async function solveCadShot(task: BenchTask, cfg: CadWorkerConfig): Promise<CadShotResult> {
|
|
304
|
+
const client = new Sandbox({ baseUrl: cfg.sandboxBaseUrl, apiKey: cfg.sandboxKey })
|
|
305
|
+
const box = await acquireSandbox(client, {
|
|
306
|
+
name: `cad-${task.id}-${randomSuffix()}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
|
|
307
|
+
environment: 'universal',
|
|
308
|
+
backend: {
|
|
309
|
+
type: 'opencode',
|
|
310
|
+
// provider/model/baseUrl pinning only — in-box model auth is the box-provisioned
|
|
311
|
+
// OPENCODE_MODEL_API_KEY (foreign keys are 403'd at egress).
|
|
312
|
+
model: {
|
|
313
|
+
provider: cfg.provider ?? 'openai',
|
|
314
|
+
model: cfg.model,
|
|
315
|
+
baseUrl: cfg.routerBaseUrl,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const t0 = Date.now()
|
|
321
|
+
const trace: Span[] = []
|
|
322
|
+
const runId = `cad-${task.id}`
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const prompt = [
|
|
326
|
+
task.prompt,
|
|
327
|
+
'',
|
|
328
|
+
`Write the OpenSCAD source to ${SCAD_PATH} (create the /work directory if needed).`,
|
|
329
|
+
`Then verify it compiles by running: xvfb-run -a openscad -o ${STL_PATH} ${SCAD_PATH}`,
|
|
330
|
+
`Fix any errors until it compiles cleanly and the geometry matches the brief. Then stop.`,
|
|
331
|
+
`The file at ${SCAD_PATH} is the deliverable.`,
|
|
332
|
+
].join('\n')
|
|
333
|
+
|
|
334
|
+
// Conversation: the brief (understood_task) + the agent's reply.
|
|
335
|
+
trace.push({
|
|
336
|
+
spanId: 's-brief',
|
|
337
|
+
runId,
|
|
338
|
+
kind: 'llm',
|
|
339
|
+
name: 'brief',
|
|
340
|
+
model: cfg.model,
|
|
341
|
+
messages: [{ role: 'user', content: task.prompt }],
|
|
342
|
+
startedAt: t0,
|
|
343
|
+
endedAt: t0,
|
|
344
|
+
status: 'ok',
|
|
345
|
+
} as Span)
|
|
346
|
+
|
|
347
|
+
const signal = cfg.timeoutMs ? AbortSignal.timeout(cfg.timeoutMs) : undefined
|
|
348
|
+
let lastErr: string | undefined
|
|
349
|
+
let agentText = ''
|
|
350
|
+
for await (const ev of box.streamPrompt(prompt, signal ? { signal } : {})) {
|
|
351
|
+
if (ev?.type === 'error') lastErr = JSON.stringify(ev.data).slice(0, 300)
|
|
352
|
+
const text = typeof (ev as { text?: unknown })?.text === 'string' ? (ev as unknown as { text: string }).text : ''
|
|
353
|
+
if (text) agentText += text
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const artifact = await box.fs.read(SCAD_PATH).catch(() => '')
|
|
357
|
+
if (artifact.trim()) {
|
|
358
|
+
trace.push({
|
|
359
|
+
spanId: 's-write',
|
|
360
|
+
runId,
|
|
361
|
+
kind: 'tool',
|
|
362
|
+
name: 'write_file',
|
|
363
|
+
toolName: 'create_file',
|
|
364
|
+
args: { path: 'model.scad', content: artifact },
|
|
365
|
+
startedAt: t0 + 1,
|
|
366
|
+
endedAt: t0 + 2,
|
|
367
|
+
status: 'ok',
|
|
368
|
+
} as Span)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// GATE + RENDER in the sandbox itself (the box's own openscad/xvfb).
|
|
372
|
+
const compile = await box.exec(`xvfb-run -a openscad -o ${STL_PATH} ${SCAD_PATH}`, {
|
|
373
|
+
timeoutMs: 120_000,
|
|
374
|
+
})
|
|
375
|
+
trace.push({
|
|
376
|
+
spanId: 's-compile',
|
|
377
|
+
runId,
|
|
378
|
+
kind: 'tool',
|
|
379
|
+
name: 'openscad compile',
|
|
380
|
+
toolName: 'shell.exec',
|
|
381
|
+
args: `openscad -o model.stl model.scad`,
|
|
382
|
+
result: `${compile.stdout}\n${compile.stderr}`.trim().slice(0, 2000),
|
|
383
|
+
startedAt: t0 + 3,
|
|
384
|
+
endedAt: t0 + 4,
|
|
385
|
+
status: compile.exitCode === 0 ? 'ok' : 'error',
|
|
386
|
+
error: compile.exitCode === 0 ? undefined : `openscad exit ${compile.exitCode}`,
|
|
387
|
+
} as Span)
|
|
388
|
+
|
|
389
|
+
let screenshot: string | undefined
|
|
390
|
+
if (compile.exitCode === 0) {
|
|
391
|
+
const render = await box.exec(
|
|
392
|
+
`xvfb-run -a openscad -o ${PNG_PATH} --imgsize=1100,850 --camera=40,30,40,55,0,25,260 --colorscheme=Tomorrow ${SCAD_PATH}`,
|
|
393
|
+
{ timeoutMs: 120_000 },
|
|
394
|
+
)
|
|
395
|
+
if (render.exitCode === 0) {
|
|
396
|
+
const localPng = join(tmpdir(), `cad-${task.id}-${randomSuffix()}.png`)
|
|
397
|
+
await box.fs.download(PNG_PATH, localPng).catch(() => undefined)
|
|
398
|
+
const buf = await readFile(localPng).catch(() => undefined)
|
|
399
|
+
if (buf) screenshot = `data:image/png;base64,${buf.toString('base64')}`
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Screen span carrying the render — drives run-capsule's screen capsule.
|
|
403
|
+
trace.push({
|
|
404
|
+
spanId: 's-render',
|
|
405
|
+
runId,
|
|
406
|
+
kind: 'tool',
|
|
407
|
+
name: 'render',
|
|
408
|
+
toolName: 'render.screenshot',
|
|
409
|
+
args: { action: 'rendered model.scad', url: 'model.png' },
|
|
410
|
+
attributes: screenshot ? { screenshot } : {},
|
|
411
|
+
startedAt: t0 + 5,
|
|
412
|
+
endedAt: t0 + 6,
|
|
413
|
+
status: screenshot ? 'ok' : 'error',
|
|
414
|
+
error: screenshot ? undefined : 'render produced no image',
|
|
415
|
+
} as Span)
|
|
416
|
+
|
|
417
|
+
// Agent's closing reply (conversation capsule).
|
|
418
|
+
if (agentText.trim()) {
|
|
419
|
+
trace.push({
|
|
420
|
+
spanId: 's-reply',
|
|
421
|
+
runId,
|
|
422
|
+
kind: 'llm',
|
|
423
|
+
name: 'reply',
|
|
424
|
+
model: cfg.model,
|
|
425
|
+
messages: [{ role: 'user', content: task.prompt }],
|
|
426
|
+
output: agentText.trim().slice(0, 600),
|
|
427
|
+
startedAt: t0 + 7,
|
|
428
|
+
endedAt: t0 + 8,
|
|
429
|
+
status: 'ok',
|
|
430
|
+
} as Span)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
artifact,
|
|
435
|
+
trace,
|
|
436
|
+
ok: artifact.trim().length > 0 && compile.exitCode === 0,
|
|
437
|
+
detail:
|
|
438
|
+
artifact.trim().length === 0
|
|
439
|
+
? `no .scad written${lastErr ? `; lastError=${lastErr}` : ''}`
|
|
440
|
+
: compile.exitCode === 0
|
|
441
|
+
? undefined
|
|
442
|
+
: `compiled with exit ${compile.exitCode}`,
|
|
443
|
+
}
|
|
444
|
+
} finally {
|
|
445
|
+
try {
|
|
446
|
+
await box.delete()
|
|
447
|
+
} catch {
|
|
448
|
+
/* staging reaps on expiry */
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SWE-bench worker: one shot = a fresh staging sandbox where a coding agent
|
|
3
|
+
* clones the repo at base_commit, resolves the issue, and writes a patch we read
|
|
4
|
+
* back via the sandbox filesystem. The artifact is a unified git diff the
|
|
5
|
+
* SWE-bench judge scores. Runs against the REAL product path (our SDK → staging
|
|
6
|
+
* sandbox → opencode agent → router model), so it also exercises provisioning.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type AgentRunSpec,
|
|
11
|
+
type Deliverable,
|
|
12
|
+
openSandboxRun,
|
|
13
|
+
} from '@tangle-network/agent-runtime/loops'
|
|
14
|
+
import { Sandbox } from '@tangle-network/sandbox'
|
|
15
|
+
import type { BenchTask } from './benchmarks/types'
|
|
16
|
+
import {
|
|
17
|
+
type BenchRuntimeDecisionPoint,
|
|
18
|
+
type BenchRuntimeHookEvent,
|
|
19
|
+
createRuntimeHookRecorder,
|
|
20
|
+
} from './runtime-hook-recorder'
|
|
21
|
+
|
|
22
|
+
export interface WorkerConfig {
|
|
23
|
+
sandboxBaseUrl: string
|
|
24
|
+
sandboxKey: string
|
|
25
|
+
/** Pins the in-box provider's baseUrl only. Model auth is the box-provisioned
|
|
26
|
+
* credential (`OPENCODE_MODEL_API_KEY`) — never an external key. */
|
|
27
|
+
routerBaseUrl: string
|
|
28
|
+
model: string
|
|
29
|
+
provider?: string
|
|
30
|
+
timeoutMs?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ShotResult {
|
|
34
|
+
patch: string
|
|
35
|
+
ok: boolean
|
|
36
|
+
detail?: string
|
|
37
|
+
runtimeEvents?: BenchRuntimeHookEvent[]
|
|
38
|
+
runtimeDecisionPoints?: BenchRuntimeDecisionPoint[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PATCH_PATH = '/tmp/solution.patch'
|
|
42
|
+
|
|
43
|
+
const randomSuffix = () => Math.random().toString(36).slice(2, 10)
|
|
44
|
+
|
|
45
|
+
/** The git diff the agent wrote, read back off the box FS (+ any in-box error). A
|
|
46
|
+
* MISSING patch file is a real "agent produced no patch" outcome; any OTHER read
|
|
47
|
+
* failure surfaces in `TurnResult.readError`, never masked as an empty patch — so a
|
|
48
|
+
* judge distinguishes agent-failure from fs-failure. */
|
|
49
|
+
interface SwePatch {
|
|
50
|
+
patch: string
|
|
51
|
+
lastErr?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const swePatchDeliverable: Deliverable<SwePatch> = {
|
|
55
|
+
kind: 'artifact',
|
|
56
|
+
path: PATCH_PATH,
|
|
57
|
+
fromArtifact: (raw, events) => {
|
|
58
|
+
let lastErr: string | undefined
|
|
59
|
+
for (const ev of events) {
|
|
60
|
+
if ((ev as { type?: string }).type === 'error') lastErr = JSON.stringify((ev as { data?: unknown }).data).slice(0, 300)
|
|
61
|
+
}
|
|
62
|
+
return { patch: raw, ...(lastErr ? { lastErr } : {}) }
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Run one resolution shot. `steer` (optional) carries guidance from a prior attempt. */
|
|
67
|
+
export async function solveShot(
|
|
68
|
+
task: BenchTask,
|
|
69
|
+
cfg: WorkerConfig,
|
|
70
|
+
steer?: string,
|
|
71
|
+
): Promise<ShotResult> {
|
|
72
|
+
const md = task.metadata ?? {}
|
|
73
|
+
const repo = String(md.repo)
|
|
74
|
+
const base = String(md.base_commit)
|
|
75
|
+
const client = new Sandbox({ baseUrl: cfg.sandboxBaseUrl, apiKey: cfg.sandboxKey })
|
|
76
|
+
|
|
77
|
+
const prompt = [
|
|
78
|
+
`Clone https://github.com/${repo} into /work and \`git checkout ${base}\`.`,
|
|
79
|
+
'',
|
|
80
|
+
'Resolve this issue by editing the SOURCE (never the tests):',
|
|
81
|
+
'',
|
|
82
|
+
String(md.problem_statement ?? task.prompt),
|
|
83
|
+
steer ? `\n--- Guidance from a prior failed attempt ---\n${steer}\n` : '',
|
|
84
|
+
'',
|
|
85
|
+
`When finished, from the repo root run EXACTLY:`,
|
|
86
|
+
` git add -A && git diff --cached -- . ':(exclude)*/tests/*' > ${PATCH_PATH}`,
|
|
87
|
+
`Then stop. The patch file is the only deliverable.`,
|
|
88
|
+
].join('\n')
|
|
89
|
+
|
|
90
|
+
// Cold-start-resilient via the shared lineage layer (a gateway-timed-out create is
|
|
91
|
+
// recovered by name lookup). The inline profile + backend override is the same
|
|
92
|
+
// generic AgentRunSpec the runLoop kernel boots against the real sandbox.
|
|
93
|
+
const controller = new AbortController()
|
|
94
|
+
const timer = cfg.timeoutMs ? setTimeout(() => controller.abort(), cfg.timeoutMs) : undefined
|
|
95
|
+
const agentRun: AgentRunSpec<string> = {
|
|
96
|
+
profile: { name: 'swebench-worker', metadata: { backendType: 'opencode' } },
|
|
97
|
+
name: 'swebench-worker',
|
|
98
|
+
taskToPrompt: () => '', // unused — the prompt is streamed directly by openSandboxRun
|
|
99
|
+
sandboxOverrides: {
|
|
100
|
+
name: `bench-${task.id}-${randomSuffix()}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
|
|
101
|
+
environment: 'universal',
|
|
102
|
+
backend: {
|
|
103
|
+
type: 'opencode',
|
|
104
|
+
model: { provider: cfg.provider ?? 'openai', model: cfg.model, baseUrl: cfg.routerBaseUrl },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
const runtime = createRuntimeHookRecorder()
|
|
109
|
+
const run = await openSandboxRun(
|
|
110
|
+
client,
|
|
111
|
+
{
|
|
112
|
+
agentRun,
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
hooks: runtime.hooks,
|
|
115
|
+
runId: `swe-bench:${task.id}`,
|
|
116
|
+
scenarioId: task.id,
|
|
117
|
+
},
|
|
118
|
+
swePatchDeliverable,
|
|
119
|
+
)
|
|
120
|
+
try {
|
|
121
|
+
const turn = await run.start(prompt)
|
|
122
|
+
const empty = turn.out.patch.trim().length === 0
|
|
123
|
+
return {
|
|
124
|
+
patch: turn.out.patch,
|
|
125
|
+
ok: !empty,
|
|
126
|
+
detail: empty
|
|
127
|
+
? `empty patch${turn.readError ? ` (patch read failed: ${turn.readError.slice(0, 120)})` : ''}${turn.out.lastErr ? `; lastError=${turn.out.lastErr}` : ''}`
|
|
128
|
+
: undefined,
|
|
129
|
+
runtimeEvents: runtime.events,
|
|
130
|
+
runtimeDecisionPoints: runtime.decisionPoints,
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
if (timer) clearTimeout(timer)
|
|
134
|
+
await run.close()
|
|
135
|
+
}
|
|
136
|
+
}
|