@tangle-network/agent-bench 0.3.6 → 0.3.7
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 +7 -0
- package/dist/adapters.js +2 -2
- package/dist/benchmarks/humaneval.d.ts +10 -1
- package/dist/benchmarks/humaneval.js +5 -3
- package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/{chunk-5SBJCB6W.js → chunk-PWQVGAJB.js} +2 -2
- package/dist/index.js +2 -2
- package/package.json +5 -4
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-pier-agent.mts +1 -0
- 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/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +788 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/calibrate.ts +116 -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 +405 -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/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -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/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 +104 -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/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 +2145 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -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 +489 -0
- package/src/swe-arena/proposer-fanout.test.mts +372 -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 +361 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +498 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +166 -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/dist/chunk-PPYSEKFM.js.map +0 -1
- /package/dist/{chunk-5SBJCB6W.js.map → chunk-PWQVGAJB.js.map} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata bootstrap — regenerates every per-instance input the experiment
|
|
3
|
+
* used to hand-generate into its scratchpad (`task-meta.json` via
|
|
4
|
+
* `load_meta.py`) straight from the SWE-bench Verified dataset through the
|
|
5
|
+
* bench adapter. The HF dataset cache under ~/.cache survives host reboots;
|
|
6
|
+
* the scratchpad does not — this module removes that scratch dependency
|
|
7
|
+
* permanently (proven necessary: a reboot wiped /tmp and took task-meta.json,
|
|
8
|
+
* the verify scripts, and the round configs with it).
|
|
9
|
+
*
|
|
10
|
+
* tsx src/swe-arena/bootstrap-meta.mts --task-meta <out.json> [iid...]
|
|
11
|
+
* tsx src/swe-arena/bootstrap-meta.mts --problems <outDir> [iid...]
|
|
12
|
+
* tsx src/swe-arena/bootstrap-meta.mts --calibrate <verifyDir> <workDir> [iid...]
|
|
13
|
+
*
|
|
14
|
+
* `iid...` defaults to the round-4 improvement set (defaultRound4Config).
|
|
15
|
+
*
|
|
16
|
+
* HONESTY FIREWALL: `--problems` writes ONLY problem statements — the mode a
|
|
17
|
+
* verify-script author is allowed to read. Gold patches surface ONLY on the
|
|
18
|
+
* `--calibrate` path, where calibrate.ts applies them MECHANICALLY (git apply
|
|
19
|
+
* / patch --fuzz=3) and the official judge grades them; no author-facing
|
|
20
|
+
* output ever contains patch/test_patch/FAIL_TO_PASS content.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
24
|
+
import { join, resolve } from 'node:path'
|
|
25
|
+
import { pathToFileURL } from 'node:url'
|
|
26
|
+
import { createSweBenchAdapter } from '../benchmarks/swe-bench.ts'
|
|
27
|
+
import { calibrateInstance, type CalibrationResult } from './calibrate.ts'
|
|
28
|
+
import { defaultRound4Config } from './outer-loop.mts'
|
|
29
|
+
import { loadInstanceImages } from './run-experiment.mts'
|
|
30
|
+
import { createSerializedJudge } from './serialized-judge.ts'
|
|
31
|
+
import type { SweInstance } from './types.ts'
|
|
32
|
+
|
|
33
|
+
/** Validate one adapter task-metadata record into the SweInstance shape the
|
|
34
|
+
* harness consumes. Throws on any missing/empty load-bearing field — a blank
|
|
35
|
+
* problem statement or gold patch must never flow silently into a run. */
|
|
36
|
+
export function assertSweInstance(id: string, metadata: Record<string, unknown> | undefined): SweInstance {
|
|
37
|
+
if (!metadata) throw new Error(`bootstrap-meta: ${id} has no metadata on its adapter task`)
|
|
38
|
+
const str = (key: keyof SweInstance): string => {
|
|
39
|
+
const v = metadata[key]
|
|
40
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
41
|
+
throw new Error(`bootstrap-meta: ${id} has a missing/empty ${String(key)}`)
|
|
42
|
+
}
|
|
43
|
+
return v
|
|
44
|
+
}
|
|
45
|
+
const optStr = (key: keyof SweInstance): string | null => {
|
|
46
|
+
const v = metadata[key]
|
|
47
|
+
return typeof v === 'string' && v.length > 0 ? v : null
|
|
48
|
+
}
|
|
49
|
+
const instance: SweInstance = {
|
|
50
|
+
instance_id: str('instance_id'),
|
|
51
|
+
repo: str('repo'),
|
|
52
|
+
base_commit: str('base_commit'),
|
|
53
|
+
problem_statement: str('problem_statement'),
|
|
54
|
+
patch: str('patch'),
|
|
55
|
+
test_patch: str('test_patch'),
|
|
56
|
+
FAIL_TO_PASS: str('FAIL_TO_PASS'),
|
|
57
|
+
PASS_TO_PASS: str('PASS_TO_PASS'),
|
|
58
|
+
version: optStr('version'),
|
|
59
|
+
environment_setup_commit: optStr('environment_setup_commit'),
|
|
60
|
+
}
|
|
61
|
+
if (instance.instance_id !== id) {
|
|
62
|
+
throw new Error(`bootstrap-meta: metadata instance_id ${instance.instance_id} != requested ${id}`)
|
|
63
|
+
}
|
|
64
|
+
return instance
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One dataset load for all ids, via the adapter (single source of truth). */
|
|
68
|
+
export async function loadSweInstances(ids: string[]): Promise<Map<string, SweInstance>> {
|
|
69
|
+
if (ids.length === 0) throw new Error('bootstrap-meta: empty instance list')
|
|
70
|
+
const adapter = createSweBenchAdapter()
|
|
71
|
+
const tasks = await adapter.loadTasks({ ids, split: 'test' })
|
|
72
|
+
const byId = new Map(tasks.map((t) => [t.id, t]))
|
|
73
|
+
const out = new Map<string, SweInstance>()
|
|
74
|
+
for (const id of ids) {
|
|
75
|
+
const task = byId.get(id)
|
|
76
|
+
if (!task) throw new Error(`bootstrap-meta: ${id} not found in SWE-bench_Verified`)
|
|
77
|
+
out.set(id, assertSweInstance(id, task.metadata as Record<string, unknown> | undefined))
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** task-meta.json — the experiment's exact shape: `{ [iid]: SweInstance }`. */
|
|
83
|
+
export async function writeTaskMeta(ids: string[], outPath: string): Promise<void> {
|
|
84
|
+
const instances = await loadSweInstances(ids)
|
|
85
|
+
await writeFile(outPath, JSON.stringify(Object.fromEntries(instances), null, 1) + '\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** <outDir>/<iid>.problem.md — problem statements ONLY (author-safe). */
|
|
89
|
+
export async function writeProblemStatements(ids: string[], outDir: string): Promise<string[]> {
|
|
90
|
+
const instances = await loadSweInstances(ids)
|
|
91
|
+
await mkdir(outDir, { recursive: true })
|
|
92
|
+
const written: string[] = []
|
|
93
|
+
for (const [id, inst] of instances) {
|
|
94
|
+
const path = join(outDir, `${id}.problem.md`)
|
|
95
|
+
await writeFile(path, inst.problem_statement)
|
|
96
|
+
written.push(path)
|
|
97
|
+
}
|
|
98
|
+
return written
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Dual-calibrate each instance through the M2 path: repro base-fail/gold-pass
|
|
102
|
+
* plus the official-judge gold gate. Gold patches come from the adapter and
|
|
103
|
+
* are only ever applied mechanically. */
|
|
104
|
+
export async function calibrateIds(
|
|
105
|
+
ids: string[],
|
|
106
|
+
opts: { verifyDir: string; workDir: string; instanceImagesPath?: string; keepWorkspaces?: boolean },
|
|
107
|
+
): Promise<CalibrationResult[]> {
|
|
108
|
+
const instances = await loadSweInstances(ids)
|
|
109
|
+
const images = await loadInstanceImages(opts.instanceImagesPath)
|
|
110
|
+
const judge = createSerializedJudge()
|
|
111
|
+
await mkdir(opts.workDir, { recursive: true })
|
|
112
|
+
const results: CalibrationResult[] = []
|
|
113
|
+
for (const id of ids) {
|
|
114
|
+
const inst = instances.get(id)!
|
|
115
|
+
const entry = images[id]
|
|
116
|
+
if (!entry) throw new Error(`bootstrap-meta: ${id} has no image mapping (fixtures/instances.json)`)
|
|
117
|
+
// verifyCmd runs with cwd = the materialized WORKSPACE (and, downstream,
|
|
118
|
+
// worker clones) — the script path must be absolute or bash exits 127.
|
|
119
|
+
const result = await calibrateInstance({
|
|
120
|
+
instanceId: id,
|
|
121
|
+
image: entry.image,
|
|
122
|
+
baseCommit: entry.base_commit,
|
|
123
|
+
goldPatch: inst.patch,
|
|
124
|
+
verifyCmd: `bash ${resolve(opts.verifyDir, `${id}.sh`)}`,
|
|
125
|
+
workDir: opts.workDir,
|
|
126
|
+
judge,
|
|
127
|
+
...(opts.keepWorkspaces !== undefined ? { keepWorkspaces: opts.keepWorkspaces } : {}),
|
|
128
|
+
})
|
|
129
|
+
results.push(result)
|
|
130
|
+
console.log(
|
|
131
|
+
`CALIBRATION ${id}: baseRc=${result.baseRc} goldApplyRc=${result.goldApplyRc} goldRc=${result.goldRc} ` +
|
|
132
|
+
`verifyCalibrated=${result.verifyCalibrated} goldOfficialResolved=${result.goldOfficialResolved} ` +
|
|
133
|
+
`experimentValid=${result.experimentValid}`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return results
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// CLI.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
144
|
+
|
|
145
|
+
if (isMain) {
|
|
146
|
+
const [mode, ...rest] = process.argv.slice(2)
|
|
147
|
+
const defaultIds = defaultRound4Config().instances
|
|
148
|
+
if (mode === '--task-meta') {
|
|
149
|
+
const [outPath, ...ids] = rest
|
|
150
|
+
if (!outPath) {
|
|
151
|
+
console.error('usage: bootstrap-meta.mts --task-meta <out.json> [iid...]')
|
|
152
|
+
process.exit(2)
|
|
153
|
+
}
|
|
154
|
+
await writeTaskMeta(ids.length > 0 ? ids : defaultIds, outPath)
|
|
155
|
+
console.log(`task-meta → ${outPath}`)
|
|
156
|
+
} else if (mode === '--problems') {
|
|
157
|
+
const [outDir, ...ids] = rest
|
|
158
|
+
if (!outDir) {
|
|
159
|
+
console.error('usage: bootstrap-meta.mts --problems <outDir> [iid...]')
|
|
160
|
+
process.exit(2)
|
|
161
|
+
}
|
|
162
|
+
for (const p of await writeProblemStatements(ids.length > 0 ? ids : defaultIds, outDir)) {
|
|
163
|
+
console.log(`problem statement → ${p}`)
|
|
164
|
+
}
|
|
165
|
+
} else if (mode === '--calibrate') {
|
|
166
|
+
const [verifyDir, workDir, ...ids] = rest
|
|
167
|
+
if (!verifyDir || !workDir) {
|
|
168
|
+
console.error('usage: bootstrap-meta.mts --calibrate <verifyDir> <workDir> [iid...]')
|
|
169
|
+
process.exit(2)
|
|
170
|
+
}
|
|
171
|
+
const results = await calibrateIds(ids.length > 0 ? ids : defaultIds, { verifyDir, workDir })
|
|
172
|
+
const summaryPath = join(workDir, 'calibration.json')
|
|
173
|
+
await writeFile(summaryPath, JSON.stringify(results, null, 2) + '\n')
|
|
174
|
+
console.log(`calibration summary → ${summaryPath}`)
|
|
175
|
+
const invalid = results.filter((r) => !r.experimentValid)
|
|
176
|
+
if (invalid.length > 0) {
|
|
177
|
+
console.error(`NOT experiment-valid: ${invalid.map((r) => r.iid).join(', ')}`)
|
|
178
|
+
process.exit(1)
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
console.error(
|
|
182
|
+
'usage: tsx src/swe-arena/bootstrap-meta.mts --task-meta <out.json> [iid...]\n' +
|
|
183
|
+
' tsx src/swe-arena/bootstrap-meta.mts --problems <outDir> [iid...]\n' +
|
|
184
|
+
' tsx src/swe-arena/bootstrap-meta.mts --calibrate <verifyDir> <workDir> [iid...]',
|
|
185
|
+
)
|
|
186
|
+
process.exit(2)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the metadata bootstrap's validation layer — pure, no dataset
|
|
3
|
+
* load. The adapter-backed loaders are exercised by the calibration CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { assertSweInstance } from './bootstrap-meta.mts'
|
|
8
|
+
|
|
9
|
+
const full = {
|
|
10
|
+
instance_id: 'django__django-11532',
|
|
11
|
+
repo: 'django/django',
|
|
12
|
+
base_commit: 'a5308514fb4bc5086c9a16a8a24a945eeebb073c',
|
|
13
|
+
problem_statement: 'Email messages crash on non-ASCII domain…',
|
|
14
|
+
patch: 'diff --git a/django/core/mail/utils.py …',
|
|
15
|
+
test_patch: 'diff --git a/tests/mail/tests.py …',
|
|
16
|
+
FAIL_TO_PASS: '["test_unicode_dns (mail.tests.MailTests)"]',
|
|
17
|
+
PASS_TO_PASS: '["test_ascii (mail.tests.MailTests)"]',
|
|
18
|
+
version: '3.0',
|
|
19
|
+
environment_setup_commit: '419a78300f7cd27611196e1e464d50fd0385ff27',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('assertSweInstance', () => {
|
|
23
|
+
it('accepts a complete metadata record and returns the typed instance', () => {
|
|
24
|
+
const inst = assertSweInstance('django__django-11532', full)
|
|
25
|
+
expect(inst.instance_id).toBe('django__django-11532')
|
|
26
|
+
expect(inst.patch).toContain('diff --git')
|
|
27
|
+
expect(inst.version).toBe('3.0')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('normalizes absent optional fields to null', () => {
|
|
31
|
+
const inst = assertSweInstance('django__django-11532', {
|
|
32
|
+
...full,
|
|
33
|
+
version: undefined,
|
|
34
|
+
environment_setup_commit: '',
|
|
35
|
+
})
|
|
36
|
+
expect(inst.version).toBeNull()
|
|
37
|
+
expect(inst.environment_setup_commit).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('throws on a missing/empty load-bearing field (never a silent blank)', () => {
|
|
41
|
+
expect(() => assertSweInstance('django__django-11532', { ...full, problem_statement: '' })).toThrow(
|
|
42
|
+
/problem_statement/,
|
|
43
|
+
)
|
|
44
|
+
expect(() => assertSweInstance('django__django-11532', { ...full, patch: undefined })).toThrow(/patch/)
|
|
45
|
+
expect(() => assertSweInstance('django__django-11532', undefined)).toThrow(/no metadata/)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('throws on an instance_id mismatch', () => {
|
|
49
|
+
expect(() => assertSweInstance('astropy__astropy-13033', full)).toThrow(/instance_id/)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
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 { materializeWorkspace } from './materialize'
|
|
23
|
+
import { run, shq } from './proc'
|
|
24
|
+
import type { SerializedJudge } from './serialized-judge'
|
|
25
|
+
|
|
26
|
+
export interface CalibrateOptions {
|
|
27
|
+
instanceId: string
|
|
28
|
+
image: string
|
|
29
|
+
baseCommit: string
|
|
30
|
+
/** The official gold patch text (task-meta `patch`). */
|
|
31
|
+
goldPatch: string
|
|
32
|
+
/**
|
|
33
|
+
* Self-repro verify command, run via `bash -c` with cwd = the workspace
|
|
34
|
+
* (calibrate.sh ran `bash verify/<iid>.sh` from inside the tree).
|
|
35
|
+
*/
|
|
36
|
+
verifyCmd: string
|
|
37
|
+
/** Scratch root; two throwaway workspaces are created and removed under it. */
|
|
38
|
+
workDir: string
|
|
39
|
+
/** Judge for the official gold gate. */
|
|
40
|
+
judge: SerializedJudge
|
|
41
|
+
/** Ceiling for one verify run (repro scripts self-limit at 180s; this is a backstop). */
|
|
42
|
+
verifyTimeoutMs?: number
|
|
43
|
+
/** Keep the calibration workspaces for post-mortem. Default: removed. */
|
|
44
|
+
keepWorkspaces?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CalibrationResult {
|
|
48
|
+
iid: string
|
|
49
|
+
/** rc of verify on pristine base (must be nonzero). */
|
|
50
|
+
baseRc: number
|
|
51
|
+
/** rc of applying the gold patch (0 via git apply or the fuzz fallback). */
|
|
52
|
+
goldApplyRc: number
|
|
53
|
+
/** rc of verify with gold applied (must be zero). */
|
|
54
|
+
goldRc: number
|
|
55
|
+
/** base FAILS and gold PASSES. */
|
|
56
|
+
verifyCalibrated: boolean
|
|
57
|
+
/** Official judge resolves the gold patch. */
|
|
58
|
+
goldOfficialResolved: boolean
|
|
59
|
+
/** verifyCalibrated && goldOfficialResolved — the experiment admission bar. */
|
|
60
|
+
experimentValid: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runVerify(verifyCmd: string, ws: string, timeoutMs: number): Promise<number> {
|
|
64
|
+
const res = await run('bash', ['-c', verifyCmd], { cwd: ws, timeoutMs })
|
|
65
|
+
return res.code
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Apply a patch file with calibrate.sh's exact fallback chain:
|
|
70
|
+
* `git apply --whitespace=nowarn`, then `patch -p1 --fuzz=3` on failure.
|
|
71
|
+
* Returns the rc of the LAST attempt (0 = applied).
|
|
72
|
+
*/
|
|
73
|
+
export async function applyPatchWithFallback(ws: string, patchFile: string): Promise<number> {
|
|
74
|
+
const gitApply = await run('git', ['apply', '--whitespace=nowarn', patchFile], { cwd: ws })
|
|
75
|
+
if (gitApply.code === 0) return 0
|
|
76
|
+
const fuzz = await run('bash', ['-c', `patch -p1 --fuzz=3 < ${shq(patchFile)}`], { cwd: ws })
|
|
77
|
+
return fuzz.code
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function calibrateInstance(opts: CalibrateOptions): Promise<CalibrationResult> {
|
|
81
|
+
const { instanceId, image, baseCommit, verifyCmd, judge } = opts
|
|
82
|
+
const verifyTimeoutMs = opts.verifyTimeoutMs ?? 600_000
|
|
83
|
+
const baseWs = join(opts.workDir, `cal-base-${instanceId}`)
|
|
84
|
+
const goldWs = join(opts.workDir, `cal-gold-${instanceId}`)
|
|
85
|
+
const goldPatchFile = join(opts.workDir, `${instanceId}.gold.patch`)
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await materializeWorkspace({ instanceId, image, baseCommit, dest: baseWs })
|
|
89
|
+
const baseRc = await runVerify(verifyCmd, baseWs, verifyTimeoutMs)
|
|
90
|
+
|
|
91
|
+
await materializeWorkspace({ instanceId, image, baseCommit, dest: goldWs })
|
|
92
|
+
await writeFile(goldPatchFile, opts.goldPatch)
|
|
93
|
+
const goldApplyRc = await applyPatchWithFallback(goldWs, goldPatchFile)
|
|
94
|
+
const goldRc = await runVerify(verifyCmd, goldWs, verifyTimeoutMs)
|
|
95
|
+
|
|
96
|
+
const verifyCalibrated = baseRc !== 0 && goldRc === 0
|
|
97
|
+
|
|
98
|
+
const goldVerdict = await judge.judge(instanceId, goldPatchFile, 'gold')
|
|
99
|
+
const goldOfficialResolved = goldVerdict.resolved === true
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
iid: instanceId,
|
|
103
|
+
baseRc,
|
|
104
|
+
goldApplyRc,
|
|
105
|
+
goldRc,
|
|
106
|
+
verifyCalibrated,
|
|
107
|
+
goldOfficialResolved,
|
|
108
|
+
experimentValid: verifyCalibrated && goldOfficialResolved,
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
if (!opts.keepWorkspaces) {
|
|
112
|
+
await rm(baseWs, { recursive: true, force: true })
|
|
113
|
+
await rm(goldWs, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -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
|
+
}
|