@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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE WHOLE REAL THING, end to end. No mock, no stub.
|
|
3
|
+
*
|
|
4
|
+
* An opencode SUPERVISOR (via the cli-bridge) mounts the coordination MCP over a live Scope and
|
|
5
|
+
* carries the real `supervise` SKILL.md (a file in its cwd's skill dir — loaded natively by the
|
|
6
|
+
* harness, not stapled into a prompt). It authors a worker profile and calls spawn_worker.
|
|
7
|
+
* Each WORKER is an opencode coding session in its OWN cwd that edits files and is graded by a
|
|
8
|
+
* REAL test it must pass (the deployable check = `valid`). The supervisor settles only on a
|
|
9
|
+
* delivered worker. Real models (bridge non-Claude), real check, real driver↔worker transcripts.
|
|
10
|
+
*
|
|
11
|
+
* ROUTER_BASE=http://127.0.0.1:3355/v1 TANGLE_API_KEY=<bridge-bearer> \
|
|
12
|
+
* WORKER_MODEL=opencode/zai-coding-plan/glm-5-turbo npx tsx bench/src/atom-mcp-e2e.mts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from 'node:child_process'
|
|
16
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import {
|
|
21
|
+
type Agent,
|
|
22
|
+
type AgentProfile,
|
|
23
|
+
type AgentSpec,
|
|
24
|
+
contentAddress,
|
|
25
|
+
createExecutorRegistry,
|
|
26
|
+
createSupervisor,
|
|
27
|
+
type Executor,
|
|
28
|
+
type ExecutorResult,
|
|
29
|
+
gitWorkspace,
|
|
30
|
+
InMemoryResultBlobStore,
|
|
31
|
+
InMemorySpawnJournal,
|
|
32
|
+
runInWorkspace,
|
|
33
|
+
type Scope,
|
|
34
|
+
type Workspace,
|
|
35
|
+
} from '../../src/runtime/index'
|
|
36
|
+
import { asAuthoredProfile } from '../../src/runtime/supervise/authoring'
|
|
37
|
+
import { serveCoordinationMcp } from '../../src/runtime/supervise/coordination-mcp'
|
|
38
|
+
|
|
39
|
+
const BRIDGE = (process.env.ROUTER_BASE ?? 'http://127.0.0.1:3355/v1').replace(/\/$/, '')
|
|
40
|
+
const BEARER = process.env.TANGLE_API_KEY ?? ''
|
|
41
|
+
const MODEL = process.env.WORKER_MODEL ?? 'opencode/zai-coding-plan/glm-5-turbo'
|
|
42
|
+
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
43
|
+
const SKILL_MD = readFileSync(join(REPO, 'skills', 'supervise', 'SKILL.md'), 'utf8')
|
|
44
|
+
|
|
45
|
+
const TASK = 'In solution.py, implement add(a, b) so it returns the sum a + b and test_solution.py passes.'
|
|
46
|
+
|
|
47
|
+
/** Seed a bare git repo with the failing task — the SHARED workspace ref every worker clones. */
|
|
48
|
+
function seedWorkspaceRepo(): string {
|
|
49
|
+
const git = (args: string[], cwd?: string): void => {
|
|
50
|
+
execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'user.email=t@t', '-c', 'user.name=t', ...args], {
|
|
51
|
+
cwd,
|
|
52
|
+
stdio: 'pipe',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
const bare = `${mkdtempSync(join(tmpdir(), 'e2e-ws-'))}.git`
|
|
56
|
+
git(['init', '--bare', '-b', 'main', bare])
|
|
57
|
+
const seed = mkdtempSync(join(tmpdir(), 'e2e-seed-'))
|
|
58
|
+
git(['clone', bare, seed])
|
|
59
|
+
writeFileSync(join(seed, 'solution.py'), 'def add(a, b):\n raise NotImplementedError\n')
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(seed, 'test_solution.py'),
|
|
62
|
+
'from solution import add\nassert add(2, 3) == 5\nassert add(-1, 1) == 0\nassert add(0, 0) == 0\nprint("PASS")\n',
|
|
63
|
+
)
|
|
64
|
+
git(['add', '-A'], seed)
|
|
65
|
+
git(['commit', '-m', 'task'], seed)
|
|
66
|
+
git(['push', 'origin', 'main'], seed)
|
|
67
|
+
rmSync(seed, { recursive: true, force: true })
|
|
68
|
+
return bare
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The deployable check: run the test in the worker's cwd. Exit 0 = delivered. No LLM judge. */
|
|
72
|
+
function checkPasses(cwd: string): boolean {
|
|
73
|
+
try {
|
|
74
|
+
execFileSync('python3', ['test_solution.py'], { cwd, stdio: 'pipe', timeout: 30_000 })
|
|
75
|
+
return true
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function bridgeChat(opts: {
|
|
82
|
+
messages: Array<{ role: string; content: string }>
|
|
83
|
+
cwd?: string
|
|
84
|
+
mcpUrl?: string
|
|
85
|
+
}): Promise<string> {
|
|
86
|
+
const r = await fetch(`${BRIDGE}/chat/completions`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { authorization: `Bearer ${BEARER}`, 'content-type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model: MODEL,
|
|
91
|
+
messages: opts.messages,
|
|
92
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
93
|
+
...(opts.mcpUrl ? { mcp: { mcpServers: { coordination: { type: 'http', url: opts.mcpUrl } } } } : {}),
|
|
94
|
+
}),
|
|
95
|
+
})
|
|
96
|
+
if (!r.ok) return `(bridge HTTP ${r.status}: ${(await r.text()).slice(0, 200)})`
|
|
97
|
+
const j = (await r.json()) as { choices?: Array<{ message?: { content?: string } }> }
|
|
98
|
+
return j.choices?.[0]?.message?.content ?? ''
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const transcripts: Array<{ who: string; said: string; delivered?: boolean }> = []
|
|
102
|
+
|
|
103
|
+
/** A WORKER = a real opencode coding session in a clone of the SHARED workspace, graded by the
|
|
104
|
+
* real test; its delivery is committed back so the next worker builds on it (not isolated). */
|
|
105
|
+
function makeWorker(rawProfile: unknown, ws: Workspace, n: number): Agent<unknown, unknown> {
|
|
106
|
+
const p = asAuthoredProfile(rawProfile)
|
|
107
|
+
const name = p?.name ?? `worker-${n}`
|
|
108
|
+
let artifact: ExecutorResult<unknown> | undefined
|
|
109
|
+
const inner: Executor<unknown> = {
|
|
110
|
+
runtime: 'router',
|
|
111
|
+
async execute() {
|
|
112
|
+
const sys = p?.systemPrompt ?? TASK
|
|
113
|
+
const run = await runInWorkspace(
|
|
114
|
+
ws,
|
|
115
|
+
async (cwd) => {
|
|
116
|
+
const said = await bridgeChat({
|
|
117
|
+
messages: [
|
|
118
|
+
{
|
|
119
|
+
role: 'user',
|
|
120
|
+
content: `${sys}\n\nYou are working in the current directory (it already holds prior workers' committed progress). Edit the files so that running \`python3 test_solution.py\` prints PASS. Do it now.`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
cwd,
|
|
124
|
+
})
|
|
125
|
+
const valid = checkPasses(cwd)
|
|
126
|
+
transcripts.push({ who: name, said: said.slice(0, 300), delivered: valid })
|
|
127
|
+
return { valid, value: said.slice(0, 120), message: `${name}: ${valid ? 'delivered' : 'wip'}` }
|
|
128
|
+
},
|
|
129
|
+
{ tmpPrefix: 'e2e-worker-', commitOnInvalid: true },
|
|
130
|
+
)
|
|
131
|
+
const delivered = run.valid
|
|
132
|
+
artifact = {
|
|
133
|
+
outRef: contentAddress(`${name}:${delivered}`),
|
|
134
|
+
out: { worker: name, delivered, rev: run.commit?.ok ? run.commit.rev : undefined, profileSystemPrompt: sys.slice(0, 120) },
|
|
135
|
+
verdict: { valid: delivered, score: delivered ? 1 : 0 },
|
|
136
|
+
spent: { iterations: 1, tokens: { input: 0, output: 0 }, usd: 0, ms: 0 },
|
|
137
|
+
}
|
|
138
|
+
return artifact
|
|
139
|
+
},
|
|
140
|
+
teardown: () => Promise.resolve({ destroyed: true }),
|
|
141
|
+
resultArtifact: () => {
|
|
142
|
+
if (!artifact) throw new Error('worker resultArtifact before execute')
|
|
143
|
+
return artifact
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
const spec: AgentSpec = { profile: { name } as AgentProfile, harness: null, executor: inner }
|
|
147
|
+
return { name, act: async () => '', executorSpec: spec } as Agent<unknown, unknown> & { executorSpec: AgentSpec }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function main(): Promise<void> {
|
|
151
|
+
console.log(`atom-mcp-e2e: model=${MODEL} (real boxes, real MCP, real test, shared workspace)`)
|
|
152
|
+
const bareRef = seedWorkspaceRepo()
|
|
153
|
+
const ws = gitWorkspace({ ref: bareRef })
|
|
154
|
+
const blobs = new InMemoryResultBlobStore()
|
|
155
|
+
let n = 0
|
|
156
|
+
|
|
157
|
+
const root: Agent<unknown, unknown> = {
|
|
158
|
+
name: 'supervisor',
|
|
159
|
+
async act(_t, scope: Scope<unknown>) {
|
|
160
|
+
const mcp = await serveCoordinationMcp({
|
|
161
|
+
scope,
|
|
162
|
+
blobs,
|
|
163
|
+
makeWorkerAgent: (raw) => makeWorker(raw, ws, n++),
|
|
164
|
+
perWorker: { maxIterations: 2, maxTokens: 200_000 },
|
|
165
|
+
})
|
|
166
|
+
// The supervisor's cwd carries the REAL skill file (opencode loads it from the cwd skill dirs).
|
|
167
|
+
const supCwd = mkdtempSync(join(tmpdir(), 'e2e-sup-'))
|
|
168
|
+
for (const d of ['.opencode/skills/supervise', '.claude/skills/supervise']) {
|
|
169
|
+
mkdirSync(join(supCwd, d), { recursive: true })
|
|
170
|
+
writeFileSync(join(supCwd, d, 'SKILL.md'), SKILL_MD)
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
console.error(`[e2e] coordination MCP at ${mcp.url}; supervisor cwd=${supCwd}`)
|
|
174
|
+
const said = await bridgeChat({
|
|
175
|
+
messages: [
|
|
176
|
+
{
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: `${TASK}\n\nYou are a SUPERVISOR. You have the "supervise" skill and a "coordination" MCP with tools spawn_worker, await_event, stop. Do NOT write code yourself. Author a worker profile (a JSON object with name + a rich systemPrompt telling the worker exactly what to implement) and call spawn_worker with it, then await_event, and stop once a worker delivered (valid:true).`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
cwd: supCwd,
|
|
182
|
+
mcpUrl: mcp.url,
|
|
183
|
+
})
|
|
184
|
+
transcripts.push({ who: 'supervisor', said: said.slice(0, 400) })
|
|
185
|
+
const settled = mcp.settled()
|
|
186
|
+
const delivered = settled.filter((w) => w.status === 'done' && w.valid === true)
|
|
187
|
+
console.error(`[e2e] supervisor spawned ${settled.length} worker(s), ${delivered.length} delivered`)
|
|
188
|
+
return delivered[0]?.outRef ? await blobs.get(delivered[0].outRef) : undefined
|
|
189
|
+
} finally {
|
|
190
|
+
await mcp.close()
|
|
191
|
+
rmSync(supCwd, { recursive: true, force: true })
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = await createSupervisor<unknown, unknown>().run(root, TASK, {
|
|
197
|
+
budget: { maxIterations: 100, maxTokens: 2_000_000 },
|
|
198
|
+
runId: 'e2e',
|
|
199
|
+
journal: new InMemorySpawnJournal(),
|
|
200
|
+
blobs,
|
|
201
|
+
executors: createExecutorRegistry(),
|
|
202
|
+
maxDepth: 4,
|
|
203
|
+
now: () => Date.now(),
|
|
204
|
+
})
|
|
205
|
+
rmSync(bareRef, { recursive: true, force: true })
|
|
206
|
+
|
|
207
|
+
console.log('\n── transcripts (real driver↔worker) ──')
|
|
208
|
+
for (const t of transcripts) {
|
|
209
|
+
console.log(`\n[${t.who}${t.delivered === undefined ? '' : t.delivered ? ' · DELIVERED' : ' · failed'}]`)
|
|
210
|
+
console.log(` ${t.said.replace(/\n/g, '\n ')}`)
|
|
211
|
+
}
|
|
212
|
+
console.log('\n── verdict ──')
|
|
213
|
+
console.log(
|
|
214
|
+
result.kind === 'winner'
|
|
215
|
+
? `✅ REAL E2E DELIVERED — supervisor (via MCP + skill) drove an in-box worker that coded + passed the real test. out=${JSON.stringify(result.out)}`
|
|
216
|
+
: `❌ no delivery (result=${result.kind}) — see transcripts above`,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main().catch((e) => {
|
|
221
|
+
console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
|
|
222
|
+
process.exit(1)
|
|
223
|
+
})
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared code-bench harness. The "stage the artifact → run the benchmark's own
|
|
3
|
+
* evaluator in an external process (mkdtemp / execFile / .venv python / Docker)
|
|
4
|
+
* → read its JSON report → { resolved, score }" spine that swe-bench,
|
|
5
|
+
* terminal-bench, commit0, programbench and aec-bench all need. Factored out so
|
|
6
|
+
* the Docker/venv/report-reading logic lives in ONE place instead of being
|
|
7
|
+
* copy-pasted per adapter.
|
|
8
|
+
*
|
|
9
|
+
* It owns NO benchmark policy: each adapter passes the argv for its evaluator
|
|
10
|
+
* and a `parseReport` that maps that evaluator's report JSON → a BenchScore. The
|
|
11
|
+
* harness owns process spawning, temp-dir lifecycle, large-buffer/timeout config
|
|
12
|
+
* and fail-loud diagnostics.
|
|
13
|
+
*
|
|
14
|
+
* Fail-loud: an absent .venv / harness / Docker daemon THROWS from `preflight`
|
|
15
|
+
* (the adapter passes the import line + the exact fix). A staged run that exits
|
|
16
|
+
* nonzero throws with the captured stderr — never a fabricated score.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile, spawn } from 'node:child_process'
|
|
20
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
21
|
+
import { tmpdir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import { promisify } from 'node:util'
|
|
25
|
+
import type { BenchScore } from './types'
|
|
26
|
+
|
|
27
|
+
const execFileAsync = promisify(execFile)
|
|
28
|
+
|
|
29
|
+
/** Repo root for the bench package (…/bench), so `.venv` and `fixtures` resolve. */
|
|
30
|
+
export const benchRoot = fileURLToPath(new URL('../..', import.meta.url))
|
|
31
|
+
/** The bench venv interpreter every python-backed evaluator runs through. */
|
|
32
|
+
export const venvPython = join(benchRoot, '.venv', 'bin', 'python')
|
|
33
|
+
|
|
34
|
+
/** Interpreter for a NAMED isolated venv (e.g. `.venv-commit0`). Benches whose pip
|
|
35
|
+
* deps conflict with the shared `.venv` (commit0 downgrades pydantic/sqlalchemy)
|
|
36
|
+
* get their own venv and pass its python explicitly — keeping the shared one clean. */
|
|
37
|
+
export const venvPythonAt = (venvDir: string): string => join(benchRoot, venvDir, 'bin', 'python')
|
|
38
|
+
/** Report/transcript reads are large; 256 MiB matches the SWE harness budget. */
|
|
39
|
+
export const bigBuffer = 1024 * 1024 * 256
|
|
40
|
+
|
|
41
|
+
/** Path to a named executable inside the bench venv (e.g. `venvBin('tb')`). */
|
|
42
|
+
export function venvBin(name: string): string {
|
|
43
|
+
return join(benchRoot, '.venv', 'bin', name)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run the bench venv python with an inline script (`-c`); return stdout. Throws
|
|
48
|
+
* (with stderr) on a nonzero exit — the loaders rely on this to fail loud rather
|
|
49
|
+
* than parse a partial dump.
|
|
50
|
+
*/
|
|
51
|
+
export async function runVenvPython(
|
|
52
|
+
script: string,
|
|
53
|
+
args: string[] = [],
|
|
54
|
+
timeoutMs = 0,
|
|
55
|
+
python: string = venvPython,
|
|
56
|
+
): Promise<string> {
|
|
57
|
+
const { stdout } = await execFileAsync(python, ['-c', script, ...args], {
|
|
58
|
+
maxBuffer: bigBuffer,
|
|
59
|
+
timeout: timeoutMs,
|
|
60
|
+
})
|
|
61
|
+
return stdout
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Preflight a python-backed harness: import the module(s) and (optionally) ping
|
|
66
|
+
* Docker, all inside the bench venv. On failure THROWS the captured error joined
|
|
67
|
+
* to the adapter's `fix` guidance — the contract every code-bench preflight wants.
|
|
68
|
+
*/
|
|
69
|
+
export async function preflightVenvImports(opts: {
|
|
70
|
+
/** Module names to `import` (e.g. ['swebench']); '' entries are ignored. */
|
|
71
|
+
modules: string[]
|
|
72
|
+
/** Also `docker.from_env().ping()` — true for Docker-backed evaluators. */
|
|
73
|
+
requireDocker?: boolean
|
|
74
|
+
/** Actionable remediation appended to the thrown message. */
|
|
75
|
+
fix: string
|
|
76
|
+
/** Override the interpreter (e.g. an isolated `.venv-commit0`). Default: shared `.venv`. */
|
|
77
|
+
python?: string
|
|
78
|
+
}): Promise<void> {
|
|
79
|
+
const imports = opts.modules.filter((m) => m.length > 0)
|
|
80
|
+
const lines = [...imports.map((m) => `import ${m}`)]
|
|
81
|
+
if (opts.requireDocker) lines.push('import docker', 'docker.from_env().ping()')
|
|
82
|
+
lines.push("print('ok')")
|
|
83
|
+
try {
|
|
84
|
+
await runVenvPython(lines.join('\n'), [], 0, opts.python ?? venvPython)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
87
|
+
throw new Error(`${msg}\n${opts.fix}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run a bench-local python driver script (in the bench venv) while piping
|
|
93
|
+
* `input` to its stdin, returning stdout. The driver's verdict JSON is its LAST
|
|
94
|
+
* stdout line; callers parse that and inspect an `error` field (fail loud).
|
|
95
|
+
*
|
|
96
|
+
* Uses spawn + an explicit `stdin.end(input)` rather than promisify(execFile)'s
|
|
97
|
+
* `input` option, because that option is NOT honored by async execFile — stdin
|
|
98
|
+
* is left open and a driver that does `sys.stdin.read()` blocks forever. The
|
|
99
|
+
* artifact-piping judges (commit0, appworld) MUST go through this.
|
|
100
|
+
*/
|
|
101
|
+
export function runVenvScriptStdin(
|
|
102
|
+
scriptPath: string,
|
|
103
|
+
args: string[],
|
|
104
|
+
input: string,
|
|
105
|
+
opts: { cwd?: string; timeoutMs?: number; python?: string } = {},
|
|
106
|
+
): Promise<string> {
|
|
107
|
+
return new Promise<string>((resolve, reject) => {
|
|
108
|
+
const child = spawn(opts.python ?? venvPython, [scriptPath, ...args], {
|
|
109
|
+
cwd: opts.cwd ?? benchRoot,
|
|
110
|
+
...(opts.timeoutMs ? { timeout: opts.timeoutMs } : {}),
|
|
111
|
+
})
|
|
112
|
+
let stdout = ''
|
|
113
|
+
let stderr = ''
|
|
114
|
+
let bytes = 0
|
|
115
|
+
child.stdout.on('data', (c: Buffer) => {
|
|
116
|
+
bytes += c.length
|
|
117
|
+
if (bytes <= bigBuffer) stdout += c.toString('utf8')
|
|
118
|
+
})
|
|
119
|
+
child.stderr.on('data', (c: Buffer) => {
|
|
120
|
+
stderr += c.toString('utf8')
|
|
121
|
+
})
|
|
122
|
+
child.on('error', (err) => reject(err))
|
|
123
|
+
child.on('close', (code) => {
|
|
124
|
+
if (code === 0) resolve(stdout)
|
|
125
|
+
else reject(new Error((stderr || stdout || `exit ${code}`).slice(0, 1500)))
|
|
126
|
+
})
|
|
127
|
+
child.stdin.end(input)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface StagedRunSpec {
|
|
132
|
+
/** mkdtemp prefix, e.g. 'swebench-' / 'commit0-'. */
|
|
133
|
+
tmpPrefix: string
|
|
134
|
+
/**
|
|
135
|
+
* Write the artifact + any harness inputs into the temp dir. Returns nothing;
|
|
136
|
+
* `argv`/`cwd` consume `dir` to point the evaluator at what was written.
|
|
137
|
+
*/
|
|
138
|
+
stage(dir: string): Promise<void>
|
|
139
|
+
/** The external evaluator to spawn. `bin` defaults to the bench venv python. */
|
|
140
|
+
bin?: string
|
|
141
|
+
/** argv for the evaluator, computed from the temp `dir`. */
|
|
142
|
+
argv(dir: string): string[]
|
|
143
|
+
/** Working directory for the evaluator. Defaults to the temp `dir`. */
|
|
144
|
+
cwd?(dir: string): string
|
|
145
|
+
/** Hard timeout for the evaluator (ms); 0 = none. */
|
|
146
|
+
timeoutMs?: number
|
|
147
|
+
/**
|
|
148
|
+
* Read the evaluator's report(s) out of `dir` and map to a BenchScore. Throws
|
|
149
|
+
* if the expected report is absent/malformed (fail loud — no default score).
|
|
150
|
+
*/
|
|
151
|
+
parseReport(dir: string): Promise<BenchScore>
|
|
152
|
+
/** Keep the temp dir on disk (debugging). Default false → always cleaned up. */
|
|
153
|
+
keepTmp?: boolean
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The shared judge body: mkdtemp → stage → spawn evaluator → parseReport →
|
|
158
|
+
* cleanup. The evaluator's stdout/stderr is surfaced on failure; the temp dir is
|
|
159
|
+
* always removed in `finally` unless `keepTmp`.
|
|
160
|
+
*/
|
|
161
|
+
export async function runStagedJudge(spec: StagedRunSpec): Promise<BenchScore> {
|
|
162
|
+
const dir = await mkdtemp(join(tmpdir(), spec.tmpPrefix))
|
|
163
|
+
try {
|
|
164
|
+
await spec.stage(dir)
|
|
165
|
+
const bin = spec.bin ?? venvPython
|
|
166
|
+
try {
|
|
167
|
+
await execFileAsync(bin, spec.argv(dir), {
|
|
168
|
+
cwd: spec.cwd ? spec.cwd(dir) : dir,
|
|
169
|
+
maxBuffer: bigBuffer,
|
|
170
|
+
...(spec.timeoutMs ? { timeout: spec.timeoutMs } : {}),
|
|
171
|
+
})
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const e = err as { stderr?: string; stdout?: string; message?: string }
|
|
174
|
+
const detail = (e.stderr || e.stdout || e.message || String(err)).slice(0, 2000)
|
|
175
|
+
throw new Error(`${spec.tmpPrefix.replace(/-$/, '')} evaluator failed (${bin} ${spec.argv(dir).join(' ')}):\n${detail}`)
|
|
176
|
+
}
|
|
177
|
+
return await spec.parseReport(dir)
|
|
178
|
+
} finally {
|
|
179
|
+
if (!spec.keepTmp) await rm(dir, { recursive: true, force: true }).catch(() => {})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Read + JSON.parse a report file from a staged run; throws with the path on failure. */
|
|
184
|
+
export async function readJsonReport<T>(path: string): Promise<T> {
|
|
185
|
+
let raw: string
|
|
186
|
+
try {
|
|
187
|
+
raw = await readFile(path, 'utf8')
|
|
188
|
+
} catch (err) {
|
|
189
|
+
throw new Error(`expected report not written: ${path} (${err instanceof Error ? err.message : err})`)
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(raw) as T
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw new Error(`report not valid JSON at ${path}: ${err instanceof Error ? err.message : err}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Write a UTF-8 file into a staged dir (artifact / preds.json / attempt.sh / …). */
|
|
199
|
+
export async function stageFile(path: string, content: string): Promise<void> {
|
|
200
|
+
await writeFile(path, content)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Sanitize an instance id into a filesystem/run-id-safe token. */
|
|
204
|
+
export function safeRunId(prefix: string, id: string): string {
|
|
205
|
+
return `${prefix}-${id}`.replace(/[^a-zA-Z0-9_.-]/g, '_')
|
|
206
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline AEC-Bench adapter test. AEC's judge runs the task's own verify.py with
|
|
3
|
+
* python3 (stdlib only, no Docker, no pip), so the FULL judge is exercised here:
|
|
4
|
+
* gold → score 1, empty → score 0, with per-field partial credit in detail. Run:
|
|
5
|
+
* AEC_FIXTURES=1 npx tsx --test src/benchmarks/aec-bench.test.mts
|
|
6
|
+
*/
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import { createAecBenchAdapter } from './aec-bench'
|
|
10
|
+
|
|
11
|
+
process.env.AEC_FIXTURES = '1'
|
|
12
|
+
|
|
13
|
+
const id = 'electrical/catenary-sag'
|
|
14
|
+
|
|
15
|
+
test('loadTasks (fixtures) yields self-contained tasks with verifier metadata', async () => {
|
|
16
|
+
const a = createAecBenchAdapter()
|
|
17
|
+
const tasks = await a.loadTasks({ ids: [id] })
|
|
18
|
+
assert.equal(tasks.length, 1)
|
|
19
|
+
const t = tasks[0]
|
|
20
|
+
assert.equal(t.id, id)
|
|
21
|
+
assert.equal(t.split, 'electrical')
|
|
22
|
+
assert.ok(t.prompt.length > 0, 'prompt is the instruction.md')
|
|
23
|
+
const md = t.metadata as Record<string, unknown>
|
|
24
|
+
assert.equal(typeof md.verifyPy, 'string')
|
|
25
|
+
assert.ok((md.verifyPy as string).length > 0)
|
|
26
|
+
assert.equal(typeof md.goldenPassMd, 'string')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('loadTasks limit slices the fixture set', async () => {
|
|
30
|
+
const a = createAecBenchAdapter()
|
|
31
|
+
const tasks = await a.loadTasks({ limit: 1 })
|
|
32
|
+
assert.equal(tasks.length, 1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('judge: REAL verify.py — gold resolves with full per-field credit', async () => {
|
|
36
|
+
const a = createAecBenchAdapter()
|
|
37
|
+
const [t] = await a.loadTasks({ ids: [id] })
|
|
38
|
+
const gold = await a.goldArtifact(t)
|
|
39
|
+
assert.equal(typeof gold, 'string')
|
|
40
|
+
const score = await a.judge(t, gold as string)
|
|
41
|
+
assert.equal(score.resolved, true)
|
|
42
|
+
assert.equal(score.score, 1)
|
|
43
|
+
const detail = JSON.parse(score.detail as string) as { fields: Record<string, number> }
|
|
44
|
+
for (const v of Object.values(detail.fields)) assert.equal(v, 1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('judge: REAL verify.py — empty artifact fails closed to 0 (no fabricated score)', async () => {
|
|
48
|
+
const a = createAecBenchAdapter()
|
|
49
|
+
const [t] = await a.loadTasks({ ids: [id] })
|
|
50
|
+
const score = await a.judge(t, '')
|
|
51
|
+
assert.equal(score.resolved, false)
|
|
52
|
+
assert.equal(score.score, 0)
|
|
53
|
+
})
|