@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +26 -1
  4. package/fixtures/aec-bench.json +18 -0
  5. package/fixtures/agentbench-dbbench.json +22 -0
  6. package/fixtures/bfcl.json +45 -0
  7. package/fixtures/commit0.json +72 -0
  8. package/fixtures/crag.json +10 -0
  9. package/fixtures/dabstep.json +22 -0
  10. package/fixtures/enterpriseops-gym.json +103 -0
  11. package/fixtures/finresearchbench.json +21 -0
  12. package/fixtures/finsearchcomp.json +66 -0
  13. package/fixtures/frames.json +26 -0
  14. package/fixtures/hotpotqa.json +182 -0
  15. package/fixtures/nomiracl.json +26 -0
  16. package/fixtures/open-rag-bench.json +16 -0
  17. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  18. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  19. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  20. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  21. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  22. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  23. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  24. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  25. package/fixtures/programbench.json +17 -0
  26. package/fixtures/ragbench.json +21 -0
  27. package/fixtures/simpleqa.json +121 -0
  28. package/fixtures/t2-ragbench.json +13 -0
  29. package/fixtures/tau2-bench.json +16 -0
  30. package/fixtures/tau3-banking.json +16 -0
  31. package/fixtures/toollm.json +28 -0
  32. package/fixtures/webarena-verified.json +20 -0
  33. package/package.json +39 -15
  34. package/pier_agents/__init__.py +18 -0
  35. package/pier_agents/candidate_contract.py +755 -0
  36. package/pier_agents/process_boundary.py +321 -0
  37. package/pier_agents/tangle_candidate.py +907 -0
  38. package/pier_agents/workspace_boundary.py +368 -0
  39. package/scripts/appworld_driver.py +359 -0
  40. package/scripts/cadbench_prepare.py +22 -0
  41. package/scripts/cadgenbench_hard_parts.py +48 -0
  42. package/scripts/clbench_codebase_judge.py +73 -0
  43. package/scripts/commit0_judge.py +170 -0
  44. package/scripts/dabstep_judge.py +42 -0
  45. package/scripts/enterpriseops_gym_judge.py +281 -0
  46. package/scripts/programbench_judge.py +120 -0
  47. package/scripts/render-gate-chart.mjs +176 -0
  48. package/scripts/run-package-tests.mjs +56 -0
  49. package/scripts/terminate-pier-trial.mts +66 -0
  50. package/scripts/trata-hedge/README.md +56 -0
  51. package/scripts/trata-hedge/run.sh +60 -0
  52. package/scripts/trata-hedge/solve.py +83 -0
  53. package/scripts/verify-packed-consumer.mjs +224 -0
  54. package/scripts/verify-pier-agent.mts +715 -0
  55. package/scripts/verify-pier-pair.mts +74 -0
  56. package/scripts/verify-pier-recovery.mts +139 -0
  57. package/src/adapters.ts +26 -0
  58. package/src/benchmarks/_harness.test.mts +178 -0
  59. package/src/benchmarks/_harness.ts +239 -16
  60. package/src/benchmarks/agentbench.ts +163 -0
  61. package/src/benchmarks/appworld.test.mts +15 -9
  62. package/src/benchmarks/bfcl.ts +346 -0
  63. package/src/benchmarks/crag.ts +137 -0
  64. package/src/benchmarks/dabstep.test.mts +70 -0
  65. package/src/benchmarks/dabstep.ts +212 -0
  66. package/src/benchmarks/external-adapters.test.mts +150 -0
  67. package/src/benchmarks/finresearchbench.ts +269 -0
  68. package/src/benchmarks/humaneval.ts +20 -8
  69. package/src/benchmarks/nomiracl.ts +180 -0
  70. package/src/benchmarks/open-rag-bench.ts +153 -0
  71. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  72. package/src/benchmarks/rag-shared.ts +327 -0
  73. package/src/benchmarks/ragbench.ts +171 -0
  74. package/src/benchmarks/swe-bench.test.mts +61 -0
  75. package/src/benchmarks/swe-bench.ts +201 -19
  76. package/src/benchmarks/t2-ragbench.ts +166 -0
  77. package/src/benchmarks/tau-bench-shared.ts +214 -0
  78. package/src/benchmarks/tau2-bench.ts +30 -0
  79. package/src/benchmarks/tau3-banking.ts +29 -0
  80. package/src/benchmarks/terminal-bench.test.mts +33 -0
  81. package/src/benchmarks/terminal-bench.ts +23 -8
  82. package/src/benchmarks/toollm.ts +254 -0
  83. package/src/benchmarks/types.ts +42 -0
  84. package/src/benchmarks/webarena-verified.ts +200 -0
  85. package/src/commit0-prereqs.sh +0 -0
  86. package/src/coordination-mcp-container-reach.mts +181 -0
  87. package/src/decoder-live.mts +1 -1
  88. package/src/examples/README.md +103 -39
  89. package/src/examples/benchmark-matrix.mts +101 -0
  90. package/src/examples/lean-proof-gate.README.md +77 -0
  91. package/src/examples/lean-proof-gate.mts +162 -0
  92. package/src/examples/lean-verify.ts +95 -0
  93. package/src/examples/lean.Dockerfile +12 -0
  94. package/src/examples/math-demo.mts +9 -7
  95. package/src/examples/strategy-demo.mts +10 -12
  96. package/src/gate.ts +3 -2
  97. package/src/hev-eval.mts +69 -0
  98. package/src/hev-improve.mts +169 -0
  99. package/src/hev-structural.mts +688 -0
  100. package/src/index.ts +73 -0
  101. package/src/mbpp-structural.mts +662 -0
  102. package/src/pier-agent.test-fixtures.mts +19 -0
  103. package/src/pier-agent.test.mts +363 -0
  104. package/src/pier-agent.ts +657 -0
  105. package/src/pier-result-grader.mjs +30 -0
  106. package/src/pier-result-grader.test.mts +62 -0
  107. package/src/pier-result-grader.ts +108 -0
  108. package/src/pier-task-outcome.test.mts +117 -0
  109. package/src/pier-task-outcome.ts +240 -0
  110. package/src/pier-trial-controller.test.mts +412 -0
  111. package/src/pier-trial-controller.ts +858 -0
  112. package/src/pier-trial-supervisor.mjs +352 -0
  113. package/src/resolve-client.ts +25 -2
  114. package/src/run-benchmarks-cli.mts +72 -0
  115. package/src/run-benchmarks-report.ts +66 -0
  116. package/src/run-benchmarks.test.mts +231 -0
  117. package/src/run-benchmarks.ts +589 -0
  118. package/src/smoke-structural-rollout.mts +393 -0
  119. package/src/swe-bench-env.test.ts +207 -0
  120. package/src/swe-bench-env.ts +554 -0
  121. package/src/swe-jail.ts +293 -0
  122. package/src/swe-self-improve.mts +84 -0
  123. package/src/swe-structural-judge-policy.test.ts +117 -0
  124. package/src/swe-structural-judge-policy.ts +133 -0
  125. package/src/swe-structural-policy.test.ts +124 -0
  126. package/src/swe-structural-policy.ts +132 -0
  127. package/src/swe-structural-provenance.test.ts +93 -0
  128. package/src/swe-structural-provenance.ts +138 -0
  129. package/src/swe-structural.mts +1260 -0
  130. package/src/swe-temp.ts +14 -0
  131. package/src/tb-container-executor.mts +234 -0
  132. package/src/tb-container-executor.test.mts +99 -0
  133. package/src/tb-supervisor-sidecar.mts +222 -0
  134. package/src/trata-gepa.mts +1 -1
  135. package/steerers/eops-itsm-population.json +1 -0
  136. package/tb_agents/opencode_refine_agent.py +117 -0
  137. package/tb_agents/opencode_router_agent.py +406 -0
  138. package/tb_agents/opencode_supervisor_agent.py +239 -0
  139. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,74 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ interface PierProofResult {
6
+ readonly arm: 'failure' | 'success'
7
+ readonly reward: number
8
+ readonly patchApplied: number
9
+ readonly modelCalls: number
10
+ readonly inputTokens: number
11
+ readonly outputTokens: number
12
+ readonly costUsd: number
13
+ readonly runReceiptDigest: string
14
+ }
15
+
16
+ const benchDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
17
+ const script = path.join(benchDir, 'scripts', 'verify-pier-agent.mts')
18
+ const recoveryScript = path.join(benchDir, 'scripts', 'verify-pier-recovery.mts')
19
+
20
+ function runArm(arm: PierProofResult['arm']): PierProofResult {
21
+ const stdout = execFileSync(process.execPath, ['--import', 'tsx', script], {
22
+ cwd: benchDir,
23
+ env: { ...process.env, PIER_PROOF_ARM: arm },
24
+ encoding: 'utf8',
25
+ stdio: ['ignore', 'pipe', 'inherit'],
26
+ timeout: 20 * 60_000,
27
+ maxBuffer: 20 * 1024 * 1024,
28
+ })
29
+ return JSON.parse(stdout) as PierProofResult
30
+ }
31
+
32
+ const failure = runArm('failure')
33
+ const recovery = JSON.parse(
34
+ execFileSync(process.execPath, ['--import', 'tsx', recoveryScript], {
35
+ cwd: benchDir,
36
+ env: { ...process.env },
37
+ encoding: 'utf8',
38
+ stdio: ['ignore', 'pipe', 'inherit'],
39
+ timeout: 120_000,
40
+ }),
41
+ ) as { freshProcess?: boolean; processExited?: boolean; containersRemoved?: boolean }
42
+ if (
43
+ recovery.freshProcess !== true ||
44
+ recovery.processExited !== true ||
45
+ recovery.containersRemoved !== true
46
+ ) {
47
+ throw new Error(`Pier fresh-process recovery failed: ${JSON.stringify(recovery)}`)
48
+ }
49
+ const success = runArm('success')
50
+ if (
51
+ failure.arm !== 'failure' ||
52
+ failure.reward !== 0 ||
53
+ failure.patchApplied !== 0 ||
54
+ success.arm !== 'success' ||
55
+ success.reward !== 1 ||
56
+ success.patchApplied !== 1
57
+ ) {
58
+ throw new Error(`Pier controls did not separate: ${JSON.stringify({ failure, success })}`)
59
+ }
60
+ for (const result of [failure, success]) {
61
+ if (
62
+ result.modelCalls !== 0 ||
63
+ result.inputTokens !== 0 ||
64
+ result.outputTokens !== 0 ||
65
+ result.costUsd !== 0
66
+ ) {
67
+ throw new Error(`Pier ${result.arm} control spent model budget: ${JSON.stringify(result)}`)
68
+ }
69
+ if (!/^sha256:[a-f0-9]{64}$/.test(result.runReceiptDigest)) {
70
+ throw new Error(`Pier ${result.arm} control omitted its run receipt digest`)
71
+ }
72
+ }
73
+
74
+ console.log(JSON.stringify({ recovery, controls: [failure, success] }, null, 2))
@@ -0,0 +1,139 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime'
8
+ import { InMemoryTraceStore } from '@tangle-network/agent-eval'
9
+
10
+ import { createStagedPierCandidateExecutionFixture } from '../src/pier-agent.test-fixtures.mts'
11
+ import { FilePierCandidateTrialController } from '../src/pier-trial-controller'
12
+
13
+ const benchDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
14
+ const root = mkdtempSync(path.join(tmpdir(), 'pier-fresh-recovery-'))
15
+ const controlRoot = path.join(root, 'control')
16
+ const jobsDirectory = path.join(root, 'jobs')
17
+ const jobName = 'fresh-recovery'
18
+ const trialName = 'fresh-recovery-trial'
19
+ const executionId = 'pier-fresh-process-recovery'
20
+ const executionPlanDigest = `sha256:${'e'.repeat(64)}` as const
21
+ const image = 'ghcr.io/tangle-network/devcontainers/universal:latest'
22
+ let containerId: string | undefined
23
+ let handle: ReturnType<FilePierCandidateTrialController['start']> | undefined
24
+ let resultSettled: Promise<unknown> | undefined
25
+
26
+ try {
27
+ containerId = execFileSync(
28
+ 'docker',
29
+ [
30
+ 'run',
31
+ '-d',
32
+ '--label',
33
+ `com.docker.compose.project=${trialName}`,
34
+ image,
35
+ 'sleep',
36
+ 'infinity',
37
+ ],
38
+ { encoding: 'utf8', timeout: 120_000 },
39
+ ).trim()
40
+ if (!/^[a-f0-9]{12,64}$/.test(containerId)) {
41
+ throw new Error(`Docker returned an invalid recovery container id: ${containerId}`)
42
+ }
43
+
44
+ const controller = new FilePierCandidateTrialController({
45
+ directory: controlRoot,
46
+ launch: () => ({
47
+ command: process.execPath,
48
+ args: [
49
+ '-e',
50
+ `require('node:fs').mkdirSync(${JSON.stringify(path.join(jobsDirectory, jobName, trialName))}, { recursive: true }); setInterval(() => undefined, 1_000)`,
51
+ ],
52
+ cwd: root,
53
+ env: { ...process.env },
54
+ jobsDirectory,
55
+ jobName,
56
+ readResult: async () => {
57
+ throw new Error('recovery probe must not complete normally')
58
+ },
59
+ }),
60
+ })
61
+ handle = controller.start(
62
+ createStagedPierCandidateExecutionFixture(executionId),
63
+ {
64
+ request: {
65
+ executionId,
66
+ executionPlan: { value: { digest: executionPlanDigest } },
67
+ } as AgentCandidateExecutorRequest,
68
+ traceStore: new InMemoryTraceStore(),
69
+ signal: new AbortController().signal,
70
+ deadlineAtMs: Date.now() + 30_000,
71
+ },
72
+ )
73
+ resultSettled = handle.result.catch(() => undefined)
74
+
75
+ const [slot] = readdirSync(controlRoot)
76
+ if (!slot) throw new Error('Pier controller omitted durable state')
77
+ const identityPath = path.join(controlRoot, slot, 'identity.json')
78
+ let pierPid: number | undefined
79
+ for (let attempt = 0; attempt < 200; attempt++) {
80
+ const identity = JSON.parse(readFileSync(identityPath, 'utf8'))
81
+ if (identity.state === 'running' && Number.isSafeInteger(identity.pier?.pid)) {
82
+ pierPid = identity.pier.pid
83
+ break
84
+ }
85
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10))
86
+ }
87
+ if (!pierPid) throw new Error('Pier controller never persisted the child process identity')
88
+ const trialDirectory = path.join(jobsDirectory, jobName, trialName)
89
+ for (let attempt = 0; attempt < 200 && !existsSync(trialDirectory); attempt++) {
90
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10))
91
+ }
92
+ if (!existsSync(trialDirectory)) {
93
+ throw new Error('recovery probe never created its evaluator-owned trial directory')
94
+ }
95
+
96
+ const recovery = JSON.parse(
97
+ execFileSync(
98
+ process.execPath,
99
+ [
100
+ '--import',
101
+ 'tsx',
102
+ path.join(benchDir, 'scripts', 'terminate-pier-trial.mts'),
103
+ controlRoot,
104
+ executionId,
105
+ executionPlanDigest,
106
+ ],
107
+ { cwd: benchDir, encoding: 'utf8', timeout: 20_000 },
108
+ ),
109
+ )
110
+ if (recovery.processExited !== true || recovery.containersRemoved !== true) {
111
+ throw new Error(`fresh recovery returned an incomplete acknowledgement: ${JSON.stringify(recovery)}`)
112
+ }
113
+ try {
114
+ process.kill(pierPid, 0)
115
+ throw new Error(`recovered Pier process ${pierPid} is still alive`)
116
+ } catch (error) {
117
+ if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
118
+ }
119
+ let containerPresent = true
120
+ try {
121
+ execFileSync('docker', ['inspect', containerId], { stdio: 'ignore', timeout: 30_000 })
122
+ } catch {
123
+ containerPresent = false
124
+ }
125
+ if (containerPresent) throw new Error(`recovered Pier container ${containerId} is still present`)
126
+ await resultSettled
127
+ process.stdout.write(
128
+ `${JSON.stringify({ freshProcess: true, processExited: true, containersRemoved: true })}\n`,
129
+ )
130
+ } finally {
131
+ if (handle) await handle.terminateAndWait().catch(() => undefined)
132
+ if (resultSettled) await resultSettled
133
+ if (containerId) {
134
+ try {
135
+ execFileSync('docker', ['rm', '-f', containerId], { stdio: 'ignore', timeout: 30_000 })
136
+ } catch {}
137
+ }
138
+ rmSync(root, { recursive: true, force: true })
139
+ }
package/src/adapters.ts CHANGED
@@ -6,22 +6,35 @@
6
6
  */
7
7
 
8
8
  import { createAecBenchAdapter } from './benchmarks/aec-bench'
9
+ import { createAgentBenchAdapter } from './benchmarks/agentbench'
9
10
  import { createAppWorldAdapter, createAppWorldReactAdapter } from './benchmarks/appworld'
11
+ import { createBfclAdapter } from './benchmarks/bfcl'
10
12
  import { createCadBenchAdapter } from './benchmarks/cadbench'
11
13
  import { createCadDesignAdapter } from './benchmarks/cad-design'
12
14
  import { createCadGenBenchAdapter } from './benchmarks/cadgenbench'
13
15
  import { createCommit0Adapter } from './benchmarks/commit0'
16
+ import { createCragAdapter } from './benchmarks/crag'
17
+ import { createDabstepAdapter } from './benchmarks/dabstep'
14
18
  import { createEnterpriseOpsGymAdapter } from './benchmarks/enterpriseops-gym'
19
+ import { createFinResearchBenchAdapter } from './benchmarks/finresearchbench'
15
20
  import { createFinsearchcompAdapter } from './benchmarks/finsearchcomp'
16
21
  import { createFramesAdapter } from './benchmarks/frames'
17
22
  import { createHotpotqaAdapter } from './benchmarks/hotpotqa'
18
23
  import { createHumanEvalAdapter } from './benchmarks/humaneval'
19
24
  import { createMind2WebAdapter } from './benchmarks/mind2web'
25
+ import { createNoMiraclAdapter } from './benchmarks/nomiracl'
26
+ import { createOpenRagBenchAdapter } from './benchmarks/open-rag-bench'
20
27
  import { createProgrambenchAdapter } from './benchmarks/programbench'
28
+ import { createRagBenchAdapter } from './benchmarks/ragbench'
21
29
  import { createSimpleQaAdapter } from './benchmarks/simpleqa'
22
30
  import { createSweBenchAdapter } from './benchmarks/swe-bench'
31
+ import { createT2RagBenchAdapter } from './benchmarks/t2-ragbench'
32
+ import { createTau2BenchAdapter } from './benchmarks/tau2-bench'
33
+ import { createTau3BankingAdapter } from './benchmarks/tau3-banking'
23
34
  import { createTerminalBenchAdapter } from './benchmarks/terminal-bench'
35
+ import { createToolLlmAdapter } from './benchmarks/toollm'
24
36
  import { createTrataHedgeAdapter } from './benchmarks/trata-hedge'
37
+ import { createWebArenaVerifiedAdapter } from './benchmarks/webarena-verified'
25
38
  import type { BenchmarkAdapter } from './benchmarks/types'
26
39
 
27
40
  export const ADAPTERS: Record<string, () => BenchmarkAdapter> = {
@@ -32,7 +45,14 @@ export const ADAPTERS: Record<string, () => BenchmarkAdapter> = {
32
45
  // delegates to the benchmark's own harness and fails loud when it/Docker is absent.
33
46
  'aec-bench': createAecBenchAdapter,
34
47
  commit0: createCommit0Adapter,
48
+ dabstep: createDabstepAdapter,
35
49
  programbench: createProgrambenchAdapter,
50
+ 'webarena-verified': createWebArenaVerifiedAdapter,
51
+ 'tau2-bench': createTau2BenchAdapter,
52
+ 'tau3-banking': createTau3BankingAdapter,
53
+ agentbench: createAgentBenchAdapter,
54
+ bfcl: createBfclAdapter,
55
+ toollm: createToolLlmAdapter,
36
56
  appworld: createAppWorldAdapter,
37
57
  // AppWorld's native interactive protocol — the worker is the in-engine ReAct
38
58
  // episode (execution feedback every turn), the mode published baselines use.
@@ -42,6 +62,12 @@ export const ADAPTERS: Record<string, () => BenchmarkAdapter> = {
42
62
  cadbench: createCadBenchAdapter,
43
63
  cadgenbench: createCadGenBenchAdapter,
44
64
  frames: createFramesAdapter,
65
+ ragbench: createRagBenchAdapter,
66
+ crag: createCragAdapter,
67
+ nomiracl: createNoMiraclAdapter,
68
+ 'open-rag-bench': createOpenRagBenchAdapter,
69
+ 't2-ragbench': createT2RagBenchAdapter,
70
+ finresearchbench: createFinResearchBenchAdapter,
45
71
  finsearchcomp: createFinsearchcompAdapter,
46
72
  simpleqa: createSimpleQaAdapter,
47
73
  hotpotqa: createHotpotqaAdapter,
@@ -0,0 +1,178 @@
1
+ import assert from 'node:assert/strict'
2
+ import { createHash } from 'node:crypto'
3
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4
+ import test from 'node:test'
5
+ import { tmpdir } from 'node:os'
6
+ import { join } from 'node:path'
7
+ import { resolveBenchPython, runStagedJudge } from './_harness'
8
+
9
+ const digest = (bytes: Uint8Array): `sha256:${string}` =>
10
+ `sha256:${createHash('sha256').update(bytes).digest('hex')}`
11
+
12
+ test('resolveBenchPython defaults to the bench-local virtual environment', () => {
13
+ assert.equal(resolveBenchPython({}, '/opt/agent-bench'), join('/opt/agent-bench', '.venv', 'bin', 'python'))
14
+ })
15
+
16
+ test('resolveBenchPython accepts an absolute consumer-managed interpreter', () => {
17
+ assert.equal(
18
+ resolveBenchPython({ AGENT_BENCH_PYTHON: '/srv/bench-venv/bin/python' }, '/opt/agent-bench'),
19
+ '/srv/bench-venv/bin/python',
20
+ )
21
+ })
22
+
23
+ test('resolveBenchPython rejects relative and empty interpreter paths', () => {
24
+ assert.throws(
25
+ () => resolveBenchPython({ AGENT_BENCH_PYTHON: '.venv/bin/python' }, '/opt/agent-bench'),
26
+ /must be an absolute path/,
27
+ )
28
+ assert.throws(
29
+ () => resolveBenchPython({ AGENT_BENCH_PYTHON: '' }, '/opt/agent-bench'),
30
+ /must be an absolute path/,
31
+ )
32
+ })
33
+
34
+ test('runStagedJudge terminates a hung evaluator at its explicit timeout', async () => {
35
+ const started = Date.now()
36
+ await assert.rejects(
37
+ () => runStagedJudge({
38
+ tmpPrefix: 'timeout-test-',
39
+ async stage() {},
40
+ bin: process.execPath,
41
+ argv: () => ['-e', 'setInterval(() => {}, 1_000)'],
42
+ timeoutMs: 100,
43
+ async parseReport() {
44
+ throw new Error('timed-out evaluator must not reach report parsing')
45
+ },
46
+ }),
47
+ /evaluator failed/,
48
+ )
49
+ assert.ok(Date.now() - started < 5_000)
50
+ })
51
+
52
+ test('runStagedJudge durably captures exact evaluator and process bytes with hashes', async () => {
53
+ const scratch = await mkdtemp(join(tmpdir(), 'staged-judge-capture-test-'))
54
+ const destination = join(scratch, 'attempt-001')
55
+ const perTestBytes = Buffer.from([0x00, 0xff, 0x80, 0x0a, 0x41])
56
+ const stdoutBytes = Buffer.from([0x53, 0x00, 0xff, 0x0a])
57
+ const stderrBytes = Buffer.from([0x45, 0x80, 0x0a])
58
+ try {
59
+ const score = await runStagedJudge({
60
+ tmpPrefix: 'capture-test-',
61
+ async stage(dir) {
62
+ await writeFile(join(dir, 'preds.json'), Buffer.from([0x7b, 0x7d]))
63
+ },
64
+ bin: process.execPath,
65
+ argv: () => [
66
+ '-e',
67
+ [
68
+ "const fs = require('node:fs')",
69
+ "fs.mkdirSync('logs/run_evaluation/run/model/task', { recursive: true })",
70
+ `fs.writeFileSync('logs/run_evaluation/run/model/task/test_output.txt', Buffer.from(${JSON.stringify([...perTestBytes])}))`,
71
+ "fs.writeFileSync('official.report.json', Buffer.from('{\"resolved\":true}'))",
72
+ `process.stdout.write(Buffer.from(${JSON.stringify([...stdoutBytes])}))`,
73
+ `process.stderr.write(Buffer.from(${JSON.stringify([...stderrBytes])}))`,
74
+ ].join(';'),
75
+ ],
76
+ capture: { destination },
77
+ async parseReport(dir) {
78
+ assert.deepEqual(
79
+ await readFile(join(dir, 'logs/run_evaluation/run/model/task/test_output.txt')),
80
+ perTestBytes,
81
+ )
82
+ return { resolved: true, score: 1 }
83
+ },
84
+ })
85
+
86
+ const receipt = score.judgeArtifacts
87
+ assert.ok(receipt)
88
+ assert.equal(receipt.schema, 'agent-bench/judge-artifacts/v1')
89
+ assert.equal(receipt.evaluatorSucceeded, true)
90
+ assert.equal(receipt.directory, destination)
91
+ assert.deepEqual(
92
+ await readFile(join(destination, 'evaluator/logs/run_evaluation/run/model/task/test_output.txt')),
93
+ perTestBytes,
94
+ )
95
+ assert.deepEqual(await readFile(join(destination, 'process/stdout.bin')), stdoutBytes)
96
+ assert.deepEqual(await readFile(join(destination, 'process/stderr.bin')), stderrBytes)
97
+ assert.deepEqual(
98
+ JSON.parse(await readFile(receipt.manifestPath, 'utf8')),
99
+ receipt,
100
+ )
101
+
102
+ const expected = new Map([
103
+ ['evaluator/logs/run_evaluation/run/model/task/test_output.txt', perTestBytes],
104
+ ['evaluator/official.report.json', Buffer.from('{"resolved":true}')],
105
+ ['evaluator/preds.json', Buffer.from('{}')],
106
+ ['process/stderr.bin', stderrBytes],
107
+ ['process/stdout.bin', stdoutBytes],
108
+ ])
109
+ assert.deepEqual(receipt.files.map((file) => file.path), [...expected.keys()])
110
+ for (const file of receipt.files) {
111
+ const bytes = expected.get(file.path)
112
+ assert.ok(bytes)
113
+ assert.equal(file.kind, 'file')
114
+ assert.equal(file.byteLength, bytes.byteLength)
115
+ assert.equal(file.sha256, digest(bytes))
116
+ }
117
+ const treeBytes = Buffer.from(
118
+ receipt.files
119
+ .map((file) => `${file.path}\0${file.kind}\0${file.byteLength}\0${file.sha256}\n`)
120
+ .join(''),
121
+ )
122
+ assert.equal(receipt.treeSha256, digest(treeBytes))
123
+ assert.equal(receipt.fileCount, expected.size)
124
+ assert.equal(
125
+ receipt.byteLength,
126
+ [...expected.values()].reduce((total, bytes) => total + bytes.byteLength, 0),
127
+ )
128
+ } finally {
129
+ await rm(scratch, { recursive: true, force: true })
130
+ }
131
+ })
132
+
133
+ test('runStagedJudge captures partial evaluator evidence before throwing', async () => {
134
+ const scratch = await mkdtemp(join(tmpdir(), 'staged-judge-failure-capture-test-'))
135
+ const destination = join(scratch, 'attempt-failed')
136
+ try {
137
+ let caught: unknown
138
+ try {
139
+ await runStagedJudge({
140
+ tmpPrefix: 'capture-failure-test-',
141
+ async stage() {},
142
+ bin: process.execPath,
143
+ argv: () => [
144
+ '-e',
145
+ [
146
+ "const fs = require('node:fs')",
147
+ "fs.mkdirSync('logs/run_evaluation/run/model/task', { recursive: true })",
148
+ "fs.writeFileSync('logs/run_evaluation/run/model/task/run_instance.log', Buffer.from([1, 2, 255]))",
149
+ 'process.stderr.write(Buffer.from([69, 82, 82, 0, 255]))',
150
+ 'process.exit(9)',
151
+ ].join(';'),
152
+ ],
153
+ capture: { destination },
154
+ async parseReport() {
155
+ throw new Error('failed evaluator must not reach report parsing')
156
+ },
157
+ })
158
+ } catch (error) {
159
+ caught = error
160
+ }
161
+ assert.ok(caught instanceof Error)
162
+ assert.match(caught.message, /evaluator failed/)
163
+ const receipt = (caught as Error & { judgeArtifacts?: { evaluatorSucceeded: boolean } })
164
+ .judgeArtifacts
165
+ assert.ok(receipt)
166
+ assert.equal(receipt.evaluatorSucceeded, false)
167
+ assert.deepEqual(
168
+ await readFile(join(destination, 'evaluator/logs/run_evaluation/run/model/task/run_instance.log')),
169
+ Buffer.from([1, 2, 255]),
170
+ )
171
+ assert.deepEqual(
172
+ await readFile(join(destination, 'process/stderr.bin')),
173
+ Buffer.from([69, 82, 82, 0, 255]),
174
+ )
175
+ } finally {
176
+ await rm(scratch, { recursive: true, force: true })
177
+ }
178
+ })