@tangle-network/agent-bench 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/README.md +7 -0
- package/dist/adapters.js +23 -23
- 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/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-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-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
- 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 +23 -23
- package/package.json +5 -5
- package/scripts/trata-hedge/README.md +6 -5
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +2 -2
- package/src/gate.ts +1 -1
- package/src/hev-eval.mts +5 -2
- package/src/hev-improve.mts +118 -73
- package/src/official-optimizer-config.mts +89 -0
- package/src/official-optimizer-config.test.mts +88 -0
- package/src/profiles.ts +2 -2
- 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 +342 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/smoke-structural-rollout.mts +15 -9
- package/src/swe-arena/activation.mts +225 -0
- package/src/swe-arena/activation.test.mts +300 -0
- package/src/swe-arena/arms.ts +30 -14
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +102 -1
- package/src/swe-arena/cell-evidence.mts +41 -9
- 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/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/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.ts +135 -2
- package/src/swe-arena/gepa-seat.mts +877 -0
- package/src/swe-arena/gepa-seat.test.mts +1059 -0
- package/src/swe-arena/implementation-ref.test.mts +64 -0
- package/src/swe-arena/implementation-ref.ts +62 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/outer-loop.mts +710 -33
- 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/proposer-fanout.mts +327 -31
- package/src/swe-arena/proposer-fanout.test.mts +246 -0
- package/src/swe-arena/proposer-provenance.mts +172 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/run-experiment.mts +367 -1
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.test.mts +5 -3
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/scratch-worktree.test.mts +55 -0
- package/src/swe-arena/scratch-worktree.ts +34 -0
- package/src/swe-arena/types.ts +52 -0
- package/src/swe-code-improve.mts +24 -25
- package/src/swe-improve.mts +129 -96
- package/src/swe-local-proof.mts +6 -1
- package/src/swe-stream.mts +4 -2
- package/src/swe-structural.mts +245 -837
- package/src/tb-container-executor.test.mts +30 -6
- package/src/tb-supervisor-sidecar.mts +2 -1
- package/src/trata-gepa.mts +182 -245
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/src/live-improve-campaign-mbpp.mts +0 -641
- package/src/live-improve-campaign.mts +0 -500
- /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-PWQVGAJB.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
|
@@ -24,9 +24,14 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
27
|
-
import {
|
|
27
|
+
import { tmpdir } from 'node:os'
|
|
28
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
28
29
|
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
29
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'
|
|
30
35
|
import {
|
|
31
36
|
extractPatch,
|
|
32
37
|
loadExcludes,
|
|
@@ -39,6 +44,7 @@ import {
|
|
|
39
44
|
type SupervisorArmResult,
|
|
40
45
|
type SupervisorArmSpec,
|
|
41
46
|
} from './arms.ts'
|
|
47
|
+
import type { FactoryJudgeResult } from './factory-judge-child.mts'
|
|
42
48
|
import { applyPatchWithFallback } from './calibrate.ts'
|
|
43
49
|
import { gatesForArmKind, waitForCapacity } from './capacity.ts'
|
|
44
50
|
import { materializeWorkspace } from './materialize.ts'
|
|
@@ -47,6 +53,10 @@ import {
|
|
|
47
53
|
type JudgeVerdict,
|
|
48
54
|
type SerializedJudge,
|
|
49
55
|
} from './serialized-judge.ts'
|
|
56
|
+
import {
|
|
57
|
+
reportSupervisorRound,
|
|
58
|
+
writeSupervisorRunReportSafe,
|
|
59
|
+
} from '@tangle-network/agent-eval/supervisor-run'
|
|
50
60
|
import type { LedgerRow } from './types.ts'
|
|
51
61
|
|
|
52
62
|
const fixturesDir = fileURLToPath(new URL('./fixtures', import.meta.url))
|
|
@@ -163,6 +173,12 @@ export interface ExperimentConfig {
|
|
|
163
173
|
capacityModel?: string
|
|
164
174
|
/** Pause between instances (orchestrate.sh: 15s, gentle on the shared key). */
|
|
165
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
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
function armPair(arms: ExecutableArmSpec[]): { solo: SoloArmSpec; sup: SupervisorArmSpec } {
|
|
@@ -242,8 +258,23 @@ export async function runExperiment(config: ExperimentConfig): Promise<void> {
|
|
|
242
258
|
const row = buildLedgerRow(soloResult, soloVerdict, supResult, supVerdict)
|
|
243
259
|
await appendFile(config.ledgerPath, JSON.stringify(row) + '\n')
|
|
244
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
|
+
|
|
245
269
|
await new Promise((r) => setTimeout(r, config.cooldownMs ?? 15_000))
|
|
246
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
|
+
})
|
|
247
278
|
}
|
|
248
279
|
|
|
249
280
|
// ---------------------------------------------------------------------------
|
|
@@ -330,6 +361,320 @@ export async function replayPatchParity(
|
|
|
330
361
|
return results
|
|
331
362
|
}
|
|
332
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
|
+
|
|
333
678
|
// ---------------------------------------------------------------------------
|
|
334
679
|
// CLI.
|
|
335
680
|
// ---------------------------------------------------------------------------
|
|
@@ -348,12 +693,33 @@ if (isMain) {
|
|
|
348
693
|
`files(fixture=${r.fixtureFiles.join(',')} extracted=${r.extractedFiles.join(',')})`,
|
|
349
694
|
)
|
|
350
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
|
+
})
|
|
351
716
|
} else if (arg && !arg.startsWith('--')) {
|
|
352
717
|
const config = JSON.parse(await readFile(arg, 'utf8')) as ExperimentConfig
|
|
353
718
|
await runExperiment(config)
|
|
354
719
|
} else {
|
|
355
720
|
console.error(
|
|
356
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' +
|
|
357
723
|
' tsx src/swe-arena/run-experiment.mts --dry-run-parity [workDir]',
|
|
358
724
|
)
|
|
359
725
|
process.exit(2)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI over the supervisor-run reader. NO metric logic lives here.
|
|
3
|
+
*
|
|
4
|
+
* The reader itself is `@tangle-network/agent-eval/supervisor-run` — a
|
|
5
|
+
* supervision tree is a rollout trace with one more dimension, so it sits in
|
|
6
|
+
* the trace-analysis layer next to single-rollout analysis, beside the
|
|
7
|
+
* `tangle.rollout.v1` ledger whose row type its tree nodes ARE. This file is
|
|
8
|
+
* only argv parsing plus the entry points the arenas already call.
|
|
9
|
+
*
|
|
10
|
+
* Harness-session view of the same run (model calls, latency, stuck loops,
|
|
11
|
+
* tool errors):
|
|
12
|
+
* `npx @tangle-network/traces analyze --harness opencode --cwd <worker-clone-cwd>`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { pathToFileURL } from 'node:url'
|
|
16
|
+
import {
|
|
17
|
+
renderSupervisorRunMarkdown,
|
|
18
|
+
reportSupervisorRound,
|
|
19
|
+
type WriteSupervisorRunOptions,
|
|
20
|
+
writeSupervisorRunReport,
|
|
21
|
+
} from '@tangle-network/agent-eval/supervisor-run'
|
|
22
|
+
|
|
23
|
+
const isMain =
|
|
24
|
+
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
25
|
+
|
|
26
|
+
if (isMain) {
|
|
27
|
+
const args = process.argv.slice(2)
|
|
28
|
+
const usage =
|
|
29
|
+
'usage:\n' +
|
|
30
|
+
' tsx run-report.mts <cellDir> [--log <run.log>] [--patch <file>] [--ledger <ledger.jsonl>] [--report-dir <dir>] [--no-opencode]\n' +
|
|
31
|
+
' tsx run-report.mts --round <outDir> [--log <run.log>] [--ledger <ledger.jsonl>] [--report-dir <dir>] [--no-opencode]\n' +
|
|
32
|
+
'\n--report-dir writes the reports outside the run directory (use it when the run dir is READ-ONLY).\n'
|
|
33
|
+
const noOpencode = args.includes('--no-opencode')
|
|
34
|
+
const flagValue = (flag: string): string | undefined => {
|
|
35
|
+
const i = args.indexOf(flag)
|
|
36
|
+
return i >= 0 ? args[i + 1] : undefined
|
|
37
|
+
}
|
|
38
|
+
const appendHeadlineTo = flagValue('--log')
|
|
39
|
+
const patchPath = flagValue('--patch')
|
|
40
|
+
const reportDir = flagValue('--report-dir')
|
|
41
|
+
const ledgerPath = flagValue('--ledger')
|
|
42
|
+
const flagValueIndices = new Set(
|
|
43
|
+
['--log', '--patch', '--report-dir', '--ledger', '--round']
|
|
44
|
+
.map((f) => args.indexOf(f))
|
|
45
|
+
.filter((i) => i >= 0)
|
|
46
|
+
.map((i) => i + 1),
|
|
47
|
+
)
|
|
48
|
+
const positional = args.filter((a, i) => !a.startsWith('--') && !flagValueIndices.has(i))
|
|
49
|
+
const roundIdx = args.indexOf('--round')
|
|
50
|
+
const opts: WriteSupervisorRunOptions = {
|
|
51
|
+
...(appendHeadlineTo !== undefined ? { appendHeadlineTo } : {}),
|
|
52
|
+
...(patchPath !== undefined ? { patchPath } : {}),
|
|
53
|
+
...(reportDir !== undefined ? { reportDir } : {}),
|
|
54
|
+
...(ledgerPath !== undefined ? { ledgerPath } : {}),
|
|
55
|
+
...(noOpencode ? { opencodeDb: null } : {}),
|
|
56
|
+
echo: true,
|
|
57
|
+
}
|
|
58
|
+
if (roundIdx >= 0) {
|
|
59
|
+
const outDir = args[roundIdx + 1]
|
|
60
|
+
if (outDir === undefined) {
|
|
61
|
+
console.error(usage)
|
|
62
|
+
process.exit(2)
|
|
63
|
+
}
|
|
64
|
+
await reportSupervisorRound(outDir, opts)
|
|
65
|
+
} else {
|
|
66
|
+
const cellDir = positional[0]
|
|
67
|
+
if (cellDir === undefined) {
|
|
68
|
+
console.error(usage)
|
|
69
|
+
process.exit(2)
|
|
70
|
+
}
|
|
71
|
+
const report = await writeSupervisorRunReport(cellDir, opts)
|
|
72
|
+
console.log('')
|
|
73
|
+
console.log(renderSupervisorRunMarkdown(report))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -28,10 +28,10 @@ describe('supervisor driver cancellation', () => {
|
|
|
28
28
|
const extension = join(dir, 'fake-extension.mjs')
|
|
29
29
|
await mkdir(workspace)
|
|
30
30
|
await writeFile(params, '{}')
|
|
31
|
-
const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); setInterval(()=>{},1000)`
|
|
31
|
+
const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; const ready=process.argv[2]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); fs.writeFileSync(ready,'ready'); setInterval(()=>{},1000)`
|
|
32
32
|
await writeFile(extension, `
|
|
33
33
|
import { spawn } from 'node:child_process'
|
|
34
|
-
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
35
35
|
import { join } from 'node:path'
|
|
36
36
|
|
|
37
37
|
const id = 'sup-1-fake12'
|
|
@@ -47,11 +47,13 @@ export default function fakeExtension(pi) {
|
|
|
47
47
|
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running', progress: 'driving' }))
|
|
48
48
|
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
49
49
|
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
50
|
-
|
|
50
|
+
const readyPath = join(ctx.cwd, 'worker-ready.txt')
|
|
51
|
+
child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'worker-cleaned.txt'), readyPath], {
|
|
51
52
|
detached: true,
|
|
52
53
|
stdio: 'ignore',
|
|
53
54
|
})
|
|
54
55
|
writeFileSync(join(ctx.cwd, 'worker.pid'), String(child.pid))
|
|
56
|
+
while (!existsSync(readyPath)) await new Promise((resolve) => setTimeout(resolve, 5))
|
|
55
57
|
return text('spawned supervisor ' + id)
|
|
56
58
|
},
|
|
57
59
|
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gen-5 public/private score split (SOTA adoption #1, AIDE2-style).
|
|
3
|
+
*
|
|
4
|
+
* Per generation the improvement instances are DETERMINISTICALLY split
|
|
5
|
+
* (seeded by runId) into a PUBLIC set — whose scores/evidence proposers and
|
|
6
|
+
* the pre-filter may see — and a PRIVATE set that is scored identically but
|
|
7
|
+
* NEVER surfaced to proposers. Winner selection stays on the COMBINED
|
|
8
|
+
* public+private score (the lib judge already averages every scenario), so
|
|
9
|
+
* the split changes only what feedback the search sees, exactly the
|
|
10
|
+
* anti-overfitting mechanism: a candidate that games the visible instances
|
|
11
|
+
* still has to survive the instances it never saw named.
|
|
12
|
+
*
|
|
13
|
+
* SMALL-N CAVEAT: with only 6 improvement instances the split is 4 public /
|
|
14
|
+
* 2 private. Two private instances give a coarse overfit signal (0, 1/2 or
|
|
15
|
+
* 2/2) and a single flake flips it — treat the private sub-score as a
|
|
16
|
+
* direction check, not a certification; the pre-registered 6-instance holdout
|
|
17
|
+
* remains the only promotion evidence.
|
|
18
|
+
*
|
|
19
|
+
* RESUME STABILITY: runId embeds a launch timestamp, so a resumed run gets a
|
|
20
|
+
* fresh runId. The split is therefore PERSISTED to <outDir>/score-split.json
|
|
21
|
+
* on first computation and reloaded verbatim afterwards — a resume can never
|
|
22
|
+
* rotate previously-private instances into view.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
import { existsSync } from 'node:fs'
|
|
27
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
28
|
+
import { dirname, join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
export const SCORE_SPLIT_SCHEMA = 'swe-arena.score-split.v1'
|
|
31
|
+
export const SCORE_SPLIT_FILENAME = 'score-split.json'
|
|
32
|
+
|
|
33
|
+
export interface ScoreSplitConfig {
|
|
34
|
+
/** Number of PUBLIC instances (the rest are private). Must satisfy
|
|
35
|
+
* 0 < publicCount < instances.length. */
|
|
36
|
+
publicCount: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScoreSplit {
|
|
40
|
+
schema: typeof SCORE_SPLIT_SCHEMA
|
|
41
|
+
/** The runId that seeded the split (provenance; a resumed run keeps it). */
|
|
42
|
+
seededBy: string
|
|
43
|
+
publicInstances: string[]
|
|
44
|
+
privateInstances: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Deterministic split: instances are ranked by sha256(runId + ':' + iid) and
|
|
48
|
+
* the first `publicCount` become public. Pure — same (runId, instances,
|
|
49
|
+
* publicCount) always yields the same split; instance-list ORDER does not
|
|
50
|
+
* matter (ranking is content-derived). */
|
|
51
|
+
export function splitInstances(runId: string, instances: readonly string[], publicCount: number): ScoreSplit {
|
|
52
|
+
if (!Number.isInteger(publicCount) || publicCount <= 0 || publicCount >= instances.length) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`score-split: publicCount must be an integer in (0, ${instances.length}), got ${JSON.stringify(publicCount)}`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (new Set(instances).size !== instances.length) {
|
|
58
|
+
throw new Error('score-split: duplicate instance ids')
|
|
59
|
+
}
|
|
60
|
+
const ranked = [...instances]
|
|
61
|
+
.map((iid) => ({ iid, rank: createHash('sha256').update(`${runId}:${iid}`).digest('hex') }))
|
|
62
|
+
.sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0))
|
|
63
|
+
const pub = ranked.slice(0, publicCount).map((r) => r.iid)
|
|
64
|
+
const priv = ranked.slice(publicCount).map((r) => r.iid)
|
|
65
|
+
return {
|
|
66
|
+
schema: SCORE_SPLIT_SCHEMA,
|
|
67
|
+
seededBy: runId,
|
|
68
|
+
publicInstances: [...pub].sort(),
|
|
69
|
+
privateInstances: [...priv].sort(),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Load the persisted split for this outDir, or compute + persist it. The
|
|
74
|
+
* persisted file always wins (resume stability); it is validated against the
|
|
75
|
+
* current instance set so a config edit mid-outDir fails loud. */
|
|
76
|
+
export async function loadOrCreateScoreSplit(args: {
|
|
77
|
+
outDir: string
|
|
78
|
+
runId: string
|
|
79
|
+
instances: readonly string[]
|
|
80
|
+
publicCount: number
|
|
81
|
+
}): Promise<ScoreSplit> {
|
|
82
|
+
const path = join(args.outDir, SCORE_SPLIT_FILENAME)
|
|
83
|
+
if (existsSync(path)) {
|
|
84
|
+
const split = JSON.parse(await readFile(path, 'utf8')) as ScoreSplit
|
|
85
|
+
if (split.schema !== SCORE_SPLIT_SCHEMA) {
|
|
86
|
+
throw new Error(`score-split: ${path} has unknown schema ${JSON.stringify(split.schema)}`)
|
|
87
|
+
}
|
|
88
|
+
const persisted = [...split.publicInstances, ...split.privateInstances].sort()
|
|
89
|
+
const expected = [...args.instances].sort()
|
|
90
|
+
if (JSON.stringify(persisted) !== JSON.stringify(expected)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`score-split: persisted split at ${path} covers [${persisted.join(', ')}] but the config ` +
|
|
93
|
+
`names [${expected.join(', ')}] — refusing to silently re-split; move the outDir or fix the config`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (split.publicInstances.length !== args.publicCount) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`score-split: persisted split has ${split.publicInstances.length} public instances, config wants ` +
|
|
99
|
+
`${args.publicCount} — refusing to silently re-split`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
return split
|
|
103
|
+
}
|
|
104
|
+
const split = splitInstances(args.runId, args.instances, args.publicCount)
|
|
105
|
+
await mkdir(dirname(path), { recursive: true })
|
|
106
|
+
await writeFile(path, JSON.stringify(split, null, 2))
|
|
107
|
+
return split
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True when `text` mentions any private instance id — the NEVER-SURFACED
|
|
111
|
+
* invariant's checkable form. Used by tests and by the evidence-index writer
|
|
112
|
+
* as a final guard on proposer-visible text. */
|
|
113
|
+
export function leaksPrivateInstance(text: string, split: Pick<ScoreSplit, 'privateInstances'>): string[] {
|
|
114
|
+
return split.privateInstances.filter((iid) => text.includes(iid))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Filter per-instance evidence records to the PUBLIC set. Identity function
|
|
118
|
+
* when no split is configured (split === null). */
|
|
119
|
+
export function publicOnly<T>(
|
|
120
|
+
records: readonly T[],
|
|
121
|
+
iidOf: (record: T) => string,
|
|
122
|
+
split: Pick<ScoreSplit, 'privateInstances'> | null,
|
|
123
|
+
): T[] {
|
|
124
|
+
if (split === null) return [...records]
|
|
125
|
+
const priv = new Set(split.privateInstances)
|
|
126
|
+
return records.filter((r) => !priv.has(iidOf(r)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Public/private sub-scores for one candidate, from per-instance AND-verdicts
|
|
130
|
+
* (fail-closed: an instance with no conclusive full-reps verdict counts 0). */
|
|
131
|
+
export function subScores(
|
|
132
|
+
verdicts: Record<string, boolean>,
|
|
133
|
+
split: Pick<ScoreSplit, 'publicInstances' | 'privateInstances'>,
|
|
134
|
+
): { publicResolvedCount: number; privateResolvedCount: number } {
|
|
135
|
+
const count = (iids: readonly string[]): number => iids.filter((iid) => verdicts[iid] === true).length
|
|
136
|
+
return {
|
|
137
|
+
publicResolvedCount: count(split.publicInstances),
|
|
138
|
+
privateResolvedCount: count(split.privateInstances),
|
|
139
|
+
}
|
|
140
|
+
}
|