@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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAVID vs GOLIATH — the program's northstar, tested head-on: does a CHEAP model
|
|
3
|
+
* with a self-verification harness beat a FRONTIER model running solo, at EQUAL OR
|
|
4
|
+
* LOWER dollar cost, on held-out code?
|
|
5
|
+
*
|
|
6
|
+
* GOLIATH — a strong model, ONE shot. The "just use the big model" baseline.
|
|
7
|
+
* DAVID — a cheap/weak model + test-time compute: generate N candidate
|
|
8
|
+
* solutions AND M of its own unit tests, EXECUTE every candidate
|
|
9
|
+
* against the generated tests, and submit the candidate that passes
|
|
10
|
+
* the most (CodeT-style execution self-selection). No ground-truth
|
|
11
|
+
* test is ever used to select — only the model's own generated tests.
|
|
12
|
+
*
|
|
13
|
+
* Both are graded by the HIDDEN HumanEval test (never shown). Cost is the real
|
|
14
|
+
* token spend × the router-reported/priced rate per arm. The win condition is a
|
|
15
|
+
* Pareto beat: David's held-out pass rate >= Goliath's AND David's $ <= Goliath's.
|
|
16
|
+
* Mechanism under test: EXECUTION-BASED VERIFICATION is the lever that lets a weak
|
|
17
|
+
* generator punch above its solo weight — the standing "verification is live" claim
|
|
18
|
+
* at its most dramatic. Paired McNemar on per-task discordant pairs for significance.
|
|
19
|
+
*
|
|
20
|
+
* Run from cwd=bench: env DAVID=groq/llama-3.1-8b-instant GOLIATH=anthropic/claude-haiku-4-5-20251001 \
|
|
21
|
+
* N=8 T=5 NTASKS=164 REPS=2 node_modules/.bin/tsx src/david-goliath.mts
|
|
22
|
+
*/
|
|
23
|
+
import { execFile } from 'node:child_process'
|
|
24
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
25
|
+
import { tmpdir } from 'node:os'
|
|
26
|
+
import { join } from 'node:path'
|
|
27
|
+
import { loadHumanEval, extractCode, type HumanEvalTask } from './benchmarks/humaneval'
|
|
28
|
+
|
|
29
|
+
const KEY = process.env.TANGLE_API_KEY
|
|
30
|
+
if (!KEY) throw new Error('TANGLE_API_KEY required')
|
|
31
|
+
const ROUTER = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
|
|
32
|
+
const DAVID = process.env.DAVID ?? 'groq/llama-3.1-8b-instant'
|
|
33
|
+
const GOLIATH = process.env.GOLIATH ?? 'anthropic/claude-haiku-4-5-20251001'
|
|
34
|
+
const N = Number(process.env.N ?? 8) // David candidate solutions
|
|
35
|
+
const T = Number(process.env.T ?? 5) // David generated tests
|
|
36
|
+
const NTASKS = Number(process.env.NTASKS ?? 164)
|
|
37
|
+
const REPS = Number(process.env.REPS ?? 2)
|
|
38
|
+
const CONC = Number(process.env.CONCURRENCY ?? 6)
|
|
39
|
+
const EXEC_TIMEOUT = Number(process.env.EXEC_TIMEOUT_MS ?? 6000)
|
|
40
|
+
|
|
41
|
+
// Approx $/1M tokens (in,out) for cost accounting — the router does not price
|
|
42
|
+
// every model inline, so use public rates; a cheap/frontier gap of ~20-30x is the
|
|
43
|
+
// point, and the ratio is robust to small rate errors. Override via PRICES env.
|
|
44
|
+
const PRICES: Record<string, [number, number]> = {
|
|
45
|
+
'groq/llama-3.1-8b-instant': [0.05, 0.08],
|
|
46
|
+
'google/gemini-2.5-flash-lite': [0.10, 0.40],
|
|
47
|
+
'openai/gpt-4o-mini': [0.15, 0.60],
|
|
48
|
+
'anthropic/claude-haiku-4-5-20251001': [1.0, 5.0],
|
|
49
|
+
'glm-5.2': [0.60, 2.20],
|
|
50
|
+
}
|
|
51
|
+
const priceOf = (m: string) => PRICES[m] ?? [0.5, 1.5]
|
|
52
|
+
|
|
53
|
+
interface Usage { in: number; out: number }
|
|
54
|
+
const zero = (): Usage => ({ in: 0, out: 0 })
|
|
55
|
+
const addU = (a: Usage, b: Usage) => { a.in += b.in; a.out += b.out }
|
|
56
|
+
const usd = (m: string, u: Usage) => { const [pi, po] = priceOf(m); return (u.in * pi + u.out * po) / 1e6 }
|
|
57
|
+
|
|
58
|
+
async function chat(model: string, messages: { role: string; content: string }[], temperature: number, usage: Usage): Promise<string> {
|
|
59
|
+
for (let a = 0; ; a++) {
|
|
60
|
+
try {
|
|
61
|
+
const r = await fetch(`${ROUTER}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${KEY}` }, body: JSON.stringify({ model, messages, temperature, max_tokens: 1000 }), signal: AbortSignal.timeout(60_000) })
|
|
62
|
+
if ([408, 429, 500, 502, 503, 504, 520, 522, 524].includes(r.status)) { if (a >= 5) return ''; await sleep(700 * 2 ** a); continue }
|
|
63
|
+
if (!r.ok) return ''
|
|
64
|
+
const j = (await r.json()) as { choices?: { message?: { content?: string } }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } }
|
|
65
|
+
addU(usage, { in: j.usage?.prompt_tokens ?? 0, out: j.usage?.completion_tokens ?? 0 })
|
|
66
|
+
return j.choices?.[0]?.message?.content ?? ''
|
|
67
|
+
} catch { if (a >= 5) return ''; await sleep(700 * 2 ** a) }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
71
|
+
const exec = (file: string, args: string[], o: object) => new Promise<{ code: number; stdout: string }>((res) => execFile(file, args, { ...o, maxBuffer: 8 * 1024 * 1024 }, (e, stdout) => res({ code: (e as { code?: number } | null)?.code ?? (e ? 1 : 0), stdout: String(stdout) })))
|
|
72
|
+
async function runPy(program: string): Promise<{ ok: boolean }> {
|
|
73
|
+
const d = mkdtempSync(join(tmpdir(), 'dg-'))
|
|
74
|
+
try { writeFileSync(join(d, 'p.py'), program); const r = await exec('python3', [join(d, 'p.py')], { cwd: d, timeout: EXEC_TIMEOUT }); return { ok: r.code === 0 } } finally { rmSync(d, { recursive: true, force: true }) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SOLVE = 'You are an expert Python programmer. Output the COMPLETE function (signature + body + imports) in a single ```python block. No prose, no tests.'
|
|
78
|
+
async function genSolution(model: string, t: HumanEvalTask, temp: number, u: Usage): Promise<string> {
|
|
79
|
+
return extractCode(await chat(model, [{ role: 'system', content: SOLVE }, { role: 'user', content: `Complete:\n\n\`\`\`python\n${t.prompt}\`\`\`` }], temp, u))
|
|
80
|
+
}
|
|
81
|
+
// David writes its OWN tests (never sees the hidden test). Parse assert lines.
|
|
82
|
+
async function genTests(model: string, t: HumanEvalTask, u: Usage): Promise<string[]> {
|
|
83
|
+
const reply = await chat(model, [
|
|
84
|
+
{ role: 'system', content: 'Write Python assert-based unit tests for the described function. Output ONLY a ```python block of standalone `assert <entry>(...) == ...` lines (at least a few, covering normal + edge cases). No function definition, no prose.' },
|
|
85
|
+
{ role: 'user', content: `Function to test (entry point: ${t.entryPoint}):\n\n\`\`\`python\n${t.prompt}\`\`\`` },
|
|
86
|
+
], 0.4, u)
|
|
87
|
+
const block = extractCode(reply) || reply
|
|
88
|
+
return block.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('assert ') && l.includes(t.entryPoint)).slice(0, T + 3)
|
|
89
|
+
}
|
|
90
|
+
// Hidden held-out judge — the truth. Never used for selection.
|
|
91
|
+
async function judge(t: HumanEvalTask, code: string): Promise<boolean> {
|
|
92
|
+
if (!code.trim()) return false
|
|
93
|
+
return (await runPy(`${code}\n\n${t.test}\n\ncheck(${t.entryPoint})\n`)).ok
|
|
94
|
+
}
|
|
95
|
+
// David's self-selection: score each candidate by how many of ITS OWN tests it passes.
|
|
96
|
+
async function scoreOnTests(code: string, tests: string[]): Promise<number> {
|
|
97
|
+
if (!code.trim() || tests.length === 0) return 0
|
|
98
|
+
let pass = 0
|
|
99
|
+
// one program per test keeps a crash on one test from voiding the rest
|
|
100
|
+
for (const a of tests) if ((await runPy(`${code}\n\n${a}\n`)).ok) pass++
|
|
101
|
+
return pass
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function davidArm(t: HumanEvalTask, u: Usage): Promise<string> {
|
|
105
|
+
const cands = (await Promise.all(Array.from({ length: N }, () => genSolution(DAVID, t, 0.7, u)))).filter((c) => c.trim())
|
|
106
|
+
if (cands.length === 0) return ''
|
|
107
|
+
const tests = await genTests(DAVID, t, u)
|
|
108
|
+
if (tests.length === 0) return cands[0]! // no verifier signal → first sample
|
|
109
|
+
const scored = await Promise.all(cands.map(async (c) => ({ c, s: await scoreOnTests(c, tests) })))
|
|
110
|
+
scored.sort((a, b) => b.s - a.s || b.c.length - a.c.length)
|
|
111
|
+
return scored[0]!.c
|
|
112
|
+
}
|
|
113
|
+
const goliathArm = (t: HumanEvalTask, u: Usage) => genSolution(GOLIATH, t, 0.2, u)
|
|
114
|
+
|
|
115
|
+
async function pool<T2, R>(xs: T2[], n: number, fn: (x: T2, i: number) => Promise<R>): Promise<R[]> {
|
|
116
|
+
const o = new Array<R>(xs.length); let i = 0
|
|
117
|
+
await Promise.all(Array.from({ length: Math.min(n, xs.length) }, async () => { while (i < xs.length) { const k = i++; o[k] = await fn(xs[k]!, k) } }))
|
|
118
|
+
return o
|
|
119
|
+
}
|
|
120
|
+
function mcnemar(b: number, c: number): number { const n = b + c; if (n === 0) return 1; const k = Math.min(b, c); const lf = (x: number) => { let s = 0; for (let i = 2; i <= x; i++) s += Math.log(i); return s }; let tl = 0; for (let i = 0; i <= k; i++) tl += Math.exp(lf(n) - lf(i) - lf(n - i) - n * Math.log(2)); return Math.min(1, 2 * tl) }
|
|
121
|
+
|
|
122
|
+
async function main(): Promise<void> {
|
|
123
|
+
if (['1', 'true'].includes((process.env.SMOKE ?? '').toLowerCase())) { console.error('SMOKE ok: david-goliath loaded'); return }
|
|
124
|
+
const tasks = await loadHumanEval(NTASKS, 0)
|
|
125
|
+
console.error(`=== DAVID(${DAVID}, N=${N} sols + ${T} self-tests) vs GOLIATH(${GOLIATH}, 1 shot) · HumanEval n=${tasks.length} · reps=${REPS} ===`)
|
|
126
|
+
const dU = zero(), gU = zero()
|
|
127
|
+
const units = tasks.flatMap((task) => Array.from({ length: REPS }, () => task))
|
|
128
|
+
let done = 0
|
|
129
|
+
const res = await pool(units, CONC, async (task) => {
|
|
130
|
+
const safe = async (fn: () => Promise<string>) => { try { return await fn() } catch { return '' } }
|
|
131
|
+
const [dCode, gCode] = await Promise.all([safe(() => davidArm(task, dU)), safe(() => goliathArm(task, gU))])
|
|
132
|
+
const [d, g] = await Promise.all([judge(task, dCode), judge(task, gCode)])
|
|
133
|
+
if (++done % 20 === 0) console.error(` ${done}/${units.length} units`)
|
|
134
|
+
return { d, g }
|
|
135
|
+
})
|
|
136
|
+
const n = res.length, dPass = res.filter((r) => r.d).length, gPass = res.filter((r) => r.g).length
|
|
137
|
+
const b = res.filter((r) => r.d && !r.g).length, c = res.filter((r) => !r.d && r.g).length
|
|
138
|
+
const p = mcnemar(b, c)
|
|
139
|
+
const dCost = usd(DAVID, dU), gCost = usd(GOLIATH, gU)
|
|
140
|
+
console.log('\n=== RESULT (held-out HumanEval) ===')
|
|
141
|
+
console.log(` GOLIATH ${GOLIATH} solo : ${gPass}/${n} = ${(gPass / n * 100).toFixed(1)}% $${gCost.toFixed(4)}`)
|
|
142
|
+
console.log(` DAVID ${DAVID} + verify: ${dPass}/${n} = ${(dPass / n * 100).toFixed(1)}% $${dCost.toFixed(4)}`)
|
|
143
|
+
console.log(` accuracy: David ${dPass >= gPass ? '>=' : '<'} Goliath (${(dPass / n * 100).toFixed(1)} vs ${(gPass / n * 100).toFixed(1)}); paired McNemar David-only=${b} Goliath-only=${c} p=${p.toFixed(4)}`)
|
|
144
|
+
console.log(` cost: David is ${(gCost / Math.max(dCost, 1e-9)).toFixed(1)}x CHEAPER ($${dCost.toFixed(4)} vs $${gCost.toFixed(4)})`)
|
|
145
|
+
const paretoBeat = dPass >= gPass && dCost <= gCost
|
|
146
|
+
const sigBeat = dPass > gPass && p < 0.05
|
|
147
|
+
console.log(` VERDICT: ${sigBeat ? 'DAVID SIGNIFICANTLY BEATS GOLIATH' : paretoBeat ? 'DAVID PARETO-DOMINATES (>= accuracy, <= cost)' : dPass >= gPass ? 'David matches accuracy (check cost)' : 'Goliath wins accuracy'}`)
|
|
148
|
+
}
|
|
149
|
+
main().catch((e) => { console.error('MAIN:', e instanceof Error ? (e.stack ?? e.message) : e); process.exit(1) })
|
package/src/hev-improve.mts
CHANGED
|
@@ -65,8 +65,11 @@ async function main(): Promise<void> {
|
|
|
65
65
|
const trainN = Number(process.env.TRAIN_N ?? 12)
|
|
66
66
|
const holdoutN = Number(process.env.HOLDOUT_N ?? 12)
|
|
67
67
|
const offset = Number(process.env.OFFSET ?? 80)
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// generations=1 never exercises the GEPA Pareto/combine path (the frontier
|
|
69
|
+
// needs >=1 completed generation before combine can fire) — default to a
|
|
70
|
+
// multi-generation budget so the default run measures the full loop.
|
|
71
|
+
const generations = Number(process.env.GENERATIONS ?? 6)
|
|
72
|
+
const population = Number(process.env.POPULATION ?? 4)
|
|
70
73
|
const workerMaxTokens = Number(process.env.MAX_TOKENS ?? 6000)
|
|
71
74
|
const reflectMaxTokens = Number(process.env.REFLECT_MAX_TOKENS ?? 8000)
|
|
72
75
|
const maxConcurrency = Number(process.env.MAX_CONCURRENCY ?? 4)
|
|
@@ -115,9 +118,21 @@ async function main(): Promise<void> {
|
|
|
115
118
|
console.log(` [judge] ${scenario.id} pass=0 (empty)`)
|
|
116
119
|
return { dimensions: { pass: 0 }, composite: 0, notes: 'empty' }
|
|
117
120
|
}
|
|
118
|
-
const { pass } = await runChecker(t, code)
|
|
121
|
+
const { pass, detail } = await runChecker(t, code)
|
|
119
122
|
console.log(` [judge] ${scenario.id} pass=${pass}`)
|
|
120
|
-
return { dimensions: { pass }, composite: pass, notes:
|
|
123
|
+
if (pass === 1) return { dimensions: { pass }, composite: pass, notes: 'passed' }
|
|
124
|
+
// Trajectory-grounded failure note: the checker's traceback/assertion tail
|
|
125
|
+
// plus the model's own emitted code, so GEPA reflection sees WHAT failed and
|
|
126
|
+
// WHAT the model wrote — not just the word 'failed'. The candidate's full
|
|
127
|
+
// raw reply additionally reaches the proposer via the campaign breakdown's
|
|
128
|
+
// `emitted` field (carried automatically from the string artifact).
|
|
129
|
+
const traceback = (detail ?? 'checker produced no output (timeout or silent non-zero exit)').slice(-800)
|
|
130
|
+
const excerpt = code.slice(0, 700)
|
|
131
|
+
return {
|
|
132
|
+
dimensions: { pass },
|
|
133
|
+
composite: pass,
|
|
134
|
+
notes: `${traceback}\n--- emitted code (first 700 chars) ---\n${excerpt}`,
|
|
135
|
+
}
|
|
121
136
|
},
|
|
122
137
|
}
|
|
123
138
|
|
|
@@ -146,12 +161,17 @@ async function main(): Promise<void> {
|
|
|
146
161
|
judge,
|
|
147
162
|
agent,
|
|
148
163
|
expectUsage: 'warn',
|
|
164
|
+
// rawTraceContext stays OFF deliberately: it swaps the distilled findings for
|
|
165
|
+
// filesystem paths + grep/cat instructions (rawTraceDistiller), which only a
|
|
166
|
+
// coding harness can execute. This run's proposer is prompt-tier (gepaProposer
|
|
167
|
+
// — a single LLM call that cannot run grep), so the trace evidence arrives via
|
|
168
|
+
// the judge's traceback notes + the breakdown's `emitted` excerpt instead.
|
|
149
169
|
budget: { generations, populationSize: population, holdoutScenarios, maxConcurrency, reps: 1 },
|
|
150
170
|
llm: { baseUrl: reflectBase, apiKey: reflectKey, model: reflectModel },
|
|
151
171
|
})
|
|
152
172
|
|
|
153
173
|
console.log('\n═══ RESULT ═══')
|
|
154
|
-
console.log(`
|
|
174
|
+
console.log(`gateDecision=${out.gateDecision} shipped=${out.shipped} lift=${out.lift}`)
|
|
155
175
|
console.log(`baseline holdout pass-rate = ${out.raw.baseline.compositeMean}`)
|
|
156
176
|
console.log(`winner holdout pass-rate = ${out.raw.winner.compositeMean}`)
|
|
157
177
|
console.log(`baseline per-scenario: ${JSON.stringify(out.raw.baseline.perScenario)}`)
|
|
@@ -160,7 +180,6 @@ async function main(): Promise<void> {
|
|
|
160
180
|
if ((out.raw.winner as { surface?: unknown }).surface) {
|
|
161
181
|
console.log(`winner instruction:\n${String((out.raw.winner as { surface?: unknown }).surface).slice(0, 1200)}`)
|
|
162
182
|
}
|
|
163
|
-
console.log(`live instruction unchanged: ${profile.prompt?.systemPrompt}`)
|
|
164
183
|
}
|
|
165
184
|
|
|
166
185
|
main().catch((e) => {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OBJECT-OF-IMPROVEMENT ablation on HumanEval — the head-on test of the claim our
|
|
3
|
+
* whole self-improvement line rests on: at EQUAL budget, does adding CAPABILITY
|
|
4
|
+
* (a real code-execution tool) beat adding IMPROVER-cleverness (blind prompt
|
|
5
|
+
* self-refinement)? Both arms get the same model, the same K rounds per task, the
|
|
6
|
+
* same held-out tasks. The ONLY difference is what the budget buys:
|
|
7
|
+
*
|
|
8
|
+
* IMPROVER arm — K rounds of "critique your own function and rewrite it",
|
|
9
|
+
* with NO execution. The classic reflect-without-a-tool loop.
|
|
10
|
+
* CAPABILITY arm — K rounds WITH a `run_python` tool: the agent executes its
|
|
11
|
+
* function on inputs it chooses, sees real output/errors, fixes.
|
|
12
|
+
*
|
|
13
|
+
* Grading is a DETERMINISTIC hidden test (never shown): the task's own `check`.
|
|
14
|
+
* If the capability arm wins the held-out pass rate, the object of improvement
|
|
15
|
+
* (what you can DO) dominates the improver (how cleverly you rewrite) — the
|
|
16
|
+
* finding the prompt-only self-improvement runs kept nulling on.
|
|
17
|
+
*
|
|
18
|
+
* Fast by construction: HumanEval tasks are tiny, graded in an isolated Python
|
|
19
|
+
* container with a hard timeout — seconds per task. Paired
|
|
20
|
+
* McNemar over the per-task pass/fail difference gives the significance.
|
|
21
|
+
*
|
|
22
|
+
* Run from cwd=bench: env WORKER_MODEL=google/gemini-2.5-flash-lite N=60 K=3 \
|
|
23
|
+
* REPS=2 node_modules/.bin/tsx src/humaneval-object-ablation.mts
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
loadHumanEval,
|
|
27
|
+
extractCode,
|
|
28
|
+
runPythonProgram,
|
|
29
|
+
type HumanEvalTask,
|
|
30
|
+
} from './benchmarks/humaneval'
|
|
31
|
+
|
|
32
|
+
const ROUTER = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
|
|
33
|
+
const KEY = process.env.TANGLE_API_KEY
|
|
34
|
+
if (!KEY) throw new Error('TANGLE_API_KEY required')
|
|
35
|
+
const MODEL = process.env.WORKER_MODEL ?? 'google/gemini-2.5-flash-lite'
|
|
36
|
+
const N = Number(process.env.N ?? 60)
|
|
37
|
+
const OFFSET = Number(process.env.OFFSET ?? 0)
|
|
38
|
+
const K = Number(process.env.K ?? 3) // rounds/budget per task (equal for both arms)
|
|
39
|
+
const REPS = Number(process.env.REPS ?? 2)
|
|
40
|
+
const CONC = Number(process.env.CONCURRENCY ?? 6)
|
|
41
|
+
const EXEC_TIMEOUT = Number(process.env.EXEC_TIMEOUT_MS ?? 8000)
|
|
42
|
+
|
|
43
|
+
interface ChatMsg { role: string; content: string; tool_calls?: unknown; tool_call_id?: string; name?: string }
|
|
44
|
+
interface Tool { type: 'function'; function: { name: string; description: string; parameters: unknown } }
|
|
45
|
+
|
|
46
|
+
async function router(messages: ChatMsg[], tools?: Tool[]): Promise<{ content: string; toolCalls: { id: string; name: string; args: Record<string, unknown> }[] }> {
|
|
47
|
+
const body: Record<string, unknown> = { model: MODEL, messages, temperature: 0.4 }
|
|
48
|
+
if (tools) { body.tools = tools; body.tool_choice = 'auto' }
|
|
49
|
+
for (let attempt = 0; ; attempt++) {
|
|
50
|
+
let res: Response
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(`${ROUTER}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${KEY}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(60_000) })
|
|
53
|
+
} catch (e) { if (attempt >= 5) throw e; await sleep(800 * 2 ** attempt); continue }
|
|
54
|
+
if ([408, 429, 500, 502, 503, 504, 520, 522, 524].includes(res.status)) { if (attempt >= 5) throw new Error(`router ${res.status} exhausted`); await sleep(800 * 2 ** attempt); continue }
|
|
55
|
+
if (!res.ok) throw new Error(`router ${res.status}: ${(await res.text()).slice(0, 200)}`)
|
|
56
|
+
const j = (await res.json()) as { choices?: { message?: { content?: string; tool_calls?: { id: string; function: { name: string; arguments: string } }[] } }[] }
|
|
57
|
+
const m = j.choices?.[0]?.message
|
|
58
|
+
const toolCalls = (m?.tool_calls ?? []).map((t) => { let args: Record<string, unknown> = {}; try { args = JSON.parse(t.function.arguments) } catch { /* keep {} */ } return { id: t.id, name: t.function.name, args } })
|
|
59
|
+
return { content: m?.content ?? '', toolCalls }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
63
|
+
/** Run model-written Python in the shared networkless, resource-capped container. */
|
|
64
|
+
async function runPython(program: string): Promise<{ stdout: string; stderr: string; ok: boolean }> {
|
|
65
|
+
const result = await runPythonProgram(program, EXEC_TIMEOUT)
|
|
66
|
+
return {
|
|
67
|
+
stdout: result.stdout.slice(0, 2000),
|
|
68
|
+
stderr: result.stderr.slice(0, 2000),
|
|
69
|
+
ok: result.exitCode === 0,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The HIDDEN judge: candidate full function + the task's own check. Never shown. */
|
|
74
|
+
async function judge(task: HumanEvalTask, candidate: string): Promise<boolean> {
|
|
75
|
+
if (!candidate.trim()) return false
|
|
76
|
+
const program = `${candidate}\n\n${task.test}\n\ncheck(${task.entryPoint})\nprint("PASS")\n`
|
|
77
|
+
const r = await runPython(program)
|
|
78
|
+
return r.ok && r.stdout.includes('PASS')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SYSTEM = 'You are an expert Python programmer. Output the COMPLETE function definition (signature + body, plus any imports) in a single ```python block. No tests, no prose outside the block.'
|
|
82
|
+
const userPrompt = (t: HumanEvalTask) => `Complete this function:\n\n\`\`\`python\n${t.prompt}\`\`\``
|
|
83
|
+
|
|
84
|
+
/** IMPROVER arm: K rounds of blind self-refinement — no execution, just "review and rewrite". */
|
|
85
|
+
async function improverArm(task: HumanEvalTask): Promise<string> {
|
|
86
|
+
const messages: ChatMsg[] = [{ role: 'system', content: SYSTEM }, { role: 'user', content: userPrompt(task) }]
|
|
87
|
+
let code = ''
|
|
88
|
+
for (let round = 0; round < K; round++) {
|
|
89
|
+
const { content } = await router(messages)
|
|
90
|
+
code = extractCode(content) || content
|
|
91
|
+
if (round < K - 1) {
|
|
92
|
+
messages.push({ role: 'assistant', content })
|
|
93
|
+
messages.push({ role: 'user', content: 'Carefully review your function for correctness bugs and edge cases. If it can be improved, output the corrected COMPLETE function in a python block; if it is already correct, output it again unchanged.' })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return code
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const RUN_TOOL: Tool = { type: 'function', function: { name: 'run_python', description: 'Execute a Python snippet and return its stdout/stderr. Use it to test your function on example inputs from the docstring before finalizing.', parameters: { type: 'object', properties: { code: { type: 'string', description: 'python source to run' } }, required: ['code'] } } }
|
|
100
|
+
|
|
101
|
+
/** CAPABILITY arm: K rounds WITH a real code-execution tool — write, run, see real output, fix. */
|
|
102
|
+
async function capabilityArm(task: HumanEvalTask): Promise<string> {
|
|
103
|
+
const messages: ChatMsg[] = [
|
|
104
|
+
{ role: 'system', content: `${SYSTEM}\nYou have a run_python tool: test your function on the docstring's example inputs before giving your final answer. Fix any failures you observe.` },
|
|
105
|
+
{ role: 'user', content: userPrompt(task) },
|
|
106
|
+
]
|
|
107
|
+
let code = ''
|
|
108
|
+
// (K-1) tool-exploration rounds + 1 forced final answer = K calls total, matching
|
|
109
|
+
// the improver arm's K blind-refine rounds (equal budget).
|
|
110
|
+
for (let round = 0; round < Math.max(1, K - 1); round++) {
|
|
111
|
+
const { content, toolCalls } = await router(messages, [RUN_TOOL])
|
|
112
|
+
if (content && extractCode(content)) code = extractCode(content)
|
|
113
|
+
messages.push({ role: 'assistant', content: content || '', ...(toolCalls.length ? { tool_calls: toolCalls.map((t) => ({ id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args) } })) } : {}) })
|
|
114
|
+
if (toolCalls.length) {
|
|
115
|
+
for (const tc of toolCalls) {
|
|
116
|
+
const snippet = String(tc.args.code ?? '')
|
|
117
|
+
const r = await runPython(snippet)
|
|
118
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, name: tc.name, content: `stdout:\n${r.stdout}\nstderr:\n${r.stderr}\nexit_ok=${r.ok}` })
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
messages.push({ role: 'user', content: 'Test your function with run_python on the docstring examples before finalizing.' })
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// FAIR FINAL ANSWER: the tool rounds are exploration; force one no-tool call to
|
|
125
|
+
// emit the complete function. Without this, an agent that ends mid-tool-call
|
|
126
|
+
// yields empty code and is unfairly scored 0 (an extraction artifact, not a
|
|
127
|
+
// real "the tool hurt" signal). Only override if it produces a real block.
|
|
128
|
+
{
|
|
129
|
+
messages.push({ role: 'user', content: 'Now output your FINAL complete function in a single ```python block, no tools, no prose.' })
|
|
130
|
+
const { content } = await router(messages)
|
|
131
|
+
const finalCode = extractCode(content)
|
|
132
|
+
if (finalCode.trim()) code = finalCode
|
|
133
|
+
}
|
|
134
|
+
return code
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function pool<T, R>(items: T[], limit: number, fn: (t: T, i: number) => Promise<R>): Promise<R[]> {
|
|
138
|
+
const out = new Array<R>(items.length)
|
|
139
|
+
let next = 0
|
|
140
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
141
|
+
while (next < items.length) { const i = next++; out[i] = await fn(items[i]!, i) }
|
|
142
|
+
}))
|
|
143
|
+
return out
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// McNemar exact (paired): b = capability-only wins, c = improver-only wins.
|
|
147
|
+
function mcnemarP(b: number, c: number): number {
|
|
148
|
+
const n = b + c; if (n === 0) return 1
|
|
149
|
+
const k = Math.min(b, c)
|
|
150
|
+
const lf = (x: number) => { let s = 0; for (let i = 2; i <= x; i++) s += Math.log(i); return s }
|
|
151
|
+
let tail = 0; for (let i = 0; i <= k; i++) tail += Math.exp(lf(n) - lf(i) - lf(n - i) - n * Math.log(2))
|
|
152
|
+
return Math.min(1, 2 * tail)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function main(): Promise<void> {
|
|
156
|
+
if (['1', 'true'].includes((process.env.SMOKE ?? '').toLowerCase())) { console.error('SMOKE ok: humaneval-object-ablation loaded'); return }
|
|
157
|
+
const tasks = await loadHumanEval(N, OFFSET)
|
|
158
|
+
console.error(`=== OBJECT-OF-IMPROVEMENT ablation · HumanEval n=${tasks.length} (offset ${OFFSET}) · model=${MODEL} · K=${K} rounds · reps=${REPS} · equal budget ===`)
|
|
159
|
+
// self-check the local grader on the gold solution of task 0 (never shown to the model)
|
|
160
|
+
if (tasks[0]?.canonicalSolution) {
|
|
161
|
+
const gold = `${tasks[0].prompt}${tasks[0].canonicalSolution}`
|
|
162
|
+
console.error(` grader self-check (gold passes): ${await judge(tasks[0], gold)}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// per (task, rep): run both arms; record pass/fail.
|
|
166
|
+
const units = tasks.flatMap((task) => Array.from({ length: REPS }, (_, rep) => ({ task, rep })))
|
|
167
|
+
let done = 0
|
|
168
|
+
let capEmpty = 0
|
|
169
|
+
const results = await pool(units, CONC, async ({ task }) => {
|
|
170
|
+
// Per-unit resilience: a transient model failure (e.g. a weak model emitting a
|
|
171
|
+
// malformed tool call → router 400) scores that arm 0 for this unit, never
|
|
172
|
+
// crashes the whole run. Both arms wrapped identically so neither is favored.
|
|
173
|
+
const safe = async (fn: () => Promise<string>) => { try { return await fn() } catch { return '' } }
|
|
174
|
+
const [impCode, capCode] = await Promise.all([safe(() => improverArm(task)), safe(() => capabilityArm(task))])
|
|
175
|
+
const [imp, cap] = await Promise.all([judge(task, impCode), judge(task, capCode)])
|
|
176
|
+
done++
|
|
177
|
+
// AUTOPSY: a capability-arm failure with EMPTY final code is an extraction
|
|
178
|
+
// artifact (ended mid-tool-call, never emitted a final function), NOT a real
|
|
179
|
+
// "the tool hurt" signal. Count it so the effect can be separated.
|
|
180
|
+
if (!cap && !capCode.trim()) { capEmpty++; if (imp) console.error(` [cap-empty] ${task.taskId} (improver passed)`) }
|
|
181
|
+
if (done % 20 === 0) console.error(` ${done}/${units.length} units`)
|
|
182
|
+
return { id: task.taskId, imp, cap }
|
|
183
|
+
})
|
|
184
|
+
console.error(` capability-arm empty-final-code (artifact) count: ${capEmpty}/${results.length}`)
|
|
185
|
+
|
|
186
|
+
const impPass = results.filter((r) => r.imp).length
|
|
187
|
+
const capPass = results.filter((r) => r.cap).length
|
|
188
|
+
const b = results.filter((r) => r.cap && !r.imp).length // capability-only wins
|
|
189
|
+
const c = results.filter((r) => !r.cap && r.imp).length // improver-only wins
|
|
190
|
+
const p = mcnemarP(b, c)
|
|
191
|
+
const n = results.length
|
|
192
|
+
const liftPp = ((capPass - impPass) / n) * 100
|
|
193
|
+
|
|
194
|
+
console.log('')
|
|
195
|
+
console.log('=== RESULT (held-out HumanEval, equal budget) ===')
|
|
196
|
+
console.log(` IMPROVER (blind self-refine, no tool): ${impPass}/${n} = ${((impPass / n) * 100).toFixed(1)}%`)
|
|
197
|
+
console.log(` CAPABILITY (self-built execution tool) : ${capPass}/${n} = ${((capPass / n) * 100).toFixed(1)}%`)
|
|
198
|
+
console.log(` lift = ${liftPp >= 0 ? '+' : ''}${liftPp.toFixed(1)}pp paired McNemar: capability-only=${b} improver-only=${c} p=${p.toFixed(4)}`)
|
|
199
|
+
console.log(` verdict: ${p < 0.05 && capPass > impPass ? 'CAPABILITY > IMPROVER (significant)' : p < 0.05 && impPass > capPass ? 'IMPROVER > CAPABILITY (significant)' : 'no significant difference'}`)
|
|
200
|
+
}
|
|
201
|
+
main().catch((e) => { console.error('MAIN:', e instanceof Error ? (e.stack ?? e.message) : e); process.exit(1) })
|