@tangle-network/agent-bench 0.8.23 → 0.9.0

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.23",
3
+ "version": "0.9.0",
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,18 +25,18 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@tangle-network/agent-eval": ">=0.163.2 <0.164.0",
29
- "@tangle-network/agent-interface": "^1.6.0",
30
- "@tangle-network/agent-knowledge": "^10.7.0",
31
- "@tangle-network/sandbox": ">=0.31.0 <0.32.0",
32
- "@tangle-network/agent-runtime": "^0.157.0"
28
+ "@tangle-network/agent-eval": ">=0.175.0 <0.176.0",
29
+ "@tangle-network/agent-interface": "^2.3.0",
30
+ "@tangle-network/agent-knowledge": "^14.0.3",
31
+ "@tangle-network/sandbox": ">=0.36.4 <0.38.0",
32
+ "@tangle-network/agent-runtime": "^0.198.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@arethetypeswrong/cli": "0.18.5",
36
- "@types/node": "26.1.1",
37
- "publint": "0.3.22",
36
+ "@types/node": "26.4.0",
37
+ "publint": "0.3.24",
38
38
  "tsdown": "0.22.14",
39
- "tsx": "4.23.1",
39
+ "tsx": "4.23.12",
40
40
  "typescript": "6.0.3"
41
41
  },
42
42
  "engines": {
@@ -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'
@@ -176,6 +176,7 @@ async function main(): Promise<void> {
176
176
  blobs,
177
177
  makeWorkerAgent: (raw) => makeWorker(raw, ws, n++),
178
178
  perWorker: { maxIterations: 2, maxTokens: 200_000 },
179
+ toolNames: ['spawn_worker', 'await_event', 'stop'],
179
180
  })
180
181
  // The supervisor's cwd carries the REAL skill file (opencode loads it from the cwd skill dirs).
181
182
  const supCwd = mkdtempSync(join(tmpdir(), 'e2e-sup-'))
@@ -13,8 +13,8 @@ describe('HumanEval Python isolation', () => {
13
13
  writeFileSync(
14
14
  fakeDocker,
15
15
  `#!/usr/bin/env node
16
- import fs from 'node:fs'
17
- import path from 'node:path'
16
+ const fs = require('node:fs')
17
+ const path = require('node:path')
18
18
  const args = process.argv.slice(2)
19
19
  if (args[0] === 'rm') process.exit(0)
20
20
  if (process.env.FAKE_DOCKER_MISSING === '1') {
@@ -118,6 +118,7 @@ async function main(): Promise<void> {
118
118
  blobs,
119
119
  makeWorkerAgent: () => trivialWorker('w'),
120
120
  perWorker: { maxIterations: 4, maxTokens: 2000 },
121
+ toolNames: ['spawn_worker', 'await_event'],
121
122
  host: HOST_BIND,
122
123
  })
123
124
  // Docker containers reach the host through the bridge gateway, not the 0.0.0.0 bind URL.
@@ -139,10 +140,10 @@ async function main(): Promise<void> {
139
140
  console.error('')
140
141
 
141
142
  const toolNames = (parsed.result?.tools ?? []).map((t) => t.name)
142
- const hasSpawn = toolNames.includes('spawn_agent')
143
+ const hasSpawn = toolNames.includes('spawn_worker')
143
144
  const hasAwait = toolNames.includes('await_event')
144
145
  console.error(`[probe] tools advertised: ${toolNames.join(', ')}`)
145
- console.error(`[probe] spawn_agent present = ${hasSpawn}; await_event present = ${hasAwait}`)
146
+ console.error(`[probe] spawn_worker present = ${hasSpawn}; await_event present = ${hasAwait}`)
146
147
  ok = hasSpawn && hasAwait
147
148
 
148
149
  if (ok) {
@@ -169,7 +170,7 @@ async function main(): Promise<void> {
169
170
 
170
171
  console.error(
171
172
  ok
172
- ? 'CONTAINER-REACHABLE: docker tools/list returned spawn_agent and await_event.'
173
+ ? 'CONTAINER-REACHABLE: docker tools/list returned spawn_worker and await_event.'
173
174
  : 'NOT reachable from container; see output above.',
174
175
  )
175
176
  process.exit(ok ? 0 : 1)
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,
@@ -90,6 +90,7 @@ async function main(): Promise<void> {
90
90
  blobs,
91
91
  makeWorkerAgent: () => deliveringLeaf('w', { ok: true }),
92
92
  perWorker: { maxIterations: 4, maxTokens: 2000 },
93
+ toolNames: ['spawn_worker', 'await_event', 'stop'],
93
94
  })
94
95
  console.error(`[probe] coordination MCP live at ${mcp.url}`)
95
96
  try {
@@ -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'
@@ -219,8 +220,12 @@ async function assertRealDirectory(path: string, label: string): Promise<void> {
219
220
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
220
221
  throw new Error(`${label} must be a real directory`)
221
222
  }
222
- if ((await realpath(path)) !== path) {
223
- throw new Error(`${label} has a symlinked path component`)
223
+ // A symlinked PREFIX is the OS's own doing on macOS, where a temp root lives under /var, a
224
+ // symlink to /private/var. The directory itself must still be real, which the lstat above
225
+ // and this one both require.
226
+ const resolvedStats = await lstat(await realpath(path))
227
+ if (!resolvedStats.isDirectory() || resolvedStats.isSymbolicLink()) {
228
+ throw new Error(`${label} must resolve to a real directory`)
224
229
  }
225
230
  }
226
231
 
@@ -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 {
@@ -493,9 +494,16 @@ function trialKey(identity: PierCandidateTrialIdentity): string {
493
494
 
494
495
  function assertRealDirectory(path: string, label: string): void {
495
496
  const stats = lstatSync(path)
496
- if (!stats.isDirectory() || stats.isSymbolicLink() || realpathSync(path) !== path) {
497
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
497
498
  throw new Error(`${label} must be a real canonical directory`)
498
499
  }
500
+ // A symlinked PREFIX is the OS's own doing on macOS, where a temp root lives under /var, a
501
+ // symlink to /private/var. The directory itself must still be real, which both this check
502
+ // and the lstat above require.
503
+ const resolvedStats = lstatSync(realpathSync(path))
504
+ if (!resolvedStats.isDirectory() || resolvedStats.isSymbolicLink()) {
505
+ throw new Error(`${label} must resolve to a real directory`)
506
+ }
499
507
  }
500
508
 
501
509
  export class FilePierCandidateTrialController implements PierCandidateTrialController {
@@ -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'],