@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,426 @@
1
+ /**
2
+ * AppWorld adapter (StonyBrookNLP/appworld). Worker artifact = the agent's
3
+ * Python solution that calls the simulated apps' APIs (the same `apis.<app>.<fn>`
4
+ * surface AppWorld exposes inside `world.execute(...)`), ending in
5
+ * `apis.supervisor.complete_task()`. Judge = AppWorld's OWN programmatic
6
+ * evaluator: a driver runs the solution in a fresh `AppWorld(task_id=...)` world,
7
+ * then `world.evaluate().to_dict()` reports `success` (binary TGC), `num_tests`
8
+ * (per-requirement total) and the `passes`/`failures` lists. Score =
9
+ * passes / num_tests — GRADED; resolved = success. Fully deterministic — no LLM judge.
10
+ *
11
+ * loadTasks enumerates the real task suite via `load_task_ids(split)`
12
+ * (train|dev|test_normal|test_challenge); the prompt = `world.task.instruction`.
13
+ * The OutputAdapter is stream-only, so the worker emits its solution as a fenced
14
+ * ```python block which the driver executes.
15
+ *
16
+ * Requires for a live run: the bench `.venv` with `appworld` installed + the
17
+ * unpacked engine + downloaded data (`appworld install` ; `appworld download
18
+ * data`). preflight + loadTasks + judge all fail loud with the exact step when the
19
+ * engine/data is absent — never a fabricated score.
20
+ */
21
+
22
+ import { spawn } from 'node:child_process'
23
+ import { join } from 'node:path'
24
+ import { createInterface } from 'node:readline'
25
+ import { type OutputAdapter, routerToolLoop, type ToolSpec } from '@tangle-network/agent-runtime/loops'
26
+ import { benchRoot, preflightVenvImports, runVenvScriptStdin, venvPython } from './_harness'
27
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
28
+
29
+ const DRIVER = join(benchRoot, 'scripts', 'appworld_driver.py')
30
+
31
+ /** AppWorld splits; only the test splits ship evaluation-only (no setup/solution). */
32
+ const DEFAULT_SPLIT = 'test_normal'
33
+
34
+ interface AppWorldMeta {
35
+ taskId: string
36
+ split: string
37
+ }
38
+
39
+ /** Worker solution code = the last fenced ```python block, else the raw text. */
40
+ export const appworldSolutionOutput: OutputAdapter<string> = {
41
+ parse(events) {
42
+ let text = ''
43
+ for (const ev of events) {
44
+ const d = (ev as { data?: Record<string, unknown> })?.data
45
+ const t = d?.finalText ?? d?.text ?? d?.result
46
+ if (typeof t === 'string' && t.length > 0) text = t
47
+ }
48
+ const fences = [...text.matchAll(/```(?:python|py)?\s*\n([\s\S]*?)```/g)]
49
+ return (fences.at(-1)?.[1] ?? text).trim()
50
+ },
51
+ }
52
+
53
+ const WORKER_CONTRACT = [
54
+ '',
55
+ 'Solve this by writing Python that calls the available app APIs (the `apis.<app>.<function>(...)` surface). You may inspect API docs with `apis.api_docs.show_api_descriptions(app_name=...)` and `apis.api_docs.show_api_doc(app_name=..., api_name=...)`.',
56
+ 'Authenticate where needed via the supervisor-provided credentials, perform every step the task requires, and FINISH with `apis.supervisor.complete_task()`.',
57
+ 'Emit your COMPLETE solution as the LAST thing in your reply, in a single fenced ```python block. Nothing after the closing fence.',
58
+ ].join('\n')
59
+
60
+ function readMeta(task: BenchTask): AppWorldMeta {
61
+ const md = task.metadata
62
+ if (!md || typeof md.taskId !== 'string') {
63
+ throw new Error(`appworld task ${task.id} missing metadata.taskId — loadTasks did not populate it`)
64
+ }
65
+ return md as unknown as AppWorldMeta
66
+ }
67
+
68
+ /**
69
+ * Run the appworld engine driver with a subcommand; JSON on the LAST stdout line.
70
+ * The solution code (evaluate) is piped to stdin via the shared stdin-aware runner —
71
+ * execFile's `input` option is not honored async and hangs the driver's
72
+ * sys.stdin.read() forever. `load` ignores stdin, so an empty pipe is harmless.
73
+ */
74
+ async function driver(args: string[], input = ''): Promise<unknown> {
75
+ let stdout: string
76
+ try {
77
+ stdout = await runVenvScriptStdin(DRIVER, args, input, { cwd: benchRoot })
78
+ } catch (err) {
79
+ const e = err as { message?: string }
80
+ throw new Error(`appworld driver failed (${args.join(' ')}): ${(e.message || String(err)).slice(0, 1500)}`)
81
+ }
82
+ const last = stdout.trim().split('\n').at(-1) ?? '{}'
83
+ const parsed = JSON.parse(last) as { error?: string }
84
+ if (parsed.error) throw new Error(`appworld driver error: ${parsed.error}`)
85
+ return parsed
86
+ }
87
+
88
+ export function createAppWorldAdapter(): BenchmarkAdapter {
89
+ return {
90
+ name: 'appworld',
91
+ output: appworldSolutionOutput,
92
+
93
+ async preflight() {
94
+ await preflightVenvImports({
95
+ modules: ['appworld'],
96
+ requireDocker: false,
97
+ fix:
98
+ 'Fix: bench/.venv/bin/pip install appworld ; ' +
99
+ 'bench/.venv/bin/appworld install ; bench/.venv/bin/appworld download data ' +
100
+ '(unpacks the engine + downloads the simulated-app data/tasks). ' +
101
+ 'Set APPWORLD_ROOT to the data root if not the default.',
102
+ })
103
+ },
104
+
105
+ async loadTasks(opts: LoadOptions = {}): Promise<BenchTask[]> {
106
+ const split = opts.split ?? DEFAULT_SPLIT
107
+ const out = (await driver([
108
+ 'load',
109
+ '--split', split,
110
+ ...(opts.limit !== undefined ? ['--limit', String(opts.limit)] : []),
111
+ ...(opts.ids ? ['--ids', opts.ids.join(',')] : []),
112
+ ])) as { tasks?: Array<{ task_id: string; instruction: string }> }
113
+ const tasks = out.tasks ?? []
114
+ if (tasks.length === 0) {
115
+ throw new Error(`appworld loadTasks returned no tasks for split=${split} ${JSON.stringify(opts)}`)
116
+ }
117
+ return tasks.map(
118
+ (t): BenchTask => ({
119
+ id: t.task_id,
120
+ split,
121
+ prompt: t.instruction + WORKER_CONTRACT,
122
+ metadata: { taskId: t.task_id, split } as unknown as Record<string, unknown>,
123
+ }),
124
+ )
125
+ },
126
+
127
+ async goldArtifact() {
128
+ // Reference solution code ships only for train/dev, and only inside the
129
+ // engine's decrypted `.bundle` (it is not a portable string this adapter can
130
+ // emit across splits). The test splits are evaluation-only. So verify-judge
131
+ // here requires a real solve on a train/dev task through the live engine
132
+ // rather than a synthetic gold — returning a fabricated artifact would be a
133
+ // fake. Returns undefined.
134
+ return undefined
135
+ },
136
+
137
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
138
+ const meta = readMeta(task)
139
+ const out = (await driver(['evaluate', '--task-id', meta.taskId, '--split', meta.split], artifact)) as {
140
+ success?: boolean
141
+ passes?: number
142
+ fails?: number
143
+ num_tests?: number
144
+ failure_names?: string[]
145
+ }
146
+ const passes = out.passes ?? 0
147
+ const fails = out.fails ?? 0
148
+ // num_tests is the evaluator's authoritative per-requirement count; prefer it
149
+ // over passes+fails (which can disagree if a requirement neither passed nor
150
+ // failed). Never default the total to a phantom denominator.
151
+ const total = out.num_tests ?? passes + fails
152
+ const score = total > 0 ? passes / total : 0
153
+ // failure_names = WHICH sub-tests failed — the evidence a trace analyst
154
+ // steers on. Carried in `detail` so it reaches the verdict's `notes`.
155
+ const failures = Array.isArray(out.failure_names) ? out.failure_names : []
156
+ return {
157
+ resolved: out.success === true,
158
+ score,
159
+ detail: JSON.stringify({
160
+ taskId: meta.taskId,
161
+ success: out.success,
162
+ passes,
163
+ fails,
164
+ total,
165
+ ...(failures.length ? { failures } : {}),
166
+ }),
167
+ }
168
+ },
169
+ }
170
+ }
171
+
172
+ /**
173
+ * AppWorld in its NATIVE protocol, run by OUR runtime: the worker is
174
+ * `routerToolLoop` (the runtime's off-box agentic tool loop) with one tool —
175
+ * `execute_python` — bound to a persistent AppWorld world session. The driver's
176
+ * `session` subcommand is a dumb world shim (stdin JSONL: execute → output,
177
+ * evaluate → verdict); every inference turn, the metering, and the typed
178
+ * toolTrace the analyst steers on belong to the runtime, so runtime
179
+ * improvements are what this benchmark measures.
180
+ *
181
+ * The one-shot codegen adapter above plays a strictly harder game (no execution
182
+ * feedback — the first wrong API call kills the whole program at judge time),
183
+ * which flatlines the score against ANY steering; this mode is what the
184
+ * benchmark's published baselines use, where behavior can move sub-tests.
185
+ *
186
+ * Protocol: the round task string is `@appworld-react <taskId> <split>` on
187
+ * line 1; everything after line 1 is the steer (an analyst correction, a push
188
+ * directive) appended to the system prompt — so the existing arms steer this
189
+ * worker without modification. The artifact is the episode evaluation JSON
190
+ * (AppWorld's evaluator ran in-world); judge() parses it, never re-executes.
191
+ */
192
+
193
+ interface ReactResult {
194
+ success?: boolean
195
+ passes?: number
196
+ fails?: number
197
+ num_tests?: number
198
+ failure_names?: string[]
199
+ turns?: number
200
+ input_tokens?: number
201
+ output_tokens?: number
202
+ transcript?: string
203
+ }
204
+
205
+ const REACT_HEADER = /^@appworld-react (\S+) (\S+)\n?/
206
+
207
+ const SESSION_SYSTEM = [
208
+ 'You are completing a task in AppWorld, a simulated multi-app environment.',
209
+ 'Use the execute_python tool to run Python that calls the app APIs (the `apis.<app>.<function>(...)` surface).',
210
+ 'Inspect API docs with `apis.api_docs.show_api_descriptions(app_name=...)` and `apis.api_docs.show_api_doc(app_name=..., api_name=...)`.',
211
+ 'Authenticate where needed via the supervisor-provided credentials (`apis.supervisor.show_account_passwords()`).',
212
+ 'Work incrementally: small snippets, read each output, correct course.',
213
+ 'When every step of the task is done, run `apis.supervisor.complete_task()` and then reply WITHOUT calling the tool again.',
214
+ ].join('\n')
215
+
216
+ const EXECUTE_TOOL: ToolSpec = {
217
+ type: 'function',
218
+ function: {
219
+ name: 'execute_python',
220
+ description:
221
+ 'Execute a Python snippet in the persistent AppWorld world. State persists across calls. Returns the execution output (API results or errors).',
222
+ parameters: {
223
+ type: 'object',
224
+ properties: { code: { type: 'string', description: 'Python code calling apis.<app>.<fn>(...)' } },
225
+ required: ['code'],
226
+ },
227
+ },
228
+ }
229
+
230
+ /** One persistent world session: line-JSONL request/response over the driver. */
231
+ async function withWorldSession<T>(
232
+ taskId: string,
233
+ split: string,
234
+ fn: (call: (cmd: Record<string, unknown>) => Promise<Record<string, unknown>>, instruction: string) => Promise<T>,
235
+ ): Promise<T> {
236
+ const child = spawn(venvPython, [DRIVER, 'session', '--task-id', taskId, '--split', split], {
237
+ cwd: benchRoot,
238
+ })
239
+ const rl = createInterface({ input: child.stdout })
240
+ const pending: Array<(line: string) => void> = []
241
+ const backlog: string[] = []
242
+ rl.on('line', (l) => {
243
+ const next = pending.shift()
244
+ if (next) next(l)
245
+ else backlog.push(l)
246
+ })
247
+ let stderr = ''
248
+ child.stderr.on('data', (c: Buffer) => {
249
+ stderr += c.toString('utf8')
250
+ })
251
+ const nextLine = (timeoutMs: number): Promise<string> =>
252
+ new Promise((resolve, reject) => {
253
+ const fromBacklog = backlog.shift()
254
+ if (fromBacklog !== undefined) return resolve(fromBacklog)
255
+ const t = setTimeout(
256
+ () => reject(new Error(`appworld session: no response in ${timeoutMs}ms; stderr: ${stderr.slice(-400)}`)),
257
+ timeoutMs,
258
+ )
259
+ // One exit listener per await leaks (25-turn episodes blow the listener
260
+ // cap) — remove it on the resolve path.
261
+ const onExit = (code: number | null): void => {
262
+ clearTimeout(t)
263
+ reject(new Error(`appworld session exited (${code}); stderr: ${stderr.slice(-400)}`))
264
+ }
265
+ pending.push((l) => {
266
+ clearTimeout(t)
267
+ child.removeListener('exit', onExit)
268
+ resolve(l)
269
+ })
270
+ child.once('exit', onExit)
271
+ })
272
+ try {
273
+ const ready = JSON.parse(await nextLine(120_000)) as { ready?: boolean; instruction?: string; error?: string }
274
+ if (!ready.ready) throw new Error(`appworld session failed to start: ${ready.error ?? 'no ready line'}`)
275
+ const call = async (cmd: Record<string, unknown>): Promise<Record<string, unknown>> => {
276
+ child.stdin.write(`${JSON.stringify(cmd)}\n`)
277
+ const res = JSON.parse(await nextLine(180_000)) as Record<string, unknown>
278
+ if (typeof res.error === 'string') throw new Error(`appworld session op failed: ${res.error}`)
279
+ return res
280
+ }
281
+ return await fn(call, ready.instruction ?? '')
282
+ } finally {
283
+ child.stdin.end()
284
+ child.kill('SIGTERM')
285
+ }
286
+ }
287
+
288
+ /** SandboxClient whose leaf is OUR routerToolLoop driving a persistent world session. */
289
+ export function appworldToolLoopClient(cfg: {
290
+ model: string
291
+ routerBaseUrl: string
292
+ routerKey: string
293
+ maxTurns?: number
294
+ }): unknown {
295
+ const maxTurns = cfg.maxTurns ?? Number(process.env.REACT_MAX_TURNS ?? 40)
296
+ let seq = 0
297
+ return {
298
+ async create() {
299
+ const id = `appworld-toolloop-${seq++}`
300
+ return {
301
+ id,
302
+ async *streamPrompt(prompt: string) {
303
+ const m = prompt.match(REACT_HEADER)
304
+ if (!m) {
305
+ throw new Error(
306
+ `appworld-react leaf: prompt missing '@appworld-react <taskId> <split>' header — got: ${prompt.slice(0, 120)}`,
307
+ )
308
+ }
309
+ const [, taskId, split] = m
310
+ const directive = prompt.replace(REACT_HEADER, '').trim()
311
+ const out = await withWorldSession(taskId as string, split as string, async (call, instruction) => {
312
+ const system = directive ? `${SESSION_SYSTEM}\n\n${directive}` : SESSION_SYSTEM
313
+ const loop = await routerToolLoop(
314
+ { routerBaseUrl: cfg.routerBaseUrl, routerKey: cfg.routerKey, model: cfg.model },
315
+ system,
316
+ `Task: ${instruction}`,
317
+ [EXECUTE_TOOL],
318
+ async (name, args) => {
319
+ if (name !== 'execute_python') return `error: unknown tool ${name}`
320
+ const res = await call({ op: 'execute', code: String(args.code ?? '') })
321
+ const done = res.task_completed === true
322
+ return `${String(res.output ?? '')}${done ? '\n\n[TASK MARKED COMPLETE — reply with a final summary and do not call the tool again]' : ''}`
323
+ },
324
+ { maxTurns },
325
+ )
326
+ const verdict = (await call({ op: 'evaluate' })) as unknown as ReactResult
327
+ const transcript = loop.toolTrace
328
+ .slice(-3)
329
+ .map((t) => `CODE:\n${t.args.slice(0, 600)}\nOUTPUT:\n${t.result.slice(0, 600)}`)
330
+ .join('\n---\n')
331
+ .slice(0, 1600)
332
+ return {
333
+ ...verdict,
334
+ turns: loop.turns,
335
+ input_tokens: loop.usage.input,
336
+ output_tokens: loop.usage.output,
337
+ transcript,
338
+ } satisfies ReactResult
339
+ })
340
+ // Real usage from the episode — flat llm_call so the kernel meters it.
341
+ if (out.input_tokens || out.output_tokens) {
342
+ yield {
343
+ type: 'llm_call',
344
+ data: { tokensIn: out.input_tokens ?? 0, tokensOut: out.output_tokens ?? 0, model: cfg.model },
345
+ }
346
+ }
347
+ yield { type: 'result', data: { finalText: JSON.stringify(out) } }
348
+ },
349
+ async delete() {},
350
+ }
351
+ },
352
+ }
353
+ }
354
+
355
+ /** Artifact = the episode's evaluation JSON, verbatim (no fence extraction). */
356
+ const reactEpisodeOutput: OutputAdapter<string> = {
357
+ parse(events) {
358
+ let text = ''
359
+ for (const ev of events) {
360
+ const d = (ev as { data?: Record<string, unknown> })?.data
361
+ const t = d?.finalText
362
+ if (typeof t === 'string' && t.length > 0) text = t
363
+ }
364
+ return text
365
+ },
366
+ }
367
+
368
+ export function createAppWorldReactAdapter(): BenchmarkAdapter {
369
+ const base = createAppWorldAdapter()
370
+ return {
371
+ name: 'appworld-react',
372
+ output: reactEpisodeOutput,
373
+ preflight: () => base.preflight(),
374
+
375
+ async loadTasks(opts: LoadOptions = {}): Promise<BenchTask[]> {
376
+ const tasks = await base.loadTasks(opts)
377
+ return tasks.map((t) => {
378
+ const meta = readMeta(t)
379
+ return {
380
+ ...t,
381
+ // Header carries task identity to the leaf; the body (empty at round 0)
382
+ // is the directive slot the arms append their steer into.
383
+ prompt: `@appworld-react ${meta.taskId} ${meta.split}\n`,
384
+ }
385
+ })
386
+ },
387
+
388
+ goldArtifact: () => Promise.resolve(undefined),
389
+
390
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
391
+ const meta = readMeta(task)
392
+ let out: ReactResult
393
+ try {
394
+ out = JSON.parse(artifact) as ReactResult
395
+ } catch {
396
+ throw new Error(
397
+ `appworld-react judge: artifact is not the episode's evaluation JSON (task ${meta.taskId}): ${artifact.slice(0, 200)}`,
398
+ )
399
+ }
400
+ if (typeof out.success !== 'boolean' || typeof out.num_tests !== 'number') {
401
+ throw new Error(
402
+ `appworld-react judge: episode JSON missing success/num_tests (task ${meta.taskId}): ${artifact.slice(0, 200)}`,
403
+ )
404
+ }
405
+ const passes = out.passes ?? 0
406
+ const total = out.num_tests
407
+ const failures = Array.isArray(out.failure_names) ? out.failure_names : []
408
+ return {
409
+ resolved: out.success === true,
410
+ score: total > 0 ? passes / total : 0,
411
+ detail: JSON.stringify({
412
+ taskId: meta.taskId,
413
+ success: out.success,
414
+ passes,
415
+ fails: out.fails ?? 0,
416
+ total,
417
+ turns: out.turns,
418
+ ...(failures.length ? { failures } : {}),
419
+ ...(out.transcript ? { transcriptTail: out.transcript.slice(-800) } : {}),
420
+ }),
421
+ }
422
+ },
423
+
424
+ leafClient: (c) => appworldToolLoopClient(c),
425
+ }
426
+ }