@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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual calibration — an instance may enter an experiment only if BOTH gates
|
|
3
|
+
* hold, mirroring the experiment's `calibrate.sh` (repro gate) plus the
|
|
4
|
+
* gold-family judge rows M1 reconciles on (official-judge gate):
|
|
5
|
+
*
|
|
6
|
+
* 1. REPRO GATE (`verifyCalibrated`): on a pristine image-materialized
|
|
7
|
+
* workspace the self-repro verify command must FAIL at base_commit and
|
|
8
|
+
* PASS once the official gold patch is applied (git apply, then
|
|
9
|
+
* `patch --fuzz=3` fallback — several Verified gold patches only apply
|
|
10
|
+
* fuzzily to their own base). A verify that can't see the gold fix can't
|
|
11
|
+
* grade an arm's fix.
|
|
12
|
+
*
|
|
13
|
+
* 2. OFFICIAL-JUDGE GOLD GATE (`goldOfficialResolved`): the official swebench
|
|
14
|
+
* judge (via serialized-judge → adapter.judge) must resolve the gold patch
|
|
15
|
+
* itself. psf__requests-2931/-2317 proved a judge can be blind on an
|
|
16
|
+
* instance whose verify calibrates fine — those became the excluded
|
|
17
|
+
* "gold-ungradeable" rows in the M1 denominator.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { rm, writeFile } from 'node:fs/promises'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
import { pathToFileURL } from 'node:url'
|
|
23
|
+
import { judgeFactoryPatch } from './factory-judge-child.mts'
|
|
24
|
+
import { loadFactoryInstance, loadFactoryInstances, type LoadedFactoryInstance } from './fixtures'
|
|
25
|
+
import { materializeWorkspace } from './materialize'
|
|
26
|
+
import { run, runOk, shq } from './proc'
|
|
27
|
+
import type { SerializedJudge } from './serialized-judge'
|
|
28
|
+
|
|
29
|
+
export interface CalibrateOptions {
|
|
30
|
+
instanceId: string
|
|
31
|
+
image: string
|
|
32
|
+
baseCommit: string
|
|
33
|
+
/** The official gold patch text (task-meta `patch`). */
|
|
34
|
+
goldPatch: string
|
|
35
|
+
/**
|
|
36
|
+
* Self-repro verify command, run via `bash -c` with cwd = the workspace
|
|
37
|
+
* (calibrate.sh ran `bash verify/<iid>.sh` from inside the tree).
|
|
38
|
+
*/
|
|
39
|
+
verifyCmd: string
|
|
40
|
+
/** Scratch root; two throwaway workspaces are created and removed under it. */
|
|
41
|
+
workDir: string
|
|
42
|
+
/** Judge for the official gold gate. */
|
|
43
|
+
judge: SerializedJudge
|
|
44
|
+
/** Ceiling for one verify run (repro scripts self-limit at 180s; this is a backstop). */
|
|
45
|
+
verifyTimeoutMs?: number
|
|
46
|
+
/** Keep the calibration workspaces for post-mortem. Default: removed. */
|
|
47
|
+
keepWorkspaces?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CalibrationResult {
|
|
51
|
+
iid: string
|
|
52
|
+
/** rc of verify on pristine base (must be nonzero). */
|
|
53
|
+
baseRc: number
|
|
54
|
+
/** rc of applying the gold patch (0 via git apply or the fuzz fallback). */
|
|
55
|
+
goldApplyRc: number
|
|
56
|
+
/** rc of verify with gold applied (must be zero). */
|
|
57
|
+
goldRc: number
|
|
58
|
+
/** base FAILS and gold PASSES. */
|
|
59
|
+
verifyCalibrated: boolean
|
|
60
|
+
/** Official judge resolves the gold patch. */
|
|
61
|
+
goldOfficialResolved: boolean
|
|
62
|
+
/** verifyCalibrated && goldOfficialResolved — the experiment admission bar. */
|
|
63
|
+
experimentValid: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runVerify(verifyCmd: string, ws: string, timeoutMs: number): Promise<number> {
|
|
67
|
+
const res = await run('bash', ['-c', verifyCmd], { cwd: ws, timeoutMs })
|
|
68
|
+
return res.code
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Apply a patch file with calibrate.sh's exact fallback chain:
|
|
73
|
+
* `git apply --whitespace=nowarn`, then `patch -p1 --fuzz=3` on failure.
|
|
74
|
+
* Returns the rc of the LAST attempt (0 = applied).
|
|
75
|
+
*/
|
|
76
|
+
export async function applyPatchWithFallback(ws: string, patchFile: string): Promise<number> {
|
|
77
|
+
const gitApply = await run('git', ['apply', '--whitespace=nowarn', patchFile], { cwd: ws })
|
|
78
|
+
if (gitApply.code === 0) return 0
|
|
79
|
+
const fuzz = await run('bash', ['-c', `patch -p1 --fuzz=3 < ${shq(patchFile)}`], { cwd: ws })
|
|
80
|
+
return fuzz.code
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function calibrateInstance(opts: CalibrateOptions): Promise<CalibrationResult> {
|
|
84
|
+
const { instanceId, image, baseCommit, verifyCmd, judge } = opts
|
|
85
|
+
const verifyTimeoutMs = opts.verifyTimeoutMs ?? 600_000
|
|
86
|
+
const baseWs = join(opts.workDir, `cal-base-${instanceId}`)
|
|
87
|
+
const goldWs = join(opts.workDir, `cal-gold-${instanceId}`)
|
|
88
|
+
const goldPatchFile = join(opts.workDir, `${instanceId}.gold.patch`)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await materializeWorkspace({ instanceId, image, baseCommit, dest: baseWs })
|
|
92
|
+
const baseRc = await runVerify(verifyCmd, baseWs, verifyTimeoutMs)
|
|
93
|
+
|
|
94
|
+
await materializeWorkspace({ instanceId, image, baseCommit, dest: goldWs })
|
|
95
|
+
await writeFile(goldPatchFile, opts.goldPatch)
|
|
96
|
+
const goldApplyRc = await applyPatchWithFallback(goldWs, goldPatchFile)
|
|
97
|
+
const goldRc = await runVerify(verifyCmd, goldWs, verifyTimeoutMs)
|
|
98
|
+
|
|
99
|
+
const verifyCalibrated = baseRc !== 0 && goldRc === 0
|
|
100
|
+
|
|
101
|
+
const goldVerdict = await judge.judge(instanceId, goldPatchFile, 'gold')
|
|
102
|
+
const goldOfficialResolved = goldVerdict.resolved === true
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
iid: instanceId,
|
|
106
|
+
baseRc,
|
|
107
|
+
goldApplyRc,
|
|
108
|
+
goldRc,
|
|
109
|
+
verifyCalibrated,
|
|
110
|
+
goldOfficialResolved,
|
|
111
|
+
experimentValid: verifyCalibrated && goldOfficialResolved,
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
if (!opts.keepWorkspaces) {
|
|
115
|
+
await rm(baseWs, { recursive: true, force: true })
|
|
116
|
+
await rm(goldWs, { recursive: true, force: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Factory-bench admission gate — the same "calibrate through the OFFICIAL
|
|
123
|
+
// judge" lesson, generalized: gold (the real PR's impl-only diff) must judge
|
|
124
|
+
// resolved, and the bare base (empty patch) must judge unresolved. Both runs
|
|
125
|
+
// go through the SAME judge code the arena uses (judgeFactoryPatch — the
|
|
126
|
+
// factory-judge-child body), deliberately bypassing serialized-judge's
|
|
127
|
+
// empty-patch short-circuit so the base direction really executes the judge
|
|
128
|
+
// tests on the bare tree instead of trivially returning false.
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
export interface FactoryCalibrationResult {
|
|
132
|
+
iid: string
|
|
133
|
+
/** Gold = impl-only PR diff. Must be resolved with full score. */
|
|
134
|
+
goldResolved: boolean
|
|
135
|
+
goldPassed: number
|
|
136
|
+
/** Base = empty patch. Must be unresolved. */
|
|
137
|
+
baseResolved: boolean
|
|
138
|
+
basePassed: number
|
|
139
|
+
total: number
|
|
140
|
+
/** goldResolved && !baseResolved — the pool admission bar. */
|
|
141
|
+
admitted: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The real PR's impl-only patch: full first-parent diff base→judge_ref minus
|
|
146
|
+
* the judge test files (they are the hidden judge, not the deliverable).
|
|
147
|
+
*/
|
|
148
|
+
export async function goldImplPatch(inst: LoadedFactoryInstance): Promise<string> {
|
|
149
|
+
const res = await runOk('git', [
|
|
150
|
+
'-C', inst.repo_local_mirror,
|
|
151
|
+
'diff', inst.base_commit, inst.judge_ref,
|
|
152
|
+
'--', '.',
|
|
153
|
+
...inst.judge_tests.map((t) => `:(exclude)${t}`),
|
|
154
|
+
])
|
|
155
|
+
if (res.stdout.trim().length === 0) {
|
|
156
|
+
throw new Error(`calibrate ${inst.id}: impl-only gold diff is empty — judge_tests exclude everything?`)
|
|
157
|
+
}
|
|
158
|
+
return res.stdout
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Run both admission directions for one instance. Throws only on infra failure. */
|
|
162
|
+
export async function calibrateFactoryInstance(inst: LoadedFactoryInstance): Promise<FactoryCalibrationResult> {
|
|
163
|
+
const gold = await judgeFactoryPatch(inst, await goldImplPatch(inst))
|
|
164
|
+
const base = await judgeFactoryPatch(inst, '')
|
|
165
|
+
return {
|
|
166
|
+
iid: inst.id,
|
|
167
|
+
goldResolved: gold.result.resolved,
|
|
168
|
+
goldPassed: gold.result.passed,
|
|
169
|
+
baseResolved: base.result.resolved,
|
|
170
|
+
basePassed: base.result.passed,
|
|
171
|
+
total: inst.judgeTestTotal,
|
|
172
|
+
admitted: gold.result.resolved && !base.result.resolved,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// CLI: tsx src/swe-arena/calibrate.ts --factory <instancesDirOrInstanceDir> [id ...]
|
|
178
|
+
// Rejection is LOUD: any instance failing either direction exits nonzero.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
182
|
+
|
|
183
|
+
if (isMain) {
|
|
184
|
+
const [mode, root, ...ids] = process.argv.slice(2)
|
|
185
|
+
if (mode !== '--factory' || !root) {
|
|
186
|
+
console.error('usage: tsx src/swe-arena/calibrate.ts --factory <instancesDir|instanceDir> [id ...]')
|
|
187
|
+
process.exit(2)
|
|
188
|
+
}
|
|
189
|
+
let instances: LoadedFactoryInstance[]
|
|
190
|
+
try {
|
|
191
|
+
instances = loadFactoryInstances(root)
|
|
192
|
+
} catch {
|
|
193
|
+
instances = [loadFactoryInstance(root)]
|
|
194
|
+
}
|
|
195
|
+
if (ids.length > 0) {
|
|
196
|
+
const byId = new Map(instances.map((i) => [i.id, i]))
|
|
197
|
+
instances = ids.map((id) => {
|
|
198
|
+
const inst = byId.get(id)
|
|
199
|
+
if (!inst) throw new Error(`unknown instance id ${id} (have: ${[...byId.keys()].join(', ')})`)
|
|
200
|
+
return inst
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
let rejected = 0
|
|
204
|
+
for (const inst of instances) {
|
|
205
|
+
const r = await calibrateFactoryInstance(inst)
|
|
206
|
+
const verdict = r.admitted ? 'ADMITTED' : 'REJECTED'
|
|
207
|
+
console.log(
|
|
208
|
+
`CALIBRATE ${r.iid}: gold ${r.goldPassed}/${r.total} resolved=${r.goldResolved}; ` +
|
|
209
|
+
`base ${r.basePassed}/${r.total} resolved=${r.baseResolved} → ${verdict}`,
|
|
210
|
+
)
|
|
211
|
+
if (!r.admitted) rejected += 1
|
|
212
|
+
}
|
|
213
|
+
if (rejected > 0) {
|
|
214
|
+
console.error(`calibration gate: ${rejected} instance(s) REJECTED (gold must pass AND base must fail)`)
|
|
215
|
+
process.exit(1)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrate passthrough guard for the improve-loop options this harness
|
|
3
|
+
* depends on: `selfImprove` forwarding `premeasuredBaseline` and
|
|
4
|
+
* `budget.maxImprovementShots` into the loop (merged to agent-eval main).
|
|
5
|
+
*
|
|
6
|
+
* The remaining risk is a STALE INSTALL: the bench consumes agent-eval via a
|
|
7
|
+
* pnpm `file:` dependency, which snapshots the checkout at install time — a
|
|
8
|
+
* rebuilt-but-never-reinstalled substrate silently reverts to a bundle whose
|
|
9
|
+
* `selfImprove` DROPS both options. Silent drop is the worst failure mode
|
|
10
|
+
* here (a "premeasured" baseline would quietly re-run and re-spend; the depth
|
|
11
|
+
* dial would quietly pin to the lib default), so the guard FAILS LOUD instead
|
|
12
|
+
* of falling back.
|
|
13
|
+
*
|
|
14
|
+
* The probe reads the RESOLVED `@tangle-network/agent-eval/contract` module
|
|
15
|
+
* text (the bundle that contains the compiled `selfImprove`) and requires
|
|
16
|
+
* both option names. Verified against the pre-merge build: that bundle
|
|
17
|
+
* contained ZERO occurrences of either literal (selfImprove never named them;
|
|
18
|
+
* `runOptimization`'s own seam compiles into a different chunk), so the probe
|
|
19
|
+
* cannot false-positive on a stale substrate.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync } from 'node:fs'
|
|
23
|
+
import { createRequire } from 'node:module'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
export interface ImproveLoopPassthroughCaps {
|
|
27
|
+
/** `selfImprove` forwards `premeasuredBaseline` into the loop. */
|
|
28
|
+
premeasuredBaseline: boolean
|
|
29
|
+
/** `selfImprove` forwards `budget.maxImprovementShots` into the loop. */
|
|
30
|
+
maxImprovementShots: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Pure probe over the contract module text — unit-testable. */
|
|
34
|
+
export function detectPassthroughCaps(contractModuleText: string): ImproveLoopPassthroughCaps {
|
|
35
|
+
return {
|
|
36
|
+
premeasuredBaseline: contractModuleText.includes('premeasuredBaseline'),
|
|
37
|
+
maxImprovementShots: contractModuleText.includes('maxImprovementShots'),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resolve the installed agent-eval contract bundle's file path. */
|
|
42
|
+
export function resolveContractModulePath(): string {
|
|
43
|
+
const require = createRequire(import.meta.url)
|
|
44
|
+
const url = import.meta.resolve?.('@tangle-network/agent-eval/contract')
|
|
45
|
+
if (typeof url === 'string' && url.startsWith('file:')) return fileURLToPath(url)
|
|
46
|
+
return require.resolve('@tangle-network/agent-eval/contract')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Fail-loud stale-install guard: throws unless the resolved substrate names
|
|
50
|
+
* BOTH passthrough options in its contract bundle. An unreadable bundle also
|
|
51
|
+
* throws — nothing here ever downgrades to a silent fallback. */
|
|
52
|
+
export function assertSubstratePassthroughs(log: (msg: string) => void = () => {}): void {
|
|
53
|
+
let path: string
|
|
54
|
+
let caps: ImproveLoopPassthroughCaps
|
|
55
|
+
try {
|
|
56
|
+
path = resolveContractModulePath()
|
|
57
|
+
caps = detectPassthroughCaps(readFileSync(path, 'utf8'))
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'substrate passthrough probe failed — cannot prove the installed agent-eval forwards ' +
|
|
61
|
+
`premeasuredBaseline/maxImprovementShots: ${(cause as Error).message}`,
|
|
62
|
+
{ cause },
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
log(
|
|
66
|
+
`substrate caps (${path}): premeasuredBaseline=${caps.premeasuredBaseline} maxImprovementShots=${caps.maxImprovementShots}`,
|
|
67
|
+
)
|
|
68
|
+
const missing = (Object.keys(caps) as Array<keyof ImproveLoopPassthroughCaps>).filter((k) => !caps[k])
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`stale substrate install: the resolved agent-eval contract bundle (${path}) never names ` +
|
|
72
|
+
`${missing.join(' + ')}, so selfImprove would silently drop the option(s). ` +
|
|
73
|
+
'Rebuild the checkout (cd ~/code/agent-eval && git pull && pnpm build), then `pnpm install --force` in the bench.',
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrate passthrough guard: the pure probe over module text plus the live
|
|
3
|
+
* contract check against the installed agent-eval bundle. The guard must pass
|
|
4
|
+
* cleanly on a substrate that threads premeasuredBaseline +
|
|
5
|
+
* maxImprovementShots and THROW LOUD (never fall back) on one that drops
|
|
6
|
+
* them — so a run against a stale install dies at t≈0, before it can
|
|
7
|
+
* silently re-spend its baseline.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { describe, expect, it } from 'vitest'
|
|
12
|
+
import {
|
|
13
|
+
assertSubstratePassthroughs,
|
|
14
|
+
detectPassthroughCaps,
|
|
15
|
+
resolveContractModulePath,
|
|
16
|
+
} from './capabilities.mts'
|
|
17
|
+
|
|
18
|
+
describe('detectPassthroughCaps (pure)', () => {
|
|
19
|
+
it('an option name absent from the bundle reads as no capability', () => {
|
|
20
|
+
expect(detectPassthroughCaps('')).toEqual({ premeasuredBaseline: false, maxImprovementShots: false })
|
|
21
|
+
expect(detectPassthroughCaps('function selfImprove(opts) { return runSelfImprove(opts) }')).toEqual({
|
|
22
|
+
premeasuredBaseline: false,
|
|
23
|
+
maxImprovementShots: false,
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('the forwarding literals flip their capability independently', () => {
|
|
28
|
+
expect(detectPassthroughCaps('premeasuredBaseline: opts.premeasuredBaseline,')).toEqual({
|
|
29
|
+
premeasuredBaseline: true,
|
|
30
|
+
maxImprovementShots: false,
|
|
31
|
+
})
|
|
32
|
+
expect(detectPassthroughCaps('maxImprovementShots: budget.maxImprovementShots,')).toEqual({
|
|
33
|
+
premeasuredBaseline: false,
|
|
34
|
+
maxImprovementShots: true,
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('live substrate guard', () => {
|
|
40
|
+
it('passes on a substrate that names both passthroughs, throws loud otherwise', () => {
|
|
41
|
+
const path = resolveContractModulePath()
|
|
42
|
+
expect(path).toMatch(/agent-eval/)
|
|
43
|
+
const caps = detectPassthroughCaps(readFileSync(path, 'utf8'))
|
|
44
|
+
// The guard's contract holds against whatever substrate is installed:
|
|
45
|
+
// both literals present ⇒ clean pass with the caps logged; anything less
|
|
46
|
+
// ⇒ a loud actionable error naming the stale bundle (never a silent
|
|
47
|
+
// fallback). A run against a stale install dies HERE, at t≈0.
|
|
48
|
+
if (caps.premeasuredBaseline && caps.maxImprovementShots) {
|
|
49
|
+
const logged: string[] = []
|
|
50
|
+
expect(() => assertSubstratePassthroughs((msg) => logged.push(msg))).not.toThrow()
|
|
51
|
+
expect(logged.join('\n')).toContain('premeasuredBaseline=true')
|
|
52
|
+
expect(logged.join('\n')).toContain('maxImprovementShots=true')
|
|
53
|
+
} else {
|
|
54
|
+
expect(() => assertSubstratePassthroughs()).toThrow(/stale substrate install/)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint capacity gate — the typed port of `probe-capacity.sh`, generalized
|
|
3
|
+
* after a proven blind spot: the bash probe watched ONLY the z.ai coding
|
|
4
|
+
* endpoint while the supervisor BRAIN rides router.tangle.tools — three
|
|
5
|
+
* evolution rounds went infra-null because the gate said "capacity" while the
|
|
6
|
+
* router 503-stormed. Rule encoded here: gate every arm on the endpoint that
|
|
7
|
+
* arm actually calls; supervisor arms MUST include the router-path probe.
|
|
8
|
+
*
|
|
9
|
+
* Secrets discipline: probes spawn `dotenvx run … -- bash -c 'curl …'` from
|
|
10
|
+
* the secrets dir; the API key is referenced by NAME inside the child shell
|
|
11
|
+
* (single-quoted script, so it is never expanded — let alone logged — in this
|
|
12
|
+
* process).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { run } from './proc'
|
|
16
|
+
import type { SecretsEnv } from './arms'
|
|
17
|
+
|
|
18
|
+
export type CapacityProbe = (signal?: AbortSignal) => Promise<boolean>
|
|
19
|
+
|
|
20
|
+
export interface EndpointCapacityGate {
|
|
21
|
+
/** Human label for status lines (e.g. 'z.ai-coding', 'router'). */
|
|
22
|
+
name: string
|
|
23
|
+
probe: CapacityProbe
|
|
24
|
+
/** Window passes when >= k of n probes succeed (bash: 3 of 4). */
|
|
25
|
+
kOfN: { k: number; n: number }
|
|
26
|
+
/** Consecutive passing windows required before opening. Default 1. */
|
|
27
|
+
steadyM?: number
|
|
28
|
+
/** Total wait budget; exceeded → gate reports no-capacity (orchestrate: 300 min). */
|
|
29
|
+
waitCeilingMs: number
|
|
30
|
+
/** Pause between probes inside a window (bash: 1s). */
|
|
31
|
+
probeIntervalMs?: number
|
|
32
|
+
/** Pause between windows while waiting (orchestrate: 30s). */
|
|
33
|
+
retryDelayMs?: number
|
|
34
|
+
onStatus?: (msg: string) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
|
|
38
|
+
signal?.throwIfAborted()
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let timer: NodeJS.Timeout | undefined
|
|
41
|
+
const onAbort = () => {
|
|
42
|
+
if (timer) clearTimeout(timer)
|
|
43
|
+
signal?.removeEventListener('abort', onAbort)
|
|
44
|
+
reject(signal?.reason ?? new Error('capacity wait aborted'))
|
|
45
|
+
}
|
|
46
|
+
timer = setTimeout(() => {
|
|
47
|
+
signal?.removeEventListener('abort', onAbort)
|
|
48
|
+
resolve()
|
|
49
|
+
}, ms)
|
|
50
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One k-of-n probe window. Exported for direct reuse (bash probe-capacity.sh body). */
|
|
55
|
+
export async function probeWindow(
|
|
56
|
+
gate: EndpointCapacityGate,
|
|
57
|
+
signal?: AbortSignal,
|
|
58
|
+
): Promise<{ ok: number; passed: boolean }> {
|
|
59
|
+
const { k, n } = gate.kOfN
|
|
60
|
+
let ok = 0
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
signal?.throwIfAborted()
|
|
63
|
+
try {
|
|
64
|
+
if (await gate.probe(signal)) ok += 1
|
|
65
|
+
} catch {
|
|
66
|
+
// Endpoint failures count as a failed probe; caller cancellation does not.
|
|
67
|
+
signal?.throwIfAborted()
|
|
68
|
+
}
|
|
69
|
+
signal?.throwIfAborted()
|
|
70
|
+
if (i < n - 1) await sleepWithSignal(gate.probeIntervalMs ?? 1000, signal)
|
|
71
|
+
}
|
|
72
|
+
return { ok, passed: ok >= k }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Block until the endpoint shows steady capacity (steadyM consecutive passing
|
|
77
|
+
* k-of-n windows) or the ceiling elapses. Returns whether capacity was found —
|
|
78
|
+
* callers decide whether a closed gate skips the instance or aborts the run.
|
|
79
|
+
*/
|
|
80
|
+
export async function waitForCapacity(gate: EndpointCapacityGate, signal?: AbortSignal): Promise<boolean> {
|
|
81
|
+
signal?.throwIfAborted()
|
|
82
|
+
const steadyM = gate.steadyM ?? 1
|
|
83
|
+
const deadline = Date.now() + gate.waitCeilingMs
|
|
84
|
+
let consecutive = 0
|
|
85
|
+
for (;;) {
|
|
86
|
+
signal?.throwIfAborted()
|
|
87
|
+
const { ok, passed } = await probeWindow(gate, signal)
|
|
88
|
+
signal?.throwIfAborted()
|
|
89
|
+
gate.onStatus?.(`[${gate.name}] capacity: ${ok}/${gate.kOfN.n}${passed ? '' : ' (below k)'} steady=${passed ? consecutive + 1 : 0}/${steadyM}`)
|
|
90
|
+
if (passed) {
|
|
91
|
+
consecutive += 1
|
|
92
|
+
if (consecutive >= steadyM) return true
|
|
93
|
+
} else {
|
|
94
|
+
consecutive = 0
|
|
95
|
+
}
|
|
96
|
+
if (Date.now() >= deadline) return false
|
|
97
|
+
await sleepWithSignal(gate.retryDelayMs ?? 30_000, signal)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Probe functions.
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export interface HttpProbeSpec {
|
|
106
|
+
url: string
|
|
107
|
+
/** NAME of the env var holding the bearer key (resolved in the dotenvx child). */
|
|
108
|
+
apiKeyEnv: string
|
|
109
|
+
model: string
|
|
110
|
+
secrets: SecretsEnv
|
|
111
|
+
/** curl --max-time, seconds. Default 40 (probe-capacity.sh). */
|
|
112
|
+
maxTimeS?: number
|
|
113
|
+
/**
|
|
114
|
+
* max_tokens in the probe body. Default 8000 — glm-5.2 returns empty content
|
|
115
|
+
* below that (measured), and an empty-content 200 would be a lying probe.
|
|
116
|
+
*/
|
|
117
|
+
maxTokens?: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const ZAI_CODING_ENDPOINT = 'https://api.z.ai/api/coding/paas/v4/chat/completions'
|
|
121
|
+
export const ROUTER_ENDPOINT = 'https://router.tangle.tools/v1/chat/completions'
|
|
122
|
+
|
|
123
|
+
/** Build the probe request body (probe-body.json semantics). */
|
|
124
|
+
export function probeBody(model: string, maxTokens: number): string {
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
model,
|
|
127
|
+
messages: [{ role: 'user', content: 'Reply with the single word OK.' }],
|
|
128
|
+
max_tokens: maxTokens,
|
|
129
|
+
temperature: 0,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generic chat-completions probe: true iff the endpoint returns HTTP 200
|
|
135
|
+
* within the time budget. The key stays inside the child shell.
|
|
136
|
+
*/
|
|
137
|
+
export function httpCapacityProbe(spec: HttpProbeSpec): CapacityProbe {
|
|
138
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(spec.apiKeyEnv)) {
|
|
139
|
+
throw new Error(`invalid apiKeyEnv name: ${spec.apiKeyEnv}`)
|
|
140
|
+
}
|
|
141
|
+
const body = probeBody(spec.model, spec.maxTokens ?? 8000)
|
|
142
|
+
const maxTime = spec.maxTimeS ?? 40
|
|
143
|
+
return async (signal?: AbortSignal) => {
|
|
144
|
+
signal?.throwIfAborted()
|
|
145
|
+
// Body via stdin (--data @-) so the payload never sits on a command line.
|
|
146
|
+
// The HTTP code is marker-anchored because dotenvx writes its injection
|
|
147
|
+
// banner to the same stdout stream.
|
|
148
|
+
const script =
|
|
149
|
+
`curl -sS -o /dev/null -w "HTTP_CODE=%{http_code}" --max-time ${maxTime} ` +
|
|
150
|
+
`-X POST "$PROBE_URL" ` +
|
|
151
|
+
`-H "Authorization: Bearer $${spec.apiKeyEnv}" -H "Content-Type: application/json" ` +
|
|
152
|
+
`--data @-`
|
|
153
|
+
const argv = ['run', ...spec.secrets.envFiles.flatMap((f) => ['-f', f]), '--', 'bash', '-c', script]
|
|
154
|
+
const res = await run('dotenvx', argv, {
|
|
155
|
+
cwd: spec.secrets.secretsDir,
|
|
156
|
+
timeoutMs: (maxTime + 20) * 1000,
|
|
157
|
+
stdin: body,
|
|
158
|
+
env: { ...process.env, PROBE_URL: spec.url },
|
|
159
|
+
signal,
|
|
160
|
+
})
|
|
161
|
+
signal?.throwIfAborted()
|
|
162
|
+
return /HTTP_CODE=200\s*$/.test(res.stdout)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** probe-capacity.sh's z.ai coding-plan probe (the WORKER path). */
|
|
167
|
+
export function zaiCodingProbe(secrets: SecretsEnv, model = 'glm-5.2'): CapacityProbe {
|
|
168
|
+
return httpCapacityProbe({ url: ZAI_CODING_ENDPOINT, apiKeyEnv: 'ZAI_API_KEY', model, secrets })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Router-path probe (the BRAIN path — router.tangle.tools with TANGLE_API_KEY).
|
|
173
|
+
* Supervisor arms must gate on this; probing only z.ai is the proven blind spot.
|
|
174
|
+
*/
|
|
175
|
+
export function routerProbe(secrets: SecretsEnv, model = 'glm-5.2'): CapacityProbe {
|
|
176
|
+
return httpCapacityProbe({ url: ROUTER_ENDPOINT, apiKeyEnv: 'TANGLE_API_KEY', model, secrets })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The gates an arm must pass, by kind: solo → worker path; supervisor → BOTH paths. */
|
|
180
|
+
export function gatesForArmKind(
|
|
181
|
+
kind: 'solo' | 'supervisor',
|
|
182
|
+
secrets: SecretsEnv,
|
|
183
|
+
opts: { waitCeilingMs?: number; model?: string; onStatus?: (msg: string) => void } = {},
|
|
184
|
+
): EndpointCapacityGate[] {
|
|
185
|
+
const base = {
|
|
186
|
+
kOfN: { k: 3, n: 4 },
|
|
187
|
+
waitCeilingMs: opts.waitCeilingMs ?? 300 * 60_000,
|
|
188
|
+
...(opts.onStatus ? { onStatus: opts.onStatus } : {}),
|
|
189
|
+
}
|
|
190
|
+
const worker: EndpointCapacityGate = { name: 'z.ai-coding', probe: zaiCodingProbe(secrets, opts.model), ...base }
|
|
191
|
+
if (kind === 'solo') return [worker]
|
|
192
|
+
const brain: EndpointCapacityGate = { name: 'router', probe: routerProbe(secrets, opts.model), ...base }
|
|
193
|
+
return [worker, brain]
|
|
194
|
+
}
|