@tangle-network/agent-bench 0.1.0 → 0.3.5
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/CHANGELOG.md +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smoke-structural-rollout — the ship gate for the PORTED structuralRollout strategy
|
|
3
|
+
* (src/runtime/structural-rollout.ts): prove the RUNTIME code path works against a REAL
|
|
4
|
+
* model on REAL HumanEval tasks. This runs `runAgentic` with the strategy over a
|
|
5
|
+
* `createVerifierEnvironment` surface — routerToolLoop, the conserved pool, the metered
|
|
6
|
+
* consult channel, `sandboxCheckRunner`, receipts — NOT the bench rig (hev-structural.mts).
|
|
7
|
+
*
|
|
8
|
+
* Honesty split (the rig's Phase A / Phase B, preserved):
|
|
9
|
+
* - The strategy sees ONLY task-visible information. The surface's check is INERT
|
|
10
|
+
* (always 0/1), so no hidden-test signal can reach selection or repair; the visible
|
|
11
|
+
* checks are the strategy's own default CheckSource (model-authored asserts, frozen
|
|
12
|
+
* before sampling), executed by the shipped `sandboxCheckRunner` over a docker
|
|
13
|
+
* `--network=none` exec channel (the thin adapter this script provides).
|
|
14
|
+
* - The task's own hidden check() suite grades every locked candidate HERE, in the
|
|
15
|
+
* script, AFTER the strategy has finished the task (`runChecker`, docker,
|
|
16
|
+
* --network=none). Nothing flows back.
|
|
17
|
+
*
|
|
18
|
+
* Per task we collect: the k per-sample hidden grades, the selected candidate's grade
|
|
19
|
+
* (argmax over samples by the exported `selectBestIndex` — the strategy's own order),
|
|
20
|
+
* the final (post-repair) grade (the receipt marked `selected`), authored-check counts,
|
|
21
|
+
* and the SelectionReceipts (cross-checked against the recorded outcomes).
|
|
22
|
+
*
|
|
23
|
+
* Run (key via dotenvx; never in the shell history):
|
|
24
|
+
* cd ~/company/devops/secrets && dotenvx run -f agent-state.env -- bash -c ' \
|
|
25
|
+
* cd /home/drew/code/agent-runtime-swe && \
|
|
26
|
+
* HUMANEVAL_GZ=/abs/HumanEval.jsonl.gz N=20 OFFSET=0 npx tsx bench/src/smoke-structural-rollout.mts'
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { execFile } from 'node:child_process'
|
|
30
|
+
import { appendFileSync } from 'node:fs'
|
|
31
|
+
import {
|
|
32
|
+
type AgenticRunResult,
|
|
33
|
+
type CheckExecChannel,
|
|
34
|
+
type CheckOutcome,
|
|
35
|
+
type CheckRunner,
|
|
36
|
+
createVerifierEnvironment,
|
|
37
|
+
defaultStructuralRolloutPolicy,
|
|
38
|
+
runAgentic,
|
|
39
|
+
type StructuralRolloutResult,
|
|
40
|
+
sandboxCheckRunner,
|
|
41
|
+
selectBestIndex,
|
|
42
|
+
structuralRollout,
|
|
43
|
+
visibleCheckScore,
|
|
44
|
+
} from '../../src/runtime/index'
|
|
45
|
+
import { basePrompt, type HumanEvalTask, loadHumanEval, runChecker } from './benchmarks/humaneval'
|
|
46
|
+
|
|
47
|
+
function must(name: string): string {
|
|
48
|
+
const v = process.env[name]
|
|
49
|
+
if (!v) throw new Error(`env ${name} is required`)
|
|
50
|
+
return v
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const N = Number(process.env.N ?? 20)
|
|
54
|
+
const OFFSET = Number(process.env.OFFSET ?? 0)
|
|
55
|
+
const MODEL = process.env.MODEL ?? 'meta-llama/Meta-Llama-3-8B-Instruct-Lite'
|
|
56
|
+
const BASE = process.env.ROUTER_BASE ?? 'https://api.together.xyz/v1'
|
|
57
|
+
const TEMP = Number(process.env.TEMPERATURE ?? 0.8)
|
|
58
|
+
const MAX_TOKENS = Number(process.env.MAX_TOKENS ?? 2500)
|
|
59
|
+
const CONCURRENCY = Number(process.env.CONCURRENCY ?? 3)
|
|
60
|
+
const OUT = process.env.OUT // optional JSONL of raw per-task rows
|
|
61
|
+
|
|
62
|
+
const systemPrompt = 'You are an expert Python programmer.'
|
|
63
|
+
const dockerImage = 'python:3.12-slim'
|
|
64
|
+
|
|
65
|
+
// ── The thin adapter: a docker --network=none exec channel for sandboxCheckRunner ────
|
|
66
|
+
// The runner pipes its check program as `printf '%s' '<b64>' | base64 -d | python3 -`;
|
|
67
|
+
// this channel runs that command inside a jailed container. Infra faults (no docker,
|
|
68
|
+
// daemon down) fail loud; a candidate crash/hang is a real outcome (non-zero exit / no
|
|
69
|
+
// sentinel), returned, never thrown. Modeled on bench/src/benchmarks/humaneval.ts.
|
|
70
|
+
let containerSeq = 0
|
|
71
|
+
function dockerExecChannel(): CheckExecChannel {
|
|
72
|
+
return {
|
|
73
|
+
exec(command, options) {
|
|
74
|
+
const timeoutMs = options?.timeoutMs ?? 20000
|
|
75
|
+
const name = `srck-${process.pid}-${containerSeq++}`
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
let settled = false
|
|
78
|
+
const reap = () => execFile('docker', ['rm', '-f', name], () => {})
|
|
79
|
+
const finish = (r: { exitCode: number; stdout: string; stderr: string }) => {
|
|
80
|
+
if (settled) return
|
|
81
|
+
settled = true
|
|
82
|
+
clearTimeout(backstop)
|
|
83
|
+
reap()
|
|
84
|
+
resolve(r)
|
|
85
|
+
}
|
|
86
|
+
const fail = (e: Error) => {
|
|
87
|
+
if (settled) return
|
|
88
|
+
settled = true
|
|
89
|
+
clearTimeout(backstop)
|
|
90
|
+
reap()
|
|
91
|
+
reject(e)
|
|
92
|
+
}
|
|
93
|
+
// execFile's timeout kills the docker CLIENT; a hung container could leave the
|
|
94
|
+
// callback unfired. The backstop guarantees resolution (no sentinel ⇒ the runner
|
|
95
|
+
// scores it crashed) and the named reap kills the stray container.
|
|
96
|
+
const backstop = setTimeout(
|
|
97
|
+
() => finish({ exitCode: 124, stdout: '', stderr: 'timed out (backstop)' }),
|
|
98
|
+
timeoutMs + 3000,
|
|
99
|
+
)
|
|
100
|
+
execFile(
|
|
101
|
+
'docker',
|
|
102
|
+
['run', '--rm', '--name', name, '--network=none', '--cpus=1', '--memory=512m', dockerImage, 'sh', '-c', command],
|
|
103
|
+
{ timeout: timeoutMs, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024 },
|
|
104
|
+
(err, stdout, stderr) => {
|
|
105
|
+
if (err) {
|
|
106
|
+
const e = err as NodeJS.ErrnoException & { code?: number | string }
|
|
107
|
+
if (e.code === 'ENOENT') {
|
|
108
|
+
fail(new Error('docker binary not found on PATH — cannot run visible checks'))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (/cannot connect to the docker daemon|is the docker daemon running|permission denied while trying to connect/i.test(stderr ?? '')) {
|
|
112
|
+
fail(new Error(`docker daemon unreachable: ${(stderr ?? '').slice(0, 200)}`))
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const exitCode = typeof e.code === 'number' ? e.code : 1
|
|
116
|
+
finish({ exitCode, stdout: stdout ?? '', stderr: stderr ?? '' })
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
finish({ exitCode: 0, stdout: stdout ?? '', stderr: stderr ?? '' })
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Recording wrapper: capture each (candidate, outcome) the strategy scores, in order.
|
|
128
|
+
// Delegates verbatim to the shipped runner — the code under test stays the strategy.
|
|
129
|
+
interface ScoredCandidate {
|
|
130
|
+
candidate: string
|
|
131
|
+
outcome: CheckOutcome
|
|
132
|
+
}
|
|
133
|
+
function recordingRunner(inner: CheckRunner, log: ScoredCandidate[]): CheckRunner {
|
|
134
|
+
return {
|
|
135
|
+
async run(candidate, checks, ctx) {
|
|
136
|
+
const outcome = await inner.run(candidate, checks, ctx)
|
|
137
|
+
log.push({ candidate, outcome })
|
|
138
|
+
return outcome
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface TaskRow {
|
|
144
|
+
taskId: string
|
|
145
|
+
error?: string
|
|
146
|
+
officialChecks: number
|
|
147
|
+
authoredChecks: number
|
|
148
|
+
repairStop: string
|
|
149
|
+
shots: number
|
|
150
|
+
sampleHidden: number[]
|
|
151
|
+
blindMean: number
|
|
152
|
+
selectedIdx: number
|
|
153
|
+
selectedHidden: number
|
|
154
|
+
finalIdx: number
|
|
155
|
+
finalHidden: number
|
|
156
|
+
selectedVisible: number
|
|
157
|
+
receipts: Array<{ candidateIndex: number; selected: boolean; score: number; reason: string }>
|
|
158
|
+
tokens: { input: number; output: number }
|
|
159
|
+
usd: number
|
|
160
|
+
ms: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function runTask(t: HumanEvalTask): Promise<TaskRow> {
|
|
164
|
+
const scored: ScoredCandidate[] = []
|
|
165
|
+
const policy = { ...defaultStructuralRolloutPolicy, temperature: TEMP }
|
|
166
|
+
const strategy = structuralRollout({
|
|
167
|
+
policy,
|
|
168
|
+
checkRunner: recordingRunner(sandboxCheckRunner({ box: dockerExecChannel() }), scored),
|
|
169
|
+
})
|
|
170
|
+
// INERT check: the strategy's harness-verified score channel must carry no hidden
|
|
171
|
+
// signal — hidden grading is this script's job, after the rollout locks its artifacts.
|
|
172
|
+
const surface = createVerifierEnvironment({
|
|
173
|
+
name: 'humaneval-inert',
|
|
174
|
+
check: () => ({ passes: 0, total: 1, errored: 0 }),
|
|
175
|
+
})
|
|
176
|
+
const result = (await runAgentic({
|
|
177
|
+
surface,
|
|
178
|
+
task: {
|
|
179
|
+
id: t.taskId,
|
|
180
|
+
systemPrompt,
|
|
181
|
+
userPrompt: basePrompt(t),
|
|
182
|
+
meta: { entryPoint: t.entryPoint },
|
|
183
|
+
},
|
|
184
|
+
routerBaseUrl: BASE,
|
|
185
|
+
routerKey: must('TOGETHER_API_KEY'),
|
|
186
|
+
model: MODEL,
|
|
187
|
+
temperature: TEMP,
|
|
188
|
+
maxTokens: MAX_TOKENS,
|
|
189
|
+
innerTurns: 2,
|
|
190
|
+
strategy,
|
|
191
|
+
// The strategy's documented sizing: k samples + repair rounds + the check-author consult.
|
|
192
|
+
budget: policy.k + policy.repairRounds + 1,
|
|
193
|
+
})) as AgenticRunResult & StructuralRolloutResult
|
|
194
|
+
|
|
195
|
+
// Receipts ↔ recorded outcomes must agree exactly (candidateIndex is the recording
|
|
196
|
+
// order: samples first, then repairs). A mismatch is an adapter or strategy defect.
|
|
197
|
+
if (result.selection.length !== scored.length) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${t.taskId}: ${result.selection.length} receipts vs ${scored.length} scored candidates`,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
for (const r of result.selection) {
|
|
203
|
+
const rec = scored[r.candidateIndex]
|
|
204
|
+
if (!rec || Math.abs(r.score - visibleCheckScore(rec.outcome)) > 1e-9) {
|
|
205
|
+
throw new Error(`${t.taskId}: receipt #${r.candidateIndex} score ${r.score} does not match the recorded outcome`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sampleCount = result.selection.filter((r) => r.reason.startsWith('sample')).length
|
|
210
|
+
const samples = scored.slice(0, sampleCount)
|
|
211
|
+
if (samples.length === 0) throw new Error(`${t.taskId}: no sample candidates settled`)
|
|
212
|
+
|
|
213
|
+
// Hidden grading (script-side, docker --network=none): every distinct candidate once.
|
|
214
|
+
const gradeCache = new Map<string, Promise<number>>()
|
|
215
|
+
const grade = (candidate: string): Promise<number> => {
|
|
216
|
+
let p = gradeCache.get(candidate)
|
|
217
|
+
if (!p) {
|
|
218
|
+
p = runChecker(t, candidate).then((r) => r.pass)
|
|
219
|
+
gradeCache.set(candidate, p)
|
|
220
|
+
}
|
|
221
|
+
return p
|
|
222
|
+
}
|
|
223
|
+
const sampleHidden = await Promise.all(samples.map((s) => grade(s.candidate)))
|
|
224
|
+
const selectedIdx = selectBestIndex(samples.map((s) => s.outcome))
|
|
225
|
+
const selectedHidden = sampleHidden[selectedIdx] as number
|
|
226
|
+
const winner = result.selection.find((r) => r.selected)
|
|
227
|
+
if (!winner) throw new Error(`${t.taskId}: no receipt marked selected`)
|
|
228
|
+
const finalIdx = winner.candidateIndex
|
|
229
|
+
const finalHidden = await grade((scored[finalIdx] as ScoredCandidate).candidate)
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
taskId: t.taskId,
|
|
233
|
+
officialChecks: result.officialChecks,
|
|
234
|
+
authoredChecks: result.authoredChecks,
|
|
235
|
+
repairStop: result.repairStop,
|
|
236
|
+
shots: result.shots,
|
|
237
|
+
sampleHidden,
|
|
238
|
+
blindMean: sampleHidden.reduce((a, b) => a + b, 0) / sampleHidden.length,
|
|
239
|
+
selectedIdx,
|
|
240
|
+
selectedHidden,
|
|
241
|
+
finalIdx,
|
|
242
|
+
finalHidden,
|
|
243
|
+
selectedVisible: visibleCheckScore((samples[selectedIdx] as ScoredCandidate).outcome),
|
|
244
|
+
receipts: result.selection.map((r) => ({
|
|
245
|
+
candidateIndex: r.candidateIndex,
|
|
246
|
+
selected: r.selected,
|
|
247
|
+
score: r.score,
|
|
248
|
+
reason: r.reason,
|
|
249
|
+
})),
|
|
250
|
+
tokens: result.tokens,
|
|
251
|
+
usd: result.usd,
|
|
252
|
+
ms: result.ms,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function pooled<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
|
|
257
|
+
const out: R[] = new Array(items.length)
|
|
258
|
+
let next = 0
|
|
259
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
260
|
+
while (next < items.length) {
|
|
261
|
+
const i = next
|
|
262
|
+
next += 1
|
|
263
|
+
out[i] = await fn(items[i] as T)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
await Promise.all(workers)
|
|
267
|
+
return out
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let unhandled = 0
|
|
271
|
+
process.on('unhandledRejection', (e) => {
|
|
272
|
+
unhandled += 1
|
|
273
|
+
console.error('UNHANDLED REJECTION:', e)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const pct = (x: number) => `${(100 * x).toFixed(1)}%`
|
|
277
|
+
|
|
278
|
+
async function main(): Promise<void> {
|
|
279
|
+
must('TOGETHER_API_KEY')
|
|
280
|
+
const tasks = await loadHumanEval(N, OFFSET)
|
|
281
|
+
const { k, repairRounds, testgen } = defaultStructuralRolloutPolicy
|
|
282
|
+
console.log(
|
|
283
|
+
`=== structuralRollout RUNTIME smoke · n=${tasks.length} offset=${OFFSET} · k=${k} repairs<=${repairRounds} testgen=${testgen} temp=${TEMP} ===`,
|
|
284
|
+
)
|
|
285
|
+
console.log(` model=${MODEL} base=${BASE} maxTokens=${MAX_TOKENS} task-concurrency=${CONCURRENCY}`)
|
|
286
|
+
console.log(` path: runAgentic → structuralRollout(default policy) → createVerifierEnvironment(inert) → sandboxCheckRunner(docker --network=none)`)
|
|
287
|
+
|
|
288
|
+
const started = Date.now()
|
|
289
|
+
const rows = await pooled(tasks, CONCURRENCY, async (t): Promise<TaskRow> => {
|
|
290
|
+
try {
|
|
291
|
+
const row = await runTask(t)
|
|
292
|
+
const hid = row.sampleHidden.join(' ')
|
|
293
|
+
console.log(
|
|
294
|
+
` ${t.taskId.padEnd(14)} auth=${row.authoredChecks} samples=[${hid}] sel=#${row.selectedIdx}:${row.selectedHidden ? 'PASS' : 'fail'} final=#${row.finalIdx}:${row.finalHidden ? 'PASS' : 'fail'} ${row.repairStop}`,
|
|
295
|
+
)
|
|
296
|
+
if (OUT) appendFileSync(OUT, `${JSON.stringify(row)}\n`)
|
|
297
|
+
return row
|
|
298
|
+
} catch (e) {
|
|
299
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
300
|
+
console.error(` ${t.taskId.padEnd(14)} ERROR: ${msg}`)
|
|
301
|
+
if (OUT) appendFileSync(OUT, `${JSON.stringify({ taskId: t.taskId, error: msg })}\n`)
|
|
302
|
+
return {
|
|
303
|
+
taskId: t.taskId,
|
|
304
|
+
error: msg,
|
|
305
|
+
officialChecks: 0,
|
|
306
|
+
authoredChecks: 0,
|
|
307
|
+
repairStop: 'error',
|
|
308
|
+
shots: 0,
|
|
309
|
+
sampleHidden: [],
|
|
310
|
+
blindMean: 0,
|
|
311
|
+
selectedIdx: -1,
|
|
312
|
+
selectedHidden: 0,
|
|
313
|
+
finalIdx: -1,
|
|
314
|
+
finalHidden: 0,
|
|
315
|
+
selectedVisible: 0,
|
|
316
|
+
receipts: [],
|
|
317
|
+
tokens: { input: 0, output: 0 },
|
|
318
|
+
usd: 0,
|
|
319
|
+
ms: 0,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const ok = rows.filter((r) => !r.error)
|
|
325
|
+
const errored = rows.filter((r) => r.error)
|
|
326
|
+
|
|
327
|
+
console.log('\n── per-task ──')
|
|
328
|
+
console.log(
|
|
329
|
+
'task auth samples(hidden) blind% sel final visSel repairStop shots tok(in/out) ms',
|
|
330
|
+
)
|
|
331
|
+
for (const r of rows) {
|
|
332
|
+
if (r.error) {
|
|
333
|
+
console.log(`${r.taskId.padEnd(15)} ERROR ${r.error.slice(0, 90)}`)
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
console.log(
|
|
337
|
+
[
|
|
338
|
+
r.taskId.padEnd(15),
|
|
339
|
+
String(r.authoredChecks).padEnd(5),
|
|
340
|
+
r.sampleHidden.join(' ').padEnd(16),
|
|
341
|
+
pct(r.blindMean).padEnd(8),
|
|
342
|
+
`#${r.selectedIdx}:${r.selectedHidden ? 'PASS' : 'fail'}`.padEnd(8),
|
|
343
|
+
`#${r.finalIdx}:${r.finalHidden ? 'PASS' : 'fail'}`.padEnd(8),
|
|
344
|
+
r.selectedVisible.toFixed(3).padEnd(7),
|
|
345
|
+
r.repairStop.padEnd(17),
|
|
346
|
+
String(r.shots).padEnd(6),
|
|
347
|
+
`${r.tokens.input}/${r.tokens.output}`.padEnd(15),
|
|
348
|
+
String(r.ms),
|
|
349
|
+
].join(' '),
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const blind = ok.reduce((a, r) => a + r.blindMean, 0) / Math.max(1, ok.length)
|
|
354
|
+
const selected = ok.reduce((a, r) => a + r.selectedHidden, 0) / Math.max(1, ok.length)
|
|
355
|
+
const final = ok.reduce((a, r) => a + r.finalHidden, 0) / Math.max(1, ok.length)
|
|
356
|
+
const sample0 = ok.reduce((a, r) => a + (r.sampleHidden[0] ?? 0), 0) / Math.max(1, ok.length)
|
|
357
|
+
const oracleAtK = ok.reduce((a, r) => a + (r.sampleHidden.some((x) => x === 1) ? 1 : 0), 0) / Math.max(1, ok.length)
|
|
358
|
+
const tokens = ok.reduce((a, r) => ({ input: a.input + r.tokens.input, output: a.output + r.tokens.output }), { input: 0, output: 0 })
|
|
359
|
+
|
|
360
|
+
console.log('\n── summary ──')
|
|
361
|
+
console.log(`tasks: ${ok.length} scored, ${errored.length} errored (of ${rows.length})`)
|
|
362
|
+
console.log(`blind mean-of-k : ${pct(blind)} (sample-0 only: ${pct(sample0)}; oracle@k ceiling: ${pct(oracleAtK)})`)
|
|
363
|
+
console.log(`selected@1 : ${pct(selected)} (lift over blind: ${(100 * (selected - blind)).toFixed(1)}pp)`)
|
|
364
|
+
console.log(`final (repaired): ${pct(final)} (lift over blind: ${(100 * (final - blind)).toFixed(1)}pp)`)
|
|
365
|
+
console.log(`spend: tokens ${tokens.input} in / ${tokens.output} out · wall ${((Date.now() - started) / 1000).toFixed(0)}s`)
|
|
366
|
+
|
|
367
|
+
// ── Acceptance (mechanism, not significance — n is small) ──
|
|
368
|
+
const authoredTasks = ok.filter((r) => r.authoredChecks > 0).length
|
|
369
|
+
const receiptsSane = ok.every((r) => r.receipts.length > 0 && r.receipts.length === r.shots)
|
|
370
|
+
const ordering = final >= selected && selected >= blind
|
|
371
|
+
const liftPp = 100 * (final - blind)
|
|
372
|
+
const rescued = ok.filter((r) => r.finalHidden === 1 && (r.sampleHidden[0] ?? 0) === 0)
|
|
373
|
+
const checks: Array<[string, boolean, string]> = [
|
|
374
|
+
['authored checks on >=17/20 tasks', authoredTasks >= 17, `${authoredTasks}/${rows.length} tasks`],
|
|
375
|
+
['selection receipts present, scores match recorded outcomes', receiptsSane && ok.length > 0, `verified on ${ok.length} tasks (hard-checked per receipt)`],
|
|
376
|
+
['final >= selected >= blind and final-blind >= +5pp', ordering && liftPp >= 5, `blind ${pct(blind)} → selected ${pct(selected)} → final ${pct(final)} (+${liftPp.toFixed(1)}pp)`],
|
|
377
|
+
['>=1 task rescued (final passes, sample 0 fails)', rescued.length >= 1, rescued.map((r) => r.taskId).join(', ') || 'none'],
|
|
378
|
+
['zero crashes / unhandled rejections', errored.length === 0 && unhandled === 0, `${errored.length} task errors, ${unhandled} unhandled rejections`],
|
|
379
|
+
]
|
|
380
|
+
console.log('\n── acceptance ──')
|
|
381
|
+
let allPass = true
|
|
382
|
+
for (const [label, pass, detail] of checks) {
|
|
383
|
+
if (!pass) allPass = false
|
|
384
|
+
console.log(` [${pass ? 'PASS' : 'FAIL'}] ${label} — ${detail}`)
|
|
385
|
+
}
|
|
386
|
+
console.log(allPass ? '\nSMOKE: PASS' : '\nSMOKE: FAIL')
|
|
387
|
+
process.exit(allPass ? 0 : 1)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
main().catch((e) => {
|
|
391
|
+
console.error(e)
|
|
392
|
+
process.exit(1)
|
|
393
|
+
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { mkdirSync, mkdtempSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { isAbsolute, join } from 'node:path'
|
|
6
|
+
import { after, describe, it } from 'node:test'
|
|
7
|
+
import {
|
|
8
|
+
copyPristineGitCheckout,
|
|
9
|
+
firstAvailableSweImageCandidate,
|
|
10
|
+
isInsideJail,
|
|
11
|
+
isTestPath,
|
|
12
|
+
jailPath,
|
|
13
|
+
parseSweImageIdentity,
|
|
14
|
+
parseSweImageCandidates,
|
|
15
|
+
SWE_RUN_TOOL_CONFIG,
|
|
16
|
+
SWE_SEED_PROMPT,
|
|
17
|
+
SWE_SEED_PROMPT_WITH_RUN,
|
|
18
|
+
} from './swe-bench-env'
|
|
19
|
+
import { absoluteSweTempDir } from './swe-temp'
|
|
20
|
+
|
|
21
|
+
const makeRelativeSymlinkRepo = (): { root: string; source: string; destination: string; links: string[] } => {
|
|
22
|
+
const root = mkdtempSync(join(tmpdir(), 'swe-cache-copy-'))
|
|
23
|
+
const source = join(root, 'source')
|
|
24
|
+
const destination = join(root, 'destination')
|
|
25
|
+
const targetDir = join(source, 'docs/_theme/djangodocs/static')
|
|
26
|
+
const linkDir = join(source, 'docs/_theme/djangodocs-epub/static')
|
|
27
|
+
const hooksDir = join(root, 'empty-hooks')
|
|
28
|
+
const links = ['docicons-note.png', 'docicons-philosophy.png', 'docicons-behindscenes.png', 'docicons-warning.png']
|
|
29
|
+
mkdirSync(targetDir, { recursive: true })
|
|
30
|
+
mkdirSync(linkDir, { recursive: true })
|
|
31
|
+
mkdirSync(hooksDir)
|
|
32
|
+
mkdirSync(destination)
|
|
33
|
+
for (const name of links) {
|
|
34
|
+
writeFileSync(join(targetDir, name), `${name}\n`)
|
|
35
|
+
symlinkSync(`../../djangodocs/static/${name}`, join(linkDir, name))
|
|
36
|
+
}
|
|
37
|
+
execFileSync('git', ['-C', source, 'init', '--quiet'])
|
|
38
|
+
execFileSync('git', ['-C', source, 'add', '.'])
|
|
39
|
+
execFileSync('git', [
|
|
40
|
+
'-C',
|
|
41
|
+
source,
|
|
42
|
+
'-c',
|
|
43
|
+
'user.name=SWE Fixture',
|
|
44
|
+
'-c',
|
|
45
|
+
'user.email=swe-fixture@example.invalid',
|
|
46
|
+
'-c',
|
|
47
|
+
`core.hooksPath=${hooksDir}`,
|
|
48
|
+
'-c',
|
|
49
|
+
'commit.gpgsign=false',
|
|
50
|
+
'commit',
|
|
51
|
+
'--quiet',
|
|
52
|
+
'-m',
|
|
53
|
+
'fixture',
|
|
54
|
+
])
|
|
55
|
+
return { root, source, destination, links }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('SWE worker prompts', () => {
|
|
59
|
+
it('keeps the run-capable prompt as the exact baseline prompt plus run guidance', () => {
|
|
60
|
+
assert.equal(SWE_SEED_PROMPT_WITH_RUN.startsWith(`${SWE_SEED_PROMPT} `), true)
|
|
61
|
+
assert.match(SWE_SEED_PROMPT_WITH_RUN, /You ALSO have a run tool/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('exports the exact run-tool settings used by the worker surface', () => {
|
|
65
|
+
assert.ok(SWE_RUN_TOOL_CONFIG.timeoutS > 0)
|
|
66
|
+
assert.ok(SWE_RUN_TOOL_CONFIG.outputLimit > 0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('captures immutable Docker image identity independently of a mutable tag', () => {
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
parseSweImageIdentity(
|
|
72
|
+
JSON.stringify([{ Id: 'sha256:image', RepoDigests: ['repo@sha256:z', 'repo@sha256:a'] }]),
|
|
73
|
+
),
|
|
74
|
+
{ id: 'sha256:image', repoDigests: ['repo@sha256:a', 'repo@sha256:z'] },
|
|
75
|
+
)
|
|
76
|
+
assert.throws(() => parseSweImageIdentity(JSON.stringify([{}])), /no image ID/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('preserves the local fallback namespace when only the unnamespaced image is cached', () => {
|
|
80
|
+
const candidates = parseSweImageCandidates(
|
|
81
|
+
JSON.stringify([
|
|
82
|
+
{ tag: 'swebench/sweb.eval.example:latest', namespace: 'swebench' },
|
|
83
|
+
{ tag: 'sweb.eval.example:latest', namespace: 'none' },
|
|
84
|
+
]),
|
|
85
|
+
)
|
|
86
|
+
const localIdentity = { id: 'sha256:local', repoDigests: [] }
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
firstAvailableSweImageCandidate(candidates, new Map([['sweb.eval.example:latest', localIdentity]])),
|
|
89
|
+
{ candidate: { tag: 'sweb.eval.example:latest', namespace: 'none' }, identity: localIdentity },
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('SWE temporary directory', () => {
|
|
95
|
+
it('stays absolute when model temperature is configured through TEMPERATURE', () => {
|
|
96
|
+
const priorTemp = process.env.TEMP
|
|
97
|
+
const priorTemperature = process.env.TEMPERATURE
|
|
98
|
+
try {
|
|
99
|
+
delete process.env.TEMP
|
|
100
|
+
process.env.TEMPERATURE = '0.8'
|
|
101
|
+
assert.equal(isAbsolute(absoluteSweTempDir()), true)
|
|
102
|
+
|
|
103
|
+
process.env.TEMP = '0.8'
|
|
104
|
+
assert.throws(() => absoluteSweTempDir(), /must be absolute.*TEMPERATURE/)
|
|
105
|
+
} finally {
|
|
106
|
+
if (priorTemp === undefined) delete process.env.TEMP
|
|
107
|
+
else process.env.TEMP = priorTemp
|
|
108
|
+
if (priorTemperature === undefined) delete process.env.TEMPERATURE
|
|
109
|
+
else process.env.TEMPERATURE = priorTemperature
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('SWE clone cache copy', () => {
|
|
115
|
+
it('preserves Django-style relative symlinks and produces a clean worktree', async () => {
|
|
116
|
+
const fixture = makeRelativeSymlinkRepo()
|
|
117
|
+
try {
|
|
118
|
+
await copyPristineGitCheckout(fixture.source, fixture.destination)
|
|
119
|
+
for (const name of fixture.links) {
|
|
120
|
+
const copiedLink = join(fixture.destination, 'docs/_theme/djangodocs-epub/static', name)
|
|
121
|
+
assert.equal(readlinkSync(copiedLink), `../../djangodocs/static/${name}`)
|
|
122
|
+
}
|
|
123
|
+
assert.equal(
|
|
124
|
+
execFileSync('git', ['-C', fixture.destination, 'status', '--porcelain'], { encoding: 'utf8' }),
|
|
125
|
+
'',
|
|
126
|
+
)
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(fixture.root, { recursive: true, force: true })
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('fails closed when a cached copy is not pristine', async () => {
|
|
133
|
+
const fixture = makeRelativeSymlinkRepo()
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(join(fixture.source, 'docs/_theme/djangodocs/static/docicons-note.png'), 'dirty\n')
|
|
136
|
+
await assert.rejects(copyPristineGitCheckout(fixture.source, fixture.destination), /not pristine/)
|
|
137
|
+
} finally {
|
|
138
|
+
rmSync(fixture.root, { recursive: true, force: true })
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('isTestPath', () => {
|
|
144
|
+
it('flags test directories and test-named python files', () => {
|
|
145
|
+
assert.equal(isTestPath('tests/test_models.py'), true)
|
|
146
|
+
assert.equal(isTestPath('pkg/test/helpers.py'), true)
|
|
147
|
+
assert.equal(isTestPath('pkg/tests/helpers.py'), true)
|
|
148
|
+
assert.equal(isTestPath('test_models.py'), true)
|
|
149
|
+
assert.equal(isTestPath('models_test.py'), true)
|
|
150
|
+
assert.equal(isTestPath('conftest.py'), true)
|
|
151
|
+
assert.equal(isTestPath('pkg/conftest.py'), true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('does not flag ordinary source files', () => {
|
|
155
|
+
assert.equal(isTestPath('src/foo.py'), false)
|
|
156
|
+
assert.equal(isTestPath('pkg/models.py'), false)
|
|
157
|
+
// `testing.py` is not a test file by the test_/_test/conftest rules.
|
|
158
|
+
assert.equal(isTestPath('pkg/testing.py'), false)
|
|
159
|
+
// A `latest/` segment must not trip the `tests?/` directory rule.
|
|
160
|
+
assert.equal(isTestPath('latest/foo.py'), false)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('jailPath', () => {
|
|
165
|
+
const root = '/work/repo'
|
|
166
|
+
|
|
167
|
+
it('rejects `..` traversal and absolute paths', () => {
|
|
168
|
+
assert.equal(jailPath(root, '../x'), null)
|
|
169
|
+
assert.equal(jailPath(root, 'a/../../etc/passwd'), null)
|
|
170
|
+
assert.equal(jailPath(root, '/etc/passwd'), null)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('accepts in-repo relative paths and strips a leading `./`', () => {
|
|
174
|
+
assert.equal(jailPath(root, 'src/a.py'), 'src/a.py')
|
|
175
|
+
assert.equal(jailPath(root, './a.py'), 'a.py')
|
|
176
|
+
assert.equal(jailPath(root, 'a.py'), 'a.py')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('isInsideJail (realpath containment)', () => {
|
|
181
|
+
// Mirror the `resolveInJail` closure in `call()`: realpath-resolve a workspace-relative path, then
|
|
182
|
+
// assert containment. Offline — operates on a throwaway temp dir, no git clone, no network.
|
|
183
|
+
const dir = mkdtempSync(join(tmpdir(), 'swe-jail-'))
|
|
184
|
+
const jailRoot = realpathSync(dir)
|
|
185
|
+
after(() => rmSync(dir, { recursive: true, force: true }))
|
|
186
|
+
|
|
187
|
+
it('admits a real file inside the jail', () => {
|
|
188
|
+
const inside = join(dir, 'a.py')
|
|
189
|
+
writeFileSync(inside, 'x = 1\n')
|
|
190
|
+
assert.equal(isInsideJail(jailRoot, realpathSync(inside)), true)
|
|
191
|
+
assert.equal(isInsideJail(jailRoot, jailRoot), true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('rejects reading through a symlink that escapes the jail', () => {
|
|
195
|
+
// A repo could ship `escape -> /etc`; following it must not let the agent read /etc/passwd.
|
|
196
|
+
const link = join(dir, 'escape')
|
|
197
|
+
symlinkSync('/etc', link)
|
|
198
|
+
// `resolveInJail` does `realpathSync(join(ws.dir, relPath))` then this containment check.
|
|
199
|
+
const real = realpathSync(join(dir, 'escape/passwd'))
|
|
200
|
+
assert.equal(real, '/etc/passwd')
|
|
201
|
+
assert.equal(isInsideJail(jailRoot, real), false)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('rejects a sibling dir that shares the jail-root prefix', () => {
|
|
205
|
+
assert.equal(isInsideJail('/tmp/swe-x', '/tmp/swe-x-evil/secret'), false)
|
|
206
|
+
})
|
|
207
|
+
})
|