@tangle-network/agent-bench 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/adapters.js +2 -2
  3. package/dist/benchmarks/humaneval.d.ts +10 -1
  4. package/dist/benchmarks/humaneval.js +5 -3
  5. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  6. package/dist/chunk-5H5XV76F.js.map +1 -0
  7. package/dist/{chunk-5SBJCB6W.js → chunk-PWQVGAJB.js} +2 -2
  8. package/dist/index.js +2 -2
  9. package/package.json +5 -4
  10. package/scripts/run-package-tests.mjs +30 -8
  11. package/scripts/verify-pier-agent.mts +1 -0
  12. package/src/benchmarks/humaneval.test.mts +122 -0
  13. package/src/benchmarks/humaneval.ts +100 -27
  14. package/src/david-attribution.mts +78 -0
  15. package/src/david-goliath.mts +149 -0
  16. package/src/hev-improve.mts +25 -6
  17. package/src/humaneval-object-ablation.mts +201 -0
  18. package/src/live-improve-campaign-mbpp.mts +641 -0
  19. package/src/live-improve-campaign.mts +500 -0
  20. package/src/mbpp-structural.mts +12 -7
  21. package/src/stream-observe.py +45 -0
  22. package/src/stream-observe.tpl.html +247 -0
  23. package/src/supervisor-arena.mts +816 -0
  24. package/src/swe-arena/analyze.ts +211 -0
  25. package/src/swe-arena/arms.ts +788 -0
  26. package/src/swe-arena/bootstrap-meta.mts +188 -0
  27. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  28. package/src/swe-arena/calibrate.ts +116 -0
  29. package/src/swe-arena/capabilities.mts +76 -0
  30. package/src/swe-arena/capabilities.test.mts +57 -0
  31. package/src/swe-arena/capacity.ts +194 -0
  32. package/src/swe-arena/cell-evidence.mts +405 -0
  33. package/src/swe-arena/cell-evidence.test.mts +248 -0
  34. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  35. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  36. package/src/swe-arena/execution.test.mts +1170 -0
  37. package/src/swe-arena/fixtures/analyze.py +80 -0
  38. package/src/swe-arena/fixtures/excludes.txt +8 -0
  39. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  40. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  41. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  42. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  43. package/src/swe-arena/fixtures/holdout.json +44 -0
  44. package/src/swe-arena/fixtures/instances.json +146 -0
  45. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  46. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  47. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  48. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  49. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  50. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  51. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  52. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  53. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  54. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  55. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  56. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  57. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  58. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  59. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  60. package/src/swe-arena/fixtures.ts +104 -0
  61. package/src/swe-arena/holdout-certify.mts +408 -0
  62. package/src/swe-arena/holdout-certify.test.mts +160 -0
  63. package/src/swe-arena/judge-child.mts +37 -0
  64. package/src/swe-arena/manifest.mts +293 -0
  65. package/src/swe-arena/manifest.test.mts +169 -0
  66. package/src/swe-arena/materialize.ts +142 -0
  67. package/src/swe-arena/outer-loop.mts +2145 -0
  68. package/src/swe-arena/outer-loop.test.mts +696 -0
  69. package/src/swe-arena/parity.test.mts +87 -0
  70. package/src/swe-arena/proc.test.mts +174 -0
  71. package/src/swe-arena/proc.ts +260 -0
  72. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  73. package/src/swe-arena/proposer-fanout.mts +489 -0
  74. package/src/swe-arena/proposer-fanout.test.mts +372 -0
  75. package/src/swe-arena/reconcile.ts +0 -0
  76. package/src/swe-arena/replay.mts +183 -0
  77. package/src/swe-arena/replay.test.mts +300 -0
  78. package/src/swe-arena/run-experiment.mts +361 -0
  79. package/src/swe-arena/run-supervisor.mjs +297 -0
  80. package/src/swe-arena/run-supervisor.test.mts +498 -0
  81. package/src/swe-arena/serialized-judge.ts +414 -0
  82. package/src/swe-arena/types.ts +166 -0
  83. package/src/swe-code-improve.mts +328 -0
  84. package/src/swe-emit-patch.mts +104 -0
  85. package/src/swe-improve.mts +232 -0
  86. package/src/swe-jail.ts +2 -2
  87. package/src/swe-local-proof.mts +169 -0
  88. package/src/swe-repro-calibrate.mts +446 -0
  89. package/src/swe-stream.mts +1497 -0
  90. package/dist/chunk-PPYSEKFM.js.map +0 -1
  91. /package/dist/{chunk-5SBJCB6W.js.map → chunk-PWQVGAJB.js.map} +0 -0
package/src/swe-jail.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  */
10
10
  import { execFile } from 'node:child_process'
11
11
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
12
+ import { tmpdir } from 'node:os'
12
13
  import { join } from 'node:path'
13
14
  import { promisify } from 'node:util'
14
- import { absoluteSweTempDir } from './swe-temp'
15
15
 
16
16
  const exec = promisify(execFile)
17
17
 
@@ -143,7 +143,7 @@ export async function runPyInJail(
143
143
  applyPatch?: string,
144
144
  opts: { timeoutS?: number } = {},
145
145
  ): Promise<JailRun> {
146
- const scriptDir = mkdtempSync(join(absoluteSweTempDir(), 'swe-repro-'))
146
+ const scriptDir = mkdtempSync(join(tmpdir(), 'swe-repro-'))
147
147
  writeFileSync(join(scriptDir, 'repro.py'), pyScript)
148
148
  if (applyPatch) writeFileSync(join(scriptDir, 'ride.patch'), applyPatch.endsWith('\n') ? applyPatch : `${applyPatch}\n`)
149
149
  const T = opts.timeoutS ?? 120
@@ -0,0 +1,169 @@
1
+ /**
2
+ * SEE-able LOCAL proof of the SWE-bench Verified pipeline — NO tangle sandbox.
3
+ *
4
+ * The whole loop runs on infra we can watch: the repo is cloned into a host tmpdir, the agent is a
5
+ * router-driven tool loop (`runAgentic` + the swe-bench `AgenticSurface`'s list/read/edit tools —
6
+ * jailed to the checkout), the patch is a plain `git diff` of the agent's edits, and the score is
7
+ * the OFFICIAL swebench Docker harness (`adapter.judge`). The only remote call is the model
8
+ * completion via the router. Nothing touches sandbox.tangle.tools.
9
+ *
10
+ * We proxy the surface's `score()` so we can SEE the exact bytes the judge grades: the extracted
11
+ * patch, whether it applies to a clean base checkout (`git apply --check`), and the swebench verdict.
12
+ * With TRACE=1 we also log every tool call so we can tell "agent never edited" from "agent can't edit".
13
+ *
14
+ * TANGLE_API_KEY=… dotenvx run -f …/agent-state.env -- \
15
+ * IDS=django__django-12419 WORKER_MODEL=glm-4.6 \
16
+ * node_modules/.bin/tsx bench/src/swe-local-proof.mts
17
+ */
18
+ import { execFile } from 'node:child_process'
19
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
20
+ import { tmpdir } from 'node:os'
21
+ import { join } from 'node:path'
22
+ import { promisify } from 'node:util'
23
+ import type { AgenticSurface, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/loops'
24
+ import { refine, runAgentic } from '@tangle-network/agent-runtime/loops'
25
+ import type { BenchScore } from './benchmarks/types'
26
+ import { createSweBenchEnvironment } from './swe-bench-env'
27
+
28
+ const exec = promisify(execFile)
29
+
30
+ async function main(): Promise<void> {
31
+ const routerKey = process.env.TANGLE_API_KEY
32
+ if (!routerKey) throw new Error('TANGLE_API_KEY required (the worker calls the router)')
33
+ const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
34
+ const model = process.env.WORKER_MODEL ?? 'glm-4.6'
35
+ const ids = (process.env.IDS ?? 'django__django-12419').split(',').map((s) => s.trim()).filter(Boolean)
36
+ const innerTurns = Number(process.env.INNER_TURNS ?? 40)
37
+ const maxTokens = Number(process.env.MAX_TOKENS ?? 8000)
38
+ const budget = Number(process.env.BUDGET ?? 1)
39
+ // WITH-TOOLS arm: RUN_TOOL=1 exposes the jailed `run` tool + the run-aware prompt. Default OFF ⇒
40
+ // the read/edit-only baseline (glm-5.2 7/12) unchanged.
41
+ const enableRun = ['1', 'true', 'yes'].includes((process.env.RUN_TOOL ?? '').toLowerCase())
42
+
43
+ console.log(`═══ SWE-bench Verified — SEE-able LOCAL proof (no tangle sandbox) ═══`)
44
+ console.log(`model=${model} ids=${ids.join(',')} innerTurns=${innerTurns} maxTokens=${maxTokens} budget=${budget} runTool=${enableRun}`)
45
+ console.log(`router=${routerBaseUrl}`)
46
+
47
+ const { environment, tasks, adapter } = await createSweBenchEnvironment(ids.length, { ids, enableRun })
48
+ const taskList = await tasks(0, ids.length)
49
+
50
+ // One shot per pinned id: proxy score() to capture the exact judged bytes + a NON-DESTRUCTIVE
51
+ // apply-coherence check, then delegate the verdict to the real Docker judge.
52
+ //
53
+ // CRITICAL: score() is called MORE THAN ONCE per task by the driver — the shotExecutor scores
54
+ // the handle to drive its loop, and depthStrategy scores it again at the end. So this proxy MUST
55
+ // be non-destructive (never mutate the working tree the agent edited) and idempotent (never
56
+ // clobber a captured non-empty patch with a later empty read). It also caches the judge by patch
57
+ // so we don't run the Docker harness twice for the identical diff.
58
+ type Rec = { patch: string; applied: boolean; applyErr?: string; applyChecked?: boolean; score?: BenchScore }
59
+ const captured = new Map<string, Rec>()
60
+ const judged = new Map<string, BenchScore>()
61
+ const toolStats = new Map<string, { list: number; read: number; edit_ok: number; edit_fail: number; run: number; run_err: number }>()
62
+ const proxy: AgenticSurface = {
63
+ ...environment,
64
+ async call(handle, name, args) {
65
+ const res = await environment.call(handle, name, args)
66
+ // Count tool usage per workspace so we can SEE whether the agent ever edited, and whether
67
+ // edits succeeded or bounced off old_string matching. handle.id keys the workspace.
68
+ const st = toolStats.get(handle.id) ?? { list: 0, read: 0, edit_ok: 0, edit_fail: 0, run: 0, run_err: 0 }
69
+ const r = String(res)
70
+ if (name === 'list_files') st.list += 1
71
+ else if (name === 'read_file') st.read += 1
72
+ else if (name === 'edit_file') {
73
+ if (r.startsWith('edited ')) st.edit_ok += 1
74
+ else st.edit_fail += 1
75
+ } else if (name === 'run') {
76
+ if (r.startsWith('ERROR:')) st.run_err += 1
77
+ else st.run += 1
78
+ }
79
+ toolStats.set(handle.id, st)
80
+ if (process.env.TRACE) {
81
+ const a = JSON.stringify(args).slice(0, 200)
82
+ console.error(`[TOOL] ${name} args=${a} -> ${r.slice(0, 200).replace(/\n/g, '⏎')}`)
83
+ }
84
+ return res
85
+ },
86
+ async score(task, handle: ArtifactHandle): Promise<SurfaceScore> {
87
+ const dir = handle.id
88
+ const diff = await exec('git', ['-C', dir, 'diff'], { maxBuffer: 40_000_000, timeout: 60_000 })
89
+ const patch = diff.stdout
90
+ // Mirror tool stats onto the task id (handle.id == dir).
91
+ const st = toolStats.get(dir)
92
+ if (st) toolStats.set(task.id, st)
93
+ // Idempotent capture: keep the FIRST non-empty patch; never overwrite it with a later empty read.
94
+ const prev = captured.get(task.id)
95
+ let rec: Rec
96
+ if (!prev) {
97
+ rec = { patch, applied: false }
98
+ captured.set(task.id, rec)
99
+ } else {
100
+ rec = prev
101
+ if (!rec.patch.trim() && patch.trim()) rec.patch = patch
102
+ }
103
+ const effPatch = rec.patch
104
+ if (!effPatch.trim()) return { passes: 0, total: 1, errored: 0 }
105
+ // NON-DESTRUCTIVE apply-coherence check (once): the diff was produced from this tree vs HEAD,
106
+ // so `git apply --check -R` proves it is a clean, self-consistent patch without touching the
107
+ // working tree. The REAL forward-apply-to-clean-base proof is the swebench judge below, which
108
+ // applies the patch in a fresh Docker checkout and reports apply failures.
109
+ if (!rec.applyChecked) {
110
+ rec.applyChecked = true
111
+ const pf = join(mkdtempSync(join(tmpdir(), 'swe-apply-')), 'p.diff')
112
+ writeFileSync(pf, effPatch)
113
+ try {
114
+ await exec('git', ['-C', dir, 'apply', '--check', '-R', pf], { timeout: 60_000 })
115
+ rec.applied = true
116
+ } catch (e) {
117
+ rec.applied = false
118
+ rec.applyErr = e instanceof Error ? e.message.slice(0, 200) : String(e)
119
+ } finally {
120
+ rmSync(pf, { force: true })
121
+ }
122
+ }
123
+ // Cached judge: identical patch ⇒ identical verdict; don't pay for a second Docker run.
124
+ let s = judged.get(effPatch)
125
+ if (!s) {
126
+ s = await adapter.judge(task, effPatch)
127
+ judged.set(effPatch, s)
128
+ }
129
+ rec.score = s
130
+ return { passes: s.resolved ? 1 : 0, total: 1, errored: 0 }
131
+ },
132
+ }
133
+
134
+ let anyResolved = 0
135
+ for (const task of taskList) {
136
+ const t0 = Date.now()
137
+ const r = await runAgentic({
138
+ surface: proxy,
139
+ task,
140
+ strategy: refine,
141
+ routerBaseUrl,
142
+ routerKey,
143
+ model,
144
+ maxTokens,
145
+ innerTurns,
146
+ budget,
147
+ })
148
+ const rec = captured.get(task.id)
149
+ const st = toolStats.get(task.id)
150
+ const patchBytes = rec?.patch.length ?? 0
151
+ const patchLines = rec?.patch ? rec.patch.split('\n').length : 0
152
+ const files = rec?.patch ? [...rec.patch.matchAll(/^diff --git a\/(\S+)/gm)].map((m) => m[1]) : []
153
+ const resolved = rec?.score?.resolved ?? false
154
+ if (resolved) anyResolved += 1
155
+ console.log(`\n──── ${task.id} ────`)
156
+ console.log(` agent shots=${r.shots} completions=${r.completions} tokens=in:${r.tokens.input}/out:${r.tokens.output} usd=${r.usd} wall=${Math.round((Date.now() - t0) / 1000)}s`)
157
+ console.log(` tools: list=${st?.list ?? 0} read=${st?.read ?? 0} edit_ok=${st?.edit_ok ?? 0} edit_fail=${st?.edit_fail ?? 0} run=${st?.run ?? 0} run_err=${st?.run_err ?? 0}`)
158
+ console.log(` patch: ${patchBytes} bytes, ${patchLines} lines, files=[${files.join(', ') || '(none)'}]`)
159
+ console.log(` patch APPLIED (git apply --check on clean base): ${rec?.applied ? 'YES' : 'NO'}${rec?.applyErr ? ` (${rec.applyErr})` : ''}`)
160
+ console.log(` swebench judge RESOLVED: ${resolved ? '1' : '0'}`)
161
+ if (rec?.score?.detail) console.log(` judge report: ${rec.score.detail.slice(0, 400)}`)
162
+ }
163
+ console.log(`\n>>> resolved ${anyResolved}/${taskList.length}`)
164
+ }
165
+
166
+ main().catch((e) => {
167
+ console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
168
+ process.exit(1)
169
+ })
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Stage 0 of the SWE-bench frontier push — the REPRODUCTION-ORACLE CALIBRATOR
3
+ * (contract: supervisor-lab/docs/results/PREREG-swe-frontier.md, Stage 0 only).
4
+ *
5
+ * Question it answers: can glm-5.2, given ONLY the issue text (+ up to 3 requested file reads),
6
+ * author a self-contained repro script that (a) FAILS on the unpatched tree — "validity" — and
7
+ * (b) PASSES after the GOLD patch — "soundness"? GATE: ≥60% of instances valid+sound (1 retry).
8
+ *
9
+ * No cheating by construction: the GOLD patch is used strictly script-side (applied host-side to a
10
+ * COPY of the tree, then run in the jail); a hard assert refuses to send any model message that
11
+ * contains gold-patch content. The repro runs in the instance's cached swebench Docker image with
12
+ * the tree mounted READ-ONLY — the byte-identical invocation pattern of swe-bench-env's `run` tool
13
+ * (conda testbed, cwd=/testbed, --network none, dual timeout) — so calibration measures the same
14
+ * substrate Stage 1/2 will select patches on.
15
+ *
16
+ * cd ~/company/devops/secrets && dotenvx run -f agent-state.env -f tangle-router.env -- bash -c \
17
+ * 'cd ~/code/agent-runtime-swe && OUT=/path/swe-stage0.jsonl node_modules/.bin/tsx bench/src/swe-repro-calibrate.mts'
18
+ *
19
+ * Env: ZAI_API_KEY (required unless CANARY_ONLY), ZAI_BASE, MODEL=glm-5.2, MAX_TOKENS=12000, TEMP=0.2,
20
+ * CONC=3, REPRO_TIMEOUT=120 (s), LLM_TIMEOUT_MS=480000, IDS=comma-list override, OUT=jsonl path,
21
+ * REPRO_EXEC=mount|image (execution substrate; see the constant below),
22
+ * CANARY_ONLY=1 (run ONLY the per-instance execution canary — no model calls, no grading —
23
+ * to decide which substrate is trustworthy per instance before spending on authoring).
24
+ */
25
+ import { execFile } from 'node:child_process'
26
+ import { appendFileSync, cpSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
27
+ import { tmpdir } from 'node:os'
28
+ import { join } from 'node:path'
29
+ import { promisify } from 'node:util'
30
+ import type { AgenticTask, ArtifactHandle } from '@tangle-network/agent-runtime/loops'
31
+ import type { BenchTask } from './benchmarks/types'
32
+ import { createSweBenchEnvironment, resolveImageForMetadata } from './swe-bench-env'
33
+ import {
34
+ APPLY_SENTINEL,
35
+ cachedInstanceIds,
36
+ extractReadRequests as extractReads,
37
+ extractReproScript as extractScript,
38
+ IMPORT_NAME,
39
+ importCanaryScript,
40
+ type JailRun,
41
+ reproAuthorSystem,
42
+ runPyInJail,
43
+ tail,
44
+ zaiChatRaw,
45
+ } from './swe-jail'
46
+
47
+ const exec = promisify(execFile)
48
+
49
+ // ---------- config ----------
50
+
51
+ const ZAI_BASE = process.env.ZAI_BASE ?? 'https://api.z.ai/api/coding/paas/v4'
52
+ const ZAI_KEY = process.env.ZAI_API_KEY ?? ''
53
+ const MODEL = process.env.MODEL ?? 'glm-5.2'
54
+ // glm-5.2 is a reasoning model: hidden reasoning consumes max_tokens, so <8000 starves content.
55
+ const MAX_TOKENS = Number(process.env.MAX_TOKENS ?? 12_000)
56
+ const TEMP = Number(process.env.TEMP ?? 0.2)
57
+ const CONC = Math.max(1, Math.min(3, Number(process.env.CONC ?? 3)))
58
+ const REPRO_TIMEOUT_S = Number(process.env.REPRO_TIMEOUT ?? 120)
59
+ const LLM_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS ?? 480_000)
60
+ const OUT = process.env.OUT ?? 'swe-stage0.jsonl'
61
+ /** Repro EXECUTION substrate. `mount` (the prereg default) mounts the fresh host clone :ro over
62
+ * /testbed — the run tool's exact pattern. `image` executes against the image's OWN /testbed
63
+ * (base_commit, BUILT — compiled extensions and generated version files present), applying the
64
+ * gold patch in-container instead of host-side. Measured on the 23 cached instances: `mount`
65
+ * kills 6 (astropy×2/matplotlib/sklearn×2/pytest) with import errors a fresh un-built clone
66
+ * cannot avoid; the SAME scripts were all valid+sound under `image`. */
67
+ const EXEC = process.env.REPRO_EXEC ?? 'mount'
68
+ if (EXEC !== 'mount' && EXEC !== 'image') throw new Error(`REPRO_EXEC must be mount|image, got ${EXEC}`)
69
+ /** Canary-only sweep: measure per-instance substrate trustworthiness (gold applied, import resolves
70
+ * into the patched tree) across all instances with ZERO model calls. */
71
+ const CANARY_ONLY = process.env.CANARY_ONLY === '1'
72
+
73
+ // ---------- model client (swe-jail's zaiChatRaw: patient 429 ladder + empty-content retry —
74
+ // the glm reasoning path starves `content` when reasoning eats max_tokens) ----------
75
+
76
+ interface ChatMsg {
77
+ role: 'system' | 'user' | 'assistant'
78
+ content: string
79
+ }
80
+
81
+ interface Completion {
82
+ content: string
83
+ attempts: number
84
+ tokensIn: number
85
+ tokensOut: number
86
+ }
87
+
88
+ async function complete(messages: ChatMsg[]): Promise<Completion> {
89
+ const { json, attempts } = await zaiChatRaw(
90
+ { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS },
91
+ { model: MODEL, max_tokens: MAX_TOKENS, temperature: TEMP, messages },
92
+ )
93
+ const d = json as {
94
+ choices?: Array<{ message?: { content?: string } }>
95
+ usage?: { prompt_tokens?: number; completion_tokens?: number }
96
+ }
97
+ return {
98
+ content: d.choices?.[0]?.message?.content ?? '',
99
+ attempts,
100
+ tokensIn: d.usage?.prompt_tokens ?? 0,
101
+ tokensOut: d.usage?.completion_tokens ?? 0,
102
+ }
103
+ }
104
+
105
+ // ---------- authoring protocol (one optional read round, plain-text READ: lines) ----------
106
+ // The protocol constants/parsers now live in swe-jail.ts (shared with the stream driver);
107
+ // behavior here is byte-identical to the in-file originals.
108
+
109
+ const AUTHOR_SYSTEM = reproAuthorSystem(REPRO_TIMEOUT_S)
110
+
111
+ // ---------- per-instance row ----------
112
+
113
+ interface Row {
114
+ instanceId: string
115
+ repo: string
116
+ execMode: string
117
+ image: string | null
118
+ imagePresent: boolean
119
+ canaryExit: number | null
120
+ canaryOut: string
121
+ /** true = gold applied AND import resolved into the patched tree; false = this substrate cannot
122
+ * grade this instance; null = repo not in IMPORT_NAME (canary not applicable). */
123
+ canaryPass: boolean | null
124
+ readsRequested: string[]
125
+ authorCalls: number
126
+ retryUsed: boolean
127
+ preExitFirst: number | null
128
+ preExitFinal: number | null
129
+ preOut: string
130
+ valid: boolean
131
+ goldApplyOk: boolean | null
132
+ postExit: number | null
133
+ postOut: string
134
+ sound: boolean
135
+ validAndSound: boolean
136
+ autoClass: string
137
+ script: string | null
138
+ scriptFirst: string | null
139
+ tokensIn: number
140
+ tokensOut: number
141
+ wallMs: number
142
+ error?: string
143
+ }
144
+
145
+ async function calibrateInstance(
146
+ env: Awaited<ReturnType<typeof createSweBenchEnvironment>>,
147
+ bt: BenchTask,
148
+ ): Promise<Row> {
149
+ const t0 = Date.now()
150
+ const id = bt.id
151
+ const row: Row = {
152
+ instanceId: id, repo: '', execMode: EXEC, image: null, imagePresent: false, canaryExit: null, canaryOut: '',
153
+ canaryPass: null, readsRequested: [], authorCalls: 0, retryUsed: false, preExitFirst: null, preExitFinal: null,
154
+ preOut: '', valid: false, goldApplyOk: null, postExit: null, postOut: '', sound: false,
155
+ validAndSound: false, autoClass: 'infra-error', script: null, scriptFirst: null,
156
+ tokensIn: 0, tokensOut: 0, wallMs: 0,
157
+ }
158
+ let handle: ArtifactHandle | null = null
159
+ let patchedDir: string | null = null
160
+ try {
161
+ // Metadata (issue + gold) comes from the adapter's task row, never the model path.
162
+ const md = bt.metadata as Record<string, string>
163
+ row.repo = md.repo
164
+ const gold = String(md.patch ?? '')
165
+ if (!gold.trim()) throw new Error('gold patch missing from metadata')
166
+
167
+ // 1. Hard-assert the cached docker image BEFORE paying for a clone or a model call.
168
+ const img = await resolveImageForMetadata(bt.metadata ?? {})
169
+ if (!img.ok) {
170
+ row.autoClass = 'image-missing'
171
+ row.error = img.reason
172
+ return row
173
+ }
174
+ row.image = img.tag
175
+ row.imagePresent = true
176
+
177
+ // 2. Open the environment: host clone at base_commit. The `mount` substrate jails against this
178
+ // tree :ro; the `image` substrate uses it only for list_files/read_file during authoring — so a
179
+ // canary-only image sweep skips the clone (and its network cost) entirely.
180
+ let treeDir: string | null = null
181
+ if (EXEC === 'mount' || !CANARY_ONLY) {
182
+ const h = await env.environment.open({ id, systemPrompt: '', userPrompt: '', meta: {} } as AgenticTask)
183
+ handle = h
184
+ treeDir = h.id
185
+ }
186
+ const execTree = EXEC === 'image' ? null : treeDir
187
+
188
+ // Mount substrate: apply the gold patch host-side to a COPY of the tree UP FRONT — the canary
189
+ // must observe the tree exactly as the soundness run will mount it. A failed apply is terminal
190
+ // before any model call is spent.
191
+ if (EXEC === 'mount') {
192
+ patchedDir = mkdtempSync(join(tmpdir(), 'swe-gold-'))
193
+ cpSync(treeDir as string, patchedDir, { recursive: true })
194
+ const goldFile = join(patchedDir, '.swe-gold.patch')
195
+ writeFileSync(goldFile, gold.endsWith('\n') ? gold : `${gold}\n`)
196
+ try {
197
+ await exec('git', ['-C', patchedDir, 'apply', '--whitespace=nowarn', goldFile], { timeout: 60_000 })
198
+ row.goldApplyOk = true
199
+ } catch {
200
+ // Official-harness fallback: GNU patch with fuzz.
201
+ try {
202
+ await exec('patch', ['-p1', '--fuzz=5', '-i', goldFile], { cwd: patchedDir, timeout: 60_000 })
203
+ row.goldApplyOk = true
204
+ } catch (e2) {
205
+ row.goldApplyOk = false
206
+ row.autoClass = 'gold-apply-failed'
207
+ row.error = `gold patch failed to apply: ${(e2 as Error).message.slice(0, 200)}`
208
+ return row
209
+ }
210
+ }
211
+ rmSync(goldFile, { force: true })
212
+ }
213
+
214
+ // EXECUTION CANARY (mode-deciding, zero model calls): with the gold patch applied to the tree
215
+ // under test, `import <pkg>` must succeed AND resolve INTO that tree. PYTHONPATH=/testbed is
216
+ // already pinned by runPyInJail, so a site-packages resolution here (exit 3) means this substrate
217
+ // would grade code the patch never reaches — the instance is env-unresolvable in this mode.
218
+ const pkg = IMPORT_NAME[md.repo]
219
+ if (pkg) {
220
+ const canaryScript = importCanaryScript(pkg)
221
+ const c = EXEC === 'image'
222
+ ? await runPyInJail(img.tag, null, canaryScript, gold, { timeoutS: REPRO_TIMEOUT_S })
223
+ : await runPyInJail(img.tag, patchedDir, canaryScript, undefined, { timeoutS: REPRO_TIMEOUT_S })
224
+ if (c.infraError) throw new Error(c.infraError)
225
+ row.canaryExit = c.code
226
+ row.canaryOut = tail(c.out, 200)
227
+ if (EXEC === 'image') {
228
+ row.goldApplyOk = c.out.includes(APPLY_SENTINEL)
229
+ if (!row.goldApplyOk) {
230
+ row.autoClass = 'gold-apply-failed'
231
+ row.error = `gold patch failed to apply in-container: ${tail(c.out, 200)}`
232
+ return row
233
+ }
234
+ }
235
+ row.canaryPass = c.code === 0
236
+ if (!row.canaryPass) {
237
+ row.autoClass = 'env-unresolvable'
238
+ row.error = `canary: import ${pkg} did not resolve into the patched tree (exit ${c.code})`
239
+ return row
240
+ }
241
+ }
242
+ if (CANARY_ONLY) {
243
+ row.autoClass = row.canaryPass === true ? 'canary-pass' : 'canary-unknown'
244
+ return row
245
+ }
246
+ const h = handle as ArtifactHandle
247
+
248
+ // 3. Author the repro: issue text + top-level listing; ONE optional read round (≤3 files).
249
+ const listing = String(await env.environment.call(h, 'list_files', { dir: '' })).slice(0, 5_000)
250
+ const issue = String(md.problem_statement ?? '').slice(0, 20_000)
251
+ const messages: ChatMsg[] = [
252
+ { role: 'system', content: AUTHOR_SYSTEM },
253
+ {
254
+ role: 'user',
255
+ content:
256
+ `Repository: ${md.repo} (checked out at the commit where the bug is PRESENT).\n\n` +
257
+ `Repository file listing (top levels):\n${listing}\n\n--- Issue ---\n${issue}\n\n--- Instructions ---\n` +
258
+ 'If you need to see specific source files before writing the script, reply with ONLY read requests, ' +
259
+ 'one per line, at most 3, in the form:\nREAD: path/relative/to/repo/root\n' +
260
+ 'Otherwise reply now with the final script in a single ```python fenced block.',
261
+ },
262
+ ]
263
+ // Leak guard: no model message may carry gold-patch content. A distinctive added line is the sentinel.
264
+ const goldMark = gold.split('\n').find((l) => l.startsWith('+') && !l.startsWith('+++') && l.trim().length > 12)?.slice(0, 80)
265
+ const guardedComplete = async (msgs: ChatMsg[]): Promise<Completion> => {
266
+ if (goldMark && msgs.some((m) => m.content.includes(goldMark))) {
267
+ throw new Error('REFUSED: gold patch content leaked into model messages')
268
+ }
269
+ const c = await complete(msgs)
270
+ row.authorCalls += 1
271
+ row.tokensIn += c.tokensIn
272
+ row.tokensOut += c.tokensOut
273
+ return c
274
+ }
275
+
276
+ let resp = await guardedComplete(messages)
277
+ let script = extractScript(resp.content)
278
+ if (!script) {
279
+ const reads = extractReads(resp.content)
280
+ row.readsRequested = reads
281
+ messages.push({ role: 'assistant', content: resp.content })
282
+ if (reads.length) {
283
+ const bodies: string[] = []
284
+ for (const p of reads) {
285
+ const c = String(await env.environment.call(h, 'read_file', { path: p }))
286
+ bodies.push(`----- ${p} -----\n${c.slice(0, 12_000)}${c.length > 12_000 ? '\n…[truncated]' : ''}`)
287
+ }
288
+ messages.push({
289
+ role: 'user',
290
+ content: `${bodies.join('\n\n')}\n\nNow reply with the final script in a single \`\`\`python fenced block.`,
291
+ })
292
+ } else {
293
+ messages.push({ role: 'user', content: 'Reply with ONLY the final Python script in a single ```python fenced block.' })
294
+ }
295
+ resp = await guardedComplete(messages)
296
+ script = extractScript(resp.content)
297
+ }
298
+ if (!script) {
299
+ row.autoClass = 'authoring-failed'
300
+ row.error = `no script in response: ${resp.content.slice(0, 200)}`
301
+ return row
302
+ }
303
+ row.script = script
304
+
305
+ // 4. VALIDITY: the script must indicate bug-present (nonzero, non-timeout) on the UNPATCHED tree.
306
+ let pre = await runPyInJail(img.tag, execTree, script, undefined, { timeoutS: REPRO_TIMEOUT_S })
307
+ if (pre.infraError) throw new Error(pre.infraError)
308
+ row.preExitFirst = pre.code
309
+ let detected = pre.code !== 0 && !pre.timedOut
310
+ if (!detected) {
311
+ // One retry with feedback, per the prereg.
312
+ row.retryUsed = true
313
+ row.scriptFirst = script
314
+ const feedback = pre.timedOut
315
+ ? `your script timed out after ${REPRO_TIMEOUT_S}s on the known-buggy code. Write a faster, simpler script that still detects the bug.`
316
+ : 'your script did not detect the bug on the known-buggy code: it exited 0 on the UNPATCHED repository. ' +
317
+ `Its output was:\n${tail(pre.out, 1_500)}\nWrite a corrected script that FAILS (nonzero exit) on the buggy code.`
318
+ messages.push({ role: 'assistant', content: `\`\`\`python\n${script}\n\`\`\`` }, { role: 'user', content: feedback })
319
+ const retry = await guardedComplete(messages)
320
+ const script2 = extractScript(retry.content)
321
+ if (script2) {
322
+ script = script2
323
+ row.script = script2
324
+ pre = await runPyInJail(img.tag, execTree, script2, undefined, { timeoutS: REPRO_TIMEOUT_S })
325
+ if (pre.infraError) throw new Error(pre.infraError)
326
+ detected = pre.code !== 0 && !pre.timedOut
327
+ }
328
+ }
329
+ row.preExitFinal = pre.code
330
+ row.preOut = tail(pre.out, 1_500)
331
+ row.valid = detected
332
+ if (!row.valid) {
333
+ row.autoClass = pre.timedOut ? 'invalid-timeout' : 'invalid-exit0'
334
+ return row
335
+ }
336
+
337
+ // 5. SOUNDNESS (script-side only): gold patch applied, same jail, must exit 0.
338
+ let post: JailRun
339
+ if (EXEC === 'image') {
340
+ // Gold applied in-container to the image's built /testbed; a failed apply surfaces as exit≠0
341
+ // with git's message in the output.
342
+ post = await runPyInJail(img.tag, null, script, gold, { timeoutS: REPRO_TIMEOUT_S })
343
+ if (post.infraError) throw new Error(post.infraError)
344
+ row.goldApplyOk = post.out.includes(APPLY_SENTINEL)
345
+ if (!row.goldApplyOk) {
346
+ row.autoClass = 'gold-apply-failed'
347
+ row.error = `gold patch failed to apply in-container: ${tail(post.out, 200)}`
348
+ return row
349
+ }
350
+ } else {
351
+ // Mount substrate: gold was already applied host-side to the canary-verified copy up front.
352
+ post = await runPyInJail(img.tag, patchedDir as string, script, undefined, { timeoutS: REPRO_TIMEOUT_S })
353
+ if (post.infraError) throw new Error(post.infraError)
354
+ }
355
+ row.postExit = post.code
356
+ row.postOut = tail(post.out, 1_500)
357
+ row.sound = post.code === 0
358
+ row.validAndSound = row.valid && row.sound
359
+ if (row.validAndSound) row.autoClass = 'ok'
360
+ else if (post.timedOut) row.autoClass = 'unsound-timeout'
361
+ else if (/ModuleNotFoundError|ImportError/.test(post.out)) row.autoClass = 'unsound-import-error'
362
+ else row.autoClass = 'unsound-still-failing'
363
+ return row
364
+ } catch (e) {
365
+ row.autoClass = row.autoClass === 'ok' ? 'infra-error' : row.autoClass
366
+ row.error = e instanceof Error ? e.message.slice(0, 400) : String(e).slice(0, 400)
367
+ return row
368
+ } finally {
369
+ row.wallMs = Date.now() - t0
370
+ if (patchedDir) rmSync(patchedDir, { recursive: true, force: true })
371
+ if (handle) await env.environment.close(handle).catch(() => {})
372
+ }
373
+ }
374
+
375
+ // ---------- driver ----------
376
+
377
+ async function main(): Promise<void> {
378
+ if (!ZAI_KEY && !CANARY_ONLY) throw new Error('ZAI_API_KEY required (run under dotenvx: agent-state.env)')
379
+ const ids = process.env.IDS
380
+ ? process.env.IDS.split(',').map((s) => s.trim()).filter(Boolean)
381
+ : await cachedInstanceIds()
382
+ if (!ids.length) throw new Error('no cached sweb.eval images found and no IDS given')
383
+
384
+ console.log(`═══ SWE-bench Stage 0 — ${CANARY_ONLY ? 'execution-canary sweep (no model calls)' : 'reproduction-oracle calibration'} ═══`)
385
+ console.log(`model=${MODEL} base=${ZAI_BASE} maxTokens=${MAX_TOKENS} temp=${TEMP} conc=${CONC} reproTimeout=${REPRO_TIMEOUT_S}s exec=${EXEC}`)
386
+ console.log(`instances (${ids.length}): ${ids.join(', ')}`)
387
+ console.log(`out=${OUT}`)
388
+
389
+ const env = await createSweBenchEnvironment(ids.length, { ids })
390
+ // One dataset scan for all instances; per-instance metadata (issue + gold) rides the BenchTask.
391
+ const taskById = new Map((await env.adapter.loadTasks({ ids, split: 'test' })).map((t) => [t.id, t]))
392
+ const missing = ids.filter((id) => !taskById.has(id))
393
+ if (missing.length) throw new Error(`instances not found in SWE-bench_Verified: ${missing.join(', ')}`)
394
+ const rows: Row[] = []
395
+ let next = 0
396
+ const worker = async (): Promise<void> => {
397
+ while (next < ids.length) {
398
+ const i = next++
399
+ const id = ids[i]
400
+ console.log(`[${i + 1}/${ids.length}] ${id} …`)
401
+ const row = await calibrateInstance(env, taskById.get(id) as BenchTask)
402
+ rows.push(row)
403
+ appendFileSync(OUT, `${JSON.stringify(row)}\n`)
404
+ console.log(
405
+ `[${i + 1}/${ids.length}] ${id} → ${row.autoClass}` +
406
+ ` (pre=${row.preExitFinal ?? '-'} post=${row.postExit ?? '-'} retry=${row.retryUsed ? 'y' : 'n'}` +
407
+ ` calls=${row.authorCalls} tok=${row.tokensIn}/${row.tokensOut} wall=${Math.round(row.wallMs / 1000)}s)` +
408
+ (row.error ? ` err=${row.error.slice(0, 120)}` : ''),
409
+ )
410
+ }
411
+ }
412
+ await Promise.all(Array.from({ length: CONC }, () => worker()))
413
+
414
+ // ---------- summary ----------
415
+ const n = rows.length
416
+ const present = rows.filter((r) => r.imagePresent).length
417
+ const valid = rows.filter((r) => r.valid).length
418
+ const sound = rows.filter((r) => r.validAndSound).length
419
+ const retried = rows.filter((r) => r.retryUsed).length
420
+ const classes = new Map<string, number>()
421
+ for (const r of rows) classes.set(r.autoClass, (classes.get(r.autoClass) ?? 0) + 1)
422
+
423
+ console.log('\n══ per-instance ══')
424
+ console.log('instance | class | canary | pre1 | preF | valid | goldApply | post | sound | retry | calls | tokIn/out | wall_s')
425
+ for (const r of [...rows].sort((a, b) => a.instanceId.localeCompare(b.instanceId))) {
426
+ const canary = r.canaryPass === null ? `?(${r.canaryExit ?? '-'})` : r.canaryPass ? 'pass' : `FAIL(${r.canaryExit})`
427
+ console.log(
428
+ `${r.instanceId} | ${r.autoClass} | ${canary} | ${r.preExitFirst ?? '-'} | ${r.preExitFinal ?? '-'} | ` +
429
+ `${r.valid ? 1 : 0} | ${r.goldApplyOk === null ? '-' : r.goldApplyOk ? 1 : 0} | ${r.postExit ?? '-'} | ${r.sound ? 1 : 0} | ` +
430
+ `${r.retryUsed ? 1 : 0} | ${r.authorCalls} | ${r.tokensIn}/${r.tokensOut} | ${Math.round(r.wallMs / 1000)}`,
431
+ )
432
+ }
433
+ console.log('\n══ summary ══')
434
+ console.log(`n=${n} imagePresent=${present} canaryPass=${rows.filter((r) => r.canaryPass === true).length} valid(bug detected pre-patch)=${valid} valid+sound=${sound} retryUsed=${retried}`)
435
+ console.log(`failure modes: ${[...classes.entries()].map(([k, v]) => `${k}=${v}`).join(' ')}`)
436
+ if (!CANARY_ONLY) {
437
+ const rate = n ? sound / n : 0
438
+ const pct = (100 * rate).toFixed(1)
439
+ console.log(`\nSTAGE-0 GATE (>=60% valid+sound): ${rate >= 0.6 ? 'PASS' : 'FAIL'} — ${sound}/${n} = ${pct}%`)
440
+ }
441
+ }
442
+
443
+ main().catch((e) => {
444
+ console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
445
+ process.exit(1)
446
+ })