@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,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
+ }