@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-bench",
3
- "version": "0.8.32",
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.174.0 <0.175.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.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.195.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
@@ -65,6 +65,7 @@ export {
65
65
  printBenchmarksReport,
66
66
  type BenchCell,
67
67
  type BenchShot,
68
+ type BenchShotResult,
68
69
  type BenchCellTaskResult,
69
70
  type BenchLeaderboardRow,
70
71
  type RunBenchmarksOptions,
@@ -7,8 +7,7 @@ import {
7
7
  canonicalAgentProfileDigest,
8
8
  canonicalCandidateDigest,
9
9
  } from '@tangle-network/agent-interface'
10
- import { profileOptimizerModelCall } from '../../src/runtime/profile-chat-client'
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 = profileOptimizerModelCall({
110
- profile,
111
- context: 'official optimizer model',
112
- executor,
113
- pricing: budget.pricing,
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
 
@@ -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
- import { executePreparedAgentCandidate } from '@tangle-network/agent-runtime'
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: 'x',
252
+ routerKey: 'sandbox-control-token',
253
+ modelApiKey: 'model-grant-token',
179
254
  resolveAdapter: () => boxAdapter,
180
- resolveClient: () => fakeClient as never,
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'],
@@ -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
- /** Runs one (adapter, task, cell) shot and returns the deliverable text. The default uses
64
- * `openSandboxRun`; tests inject a deterministic stub so the matrix runs offline. */
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<{ artifact: string; ok: boolean; detail?: string }>
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
- const run = await openSandboxRun(client, runOptions, deliverable)
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.readError !== undefined
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
- return {
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
- await run.close()
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<{ artifact: string; ok: boolean; detail?: string }> {
425
+ ): Promise<BenchShotResult> {
384
426
  const scores = new Map<number, BenchScore>()
385
- const result = await runRefineLoop<string>({
386
- rounds: attempts,
387
- prompt: (round, history) => (round === 1 ? input.task.prompt : retryPrompt(input.task, history, scores)),
388
- runShot: async (prompt, round) => {
389
- const out = await shot({ ...input, prompt, attempt: round })
390
- return { artifact: out.artifact, note: out.detail }
391
- },
392
- judge: async (artifact, round) => {
393
- const score = await input.adapter.judge(input.task, artifact)
394
- scores.set(round, score)
395
- return { valid: score.resolved, score: score.score }
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
- const out = loopAttempts > 1 ? await loopedShot(shotInput, shot, loopAttempts) : await shot(shotInput)
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
@@ -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')