@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,635 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import type { DispatchContext } from '@tangle-network/agent-eval/campaign'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
8
|
+
import { ACTIVATION_PREDICATE_RELPATH, parseActivationPredicate } from './activation.mts'
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_GEPA_PYTHON,
|
|
11
|
+
DEFAULT_MAX_METRIC_CALLS,
|
|
12
|
+
GEPA_ADAPTER_UPGRADE_HINT,
|
|
13
|
+
gepaBridgeScenarios,
|
|
14
|
+
innerSmokeComposite,
|
|
15
|
+
innerSmokeJudge,
|
|
16
|
+
isGepaSeat,
|
|
17
|
+
loadGepaMethodFactory,
|
|
18
|
+
mechanicalActivationPredicate,
|
|
19
|
+
probeGepaRuntime,
|
|
20
|
+
recipeEvaluationBudget,
|
|
21
|
+
recipeForSeat,
|
|
22
|
+
recordGepaSeatInnerRun,
|
|
23
|
+
validateGepaSeat,
|
|
24
|
+
type GepaMethodFactory,
|
|
25
|
+
type GepaSeatInnerRun,
|
|
26
|
+
type GepaSeatSpec,
|
|
27
|
+
type ProbeExec,
|
|
28
|
+
} from './gepa-seat.mts'
|
|
29
|
+
import { defaultRound4Config, type OuterLoopConfig } from './outer-loop.mts'
|
|
30
|
+
import { fanOutLoopsGenerator, type ProposerSpec, type SmokeRunner, type SmokeVerdict } from './proposer-fanout.mts'
|
|
31
|
+
import { captureProposerProvenance } from './proposer-provenance.mts'
|
|
32
|
+
import { runOk } from './proc.ts'
|
|
33
|
+
|
|
34
|
+
const SURFACE = 'extensions/pi/prompts/worker-coding-system.md'
|
|
35
|
+
|
|
36
|
+
const seat = (over: Partial<ProposerSpec> = {}): ProposerSpec => ({
|
|
37
|
+
name: 'gepa-author',
|
|
38
|
+
engine: 'gepa',
|
|
39
|
+
surface: SURFACE,
|
|
40
|
+
...over,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Spec validation.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe('validateGepaSeat', () => {
|
|
48
|
+
it('accepts the gen-6 draft seat shape and isGepaSeat discriminates on engine', () => {
|
|
49
|
+
expect(() => validateGepaSeat(seat())).not.toThrow()
|
|
50
|
+
expect(() => validateGepaSeat(seat({ engine: 'omni', maxMetricCalls: 8 }))).not.toThrow()
|
|
51
|
+
expect(isGepaSeat(seat())).toBe(true)
|
|
52
|
+
expect(isGepaSeat({ name: 'x', harness: 'claude' })).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('requires a surface inside the declared change-space', () => {
|
|
56
|
+
expect(() => validateGepaSeat(seat({ surface: undefined }))).toThrow(/surface is required/)
|
|
57
|
+
expect(() => validateGepaSeat(seat({ surface: 'judge.py' }))).toThrow(/outside the declared change-space/)
|
|
58
|
+
expect(() => validateGepaSeat(seat({ surface: '../escape.md' }))).toThrow(/outside the declared change-space/)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects harness-seat fields on an engine seat instead of silently ignoring them', () => {
|
|
62
|
+
expect(() => validateGepaSeat(seat({ harness: 'claude' }))).toThrow(/'harness' belongs to harness-authored/)
|
|
63
|
+
expect(() => validateGepaSeat(seat({ merge: true }))).toThrow(/'merge'/)
|
|
64
|
+
expect(() => validateGepaSeat(seat({ model: 'x' }))).toThrow(/'model'/)
|
|
65
|
+
expect(() => validateGepaSeat(seat({ profile: 'p.json' }))).toThrow(/'profile'/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('bounds the budget: positive integer calls, omni needs >= 4, positive cost cap', () => {
|
|
69
|
+
expect(() => validateGepaSeat(seat({ maxMetricCalls: 0 }))).toThrow(/maxMetricCalls/)
|
|
70
|
+
expect(() => validateGepaSeat(seat({ maxMetricCalls: 2.5 }))).toThrow(/maxMetricCalls/)
|
|
71
|
+
expect(() => validateGepaSeat(seat({ engine: 'omni', maxMetricCalls: 3 }))).toThrow(/omni.*needs maxMetricCalls >= 4/)
|
|
72
|
+
expect(() => validateGepaSeat(seat({ maxProposerCostUsd: 0 }))).toThrow(/maxProposerCostUsd/)
|
|
73
|
+
expect(() => validateGepaSeat(seat({ engine: 'nope' as never }))).toThrow(/engine must be one of/)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Recipe / budget cap.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
describe('recipeForSeat', () => {
|
|
82
|
+
it("'gepa' is one bounded engine run carrying the full budget (default 10)", () => {
|
|
83
|
+
const recipe = recipeForSeat(seat() as GepaSeatSpec)
|
|
84
|
+
expect(recipe).toMatchObject({ kind: 'engine', run: { engine: 'gepa', maxEvaluations: DEFAULT_MAX_METRIC_CALLS } })
|
|
85
|
+
expect(recipeEvaluationBudget(recipe)).toBe(DEFAULT_MAX_METRIC_CALLS)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("'omni' is GEPA's best-of-then-continue shape and the four bounded runs sum EXACTLY to the budget", () => {
|
|
89
|
+
const recipe = recipeForSeat(seat({ engine: 'omni', maxMetricCalls: 10 }) as GepaSeatSpec)
|
|
90
|
+
expect(recipe.kind).toBe('best-of-then-continue')
|
|
91
|
+
if (recipe.kind !== 'best-of-then-continue') throw new Error('unreachable')
|
|
92
|
+
expect(recipe.explore.map((r) => r.engine)).toEqual(['gepa', 'autoresearch', 'meta_harness'])
|
|
93
|
+
expect(recipe.continueWith.engine).toBe('gepa')
|
|
94
|
+
expect(recipeEvaluationBudget(recipe)).toBe(10)
|
|
95
|
+
for (const run of [...recipe.explore, recipe.continueWith]) {
|
|
96
|
+
expect(run.maxEvaluations).toBeGreaterThan(0)
|
|
97
|
+
expect(run.maxProposerCostUsd).toBeGreaterThan(0)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('every budget from 4 upward is preserved exactly by the omni split', () => {
|
|
102
|
+
for (const calls of [4, 5, 8, 12, 24]) {
|
|
103
|
+
const recipe = recipeForSeat(seat({ engine: 'omni', maxMetricCalls: calls }) as GepaSeatSpec)
|
|
104
|
+
expect(recipeEvaluationBudget(recipe)).toBe(calls)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Public-only bridge examples.
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
describe('gepaBridgeScenarios (public-only invariant)', () => {
|
|
114
|
+
const split = { privateInstances: ['django__django-11532', 'sphinx-doc__sphinx-9658'] }
|
|
115
|
+
|
|
116
|
+
it('serializes ONLY the public smoke instance (train + a distinct-id selection alias)', () => {
|
|
117
|
+
const { train, selection } = gepaBridgeScenarios('astropy__astropy-13033', split)
|
|
118
|
+
expect(train).toEqual([{ id: 'astropy__astropy-13033', kind: 'swe-smoke', smokeIid: 'astropy__astropy-13033' }])
|
|
119
|
+
expect(selection[0]!.id).toBe('astropy__astropy-13033::selection')
|
|
120
|
+
expect(selection[0]!.smokeIid).toBe('astropy__astropy-13033')
|
|
121
|
+
// Disjoint ids — the adapter's scenario map requires uniqueness.
|
|
122
|
+
expect(train[0]!.id).not.toBe(selection[0]!.id)
|
|
123
|
+
const serialized = JSON.stringify([...train, ...selection])
|
|
124
|
+
for (const iid of split.privateInstances) expect(serialized).not.toContain(iid)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('fails loud when the smoke instance is private — private ids never cross the bridge', () => {
|
|
128
|
+
expect(() => gepaBridgeScenarios('django__django-11532', split)).toThrow(/PRIVATE under the score split/)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('passes through with no split configured (pre-gen-5 behavior)', () => {
|
|
132
|
+
expect(gepaBridgeScenarios('astropy__astropy-13033', null).train).toHaveLength(1)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Inner score.
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
describe('inner smoke score', () => {
|
|
141
|
+
const verdict = (over: Partial<SmokeVerdict>): SmokeVerdict => ({
|
|
142
|
+
iid: 'astropy__astropy-13033',
|
|
143
|
+
pass: true,
|
|
144
|
+
reason: 'ok',
|
|
145
|
+
resolved: false,
|
|
146
|
+
patchLines: 3,
|
|
147
|
+
wallS: 60,
|
|
148
|
+
...over,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('resolve dominates; verify-pass is a bounded tiebreak that can never beat a resolve', () => {
|
|
152
|
+
expect(innerSmokeComposite(verdict({ resolved: false, verifyPass: false }))).toBe(0)
|
|
153
|
+
expect(innerSmokeComposite(verdict({ resolved: false, verifyPass: true }))).toBe(0.25)
|
|
154
|
+
expect(innerSmokeComposite(verdict({ resolved: true, verifyPass: false }))).toBe(1)
|
|
155
|
+
expect(innerSmokeComposite(verdict({ resolved: true, verifyPass: true }))).toBe(1.25)
|
|
156
|
+
// Pre-gen-6 verdicts without the field score as no verify signal.
|
|
157
|
+
expect(innerSmokeComposite(verdict({ resolved: true }))).toBe(1)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('the judge reports both dimensions and the composite', async () => {
|
|
161
|
+
const judge = innerSmokeJudge()
|
|
162
|
+
const score = await judge.score({
|
|
163
|
+
artifact: verdict({ resolved: true, verifyPass: true, reason: 'smoke line' }),
|
|
164
|
+
scenario: { id: 'x', kind: 'swe-smoke', smokeIid: 'x' },
|
|
165
|
+
signal: new AbortController().signal,
|
|
166
|
+
})
|
|
167
|
+
expect(score).toMatchObject({ dimensions: { resolved: 1, verifyPass: 1 }, composite: 1.25, notes: 'smoke line' })
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Runtime seams: loud fails with exact instructions.
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
describe('loadGepaMethodFactory', () => {
|
|
176
|
+
it('throws the upgrade instruction when the installed agent-eval predates the adapter', async () => {
|
|
177
|
+
await expect(loadGepaMethodFactory(async () => ({}))).rejects.toThrow(/#408/)
|
|
178
|
+
await expect(loadGepaMethodFactory(async () => ({}))).rejects.toThrow(/gepaOptimizationMethod/)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('returns the export when present', async () => {
|
|
182
|
+
const factory = (() => ({})) as unknown as GepaMethodFactory
|
|
183
|
+
await expect(loadGepaMethodFactory(async () => ({ gepaOptimizationMethod: factory }))).resolves.toBe(factory)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('probeGepaRuntime', () => {
|
|
188
|
+
const execFailingOn =
|
|
189
|
+
(failFragment: string, stderr: string): ProbeExec =>
|
|
190
|
+
async (_cmd, args) => {
|
|
191
|
+
const line = args.join(' ')
|
|
192
|
+
if (line.includes(failFragment)) return { code: 1, stdout: '', stderr }
|
|
193
|
+
if (line === '--version') return { code: 0, stdout: 'Python 3.12.3', stderr: '' }
|
|
194
|
+
return { code: 0, stdout: '0.2.0', stderr: '' }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
it('fails loud with pip install instructions when the bridge module is missing', async () => {
|
|
198
|
+
const exec = execFailingOn('agent_eval_rpc.gepa_bridge', "ModuleNotFoundError: No module named 'agent_eval_rpc'")
|
|
199
|
+
await expect(probeGepaRuntime('python3', exec, 'gepa-author')).rejects.toThrow(/pip install 'agent-eval-rpc\[gepa\]'/)
|
|
200
|
+
await expect(probeGepaRuntime('python3', exec, 'gepa-author')).rejects.toThrow(/not installed/)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('fails loud when the installed gepa lacks the multi-engine optimize_anything API', async () => {
|
|
204
|
+
const exec = execFailingOn('OptimizeAnythingConfig', "ImportError: cannot import name 'OptimizeAnythingConfig'")
|
|
205
|
+
await expect(probeGepaRuntime('python3', exec, 'gepa-author')).rejects.toThrow(/optimize_anything API/)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('fails loud when python itself is missing', async () => {
|
|
209
|
+
const exec: ProbeExec = async () => ({ code: 127, stdout: '', stderr: 'not found' })
|
|
210
|
+
await expect(probeGepaRuntime('python3', exec, 'gepa-author')).rejects.toThrow(/--version' failed/)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('returns python + gepa versions on a complete runtime', async () => {
|
|
214
|
+
const exec: ProbeExec = async (_cmd, args) =>
|
|
215
|
+
args.join(' ') === '--version'
|
|
216
|
+
? { code: 0, stdout: 'Python 3.12.3\n', stderr: '' }
|
|
217
|
+
: { code: 0, stdout: 'source\n', stderr: '' }
|
|
218
|
+
await expect(probeGepaRuntime('python3', exec, 'gepa-author')).resolves.toEqual({
|
|
219
|
+
pythonVersion: 'Python 3.12.3',
|
|
220
|
+
gepaVersion: 'source',
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Provenance capture at t=0.
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
describe('captureProposerProvenance with a gepa seat', () => {
|
|
230
|
+
const okExec: ProbeExec = async (cmd, args) => {
|
|
231
|
+
const line = args.join(' ')
|
|
232
|
+
if (line === '--version') {
|
|
233
|
+
return { code: 0, stdout: cmd === 'python3' ? 'Python 3.12.3' : `${cmd} 1.0.0`, stderr: '' }
|
|
234
|
+
}
|
|
235
|
+
return { code: 0, stdout: 'source', stderr: '' }
|
|
236
|
+
}
|
|
237
|
+
const withAdapter = async (): Promise<Record<string, unknown>> => ({
|
|
238
|
+
gepaOptimizationMethod: () => ({}),
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('records engine, surface, gepa version, bridge module, and the python runtime as harnessVersion', async () => {
|
|
242
|
+
const record = await captureProposerProvenance([{ name: 'claude-author', harness: 'claude' }, seat()], {
|
|
243
|
+
exec: okExec,
|
|
244
|
+
readSettingsModel: () => 'settings-model',
|
|
245
|
+
importCampaign: withAdapter,
|
|
246
|
+
})
|
|
247
|
+
const gepa = record.proposers.find((p) => p.name === 'gepa-author')!
|
|
248
|
+
expect(gepa).toMatchObject({
|
|
249
|
+
engine: 'gepa',
|
|
250
|
+
surface: SURFACE,
|
|
251
|
+
gepaVersion: 'source',
|
|
252
|
+
bridge: 'agent_eval_rpc.gepa_bridge',
|
|
253
|
+
harnessVersion: 'Python 3.12.3',
|
|
254
|
+
pinnedModel: null,
|
|
255
|
+
merge: false,
|
|
256
|
+
})
|
|
257
|
+
expect(gepa.harness).toBeUndefined()
|
|
258
|
+
// The claude seat is untouched by the gepa capture path.
|
|
259
|
+
expect(record.proposers.find((p) => p.name === 'claude-author')).toMatchObject({
|
|
260
|
+
harness: 'claude',
|
|
261
|
+
settingsModel: 'settings-model',
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('fails LOUD at t=0 when the python runtime is missing — with install instructions', async () => {
|
|
266
|
+
const exec: ProbeExec = async (_cmd, args) =>
|
|
267
|
+
args.join(' ').includes('gepa_bridge')
|
|
268
|
+
? { code: 1, stdout: '', stderr: 'ModuleNotFoundError' }
|
|
269
|
+
: { code: 0, stdout: 'Python 3.12.3', stderr: '' }
|
|
270
|
+
await expect(
|
|
271
|
+
captureProposerProvenance([seat()], { exec, importCampaign: withAdapter }),
|
|
272
|
+
).rejects.toThrow(/pip install 'agent-eval-rpc\[gepa\]'/)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('fails LOUD at t=0 when the installed agent-eval predates the adapter export', async () => {
|
|
276
|
+
await expect(
|
|
277
|
+
captureProposerProvenance([seat()], { exec: okExec, importCampaign: async () => ({}) }),
|
|
278
|
+
).rejects.toThrow(GEPA_ADAPTER_UPGRADE_HINT.slice(0, 40))
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Mechanical activation predicate.
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
describe('mechanicalActivationPredicate', () => {
|
|
287
|
+
it('targets the longest added line and produces a parseable v1 grep predicate', () => {
|
|
288
|
+
const seed = 'alpha\nshared line stays here\n'
|
|
289
|
+
const winner = 'alpha\nshared line stays here\nAlways run the neighboring test file before finalizing.\nshort\n'
|
|
290
|
+
const predicate = mechanicalActivationPredicate(seed, winner, SURFACE)!
|
|
291
|
+
expect(predicate.kind).toBe('grep')
|
|
292
|
+
expect(predicate.pattern).toContain('Always run the neighboring test file')
|
|
293
|
+
const parsed = parseActivationPredicate(JSON.stringify(predicate))
|
|
294
|
+
expect(parsed.ok).toBe(true)
|
|
295
|
+
// The pattern is regex-escaped: it must match its own source line.
|
|
296
|
+
expect(new RegExp(predicate.pattern!).test('Always run the neighboring test file before finalizing.')).toBe(true)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('escapes regex metacharacters in the added line', () => {
|
|
300
|
+
const predicate = mechanicalActivationPredicate('', 'Use pattern (a|b).* with $VAR [strictly].\n', SURFACE)!
|
|
301
|
+
expect(new RegExp(predicate.pattern!).test('Use pattern (a|b).* with $VAR [strictly].')).toBe(true)
|
|
302
|
+
expect(new RegExp(predicate.pattern!).test('Use pattern axb1* with 2VAR strictly.')).toBe(false)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('returns null when no added line is distinctive enough', () => {
|
|
306
|
+
expect(mechanicalActivationPredicate('a\nb\n', 'a\nb\nshort\n', SURFACE)).toBeNull()
|
|
307
|
+
expect(mechanicalActivationPredicate('same\n', 'same\n', SURFACE)).toBeNull()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('recordGepaSeatInnerRun', () => {
|
|
312
|
+
it('appends to gepaInnerRuns while preserving the t=0 capture record', async () => {
|
|
313
|
+
const dir = await mkdtemp(join(tmpdir(), 'gepa-prov-'))
|
|
314
|
+
try {
|
|
315
|
+
await writeFile(join(dir, 'proposer-provenance.json'), JSON.stringify({ schema: 'swe-arena.proposer-provenance.v1', proposers: [] }))
|
|
316
|
+
const run: GepaSeatInnerRun = {
|
|
317
|
+
seat: 'gepa-author',
|
|
318
|
+
engine: 'gepa',
|
|
319
|
+
surface: SURFACE,
|
|
320
|
+
generation: 0,
|
|
321
|
+
budget: 10,
|
|
322
|
+
innerCallCount: 2,
|
|
323
|
+
innerScores: [],
|
|
324
|
+
bestComposite: 1,
|
|
325
|
+
adapterReportedCostUsd: 0,
|
|
326
|
+
adapterCostAccountingComplete: false,
|
|
327
|
+
durationMs: 5,
|
|
328
|
+
}
|
|
329
|
+
await recordGepaSeatInnerRun(dir, run)
|
|
330
|
+
await recordGepaSeatInnerRun(dir, { ...run, generation: 1 })
|
|
331
|
+
const record = JSON.parse(await readFile(join(dir, 'proposer-provenance.json'), 'utf8'))
|
|
332
|
+
expect(record.schema).toBe('swe-arena.proposer-provenance.v1')
|
|
333
|
+
expect(record.gepaInnerRuns).toHaveLength(2)
|
|
334
|
+
expect(record.gepaInnerRuns[0]).toMatchObject({ seat: 'gepa-author', innerCallCount: 2 })
|
|
335
|
+
expect(record.gepaInnerRuns[1]).toMatchObject({ generation: 1 })
|
|
336
|
+
} finally {
|
|
337
|
+
await rm(dir, { recursive: true, force: true })
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// The seat inside the fan-out generator, against a real temp git repo.
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
const fakeCtx = {} as unknown as DispatchContext
|
|
347
|
+
|
|
348
|
+
/** Mimics the adapter's loop: score the seed and each provided candidate via
|
|
349
|
+
* the seat's dispatch + judge, return the best-scoring candidate — exactly
|
|
350
|
+
* the contract gepaOptimizationMethod fulfills through the Python bridge. */
|
|
351
|
+
const fakeGepaFactory =
|
|
352
|
+
(candidates: string[], observed?: { config?: unknown }): GepaMethodFactory =>
|
|
353
|
+
(config) => {
|
|
354
|
+
if (observed) observed.config = config
|
|
355
|
+
return {
|
|
356
|
+
name: config.name ?? 'fake-gepa',
|
|
357
|
+
async optimize(input) {
|
|
358
|
+
const judge = input.judges[0]!
|
|
359
|
+
const scenario = input.trainScenarios[0]!
|
|
360
|
+
let best = { surface: input.baselineSurface as string, composite: -Infinity }
|
|
361
|
+
for (const candidate of [input.baselineSurface as string, ...candidates]) {
|
|
362
|
+
const artifact = await input.dispatchWithSurface(candidate, scenario, fakeCtx)
|
|
363
|
+
const score = await judge.score({ artifact, scenario, signal: new AbortController().signal })
|
|
364
|
+
if (score.composite > best.composite) best = { surface: candidate, composite: score.composite }
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
winnerSurface: best.surface,
|
|
368
|
+
cost: { totalCostUsd: 0, accountingComplete: false, incompleteReasons: ['fake factory'] },
|
|
369
|
+
durationMs: 1,
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
describe('fanOutLoopsGenerator with the gepa seat', () => {
|
|
376
|
+
let loopsRepo: string
|
|
377
|
+
let outDir: string
|
|
378
|
+
let driverWt: string
|
|
379
|
+
|
|
380
|
+
const git = async (args: string[], cwd: string): Promise<string> =>
|
|
381
|
+
(await runOk('git', ['-C', cwd, ...args])).stdout.trim()
|
|
382
|
+
|
|
383
|
+
const SEED = '# worker coding system\nkeep tests green\n'
|
|
384
|
+
|
|
385
|
+
beforeEach(async () => {
|
|
386
|
+
loopsRepo = await mkdtemp(join(tmpdir(), 'gepa-repo-'))
|
|
387
|
+
outDir = await mkdtemp(join(tmpdir(), 'gepa-out-'))
|
|
388
|
+
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
389
|
+
await git(['config', 'user.email', 't@t.dev'], loopsRepo)
|
|
390
|
+
await git(['config', 'user.name', 'T'], loopsRepo)
|
|
391
|
+
await mkdir(join(loopsRepo, 'extensions', 'pi', 'prompts'), { recursive: true })
|
|
392
|
+
await writeFile(join(loopsRepo, SURFACE), SEED)
|
|
393
|
+
await git(['add', '-A'], loopsRepo)
|
|
394
|
+
await git(['commit', '-q', '-m', 'init'], loopsRepo)
|
|
395
|
+
driverWt = join(outDir, 'driver-wt')
|
|
396
|
+
await runOk('git', ['-C', loopsRepo, 'worktree', 'add', '--detach', driverWt, 'HEAD'])
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
afterEach(async () => {
|
|
400
|
+
await rm(outDir, { recursive: true, force: true })
|
|
401
|
+
await rm(loopsRepo, { recursive: true, force: true })
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const baseConfig = (proposers: ProposerSpec[], over: Partial<OuterLoopConfig> = {}): OuterLoopConfig => ({
|
|
405
|
+
...defaultRound4Config(),
|
|
406
|
+
loopsRepo,
|
|
407
|
+
outDir,
|
|
408
|
+
populationSize: proposers.length,
|
|
409
|
+
proposers,
|
|
410
|
+
prefilter: { enabled: true, smokeInstance: 'cheapest-of-set', requireResolved: false },
|
|
411
|
+
...over,
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
const generatorArgs = (candidateIndex: number) => ({
|
|
415
|
+
worktreePath: driverWt,
|
|
416
|
+
report: undefined,
|
|
417
|
+
findings: [],
|
|
418
|
+
maxShots: 1,
|
|
419
|
+
signal: new AbortController().signal,
|
|
420
|
+
generation: 0,
|
|
421
|
+
candidateIndex,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const smokeVerdict = (over: Partial<SmokeVerdict> = {}): SmokeVerdict => ({
|
|
425
|
+
iid: 'astropy__astropy-13033',
|
|
426
|
+
pass: true,
|
|
427
|
+
reason: 'smoke ok',
|
|
428
|
+
resolved: false,
|
|
429
|
+
patchLines: 3,
|
|
430
|
+
wallS: 5,
|
|
431
|
+
verifyPass: false,
|
|
432
|
+
...over,
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('construction fails loud without the smoke runner (the inner evaluator)', () => {
|
|
436
|
+
expect(() => fanOutLoopsGenerator(baseConfig([seat()]))).toThrow(/inner evaluator/)
|
|
437
|
+
expect(() => fanOutLoopsGenerator(baseConfig([{ name: 'no-seat-kind' }]))).toThrow(/neither a harness nor an engine/)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('materializes each candidate into the scratch surface, applies the winner through the normal prefilter path, and records provenance', async () => {
|
|
441
|
+
const WINNER = `${SEED}Always run the neighboring test file before finalizing.\n`
|
|
442
|
+
const LOSER = `${SEED}delete all tests\n`
|
|
443
|
+
const seen: Array<{ content: string; scratch: string }> = []
|
|
444
|
+
const smokeRunner: SmokeRunner = async ({ scratchPath }) => {
|
|
445
|
+
const content = await readFile(join(scratchPath, SURFACE), 'utf8')
|
|
446
|
+
seen.push({ content, scratch: scratchPath })
|
|
447
|
+
// The winner candidate resolves; the seed gets verify-pass only; the
|
|
448
|
+
// loser gets nothing — exercising resolve-dominates + tiebreak.
|
|
449
|
+
if (content === WINNER) return smokeVerdict({ resolved: true, verifyPass: true })
|
|
450
|
+
if (content === SEED) return smokeVerdict({ verifyPass: true })
|
|
451
|
+
return smokeVerdict()
|
|
452
|
+
}
|
|
453
|
+
const observed: { config?: unknown } = {}
|
|
454
|
+
const config = baseConfig([seat({ maxMetricCalls: 5 })], { activationGate: true })
|
|
455
|
+
const gen = fanOutLoopsGenerator(config, {
|
|
456
|
+
smokeRunner,
|
|
457
|
+
smokeInstanceId: 'astropy__astropy-13033',
|
|
458
|
+
scoreSplit: { privateInstances: ['django__django-11532'] },
|
|
459
|
+
gepaMethodFactory: fakeGepaFactory([LOSER, WINNER], observed),
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const result = await gen.generate(generatorArgs(0))
|
|
463
|
+
|
|
464
|
+
expect(result).toMatchObject({ applied: true, label: 'gepa-author' })
|
|
465
|
+
expect(result.rationale).toContain('engine gepa')
|
|
466
|
+
// 3 inner calls (seed, loser, winner) + 1 stage-B prefilter smoke on the
|
|
467
|
+
// final candidate — all in the seat's scratch worktree, never the driver.
|
|
468
|
+
expect(seen).toHaveLength(4)
|
|
469
|
+
for (const call of seen) expect(call.scratch).not.toBe(driverWt)
|
|
470
|
+
expect(seen.map((c) => c.content)).toEqual([SEED, LOSER, WINNER, WINNER])
|
|
471
|
+
// The winner landed on the driver worktree with the mechanical predicate.
|
|
472
|
+
expect(await readFile(join(driverWt, SURFACE), 'utf8')).toBe(WINNER)
|
|
473
|
+
const predicate = parseActivationPredicate(await readFile(join(driverWt, ACTIVATION_PREDICATE_RELPATH), 'utf8'))
|
|
474
|
+
expect(predicate.ok).toBe(true)
|
|
475
|
+
// Only the surface + predicate changed.
|
|
476
|
+
const changed = (await runOk('git', ['-C', driverWt, 'status', '--porcelain', '--untracked-files=all'])).stdout
|
|
477
|
+
.split('\n')
|
|
478
|
+
.map((l) => l.slice(3).trim())
|
|
479
|
+
.filter(Boolean)
|
|
480
|
+
expect(changed.sort()).toEqual([ACTIVATION_PREDICATE_RELPATH, SURFACE].sort())
|
|
481
|
+
// Budget threaded into the adapter recipe.
|
|
482
|
+
expect(observed.config).toMatchObject({ recipe: { kind: 'engine', run: { engine: 'gepa', maxEvaluations: 5 } } })
|
|
483
|
+
// Inner-run provenance: per-seat file + the merged proposer-provenance.json.
|
|
484
|
+
const inner = JSON.parse(await readFile(join(outDir, 'gepa-seat', 'gen0-gepa-author', 'inner-provenance.json'), 'utf8'))
|
|
485
|
+
expect(inner).toMatchObject({ seat: 'gepa-author', engine: 'gepa', budget: 5, innerCallCount: 3, bestComposite: 1.25 })
|
|
486
|
+
expect(inner.innerScores.map((s: { composite: number }) => s.composite)).toEqual([0.25, 0, 1.25])
|
|
487
|
+
const merged = JSON.parse(await readFile(join(outDir, 'proposer-provenance.json'), 'utf8'))
|
|
488
|
+
expect(merged.gepaInnerRuns).toHaveLength(1)
|
|
489
|
+
expect(gen.drainPrefilterKills()).toEqual([])
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('enforces the inner-call budget cap fail-closed', async () => {
|
|
493
|
+
const runaway: GepaMethodFactory = () => ({
|
|
494
|
+
name: 'runaway',
|
|
495
|
+
async optimize(input) {
|
|
496
|
+
for (let i = 0; i < 4; i++) {
|
|
497
|
+
await input.dispatchWithSurface(`${SEED}candidate ${i}\n`, input.trainScenarios[0]!, fakeCtx)
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
winnerSurface: SEED,
|
|
501
|
+
cost: { totalCostUsd: 0, accountingComplete: false, incompleteReasons: [] },
|
|
502
|
+
durationMs: 1,
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
const gen = fanOutLoopsGenerator(baseConfig([seat({ maxMetricCalls: 3 })]), {
|
|
507
|
+
smokeRunner: async () => smokeVerdict(),
|
|
508
|
+
smokeInstanceId: 'astropy__astropy-13033',
|
|
509
|
+
scoreSplit: null,
|
|
510
|
+
gepaMethodFactory: runaway,
|
|
511
|
+
})
|
|
512
|
+
await expect(gen.generate(generatorArgs(0))).rejects.toThrow(/inner-call budget 3 exhausted/)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('refuses to feed a PRIVATE smoke verdict to the bridge', async () => {
|
|
516
|
+
const gen = fanOutLoopsGenerator(baseConfig([seat()]), {
|
|
517
|
+
smokeRunner: async () => smokeVerdict({ iid: 'django__django-11532' }),
|
|
518
|
+
smokeInstanceId: 'astropy__astropy-13033',
|
|
519
|
+
scoreSplit: { privateInstances: ['django__django-11532'] },
|
|
520
|
+
gepaMethodFactory: fakeGepaFactory([`${SEED}x line long enough\n`]),
|
|
521
|
+
})
|
|
522
|
+
await expect(gen.generate(generatorArgs(0))).rejects.toThrow(/PRIVATE instance django__django-11532/)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('declines the slot without a kill when GEPA returns the seed unchanged', async () => {
|
|
526
|
+
const gen = fanOutLoopsGenerator(baseConfig([seat()]), {
|
|
527
|
+
smokeRunner: async () => smokeVerdict(),
|
|
528
|
+
smokeInstanceId: 'astropy__astropy-13033',
|
|
529
|
+
scoreSplit: null,
|
|
530
|
+
gepaMethodFactory: fakeGepaFactory([]),
|
|
531
|
+
})
|
|
532
|
+
const result = await gen.generate(generatorArgs(0))
|
|
533
|
+
expect(result.applied).toBe(false)
|
|
534
|
+
expect(result.summary).toContain('equals the seed')
|
|
535
|
+
expect(gen.drainPrefilterKills()).toEqual([])
|
|
536
|
+
expect((await runOk('git', ['-C', driverWt, 'status', '--porcelain'])).stdout.trim()).toBe('')
|
|
537
|
+
})
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// Integration: ONE real Node→Python→score roundtrip through the installed
|
|
542
|
+
// adapter + bridge. Skips with an exact reason when either runtime is absent
|
|
543
|
+
// (this is the same condition the t=0 provenance capture enforces loud).
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
const pythonBridgeReady = (): { ok: boolean; reason: string } => {
|
|
547
|
+
const probe = spawnSync(DEFAULT_GEPA_PYTHON, [
|
|
548
|
+
'-c',
|
|
549
|
+
'import agent_eval_rpc.gepa_bridge; from gepa.optimize_anything import optimize_anything, OptimizeAnythingConfig',
|
|
550
|
+
])
|
|
551
|
+
if (probe.status !== 0) {
|
|
552
|
+
return { ok: false, reason: `python bridge unavailable: ${String(probe.stderr).trim().split('\n').pop()}` }
|
|
553
|
+
}
|
|
554
|
+
return { ok: true, reason: '' }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
describe('integration: real adapter roundtrip', () => {
|
|
558
|
+
it('runs one inner smoke roundtrip through gepaOptimizationMethod when the full runtime is installed', async (ctx) => {
|
|
559
|
+
const campaign = (await import('@tangle-network/agent-eval/campaign')) as Record<string, unknown>
|
|
560
|
+
const missing: string[] = []
|
|
561
|
+
if (typeof campaign.gepaOptimizationMethod !== 'function') {
|
|
562
|
+
missing.push(
|
|
563
|
+
'installed @tangle-network/agent-eval lacks gepaOptimizationMethod (needs a release after 0.123.5 containing PRs #408/#409)',
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
const python = pythonBridgeReady()
|
|
567
|
+
if (!python.ok) missing.push(python.reason)
|
|
568
|
+
if (missing.length > 0) {
|
|
569
|
+
ctx.skip(`skip-with-reason: ${missing.join('; ')}`)
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Full runtime present: drive the REAL adapter with a stub smoke runner
|
|
574
|
+
// (no arm spend) over a synthetic tiny surface in a temp repo.
|
|
575
|
+
const loopsRepo = await mkdtemp(join(tmpdir(), 'gepa-int-repo-'))
|
|
576
|
+
const outDir = await mkdtemp(join(tmpdir(), 'gepa-int-out-'))
|
|
577
|
+
try {
|
|
578
|
+
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
579
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'user.email', 't@t.dev'])
|
|
580
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'user.name', 'T'])
|
|
581
|
+
await mkdir(join(loopsRepo, 'extensions', 'pi', 'prompts'), { recursive: true })
|
|
582
|
+
await writeFile(join(loopsRepo, SURFACE), 'tiny synthetic surface\n')
|
|
583
|
+
await runOk('git', ['-C', loopsRepo, 'add', '-A'])
|
|
584
|
+
await runOk('git', ['-C', loopsRepo, 'commit', '-q', '-m', 'init'])
|
|
585
|
+
const driverWt = join(outDir, 'driver-wt')
|
|
586
|
+
await runOk('git', ['-C', loopsRepo, 'worktree', 'add', '--detach', driverWt, 'HEAD'])
|
|
587
|
+
|
|
588
|
+
let innerCalls = 0
|
|
589
|
+
const gen = fanOutLoopsGenerator(
|
|
590
|
+
{
|
|
591
|
+
...defaultRound4Config(),
|
|
592
|
+
loopsRepo,
|
|
593
|
+
outDir,
|
|
594
|
+
populationSize: 1,
|
|
595
|
+
proposers: [seat({ maxMetricCalls: 4 })],
|
|
596
|
+
prefilter: { enabled: true, smokeInstance: 'cheapest-of-set', requireResolved: false },
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
smokeRunner: async () => {
|
|
600
|
+
innerCalls += 1
|
|
601
|
+
return {
|
|
602
|
+
iid: 'astropy__astropy-13033',
|
|
603
|
+
pass: true,
|
|
604
|
+
reason: 'stub smoke (integration)',
|
|
605
|
+
resolved: innerCalls > 1,
|
|
606
|
+
patchLines: 1,
|
|
607
|
+
wallS: 0,
|
|
608
|
+
verifyPass: true,
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
smokeInstanceId: 'astropy__astropy-13033',
|
|
612
|
+
scoreSplit: null,
|
|
613
|
+
},
|
|
614
|
+
)
|
|
615
|
+
const result = await gen.generate({
|
|
616
|
+
worktreePath: driverWt,
|
|
617
|
+
report: undefined,
|
|
618
|
+
findings: [],
|
|
619
|
+
maxShots: 1,
|
|
620
|
+
signal: new AbortController().signal,
|
|
621
|
+
generation: 0,
|
|
622
|
+
candidateIndex: 0,
|
|
623
|
+
})
|
|
624
|
+
// The bridge ran: inner provenance must show >= 1 scored callback.
|
|
625
|
+
const inner = JSON.parse(
|
|
626
|
+
await readFile(join(outDir, 'gepa-seat', 'gen0-gepa-author', 'inner-provenance.json'), 'utf8'),
|
|
627
|
+
)
|
|
628
|
+
expect(inner.innerCallCount).toBeGreaterThanOrEqual(1)
|
|
629
|
+
expect(typeof result.applied).toBe('boolean')
|
|
630
|
+
} finally {
|
|
631
|
+
await rm(outDir, { recursive: true, force: true })
|
|
632
|
+
await rm(loopsRepo, { recursive: true, force: true })
|
|
633
|
+
}
|
|
634
|
+
}, 300_000)
|
|
635
|
+
})
|