@tangle-network/agent-bench 0.8.32 → 0.9.1
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 +15 -0
- package/HARNESS.md +11 -2
- package/dist/index.d.ts +27 -10
- package/dist/index.js +135 -38
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/scripts/verify-pier-agent.mts +1 -1
- package/scripts/verify-pier-recovery.mts +2 -1
- package/src/index.ts +1 -0
- package/src/official-optimizer-config.mts +17 -8
- package/src/pier-agent.test.mts +1 -1
- package/src/pier-agent.ts +3 -2
- package/src/pier-trial-controller.test.mts +7 -5
- package/src/pier-trial-controller.ts +2 -1
- package/src/run-benchmarks.test.mts +86 -4
- package/src/run-benchmarks.ts +128 -28
- package/src/sandbox-run.ts +2 -1
- package/src/swe-arena/activation.test.mts +1 -0
- package/src/swe-arena/briefing.test.mts +1 -0
- package/src/swe-arena/execution.test.mts +3 -2
- package/src/swe-arena/factory.test.mts +5 -3
- package/src/swe-arena/gepa-seat.mts +4 -2
- package/src/swe-arena/gepa-seat.test.mts +70 -3
- package/src/swe-arena/premeasured-from-cells.test.mts +1 -0
- package/src/swe-arena/proc.test.mts +3 -5
- package/src/swe-arena/proposer-fanout.test.mts +1 -0
- package/src/swe-arena/run-experiment.mts +2 -0
- package/src/swe-arena/scratch-worktree.test.mts +2 -1
- package/src/swe-bench-env.test.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-bench",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Benchmark adapters and execution for agent-runtime across coding, tool-use, RAG, memory, browser, and terminal tasks.",
|
|
6
6
|
"repository": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@tangle-network/agent-eval": ">=0.
|
|
28
|
+
"@tangle-network/agent-eval": ">=0.175.0 <0.176.0",
|
|
29
29
|
"@tangle-network/agent-interface": "^2.3.0",
|
|
30
|
-
"@tangle-network/agent-knowledge": "^14.0.
|
|
30
|
+
"@tangle-network/agent-knowledge": "^14.0.3",
|
|
31
31
|
"@tangle-network/sandbox": ">=0.36.4 <0.38.0",
|
|
32
|
-
"@tangle-network/agent-runtime": "^0.
|
|
32
|
+
"@tangle-network/agent-runtime": "^0.199.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@arethetypeswrong/cli": "0.18.5",
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type ResolvedAgentCandidateContainer,
|
|
38
38
|
sealAgentCandidateBundle,
|
|
39
39
|
verifyAgentCandidateBundle,
|
|
40
|
-
} from '@tangle-network/agent-runtime'
|
|
40
|
+
} from '@tangle-network/agent-runtime/candidate-execution'
|
|
41
41
|
|
|
42
42
|
import { executePreparedPierCandidate } from '../src/pier-agent'
|
|
43
43
|
import { createPierResultGrader } from '../src/pier-result-grader'
|
|
@@ -4,7 +4,8 @@ import { tmpdir } from 'node:os'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
|
-
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime'
|
|
7
|
+
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime/candidate-execution'
|
|
8
|
+
|
|
8
9
|
import { InMemoryTraceStore } from '@tangle-network/agent-eval'
|
|
9
10
|
|
|
10
11
|
import { createStagedPierCandidateExecutionFixture } from '../src/pier-agent.test-fixtures.mts'
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,7 @@ import {
|
|
|
7
7
|
canonicalAgentProfileDigest,
|
|
8
8
|
canonicalCandidateDigest,
|
|
9
9
|
} from '@tangle-network/agent-interface'
|
|
10
|
-
import { profileOptimizerModelCall } from '
|
|
11
|
-
import type { RouterSeam } from '../../src/runtime/supervise/runtime'
|
|
10
|
+
import { profileOptimizerModelCall, type RouterSeam } from '@tangle-network/agent-runtime/kernel'
|
|
12
11
|
|
|
13
12
|
function requiredNonNegativeNumber(
|
|
14
13
|
env: NodeJS.ProcessEnv,
|
|
@@ -73,6 +72,7 @@ export function officialOptimizerModel(options: {
|
|
|
73
72
|
temperature?: number
|
|
74
73
|
reasoningEffort?: NonNullable<AgentProfile['model']>['reasoningEffort']
|
|
75
74
|
callRef?: string
|
|
75
|
+
anthropicEndpoint?: boolean
|
|
76
76
|
complete?: RouterSeam['complete']
|
|
77
77
|
}): OpenAICompatibleOptimizerModel {
|
|
78
78
|
const { env } = options
|
|
@@ -106,12 +106,20 @@ export function officialOptimizerModel(options: {
|
|
|
106
106
|
routerKey: options.apiKey,
|
|
107
107
|
...(options.complete ? { complete: options.complete } : {}),
|
|
108
108
|
}
|
|
109
|
-
const call =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
const call: OpenAICompatibleOptimizerModel['call'] = (request) =>
|
|
110
|
+
profileOptimizerModelCall({
|
|
111
|
+
// Eval admits the request against the budget; execute its exact output limit.
|
|
112
|
+
profile: {
|
|
113
|
+
...profile,
|
|
114
|
+
model: {
|
|
115
|
+
...profile.model,
|
|
116
|
+
maxVisibleOutputTokens: request.request.maxTokens ?? options.maxOutputTokensPerRequest,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
context: 'official optimizer model',
|
|
120
|
+
executor,
|
|
121
|
+
pricing: budget.pricing,
|
|
122
|
+
})(request)
|
|
115
123
|
return {
|
|
116
124
|
model: options.model,
|
|
117
125
|
callRef:
|
|
@@ -122,6 +130,7 @@ export function officialOptimizerModel(options: {
|
|
|
122
130
|
})}`,
|
|
123
131
|
call,
|
|
124
132
|
budget,
|
|
133
|
+
...(options.anthropicEndpoint === true ? { anthropicEndpoint: true } : {}),
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
|
package/src/pier-agent.test.mts
CHANGED
|
@@ -9,7 +9,7 @@ import { canonicalJson } from '@tangle-network/agent-eval'
|
|
|
9
9
|
import type {
|
|
10
10
|
AgentCandidateExecutorRequest,
|
|
11
11
|
PreparedAgentCandidateExecution,
|
|
12
|
-
} from '@tangle-network/agent-runtime'
|
|
12
|
+
} from '@tangle-network/agent-runtime/candidate-execution'
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
awaitAbortableTrial,
|
package/src/pier-agent.ts
CHANGED
|
@@ -14,8 +14,9 @@ import type {
|
|
|
14
14
|
AgentCandidateProtectedRunCapture,
|
|
15
15
|
AgentCandidateRunFinalization,
|
|
16
16
|
PreparedAgentCandidateExecution,
|
|
17
|
-
} from '@tangle-network/agent-runtime'
|
|
18
|
-
|
|
17
|
+
} from '@tangle-network/agent-runtime/candidate-execution'
|
|
18
|
+
|
|
19
|
+
import { executePreparedAgentCandidate } from '@tangle-network/agent-runtime/candidate-execution'
|
|
19
20
|
import { canonicalJson, type TraceStore } from '@tangle-network/agent-eval'
|
|
20
21
|
|
|
21
22
|
import { capturePierTaskOutcome } from './pier-task-outcome'
|
|
@@ -6,12 +6,14 @@ import path from 'node:path'
|
|
|
6
6
|
import test from 'node:test'
|
|
7
7
|
import { promisify } from 'node:util'
|
|
8
8
|
|
|
9
|
-
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime'
|
|
9
|
+
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime/candidate-execution'
|
|
10
|
+
|
|
10
11
|
import { InMemoryTraceStore } from '@tangle-network/agent-eval'
|
|
11
12
|
|
|
12
13
|
import { createStagedPierCandidateExecutionFixture } from './pier-agent.test-fixtures.mts'
|
|
13
14
|
import { FilePierCandidateTrialController } from './pier-trial-controller'
|
|
14
15
|
|
|
16
|
+
// Process identity and restart recovery require Linux /proc start-time receipts.
|
|
15
17
|
const execFileAsync = promisify(execFile)
|
|
16
18
|
|
|
17
19
|
function testRequest(executionId: string, executionPlanDigest: `sha256:${string}`) {
|
|
@@ -71,7 +73,7 @@ test('an existing Pier job is rejected without deleting or starting it', async (
|
|
|
71
73
|
}
|
|
72
74
|
})
|
|
73
75
|
|
|
74
|
-
test('the Pier result wait does not add time beyond an expired deadline', async () => {
|
|
76
|
+
test('the Pier result wait does not add time beyond an expired deadline', { skip: process.platform !== 'linux' }, async () => {
|
|
75
77
|
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-deadline-'))
|
|
76
78
|
const controlRoot = path.join(root, 'control')
|
|
77
79
|
const jobsDirectory = path.join(root, 'jobs')
|
|
@@ -144,7 +146,7 @@ setInterval(() => undefined, 1_000)
|
|
|
144
146
|
}
|
|
145
147
|
})
|
|
146
148
|
|
|
147
|
-
test('the supervisor receives only launch data and no inherited evaluator environment', async () => {
|
|
149
|
+
test('the supervisor receives only launch data and no inherited evaluator environment', { skip: process.platform !== 'linux' }, async () => {
|
|
148
150
|
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-supervisor-env-'))
|
|
149
151
|
const controlRoot = path.join(root, 'control')
|
|
150
152
|
const jobsDirectory = path.join(root, 'jobs')
|
|
@@ -217,7 +219,7 @@ renameSync(temporary, target)
|
|
|
217
219
|
}
|
|
218
220
|
})
|
|
219
221
|
|
|
220
|
-
test('terminal acknowledgements reject unknown fields', async () => {
|
|
222
|
+
test('terminal acknowledgements reject unknown fields', { skip: process.platform !== 'linux' }, async () => {
|
|
221
223
|
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-terminal-schema-'))
|
|
222
224
|
const controlRoot = path.join(root, 'control')
|
|
223
225
|
const jobsDirectory = path.join(root, 'jobs')
|
|
@@ -274,7 +276,7 @@ renameSync(temporary, target)
|
|
|
274
276
|
}
|
|
275
277
|
})
|
|
276
278
|
|
|
277
|
-
test('a fresh evaluator process terminates the persisted process and container identity', async () => {
|
|
279
|
+
test('a fresh evaluator process terminates the persisted process and container identity', { skip: process.platform !== 'linux' }, async () => {
|
|
278
280
|
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-recovery-'))
|
|
279
281
|
const controlRoot = path.join(root, 'control')
|
|
280
282
|
const jobsDirectory = path.join(root, 'jobs')
|
|
@@ -21,7 +21,8 @@ import { isAbsolute, join, resolve } from 'node:path'
|
|
|
21
21
|
import { fileURLToPath } from 'node:url'
|
|
22
22
|
import type { Writable } from 'node:stream'
|
|
23
23
|
|
|
24
|
-
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime'
|
|
24
|
+
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime/candidate-execution'
|
|
25
|
+
|
|
25
26
|
import type { TraceStore } from '@tangle-network/agent-eval'
|
|
26
27
|
|
|
27
28
|
import type {
|
|
@@ -60,6 +60,41 @@ async function main(): Promise<void> {
|
|
|
60
60
|
assert.equal(report.perTask.length, 24, 'matrix expands to benchmarks × cells × tasks')
|
|
61
61
|
assert.equal(report.rows.length, 6, 'one row per (benchmark × cell)')
|
|
62
62
|
|
|
63
|
+
const measured = await runBenchmarks({
|
|
64
|
+
benchmarks: ['alpha'], cells: [{ label: 'measured', model: 'm' }],
|
|
65
|
+
routerBaseUrl: 'x', routerKey: 'x', resolveAdapter: resolveStub, n: 1,
|
|
66
|
+
runShot: async () => ({ artifact: 'WRONG', ok: true, usage: { input: 23, output: 7, costUsd: 0.04 } }),
|
|
67
|
+
})
|
|
68
|
+
assert.equal(measured.perTask[0]?.artifact, 'WRONG', 'the exact judged artifact reaches the caller even when it fails')
|
|
69
|
+
assert.deepEqual(measured.perTask[0]?.usage, { input: 23, output: 7, costUsd: 0.04 }, 'measured usage reaches the caller')
|
|
70
|
+
|
|
71
|
+
const judgeFailure = await runBenchmarks({
|
|
72
|
+
benchmarks: ['alpha'], cells: [{ label: 'measured', model: 'm' }],
|
|
73
|
+
routerBaseUrl: 'x', routerKey: 'x', n: 1, verifyJudge: false,
|
|
74
|
+
resolveAdapter: () => ({ ...REGISTRY.alpha!, judge: async () => { throw new Error('judge unavailable') } }),
|
|
75
|
+
runShot: async () => ({ artifact: 'PATCH', ok: true, usage: { input: 23, output: 7, costUsd: 0.04 } }),
|
|
76
|
+
})
|
|
77
|
+
assert.equal(judgeFailure.perTask[0]?.ok, false)
|
|
78
|
+
assert.equal(judgeFailure.perTask[0]?.artifact, 'PATCH', 'a judge outage retains the completed agent artifact')
|
|
79
|
+
assert.equal(judgeFailure.perTask[0]?.usage?.costUsd, 0.04, 'a judge outage retains already incurred usage')
|
|
80
|
+
|
|
81
|
+
const controller = new AbortController()
|
|
82
|
+
let started = 0
|
|
83
|
+
const cancelled = await runBenchmarks({
|
|
84
|
+
benchmarks: ['alpha'], cells: [{ label: 'cancelled', model: 'm' }],
|
|
85
|
+
routerBaseUrl: 'x', routerKey: 'x', resolveAdapter: resolveStub, concurrency: 1,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
runShot: async ({ signal }) => {
|
|
88
|
+
assert.equal(signal, controller.signal)
|
|
89
|
+
started += 1
|
|
90
|
+
controller.abort()
|
|
91
|
+
return { artifact: 'PATCH', ok: true, usage: { input: 1, output: 2, costUsd: 0.01 } }
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
assert.equal(started, 1, 'cancellation prevents every queued model call')
|
|
95
|
+
assert.equal(cancelled.perTask.length, 4, 'cancelled work remains visible')
|
|
96
|
+
assert.equal(cancelled.perTask[0]?.usage?.costUsd, 0.01)
|
|
97
|
+
|
|
63
98
|
const row = (b: string, c: string) => report.rows.find((r) => r.benchmark === b && r.cell === c)!
|
|
64
99
|
assert.equal(row('alpha', 'perfect').resolveRate, 1, 'perfect cell resolves every task')
|
|
65
100
|
assert.equal(row('alpha', 'half').resolveRate, 0.5, 'half cell resolves the even tasks')
|
|
@@ -94,7 +129,7 @@ async function main(): Promise<void> {
|
|
|
94
129
|
let prompts: string[] = []
|
|
95
130
|
const retryShot: BenchShot = async ({ task, prompt }) => {
|
|
96
131
|
prompts.push(prompt ?? task.prompt)
|
|
97
|
-
return { artifact: prompts.length === 1 ? 'WRONG' : String(task.metadata?.gold), ok: true }
|
|
132
|
+
return { artifact: prompts.length === 1 ? 'WRONG' : String(task.metadata?.gold), ok: true, usage: { input: 11, output: 3, costUsd: 0.02 } }
|
|
98
133
|
}
|
|
99
134
|
const oneShot = await runBenchmarks({
|
|
100
135
|
benchmarks: ['alpha'], cells: [{ label: 'retrying', model: 'm' }],
|
|
@@ -110,6 +145,40 @@ async function main(): Promise<void> {
|
|
|
110
145
|
assert.equal(prompts.length, 2, 'loop stops after the passing second attempt')
|
|
111
146
|
assert.match(prompts[1]!, /Previous attempts and safe checker feedback/)
|
|
112
147
|
assert.match(looped.perTask[0]!.detail ?? '', /"mode":"refine-loop"/)
|
|
148
|
+
assert.deepEqual(looped.perTask[0]?.usage, { input: 22, output: 6, costUsd: 0.04 }, 'retry cost includes the rejected attempt')
|
|
149
|
+
|
|
150
|
+
const failedRetry = await runBenchmarks({
|
|
151
|
+
benchmarks: ['alpha'], cells: [{ label: 'retrying', model: 'm' }],
|
|
152
|
+
routerBaseUrl: 'x', routerKey: 'x', resolveAdapter: resolveStub, n: 1, loopAttempts: 2,
|
|
153
|
+
runShot: async ({ attempt }) => {
|
|
154
|
+
if (attempt === 2) throw new Error('connection lost after dispatch')
|
|
155
|
+
return { artifact: 'WRONG', ok: true, usage: { input: 11, output: 3, costUsd: 0.02 } }
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
assert.deepEqual(failedRetry.perTask[0]?.usage, {
|
|
159
|
+
input: 11, output: 3, costUsd: 0.02, tokensKnown: false, usdKnown: false,
|
|
160
|
+
}, 'an unreported retry preserves the measured floor without claiming complete accounting')
|
|
161
|
+
|
|
162
|
+
let validAttempts = 0
|
|
163
|
+
const validRetry = await runBenchmarks({
|
|
164
|
+
benchmarks: ['alpha'], cells: [{ label: 'retrying', model: 'm' }],
|
|
165
|
+
routerBaseUrl: 'x', routerKey: 'x', resolveAdapter: resolveStub, n: 1, loopAttempts: 3,
|
|
166
|
+
runShot: async ({ task, attempt }) => {
|
|
167
|
+
validAttempts += 1
|
|
168
|
+
return { artifact: attempt === 1 ? 'WRONG' : String(task.metadata?.gold), ok: attempt !== 2 }
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
assert.equal(validAttempts, 3, 'a failed execution cannot stop refinement with apparent gold')
|
|
172
|
+
assert.equal(validRetry.perTask[0]?.ok, true)
|
|
173
|
+
const validIncumbent = await runBenchmarks({
|
|
174
|
+
benchmarks: ['alpha'], cells: [{ label: 'retrying', model: 'm' }],
|
|
175
|
+
routerBaseUrl: 'x', routerKey: 'x', resolveAdapter: resolveStub, n: 1, loopAttempts: 2,
|
|
176
|
+
runShot: async ({ task, attempt }) => ({
|
|
177
|
+
artifact: attempt === 1 ? 'WRONG' : String(task.metadata?.gold), ok: attempt === 1,
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
assert.equal(validIncumbent.perTask[0]?.artifact, 'WRONG', 'a failed execution cannot replace a valid incumbent')
|
|
181
|
+
assert.equal(validIncumbent.perTask[0]?.ok, true)
|
|
113
182
|
|
|
114
183
|
// A benchmark's detail may include hidden answer fields; those must never be fed back as hints.
|
|
115
184
|
const leakyGold = 'SECRET-GOLD'
|
|
@@ -141,8 +210,11 @@ async function main(): Promise<void> {
|
|
|
141
210
|
if (runtime.openSandboxRun.toString().includes('beforeStart')) {
|
|
142
211
|
// The default shot path supports benchmark-owned box setup/extract without real sandbox infra.
|
|
143
212
|
const order: string[] = []
|
|
213
|
+
let createdOptions: unknown
|
|
214
|
+
let controlCredential: string | undefined
|
|
144
215
|
const fakeClient = {
|
|
145
|
-
async create() {
|
|
216
|
+
async create(options: unknown) {
|
|
217
|
+
createdOptions = options
|
|
146
218
|
return {
|
|
147
219
|
id: 'box-default-shot',
|
|
148
220
|
async exec(command: string, options?: { sessionId?: string }) {
|
|
@@ -151,7 +223,9 @@ async function main(): Promise<void> {
|
|
|
151
223
|
},
|
|
152
224
|
async *streamPrompt(_prompt: string, options?: { sessionId?: string }) {
|
|
153
225
|
order.push(`stream:session=${options?.sessionId ? 'yes' : 'no'}`)
|
|
226
|
+
yield { type: 'llm_call', data: { tokensIn: 23, tokensOut: 7, costUsd: 0.04 } }
|
|
154
227
|
yield { type: 'result', data: { finalText: 'fallback text' } }
|
|
228
|
+
yield { type: 'done', data: { outcome: { type: 'completed' } } }
|
|
155
229
|
},
|
|
156
230
|
async delete() {
|
|
157
231
|
order.push('delete')
|
|
@@ -175,11 +249,19 @@ async function main(): Promise<void> {
|
|
|
175
249
|
benchmarks: ['boxy'],
|
|
176
250
|
cells: [{ label: 'default-shot', model: 'm', backend: 'sandbox' }],
|
|
177
251
|
routerBaseUrl: 'x',
|
|
178
|
-
routerKey: '
|
|
252
|
+
routerKey: 'sandbox-control-token',
|
|
253
|
+
modelApiKey: 'model-grant-token',
|
|
179
254
|
resolveAdapter: () => boxAdapter,
|
|
180
|
-
resolveClient: () =>
|
|
255
|
+
resolveClient: (options) => {
|
|
256
|
+
controlCredential = options.routerKey
|
|
257
|
+
return fakeClient as never
|
|
258
|
+
},
|
|
181
259
|
})
|
|
182
260
|
assert.equal(boxy.rows[0]!.resolveRate, 1, 'boxExtract artifact is judged instead of fallback text')
|
|
261
|
+
assert.equal(boxy.perTask[0]?.artifact, 'PATCH')
|
|
262
|
+
assert.deepEqual(boxy.perTask[0]?.usage, { input: 23, output: 7, costUsd: 0.04 })
|
|
263
|
+
assert.equal(controlCredential, 'sandbox-control-token', 'inference grant never authorizes sandbox control')
|
|
264
|
+
assert.equal((createdOptions as { backend: { model: { apiKey: string } } }).backend.model.apiKey, 'model-grant-token')
|
|
183
265
|
assert.deepEqual(
|
|
184
266
|
order.slice(0, 3),
|
|
185
267
|
['exec:setup-repo:streams=0:session=yes', 'stream:session=yes', 'exec:extract-patch:streams=1:session=yes'],
|
package/src/run-benchmarks.ts
CHANGED
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
Deliverable,
|
|
36
36
|
OpenSandboxRunOptions,
|
|
37
37
|
} from '@tangle-network/agent-runtime/kernel'
|
|
38
|
-
import { openSandboxRun } from '@tangle-network/agent-runtime/kernel'
|
|
38
|
+
import { openSandboxRun, SandboxRunAbortError, sumSandboxUsage } from '@tangle-network/agent-runtime/kernel'
|
|
39
39
|
import type { SandboxEvent } from '@tangle-network/sandbox'
|
|
40
40
|
import { resolveAdapter } from './adapters'
|
|
41
41
|
import type { BenchmarkAdapter, BenchScore, BenchTask } from './benchmarks/types'
|
|
@@ -60,8 +60,17 @@ export interface BenchCell {
|
|
|
60
60
|
readonly profile?: AgentProfile
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/** A worker's artifact and observed execution evidence, before external grading. */
|
|
64
|
+
export interface BenchShotResult {
|
|
65
|
+
readonly artifact: string
|
|
66
|
+
readonly ok: boolean
|
|
67
|
+
readonly detail?: string
|
|
68
|
+
/** Provider observations, including explicit unknown counters. Omitted when the shot reports none. */
|
|
69
|
+
readonly usage?: ReturnType<typeof sumSandboxUsage>
|
|
70
|
+
readonly events?: readonly SandboxEvent[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Runs one (adapter, task, cell) shot. Defaults to `openSandboxRun`. */
|
|
65
74
|
export type BenchShot = (input: {
|
|
66
75
|
readonly adapter: BenchmarkAdapter
|
|
67
76
|
readonly task: BenchTask
|
|
@@ -72,12 +81,15 @@ export type BenchShot = (input: {
|
|
|
72
81
|
readonly attempt?: number
|
|
73
82
|
readonly routerBaseUrl: string
|
|
74
83
|
readonly routerKey: string
|
|
84
|
+
/** Optional inference credential for the box; routerKey continues to authorize sandbox control. */
|
|
85
|
+
readonly modelApiKey?: string
|
|
75
86
|
readonly bridgeUrl?: string
|
|
76
87
|
readonly bridgeBearer?: string
|
|
77
88
|
readonly sandboxBaseUrl?: string
|
|
78
89
|
readonly timeoutMs?: number
|
|
90
|
+
readonly signal?: AbortSignal
|
|
79
91
|
readonly resolveClient?: typeof resolveBenchClient
|
|
80
|
-
}) => Promise<
|
|
92
|
+
}) => Promise<BenchShotResult>
|
|
81
93
|
|
|
82
94
|
export interface RunBenchmarksOptions {
|
|
83
95
|
/** Registry keys (`resolveAdapter`) — the benchmark subset to run. */
|
|
@@ -86,6 +98,8 @@ export interface RunBenchmarksOptions {
|
|
|
86
98
|
readonly cells: readonly BenchCell[]
|
|
87
99
|
readonly routerBaseUrl: string
|
|
88
100
|
readonly routerKey: string
|
|
101
|
+
/** Optional inference credential for the box; never used for sandbox creation or deletion. */
|
|
102
|
+
readonly modelApiKey?: string
|
|
89
103
|
readonly bridgeUrl?: string
|
|
90
104
|
readonly bridgeBearer?: string
|
|
91
105
|
readonly sandboxBaseUrl?: string
|
|
@@ -99,6 +113,8 @@ export interface RunBenchmarksOptions {
|
|
|
99
113
|
readonly concurrency?: number
|
|
100
114
|
/** Per-shot wall-clock (ms). */
|
|
101
115
|
readonly timeoutMs?: number
|
|
116
|
+
/** Cancels active shots and prevents queued shots from starting. */
|
|
117
|
+
readonly signal?: AbortSignal
|
|
102
118
|
/** Test seam: resolve the runtime transport. Defaults to `resolveBenchClient`. */
|
|
103
119
|
readonly resolveClient?: typeof resolveBenchClient
|
|
104
120
|
/** Max attempts per (benchmark × cell × task). Default 1. Attempts after the first receive
|
|
@@ -126,6 +142,11 @@ export interface BenchCellTaskResult {
|
|
|
126
142
|
readonly ok: boolean
|
|
127
143
|
readonly detail?: string
|
|
128
144
|
readonly wallMs: number
|
|
145
|
+
/** Exact bytes given to the benchmark judge, retained even when judging fails. */
|
|
146
|
+
readonly artifact?: string
|
|
147
|
+
readonly usage?: ReturnType<typeof sumSandboxUsage>
|
|
148
|
+
/** Worker events only; benchmark grading remains outside this trace. */
|
|
149
|
+
readonly events?: readonly SandboxEvent[]
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
export interface BenchLeaderboardRow {
|
|
@@ -173,7 +194,8 @@ function finalText(events: readonly SandboxEvent[]): string {
|
|
|
173
194
|
|
|
174
195
|
/** The default real-agent shot: one `openSandboxRun` over the cell's harness+model, deliverable
|
|
175
196
|
* extracted by the adapter's parser (or final text), abortable on `timeoutMs`. */
|
|
176
|
-
const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerBaseUrl, routerKey, bridgeUrl, bridgeBearer, sandboxBaseUrl, timeoutMs, resolveClient }) => {
|
|
197
|
+
const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerBaseUrl, routerKey, modelApiKey, bridgeUrl, bridgeBearer, sandboxBaseUrl, timeoutMs, signal, resolveClient }) => {
|
|
198
|
+
signal?.throwIfAborted()
|
|
177
199
|
const client = (resolveClient ?? resolveBenchClient)({
|
|
178
200
|
backend: cell.backend ?? 'router',
|
|
179
201
|
routerBaseUrl,
|
|
@@ -206,7 +228,7 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
206
228
|
sandboxOverrides: {
|
|
207
229
|
name: `bench-${adapter.name}-${task.id}-${uniq}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
|
|
208
230
|
environment: 'universal',
|
|
209
|
-
backend: { type: harness as never, model: { provider: profileProvider, model: cell.model, baseUrl: routerBaseUrl } },
|
|
231
|
+
backend: { type: harness as never, model: { provider: profileProvider, model: cell.model, baseUrl: routerBaseUrl, ...(modelApiKey === undefined ? {} : { apiKey: modelApiKey }) } },
|
|
210
232
|
},
|
|
211
233
|
}
|
|
212
234
|
const deliverable: Deliverable<string> = {
|
|
@@ -217,7 +239,7 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
217
239
|
const timer = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : undefined
|
|
218
240
|
const runOptions: OpenSandboxRunOptions = {
|
|
219
241
|
agentRun,
|
|
220
|
-
signal: controller.signal,
|
|
242
|
+
signal: signal ? AbortSignal.any([controller.signal, signal]) : controller.signal,
|
|
221
243
|
runId: `bench:${adapter.name}:${task.id}:${uniq}`,
|
|
222
244
|
scenarioId: task.id,
|
|
223
245
|
}
|
|
@@ -236,9 +258,12 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
236
258
|
)
|
|
237
259
|
}
|
|
238
260
|
}
|
|
239
|
-
|
|
261
|
+
let run: Awaited<ReturnType<typeof openSandboxRun<string>>> | undefined
|
|
262
|
+
let result: BenchShotResult = { artifact: '', ok: false }
|
|
240
263
|
try {
|
|
264
|
+
run = await openSandboxRun(client, runOptions, deliverable)
|
|
241
265
|
const turn = await run.start(prompt ?? task.prompt)
|
|
266
|
+
result = { artifact: '', ok: false, usage: sumSandboxUsage(turn.events), events: turn.events }
|
|
242
267
|
// Event-stream deliverable (adapter.output ?? finalText) — the FALLBACK.
|
|
243
268
|
let artifact = (turn.out ?? '').trim()
|
|
244
269
|
let boxExtractError: string | undefined
|
|
@@ -289,7 +314,9 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
289
314
|
}
|
|
290
315
|
}
|
|
291
316
|
const detail =
|
|
292
|
-
turn.
|
|
317
|
+
!turn.outcome.success
|
|
318
|
+
? turn.outcome.error ?? `agent ended with status ${turn.outcome.status}`
|
|
319
|
+
: turn.readError !== undefined
|
|
293
320
|
? `read: ${turn.readError.slice(0, 160)}`
|
|
294
321
|
: boxExtractError !== undefined
|
|
295
322
|
? `boxExtract: ${boxExtractError}`
|
|
@@ -306,15 +333,30 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
306
333
|
// debug-only; never fail the shot on a dump error
|
|
307
334
|
}
|
|
308
335
|
}
|
|
309
|
-
|
|
336
|
+
result = {
|
|
310
337
|
artifact,
|
|
311
|
-
ok: artifact.length > 0,
|
|
338
|
+
ok: turn.outcome.success && artifact.length > 0 && turn.readError === undefined && boxExtractError === undefined,
|
|
339
|
+
usage: result.usage,
|
|
340
|
+
events: turn.events,
|
|
312
341
|
...(detail ? { detail } : {}),
|
|
313
342
|
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
result = {
|
|
345
|
+
...result,
|
|
346
|
+
ok: false,
|
|
347
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
348
|
+
...(err instanceof SandboxRunAbortError ? { usage: sumSandboxUsage(err.events), events: err.events } : {}),
|
|
349
|
+
}
|
|
314
350
|
} finally {
|
|
315
351
|
if (timer) clearTimeout(timer)
|
|
316
|
-
|
|
352
|
+
try {
|
|
353
|
+
await run?.close()
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const cleanup = `cleanup: ${err instanceof Error ? err.message : String(err)}`
|
|
356
|
+
result = { ...result, detail: combineDetails(result.detail, cleanup) }
|
|
357
|
+
}
|
|
317
358
|
}
|
|
359
|
+
return result
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
function parseMaybeJson(value: string): unknown {
|
|
@@ -380,23 +422,44 @@ async function loopedShot(
|
|
|
380
422
|
input: Parameters<BenchShot>[0],
|
|
381
423
|
shot: BenchShot,
|
|
382
424
|
attempts: number,
|
|
383
|
-
): Promise<
|
|
425
|
+
): Promise<BenchShotResult> {
|
|
384
426
|
const scores = new Map<number, BenchScore>()
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
427
|
+
const shots = new Map<number, BenchShotResult>()
|
|
428
|
+
let pendingShot = false
|
|
429
|
+
let result: Awaited<ReturnType<typeof runRefineLoop<string>>>
|
|
430
|
+
try {
|
|
431
|
+
result = await runRefineLoop<string>({
|
|
432
|
+
rounds: attempts,
|
|
433
|
+
prompt: (round, history) => (round === 1 ? input.task.prompt : retryPrompt(input.task, history, scores)),
|
|
434
|
+
runShot: async (prompt, round) => {
|
|
435
|
+
input.signal?.throwIfAborted()
|
|
436
|
+
pendingShot = true
|
|
437
|
+
const out = await shot({ ...input, prompt, attempt: round })
|
|
438
|
+
shots.set(round, out)
|
|
439
|
+
pendingShot = false
|
|
440
|
+
return { artifact: out.artifact, note: out.detail }
|
|
441
|
+
},
|
|
442
|
+
judge: async (artifact, round) => {
|
|
443
|
+
const score = await input.adapter.judge(input.task, artifact)
|
|
444
|
+
scores.set(round, score)
|
|
445
|
+
const succeeded = shots.get(round)?.ok === true
|
|
446
|
+
return { valid: succeeded && score.resolved, score: succeeded ? score.score : 0 }
|
|
447
|
+
},
|
|
448
|
+
})
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const completed = [...shots.values()]
|
|
451
|
+
return {
|
|
452
|
+
artifact: completed.at(-1)?.artifact ?? '',
|
|
453
|
+
ok: false,
|
|
454
|
+
usage: combinedUsage(pendingShot ? [...completed, { artifact: '', ok: false }] : completed),
|
|
455
|
+
events: completed.flatMap((shot) => shot.events ?? []),
|
|
456
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
398
459
|
|
|
399
460
|
const best = result.rounds.reduce((winner, candidate) => {
|
|
461
|
+
if (shots.get(candidate.round)?.ok !== true) return winner
|
|
462
|
+
if (shots.get(winner.round)?.ok !== true) return candidate
|
|
400
463
|
const a = scores.get(winner.round)
|
|
401
464
|
const b = scores.get(candidate.round)
|
|
402
465
|
if (!a) return candidate
|
|
@@ -408,7 +471,9 @@ async function loopedShot(
|
|
|
408
471
|
const bestScore = scores.get(best.round)
|
|
409
472
|
return {
|
|
410
473
|
artifact: best.artifact,
|
|
411
|
-
ok: best.artifact.trim().length > 0,
|
|
474
|
+
ok: shots.get(best.round)?.ok === true && best.artifact.trim().length > 0,
|
|
475
|
+
usage: combinedUsage([...shots.values()]),
|
|
476
|
+
events: [...shots.values()].flatMap((shot) => shot.events ?? []),
|
|
412
477
|
detail: JSON.stringify({
|
|
413
478
|
mode: 'refine-loop',
|
|
414
479
|
attempts: result.rounds.length,
|
|
@@ -425,6 +490,30 @@ async function loopedShot(
|
|
|
425
490
|
}
|
|
426
491
|
}
|
|
427
492
|
|
|
493
|
+
function combinedUsage(shots: readonly BenchShotResult[]): ReturnType<typeof sumSandboxUsage> {
|
|
494
|
+
const usage = { input: 0, output: 0, costUsd: 0 }
|
|
495
|
+
let tokensKnown = shots.length > 0
|
|
496
|
+
let usdKnown = shots.length > 0
|
|
497
|
+
let estimate: number | undefined
|
|
498
|
+
let unknownReason: string | undefined
|
|
499
|
+
for (const shot of shots) {
|
|
500
|
+
usage.input += shot.usage?.input ?? 0
|
|
501
|
+
usage.output += shot.usage?.output ?? 0
|
|
502
|
+
usage.costUsd += shot.usage?.costUsd ?? 0
|
|
503
|
+
tokensKnown &&= shot.usage !== undefined && shot.usage.tokensKnown !== false
|
|
504
|
+
usdKnown &&= shot.usage !== undefined && shot.usage.usdKnown !== false
|
|
505
|
+
unknownReason ??= shot.usage?.tokensUnknownReason
|
|
506
|
+
if (shot.usage?.estimatedCostUsd !== undefined) estimate = (estimate ?? 0) + shot.usage.estimatedCostUsd
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...usage,
|
|
510
|
+
...(tokensKnown ? {} : { tokensKnown: false as const }),
|
|
511
|
+
...(usdKnown ? {} : { usdKnown: false as const }),
|
|
512
|
+
...(estimate === undefined ? {} : { estimatedCostUsd: estimate }),
|
|
513
|
+
...(unknownReason === undefined ? {} : { tokensUnknownReason: unknownReason }),
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
428
517
|
function combineDetails(runDetail: string | undefined, scoreDetail: string | undefined): string | undefined {
|
|
429
518
|
if (runDetail && scoreDetail) {
|
|
430
519
|
return JSON.stringify({ run: parseMaybeJson(runDetail), score: parseMaybeJson(scoreDetail) })
|
|
@@ -481,6 +570,7 @@ async function prepareBenchmarks(
|
|
|
481
570
|
}
|
|
482
571
|
|
|
483
572
|
export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenchmarksReport> {
|
|
573
|
+
opts.signal?.throwIfAborted()
|
|
484
574
|
if (opts.benchmarks.length === 0) throw new Error('runBenchmarks: no benchmarks selected')
|
|
485
575
|
if (opts.cells.length === 0) throw new Error('runBenchmarks: no cells to run')
|
|
486
576
|
const reps = Math.max(1, opts.reps ?? 1)
|
|
@@ -497,20 +587,24 @@ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenc
|
|
|
497
587
|
await runPool(jobs, Math.max(1, opts.concurrency ?? 4), async (job, index) => {
|
|
498
588
|
const startedAt = Date.now()
|
|
499
589
|
let result: BenchCellTaskResult
|
|
590
|
+
let out: BenchShotResult | undefined
|
|
500
591
|
try {
|
|
592
|
+
opts.signal?.throwIfAborted()
|
|
501
593
|
const shotInput = {
|
|
502
594
|
adapter: job.adapter,
|
|
503
595
|
task: job.task,
|
|
504
596
|
cell: job.cell,
|
|
505
597
|
routerBaseUrl: opts.routerBaseUrl,
|
|
506
598
|
routerKey: opts.routerKey,
|
|
599
|
+
...(opts.modelApiKey === undefined ? {} : { modelApiKey: opts.modelApiKey }),
|
|
507
600
|
...(opts.bridgeUrl ? { bridgeUrl: opts.bridgeUrl } : {}),
|
|
508
601
|
...(opts.bridgeBearer ? { bridgeBearer: opts.bridgeBearer } : {}),
|
|
509
602
|
...(opts.sandboxBaseUrl ? { sandboxBaseUrl: opts.sandboxBaseUrl } : {}),
|
|
510
603
|
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
604
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
511
605
|
...(opts.resolveClient ? { resolveClient: opts.resolveClient } : {}),
|
|
512
606
|
}
|
|
513
|
-
|
|
607
|
+
out = loopAttempts > 1 ? await loopedShot(shotInput, shot, loopAttempts) : await shot(shotInput)
|
|
514
608
|
const score: BenchScore = await job.adapter.judge(job.task, out.artifact)
|
|
515
609
|
result = {
|
|
516
610
|
benchmark: job.benchmark,
|
|
@@ -522,6 +616,9 @@ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenc
|
|
|
522
616
|
ok: out.ok,
|
|
523
617
|
...(out.detail ?? score.detail ? { detail: combineDetails(out.detail, score.detail) } : {}),
|
|
524
618
|
wallMs: Date.now() - startedAt,
|
|
619
|
+
artifact: out.artifact,
|
|
620
|
+
...(out.usage === undefined ? {} : { usage: out.usage }),
|
|
621
|
+
...(out.events === undefined ? {} : { events: out.events }),
|
|
525
622
|
}
|
|
526
623
|
} catch (err) {
|
|
527
624
|
// A thrown shot/judge is infra error for THIS cell-task: ok=false excludes it from the
|
|
@@ -536,6 +633,9 @@ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenc
|
|
|
536
633
|
ok: false,
|
|
537
634
|
detail: err instanceof Error ? err.message.slice(0, 200) : String(err),
|
|
538
635
|
wallMs: Date.now() - startedAt,
|
|
636
|
+
...(out === undefined ? {} : { artifact: out.artifact }),
|
|
637
|
+
...(out?.usage === undefined ? {} : { usage: out.usage }),
|
|
638
|
+
...(out?.events === undefined ? {} : { events: out.events }),
|
|
539
639
|
}
|
|
540
640
|
}
|
|
541
641
|
void index
|
package/src/sandbox-run.ts
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
type AgentRunSpec,
|
|
15
15
|
type OutputAdapter,
|
|
16
16
|
} from '@tangle-network/agent-runtime/kernel'
|
|
17
|
-
import { parseExactAgentProfile } from '@tangle-network/agent-runtime'
|
|
17
|
+
import { parseExactAgentProfile } from '@tangle-network/agent-runtime/candidate-execution'
|
|
18
|
+
|
|
18
19
|
// `BackendType` is the sandbox SDK's harness union and its canonical home. Runtime consumes it
|
|
19
20
|
// from there too; benchmark profiles use the same values as their exact harness identity.
|
|
20
21
|
import type { BackendType } from '@tangle-network/sandbox'
|
|
@@ -211,6 +211,7 @@ describe('activation-predicate prefilter', () => {
|
|
|
211
211
|
loopsRepo = await mkdtemp(join(tmpdir(), 'act-repo-'))
|
|
212
212
|
outDir = await mkdtemp(join(tmpdir(), 'act-out-'))
|
|
213
213
|
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
214
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'core.hooksPath', '/dev/null'])
|
|
214
215
|
await runOk('git', ['-C', loopsRepo, 'config', 'user.email', 't@t.dev'])
|
|
215
216
|
await runOk('git', ['-C', loopsRepo, 'config', 'user.name', 'T'])
|
|
216
217
|
await writeFile(join(loopsRepo, 'src.ts'), 'base\n')
|