@tangle-network/agent-bench 0.3.6 → 0.3.8
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 +11 -0
- package/HARNESS.md +43 -0
- package/dist/adapters.js +24 -24
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/humaneval.d.ts +10 -1
- package/dist/benchmarks/humaneval.js +5 -3
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +24 -24
- package/package.json +6 -5
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/scripts/verify-pier-agent.mts +1 -0
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +122 -0
- package/src/benchmarks/humaneval.ts +100 -27
- package/src/david-attribution.mts +78 -0
- package/src/david-goliath.mts +149 -0
- package/src/hev-improve.mts +25 -6
- package/src/humaneval-object-ablation.mts +201 -0
- package/src/live-improve-campaign-mbpp.mts +641 -0
- package/src/live-improve-campaign.mts +500 -0
- package/src/mbpp-structural.mts +12 -7
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/activation.mts +228 -0
- package/src/swe-arena/activation.test.mts +303 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +804 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +217 -0
- package/src/swe-arena/capabilities.mts +76 -0
- package/src/swe-arena/capabilities.test.mts +57 -0
- package/src/swe-arena/capacity.ts +194 -0
- package/src/swe-arena/cell-evidence.mts +437 -0
- package/src/swe-arena/cell-evidence.test.mts +248 -0
- package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
- package/src/swe-arena/diagnosis-ensemble.ts +520 -0
- package/src/swe-arena/execution.test.mts +1170 -0
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
- package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
- package/src/swe-arena/fixtures/holdout.json +44 -0
- package/src/swe-arena/fixtures/instances.json +146 -0
- package/src/swe-arena/fixtures/ledger.jsonl +12 -0
- package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
- package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
- package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
- package/src/swe-arena/fixtures/rematch.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
- package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
- package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
- package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
- package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
- package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
- package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
- package/src/swe-arena/fixtures/worker-tokens.json +42 -0
- package/src/swe-arena/fixtures.ts +237 -0
- package/src/swe-arena/gepa-seat.mts +583 -0
- package/src/swe-arena/gepa-seat.test.mts +635 -0
- package/src/swe-arena/holdout-certify.mts +408 -0
- package/src/swe-arena/holdout-certify.test.mts +160 -0
- package/src/swe-arena/judge-child.mts +37 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/lineage-record.mts +164 -0
- package/src/swe-arena/lineage-record.test.mts +115 -0
- package/src/swe-arena/manifest.mts +293 -0
- package/src/swe-arena/manifest.test.mts +169 -0
- package/src/swe-arena/materialize.ts +142 -0
- package/src/swe-arena/outer-loop.mts +2795 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -0
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proc.test.mts +174 -0
- package/src/swe-arena/proc.ts +260 -0
- package/src/swe-arena/profiles/default-author.profile.json +4 -0
- package/src/swe-arena/proposer-fanout.mts +770 -0
- package/src/swe-arena/proposer-fanout.test.mts +619 -0
- package/src/swe-arena/proposer-provenance.mts +177 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/reconcile.ts +0 -0
- package/src/swe-arena/replay.mts +183 -0
- package/src/swe-arena/replay.test.mts +300 -0
- package/src/swe-arena/run-experiment.mts +727 -0
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +500 -0
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +218 -0
- package/src/swe-code-improve.mts +328 -0
- package/src/swe-emit-patch.mts +104 -0
- package/src/swe-improve.mts +232 -0
- package/src/swe-jail.ts +2 -2
- package/src/swe-local-proof.mts +169 -0
- package/src/swe-repro-calibrate.mts +446 -0
- package/src/swe-stream.mts +1497 -0
- package/src/swe-structural.mts +245 -837
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/dist/chunk-PPYSEKFM.js.map +0 -1
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment runner CLI — the typed replacement for the experiment's
|
|
3
|
+
* `orchestrate.sh` + `run-instance.sh`:
|
|
4
|
+
*
|
|
5
|
+
* tsx src/swe-arena/run-experiment.mts <config.json>
|
|
6
|
+
* tsx src/swe-arena/run-experiment.mts --dry-run-parity [workDir]
|
|
7
|
+
*
|
|
8
|
+
* Per instance, sequentially: ledger-skip resume → endpoint capacity gates
|
|
9
|
+
* (supervisor arms gate on the ROUTER path too — probing z.ai alone was the
|
|
10
|
+
* proven blind spot) → solo arm → serialized judge → supervisor arm →
|
|
11
|
+
* serialized judge → append one typed LedgerRow (M1 schema) to the ledger.
|
|
12
|
+
*
|
|
13
|
+
* DRY-RUN PARITY (the M2 gate): `--dry-run-parity` executes NO arms and spends
|
|
14
|
+
* NO model tokens. It replays patch extraction + official judging for two
|
|
15
|
+
* committed fixture patches (pallets__flask-5014 SOLO — resolved;
|
|
16
|
+
* pydata__xarray-4687 SUP — unresolved) through materialize → apply →
|
|
17
|
+
* extractPatch → serialized-judge, and checks the verdicts against the pinned
|
|
18
|
+
* M1 fixtures. Docker time only.
|
|
19
|
+
*
|
|
20
|
+
* TODO(operator approval): full 12-instance parity re-run — re-execute both
|
|
21
|
+
* arms on the same 12 instances through this typed path and diff the resulting
|
|
22
|
+
* ledger against fixtures/ledger.jsonl. Costs ~$1 in model spend + ~4h wall;
|
|
23
|
+
* do not launch without an explicit operator go.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
27
|
+
import { tmpdir } from 'node:os'
|
|
28
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
29
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
30
|
+
import { createSweBenchAdapter } from '../benchmarks/swe-bench.ts'
|
|
31
|
+
import { exportBaseTree } from './factory-judge-child.mts'
|
|
32
|
+
import { runFactoryCommand } from './factory-command-container.ts'
|
|
33
|
+
import { loadFactoryInstances, type LoadedFactoryInstance } from './fixtures.ts'
|
|
34
|
+
import { run, runOk } from './proc.ts'
|
|
35
|
+
import {
|
|
36
|
+
extractPatch,
|
|
37
|
+
loadExcludes,
|
|
38
|
+
runSoloArm,
|
|
39
|
+
runSupervisorArm,
|
|
40
|
+
type ExecutableArmSpec,
|
|
41
|
+
type SecretsEnv,
|
|
42
|
+
type SoloArmResult,
|
|
43
|
+
type SoloArmSpec,
|
|
44
|
+
type SupervisorArmResult,
|
|
45
|
+
type SupervisorArmSpec,
|
|
46
|
+
} from './arms.ts'
|
|
47
|
+
import type { FactoryJudgeResult } from './factory-judge-child.mts'
|
|
48
|
+
import { applyPatchWithFallback } from './calibrate.ts'
|
|
49
|
+
import { gatesForArmKind, waitForCapacity } from './capacity.ts'
|
|
50
|
+
import { materializeWorkspace } from './materialize.ts'
|
|
51
|
+
import {
|
|
52
|
+
createSerializedJudge,
|
|
53
|
+
type JudgeVerdict,
|
|
54
|
+
type SerializedJudge,
|
|
55
|
+
} from './serialized-judge.ts'
|
|
56
|
+
import {
|
|
57
|
+
reportSupervisorRound,
|
|
58
|
+
writeSupervisorRunReportSafe,
|
|
59
|
+
} from '@tangle-network/agent-eval/supervisor-run'
|
|
60
|
+
import type { LedgerRow } from './types.ts'
|
|
61
|
+
|
|
62
|
+
const fixturesDir = fileURLToPath(new URL('./fixtures', import.meta.url))
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Instance images (fixtures/instances.json, vendored from the experiment).
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export interface InstanceImageEntry {
|
|
69
|
+
repo: string
|
|
70
|
+
base_commit: string
|
|
71
|
+
image: string
|
|
72
|
+
environment_setup_commit: string | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadInstanceImages(path?: string): Promise<Record<string, InstanceImageEntry>> {
|
|
76
|
+
const raw = await readFile(path ?? join(fixturesDir, 'instances.json'), 'utf8')
|
|
77
|
+
return JSON.parse(raw) as Record<string, InstanceImageEntry>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Ledger row assembly + resume.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One paired LedgerRow from the two arm results + judge verdicts — the exact
|
|
86
|
+
* field mapping run-instance.sh wrote. Throws on an inconclusive verdict
|
|
87
|
+
* (resolved: null): an infra failure must abort the pair, never be written
|
|
88
|
+
* into a boolean column.
|
|
89
|
+
*/
|
|
90
|
+
export function buildLedgerRow(
|
|
91
|
+
solo: SoloArmResult,
|
|
92
|
+
soloVerdict: JudgeVerdict,
|
|
93
|
+
sup: SupervisorArmResult,
|
|
94
|
+
supVerdict: JudgeVerdict,
|
|
95
|
+
): LedgerRow {
|
|
96
|
+
if (solo.iid !== sup.iid) throw new Error(`ledger row: arm iid mismatch ${solo.iid} vs ${sup.iid}`)
|
|
97
|
+
if (soloVerdict.resolved === null || supVerdict.resolved === null) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`ledger row ${solo.iid}: inconclusive judge verdict (solo=${soloVerdict.resolved}, sup=${supVerdict.resolved}) — not writing a fabricated boolean`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
const SUP_STATUSES = ['completed', 'running', 'failed', 'cancelled', null] as const
|
|
103
|
+
const SUP_VERDICTS = ['delivered', 'no-winner', 'best-effort', null] as const
|
|
104
|
+
if (!SUP_STATUSES.includes(sup.sup_status as (typeof SUP_STATUSES)[number])) {
|
|
105
|
+
throw new Error(`ledger row ${solo.iid}: unknown sup_status ${JSON.stringify(sup.sup_status)} — loops contract changed?`)
|
|
106
|
+
}
|
|
107
|
+
if (!SUP_VERDICTS.includes(sup.sup_verdict as (typeof SUP_VERDICTS)[number])) {
|
|
108
|
+
throw new Error(`ledger row ${solo.iid}: unknown sup_verdict ${JSON.stringify(sup.sup_verdict)} — loops contract changed?`)
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
iid: solo.iid,
|
|
112
|
+
solo_resolved: soloVerdict.resolved,
|
|
113
|
+
sup_resolved: supVerdict.resolved,
|
|
114
|
+
solo_verify_pass: solo.verify_pass,
|
|
115
|
+
sup_verify_pass: sup.verify_pass,
|
|
116
|
+
solo_patch_lines: solo.patch_lines,
|
|
117
|
+
sup_patch_lines: sup.patch_lines,
|
|
118
|
+
solo_wall_s: solo.wall_s,
|
|
119
|
+
sup_wall_s: sup.wall_s,
|
|
120
|
+
solo_tokens: solo.usage.total_io,
|
|
121
|
+
solo_usage: solo.usage,
|
|
122
|
+
sup_spentTokens: sup.spentTokens,
|
|
123
|
+
sup_spentUsd: sup.spentUsd,
|
|
124
|
+
sup_spawned: sup.spawned,
|
|
125
|
+
sup_workers: sup.workers,
|
|
126
|
+
sup_settled: sup.settled,
|
|
127
|
+
sup_subtasks: sup.subtasks,
|
|
128
|
+
sup_delivered: sup.delivered,
|
|
129
|
+
sup_status: sup.sup_status as LedgerRow['sup_status'],
|
|
130
|
+
sup_verdict: sup.sup_verdict as LedgerRow['sup_verdict'],
|
|
131
|
+
solo_oc_rc: solo.oc_rc,
|
|
132
|
+
sup_driver_rc: sup.driver_rc,
|
|
133
|
+
solo_patch: solo.patchPath,
|
|
134
|
+
sup_patch: sup.patchPath,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** iids already present in the ledger (resume-skip, orchestrate.sh semantics). */
|
|
139
|
+
export async function ledgerIids(ledgerPath: string): Promise<Set<string>> {
|
|
140
|
+
const raw = await readFile(ledgerPath, 'utf8').catch(() => '')
|
|
141
|
+
const iids = new Set<string>()
|
|
142
|
+
for (const line of raw.split('\n')) {
|
|
143
|
+
if (!line.trim()) continue
|
|
144
|
+
try {
|
|
145
|
+
const row = JSON.parse(line) as { iid?: string }
|
|
146
|
+
if (typeof row.iid === 'string') iids.add(row.iid)
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error(`corrupt ledger line in ${ledgerPath}: ${line.slice(0, 120)}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return iids
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Experiment config + loop.
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
export interface ExperimentConfig {
|
|
159
|
+
instances: string[]
|
|
160
|
+
/** Exactly one solo and one supervisor arm (the paired-ledger contract). */
|
|
161
|
+
arms: ExecutableArmSpec[]
|
|
162
|
+
ledgerPath: string
|
|
163
|
+
outDir: string
|
|
164
|
+
secretsDir: string
|
|
165
|
+
envFiles: string[]
|
|
166
|
+
/** Per-instance self-repro verify scripts: <verifyDir>/<iid>.sh. */
|
|
167
|
+
verifyDir: string
|
|
168
|
+
/** Override fixtures/instances.json (image map). */
|
|
169
|
+
instanceImagesPath?: string
|
|
170
|
+
judgeTimeoutMs?: number
|
|
171
|
+
gateWaitCeilingMs?: number
|
|
172
|
+
/** Probe model id. Defaults per-endpoint in capacity.ts. */
|
|
173
|
+
capacityModel?: string
|
|
174
|
+
/** Pause between instances (orchestrate.sh: 15s, gentle on the shared key). */
|
|
175
|
+
cooldownMs?: number
|
|
176
|
+
/**
|
|
177
|
+
* Run log the per-cell run-report headline is appended to. Defaults to
|
|
178
|
+
* `<outDir>/run.log`; the headline is always echoed to stdout as well, so a
|
|
179
|
+
* shell-redirected log gets it either way.
|
|
180
|
+
*/
|
|
181
|
+
runLogPath?: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function armPair(arms: ExecutableArmSpec[]): { solo: SoloArmSpec; sup: SupervisorArmSpec } {
|
|
185
|
+
const solo = arms.filter((a): a is SoloArmSpec => a.kind === 'solo')
|
|
186
|
+
const sup = arms.filter((a): a is SupervisorArmSpec => a.kind === 'supervisor')
|
|
187
|
+
if (solo.length !== 1 || sup.length !== 1) {
|
|
188
|
+
throw new Error(`expected exactly one solo + one supervisor arm, got ${arms.map((a) => a.kind).join(', ')}`)
|
|
189
|
+
}
|
|
190
|
+
return { solo: solo[0], sup: sup[0] }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function runExperiment(config: ExperimentConfig): Promise<void> {
|
|
194
|
+
const { solo, sup } = armPair(config.arms)
|
|
195
|
+
const secrets: SecretsEnv = { secretsDir: config.secretsDir, envFiles: config.envFiles }
|
|
196
|
+
const excludes = await loadExcludes()
|
|
197
|
+
const images = await loadInstanceImages(config.instanceImagesPath)
|
|
198
|
+
const judge = createSerializedJudge({
|
|
199
|
+
...(config.judgeTimeoutMs !== undefined ? { timeoutMs: config.judgeTimeoutMs } : {}),
|
|
200
|
+
})
|
|
201
|
+
const adapter = createSweBenchAdapter()
|
|
202
|
+
const log = (msg: string): void => console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`)
|
|
203
|
+
|
|
204
|
+
const done = await ledgerIids(config.ledgerPath)
|
|
205
|
+
const pending = config.instances.filter((iid) => !done.has(iid))
|
|
206
|
+
for (const iid of config.instances.filter((i) => done.has(i))) log(`SKIP ${iid} (already in ledger)`)
|
|
207
|
+
if (pending.length === 0) {
|
|
208
|
+
log('nothing to do — all instances already in ledger')
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// One dataset load for all pending instances (problem statements + metadata).
|
|
213
|
+
const tasks = await adapter.loadTasks({ ids: pending, split: 'test' })
|
|
214
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]))
|
|
215
|
+
|
|
216
|
+
for (const iid of pending) {
|
|
217
|
+
const task = taskById.get(iid)
|
|
218
|
+
if (!task) throw new Error(`instance ${iid} not found in SWE-bench_Verified`)
|
|
219
|
+
const entry = images[iid]
|
|
220
|
+
if (!entry) throw new Error(`instance ${iid} has no image mapping (instances.json)`)
|
|
221
|
+
const problemStatement = String(task.metadata?.problem_statement ?? '')
|
|
222
|
+
if (!problemStatement) throw new Error(`instance ${iid}: empty problem_statement`)
|
|
223
|
+
|
|
224
|
+
// Capacity gates: worker path always; router path because a supervisor arm runs.
|
|
225
|
+
const gateOpts = {
|
|
226
|
+
...(config.gateWaitCeilingMs !== undefined ? { waitCeilingMs: config.gateWaitCeilingMs } : {}),
|
|
227
|
+
...(config.capacityModel !== undefined ? { model: config.capacityModel } : {}),
|
|
228
|
+
onStatus: log,
|
|
229
|
+
}
|
|
230
|
+
for (const gate of gatesForArmKind('supervisor', secrets, gateOpts)) {
|
|
231
|
+
if (!(await waitForCapacity(gate))) {
|
|
232
|
+
log(`NO CAPACITY on ${gate.name} within ceiling — stopping before ${iid} (resume later)`)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ctx = {
|
|
238
|
+
instanceId: iid,
|
|
239
|
+
image: entry.image,
|
|
240
|
+
baseCommit: entry.base_commit,
|
|
241
|
+
problemStatement,
|
|
242
|
+
verifyCmd: `bash ${join(config.verifyDir, `${iid}.sh`)}`,
|
|
243
|
+
outDir: config.outDir,
|
|
244
|
+
secrets,
|
|
245
|
+
excludes,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
log(`>>> ${iid} SOLO arm`)
|
|
249
|
+
const soloResult = await runSoloArm(solo, ctx)
|
|
250
|
+
const soloVerdict = await judge.judge(iid, soloResult.patchPath, 'solo')
|
|
251
|
+
log(`${iid} SOLO judged: ${JSON.stringify(soloVerdict)}`)
|
|
252
|
+
|
|
253
|
+
log(`>>> ${iid} SUP arm`)
|
|
254
|
+
const supResult = await runSupervisorArm(sup, ctx)
|
|
255
|
+
const supVerdict = await judge.judge(iid, supResult.patchPath, 'sup')
|
|
256
|
+
log(`${iid} SUP judged: ${JSON.stringify(supVerdict)}`)
|
|
257
|
+
|
|
258
|
+
const row = buildLedgerRow(soloResult, soloVerdict, supResult, supVerdict)
|
|
259
|
+
await appendFile(config.ledgerPath, JSON.stringify(row) + '\n')
|
|
260
|
+
log(`LEDGER_ROW ${iid} solo=${row.solo_resolved} sup=${row.sup_resolved}`)
|
|
261
|
+
|
|
262
|
+
// Deterministic run observability: never hand-grep a journal for steers/waves/
|
|
263
|
+
// idle/cost again. Best-effort — a reporting failure can't lose a finished cell.
|
|
264
|
+
await writeSupervisorRunReportSafe(join(config.outDir, 'runs', iid, sup.name), {
|
|
265
|
+
appendHeadlineTo: config.runLogPath ?? join(config.outDir, 'run.log'),
|
|
266
|
+
ledgerPath: config.ledgerPath,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
await new Promise((r) => setTimeout(r, config.cooldownMs ?? 15_000))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await reportSupervisorRound(config.outDir, {
|
|
273
|
+
appendHeadlineTo: config.runLogPath ?? join(config.outDir, 'run.log'),
|
|
274
|
+
ledgerPath: config.ledgerPath,
|
|
275
|
+
title: 'Round rollup — paired solo/supervisor experiment',
|
|
276
|
+
echo: true,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Dry-run parity — the M2 gate. No arms, no tokens; docker only.
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
export interface ParityCaseSpec {
|
|
285
|
+
iid: string
|
|
286
|
+
arm: 'solo' | 'sup'
|
|
287
|
+
/** Committed patch fixture, relative to fixtures/ (e.g. patches/x.solo.patch). */
|
|
288
|
+
patchFixture: string
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface ParityCaseResult {
|
|
292
|
+
iid: string
|
|
293
|
+
arm: 'solo' | 'sup'
|
|
294
|
+
applyRc: number
|
|
295
|
+
fixtureFiles: string[]
|
|
296
|
+
extractedFiles: string[]
|
|
297
|
+
extractedPatchLines: number
|
|
298
|
+
verdict: JudgeVerdict
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The two pinned parity cases: one resolved SOLO patch, one unresolved SUP patch. */
|
|
302
|
+
export const PARITY_CASES: ParityCaseSpec[] = [
|
|
303
|
+
{ iid: 'pallets__flask-5014', arm: 'solo', patchFixture: 'patches/pallets__flask-5014.solo.patch' },
|
|
304
|
+
{ iid: 'pydata__xarray-4687', arm: 'sup', patchFixture: 'patches/pydata__xarray-4687.sup.patch' },
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
/** Changed paths of a unified diff (b/ side), for extraction-parity checks. */
|
|
308
|
+
export function diffChangedFiles(patch: string): string[] {
|
|
309
|
+
const files = new Set<string>()
|
|
310
|
+
for (const m of patch.matchAll(/^diff --git a\/.+ b\/(.+)$/gm)) files.add(m[1])
|
|
311
|
+
return [...files].sort()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Replay extraction + judging for committed patches WITHOUT running any arm:
|
|
316
|
+
* materialize the instance workspace from its image, apply the committed
|
|
317
|
+
* patch, re-extract it via the arms.ts extraction path, then grade the
|
|
318
|
+
* re-extracted patch with the serialized judge. Byte-identical output is not
|
|
319
|
+
* required (git normalizes); the changed-file set and the official verdict are.
|
|
320
|
+
*/
|
|
321
|
+
export async function replayPatchParity(
|
|
322
|
+
cases: ParityCaseSpec[],
|
|
323
|
+
opts: { workDir: string; judge?: SerializedJudge; keepWorkspaces?: boolean },
|
|
324
|
+
): Promise<ParityCaseResult[]> {
|
|
325
|
+
const judge = opts.judge ?? createSerializedJudge()
|
|
326
|
+
const excludes = await loadExcludes()
|
|
327
|
+
const images = await loadInstanceImages()
|
|
328
|
+
const results: ParityCaseResult[] = []
|
|
329
|
+
for (const c of cases) {
|
|
330
|
+
const entry = images[c.iid]
|
|
331
|
+
if (!entry) throw new Error(`parity: no image mapping for ${c.iid}`)
|
|
332
|
+
const fixturePatchPath = join(fixturesDir, c.patchFixture)
|
|
333
|
+
const fixturePatch = await readFile(fixturePatchPath, 'utf8')
|
|
334
|
+
const ws = join(opts.workDir, `parity-${c.iid}-${c.arm}`)
|
|
335
|
+
try {
|
|
336
|
+
await materializeWorkspace({
|
|
337
|
+
instanceId: c.iid,
|
|
338
|
+
image: entry.image,
|
|
339
|
+
baseCommit: entry.base_commit,
|
|
340
|
+
dest: ws,
|
|
341
|
+
})
|
|
342
|
+
const applyRc = await applyPatchWithFallback(ws, fixturePatchPath)
|
|
343
|
+
if (applyRc !== 0) throw new Error(`parity ${c.iid}: committed patch failed to apply (rc=${applyRc})`)
|
|
344
|
+
const extracted = await extractPatch(ws, entry.base_commit, excludes)
|
|
345
|
+
const extractedPath = join(opts.workDir, `parity-${c.iid}.${c.arm}.extracted.patch`)
|
|
346
|
+
await writeFile(extractedPath, extracted)
|
|
347
|
+
const verdict = await judge.judge(c.iid, extractedPath, `parity-${c.arm}`)
|
|
348
|
+
results.push({
|
|
349
|
+
iid: c.iid,
|
|
350
|
+
arm: c.arm,
|
|
351
|
+
applyRc,
|
|
352
|
+
fixtureFiles: diffChangedFiles(fixturePatch),
|
|
353
|
+
extractedFiles: diffChangedFiles(extracted),
|
|
354
|
+
extractedPatchLines: extracted.length === 0 ? 0 : extracted.split('\n').length - 1,
|
|
355
|
+
verdict,
|
|
356
|
+
})
|
|
357
|
+
} finally {
|
|
358
|
+
if (!opts.keepWorkspaces) await rm(ws, { recursive: true, force: true })
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return results
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Factory-bench: worker workspace + experiment loop.
|
|
366
|
+
//
|
|
367
|
+
// The worker cell for a factory instance is the `git archive` export of the
|
|
368
|
+
// base commit re-initialized as a FRESH git repo with one synthetic commit —
|
|
369
|
+
// worker tooling that expects git works, but `git log`/refs cannot leak the
|
|
370
|
+
// real repo's future history (the PR's impl and tests live only on the
|
|
371
|
+
// judge-side mirror). SPEC.md (the rewritten PM-ticket spec) is part of that
|
|
372
|
+
// initial commit. Everything downstream — arm runners, budgets, serialized
|
|
373
|
+
// judge queue/ceiling, ledger resume — is the same machinery as swe-arena.
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
/** Ref name the synthetic initial commit is pinned to; the arm's diff base. */
|
|
377
|
+
export const FACTORY_BASE_REF = 'factory-base'
|
|
378
|
+
|
|
379
|
+
export interface FactoryWorkspace {
|
|
380
|
+
/** Sha of the synthetic initial commit (== FACTORY_BASE_REF). */
|
|
381
|
+
syntheticBase: string
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Materialize a worker workspace for a factory instance: archive-export the
|
|
386
|
+
* base tree, add SPEC.md, re-init as a synthetic-history repo (single commit,
|
|
387
|
+
* no remotes), then pre-run `setup_cmds` so the worker starts on installed
|
|
388
|
+
* deps. The real repo's refs/objects are unreachable by construction — the
|
|
389
|
+
* leak test greps the workspace for them after setup.
|
|
390
|
+
*/
|
|
391
|
+
export async function materializeFactoryWorkspace(
|
|
392
|
+
inst: LoadedFactoryInstance,
|
|
393
|
+
dest: string,
|
|
394
|
+
opts: { setup?: boolean } = {},
|
|
395
|
+
): Promise<FactoryWorkspace> {
|
|
396
|
+
await rm(dest, { recursive: true, force: true })
|
|
397
|
+
await mkdir(dirname(dest), { recursive: true })
|
|
398
|
+
await exportBaseTree(inst.repo_local_mirror, inst.base_commit, dest)
|
|
399
|
+
await writeFile(join(dest, 'SPEC.md'), inst.spec)
|
|
400
|
+
|
|
401
|
+
await runOk('git', ['-C', dest, 'init', '-q', '-b', 'work'])
|
|
402
|
+
await runOk('git', ['-C', dest, 'config', 'user.email', 'factory-bench@local'])
|
|
403
|
+
await runOk('git', ['-C', dest, 'config', 'user.name', 'factory-bench'])
|
|
404
|
+
await runOk('git', ['-C', dest, 'add', '-A'])
|
|
405
|
+
await runOk('git', ['-C', dest, 'commit', '-q', '-m', 'baseline workspace'])
|
|
406
|
+
await runOk('git', ['-C', dest, 'branch', '-f', FACTORY_BASE_REF, 'HEAD'])
|
|
407
|
+
const syntheticBase = (await runOk('git', ['-C', dest, 'rev-parse', 'HEAD'])).stdout.trim()
|
|
408
|
+
if (syntheticBase === inst.base_commit) {
|
|
409
|
+
throw new Error(`factory workspace ${inst.id}: synthetic base equals the real base commit — history leaked`)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (opts.setup !== false && inst.setup_cmds.length > 0) {
|
|
413
|
+
for (const cmd of inst.setup_cmds) {
|
|
414
|
+
const res = await runFactoryCommand(dest, cmd, {
|
|
415
|
+
image: inst.command_image,
|
|
416
|
+
network: 'enabled',
|
|
417
|
+
timeoutMs: inst.timeout_s * 1000,
|
|
418
|
+
})
|
|
419
|
+
if (res.code !== 0) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`factory workspace ${inst.id}: setup_cmd failed (rc=${res.code}): ${cmd}\n${(res.stderr || res.stdout).slice(-2000)}`,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { syntheticBase }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** One appended line of the factory ledger (JSONL, resume key = iid + rep). */
|
|
430
|
+
export interface FactoryLedgerRow {
|
|
431
|
+
at: string
|
|
432
|
+
iid: string
|
|
433
|
+
rep: number
|
|
434
|
+
arm: string
|
|
435
|
+
resolved: boolean
|
|
436
|
+
/** passed / calibrated total — the dense partial-credit signal. */
|
|
437
|
+
score: number
|
|
438
|
+
passed: number | null
|
|
439
|
+
total: number | null
|
|
440
|
+
verify_pass: boolean
|
|
441
|
+
patch_lines: number
|
|
442
|
+
wall_s: number
|
|
443
|
+
judge_secs: number | null
|
|
444
|
+
judge_attempts: number | null
|
|
445
|
+
driver_rc: number
|
|
446
|
+
sup_status: string | null
|
|
447
|
+
sup_verdict: string | null
|
|
448
|
+
delivered: boolean | null
|
|
449
|
+
spentTokens: number | null
|
|
450
|
+
spentUsd: number | null
|
|
451
|
+
spawned: number
|
|
452
|
+
workers: number
|
|
453
|
+
settled: number
|
|
454
|
+
patchPath: string
|
|
455
|
+
runDir: string
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** `iid#r<rep>` keys already in a factory ledger (resume-skip). */
|
|
459
|
+
export async function factoryLedgerKeys(ledgerPath: string): Promise<Set<string>> {
|
|
460
|
+
const raw = await readFile(ledgerPath, 'utf8').catch(() => '')
|
|
461
|
+
const keys = new Set<string>()
|
|
462
|
+
for (const line of raw.split('\n')) {
|
|
463
|
+
if (!line.trim()) continue
|
|
464
|
+
let row: { iid?: string; rep?: number }
|
|
465
|
+
try {
|
|
466
|
+
row = JSON.parse(line) as { iid?: string; rep?: number }
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Error(`corrupt factory ledger line in ${ledgerPath}: ${line.slice(0, 120)}`)
|
|
469
|
+
}
|
|
470
|
+
if (typeof row.iid === 'string' && typeof row.rep === 'number') keys.add(`${row.iid}#r${row.rep}`)
|
|
471
|
+
}
|
|
472
|
+
return keys
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface FactoryArmConfig {
|
|
476
|
+
workerModel: string
|
|
477
|
+
driverModel: string
|
|
478
|
+
budget?: number
|
|
479
|
+
maxSandboxes?: number
|
|
480
|
+
maxUsd?: number
|
|
481
|
+
maxDepth?: number
|
|
482
|
+
timeoutMs?: number
|
|
483
|
+
envKnobs?: Record<string, string>
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export interface FactoryExperimentConfig {
|
|
487
|
+
/** Instance-dir root; relative paths resolve against the config file. */
|
|
488
|
+
instancesDir: string
|
|
489
|
+
/** Manifest ids to run (subset of instancesDir). */
|
|
490
|
+
instances: string[]
|
|
491
|
+
repsPerInstance: number
|
|
492
|
+
armName: string
|
|
493
|
+
arm: FactoryArmConfig
|
|
494
|
+
/** The loops checkout in the supervisor seat (baseline = loops main). */
|
|
495
|
+
loopsRepo: string
|
|
496
|
+
ledgerPath: string
|
|
497
|
+
outDir: string
|
|
498
|
+
secretsDir: string
|
|
499
|
+
envFiles: string[]
|
|
500
|
+
/**
|
|
501
|
+
* Worker-side self-check per instance (the supervisor's internal verify
|
|
502
|
+
* gate). NEVER the judge tests — those stay hidden. Default `true` (no gate).
|
|
503
|
+
*/
|
|
504
|
+
verifyCmds?: Record<string, string>
|
|
505
|
+
judgeTimeoutMs?: number
|
|
506
|
+
cooldownMs?: number
|
|
507
|
+
gateWaitCeilingMs?: number
|
|
508
|
+
capacityModel?: string
|
|
509
|
+
/** Run log the per-cell run-report headline is appended to (default `<outDir>/run.log`). */
|
|
510
|
+
runLogPath?: string
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const factoryJudgeChildPath = fileURLToPath(new URL('./factory-judge-child.mts', import.meta.url))
|
|
514
|
+
const benchRootDir = fileURLToPath(new URL('../..', import.meta.url))
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Serialized judge whose child is factory-judge-child.mts — same JUDGE_RESULT
|
|
518
|
+
* line protocol, queue, retry, and SIGKILL ceiling as the swebench judge. The
|
|
519
|
+
* 1800s ceiling floor is kept as the backstop; the child self-enforces the
|
|
520
|
+
* manifest's (much smaller) per-command timeout_s inside it.
|
|
521
|
+
*/
|
|
522
|
+
export function createFactoryJudge(
|
|
523
|
+
instances: LoadedFactoryInstance[],
|
|
524
|
+
opts: { timeoutMs?: number } = {},
|
|
525
|
+
): SerializedJudge {
|
|
526
|
+
const dirById = new Map(instances.map((i) => [i.id, i.dir]))
|
|
527
|
+
return createSerializedJudge({
|
|
528
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
529
|
+
lockFile: join(tmpdir(), 'factory-arena-judge.lock'),
|
|
530
|
+
command: (iid, patchPath) => {
|
|
531
|
+
const dir = dirById.get(iid)
|
|
532
|
+
if (!dir) throw new Error(`factory judge: unknown instance id ${iid}`)
|
|
533
|
+
return { bin: 'node', argv: ['--import', 'tsx', factoryJudgeChildPath, dir, patchPath], cwd: benchRootDir }
|
|
534
|
+
},
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Factory gen0 loop: per (instance × rep), sequentially — ledger resume →
|
|
540
|
+
* capacity gates → supervisor arm on a factory workspace → factory judge →
|
|
541
|
+
* one FactoryLedgerRow appended. Structure mirrors runExperiment.
|
|
542
|
+
*/
|
|
543
|
+
export async function runFactoryExperiment(
|
|
544
|
+
config: FactoryExperimentConfig,
|
|
545
|
+
opts: { configDir?: string; only?: string[]; repsOverride?: number } = {},
|
|
546
|
+
): Promise<void> {
|
|
547
|
+
const log = (msg: string): void => console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`)
|
|
548
|
+
const baseDir = opts.configDir ?? process.cwd()
|
|
549
|
+
const instancesDir = isAbsolute(config.instancesDir) ? config.instancesDir : resolve(baseDir, config.instancesDir)
|
|
550
|
+
const all = loadFactoryInstances(instancesDir)
|
|
551
|
+
const byId = new Map(all.map((i) => [i.id, i]))
|
|
552
|
+
const wanted = (opts.only ?? config.instances).map((id) => {
|
|
553
|
+
const inst = byId.get(id)
|
|
554
|
+
if (!inst) throw new Error(`factory config: instance ${id} not found under ${instancesDir}`)
|
|
555
|
+
return inst
|
|
556
|
+
})
|
|
557
|
+
const reps = opts.repsOverride ?? config.repsPerInstance
|
|
558
|
+
if (!Number.isInteger(reps) || reps < 1) throw new Error(`repsPerInstance must be an integer ≥ 1, got ${reps}`)
|
|
559
|
+
|
|
560
|
+
const secrets: SecretsEnv = { secretsDir: config.secretsDir, envFiles: config.envFiles }
|
|
561
|
+
const judge = createFactoryJudge(all, {
|
|
562
|
+
...(config.judgeTimeoutMs !== undefined ? { timeoutMs: config.judgeTimeoutMs } : {}),
|
|
563
|
+
})
|
|
564
|
+
await mkdir(config.outDir, { recursive: true })
|
|
565
|
+
await mkdir(dirname(config.ledgerPath), { recursive: true })
|
|
566
|
+
const done = await factoryLedgerKeys(config.ledgerPath)
|
|
567
|
+
|
|
568
|
+
for (const inst of wanted) {
|
|
569
|
+
for (let rep = 0; rep < reps; rep += 1) {
|
|
570
|
+
const key = `${inst.id}#r${rep}`
|
|
571
|
+
if (done.has(key)) {
|
|
572
|
+
log(`SKIP ${key} (already in ledger)`)
|
|
573
|
+
continue
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const gateOpts = {
|
|
577
|
+
...(config.gateWaitCeilingMs !== undefined ? { waitCeilingMs: config.gateWaitCeilingMs } : {}),
|
|
578
|
+
...(config.capacityModel !== undefined ? { model: config.capacityModel } : {}),
|
|
579
|
+
onStatus: log,
|
|
580
|
+
}
|
|
581
|
+
for (const gate of gatesForArmKind('supervisor', secrets, gateOpts)) {
|
|
582
|
+
if (!(await waitForCapacity(gate))) {
|
|
583
|
+
log(`NO CAPACITY on ${gate.name} within ceiling — stopping before ${key} (resume later)`)
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const spec: SupervisorArmSpec = {
|
|
589
|
+
kind: 'supervisor',
|
|
590
|
+
name: config.armName,
|
|
591
|
+
workerModel: config.arm.workerModel,
|
|
592
|
+
driverModel: config.arm.driverModel,
|
|
593
|
+
budget: config.arm.budget,
|
|
594
|
+
maxSandboxes: config.arm.maxSandboxes,
|
|
595
|
+
maxUsd: config.arm.maxUsd,
|
|
596
|
+
maxDepth: config.arm.maxDepth,
|
|
597
|
+
...(config.arm.envKnobs ? { envKnobs: config.arm.envKnobs } : {}),
|
|
598
|
+
loopsRepo: config.loopsRepo,
|
|
599
|
+
timeoutMs: config.arm.timeoutMs,
|
|
600
|
+
}
|
|
601
|
+
const armOutDir = join(config.outDir, `rep-${rep}`)
|
|
602
|
+
log(`>>> ${config.armName} ${inst.id} rep=${rep}`)
|
|
603
|
+
const armRes = await runSupervisorArm(spec, {
|
|
604
|
+
instanceId: inst.id,
|
|
605
|
+
image: `factory-archive:${inst.id}`,
|
|
606
|
+
// The synthetic-history ref, NOT the real base sha: patch extraction
|
|
607
|
+
// diffs against the workspace's own single commit.
|
|
608
|
+
baseCommit: FACTORY_BASE_REF,
|
|
609
|
+
materialize: async (dest) => {
|
|
610
|
+
await materializeFactoryWorkspace(inst, dest)
|
|
611
|
+
},
|
|
612
|
+
problemStatement: inst.spec,
|
|
613
|
+
verifyCmd: config.verifyCmds?.[inst.id] ?? 'true',
|
|
614
|
+
outDir: armOutDir,
|
|
615
|
+
secrets,
|
|
616
|
+
// SPEC.md is workspace furniture, not worker product.
|
|
617
|
+
excludes: [':(exclude)SPEC.md'],
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
const factoryRunDir = join(armOutDir, 'runs', inst.id, config.armName)
|
|
621
|
+
const { ws: _ws, ...armSummary } = armRes
|
|
622
|
+
await writeFile(join(factoryRunDir, 'result.json'), JSON.stringify(armSummary, null, 1)).catch(() => {})
|
|
623
|
+
|
|
624
|
+
const verdict = await judge.judge(inst.id, armRes.patchPath, `${config.armName}-r${rep}`)
|
|
625
|
+
log(`${inst.id} r${rep} judged: ${JSON.stringify(verdict)}`)
|
|
626
|
+
await writeFile(join(factoryRunDir, 'judge.json'), JSON.stringify(verdict, null, 1)).catch(() => {})
|
|
627
|
+
if (verdict.resolved === null) {
|
|
628
|
+
throw new Error(`inconclusive factory judge verdict for ${key} (${verdict.error ?? 'unknown'}) — not writing a fabricated boolean`)
|
|
629
|
+
}
|
|
630
|
+
const fv = verdict as JudgeVerdict & Partial<FactoryJudgeResult>
|
|
631
|
+
const row: FactoryLedgerRow = {
|
|
632
|
+
at: new Date().toISOString(),
|
|
633
|
+
iid: inst.id,
|
|
634
|
+
rep,
|
|
635
|
+
arm: config.armName,
|
|
636
|
+
resolved: verdict.resolved,
|
|
637
|
+
score: typeof fv.score === 'number' ? fv.score : 0,
|
|
638
|
+
passed: typeof fv.passed === 'number' ? fv.passed : null,
|
|
639
|
+
total: typeof fv.total === 'number' ? fv.total : null,
|
|
640
|
+
verify_pass: armRes.verify_pass,
|
|
641
|
+
patch_lines: armRes.patch_lines,
|
|
642
|
+
wall_s: armRes.wall_s,
|
|
643
|
+
judge_secs: fv.secs ?? null,
|
|
644
|
+
judge_attempts: fv.attempts ?? null,
|
|
645
|
+
driver_rc: armRes.driver_rc,
|
|
646
|
+
sup_status: armRes.sup_status,
|
|
647
|
+
sup_verdict: armRes.sup_verdict,
|
|
648
|
+
delivered: armRes.delivered,
|
|
649
|
+
spentTokens: armRes.spentTokens,
|
|
650
|
+
spentUsd: armRes.spentUsd,
|
|
651
|
+
spawned: armRes.spawned,
|
|
652
|
+
workers: armRes.workers,
|
|
653
|
+
settled: armRes.settled,
|
|
654
|
+
patchPath: armRes.patchPath,
|
|
655
|
+
runDir: factoryRunDir,
|
|
656
|
+
}
|
|
657
|
+
await appendFile(config.ledgerPath, JSON.stringify(row) + '\n')
|
|
658
|
+
log(`LEDGER_ROW ${key} resolved=${row.resolved} score=${row.score} (${row.passed}/${row.total})`)
|
|
659
|
+
|
|
660
|
+
await writeSupervisorRunReportSafe(row.runDir, {
|
|
661
|
+
appendHeadlineTo: config.runLogPath ?? join(config.outDir, 'run.log'),
|
|
662
|
+
ledgerPath: config.ledgerPath,
|
|
663
|
+
patchPath: armRes.patchPath,
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
await new Promise((r) => setTimeout(r, config.cooldownMs ?? 15_000))
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
await reportSupervisorRound(config.outDir, {
|
|
671
|
+
appendHeadlineTo: config.runLogPath ?? join(config.outDir, 'run.log'),
|
|
672
|
+
ledgerPath: config.ledgerPath,
|
|
673
|
+
title: `Round rollup — factory ${config.armName}`,
|
|
674
|
+
echo: true,
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
// CLI.
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
683
|
+
|
|
684
|
+
if (isMain) {
|
|
685
|
+
const [arg, extra] = process.argv.slice(2)
|
|
686
|
+
if (arg === '--dry-run-parity') {
|
|
687
|
+
const workDir = extra ?? join(process.env.TMPDIR ?? '/tmp', 'swe-arena-parity')
|
|
688
|
+
await mkdir(workDir, { recursive: true })
|
|
689
|
+
const results = await replayPatchParity(PARITY_CASES, { workDir })
|
|
690
|
+
for (const r of results) {
|
|
691
|
+
console.log(
|
|
692
|
+
`PARITY ${r.iid} [${r.arm}] resolved=${r.verdict.resolved} score=${r.verdict.score} ` +
|
|
693
|
+
`files(fixture=${r.fixtureFiles.join(',')} extracted=${r.extractedFiles.join(',')})`,
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
} else if (arg === '--factory') {
|
|
697
|
+
if (!extra) {
|
|
698
|
+
console.error('usage: tsx src/swe-arena/run-experiment.mts --factory <config.json> [--only <iid> ...] [--reps <n>]')
|
|
699
|
+
process.exit(2)
|
|
700
|
+
}
|
|
701
|
+
const rest = process.argv.slice(4)
|
|
702
|
+
const only: string[] = []
|
|
703
|
+
let repsOverride: number | undefined
|
|
704
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
705
|
+
if (rest[i] === '--only' && rest[i + 1]) only.push(rest[(i += 1)]!)
|
|
706
|
+
else if (rest[i] === '--reps' && rest[i + 1]) repsOverride = Number(rest[(i += 1)])
|
|
707
|
+
else throw new Error(`unknown --factory flag: ${rest[i]}`)
|
|
708
|
+
}
|
|
709
|
+
const configPath = resolve(extra)
|
|
710
|
+
const config = JSON.parse(await readFile(configPath, 'utf8')) as FactoryExperimentConfig
|
|
711
|
+
await runFactoryExperiment(config, {
|
|
712
|
+
configDir: dirname(configPath),
|
|
713
|
+
...(only.length > 0 ? { only } : {}),
|
|
714
|
+
...(repsOverride !== undefined ? { repsOverride } : {}),
|
|
715
|
+
})
|
|
716
|
+
} else if (arg && !arg.startsWith('--')) {
|
|
717
|
+
const config = JSON.parse(await readFile(arg, 'utf8')) as ExperimentConfig
|
|
718
|
+
await runExperiment(config)
|
|
719
|
+
} else {
|
|
720
|
+
console.error(
|
|
721
|
+
'usage: tsx src/swe-arena/run-experiment.mts <config.json>\n' +
|
|
722
|
+
' tsx src/swe-arena/run-experiment.mts --factory <config.json> [--only <iid> ...] [--reps <n>]\n' +
|
|
723
|
+
' tsx src/swe-arena/run-experiment.mts --dry-run-parity [workDir]',
|
|
724
|
+
)
|
|
725
|
+
process.exit(2)
|
|
726
|
+
}
|
|
727
|
+
}
|