@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,212 @@
1
+ /**
2
+ * DABStep adapter (EnvCommons/DABStep) — data-analysis questions over synthetic
3
+ * payment files. Worker artifact = final answer text. Judge = the official
4
+ * DABStep `grade.py` normalization/matching function. No LLM judge.
5
+ *
6
+ * Live tasks require `DABSTEP_DIR` pointing at an official DABStep checkout that
7
+ * includes `dataset.csv`, `splits/*.txt`, `files/*`, and `grade.py`. The adapter
8
+ * exposes `metadata.resourceRoot` so runners can mount the benchmark files into
9
+ * AgentProfile.resources.files; it does not paste the dataset into prompt text.
10
+ */
11
+
12
+ import { join } from 'node:path'
13
+ import { access, readFile, stat } from 'node:fs/promises'
14
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
15
+ import { benchRoot, runVenvPython, runVenvScriptStdin } from './_harness'
16
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
17
+
18
+ const FIXTURES = join(benchRoot, 'fixtures', 'dabstep.json')
19
+ const DEFAULT_SPLIT = 'easy'
20
+
21
+ interface DabstepFixtureRow {
22
+ task_id: number
23
+ instructions: string
24
+ all_golds_by_task: Array<Record<string, unknown>>
25
+ }
26
+
27
+ interface DabstepMeta {
28
+ taskId: number
29
+ split: string
30
+ golds: Array<Record<string, unknown>>
31
+ resourceRoot?: string
32
+ }
33
+
34
+ const dabstepDir = (): string | undefined => process.env.DABSTEP_DIR
35
+ const gradeFile = (dir: string): string => join(dir, 'grade.py')
36
+ const resourceRoot = (dir: string): string => join(dir, 'files')
37
+
38
+ async function assertFile(path: string, label: string): Promise<void> {
39
+ try {
40
+ await access(path)
41
+ } catch (err) {
42
+ throw new Error(`DABStep: missing ${label} at ${path} (${err instanceof Error ? err.message : err})`)
43
+ }
44
+ }
45
+
46
+ async function assertOfficialFiles(dir: string, split: string): Promise<void> {
47
+ await assertFile(join(dir, 'dataset.csv'), 'released dataset.csv')
48
+ await assertFile(join(dir, 'splits', `${split}.txt`), `${split} split file`)
49
+ await assertFile(gradeFile(dir), 'official grade.py')
50
+ const files = resourceRoot(dir)
51
+ try {
52
+ const s = await stat(files)
53
+ if (!s.isDirectory()) throw new Error('not a directory')
54
+ } catch (err) {
55
+ throw new Error(`DABStep: missing benchmark files directory at ${files} (${err instanceof Error ? err.message : err})`)
56
+ }
57
+ }
58
+
59
+ export const dabstepAnswerOutput: OutputAdapter<string> = {
60
+ parse(events) {
61
+ let text = ''
62
+ for (const ev of events) {
63
+ const d = (ev as { data?: Record<string, unknown> })?.data
64
+ const t = d?.finalText ?? d?.text ?? d?.result
65
+ if (typeof t === 'string' && t.length > 0) text = t
66
+ }
67
+ const fences = [...text.matchAll(/```(?:text|answer)?\s*\n([\s\S]*?)```/g)]
68
+ return (fences.at(-1)?.[1] ?? text).trim()
69
+ },
70
+ }
71
+
72
+ function rowToTask(row: DabstepFixtureRow, split: string, dir?: string): BenchTask {
73
+ const meta: DabstepMeta = {
74
+ taskId: row.task_id,
75
+ split,
76
+ golds: row.all_golds_by_task,
77
+ ...(dir ? { resourceRoot: resourceRoot(dir) } : {}),
78
+ }
79
+ return {
80
+ id: String(row.task_id),
81
+ split,
82
+ prompt: [
83
+ 'Solve this DABStep data-analysis task using the mounted payment files.',
84
+ 'Use code or shell commands as needed, then return only the final answer.',
85
+ '',
86
+ row.instructions,
87
+ ].join('\n'),
88
+ metadata: meta as unknown as Record<string, unknown>,
89
+ }
90
+ }
91
+
92
+ function readMeta(task: BenchTask): DabstepMeta {
93
+ const md = task.metadata
94
+ if (!md || typeof md.taskId !== 'number' || !Array.isArray(md.golds)) {
95
+ throw new Error(`dabstep task ${task.id} missing metadata — loadTasks did not populate it`)
96
+ }
97
+ return md as unknown as DabstepMeta
98
+ }
99
+
100
+ function selectRows(rows: DabstepFixtureRow[], opts: LoadOptions, split: string, dir?: string): BenchTask[] {
101
+ let tasks = rows.map((row) => rowToTask(row, split, dir))
102
+ if (opts.ids) {
103
+ const want = new Set(opts.ids)
104
+ tasks = tasks.filter((task) => want.has(task.id))
105
+ } else if (opts.limit !== undefined) {
106
+ tasks = tasks.slice(0, opts.limit)
107
+ }
108
+ if (tasks.length === 0) throw new Error(`DABStep: no tasks matched ${JSON.stringify(opts)} for split=${split}`)
109
+ return tasks
110
+ }
111
+
112
+ async function loadFixtures(opts: LoadOptions, split: string): Promise<BenchTask[]> {
113
+ const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as DabstepFixtureRow[]
114
+ console.warn(`[dabstep] DABSTEP_FIXTURES=1 — loading ${rows.length} adapter fixtures from ${FIXTURES}`)
115
+ return selectRows(rows, opts, split)
116
+ }
117
+
118
+ async function loadOfficialTasks(dir: string, opts: LoadOptions, split: string): Promise<BenchTask[]> {
119
+ const script = `
120
+ import ast, csv, json, sys
121
+ from pathlib import Path
122
+
123
+ root = Path(sys.argv[1])
124
+ split = sys.argv[2]
125
+ limit = None if sys.argv[3] == "" else int(sys.argv[3])
126
+ ids = set(json.loads(sys.argv[4]))
127
+ dataset = root / "dataset.csv"
128
+ split_file = root / "splits" / f"{split}.txt"
129
+ if not dataset.exists():
130
+ raise SystemExit(f"missing official DABStep dataset.csv at {dataset}")
131
+ if not split_file.exists():
132
+ raise SystemExit(f"missing official DABStep split file at {split_file}")
133
+ split_ids = {int(line.strip()) for line in split_file.read_text().splitlines() if line.strip()}
134
+ out = []
135
+ with dataset.open(newline="") as f:
136
+ for row in csv.DictReader(f):
137
+ task_id = int(row["task_id"])
138
+ if task_id not in split_ids:
139
+ continue
140
+ if ids and str(task_id) not in ids:
141
+ continue
142
+ out.append({
143
+ "task_id": task_id,
144
+ "instructions": f"{row['question']}\\n{row['guidelines']}",
145
+ "all_golds_by_task": ast.literal_eval(str(row["all_golds_by_task"])),
146
+ })
147
+ if limit is not None and len(out) >= limit:
148
+ break
149
+ if not out:
150
+ raise SystemExit(f"no DABStep rows matched split={split} ids={sorted(ids)} limit={limit}")
151
+ print(json.dumps(out))
152
+ `
153
+ const stdout = await runVenvPython(script, [dir, split, opts.limit === undefined ? '' : String(opts.limit), JSON.stringify(opts.ids ?? [])])
154
+ return selectRows(JSON.parse(stdout) as DabstepFixtureRow[], opts, split, dir)
155
+ }
156
+
157
+ export function createDabstepAdapter(): BenchmarkAdapter {
158
+ const fixturesMode = process.env.DABSTEP_FIXTURES === '1'
159
+
160
+ return {
161
+ name: 'dabstep',
162
+ output: dabstepAnswerOutput,
163
+
164
+ async preflight() {
165
+ if (fixturesMode) return
166
+ const dir = dabstepDir()
167
+ if (!dir) {
168
+ throw new Error(
169
+ 'DABSTEP_DIR is required. Fix: clone https://github.com/EnvCommons/DABStep, add the released dataset.csv under that checkout, then set DABSTEP_DIR=/path/to/DABStep.',
170
+ )
171
+ }
172
+ await assertOfficialFiles(dir, DEFAULT_SPLIT)
173
+ await loadOfficialTasks(dir, { limit: 1 }, DEFAULT_SPLIT)
174
+ },
175
+
176
+ async loadTasks(opts: LoadOptions = {}) {
177
+ const split = opts.split ?? DEFAULT_SPLIT
178
+ if (fixturesMode) return loadFixtures(opts, split)
179
+ const dir = dabstepDir()
180
+ if (!dir) throw new Error('DABSTEP_DIR is required to load official DABStep tasks')
181
+ return loadOfficialTasks(dir, opts, split)
182
+ },
183
+
184
+ async goldArtifact(task: BenchTask) {
185
+ const meta = readMeta(task)
186
+ const first = meta.golds[0]
187
+ if (!first) return undefined
188
+ const value = first.value
189
+ return value === undefined ? undefined : String(value)
190
+ },
191
+
192
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
193
+ const meta = readMeta(task)
194
+ const dir = dabstepDir()
195
+ if (!dir) throw new Error('DABSTEP_DIR is required to judge DABStep tasks with the official grade.py')
196
+ const stdout = await runVenvScriptStdin(
197
+ join(benchRoot, 'scripts', 'dabstep_judge.py'),
198
+ ['--grade-file', gradeFile(dir)],
199
+ JSON.stringify({ prediction: artifact, golds: meta.golds }),
200
+ { cwd: benchRoot },
201
+ )
202
+ const report = JSON.parse(stdout.trim().split('\n').at(-1) ?? '{}') as { correct?: boolean; score?: number; error?: string }
203
+ if (report.error) throw new Error(`DABStep judge error for ${task.id}: ${report.error}`)
204
+ const score = typeof report.score === 'number' ? report.score : 0
205
+ return {
206
+ resolved: report.correct === true,
207
+ score,
208
+ detail: JSON.stringify({ taskId: meta.taskId, split: meta.split, correct: report.correct }),
209
+ }
210
+ },
211
+ }
212
+ }
@@ -0,0 +1,150 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { createAgentBenchAdapter } from './agentbench'
7
+ import { createBfclAdapter } from './bfcl'
8
+ import { createFinResearchBenchAdapter } from './finresearchbench'
9
+ import { createTau2BenchAdapter } from './tau2-bench'
10
+ import { createTau3BankingAdapter } from './tau3-banking'
11
+ import { createToolLlmAdapter } from './toollm'
12
+ import { createWebArenaVerifiedAdapter } from './webarena-verified'
13
+
14
+ async function withEnv<T>(patch: Record<string, string | undefined>, fn: () => Promise<T>): Promise<T> {
15
+ const old: Record<string, string | undefined> = {}
16
+ for (const [key, value] of Object.entries(patch)) {
17
+ old[key] = process.env[key]
18
+ if (value === undefined) delete process.env[key]
19
+ else process.env[key] = value
20
+ }
21
+ try {
22
+ return await fn()
23
+ } finally {
24
+ for (const [key, value] of Object.entries(old)) {
25
+ if (value === undefined) delete process.env[key]
26
+ else process.env[key] = value
27
+ }
28
+ }
29
+ }
30
+
31
+ test('WebArena-Verified fixture mode loads tasks and live mode fails loud without checkout', async () => {
32
+ await withEnv({ WEBARENA_VERIFIED_FIXTURES: '1', WEBARENA_VERIFIED_DIR: undefined }, async () => {
33
+ const adapter = createWebArenaVerifiedAdapter()
34
+ await adapter.preflight()
35
+ const tasks = await adapter.loadTasks({ limit: 1 })
36
+ assert.equal(tasks.length, 1)
37
+ assert.equal(tasks[0].id, '0')
38
+ assert.match(tasks[0].prompt, /WebArena-Verified/)
39
+ })
40
+
41
+ await withEnv({ WEBARENA_VERIFIED_FIXTURES: undefined, WEBARENA_VERIFIED_DIR: undefined }, async () => {
42
+ await assert.rejects(() => createWebArenaVerifiedAdapter().preflight(), /WEBARENA_VERIFIED_DIR is required/)
43
+ })
44
+ })
45
+
46
+ test('tau2-bench fixture mode loads tasks and live mode fails loud without checkout', async () => {
47
+ await withEnv({ TAU2_FIXTURES: '1', TAU2_BENCH_DIR: undefined }, async () => {
48
+ const adapter = createTau2BenchAdapter()
49
+ await adapter.preflight()
50
+ const tasks = await adapter.loadTasks({ limit: 1 })
51
+ assert.equal(tasks.length, 1)
52
+ assert.equal(tasks[0].id, 'retail-fixture-0')
53
+ assert.match(tasks[0].prompt, /tau2 task/)
54
+ })
55
+
56
+ await withEnv({ TAU2_FIXTURES: undefined, TAU2_BENCH_DIR: undefined }, async () => {
57
+ await assert.rejects(() => createTau2BenchAdapter().preflight(), /TAU2_BENCH_DIR is required/)
58
+ })
59
+ })
60
+
61
+ test('tau3-banking fixture mode loads tasks and live mode fails loud without checkout', async () => {
62
+ await withEnv({ TAU3_FIXTURES: '1', TAU3_BENCH_DIR: undefined }, async () => {
63
+ const adapter = createTau3BankingAdapter()
64
+ await adapter.preflight()
65
+ const tasks = await adapter.loadTasks({ limit: 1 })
66
+ assert.equal(tasks.length, 1)
67
+ assert.equal(tasks[0].id, 'banking-knowledge-fixture-0')
68
+ assert.match(tasks[0].prompt, /tau3 banking task/)
69
+ })
70
+
71
+ await withEnv({ TAU3_FIXTURES: undefined, TAU3_BENCH_DIR: undefined }, async () => {
72
+ await assert.rejects(() => createTau3BankingAdapter().preflight(), /TAU3_BENCH_DIR is required/)
73
+ })
74
+ })
75
+
76
+ test('AgentBench DBBench fixture mode loads and exact-label judge scores deterministically', async () => {
77
+ await withEnv({ AGENTBENCH_FIXTURES: '1', AGENTBENCH_DIR: undefined }, async () => {
78
+ const adapter = createAgentBenchAdapter()
79
+ await adapter.preflight()
80
+ const [task] = await adapter.loadTasks({ limit: 1 })
81
+ assert.equal(await adapter.goldArtifact(task), 'Women +60kg Bronze')
82
+ assert.equal((await adapter.judge(task, 'women +60kg bronze')).score, 1)
83
+ assert.equal((await adapter.judge(task, 'wrong')).score, 0)
84
+ })
85
+
86
+ await withEnv({ AGENTBENCH_FIXTURES: undefined, AGENTBENCH_DIR: undefined }, async () => {
87
+ await assert.rejects(() => createAgentBenchAdapter().preflight(), /AGENTBENCH_DIR is required/)
88
+ })
89
+ })
90
+
91
+ test('BFCL fixture mode loads official-shaped rows and scores function calls', async () => {
92
+ await withEnv({ BFCL_FIXTURES: '1', BFCL_DIR: undefined }, async () => {
93
+ const adapter = createBfclAdapter()
94
+ await adapter.preflight()
95
+ const [task] = await adapter.loadTasks({ limit: 1 })
96
+ assert.equal(task.id, 'simple_python_fixture_0')
97
+ const gold = await adapter.goldArtifact(task)
98
+ assert.match(gold ?? '', /calculate_triangle_area/)
99
+ assert.equal((await adapter.judge(task, gold ?? '')).score, 1)
100
+ assert.equal((await adapter.judge(task, '{"function_calls":[{"name":"wrong","arguments":{}}]}')).score, 0)
101
+ })
102
+
103
+ await withEnv({ BFCL_FIXTURES: undefined, BFCL_DIR: undefined }, async () => {
104
+ await assert.rejects(() => createBfclAdapter().preflight(), /BFCL_DIR is required/)
105
+ })
106
+ })
107
+
108
+ test('ToolLLM scores only deterministic API-selection labels and rejects unlabeled rows', async () => {
109
+ await withEnv({ TOOLLM_FIXTURES: '1', TOOLBENCH_DIR: undefined }, async () => {
110
+ const adapter = createToolLlmAdapter()
111
+ await adapter.preflight()
112
+ const [task] = await adapter.loadTasks({ limit: 1 })
113
+ assert.equal(task.id, '1')
114
+ const gold = await adapter.goldArtifact(task)
115
+ assert.match(gold ?? '', /Checkhealth/)
116
+ assert.equal((await adapter.judge(task, '')).score, 0)
117
+ assert.equal((await adapter.judge(task, gold ?? '')).score, 1)
118
+ assert.equal((await adapter.judge(task, '{"api_calls":[{"tool_name":"SQUAKE","api_name":"Checkhealth"}]}')).score, 0.5)
119
+ const freeText = await adapter.judge(task, 'Use SQUAKE Checkhealth, then call SQUAKE Projects.')
120
+ assert.equal(freeText.score, 1)
121
+ assert.equal(freeText.resolved, false)
122
+ })
123
+
124
+ const dir = await mkdtemp(join(tmpdir(), 'toollm-'))
125
+ try {
126
+ const query = join(dir, 'queries.json')
127
+ await writeFile(query, JSON.stringify([{ query_id: 1, query: 'Call a tool.', api_list: [] }]))
128
+ await withEnv({ TOOLLM_FIXTURES: undefined, TOOLBENCH_DIR: dir, TOOLLM_QUERY_FILE: query }, async () => {
129
+ await assert.rejects(() => createToolLlmAdapter().preflight(), /deterministic API-selection labels missing/)
130
+ })
131
+ } finally {
132
+ await rm(dir, { recursive: true, force: true })
133
+ }
134
+ })
135
+
136
+ test('FinResearchBench fixture mode is explicit and live mode requires exported judge rows', async () => {
137
+ await withEnv({ FINRESEARCHBENCH_FIXTURES: '1', FINRESEARCHBENCH_DATA_FILE: undefined }, async () => {
138
+ const adapter = createFinResearchBenchAdapter()
139
+ await adapter.preflight()
140
+ const [task] = await adapter.loadTasks({ limit: 1 })
141
+ const gold = await adapter.goldArtifact(task)
142
+ assert.match(gold ?? '', /Margin expansion/)
143
+ assert.equal((await adapter.judge(task, gold ?? '')).score, 1)
144
+ assert.equal((await adapter.judge(task, 'unrelated answer')).score, 0)
145
+ })
146
+
147
+ await withEnv({ FINRESEARCHBENCH_FIXTURES: undefined, FINRESEARCHBENCH_DATA_FILE: undefined }, async () => {
148
+ await assert.rejects(() => createFinResearchBenchAdapter().preflight(), /FINRESEARCHBENCH_DATA_FILE is required/)
149
+ })
150
+ })
@@ -0,0 +1,269 @@
1
+ /**
2
+ * FinResearchBench-compatible adapter.
3
+ *
4
+ * The paper defines a logic-tree Agent-as-a-Judge benchmark for financial
5
+ * research reports, but there is no stable public scorer package wired here.
6
+ * Live mode therefore requires a local data export whose rows carry the official
7
+ * judge prompt/template/logic tree. The adapter refuses to invent a judge.
8
+ */
9
+
10
+ import { readFile, stat } from 'node:fs/promises'
11
+ import { join } from 'node:path'
12
+ import { benchRoot } from './_harness'
13
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
14
+
15
+ const FIXTURES = join(benchRoot, 'fixtures', 'finresearchbench.json')
16
+
17
+ interface FinResearchRecord {
18
+ id: string
19
+ question: string
20
+ category?: string
21
+ reference_answer?: string
22
+ reference_report?: string
23
+ logic_tree?: unknown
24
+ rubric?: unknown
25
+ judge_system_prompt?: string
26
+ judge_prompt_template?: string
27
+ }
28
+
29
+ interface FinResearchMeta {
30
+ id: string
31
+ category: string
32
+ question: string
33
+ referenceAnswer: string
34
+ referenceReport: string
35
+ logicTree: unknown
36
+ rubric: unknown
37
+ judgeSystemPrompt?: string
38
+ judgePromptTemplate?: string
39
+ scoring: 'official-logic-tree-judge' | 'fixture-exact-reference'
40
+ }
41
+
42
+ const dataFile = (): string | undefined => process.env.FINRESEARCHBENCH_DATA_FILE
43
+
44
+ function routerConfig(): { baseUrl: string; key: string; model: string } {
45
+ const key = process.env.TANGLE_API_KEY
46
+ if (!key) throw new Error('TANGLE_API_KEY is required for FinResearchBench live LLM judging')
47
+ return {
48
+ baseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
49
+ key,
50
+ model: process.env.FINRESEARCHBENCH_JUDGE_MODEL ?? process.env.JUDGE_MODEL ?? 'deepseek-v4-flash',
51
+ }
52
+ }
53
+
54
+ async function assertReadable(path: string, label: string): Promise<void> {
55
+ try {
56
+ await stat(path)
57
+ } catch (err) {
58
+ throw new Error(`FinResearchBench: missing ${label} at ${path} (${err instanceof Error ? err.message : err})`)
59
+ }
60
+ }
61
+
62
+ function readRecords(raw: string): FinResearchRecord[] {
63
+ const trimmed = raw.trim()
64
+ if (trimmed.startsWith('[')) return JSON.parse(trimmed) as FinResearchRecord[]
65
+ return trimmed
66
+ .split(/\r?\n/)
67
+ .map((line) => line.trim())
68
+ .filter((line) => line.length > 0)
69
+ .map((line) => JSON.parse(line) as FinResearchRecord)
70
+ }
71
+
72
+ function assertLiveJudgeFields(records: readonly FinResearchRecord[], source: string): void {
73
+ const missing = records
74
+ .filter((row) => !row.judge_system_prompt || !row.judge_prompt_template)
75
+ .map((row) => row.id)
76
+ if (missing.length > 0) {
77
+ throw new Error(
78
+ `FinResearchBench live rows from ${source} missing official judge prompts for ${missing.length}/${records.length} row(s): ${missing.slice(0, 5).join(', ')}. ` +
79
+ 'Use a benchmark export with judge_system_prompt and judge_prompt_template, or do not score this benchmark live.',
80
+ )
81
+ }
82
+ }
83
+
84
+ function rowToTask(row: FinResearchRecord, fixturesMode: boolean): BenchTask {
85
+ const referenceAnswer = row.reference_answer ?? ''
86
+ const referenceReport = row.reference_report ?? referenceAnswer
87
+ const meta: FinResearchMeta = {
88
+ id: row.id,
89
+ category: row.category ?? 'unknown',
90
+ question: row.question,
91
+ referenceAnswer,
92
+ referenceReport,
93
+ logicTree: row.logic_tree ?? null,
94
+ rubric: row.rubric ?? null,
95
+ judgeSystemPrompt: row.judge_system_prompt,
96
+ judgePromptTemplate: row.judge_prompt_template,
97
+ scoring: fixturesMode ? 'fixture-exact-reference' : 'official-logic-tree-judge',
98
+ }
99
+ return {
100
+ id: row.id,
101
+ split: row.category,
102
+ prompt: [
103
+ 'Complete this FinResearchBench financial research task.',
104
+ 'Produce a decision-grade research answer with explicit reasoning, evidence, and final conclusion.',
105
+ '',
106
+ row.question,
107
+ ].join('\n'),
108
+ metadata: meta as unknown as Record<string, unknown>,
109
+ }
110
+ }
111
+
112
+ function readMeta(task: BenchTask): FinResearchMeta {
113
+ const md = task.metadata
114
+ if (!md || typeof md.question !== 'string') {
115
+ throw new Error(`FinResearchBench task ${task.id} missing metadata — loadTasks did not populate it`)
116
+ }
117
+ return md as unknown as FinResearchMeta
118
+ }
119
+
120
+ function selectRows(rows: FinResearchRecord[], opts: LoadOptions, fixturesMode: boolean): BenchTask[] {
121
+ let tasks = rows.map((row) => rowToTask(row, fixturesMode))
122
+ if (opts.split) tasks = tasks.filter((task) => task.split === opts.split)
123
+ if (opts.ids) {
124
+ const want = new Set(opts.ids)
125
+ tasks = tasks.filter((task) => want.has(task.id))
126
+ } else if (opts.limit !== undefined) {
127
+ tasks = tasks.slice(0, opts.limit)
128
+ }
129
+ if (tasks.length === 0) throw new Error(`FinResearchBench: no tasks matched ${JSON.stringify(opts)}`)
130
+ return tasks
131
+ }
132
+
133
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
134
+ const records = readRecords(await readFile(FIXTURES, 'utf8'))
135
+ console.warn(`[finresearchbench] FINRESEARCHBENCH_FIXTURES=1 — loading ${records.length} adapter fixtures`)
136
+ return selectRows(records, opts, true)
137
+ }
138
+
139
+ async function loadOfficialTasks(path: string, opts: LoadOptions): Promise<BenchTask[]> {
140
+ const records = readRecords(await readFile(path, 'utf8'))
141
+ assertLiveJudgeFields(records, path)
142
+ return selectRows(records, opts, false)
143
+ }
144
+
145
+ function fillTemplate(meta: FinResearchMeta, response: string): string {
146
+ const template = meta.judgePromptTemplate
147
+ if (!template) throw new Error(`FinResearchBench task ${meta.id} missing judge_prompt_template`)
148
+ return template
149
+ .replaceAll('{question}', meta.question)
150
+ .replaceAll('{response}', response)
151
+ .replaceAll('{reference_answer}', meta.referenceAnswer)
152
+ .replaceAll('{reference_report}', meta.referenceReport)
153
+ .replaceAll('{logic_tree}', JSON.stringify(meta.logicTree, null, 2))
154
+ .replaceAll('{rubric}', JSON.stringify(meta.rubric, null, 2))
155
+ }
156
+
157
+ function parseJudgeScore(content: string): { score: number; raw: unknown } {
158
+ const candidates: string[] = []
159
+ for (const m of content.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)) candidates.push(m[1].trim())
160
+ for (const m of content.matchAll(/\{[\s\S]*?\}/g)) candidates.push(m[0])
161
+ candidates.push(content.trim())
162
+ for (const candidate of candidates) {
163
+ let parsed: Record<string, unknown>
164
+ try {
165
+ parsed = JSON.parse(candidate) as Record<string, unknown>
166
+ } catch {
167
+ continue
168
+ }
169
+ const raw = parsed.score ?? parsed.overall_score ?? parsed.total_score ?? parsed.answer_score
170
+ const n = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN
171
+ if (!Number.isFinite(n)) continue
172
+ if (n < 0) throw new Error(`FinResearchBench judge score is negative: ${JSON.stringify(parsed)}`)
173
+ const score = n <= 1 ? n : n <= 10 ? n / 10 : n / 100
174
+ if (score > 1) throw new Error(`FinResearchBench judge score outside supported range: ${JSON.stringify(parsed)}`)
175
+ return { score, raw: parsed }
176
+ }
177
+ throw new Error(`FinResearchBench judge produced no parseable JSON score: ${content.slice(0, 400)}`)
178
+ }
179
+
180
+ async function runOfficialJudge(meta: FinResearchMeta, response: string): Promise<BenchScore> {
181
+ if (!meta.judgeSystemPrompt) throw new Error(`FinResearchBench task ${meta.id} missing judge_system_prompt`)
182
+ const router = routerConfig()
183
+ const res = await fetch(`${router.baseUrl}/chat/completions`, {
184
+ method: 'POST',
185
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${router.key}` },
186
+ body: JSON.stringify({
187
+ model: router.model,
188
+ temperature: 0,
189
+ messages: [
190
+ { role: 'system', content: meta.judgeSystemPrompt },
191
+ { role: 'user', content: fillTemplate(meta, response) },
192
+ ],
193
+ }),
194
+ })
195
+ if (!res.ok) throw new Error(`FinResearchBench judge HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`)
196
+ const body = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
197
+ const content = body.choices?.[0]?.message?.content
198
+ if (typeof content !== 'string') throw new Error(`FinResearchBench judge returned no message content: ${JSON.stringify(body).slice(0, 300)}`)
199
+ const { score, raw } = parseJudgeScore(content)
200
+ return {
201
+ resolved: score >= Number(process.env.FINRESEARCHBENCH_PASS_THRESHOLD ?? 0.8),
202
+ score,
203
+ detail: JSON.stringify({ scoring: meta.scoring, category: meta.category, judgeModel: router.model, raw }),
204
+ }
205
+ }
206
+
207
+ function normalizeText(value: string): string {
208
+ return value.toLowerCase().replace(/\s+/g, ' ').trim()
209
+ }
210
+
211
+ function scoreFixture(meta: FinResearchMeta, artifact: string): BenchScore {
212
+ const answer = normalizeText(meta.referenceAnswer || meta.referenceReport)
213
+ const response = normalizeText(artifact)
214
+ const score = answer.length > 0 && response.includes(answer) ? 1 : 0
215
+ return {
216
+ resolved: score === 1,
217
+ score,
218
+ detail: JSON.stringify({ scoring: meta.scoring, category: meta.category }),
219
+ }
220
+ }
221
+
222
+ export function createFinResearchBenchAdapter(): BenchmarkAdapter {
223
+ const fixturesMode = process.env.FINRESEARCHBENCH_FIXTURES === '1'
224
+
225
+ return {
226
+ name: 'finresearchbench',
227
+
228
+ async preflight() {
229
+ if (fixturesMode) {
230
+ await assertReadable(FIXTURES, 'fixture file')
231
+ return
232
+ }
233
+ const file = dataFile()
234
+ if (!file) {
235
+ throw new Error(
236
+ 'FINRESEARCHBENCH_DATA_FILE is required. Fix: export the official FinResearchBench rows as JSON/JSONL with judge_system_prompt and judge_prompt_template fields, then set FINRESEARCHBENCH_DATA_FILE=/path/to/export.jsonl.',
237
+ )
238
+ }
239
+ routerConfig()
240
+ await assertReadable(file, 'data file')
241
+ await loadOfficialTasks(file, { limit: 1 })
242
+ },
243
+
244
+ async loadTasks(opts: LoadOptions = {}) {
245
+ if (fixturesMode) return loadFixtures(opts)
246
+ const file = dataFile()
247
+ if (!file) throw new Error('FINRESEARCHBENCH_DATA_FILE is required to load FinResearchBench rows')
248
+ return loadOfficialTasks(file, opts)
249
+ },
250
+
251
+ async goldArtifact(task: BenchTask) {
252
+ const meta = readMeta(task)
253
+ return meta.referenceReport || meta.referenceAnswer || undefined
254
+ },
255
+
256
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
257
+ const meta = readMeta(task)
258
+ if (fixturesMode) return scoreFixture(meta, artifact)
259
+ if (artifact.trim().length === 0) {
260
+ return {
261
+ resolved: false,
262
+ score: 0,
263
+ detail: JSON.stringify({ scoring: meta.scoring, category: meta.category, reason: 'empty answer' }),
264
+ }
265
+ }
266
+ return runOfficialJudge(meta, artifact)
267
+ },
268
+ }
269
+ }
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { execFile } from 'node:child_process'
20
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
20
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
21
21
  import { tmpdir } from 'node:os'
22
22
  import { join } from 'node:path'
23
23
  import { gunzipSync } from 'node:zlib'
@@ -41,9 +41,17 @@ export interface HumanEvalTask {
41
41
  * selects a deeper slice (the later tasks are harder) so the worker has a
42
42
  * correctable middle band rather than a saturated easy prefix. */
43
43
  export async function loadHumanEval(limit: number, offset = 0): Promise<HumanEvalTask[]> {
44
- const res = await fetch(humanevalUrl)
45
- if (!res.ok) throw new Error(`HumanEval fetch HTTP ${res.status}: ${humanevalUrl}`)
46
- const gz = Buffer.from(await res.arrayBuffer())
44
+ // Prefer a locally-cached .jsonl.gz (HUMANEVAL_GZ) — the GitHub raw URL rate-limits
45
+ // (429) under repeated runs. Fall back to the network fetch when unset.
46
+ const localGz = process.env.HUMANEVAL_GZ
47
+ let gz: Buffer
48
+ if (localGz) {
49
+ gz = readFileSync(localGz)
50
+ } else {
51
+ const res = await fetch(humanevalUrl)
52
+ if (!res.ok) throw new Error(`HumanEval fetch HTTP ${res.status}: ${humanevalUrl}`)
53
+ gz = Buffer.from(await res.arrayBuffer())
54
+ }
47
55
  const text = gunzipSync(gz).toString('utf8')
48
56
  const tasks: HumanEvalTask[] = []
49
57
  for (const line of text.split('\n')) {
@@ -242,10 +250,14 @@ export function createHumanEvalAdapter(): BenchmarkAdapter {
242
250
  return { resolved: pass === 1, score: pass, detail: pass === 1 ? 'tests passed' : 'tests failed' }
243
251
  },
244
252
  async goldArtifact(task: BenchTask) {
245
- const sol = (task.metadata as HumanEvalMeta | undefined)?.canonicalSolution
246
- // The canonical solution is the function BODY; the program header supplies the
247
- // signature, so the body alone runs against check().
248
- return sol ? sol : undefined
253
+ const m = task.metadata as HumanEvalMeta | undefined
254
+ const sol = m?.canonicalSolution
255
+ // Return the COMPLETE function (signature header + canonical body), i.e. what a
256
+ // real worker emits NOT the body alone. The judge runs `extractCode`, whose
257
+ // unfenced fallback is `reply.trim()`; trimming a body-only string strips its
258
+ // leading indent and breaks it, so a body-only gold fails its own judge. A full
259
+ // def starts at column 0, trims safely, and self-verifies.
260
+ return sol ? `${m!.promptHeader}${sol}` : undefined
249
261
  },
250
262
  }
251
263
  }