@tangle-network/agent-bench 0.1.0 → 0.3.5
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 +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
createReadStream,
|
|
5
|
+
existsSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs'
|
|
13
|
+
import { basename, isAbsolute, join, resolve } from 'node:path'
|
|
14
|
+
|
|
15
|
+
const identityFile = 'identity.json'
|
|
16
|
+
const terminalFile = 'terminal.json'
|
|
17
|
+
const stopFile = 'stop-requested'
|
|
18
|
+
|
|
19
|
+
function fail(message) {
|
|
20
|
+
throw new Error(message)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJson(path, label) {
|
|
24
|
+
let value
|
|
25
|
+
try {
|
|
26
|
+
value = JSON.parse(readFileSync(path, 'utf8'))
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`${label} is not valid JSON`, { cause: error })
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
31
|
+
fail(`${label} must be an object`)
|
|
32
|
+
}
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function syncDirectory(directory) {
|
|
37
|
+
const fd = openSync(directory, 'r')
|
|
38
|
+
try {
|
|
39
|
+
fsyncSync(fd)
|
|
40
|
+
} finally {
|
|
41
|
+
closeSync(fd)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeJsonAtomic(directory, name, value) {
|
|
46
|
+
const target = join(directory, name)
|
|
47
|
+
const temporary = join(directory, `.${name}.${process.pid}.tmp`)
|
|
48
|
+
const fd = openSync(temporary, 'wx', 0o600)
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(fd, `${JSON.stringify(value)}\n`, 'utf8')
|
|
51
|
+
fsyncSync(fd)
|
|
52
|
+
} finally {
|
|
53
|
+
closeSync(fd)
|
|
54
|
+
}
|
|
55
|
+
renameSync(temporary, target)
|
|
56
|
+
syncDirectory(directory)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readControlInput() {
|
|
60
|
+
const chunks = []
|
|
61
|
+
for await (const chunk of createReadStream('', { fd: 3 })) {
|
|
62
|
+
chunks.push(Buffer.from(chunk))
|
|
63
|
+
}
|
|
64
|
+
const bytes = Buffer.concat(chunks)
|
|
65
|
+
if (bytes.byteLength === 0) fail('Pier supervisor received no launch request')
|
|
66
|
+
return JSON.parse(bytes.toString('utf8'))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function processIdentity(pid) {
|
|
70
|
+
const text = readFileSync(`/proc/${pid}/stat`, 'utf8')
|
|
71
|
+
const closing = text.lastIndexOf(')')
|
|
72
|
+
if (closing < 0) fail(`cannot parse process identity for ${pid}`)
|
|
73
|
+
const fields = text.slice(closing + 2).trim().split(/\s+/)
|
|
74
|
+
const processGroupId = Number(fields[2])
|
|
75
|
+
const sessionId = Number(fields[3])
|
|
76
|
+
const startTicks = fields[19]
|
|
77
|
+
if (
|
|
78
|
+
!Number.isSafeInteger(processGroupId) ||
|
|
79
|
+
!Number.isSafeInteger(sessionId) ||
|
|
80
|
+
!/^\d+$/.test(startTicks ?? '')
|
|
81
|
+
) {
|
|
82
|
+
fail(`cannot parse process identity for ${pid}`)
|
|
83
|
+
}
|
|
84
|
+
return { pid, processGroupId, sessionId, startTicks }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function processMatches(identity) {
|
|
88
|
+
try {
|
|
89
|
+
const current = processIdentity(identity.pid)
|
|
90
|
+
return (
|
|
91
|
+
current.startTicks === identity.startTicks &&
|
|
92
|
+
current.processGroupId === identity.processGroupId &&
|
|
93
|
+
current.sessionId === identity.sessionId
|
|
94
|
+
)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error?.code === 'ENOENT' || error?.code === 'ESRCH') return false
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sessionMembers(sessionId) {
|
|
102
|
+
const members = []
|
|
103
|
+
for (const entry of readdirSync('/proc', { withFileTypes: true })) {
|
|
104
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue
|
|
105
|
+
try {
|
|
106
|
+
const identity = processIdentity(Number(entry.name))
|
|
107
|
+
if (identity.sessionId === sessionId) members.push(identity)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error?.code !== 'ENOENT') throw error
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return members
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function signalSessionUntilEmpty(identity, signal, timeoutMs) {
|
|
116
|
+
const deadline = Date.now() + timeoutMs
|
|
117
|
+
while (true) {
|
|
118
|
+
const members = sessionMembers(identity.sessionId)
|
|
119
|
+
if (members.length === 0) return true
|
|
120
|
+
for (const member of members) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(member.pid, signal)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code !== 'ESRCH') throw error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (Date.now() >= deadline) return false
|
|
128
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 20))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sanitizeProject(value) {
|
|
133
|
+
let project = value.toLowerCase()
|
|
134
|
+
if (!/^[a-z0-9]/.test(project)) project = `0${project}`
|
|
135
|
+
return project.replace(/[^a-z0-9_-]/g, '-')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function trialProjects(jobsDirectory, jobName) {
|
|
139
|
+
const jobRoot = join(jobsDirectory, jobName)
|
|
140
|
+
if (!existsSync(jobRoot)) return []
|
|
141
|
+
return readdirSync(jobRoot, { withFileTypes: true })
|
|
142
|
+
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
|
|
143
|
+
.map((entry) => sanitizeProject(entry.name))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function matchingContainers(dockerCommand, projects) {
|
|
147
|
+
if (projects.length === 0) return []
|
|
148
|
+
const output = execFileSync(
|
|
149
|
+
dockerCommand,
|
|
150
|
+
['ps', '-a', '--format', '{{.ID}}\t{{.Label "com.docker.compose.project"}}'],
|
|
151
|
+
{ encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 },
|
|
152
|
+
)
|
|
153
|
+
const matches = []
|
|
154
|
+
for (const line of output.split('\n')) {
|
|
155
|
+
if (!line) continue
|
|
156
|
+
const tab = line.indexOf('\t')
|
|
157
|
+
if (tab < 1) continue
|
|
158
|
+
const id = line.slice(0, tab)
|
|
159
|
+
const project = line.slice(tab + 1)
|
|
160
|
+
if (
|
|
161
|
+
projects.some(
|
|
162
|
+
(expected) =>
|
|
163
|
+
project === expected || project.startsWith(`${expected}__verifier__`),
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
166
|
+
matches.push(id)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return matches
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function removeTrialContainers(config) {
|
|
173
|
+
const projects = trialProjects(config.jobsDirectory, config.jobName)
|
|
174
|
+
const containers = matchingContainers(config.dockerCommand, projects)
|
|
175
|
+
if (containers.length > 0) {
|
|
176
|
+
try {
|
|
177
|
+
execFileSync(config.dockerCommand, ['rm', '-f', ...containers], {
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
timeout: 30_000,
|
|
180
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
181
|
+
})
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (matchingContainers(config.dockerCommand, projects).length > 0) throw error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const remaining = matchingContainers(config.dockerCommand, projects)
|
|
187
|
+
if (remaining.length > 0) {
|
|
188
|
+
fail(`Pier task containers survived removal: ${remaining.join(', ')}`)
|
|
189
|
+
}
|
|
190
|
+
return containers.length
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function waitForExit(exited, timeoutMs) {
|
|
194
|
+
return await Promise.race([
|
|
195
|
+
exited.then(() => true),
|
|
196
|
+
new Promise((resolveTimeout) => setTimeout(() => resolveTimeout(false), timeoutMs)),
|
|
197
|
+
])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const directory = resolve(process.argv[2] ?? '')
|
|
201
|
+
if (!isAbsolute(directory) || basename(directory) === '') {
|
|
202
|
+
fail('Pier supervisor control directory must be absolute')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let config
|
|
206
|
+
let child
|
|
207
|
+
let childIdentity
|
|
208
|
+
let stopRequested = existsSync(join(directory, stopFile))
|
|
209
|
+
let stopping
|
|
210
|
+
|
|
211
|
+
async function stopChild() {
|
|
212
|
+
if (!child) return
|
|
213
|
+
if (!childIdentity) {
|
|
214
|
+
try {
|
|
215
|
+
child.process.kill('SIGKILL')
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error?.code !== 'ESRCH') throw error
|
|
218
|
+
}
|
|
219
|
+
if (!(await waitForExit(child.exited, 5_000))) {
|
|
220
|
+
fail('Pier process with unreadable identity survived SIGKILL')
|
|
221
|
+
}
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
if (!(await signalSessionUntilEmpty(childIdentity, 'SIGTERM', 2_000))) {
|
|
225
|
+
if (!(await signalSessionUntilEmpty(childIdentity, 'SIGKILL', 5_000))) {
|
|
226
|
+
fail(`Pier process session ${childIdentity.sessionId} survived SIGKILL`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!(await waitForExit(child.exited, 5_000))) {
|
|
230
|
+
fail(`Pier process session ${childIdentity.sessionId} retained child processes`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function requestStop() {
|
|
235
|
+
stopRequested = true
|
|
236
|
+
stopping ??= stopChild()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
process.on('SIGTERM', requestStop)
|
|
240
|
+
process.on('SIGINT', requestStop)
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
config = await readControlInput()
|
|
244
|
+
if (
|
|
245
|
+
!config ||
|
|
246
|
+
typeof config !== 'object' ||
|
|
247
|
+
typeof config.command !== 'string' ||
|
|
248
|
+
!Array.isArray(config.args) ||
|
|
249
|
+
typeof config.cwd !== 'string' ||
|
|
250
|
+
!config.env ||
|
|
251
|
+
typeof config.env !== 'object' ||
|
|
252
|
+
typeof config.jobsDirectory !== 'string' ||
|
|
253
|
+
typeof config.jobName !== 'string' ||
|
|
254
|
+
typeof config.dockerCommand !== 'string'
|
|
255
|
+
) {
|
|
256
|
+
fail('Pier supervisor launch request is malformed')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const identity = readJson(join(directory, identityFile), 'Pier trial identity')
|
|
260
|
+
if (stopRequested || existsSync(join(directory, stopFile))) {
|
|
261
|
+
const removedContainers = removeTrialContainers(config)
|
|
262
|
+
writeJsonAtomic(directory, terminalFile, {
|
|
263
|
+
schemaVersion: 1,
|
|
264
|
+
kind: 'pier-trial-terminal',
|
|
265
|
+
status: 'stopped',
|
|
266
|
+
processExited: true,
|
|
267
|
+
containersRemoved: true,
|
|
268
|
+
removedContainers,
|
|
269
|
+
})
|
|
270
|
+
process.exitCode = 0
|
|
271
|
+
} else {
|
|
272
|
+
const spawned = spawn(config.command, config.args, {
|
|
273
|
+
cwd: config.cwd,
|
|
274
|
+
env: config.env,
|
|
275
|
+
detached: true,
|
|
276
|
+
stdio: 'ignore',
|
|
277
|
+
})
|
|
278
|
+
if (spawned.pid === undefined) fail('Pier process started without a pid')
|
|
279
|
+
const exited = new Promise((resolveExit) => {
|
|
280
|
+
let settled = false
|
|
281
|
+
spawned.once('error', (error) => {
|
|
282
|
+
if (settled) return
|
|
283
|
+
settled = true
|
|
284
|
+
resolveExit({ code: null, signal: null, error })
|
|
285
|
+
})
|
|
286
|
+
spawned.once('close', (code, signal) => {
|
|
287
|
+
if (settled) return
|
|
288
|
+
settled = true
|
|
289
|
+
resolveExit({ code, signal })
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
child = { process: spawned, exited }
|
|
293
|
+
childIdentity = processIdentity(spawned.pid)
|
|
294
|
+
writeJsonAtomic(directory, identityFile, {
|
|
295
|
+
...identity,
|
|
296
|
+
state: 'running',
|
|
297
|
+
pier: childIdentity,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
if (stopRequested || existsSync(join(directory, stopFile))) {
|
|
301
|
+
stopRequested = true
|
|
302
|
+
stopping = stopChild()
|
|
303
|
+
}
|
|
304
|
+
const exit = await exited
|
|
305
|
+
if (stopping) await stopping
|
|
306
|
+
else await stopChild()
|
|
307
|
+
const removedContainers = removeTrialContainers(config)
|
|
308
|
+
const status = stopRequested ? 'stopped' : exit.code === 0 ? 'completed' : 'failed'
|
|
309
|
+
writeJsonAtomic(directory, terminalFile, {
|
|
310
|
+
schemaVersion: 1,
|
|
311
|
+
kind: 'pier-trial-terminal',
|
|
312
|
+
status,
|
|
313
|
+
processExited: true,
|
|
314
|
+
containersRemoved: true,
|
|
315
|
+
removedContainers,
|
|
316
|
+
exitCode: exit.code,
|
|
317
|
+
signal: exit.signal,
|
|
318
|
+
...(exit.error ? { error: 'Pier process failed to start' } : {}),
|
|
319
|
+
...(status === 'failed' && !exit.error
|
|
320
|
+
? { error: `Pier process exited ${exit.signal ?? exit.code ?? 'without status'}` }
|
|
321
|
+
: {}),
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const cleanupErrors = []
|
|
326
|
+
try {
|
|
327
|
+
requestStop()
|
|
328
|
+
if (stopping) await stopping
|
|
329
|
+
} catch (cleanup) {
|
|
330
|
+
cleanupErrors.push(cleanup)
|
|
331
|
+
}
|
|
332
|
+
if (config) {
|
|
333
|
+
try {
|
|
334
|
+
removeTrialContainers(config)
|
|
335
|
+
} catch (cleanup) {
|
|
336
|
+
cleanupErrors.push(cleanup)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
writeJsonAtomic(directory, terminalFile, {
|
|
340
|
+
schemaVersion: 1,
|
|
341
|
+
kind: 'pier-trial-terminal',
|
|
342
|
+
status: 'failed',
|
|
343
|
+
processExited: !childIdentity || !processMatches(childIdentity),
|
|
344
|
+
containersRemoved: cleanupErrors.length === 0,
|
|
345
|
+
error: `${error instanceof Error ? error.message : String(error)}${
|
|
346
|
+
cleanupErrors.length > 0
|
|
347
|
+
? `; cleanup failed: ${cleanupErrors.map((item) => item?.message ?? String(item)).join('; ')}`
|
|
348
|
+
: ''
|
|
349
|
+
}`,
|
|
350
|
+
})
|
|
351
|
+
process.exitCode = 1
|
|
352
|
+
}
|
package/src/resolve-client.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* domains whose worker is a completion, or where box egress is blocked.
|
|
9
9
|
* - `router` + searchProvider → OFF-BOX `router-tools`: the same off-box loop with a live
|
|
10
10
|
* `web_search` tool (the capability axis research benches need).
|
|
11
|
+
* - `bridge` → OFF-BOX: a local cli-bridge fronting a harness CLI
|
|
12
|
+
* (opencode/kimi-code/…) as the leaf executor. Same resumable
|
|
13
|
+
* `bridgeExecutor` the supervisor uses; harness+model ride the
|
|
14
|
+
* bridge `model` id (`${harness}/${model}`).
|
|
11
15
|
* - anything else (`sandbox`/a BackendType) → IN-BOX: a real `Sandbox`. The in-box backend
|
|
12
16
|
* TYPE (opencode/codex/…) is set separately on the `AgentRunSpec`;
|
|
13
17
|
* this only decides off-box-vs-in-box transport for `runLoop`.
|
|
@@ -15,7 +19,12 @@
|
|
|
15
19
|
* Centralizing it here means a new entrypoint gets the full off-box/in-box matrix for free and
|
|
16
20
|
* the mapping can't drift between callers.
|
|
17
21
|
*/
|
|
18
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createExecutor,
|
|
24
|
+
inlineSandboxClient,
|
|
25
|
+
resolveSandboxClient,
|
|
26
|
+
type SandboxClient,
|
|
27
|
+
} from '@tangle-network/agent-runtime/loops'
|
|
19
28
|
import { Sandbox } from '@tangle-network/sandbox'
|
|
20
29
|
import { makeSearchExecutor, webSearchTool } from './search-tool'
|
|
21
30
|
|
|
@@ -29,8 +38,12 @@ export interface ResolveBenchClientOptions {
|
|
|
29
38
|
* with a live `web_search` tool backed by this provider (`you`/`exa`/…). */
|
|
30
39
|
searchProvider?: string
|
|
31
40
|
sandboxBaseUrl?: string
|
|
32
|
-
/** In-box sandbox timeout (ms). */
|
|
41
|
+
/** In-box sandbox timeout (ms). Also the per-turn deadline for the `bridge` backend. */
|
|
33
42
|
timeoutMs?: number
|
|
43
|
+
/** `bridge` backend: cli-bridge base URL. Defaults to `http://127.0.0.1:3355`. */
|
|
44
|
+
bridgeUrl?: string
|
|
45
|
+
/** `bridge` backend: bearer the bridge requires. Falls back to `routerKey` when unset. */
|
|
46
|
+
bridgeBearer?: string
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export function resolveBenchClient(opts: ResolveBenchClientOptions): SandboxClient {
|
|
@@ -50,6 +63,16 @@ export function resolveBenchClient(opts: ResolveBenchClientOptions): SandboxClie
|
|
|
50
63
|
}
|
|
51
64
|
return inlineSandboxClient(createExecutor({ backend: 'router', routerBaseUrl, routerKey, model }))
|
|
52
65
|
}
|
|
66
|
+
if (backend === 'bridge') {
|
|
67
|
+
// bench's bearer fallback (`?? routerKey`) resolves first, then the shared
|
|
68
|
+
// resolver core wires the bridge seam — no re-implemented createExecutor branch.
|
|
69
|
+
const bridgeBearer = opts.bridgeBearer ?? routerKey
|
|
70
|
+
if (!bridgeBearer) throw new Error("resolveBenchClient: backend 'bridge' needs bridgeBearer or routerKey")
|
|
71
|
+
return resolveSandboxClient({
|
|
72
|
+
backend: 'bridge',
|
|
73
|
+
bridge: { url: opts.bridgeUrl, bearer: bridgeBearer, model, timeoutMs: opts.timeoutMs },
|
|
74
|
+
})
|
|
75
|
+
}
|
|
53
76
|
return new Sandbox({
|
|
54
77
|
baseUrl: opts.sandboxBaseUrl ?? 'https://sandbox.tangle.tools',
|
|
55
78
|
apiKey: routerKey,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a SUBSET of the benchmark registry over a matrix of agent cells, end to end:
|
|
3
|
+
*
|
|
4
|
+
* export TANGLE_API_KEY=... # router + each adapter judge's creds
|
|
5
|
+
* BENCHMARKS=humaneval,swe-bench \
|
|
6
|
+
* CELLS=opencode/glm-4.6,codex/gpt-5,deepseek-v4-flash \
|
|
7
|
+
* N=20 CONCURRENCY=6 tsx src/run-benchmarks-cli.mts
|
|
8
|
+
*
|
|
9
|
+
* A cell is `harness/model` (in-box: that coding harness runs the model) or a bare `model`
|
|
10
|
+
* (off-box: a router completion is the worker). The number is the adapter's OWN deterministic
|
|
11
|
+
* judge — pick benchmarks whose `adapter.judge` is a runnable checker. Output is a leaderboard:
|
|
12
|
+
* resolve-rate and mean graded score per (benchmark × cell), with any unavailable benchmark
|
|
13
|
+
* (missing Docker/venv/dataset) listed, never silently dropped.
|
|
14
|
+
*/
|
|
15
|
+
import { printBenchmarksReport, runBenchmarks, type BenchCell } from './run-benchmarks'
|
|
16
|
+
|
|
17
|
+
const must = (k: string): string => {
|
|
18
|
+
const v = process.env[k]
|
|
19
|
+
if (!v) throw new Error(`env ${k} is required`)
|
|
20
|
+
return v
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `harness/model` → in-box cell; bare `model` → off-box router cell. For cli-bridge, the
|
|
24
|
+
* full model id is already `harness/model`, so do not split it. */
|
|
25
|
+
function parseCell(spec: string): BenchCell {
|
|
26
|
+
const s = spec.trim()
|
|
27
|
+
if (process.env.BACKEND === 'bridge') {
|
|
28
|
+
return { label: s, model: s, backend: 'bridge', ...(process.env.SEARCH_PROVIDER ? { searchProvider: process.env.SEARCH_PROVIDER } : {}) }
|
|
29
|
+
}
|
|
30
|
+
const slash = s.indexOf('/')
|
|
31
|
+
if (slash > 0) {
|
|
32
|
+
const harness = s.slice(0, slash)
|
|
33
|
+
const model = s.slice(slash + 1)
|
|
34
|
+
return { label: s, harness, model, backend: process.env.BACKEND ?? 'sandbox', ...(process.env.SEARCH_PROVIDER ? { searchProvider: process.env.SEARCH_PROVIDER } : {}) }
|
|
35
|
+
}
|
|
36
|
+
return { label: s, model: s, backend: process.env.BACKEND ?? 'router', ...(process.env.SEARCH_PROVIDER ? { searchProvider: process.env.SEARCH_PROVIDER } : {}) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main(): Promise<void> {
|
|
40
|
+
const benchmarks = (process.env.BENCHMARKS ?? '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
41
|
+
if (benchmarks.length === 0) throw new Error('env BENCHMARKS is required (comma-separated registry keys, e.g. humaneval,swe-bench)')
|
|
42
|
+
const cells = (process.env.CELLS ?? process.env.WORKER_MODEL ?? 'deepseek-v4-flash')
|
|
43
|
+
.split(',').map((s) => s.trim()).filter(Boolean).map(parseCell)
|
|
44
|
+
|
|
45
|
+
const report = await runBenchmarks({
|
|
46
|
+
benchmarks,
|
|
47
|
+
cells,
|
|
48
|
+
routerBaseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
|
|
49
|
+
routerKey: must('TANGLE_API_KEY'),
|
|
50
|
+
...(process.env.BRIDGE_URL ? { bridgeUrl: process.env.BRIDGE_URL } : {}),
|
|
51
|
+
...(process.env.BRIDGE_BEARER ?? process.env.CLI_BRIDGE_BEARER ? { bridgeBearer: process.env.BRIDGE_BEARER ?? process.env.CLI_BRIDGE_BEARER } : {}),
|
|
52
|
+
...(process.env.SANDBOX_BASE ? { sandboxBaseUrl: process.env.SANDBOX_BASE } : {}),
|
|
53
|
+
n: Number(process.env.N ?? 10),
|
|
54
|
+
...(process.env.IDS ? { ids: process.env.IDS.split(',') } : {}),
|
|
55
|
+
...(process.env.SPLIT ? { split: process.env.SPLIT } : {}),
|
|
56
|
+
...(process.env.REPS ? { reps: Number(process.env.REPS) } : {}),
|
|
57
|
+
...(process.env.LOOP_ATTEMPTS ? { loopAttempts: Number(process.env.LOOP_ATTEMPTS) } : {}),
|
|
58
|
+
concurrency: Number(process.env.CONCURRENCY ?? 4),
|
|
59
|
+
...(process.env.TIMEOUT_MS ? { timeoutMs: Number(process.env.TIMEOUT_MS) } : {}),
|
|
60
|
+
...(process.env.VERIFY_JUDGE === '0' ? { verifyJudge: false } : {}),
|
|
61
|
+
onResult: (r) =>
|
|
62
|
+
console.log(` [${r.ok ? (r.resolved ? 'PASS' : 'fail') : 'ERR '}] ${r.benchmark} ${r.cell} ${r.taskId} score=${r.score.toFixed(2)} ${(r.wallMs / 1000).toFixed(1)}s${r.detail ? ` (${r.detail})` : ''}`),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
console.log(`\n=== benchmark matrix: ${report.benchmarks.join(', ')} × ${report.cells.length} cell(s) ===`)
|
|
66
|
+
console.log(printBenchmarksReport(report))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main().catch((e) => {
|
|
70
|
+
console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
|
|
71
|
+
process.exit(1)
|
|
72
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-benchmarks-report — render a `runBenchmarks` result as a rich, multi-axis leaderboard (markdown +
|
|
3
|
+
* SVG + HTML), with the BENCHMARKS as the axes. `runBenchmarks` (the registry matrix runner) already
|
|
4
|
+
* returns a fixed-width CLI table; this bridges its per-task results into the domain-agnostic `leaderboard`
|
|
5
|
+
* engine so the same run also yields a publishable board — every harness×model×persona cell scored on every
|
|
6
|
+
* benchmark, the vals.ai-style surface.
|
|
7
|
+
*
|
|
8
|
+
* No new aggregation logic: it maps each `BenchCellTaskResult` into the universal `RunRecord` shape the
|
|
9
|
+
* engine reads (cell → profile, benchmark → axis, score → score), then calls the shared renderers.
|
|
10
|
+
* Errored shots (`ok:false`) are dropped, exactly as the runner excludes them from its own denominator.
|
|
11
|
+
*/
|
|
12
|
+
import type { RunRecord } from '@tangle-network/agent-eval'
|
|
13
|
+
import {
|
|
14
|
+
type Leaderboard,
|
|
15
|
+
leaderboard,
|
|
16
|
+
renderLeaderboardHtml,
|
|
17
|
+
renderLeaderboardMarkdown,
|
|
18
|
+
renderLeaderboardSvg,
|
|
19
|
+
} from '@tangle-network/agent-runtime/loops'
|
|
20
|
+
import type { BenchCellTaskResult, RunBenchmarksReport } from './run-benchmarks'
|
|
21
|
+
|
|
22
|
+
/** Project the runner's per-task results into `RunRecord`s the leaderboard engine reads. Cost/tokens are
|
|
23
|
+
* left at 0 — `runBenchmarks` does not meter them per task, so the board shows them as not-captured rather
|
|
24
|
+
* than inventing a number. */
|
|
25
|
+
function toRecords(perTask: readonly BenchCellTaskResult[]): RunRecord[] {
|
|
26
|
+
return perTask
|
|
27
|
+
.filter((t) => t.ok)
|
|
28
|
+
.map(
|
|
29
|
+
(t) =>
|
|
30
|
+
({
|
|
31
|
+
runId: `${t.benchmark}:${t.cell}:${t.taskId}:${t.rep}`,
|
|
32
|
+
experimentId: t.benchmark,
|
|
33
|
+
candidateId: t.cell,
|
|
34
|
+
seed: t.rep,
|
|
35
|
+
// The cell label is `harness/model` (or a persona id); carry it as the model + harness so the
|
|
36
|
+
// leaderboard's default profile key resolves to the cell.
|
|
37
|
+
model: t.cell,
|
|
38
|
+
promptHash: '',
|
|
39
|
+
configHash: '',
|
|
40
|
+
commitSha: '',
|
|
41
|
+
wallMs: t.wallMs,
|
|
42
|
+
costUsd: 0,
|
|
43
|
+
tokenUsage: { input: 0, output: 0 },
|
|
44
|
+
outcome: { holdoutScore: t.score, raw: { resolved: t.resolved ? 1 : 0 } },
|
|
45
|
+
splitTag: 'holdout',
|
|
46
|
+
scenarioId: t.benchmark,
|
|
47
|
+
agentProfile: { harness: t.cell.split('/')[0] ?? t.cell, model: t.cell },
|
|
48
|
+
}) as unknown as RunRecord,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build the rich leaderboard from a `runBenchmarks` result — benchmarks are the axes, cells are the rows. */
|
|
53
|
+
export function benchmarksLeaderboard(
|
|
54
|
+
report: RunBenchmarksReport,
|
|
55
|
+
opts: { title?: string; meta?: Record<string, string> } = {},
|
|
56
|
+
): Leaderboard {
|
|
57
|
+
return leaderboard(toRecords(report.perTask), {
|
|
58
|
+
title: opts.title ?? 'Benchmark matrix — cell × benchmark',
|
|
59
|
+
// The cell (harness/model/persona) is the row label; one column per benchmark.
|
|
60
|
+
profileKeyOf: (r) => r.candidateId,
|
|
61
|
+
groupOf: (r) => r.scenarioId ?? r.experimentId,
|
|
62
|
+
...(opts.meta ? { meta: opts.meta } : {}),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { renderLeaderboardHtml, renderLeaderboardMarkdown, renderLeaderboardSvg }
|