@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,1260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-session SWE-bench experiment with generation and official scoring split into separate modes.
|
|
3
|
+
*
|
|
4
|
+
* MODE=generate requires EXPERIMENT_ARM=independent-2|persistent-refine-2 and writes only Phase A.
|
|
5
|
+
* The independent arm runs two fresh attempts. The persistent arm runs one attempt and one fresh
|
|
6
|
+
* continuation with attempt one's cumulative patch pre-applied. Both use the same run-capable tools,
|
|
7
|
+
* prompt, temperature, two worker sessions, and later-on-visible-tie selection rule. Per-row hashes
|
|
8
|
+
* bind source, config, reproduction, prompt, tools, task, parent patch, and final patch.
|
|
9
|
+
*
|
|
10
|
+
* MODE=judge-only requires INDEPENDENT_PHASE_A, PERSISTENT_PHASE_A, and a distinct OUT. It validates
|
|
11
|
+
* both complete Phase-A files and every paired fingerprint before the first serialized official score.
|
|
12
|
+
*
|
|
13
|
+
* Generate env: ZAI_API_KEY, REPRO_MANIFEST, IDS, OUT, MODEL, ZAI_BASE, MAX_TOKENS, TEMPERATURE,
|
|
14
|
+
* INNER_TURNS, CONC, REPRO_TIMEOUT, LLM_TIMEOUT_MS, SWE_RUN_TIMEOUT, SWE_RUN_OUTPUT_LIMIT,
|
|
15
|
+
* PRICE_IN, PRICE_OUT. Judge env: INDEPENDENT_PHASE_A, PERSISTENT_PHASE_A, OUT.
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from 'node:child_process'
|
|
18
|
+
import { appendFileSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { join, relative } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
import { promisify } from 'node:util'
|
|
22
|
+
import type { AgenticSurface, AgenticTask, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/loops'
|
|
23
|
+
import { refine, runAgentic } from '@tangle-network/agent-runtime/loops'
|
|
24
|
+
import type { BenchTask } from './benchmarks/types'
|
|
25
|
+
import {
|
|
26
|
+
createSweBenchEnvironment,
|
|
27
|
+
resolveImageForMetadata,
|
|
28
|
+
resolveSweBenchScorerVersion,
|
|
29
|
+
SWE_RUN_TOOL_CONFIG,
|
|
30
|
+
SWE_SEED_PROMPT_WITH_RUN,
|
|
31
|
+
type SweImageIdentity,
|
|
32
|
+
} from './swe-bench-env'
|
|
33
|
+
import {
|
|
34
|
+
APPLY_SENTINEL,
|
|
35
|
+
assertNoHiddenLeak,
|
|
36
|
+
cachedInstanceIds,
|
|
37
|
+
IMPORT_NAME,
|
|
38
|
+
importCanaryScript,
|
|
39
|
+
runPyInJail,
|
|
40
|
+
tail,
|
|
41
|
+
zaiChatRaw,
|
|
42
|
+
} from './swe-jail'
|
|
43
|
+
import {
|
|
44
|
+
type ExperimentArm,
|
|
45
|
+
type ExperimentArmPreset,
|
|
46
|
+
assertExactCompletedWorkerSessions,
|
|
47
|
+
continuationDisposition,
|
|
48
|
+
continuationStateNotice,
|
|
49
|
+
preferLaterCandidate,
|
|
50
|
+
resolveExperimentArm,
|
|
51
|
+
resolveExperimentTemperature,
|
|
52
|
+
shouldAcceptContinuation,
|
|
53
|
+
shouldRunContinuation,
|
|
54
|
+
} from './swe-structural-policy'
|
|
55
|
+
import {
|
|
56
|
+
assertFingerprintsEqual,
|
|
57
|
+
createExecutionReceipt,
|
|
58
|
+
createFingerprints,
|
|
59
|
+
diffChanged,
|
|
60
|
+
diffFingerprint,
|
|
61
|
+
fingerprint,
|
|
62
|
+
runtimeImplementationFingerprint,
|
|
63
|
+
type ExecutionReceipt,
|
|
64
|
+
type Fingerprints,
|
|
65
|
+
type SharedExecutionReceipt,
|
|
66
|
+
} from './swe-structural-provenance'
|
|
67
|
+
import {
|
|
68
|
+
assertCompleteTaskSet,
|
|
69
|
+
assertDistinctArtifactPaths,
|
|
70
|
+
assertJudgeCompletionMatchesInput,
|
|
71
|
+
assertJudgeResumeFingerprints,
|
|
72
|
+
assertPairedExecutionFingerprint,
|
|
73
|
+
assertPairedFingerprints,
|
|
74
|
+
completeJudgeScore,
|
|
75
|
+
} from './swe-structural-judge-policy'
|
|
76
|
+
|
|
77
|
+
const exec = promisify(execFile)
|
|
78
|
+
const TEMPERATURE = resolveExperimentTemperature(process.env)
|
|
79
|
+
|
|
80
|
+
function sourceTreeReceipt(
|
|
81
|
+
rootUrl: URL,
|
|
82
|
+
label: string,
|
|
83
|
+
include: (path: string) => boolean,
|
|
84
|
+
): Array<{ name: string; content: string }> {
|
|
85
|
+
const root = fileURLToPath(rootUrl)
|
|
86
|
+
const receipt: Array<{ name: string; content: string }> = []
|
|
87
|
+
const visit = (dir: string): void => {
|
|
88
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
89
|
+
const path = join(dir, entry.name)
|
|
90
|
+
if (entry.isDirectory()) visit(path)
|
|
91
|
+
else if (entry.isFile() && include(path)) {
|
|
92
|
+
receipt.push({ name: `${label}/${relative(root, path)}`, content: readFileSync(path, 'utf8') })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
visit(root)
|
|
97
|
+
return receipt
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------- config ----------
|
|
101
|
+
|
|
102
|
+
type Mode = 'generate' | 'judge-only'
|
|
103
|
+
const MODE_INPUT = process.env.MODE ?? 'generate'
|
|
104
|
+
if (MODE_INPUT !== 'generate' && MODE_INPUT !== 'judge-only') {
|
|
105
|
+
throw new Error(`MODE must be generate|judge-only, got "${MODE_INPUT}"`)
|
|
106
|
+
}
|
|
107
|
+
const MODE: Mode = MODE_INPUT
|
|
108
|
+
const OFFICIAL_SCORER_CACHE_LEVEL = 'instance' as const
|
|
109
|
+
if (
|
|
110
|
+
MODE === 'judge-only' &&
|
|
111
|
+
process.env.SWEBENCH_CACHE_LEVEL !== undefined &&
|
|
112
|
+
process.env.SWEBENCH_CACHE_LEVEL !== OFFICIAL_SCORER_CACHE_LEVEL
|
|
113
|
+
) {
|
|
114
|
+
throw new Error('MODE=judge-only requires SWEBENCH_CACHE_LEVEL=instance')
|
|
115
|
+
}
|
|
116
|
+
if (MODE === 'judge-only') process.env.SWEBENCH_CACHE_LEVEL = OFFICIAL_SCORER_CACHE_LEVEL
|
|
117
|
+
for (const legacy of ['ARM', 'ARM_NAME', 'K', 'REPAIRS', 'FORCE_TWO_SESSIONS', 'SOLO_TEMP', 'SKIP_JUDGE']) {
|
|
118
|
+
if (process.env[legacy] !== undefined) {
|
|
119
|
+
throw new Error(`${legacy} is not supported; use typed EXPERIMENT_ARM plus MODE=generate|judge-only`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const ARM_PRESET: ExperimentArmPreset | null = MODE === 'generate'
|
|
123
|
+
? resolveExperimentArm(process.env.EXPERIMENT_ARM ?? '')
|
|
124
|
+
: null
|
|
125
|
+
const ZAI_BASE = process.env.ZAI_BASE ?? 'https://api.z.ai/api/coding/paas/v4'
|
|
126
|
+
const ZAI_KEY = process.env.ZAI_API_KEY ?? ''
|
|
127
|
+
if (MODE === 'generate' && !ZAI_KEY) throw new Error('ZAI_API_KEY required for MODE=generate')
|
|
128
|
+
const MODEL = process.env.MODEL ?? 'glm-5.2'
|
|
129
|
+
// glm-5.2 is a reasoning model: hidden reasoning consumes max_tokens, so <8000 starves content.
|
|
130
|
+
const MAX_TOKENS = Number(process.env.MAX_TOKENS ?? 12_000)
|
|
131
|
+
const INNER_TURNS = Number(process.env.INNER_TURNS ?? 40)
|
|
132
|
+
const CONC = Math.max(1, Math.min(4, Number(process.env.CONC ?? 2)))
|
|
133
|
+
const REPRO_TIMEOUT_S = Number(process.env.REPRO_TIMEOUT ?? 120)
|
|
134
|
+
const LLM_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS ?? 480_000)
|
|
135
|
+
// Cost-table rates, USD per Mtok. ASSUMED (zai coding-plan tokens have no per-call list price);
|
|
136
|
+
// override with PRICE_IN/PRICE_OUT. The summary labels them as assumed.
|
|
137
|
+
const PRICE_IN = Number(process.env.PRICE_IN ?? 0.6)
|
|
138
|
+
const PRICE_OUT = Number(process.env.PRICE_OUT ?? 2.2)
|
|
139
|
+
|
|
140
|
+
interface ReproManifestEntry {
|
|
141
|
+
script: string
|
|
142
|
+
source: string
|
|
143
|
+
stage0Class: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const MANIFEST: Record<string, ReproManifestEntry> = (() => {
|
|
147
|
+
if (MODE !== 'generate') return {}
|
|
148
|
+
const p = process.env.REPRO_MANIFEST
|
|
149
|
+
if (!p) throw new Error('REPRO_MANIFEST required for MODE=generate')
|
|
150
|
+
return JSON.parse(readFileSync(p, 'utf8')) as Record<string, ReproManifestEntry>
|
|
151
|
+
})()
|
|
152
|
+
|
|
153
|
+
interface CommonConfigReceipt {
|
|
154
|
+
schema: 'swe-structural-v2'
|
|
155
|
+
model: string
|
|
156
|
+
zaiBase: string
|
|
157
|
+
maxTokens: number
|
|
158
|
+
temperature: number
|
|
159
|
+
innerTurns: number
|
|
160
|
+
concurrency: number
|
|
161
|
+
reproTimeoutS: number
|
|
162
|
+
llmTimeoutMs: number
|
|
163
|
+
sweRunTimeoutS: number
|
|
164
|
+
sweRunOutputLimit: number
|
|
165
|
+
runTool: true
|
|
166
|
+
seedPrompt: 'SWE_SEED_PROMPT_WITH_RUN'
|
|
167
|
+
taskIds: string[]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface ExperimentConfigReceipt extends CommonConfigReceipt {
|
|
171
|
+
arm: ExperimentArm
|
|
172
|
+
k: 1 | 2
|
|
173
|
+
repairs: 0 | 1
|
|
174
|
+
alwaysRunContinuation: boolean
|
|
175
|
+
persistent: boolean
|
|
176
|
+
workerSessions: 2
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const SOURCE_RECEIPT = [
|
|
180
|
+
['swe-structural.mts', new URL('./swe-structural.mts', import.meta.url)],
|
|
181
|
+
['swe-structural-policy.ts', new URL('./swe-structural-policy.ts', import.meta.url)],
|
|
182
|
+
['swe-structural-provenance.ts', new URL('./swe-structural-provenance.ts', import.meta.url)],
|
|
183
|
+
['swe-structural-judge-policy.ts', new URL('./swe-structural-judge-policy.ts', import.meta.url)],
|
|
184
|
+
['swe-bench-env.ts', new URL('./swe-bench-env.ts', import.meta.url)],
|
|
185
|
+
['swe-jail.ts', new URL('./swe-jail.ts', import.meta.url)],
|
|
186
|
+
['swe-temp.ts', new URL('./swe-temp.ts', import.meta.url)],
|
|
187
|
+
['benchmarks/swe-bench.ts', new URL('./benchmarks/swe-bench.ts', import.meta.url)],
|
|
188
|
+
['benchmarks/_harness.ts', new URL('./benchmarks/_harness.ts', import.meta.url)],
|
|
189
|
+
['runtime/strategy.ts', new URL('../../src/runtime/strategy.ts', import.meta.url)],
|
|
190
|
+
].map(([name, url]) => ({ name: String(name), content: readFileSync(url as URL, 'utf8') }))
|
|
191
|
+
|
|
192
|
+
const RUNTIME_IMPLEMENTATION_FINGERPRINT = runtimeImplementationFingerprint({ runAgentic, refine })
|
|
193
|
+
const RUNTIME_TREE_FINGERPRINT = fingerprint([
|
|
194
|
+
...sourceTreeReceipt(new URL('../../src/', import.meta.url), 'agent-runtime/src', (path) => path.endsWith('.ts')),
|
|
195
|
+
...sourceTreeReceipt(
|
|
196
|
+
new URL('../../node_modules/@tangle-network/agent-eval/dist/', import.meta.url),
|
|
197
|
+
'agent-eval/dist',
|
|
198
|
+
(path) => path.endsWith('.js'),
|
|
199
|
+
),
|
|
200
|
+
{
|
|
201
|
+
name: 'agent-runtime/package.json',
|
|
202
|
+
content: readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'agent-runtime/pnpm-lock.yaml',
|
|
206
|
+
content: readFileSync(new URL('../../pnpm-lock.yaml', import.meta.url), 'utf8'),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'agent-eval/package.json',
|
|
210
|
+
content: readFileSync(new URL('../../node_modules/@tangle-network/agent-eval/package.json', import.meta.url), 'utf8'),
|
|
211
|
+
},
|
|
212
|
+
])
|
|
213
|
+
|
|
214
|
+
function makeExperimentConfig(preset: ExperimentArmPreset, taskIds: string[]): ExperimentConfigReceipt {
|
|
215
|
+
return {
|
|
216
|
+
schema: 'swe-structural-v2',
|
|
217
|
+
arm: preset.arm,
|
|
218
|
+
k: preset.k,
|
|
219
|
+
repairs: preset.repairs,
|
|
220
|
+
alwaysRunContinuation: preset.alwaysRunContinuation,
|
|
221
|
+
persistent: preset.persistent,
|
|
222
|
+
workerSessions: preset.workerSessions,
|
|
223
|
+
model: MODEL,
|
|
224
|
+
zaiBase: ZAI_BASE,
|
|
225
|
+
maxTokens: MAX_TOKENS,
|
|
226
|
+
temperature: TEMPERATURE,
|
|
227
|
+
innerTurns: INNER_TURNS,
|
|
228
|
+
concurrency: CONC,
|
|
229
|
+
reproTimeoutS: REPRO_TIMEOUT_S,
|
|
230
|
+
llmTimeoutMs: LLM_TIMEOUT_MS,
|
|
231
|
+
sweRunTimeoutS: SWE_RUN_TOOL_CONFIG.timeoutS,
|
|
232
|
+
sweRunOutputLimit: SWE_RUN_TOOL_CONFIG.outputLimit,
|
|
233
|
+
runTool: true,
|
|
234
|
+
seedPrompt: 'SWE_SEED_PROMPT_WITH_RUN',
|
|
235
|
+
taskIds: [...taskIds],
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function commonConfig(config: ExperimentConfigReceipt): CommonConfigReceipt {
|
|
240
|
+
const {
|
|
241
|
+
arm: _arm,
|
|
242
|
+
k: _k,
|
|
243
|
+
repairs: _repairs,
|
|
244
|
+
alwaysRunContinuation: _alwaysRunContinuation,
|
|
245
|
+
persistent: _persistent,
|
|
246
|
+
workerSessions: _workerSessions,
|
|
247
|
+
...common
|
|
248
|
+
} = config
|
|
249
|
+
return common
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function reproReceipt(entry: ReproManifestEntry | undefined): ReproManifestEntry | null {
|
|
253
|
+
return entry ? { script: entry.script, source: entry.source, stage0Class: entry.stage0Class } : null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function expectedFingerprints(
|
|
257
|
+
bt: BenchTask,
|
|
258
|
+
config: ExperimentConfigReceipt,
|
|
259
|
+
tools: unknown,
|
|
260
|
+
repro: ReproManifestEntry | undefined,
|
|
261
|
+
): Fingerprints {
|
|
262
|
+
return createFingerprints({
|
|
263
|
+
source: SOURCE_RECEIPT,
|
|
264
|
+
config,
|
|
265
|
+
commonConfig: commonConfig(config),
|
|
266
|
+
repro: reproReceipt(repro),
|
|
267
|
+
prompt: SWE_SEED_PROMPT_WITH_RUN,
|
|
268
|
+
tools,
|
|
269
|
+
task: { id: bt.id, prompt: bt.prompt, metadata: bt.metadata ?? null },
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------- transport: zai direct, patient ladder, leak guard at the chokepoint ----------
|
|
274
|
+
|
|
275
|
+
interface Counter {
|
|
276
|
+
workerSessionsStarted: number
|
|
277
|
+
workerSessionsCompleted: number
|
|
278
|
+
calls: number
|
|
279
|
+
httpAttempts: number
|
|
280
|
+
tokensIn: number
|
|
281
|
+
tokensOut: number
|
|
282
|
+
guardedMsgs: number
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const newCounter = (): Counter => ({
|
|
286
|
+
workerSessionsStarted: 0,
|
|
287
|
+
workerSessionsCompleted: 0,
|
|
288
|
+
calls: 0,
|
|
289
|
+
httpAttempts: 0,
|
|
290
|
+
tokensIn: 0,
|
|
291
|
+
tokensOut: 0,
|
|
292
|
+
guardedMsgs: 0,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
/** Distinctive content marks for the leak guard: the first substantive ADDED line of the gold patch
|
|
296
|
+
* and of the hidden test patch. Never shown to any model; used only to refuse outbound messages. */
|
|
297
|
+
function leakMarks(md: Record<string, string>): string[] {
|
|
298
|
+
const marks: string[] = []
|
|
299
|
+
for (const src of [md.patch, md.test_patch]) {
|
|
300
|
+
const m = String(src ?? '')
|
|
301
|
+
.split('\n')
|
|
302
|
+
.find((l) => l.startsWith('+') && !l.startsWith('+++') && l.trim().length > 12)
|
|
303
|
+
?.slice(0, 80)
|
|
304
|
+
if (m) marks.push(m)
|
|
305
|
+
}
|
|
306
|
+
return marks
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Every model call in this driver flows through here (runAgentic's `complete` seam), so the judge
|
|
310
|
+
* separation is asserted once, at the single chokepoint, for every arm and every round. */
|
|
311
|
+
const makeTransport =
|
|
312
|
+
(marks: string[], counter: Counter) =>
|
|
313
|
+
async (body: Record<string, unknown>): Promise<unknown> => {
|
|
314
|
+
const msgs = (body.messages ?? []) as Array<{ role?: string; content?: unknown }>
|
|
315
|
+
counter.guardedMsgs += assertNoHiddenLeak(marks, msgs)
|
|
316
|
+
const { json, attempts } = await zaiChatRaw({ base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS }, body)
|
|
317
|
+
counter.calls += 1
|
|
318
|
+
counter.httpAttempts += attempts
|
|
319
|
+
const u = (json as { usage?: { prompt_tokens?: number; completion_tokens?: number } }).usage
|
|
320
|
+
counter.tokensIn += u?.prompt_tokens ?? 0
|
|
321
|
+
counter.tokensOut += u?.completion_tokens ?? 0
|
|
322
|
+
return json
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------- one emit-patch attempt (the swe-emit-patch protocol, with an optional pre-applied
|
|
326
|
+
// base diff for repair rounds) ----------
|
|
327
|
+
|
|
328
|
+
interface AttemptOut {
|
|
329
|
+
diff: string
|
|
330
|
+
completions: number
|
|
331
|
+
tokensIn: number
|
|
332
|
+
tokensOut: number
|
|
333
|
+
calls: number
|
|
334
|
+
wallMs: number
|
|
335
|
+
error?: string
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function emitAttempt(
|
|
339
|
+
environment: AgenticSurface,
|
|
340
|
+
bt: BenchTask,
|
|
341
|
+
cfg: {
|
|
342
|
+
temperature: number
|
|
343
|
+
marks: string[]
|
|
344
|
+
instanceCounter: Counter
|
|
345
|
+
preApply?: string
|
|
346
|
+
promptAppendix?: string
|
|
347
|
+
},
|
|
348
|
+
): Promise<AttemptOut> {
|
|
349
|
+
const t0 = Date.now()
|
|
350
|
+
const counter = newCounter()
|
|
351
|
+
// Capture the patch from inside score() (called during the refine loop, BEFORE the surface closes
|
|
352
|
+
// and rms the checkout). Keep the LATEST non-empty diff — the emit-patch pattern.
|
|
353
|
+
const capture = { patch: '' }
|
|
354
|
+
const proxy: AgenticSurface = {
|
|
355
|
+
...environment,
|
|
356
|
+
async open(t: AgenticTask): Promise<ArtifactHandle> {
|
|
357
|
+
const h = await environment.open(t)
|
|
358
|
+
const pre = cfg.preApply
|
|
359
|
+
if (pre?.trim()) {
|
|
360
|
+
const f = join(h.id, '.swe-preapply.diff')
|
|
361
|
+
writeFileSync(f, pre.endsWith('\n') ? pre : `${pre}\n`)
|
|
362
|
+
try {
|
|
363
|
+
await exec('git', ['-C', h.id, 'apply', '--whitespace=nowarn', f], { timeout: 60_000 })
|
|
364
|
+
} finally {
|
|
365
|
+
rmSync(f, { force: true })
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return h
|
|
369
|
+
},
|
|
370
|
+
async score(_t: AgenticTask, handle: ArtifactHandle): Promise<SurfaceScore> {
|
|
371
|
+
try {
|
|
372
|
+
const d = await exec('git', ['-C', handle.id, 'diff'], { maxBuffer: 40_000_000, timeout: 60_000 })
|
|
373
|
+
if (d.stdout.trim()) capture.patch = d.stdout
|
|
374
|
+
} catch {
|
|
375
|
+
/* workspace gone or git error → keep whatever we already captured */
|
|
376
|
+
}
|
|
377
|
+
return { passes: capture.patch.trim() ? 1 : 0, total: 1, errored: 0 }
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
const task: AgenticTask = {
|
|
381
|
+
id: bt.id,
|
|
382
|
+
systemPrompt: SWE_SEED_PROMPT_WITH_RUN,
|
|
383
|
+
userPrompt: cfg.promptAppendix ? `${bt.prompt}\n\n${cfg.promptAppendix}` : bt.prompt,
|
|
384
|
+
meta: { instanceId: bt.id },
|
|
385
|
+
}
|
|
386
|
+
let error: string | undefined
|
|
387
|
+
try {
|
|
388
|
+
cfg.instanceCounter.workerSessionsStarted += 1
|
|
389
|
+
const r = await runAgentic({
|
|
390
|
+
surface: proxy,
|
|
391
|
+
task,
|
|
392
|
+
strategy: refine,
|
|
393
|
+
routerBaseUrl: 'zai-direct', // unused: the `complete` transport short-circuits the router
|
|
394
|
+
routerKey: 'zai-direct',
|
|
395
|
+
model: MODEL,
|
|
396
|
+
maxTokens: MAX_TOKENS,
|
|
397
|
+
temperature: cfg.temperature,
|
|
398
|
+
innerTurns: INNER_TURNS,
|
|
399
|
+
budget: 1,
|
|
400
|
+
complete: makeTransport(cfg.marks, counter),
|
|
401
|
+
})
|
|
402
|
+
if (counter.calls < 1) throw new Error('worker session completed without a successful model call')
|
|
403
|
+
cfg.instanceCounter.workerSessionsCompleted += 1
|
|
404
|
+
cfg.instanceCounter.calls += counter.calls
|
|
405
|
+
cfg.instanceCounter.httpAttempts += counter.httpAttempts
|
|
406
|
+
cfg.instanceCounter.tokensIn += counter.tokensIn
|
|
407
|
+
cfg.instanceCounter.tokensOut += counter.tokensOut
|
|
408
|
+
cfg.instanceCounter.guardedMsgs += counter.guardedMsgs
|
|
409
|
+
return {
|
|
410
|
+
diff: capture.patch,
|
|
411
|
+
completions: r.completions,
|
|
412
|
+
tokensIn: counter.tokensIn,
|
|
413
|
+
tokensOut: counter.tokensOut,
|
|
414
|
+
calls: counter.calls,
|
|
415
|
+
wallMs: Date.now() - t0,
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
error = e instanceof Error ? e.message.slice(0, 300) : String(e).slice(0, 300)
|
|
419
|
+
cfg.instanceCounter.calls += counter.calls
|
|
420
|
+
cfg.instanceCounter.httpAttempts += counter.httpAttempts
|
|
421
|
+
cfg.instanceCounter.tokensIn += counter.tokensIn
|
|
422
|
+
cfg.instanceCounter.tokensOut += counter.tokensOut
|
|
423
|
+
cfg.instanceCounter.guardedMsgs += counter.guardedMsgs
|
|
424
|
+
return {
|
|
425
|
+
diff: capture.patch,
|
|
426
|
+
completions: 0,
|
|
427
|
+
tokensIn: counter.tokensIn,
|
|
428
|
+
tokensOut: counter.tokensOut,
|
|
429
|
+
calls: counter.calls,
|
|
430
|
+
wallMs: Date.now() - t0,
|
|
431
|
+
error,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Arm composition deliberately stays outside the built-in sample/refine strategies while each
|
|
437
|
+
// worker session still runs through runAgentic(refine, budget=1). The built-ins cannot reproduce
|
|
438
|
+
// this controlled comparison: sample exposes only aggregate scores and has no stable later-on-tie
|
|
439
|
+
// patch receipt; refine may stop after shot one, adds an analyst call, and carries conversation
|
|
440
|
+
// history. This layer supplies only the missing experiment policy: exactly two fresh worker
|
|
441
|
+
// sessions, optional parent-patch state, shared visible selection, and exact per-session receipts.
|
|
442
|
+
|
|
443
|
+
// ---------- in-image candidate scoring ----------
|
|
444
|
+
|
|
445
|
+
/** Severity ordering for selection (lower is better): 0 repro-pass, 1 repro-fail, 2 timeout,
|
|
446
|
+
* 3 apply-fail, 4 empty. Both experiment arms deterministically prefer session two on a tie. */
|
|
447
|
+
interface CandScore {
|
|
448
|
+
applyOk: boolean | null
|
|
449
|
+
exit: number | null
|
|
450
|
+
timedOut: boolean
|
|
451
|
+
severity: number
|
|
452
|
+
out: string
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const EMPTY_SCORE: CandScore = { applyOk: null, exit: null, timedOut: false, severity: 4, out: '' }
|
|
456
|
+
const UNSCORED: CandScore = { applyOk: null, exit: null, timedOut: false, severity: 1, out: '(no repro signal)' }
|
|
457
|
+
|
|
458
|
+
async function scoreCandidate(imageTag: string, repro: string | null, diff: string): Promise<CandScore> {
|
|
459
|
+
if (!diff.trim()) return EMPTY_SCORE
|
|
460
|
+
if (!repro) return UNSCORED
|
|
461
|
+
const r = await runPyInJail(imageTag, null, repro, diff, { timeoutS: REPRO_TIMEOUT_S })
|
|
462
|
+
if (r.infraError) throw new Error(r.infraError)
|
|
463
|
+
const applyOk = r.out.includes(APPLY_SENTINEL)
|
|
464
|
+
if (!applyOk) return { applyOk, exit: r.code, timedOut: r.timedOut, severity: 3, out: tail(r.out, 800) }
|
|
465
|
+
if (r.timedOut) return { applyOk, exit: r.code, timedOut: true, severity: 2, out: tail(r.out, 800) }
|
|
466
|
+
return { applyOk, exit: r.code, timedOut: false, severity: r.code === 0 ? 0 : 1, out: tail(r.out, 1_500) }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---------- rows ----------
|
|
470
|
+
|
|
471
|
+
interface CandidateRow {
|
|
472
|
+
idx: number
|
|
473
|
+
diff: string
|
|
474
|
+
diffHash: string
|
|
475
|
+
diffBytes: number
|
|
476
|
+
completions: number
|
|
477
|
+
calls: number
|
|
478
|
+
tokensIn: number
|
|
479
|
+
tokensOut: number
|
|
480
|
+
wallMs: number
|
|
481
|
+
attemptError: string | null
|
|
482
|
+
applyOk: boolean | null
|
|
483
|
+
reproExit: number | null
|
|
484
|
+
reproTimedOut: boolean
|
|
485
|
+
severity: number
|
|
486
|
+
reproOutTail: string
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
interface RepairRow {
|
|
490
|
+
round: number
|
|
491
|
+
baseFrom: string
|
|
492
|
+
baseSeverity: number
|
|
493
|
+
parentDiffHash: string
|
|
494
|
+
diff: string
|
|
495
|
+
finalDiffHash: string
|
|
496
|
+
changedFromParent: boolean
|
|
497
|
+
diffBytes: number
|
|
498
|
+
completions: number
|
|
499
|
+
calls: number
|
|
500
|
+
tokensIn: number
|
|
501
|
+
tokensOut: number
|
|
502
|
+
wallMs: number
|
|
503
|
+
attemptError: string | null
|
|
504
|
+
applyOk: boolean | null
|
|
505
|
+
reproExit: number | null
|
|
506
|
+
severity: number
|
|
507
|
+
accepted: boolean
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
interface PhaseARow {
|
|
511
|
+
instanceId: string
|
|
512
|
+
arm: ExperimentArm
|
|
513
|
+
config: ExperimentConfigReceipt
|
|
514
|
+
fingerprints: Fingerprints
|
|
515
|
+
repo: string
|
|
516
|
+
model: string
|
|
517
|
+
image: string | null
|
|
518
|
+
execution: ExecutionReceipt | null
|
|
519
|
+
executionFingerprint: string
|
|
520
|
+
execMode: 'image'
|
|
521
|
+
temperature: number
|
|
522
|
+
innerTurns: number
|
|
523
|
+
k: number
|
|
524
|
+
maxRepairs: number
|
|
525
|
+
// canary + repro provenance (system arm)
|
|
526
|
+
canaryExit: number | null
|
|
527
|
+
canaryPass: boolean | null
|
|
528
|
+
reproSource: string
|
|
529
|
+
reproStage0Class: string | null
|
|
530
|
+
reproStatus: string
|
|
531
|
+
reproScript: string | null
|
|
532
|
+
reproPreExit: number | null
|
|
533
|
+
reproGoldExit: number | null
|
|
534
|
+
reproOutcomeFingerprint: string
|
|
535
|
+
// candidates + selection + continuation receipts
|
|
536
|
+
candidates: CandidateRow[]
|
|
537
|
+
selection: { mode: string; selectedIdx: number; movedOffFirst: boolean } | null
|
|
538
|
+
repairs: RepairRow[]
|
|
539
|
+
repairStop: string | null
|
|
540
|
+
finalFrom: string
|
|
541
|
+
parentDiffHash: string | null
|
|
542
|
+
finalDiff: string
|
|
543
|
+
finalDiffHash: string
|
|
544
|
+
changedFromParent: boolean | null
|
|
545
|
+
// cost + guard receipts
|
|
546
|
+
/** Number of fresh runAgentic worker invocations, including invocations that returned an error. */
|
|
547
|
+
workerSessionsStarted: number
|
|
548
|
+
/** Number of runAgentic invocations that returned successfully after at least one model call. */
|
|
549
|
+
workerSessions: number
|
|
550
|
+
llmCalls: number
|
|
551
|
+
httpAttempts: number
|
|
552
|
+
tokensIn: number
|
|
553
|
+
tokensOut: number
|
|
554
|
+
guardedMsgs: number
|
|
555
|
+
wallMs: number
|
|
556
|
+
error?: string
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ---------- phase A: all arm decisions (no judge anywhere) ----------
|
|
560
|
+
|
|
561
|
+
type Env = Awaited<ReturnType<typeof createSweBenchEnvironment>>
|
|
562
|
+
|
|
563
|
+
interface PhaseAContext {
|
|
564
|
+
preset: ExperimentArmPreset
|
|
565
|
+
config: ExperimentConfigReceipt
|
|
566
|
+
tools: unknown
|
|
567
|
+
sharedExecution: SharedExecutionReceipt
|
|
568
|
+
expectedImageIdentities: Map<string, SweImageIdentity>
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function phaseA(env: Env, bt: BenchTask, ctx: PhaseAContext): Promise<PhaseARow> {
|
|
572
|
+
const t0 = Date.now()
|
|
573
|
+
const md = bt.metadata as Record<string, string>
|
|
574
|
+
const counter = newCounter()
|
|
575
|
+
const row: PhaseARow = {
|
|
576
|
+
instanceId: bt.id, arm: ctx.preset.arm, config: ctx.config,
|
|
577
|
+
fingerprints: expectedFingerprints(bt, ctx.config, ctx.tools, MANIFEST[bt.id]),
|
|
578
|
+
repo: md.repo, model: MODEL, image: null, execution: null, executionFingerprint: '', execMode: 'image',
|
|
579
|
+
temperature: TEMPERATURE, innerTurns: INNER_TURNS,
|
|
580
|
+
k: ctx.preset.k, maxRepairs: ctx.preset.repairs,
|
|
581
|
+
canaryExit: null, canaryPass: null, reproSource: 'none', reproStage0Class: null,
|
|
582
|
+
reproStatus: 'none', reproScript: null,
|
|
583
|
+
reproPreExit: null, reproGoldExit: null, reproOutcomeFingerprint: '',
|
|
584
|
+
candidates: [], selection: null, repairs: [],
|
|
585
|
+
repairStop: null, finalFrom: 'none', parentDiffHash: null, finalDiff: '',
|
|
586
|
+
finalDiffHash: diffFingerprint(''), changedFromParent: null,
|
|
587
|
+
workerSessionsStarted: 0, workerSessions: 0, llmCalls: 0, httpAttempts: 0,
|
|
588
|
+
tokensIn: 0, tokensOut: 0,
|
|
589
|
+
guardedMsgs: 0, wallMs: 0,
|
|
590
|
+
}
|
|
591
|
+
const marks = leakMarks(md)
|
|
592
|
+
try {
|
|
593
|
+
const img = await resolveImageForMetadata(bt.metadata ?? {})
|
|
594
|
+
if (!img.ok) throw new Error(`image missing: ${img.reason}`)
|
|
595
|
+
row.image = img.tag
|
|
596
|
+
row.execution = createExecutionReceipt(ctx.sharedExecution, img)
|
|
597
|
+
row.executionFingerprint = fingerprint(row.execution)
|
|
598
|
+
ctx.expectedImageIdentities.set(bt.id, img.identity)
|
|
599
|
+
|
|
600
|
+
// 1. EXECUTION CANARY (image substrate, gold in-container — script-side only, zero model calls).
|
|
601
|
+
const pkg = IMPORT_NAME[md.repo]
|
|
602
|
+
if (!pkg) throw new Error(`no IMPORT_NAME for ${md.repo} — canary not expressible`)
|
|
603
|
+
const gold = String(md.patch ?? '')
|
|
604
|
+
if (!gold.trim()) throw new Error('gold patch missing from metadata')
|
|
605
|
+
const c = await runPyInJail(img.identity.id, null, importCanaryScript(pkg), gold, { timeoutS: REPRO_TIMEOUT_S })
|
|
606
|
+
if (c.infraError) throw new Error(c.infraError)
|
|
607
|
+
row.canaryExit = c.code
|
|
608
|
+
row.canaryPass = c.code === 0 && c.out.includes(APPLY_SENTINEL)
|
|
609
|
+
if (!row.canaryPass) throw new Error(`canary failed (exit ${c.code}): this substrate cannot grade this instance`)
|
|
610
|
+
|
|
611
|
+
// 2. Repro reuse + re-verification on THIS substrate (validity without gold, soundness with).
|
|
612
|
+
const manifest = MANIFEST[bt.id]
|
|
613
|
+
let repro: string | null = null
|
|
614
|
+
if (manifest) {
|
|
615
|
+
row.reproSource = manifest.source
|
|
616
|
+
row.reproStage0Class = manifest.stage0Class
|
|
617
|
+
row.reproScript = manifest.script
|
|
618
|
+
const pre = await runPyInJail(img.identity.id, null, manifest.script, undefined, { timeoutS: REPRO_TIMEOUT_S })
|
|
619
|
+
if (pre.infraError) throw new Error(pre.infraError)
|
|
620
|
+
row.reproPreExit = pre.code
|
|
621
|
+
if (pre.timedOut) row.reproStatus = 'degraded-timeout'
|
|
622
|
+
else if (pre.code === 0) row.reproStatus = 'degraded-invalid'
|
|
623
|
+
else {
|
|
624
|
+
const post = await runPyInJail(img.identity.id, null, manifest.script, gold, { timeoutS: REPRO_TIMEOUT_S })
|
|
625
|
+
if (post.infraError) throw new Error(post.infraError)
|
|
626
|
+
row.reproGoldExit = post.code
|
|
627
|
+
if (post.code === 0 && post.out.includes(APPLY_SENTINEL)) {
|
|
628
|
+
row.reproStatus = 'ok'
|
|
629
|
+
repro = manifest.script
|
|
630
|
+
} else {
|
|
631
|
+
row.reproStatus = 'degraded-unsound'
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 3. k independent candidates (serial within the instance — CONC instances bound zai concurrency).
|
|
637
|
+
const diffs: string[] = []
|
|
638
|
+
for (let i = 0; i < ctx.preset.k; i += 1) {
|
|
639
|
+
const a = await emitAttempt(env.environment, bt, { temperature: TEMPERATURE, marks, instanceCounter: counter })
|
|
640
|
+
diffs.push(a.diff)
|
|
641
|
+
row.candidates.push({
|
|
642
|
+
idx: i, diff: a.diff, diffHash: diffFingerprint(a.diff), diffBytes: a.diff.length,
|
|
643
|
+
completions: a.completions, calls: a.calls,
|
|
644
|
+
tokensIn: a.tokensIn, tokensOut: a.tokensOut, wallMs: a.wallMs, attemptError: a.error ?? null,
|
|
645
|
+
applyOk: null, reproExit: null, reproTimedOut: false, severity: -1, reproOutTail: '',
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 4. In-image scoring + argmax.
|
|
650
|
+
const scores: CandScore[] = []
|
|
651
|
+
for (let i = 0; i < ctx.preset.k; i += 1) {
|
|
652
|
+
const s = await scoreCandidate(img.identity.id, repro, diffs[i] as string)
|
|
653
|
+
scores.push(s)
|
|
654
|
+
const cand = row.candidates[i] as CandidateRow
|
|
655
|
+
cand.applyOk = s.applyOk
|
|
656
|
+
cand.reproExit = s.exit
|
|
657
|
+
cand.reproTimedOut = s.timedOut
|
|
658
|
+
cand.severity = s.severity
|
|
659
|
+
cand.reproOutTail = s.out
|
|
660
|
+
}
|
|
661
|
+
let selectedIdx = 0
|
|
662
|
+
for (let i = 1; i < ctx.preset.k; i += 1) {
|
|
663
|
+
if (preferLaterCandidate((scores[selectedIdx] as CandScore).severity, (scores[i] as CandScore).severity)) {
|
|
664
|
+
selectedIdx = i
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const mode = repro ? 'visible-severity-later-tie' : 'no-repro-later-tie'
|
|
668
|
+
row.selection = { mode, selectedIdx, movedOffFirst: selectedIdx !== 0 }
|
|
669
|
+
|
|
670
|
+
// 5. The persistent arm starts exactly one continuation from session one's cumulative patch.
|
|
671
|
+
// Both arms use the same later-on-visible-tie policy.
|
|
672
|
+
let best = {
|
|
673
|
+
diff: diffs[selectedIdx] as string,
|
|
674
|
+
score: scores[selectedIdx] as CandScore,
|
|
675
|
+
from: ctx.preset.persistent ? 'session:1' : `attempt:${selectedIdx + 1}`,
|
|
676
|
+
}
|
|
677
|
+
if (!ctx.preset.persistent) {
|
|
678
|
+
row.repairStop = 'independent-arm'
|
|
679
|
+
} else {
|
|
680
|
+
for (let round = 1; shouldRunContinuation({ round, preset: ctx.preset }); round += 1) {
|
|
681
|
+
const parentDiff = best.diff
|
|
682
|
+
const parentDiffHash = diffFingerprint(parentDiff)
|
|
683
|
+
row.parentDiffHash = parentDiffHash
|
|
684
|
+
const reproductionEvidence = repro
|
|
685
|
+
? `--- REPRODUCTION SCRIPT (written from the issue; exit 0 = fixed) ---\n${tail(repro, 6_000)}\n\n` +
|
|
686
|
+
`--- REPRODUCTION OUTPUT on the current state (exit ${best.score.exit ?? 'n/a'}) ---\n${best.score.out}\n\n`
|
|
687
|
+
: '--- EXTERNAL REPRODUCTION ---\nNo external reproduction is available. Use the run tool to construct local, issue-specific checks.\n\n'
|
|
688
|
+
const repairInstruction = best.score.severity === 0
|
|
689
|
+
? 'The visible reproduction PASSES, but it is only a partial check. Re-read the full issue and audit ' +
|
|
690
|
+
'the current patch for missed cases or regressions. Keep the visible check passing while correcting ' +
|
|
691
|
+
'any incomplete source behavior you find with minimal edit_file changes. Do not modify tests.'
|
|
692
|
+
: 'The visible reproduction still fails (or no external reproduction is available). Diagnose why the ' +
|
|
693
|
+
'current state does not resolve the full issue, then correct the SOURCE with minimal edit_file changes ' +
|
|
694
|
+
'(you may revise or revert parts of the previous fix — it is already in the files). Do not modify tests.'
|
|
695
|
+
const appendix =
|
|
696
|
+
continuationStateNotice(best.diff) +
|
|
697
|
+
reproductionEvidence +
|
|
698
|
+
'--- REPAIR INSTRUCTIONS ---\n' +
|
|
699
|
+
repairInstruction
|
|
700
|
+
const a = await emitAttempt(env.environment, bt, {
|
|
701
|
+
temperature: TEMPERATURE, marks, instanceCounter: counter, preApply: best.diff, promptAppendix: appendix,
|
|
702
|
+
})
|
|
703
|
+
const ns = await scoreCandidate(img.identity.id, repro, a.diff)
|
|
704
|
+
const finalDiffHash = diffFingerprint(a.diff)
|
|
705
|
+
const changedFromParent = diffChanged(parentDiff, a.diff)
|
|
706
|
+
const accepted = shouldAcceptContinuation(best.score.severity, ns.severity)
|
|
707
|
+
const disposition = continuationDisposition(accepted, changedFromParent)
|
|
708
|
+
row.repairs.push({
|
|
709
|
+
round, baseFrom: best.from, baseSeverity: best.score.severity, parentDiffHash,
|
|
710
|
+
diff: a.diff, finalDiffHash, changedFromParent, diffBytes: a.diff.length,
|
|
711
|
+
completions: a.completions, calls: a.calls, tokensIn: a.tokensIn, tokensOut: a.tokensOut,
|
|
712
|
+
wallMs: a.wallMs, attemptError: a.error ?? null, applyOk: ns.applyOk, reproExit: ns.exit,
|
|
713
|
+
severity: ns.severity, accepted,
|
|
714
|
+
})
|
|
715
|
+
if (accepted) {
|
|
716
|
+
best = {
|
|
717
|
+
diff: a.diff,
|
|
718
|
+
score: ns,
|
|
719
|
+
from: disposition.finalFrom,
|
|
720
|
+
}
|
|
721
|
+
row.repairStop = disposition.stop
|
|
722
|
+
} else {
|
|
723
|
+
best.from = disposition.finalFrom
|
|
724
|
+
row.repairStop = disposition.stop
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
row.finalDiff = best.diff
|
|
729
|
+
row.finalDiffHash = diffFingerprint(best.diff)
|
|
730
|
+
row.finalFrom = best.from
|
|
731
|
+
row.changedFromParent = row.parentDiffHash === null
|
|
732
|
+
? null
|
|
733
|
+
: row.finalDiffHash !== row.parentDiffHash
|
|
734
|
+
return row
|
|
735
|
+
} catch (e) {
|
|
736
|
+
row.error = e instanceof Error ? e.message.slice(0, 400) : String(e).slice(0, 400)
|
|
737
|
+
return row
|
|
738
|
+
} finally {
|
|
739
|
+
row.reproOutcomeFingerprint = fingerprint({
|
|
740
|
+
canaryExit: row.canaryExit,
|
|
741
|
+
canaryPass: row.canaryPass,
|
|
742
|
+
image: row.execution?.image ?? null,
|
|
743
|
+
reproGoldExit: row.reproGoldExit,
|
|
744
|
+
reproPreExit: row.reproPreExit,
|
|
745
|
+
reproSource: row.reproSource,
|
|
746
|
+
reproStage0Class: row.reproStage0Class,
|
|
747
|
+
reproStatus: row.reproStatus,
|
|
748
|
+
})
|
|
749
|
+
row.workerSessionsStarted = counter.workerSessionsStarted
|
|
750
|
+
row.workerSessions = counter.workerSessionsCompleted
|
|
751
|
+
row.llmCalls = counter.calls
|
|
752
|
+
row.httpAttempts = counter.httpAttempts
|
|
753
|
+
row.tokensIn = counter.tokensIn
|
|
754
|
+
row.tokensOut = counter.tokensOut
|
|
755
|
+
row.guardedMsgs = counter.guardedMsgs
|
|
756
|
+
row.wallMs = Date.now() - t0
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ---------- driver ----------
|
|
761
|
+
|
|
762
|
+
function loadPhaseRows(path: string, required = false): Map<string, PhaseARow> {
|
|
763
|
+
if (!existsSync(path)) {
|
|
764
|
+
if (required) throw new Error(`required Phase-A file does not exist: ${path}`)
|
|
765
|
+
return new Map()
|
|
766
|
+
}
|
|
767
|
+
const rows = new Map<string, PhaseARow>()
|
|
768
|
+
for (const [index, line] of readFileSync(path, 'utf8').split('\n').entries()) {
|
|
769
|
+
if (!line.trim()) continue
|
|
770
|
+
const row = JSON.parse(line) as PhaseARow
|
|
771
|
+
if (!row.instanceId) throw new Error(`${path}:${index + 1}: missing instanceId`)
|
|
772
|
+
if (rows.has(row.instanceId)) throw new Error(`${path}:${index + 1}: duplicate instanceId ${row.instanceId}`)
|
|
773
|
+
rows.set(row.instanceId, row)
|
|
774
|
+
}
|
|
775
|
+
if (required && rows.size === 0) throw new Error(`required Phase-A file is empty: ${path}`)
|
|
776
|
+
return rows
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function manifestFromRow(row: PhaseARow): ReproManifestEntry | undefined {
|
|
780
|
+
if (row.reproScript === null) {
|
|
781
|
+
if (row.reproSource !== 'none' || row.reproStage0Class !== null) {
|
|
782
|
+
throw new Error(`${row.instanceId}: incomplete null-repro receipt`)
|
|
783
|
+
}
|
|
784
|
+
return undefined
|
|
785
|
+
}
|
|
786
|
+
if (row.reproSource === 'none' || row.reproStage0Class === null) {
|
|
787
|
+
throw new Error(`${row.instanceId}: incomplete repro receipt`)
|
|
788
|
+
}
|
|
789
|
+
return { script: row.reproScript, source: row.reproSource, stage0Class: row.reproStage0Class }
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function assertPresetConfig(config: ExperimentConfigReceipt, arm: ExperimentArm, context: string): void {
|
|
793
|
+
const preset = resolveExperimentArm(arm)
|
|
794
|
+
if (
|
|
795
|
+
config.schema !== 'swe-structural-v2' ||
|
|
796
|
+
!config.model ||
|
|
797
|
+
!config.zaiBase ||
|
|
798
|
+
!Number.isFinite(config.maxTokens) ||
|
|
799
|
+
config.maxTokens <= 0 ||
|
|
800
|
+
!Number.isFinite(config.temperature) ||
|
|
801
|
+
!Number.isInteger(config.innerTurns) ||
|
|
802
|
+
config.innerTurns <= 0 ||
|
|
803
|
+
!Number.isInteger(config.concurrency) ||
|
|
804
|
+
config.concurrency < 1 ||
|
|
805
|
+
!Number.isFinite(config.reproTimeoutS) ||
|
|
806
|
+
config.reproTimeoutS <= 0 ||
|
|
807
|
+
!Number.isFinite(config.llmTimeoutMs) ||
|
|
808
|
+
config.llmTimeoutMs <= 0 ||
|
|
809
|
+
config.runTool !== true ||
|
|
810
|
+
config.seedPrompt !== 'SWE_SEED_PROMPT_WITH_RUN' ||
|
|
811
|
+
!Number.isFinite(config.sweRunTimeoutS) ||
|
|
812
|
+
config.sweRunTimeoutS <= 0 ||
|
|
813
|
+
!Number.isFinite(config.sweRunOutputLimit) ||
|
|
814
|
+
config.sweRunOutputLimit <= 0 ||
|
|
815
|
+
!Array.isArray(config.taskIds) ||
|
|
816
|
+
config.taskIds.length === 0
|
|
817
|
+
) {
|
|
818
|
+
throw new Error(`${context}: unsupported config schema or worker surface`)
|
|
819
|
+
}
|
|
820
|
+
for (const key of ['arm', 'k', 'repairs', 'alwaysRunContinuation', 'persistent', 'workerSessions'] as const) {
|
|
821
|
+
if (config[key] !== preset[key]) {
|
|
822
|
+
throw new Error(`${context}: config ${key}=${String(config[key])}, expected ${String(preset[key])}`)
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function assertPhaseRow(
|
|
828
|
+
row: PhaseARow,
|
|
829
|
+
bt: BenchTask,
|
|
830
|
+
config: ExperimentConfigReceipt,
|
|
831
|
+
tools: unknown,
|
|
832
|
+
repro: ReproManifestEntry | undefined,
|
|
833
|
+
sharedExecution: SharedExecutionReceipt,
|
|
834
|
+
context: string,
|
|
835
|
+
): Promise<void> {
|
|
836
|
+
if (row.instanceId !== bt.id) throw new Error(`${context}: instance mismatch ${row.instanceId} != ${bt.id}`)
|
|
837
|
+
if (row.arm !== config.arm) throw new Error(`${context}: arm mismatch ${row.arm} != ${config.arm}`)
|
|
838
|
+
if (!row.config) throw new Error(`${context}: missing config receipt`)
|
|
839
|
+
assertPresetConfig(row.config, row.arm, context)
|
|
840
|
+
if (fingerprint(row.config) !== fingerprint(config)) throw new Error(`${context}: config receipt mismatch`)
|
|
841
|
+
assertFingerprintsEqual(row.fingerprints, expectedFingerprints(bt, config, tools, repro), context)
|
|
842
|
+
if (
|
|
843
|
+
row.model !== config.model ||
|
|
844
|
+
row.temperature !== config.temperature ||
|
|
845
|
+
row.innerTurns !== config.innerTurns ||
|
|
846
|
+
row.k !== config.k ||
|
|
847
|
+
row.maxRepairs !== config.repairs
|
|
848
|
+
) {
|
|
849
|
+
throw new Error(`${context}: row execution fields do not match config receipt`)
|
|
850
|
+
}
|
|
851
|
+
if (row.repo !== String((bt.metadata as Record<string, unknown> | undefined)?.repo ?? '')) {
|
|
852
|
+
throw new Error(`${context}: row repo does not match task metadata`)
|
|
853
|
+
}
|
|
854
|
+
if (row.error) throw new Error(`${context}: Phase A contains an error: ${row.error}`)
|
|
855
|
+
assertExactCompletedWorkerSessions({
|
|
856
|
+
started: row.workerSessionsStarted,
|
|
857
|
+
completed: row.workerSessions,
|
|
858
|
+
sessions: [...row.candidates, ...row.repairs],
|
|
859
|
+
context,
|
|
860
|
+
})
|
|
861
|
+
const currentImage = await resolveImageForMetadata(bt.metadata ?? {})
|
|
862
|
+
if (!currentImage.ok) throw new Error(`${context}: image unavailable during receipt validation: ${currentImage.reason}`)
|
|
863
|
+
const expectedExecution = createExecutionReceipt(sharedExecution, currentImage)
|
|
864
|
+
const actualExecution = row.execution
|
|
865
|
+
if (!actualExecution || fingerprint(actualExecution) !== fingerprint(expectedExecution)) {
|
|
866
|
+
throw new Error(`${context}: execution receipt mismatch`)
|
|
867
|
+
}
|
|
868
|
+
if (
|
|
869
|
+
row.config.sweRunTimeoutS !== actualExecution.runTool.timeoutS ||
|
|
870
|
+
row.config.sweRunOutputLimit !== actualExecution.runTool.outputLimit
|
|
871
|
+
) {
|
|
872
|
+
throw new Error(`${context}: config and execution run-tool settings do not match`)
|
|
873
|
+
}
|
|
874
|
+
if (row.executionFingerprint !== fingerprint(expectedExecution)) {
|
|
875
|
+
throw new Error(`${context}: execution fingerprint mismatch`)
|
|
876
|
+
}
|
|
877
|
+
if (row.image !== expectedExecution.image.tag) throw new Error(`${context}: image tag receipt mismatch`)
|
|
878
|
+
if (row.finalDiffHash !== diffFingerprint(row.finalDiff)) throw new Error(`${context}: final diff hash mismatch`)
|
|
879
|
+
const expectedReproOutcome = fingerprint({
|
|
880
|
+
canaryExit: row.canaryExit,
|
|
881
|
+
canaryPass: row.canaryPass,
|
|
882
|
+
image: actualExecution.image,
|
|
883
|
+
reproGoldExit: row.reproGoldExit,
|
|
884
|
+
reproPreExit: row.reproPreExit,
|
|
885
|
+
reproSource: row.reproSource,
|
|
886
|
+
reproStage0Class: row.reproStage0Class,
|
|
887
|
+
reproStatus: row.reproStatus,
|
|
888
|
+
})
|
|
889
|
+
if (row.reproOutcomeFingerprint !== expectedReproOutcome) {
|
|
890
|
+
throw new Error(`${context}: reproduction outcome fingerprint mismatch`)
|
|
891
|
+
}
|
|
892
|
+
for (const candidate of row.candidates) {
|
|
893
|
+
if (candidate.diffHash !== diffFingerprint(candidate.diff)) {
|
|
894
|
+
throw new Error(`${context}: candidate ${candidate.idx} diff hash mismatch`)
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const preset = resolveExperimentArm(row.arm)
|
|
899
|
+
if (row.candidates.length !== preset.k || row.repairs.length !== preset.repairs) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`${context}: arm shape mismatch (candidates=${row.candidates.length}, repairs=${row.repairs.length})`,
|
|
902
|
+
)
|
|
903
|
+
}
|
|
904
|
+
let selectedIdx = 0
|
|
905
|
+
for (let i = 1; i < row.candidates.length; i += 1) {
|
|
906
|
+
if (preferLaterCandidate(row.candidates[selectedIdx]!.severity, row.candidates[i]!.severity)) selectedIdx = i
|
|
907
|
+
}
|
|
908
|
+
const expectedMode = row.reproStatus === 'ok' ? 'visible-severity-later-tie' : 'no-repro-later-tie'
|
|
909
|
+
if (
|
|
910
|
+
row.selection?.selectedIdx !== selectedIdx ||
|
|
911
|
+
row.selection.mode !== expectedMode ||
|
|
912
|
+
row.selection.movedOffFirst !== (selectedIdx !== 0)
|
|
913
|
+
) {
|
|
914
|
+
throw new Error(`${context}: selectedIdx violates later-on-visible-tie policy`)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (!preset.persistent) {
|
|
918
|
+
if (row.parentDiffHash !== null || row.changedFromParent !== null) {
|
|
919
|
+
throw new Error(`${context}: independent arm must not claim a parent/refinement`)
|
|
920
|
+
}
|
|
921
|
+
if (row.repairStop !== 'independent-arm') throw new Error(`${context}: independent arm stop receipt mismatch`)
|
|
922
|
+
const selected = row.candidates[selectedIdx]!
|
|
923
|
+
if (row.finalDiff !== selected.diff || row.finalFrom !== `attempt:${selectedIdx + 1}`) {
|
|
924
|
+
throw new Error(`${context}: independent final patch does not match selected attempt`)
|
|
925
|
+
}
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const continuation = row.repairs[0]!
|
|
930
|
+
const parent = row.candidates[0]!.diff
|
|
931
|
+
if (continuation.baseFrom !== 'session:1' || continuation.baseSeverity !== row.candidates[0]!.severity) {
|
|
932
|
+
throw new Error(`${context}: continuation parent receipt mismatch`)
|
|
933
|
+
}
|
|
934
|
+
if (continuation.parentDiffHash !== diffFingerprint(parent)) throw new Error(`${context}: parent diff hash mismatch`)
|
|
935
|
+
if (continuation.finalDiffHash !== diffFingerprint(continuation.diff)) throw new Error(`${context}: continuation diff hash mismatch`)
|
|
936
|
+
if (continuation.changedFromParent !== diffChanged(parent, continuation.diff)) {
|
|
937
|
+
throw new Error(`${context}: continuation changedFromParent mismatch`)
|
|
938
|
+
}
|
|
939
|
+
const shouldAccept = shouldAcceptContinuation(continuation.baseSeverity, continuation.severity)
|
|
940
|
+
if (continuation.accepted !== shouldAccept) throw new Error(`${context}: continuation violates shared tie policy`)
|
|
941
|
+
const expectedFinal = continuation.accepted ? continuation.diff : parent
|
|
942
|
+
const disposition = continuationDisposition(continuation.accepted, continuation.changedFromParent)
|
|
943
|
+
if (row.finalDiff !== expectedFinal || row.finalFrom !== disposition.finalFrom || row.repairStop !== disposition.stop) {
|
|
944
|
+
throw new Error(`${context}: persistent final patch/provenance mismatch`)
|
|
945
|
+
}
|
|
946
|
+
if (row.parentDiffHash !== continuation.parentDiffHash) throw new Error(`${context}: row parent diff hash mismatch`)
|
|
947
|
+
if (row.changedFromParent !== (row.finalDiffHash !== row.parentDiffHash)) {
|
|
948
|
+
throw new Error(`${context}: row changedFromParent mismatch`)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async function assertOfficialScoreImagePinned(row: PhaseARow, bt: BenchTask, context: string): Promise<void> {
|
|
953
|
+
const expected = row.execution?.image
|
|
954
|
+
if (!expected) throw new Error(`${context}: missing immutable image receipt`)
|
|
955
|
+
const current = await resolveImageForMetadata(bt.metadata ?? {})
|
|
956
|
+
if (!current.ok) throw new Error(`${context}: image unavailable: ${current.reason}`)
|
|
957
|
+
if (
|
|
958
|
+
current.tag !== expected.tag ||
|
|
959
|
+
current.namespace !== expected.namespace ||
|
|
960
|
+
current.identity.id !== expected.id
|
|
961
|
+
) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`${context}: official-score image changed ` +
|
|
964
|
+
`(${expected.namespace}:${expected.tag}@${expected.id} -> ` +
|
|
965
|
+
`${current.namespace}:${current.tag}@${current.identity.id})`,
|
|
966
|
+
)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function loadTasksAndTools(ids: string[]): Promise<{
|
|
971
|
+
env: Env
|
|
972
|
+
taskById: Map<string, BenchTask>
|
|
973
|
+
tools: unknown
|
|
974
|
+
sharedExecution: SharedExecutionReceipt
|
|
975
|
+
expectedImageIdentities: Map<string, SweImageIdentity>
|
|
976
|
+
}> {
|
|
977
|
+
const expectedImageIdentities = new Map<string, SweImageIdentity>()
|
|
978
|
+
const env = await createSweBenchEnvironment(ids.length, {
|
|
979
|
+
ids,
|
|
980
|
+
cloneCache: true,
|
|
981
|
+
enableRun: true,
|
|
982
|
+
expectedImageIdentities,
|
|
983
|
+
adapterOptions: { cacheLevel: OFFICIAL_SCORER_CACHE_LEVEL },
|
|
984
|
+
})
|
|
985
|
+
await env.adapter.preflight?.()
|
|
986
|
+
const scorerVersion = await resolveSweBenchScorerVersion()
|
|
987
|
+
const taskById = new Map((await env.adapter.loadTasks({ ids, split: 'test' })).map((task) => [task.id, task]))
|
|
988
|
+
const missing = ids.filter((id) => !taskById.has(id))
|
|
989
|
+
if (missing.length) throw new Error(`instances not found in SWE-bench_Verified: ${missing.join(', ')}`)
|
|
990
|
+
const first = taskById.get(ids[0]!)!
|
|
991
|
+
const fingerprintTask: AgenticTask = {
|
|
992
|
+
id: first.id,
|
|
993
|
+
systemPrompt: SWE_SEED_PROMPT_WITH_RUN,
|
|
994
|
+
userPrompt: first.prompt,
|
|
995
|
+
meta: { instanceId: first.id },
|
|
996
|
+
}
|
|
997
|
+
const fingerprintHandle: ArtifactHandle = { id: 'fingerprint-only', surface: 'swe-bench-verified' }
|
|
998
|
+
return {
|
|
999
|
+
env,
|
|
1000
|
+
taskById,
|
|
1001
|
+
tools: await env.environment.tools(fingerprintTask, fingerprintHandle),
|
|
1002
|
+
sharedExecution: {
|
|
1003
|
+
runTool: { ...SWE_RUN_TOOL_CONFIG },
|
|
1004
|
+
runtimeImplementationFingerprint: RUNTIME_IMPLEMENTATION_FINGERPRINT,
|
|
1005
|
+
runtimeTreeFingerprint: RUNTIME_TREE_FINGERPRINT,
|
|
1006
|
+
officialScorer: {
|
|
1007
|
+
package: 'swebench',
|
|
1008
|
+
version: scorerVersion,
|
|
1009
|
+
cacheLevel: OFFICIAL_SCORER_CACHE_LEVEL,
|
|
1010
|
+
namespacePolicy: 'phase-a-image',
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
expectedImageIdentities,
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function generateMain(): Promise<void> {
|
|
1018
|
+
const preset = ARM_PRESET as ExperimentArmPreset
|
|
1019
|
+
const ids = process.env.IDS
|
|
1020
|
+
? process.env.IDS.split(',').map((value) => value.trim()).filter(Boolean)
|
|
1021
|
+
: await cachedInstanceIds()
|
|
1022
|
+
if (!ids.length) throw new Error('no cached sweb.eval images found and no IDS given')
|
|
1023
|
+
if (new Set(ids).size !== ids.length) throw new Error('IDS contains duplicates')
|
|
1024
|
+
const out = process.env.OUT ?? `swe-stage1-${preset.arm}.phaseA.jsonl`
|
|
1025
|
+
const config = makeExperimentConfig(preset, ids)
|
|
1026
|
+
assertPresetConfig(config, preset.arm, 'generate config')
|
|
1027
|
+
const { env, taskById, tools, sharedExecution, expectedImageIdentities } = await loadTasksAndTools(ids)
|
|
1028
|
+
|
|
1029
|
+
console.log(`═══ SWE-bench Phase A — ${preset.arm} ═══`)
|
|
1030
|
+
console.log(
|
|
1031
|
+
`sessions=2 k=${preset.k} repairs=${preset.repairs} persistent=${preset.persistent ? 1 : 0} ` +
|
|
1032
|
+
`model=${MODEL} maxTokens=${MAX_TOKENS} innerTurns=${INNER_TURNS} temperature=${TEMPERATURE} runTool=1 judge=DISABLED`,
|
|
1033
|
+
)
|
|
1034
|
+
console.log(`instances=${ids.length} out=${out}`)
|
|
1035
|
+
|
|
1036
|
+
const done = loadPhaseRows(out)
|
|
1037
|
+
for (const [id, row] of done) {
|
|
1038
|
+
if (!ids.includes(id)) throw new Error(`${out}: resume row ${id} is outside current IDS`)
|
|
1039
|
+
await assertPhaseRow(row, taskById.get(id)!, config, tools, MANIFEST[id], sharedExecution, `${out}:${id}`)
|
|
1040
|
+
}
|
|
1041
|
+
const todo = ids.filter((id) => !done.has(id))
|
|
1042
|
+
if (done.size) console.log(`resume accepted: ${done.size}/${ids.length} fingerprint-matched rows`)
|
|
1043
|
+
|
|
1044
|
+
let next = 0
|
|
1045
|
+
const worker = async (): Promise<void> => {
|
|
1046
|
+
while (next < todo.length) {
|
|
1047
|
+
const index = next++
|
|
1048
|
+
const id = todo[index]!
|
|
1049
|
+
const row = await phaseA(env, taskById.get(id)!, {
|
|
1050
|
+
preset,
|
|
1051
|
+
config,
|
|
1052
|
+
tools,
|
|
1053
|
+
sharedExecution,
|
|
1054
|
+
expectedImageIdentities,
|
|
1055
|
+
})
|
|
1056
|
+
await assertPhaseRow(
|
|
1057
|
+
row,
|
|
1058
|
+
taskById.get(id)!,
|
|
1059
|
+
config,
|
|
1060
|
+
tools,
|
|
1061
|
+
MANIFEST[id],
|
|
1062
|
+
sharedExecution,
|
|
1063
|
+
`${preset.arm}:${id}`,
|
|
1064
|
+
)
|
|
1065
|
+
done.set(id, row)
|
|
1066
|
+
appendFileSync(out, `${JSON.stringify(row)}\n`)
|
|
1067
|
+
console.log(
|
|
1068
|
+
`[${index + 1}/${todo.length}] ${id} sessions=${row.workerSessions} ` +
|
|
1069
|
+
`severity=[${row.candidates.map((candidate) => candidate.severity).join(',')}] ` +
|
|
1070
|
+
`continuation=[${row.repairs.map((repair) => repair.severity).join(',')}] ` +
|
|
1071
|
+
`final=${row.finalFrom} changed=${row.changedFromParent ?? 'n/a'} calls=${row.llmCalls}`,
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
await Promise.all(Array.from({ length: CONC }, () => worker()))
|
|
1076
|
+
|
|
1077
|
+
const rows = ids.map((id) => done.get(id)!)
|
|
1078
|
+
const totalSessions = rows.reduce((sum, row) => sum + row.workerSessions, 0)
|
|
1079
|
+
const totalIn = rows.reduce((sum, row) => sum + row.tokensIn, 0)
|
|
1080
|
+
const totalOut = rows.reduce((sum, row) => sum + row.tokensOut, 0)
|
|
1081
|
+
const usd = (totalIn / 1e6) * PRICE_IN + (totalOut / 1e6) * PRICE_OUT
|
|
1082
|
+
console.log(
|
|
1083
|
+
`Phase A complete: rows=${rows.length}/${ids.length} sessions=${totalSessions}/${2 * ids.length} ` +
|
|
1084
|
+
`tokens=${totalIn}/${totalOut} assumedCost=$${usd.toFixed(2)}; no official scores were called`,
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
interface JudgeRow {
|
|
1089
|
+
schema: 'swe-structural-judge-v2'
|
|
1090
|
+
instanceId: string
|
|
1091
|
+
arm: ExperimentArm
|
|
1092
|
+
pairFingerprint: string
|
|
1093
|
+
phaseFileFingerprint: string
|
|
1094
|
+
inputFingerprint: string
|
|
1095
|
+
executionFingerprint: string
|
|
1096
|
+
finalDiffHash: string
|
|
1097
|
+
hiddenResolved: boolean
|
|
1098
|
+
judgeDetail: string | null
|
|
1099
|
+
judgeMs: number
|
|
1100
|
+
judgeSkipped: 'empty-patch' | null
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function loadJudgeRows(path: string): Map<string, JudgeRow> {
|
|
1104
|
+
if (!existsSync(path)) return new Map()
|
|
1105
|
+
const rows = new Map<string, JudgeRow>()
|
|
1106
|
+
for (const [index, line] of readFileSync(path, 'utf8').split('\n').entries()) {
|
|
1107
|
+
if (!line.trim()) continue
|
|
1108
|
+
const row = JSON.parse(line) as JudgeRow
|
|
1109
|
+
if (row.schema !== 'swe-structural-judge-v2') {
|
|
1110
|
+
throw new Error(`${path}:${index + 1}: unsupported judge row schema`)
|
|
1111
|
+
}
|
|
1112
|
+
if (typeof row.hiddenResolved !== 'boolean') {
|
|
1113
|
+
throw new Error(`${path}:${index + 1}: incomplete judge row must be removed and retried`)
|
|
1114
|
+
}
|
|
1115
|
+
if (row.judgeSkipped !== null && row.judgeSkipped !== 'empty-patch') {
|
|
1116
|
+
throw new Error(`${path}:${index + 1}: unsupported judge skip receipt ${row.judgeSkipped}`)
|
|
1117
|
+
}
|
|
1118
|
+
resolveExperimentArm(row.arm)
|
|
1119
|
+
const key = `${row.arm}:${row.instanceId}`
|
|
1120
|
+
if (rows.has(key)) throw new Error(`${path}:${index + 1}: duplicate judge row ${key}`)
|
|
1121
|
+
rows.set(key, row)
|
|
1122
|
+
}
|
|
1123
|
+
return rows
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function requirePath(name: string): string {
|
|
1127
|
+
const value = process.env[name]?.trim()
|
|
1128
|
+
if (!value) throw new Error(`${name} required for MODE=judge-only`)
|
|
1129
|
+
return value
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function judgeOnlyMain(): Promise<void> {
|
|
1133
|
+
const independentPath = requirePath('INDEPENDENT_PHASE_A')
|
|
1134
|
+
const persistentPath = requirePath('PERSISTENT_PHASE_A')
|
|
1135
|
+
const out = requirePath('OUT')
|
|
1136
|
+
assertDistinctArtifactPaths({ INDEPENDENT_PHASE_A: independentPath, PERSISTENT_PHASE_A: persistentPath, OUT: out })
|
|
1137
|
+
const independent = loadPhaseRows(independentPath, true)
|
|
1138
|
+
const persistent = loadPhaseRows(persistentPath, true)
|
|
1139
|
+
const independentConfig = [...independent.values()][0]!.config
|
|
1140
|
+
const persistentConfig = [...persistent.values()][0]!.config
|
|
1141
|
+
if (!independentConfig || !persistentConfig) throw new Error('Phase-A file is missing its config receipt')
|
|
1142
|
+
assertPresetConfig(independentConfig, 'independent-2', independentPath)
|
|
1143
|
+
assertPresetConfig(persistentConfig, 'persistent-refine-2', persistentPath)
|
|
1144
|
+
if (fingerprint(commonConfig(independentConfig)) !== fingerprint(commonConfig(persistentConfig))) {
|
|
1145
|
+
throw new Error('Phase-A files have different common configuration fingerprints')
|
|
1146
|
+
}
|
|
1147
|
+
const ids = independentConfig.taskIds
|
|
1148
|
+
if (fingerprint(ids) !== fingerprint(persistentConfig.taskIds)) {
|
|
1149
|
+
throw new Error('Phase-A files have different task-id fingerprints')
|
|
1150
|
+
}
|
|
1151
|
+
for (const [path, rows] of [[independentPath, independent], [persistentPath, persistent]] as const) {
|
|
1152
|
+
assertCompleteTaskSet(rows.keys(), ids, path)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// No official judge call occurs before every row in both files passes these checks.
|
|
1156
|
+
const { env, taskById, tools, sharedExecution } = await loadTasksAndTools(ids)
|
|
1157
|
+
for (const id of ids) {
|
|
1158
|
+
const left = independent.get(id)!
|
|
1159
|
+
const right = persistent.get(id)!
|
|
1160
|
+
await assertPhaseRow(
|
|
1161
|
+
left,
|
|
1162
|
+
taskById.get(id)!,
|
|
1163
|
+
independentConfig,
|
|
1164
|
+
tools,
|
|
1165
|
+
manifestFromRow(left),
|
|
1166
|
+
sharedExecution,
|
|
1167
|
+
`${independentPath}:${id}`,
|
|
1168
|
+
)
|
|
1169
|
+
await assertPhaseRow(
|
|
1170
|
+
right,
|
|
1171
|
+
taskById.get(id)!,
|
|
1172
|
+
persistentConfig,
|
|
1173
|
+
tools,
|
|
1174
|
+
manifestFromRow(right),
|
|
1175
|
+
sharedExecution,
|
|
1176
|
+
`${persistentPath}:${id}`,
|
|
1177
|
+
)
|
|
1178
|
+
assertPairedFingerprints(left.fingerprints, right.fingerprints, id)
|
|
1179
|
+
assertPairedExecutionFingerprint(left.executionFingerprint, right.executionFingerprint, id)
|
|
1180
|
+
if (left.reproOutcomeFingerprint !== right.reproOutcomeFingerprint) {
|
|
1181
|
+
throw new Error(`${id}: paired reproduction outcomes do not match`)
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const phaseFileFingerprints: Record<ExperimentArm, string> = {
|
|
1186
|
+
'independent-2': fingerprint({ bytes: readFileSync(independentPath, 'utf8') }),
|
|
1187
|
+
'persistent-refine-2': fingerprint({ bytes: readFileSync(persistentPath, 'utf8') }),
|
|
1188
|
+
}
|
|
1189
|
+
const pairFingerprint = fingerprint({
|
|
1190
|
+
commonConfig: independent.get(ids[0]!)!.fingerprints.commonConfig,
|
|
1191
|
+
ids,
|
|
1192
|
+
phaseFiles: phaseFileFingerprints,
|
|
1193
|
+
prompt: independent.get(ids[0]!)!.fingerprints.prompt,
|
|
1194
|
+
source: independent.get(ids[0]!)!.fingerprints.source,
|
|
1195
|
+
tools: independent.get(ids[0]!)!.fingerprints.tools,
|
|
1196
|
+
executions: ids.map((id) => independent.get(id)!.executionFingerprint),
|
|
1197
|
+
})
|
|
1198
|
+
const judged = loadJudgeRows(out)
|
|
1199
|
+
const arms: Array<{ arm: ExperimentArm; path: string; rows: Map<string, PhaseARow> }> = [
|
|
1200
|
+
{ arm: 'independent-2', path: independentPath, rows: independent },
|
|
1201
|
+
{ arm: 'persistent-refine-2', path: persistentPath, rows: persistent },
|
|
1202
|
+
]
|
|
1203
|
+
for (const existing of judged.values()) {
|
|
1204
|
+
const input = arms.find(({ arm }) => arm === existing.arm)?.rows.get(existing.instanceId)
|
|
1205
|
+
if (!input) throw new Error(`${out}: judge resume row has no paired Phase-A input`)
|
|
1206
|
+
assertJudgeCompletionMatchesInput(existing, input.finalDiff, `${out}:${existing.arm}:${existing.instanceId}`)
|
|
1207
|
+
assertJudgeResumeFingerprints(existing, {
|
|
1208
|
+
pairFingerprint,
|
|
1209
|
+
phaseFileFingerprint: phaseFileFingerprints[existing.arm],
|
|
1210
|
+
inputFingerprint: input.fingerprints.composite,
|
|
1211
|
+
executionFingerprint: input.executionFingerprint,
|
|
1212
|
+
finalDiffHash: input.finalDiffHash,
|
|
1213
|
+
}, `${out}:${existing.arm}:${existing.instanceId}`)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
console.log(`all ${2 * ids.length} Phase-A rows matched; official scoring starts now (serialized)`)
|
|
1217
|
+
for (const id of ids) {
|
|
1218
|
+
for (const { arm, rows } of arms) {
|
|
1219
|
+
const key = `${arm}:${id}`
|
|
1220
|
+
if (judged.has(key)) continue
|
|
1221
|
+
const input = rows.get(id)!
|
|
1222
|
+
const started = Date.now()
|
|
1223
|
+
// Fail without appending: a transient official-scorer error must be retried on resume.
|
|
1224
|
+
const { hiddenResolved, judgeDetail, judgeSkipped } = await completeJudgeScore(
|
|
1225
|
+
input.finalDiff,
|
|
1226
|
+
async () => {
|
|
1227
|
+
const task = taskById.get(id)!
|
|
1228
|
+
process.env.SWEBENCH_NAMESPACE = input.execution!.image.namespace
|
|
1229
|
+
await assertOfficialScoreImagePinned(input, task, `${key}:before`)
|
|
1230
|
+
const score = await env.adapter.judge(task, input.finalDiff)
|
|
1231
|
+
await assertOfficialScoreImagePinned(input, task, `${key}:after`)
|
|
1232
|
+
return score
|
|
1233
|
+
},
|
|
1234
|
+
)
|
|
1235
|
+
const row: JudgeRow = {
|
|
1236
|
+
schema: 'swe-structural-judge-v2',
|
|
1237
|
+
instanceId: id,
|
|
1238
|
+
arm,
|
|
1239
|
+
pairFingerprint,
|
|
1240
|
+
phaseFileFingerprint: phaseFileFingerprints[arm],
|
|
1241
|
+
inputFingerprint: input.fingerprints.composite,
|
|
1242
|
+
executionFingerprint: input.executionFingerprint,
|
|
1243
|
+
finalDiffHash: input.finalDiffHash,
|
|
1244
|
+
hiddenResolved,
|
|
1245
|
+
judgeDetail,
|
|
1246
|
+
judgeMs: Date.now() - started,
|
|
1247
|
+
judgeSkipped,
|
|
1248
|
+
}
|
|
1249
|
+
judged.set(key, row)
|
|
1250
|
+
appendFileSync(out, `${JSON.stringify(row)}\n`)
|
|
1251
|
+
console.log(`[judge] ${key} resolved=${hiddenResolved} skipped=${judgeSkipped ?? '-'}`)
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const main = MODE === 'generate' ? generateMain : judgeOnlyMain
|
|
1257
|
+
main().catch((e) => {
|
|
1258
|
+
console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
|
|
1259
|
+
process.exit(1)
|
|
1260
|
+
})
|