@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
@@ -21,15 +21,26 @@ import {
21
21
  runVenvPython,
22
22
  safeRunId,
23
23
  stageFile,
24
+ type StagedRunCaptureSpec,
24
25
  } from './_harness'
25
26
  import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
26
27
 
27
28
  /**
28
- * The SWE deliverable, extracted from the agent's event STREAM (not the box FS).
29
- * `runLoop`'s `OutputAdapter` only sees events, so the agent prints its unified
30
- * diff in a fenced block and this pulls the last one out the seam that lets the
31
- * SWE benchmark run through the gate runner (`runGate` / `runBenchmark`)
32
- * like any other.
29
+ * Fixed in-box path the agent clones the instance repo into. It is the SINGLE
30
+ * source of truth shared by the prompt template (which tells the agent to clone
31
+ * here) and `boxExtract` (which runs `git diff` here after the shot) so the
32
+ * harness always knows exactly where the agent's edits live, for any instance.
33
+ */
34
+ const SWE_REPO_DIR = '/work'
35
+
36
+ /**
37
+ * The SWE deliverable's FALLBACK parser, from the agent's event STREAM.
38
+ *
39
+ * The PRIMARY deliverable is `boxExtract` below: a `git diff` of the agent's
40
+ * actual edits, read from the cloned repo's STATE inside the box (standard
41
+ * SWE-bench practice). This event-stream parse only runs when that diff is empty
42
+ * — a model that edited the source correctly but never printed a fenced diff (the
43
+ * exact failure this replaces) still scores off its real changes, not its prose.
33
44
  */
34
45
  export const swePatchOutput: OutputAdapter<string> = {
35
46
  parse(events) {
@@ -48,17 +59,179 @@ export const swePatchOutput: OutputAdapter<string> = {
48
59
  }
49
60
 
50
61
  const DATASET = 'princeton-nlp/SWE-bench_Verified'
62
+ export type SweBenchCacheLevel = 'none' | 'base' | 'env' | 'instance'
63
+
64
+ export interface SweBenchArtifactCaptureContext {
65
+ readonly taskId: string
66
+ readonly runId: string
67
+ /** One-based sequence unique within this adapter instance. */
68
+ readonly attemptSequence: number
69
+ }
70
+
71
+ export interface SweBenchAdapterOptions {
72
+ readonly timeoutMs?: number
73
+ readonly cacheLevel?: SweBenchCacheLevel
74
+ /**
75
+ * Return a unique destination for any attempt whose complete official
76
+ * evaluator directory and process logs should be retained.
77
+ */
78
+ readonly captureEvaluatorArtifacts?: (
79
+ context: SweBenchArtifactCaptureContext,
80
+ ) => StagedRunCaptureSpec | undefined
81
+ }
82
+
83
+ const SWE_CACHE_LEVELS = new Set<SweBenchCacheLevel>(['none', 'base', 'env', 'instance'])
84
+
85
+ function scorerNamespace(): 'swebench' | 'none' {
86
+ const namespace = process.env.SWEBENCH_NAMESPACE ?? 'swebench'
87
+ if (namespace !== 'swebench' && namespace !== 'none') {
88
+ throw new Error(`SWEBENCH_NAMESPACE must be swebench|none, got "${namespace}"`)
89
+ }
90
+ return namespace
91
+ }
92
+ const TEST_FILE_EXCLUDES = [
93
+ "':(exclude,glob)**/tests/**'",
94
+ "':(exclude,glob)**/test/**'",
95
+ "':(exclude,glob)test_*.py'",
96
+ "':(exclude,glob)**/test_*.py'",
97
+ "':(exclude,glob)*_test.py'",
98
+ "':(exclude,glob)**/*_test.py'",
99
+ "':(exclude,glob)conftest.py'",
100
+ "':(exclude,glob)**/conftest.py'",
101
+ ].join(' ')
51
102
 
52
103
  interface SweReport {
53
104
  resolved_instances?: number
54
105
  resolved_ids?: string[]
106
+ unresolved_ids?: string[]
107
+ empty_patch_ids?: string[]
108
+ completed_ids?: string[]
109
+ incomplete_ids?: string[]
110
+ error_ids?: string[]
111
+ submitted_ids?: string[]
112
+ }
113
+
114
+ function stringIds(report: Record<string, unknown>, key: keyof SweReport): string[] {
115
+ const value = report[key]
116
+ if (value === undefined) return []
117
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
118
+ throw new Error(`swe-bench: malformed ${key}`)
119
+ }
120
+ return value
55
121
  }
56
122
 
57
- export function createSweBenchAdapter(): BenchmarkAdapter {
123
+ /** Convert one official report into a score without turning evaluator failures into agent failures. */
124
+ export function scoreSweReport(taskId: string, value: unknown): BenchScore {
125
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
126
+ throw new Error('swe-bench: report must be an object')
127
+ }
128
+ const report = value as Record<string, unknown>
129
+ const statusIds = {
130
+ resolved: stringIds(report, 'resolved_ids'),
131
+ unresolved: stringIds(report, 'unresolved_ids'),
132
+ emptyPatch: stringIds(report, 'empty_patch_ids'),
133
+ completed: stringIds(report, 'completed_ids'),
134
+ incomplete: stringIds(report, 'incomplete_ids'),
135
+ error: stringIds(report, 'error_ids'),
136
+ }
137
+ const submitted = stringIds(report, 'submitted_ids')
138
+ const mentioned = Object.values(statusIds).flat()
139
+ if (
140
+ mentioned.some((id) => id !== taskId)
141
+ || (submitted.length > 0 && (submitted.length !== 1 || submitted[0] !== taskId))
142
+ ) {
143
+ throw new Error(`swe-bench: report identity mismatch for ${taskId}`)
144
+ }
145
+ if (statusIds.error.includes(taskId) || statusIds.incomplete.includes(taskId)) {
146
+ throw new Error(`swe-bench: evaluator failed for ${taskId}`)
147
+ }
148
+ const outcomes = [
149
+ statusIds.resolved.includes(taskId),
150
+ statusIds.unresolved.includes(taskId),
151
+ statusIds.emptyPatch.includes(taskId),
152
+ ]
153
+ if (outcomes.filter(Boolean).length !== 1) {
154
+ throw new Error(`swe-bench: report has no unique outcome for ${taskId}`)
155
+ }
156
+ if ((outcomes[0] || outcomes[1]) && !statusIds.completed.includes(taskId)) {
157
+ throw new Error(`swe-bench: report lacks a completed evaluation for ${taskId}`)
158
+ }
159
+ const resolved = outcomes[0]
160
+ return { resolved, score: resolved ? 1 : 0, detail: JSON.stringify(report) }
161
+ }
162
+
163
+ export function sweEvaluationArgv(args: {
164
+ readonly predictionsPath: string
165
+ readonly runId: string
166
+ readonly instanceId: string
167
+ readonly cacheLevel: SweBenchCacheLevel
168
+ readonly namespace?: 'swebench' | 'none'
169
+ }): string[] {
170
+ return [
171
+ '-m', 'swebench.harness.run_evaluation',
172
+ '--dataset_name', DATASET,
173
+ '--predictions_path', args.predictionsPath,
174
+ '--run_id', args.runId,
175
+ '--instance_ids', args.instanceId,
176
+ '--max_workers', '1',
177
+ '--namespace', args.namespace ?? scorerNamespace(),
178
+ '--cache_level', args.cacheLevel,
179
+ ]
180
+ }
181
+
182
+ function shellQuote(value: string): string {
183
+ return `'${value.replace(/'/g, `'\\''`)}'`
184
+ }
185
+
186
+ function sweMetadata(task: BenchTask): { repo: string; base: string } {
187
+ const repo = String(task.metadata?.repo ?? '')
188
+ const base = String(task.metadata?.base_commit ?? '')
189
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
190
+ throw new Error(`swe-bench: invalid repo metadata for ${task.id}: ${repo}`)
191
+ }
192
+ if (!/^[0-9a-f]{7,40}$/i.test(base)) {
193
+ throw new Error(`swe-bench: invalid base_commit metadata for ${task.id}: ${base}`)
194
+ }
195
+ return { repo, base }
196
+ }
197
+
198
+ export function createSweBenchAdapter(options: SweBenchAdapterOptions = {}): BenchmarkAdapter {
199
+ if (
200
+ options.timeoutMs !== undefined
201
+ && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)
202
+ ) throw new Error('swe-bench: timeoutMs must be a positive integer')
203
+ const cacheLevel = options.cacheLevel ?? 'env'
204
+ if (!SWE_CACHE_LEVELS.has(cacheLevel)) throw new Error('swe-bench: invalid cacheLevel')
205
+ if (
206
+ options.captureEvaluatorArtifacts !== undefined
207
+ && typeof options.captureEvaluatorArtifacts !== 'function'
208
+ ) throw new Error('swe-bench: captureEvaluatorArtifacts must be a function')
209
+ let attemptSequence = 0
58
210
  return {
59
211
  name: 'swe-bench-verified',
60
212
  output: swePatchOutput,
61
213
 
214
+ // Extract the patch from repo STATE, not printed text: stage every edit the
215
+ // agent made in the cloned repo and diff it against the checked-out
216
+ // base_commit (`HEAD`). Test files are excluded — the judge applies the gold
217
+ // `test_patch` itself, so an agent edit to a test would collide on apply. The
218
+ // paths come out `a/<repo-relative>` (cwd = repo root), matching the gold
219
+ // patch format the swebench judge's `git apply` expects.
220
+ // Pre-stage: clone the instance repo at base_commit into SWE_REPO_DIR so the
221
+ // agent only edits (the harness owns the checkout — a stochastic model can't be
222
+ // trusted to clone to an exact path). `--quiet` keeps the exec output small.
223
+ boxSetup(task) {
224
+ const { repo, base } = sweMetadata(task)
225
+ return {
226
+ command: `rm -rf ${shellQuote(SWE_REPO_DIR)} && git clone --quiet ${shellQuote(`https://github.com/${repo}`)} ${shellQuote(SWE_REPO_DIR)} && git -C ${shellQuote(SWE_REPO_DIR)} checkout --quiet ${shellQuote(base)}`,
227
+ }
228
+ },
229
+ boxExtract() {
230
+ return {
231
+ command: `git -C ${shellQuote(SWE_REPO_DIR)} add -A && git -C ${shellQuote(SWE_REPO_DIR)} diff --cached -- . ${TEST_FILE_EXCLUDES}`,
232
+ }
233
+ },
234
+
62
235
  async preflight() {
63
236
  await preflightVenvImports({
64
237
  modules: ['swebench'],
@@ -101,8 +274,10 @@ print(json.dumps(out))
101
274
  prompt: [
102
275
  `Repository: ${r.repo} @ ${r.base_commit}`,
103
276
  '',
104
- 'Resolve this issue by editing the repository SOURCE so the failing tests pass without breaking the passing ones. Do NOT edit test files the evaluation runs hidden tests, so editing tests does not count. Keep the change minimal.',
105
- 'When done, END your reply with the COMPLETE unified git diff as the LAST thing, fenced exactly as ```diff … ``` (nothing after the closing fence). That fenced diff is the only deliverable.',
277
+ `The repository is ALREADY cloned at ${SWE_REPO_DIR}, checked out at commit ${r.base_commit}. Work there directly (\`cd ${SWE_REPO_DIR}\`); do not re-clone.`,
278
+ '',
279
+ 'Resolve this issue by editing the repository SOURCE so the failing tests pass without breaking the passing ones. Do NOT edit test files — the evaluation runs hidden tests on a fresh checkout, so editing tests does not count. Keep the change minimal and confined to the cloned repo.',
280
+ 'Work iteratively: reproduce the issue, implement the fix in the source, and re-run the relevant tests until they pass. You do NOT need to print the diff — the harness reads your committed edits directly from the repo.',
106
281
  '',
107
282
  '--- Issue ---',
108
283
  String(r.problem_statement ?? ''),
@@ -119,8 +294,18 @@ print(json.dumps(out))
119
294
 
120
295
  async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
121
296
  const runId = safeRunId('bench', task.id)
297
+ const capture = options.captureEvaluatorArtifacts?.({
298
+ taskId: task.id,
299
+ runId,
300
+ attemptSequence: ++attemptSequence,
301
+ })
122
302
  return runStagedJudge({
123
303
  tmpPrefix: 'swebench-',
304
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
305
+ ...(capture === undefined ? {} : { capture }),
306
+ // Debug: retain the staged dir (holds swebench's per-instance apply/run
307
+ // logs) for post-mortem when SWEBENCH_KEEP_TMP is set. Off by default.
308
+ ...(process.env.SWEBENCH_KEEP_TMP ? { keepTmp: true } : {}),
124
309
  async stage(dir) {
125
310
  await stageFile(
126
311
  join(dir, 'preds.json'),
@@ -131,20 +316,17 @@ print(json.dumps(out))
131
316
  },
132
317
  // The official evaluation harness. Pulls/builds the instance image, applies
133
318
  // the patch, runs the test spec, writes a per-run report JSON in cwd.
134
- argv: (dir) => [
135
- '-m', 'swebench.harness.run_evaluation',
136
- '--dataset_name', DATASET,
137
- '--predictions_path', join(dir, 'preds.json'),
138
- '--run_id', runId,
139
- '--instance_ids', task.id,
140
- '--max_workers', '1',
141
- '--cache_level', 'env',
142
- ],
319
+ argv: (dir) => sweEvaluationArgv({
320
+ predictionsPath: join(dir, 'preds.json'),
321
+ runId,
322
+ instanceId: task.id,
323
+ cacheLevel,
324
+ namespace: scorerNamespace(),
325
+ }),
143
326
  async parseReport(dir) {
144
327
  // Report file: agent-runtime-bench.<run_id>.json
145
328
  const report = await readJsonReport<SweReport>(join(dir, `agent-runtime-bench.${runId}.json`))
146
- const resolved = (report.resolved_ids ?? []).includes(task.id)
147
- return { resolved, score: resolved ? 1 : 0, detail: JSON.stringify(report) }
329
+ return scoreSweReport(task.id, report)
148
330
  },
149
331
  })
150
332
  },
@@ -0,0 +1,166 @@
1
+ /**
2
+ * T2-RAGBench adapter.
3
+ *
4
+ * T2-RAGBench stresses text+table retrieval and numerical reasoning over
5
+ * financial documents. The judge uses the shared deterministic answer scorer
6
+ * with numeric tolerance enabled by default.
7
+ */
8
+
9
+ import { readFile } from 'node:fs/promises'
10
+ import { join } from 'node:path'
11
+ import { benchRoot } from './_harness'
12
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
13
+ import {
14
+ FINAL_ANSWER_SENTINEL,
15
+ allStrings,
16
+ answerScoreToBenchScore,
17
+ contextBlock,
18
+ contextsFrom,
19
+ firstString,
20
+ isObject,
21
+ ragAnswerOutput,
22
+ readJsonRows,
23
+ scoreAnswerArtifact,
24
+ selectTasks,
25
+ stringFrom,
26
+ type RagContext,
27
+ } from './rag-shared'
28
+
29
+ const FIXTURES = join(benchRoot, 'fixtures', 't2-ragbench.json')
30
+
31
+ interface T2RagBenchMeta {
32
+ benchmark: 't2-ragbench'
33
+ query: string
34
+ goldAnswers: string[]
35
+ contexts: RagContext[]
36
+ subset: string
37
+ documentId: string
38
+ }
39
+
40
+ const dataFile = (): string | undefined => process.env.T2_RAGBENCH_DATA_FILE
41
+
42
+ function rowToTask(raw: unknown, index: number): BenchTask {
43
+ if (!isObject(raw)) throw new Error(`T2-RAGBench row ${index} must be an object`)
44
+ const query = firstString(raw, ['question', 'query', 'prompt'])
45
+ const goldAnswers = allStrings(raw, [
46
+ 'program_answer',
47
+ 'original_answer',
48
+ 'answer',
49
+ 'answers',
50
+ 'reference',
51
+ 'reference_answer',
52
+ 'gold',
53
+ ])
54
+ if (!query) throw new Error(`T2-RAGBench row ${index} missing question/query`)
55
+ if (goldAnswers.length === 0) throw new Error(`T2-RAGBench row ${index} missing answer`)
56
+ const baseContexts =
57
+ contextsFrom(raw.context).length > 0
58
+ ? contextsFrom(raw.context)
59
+ : contextsFrom(raw.contexts).length > 0
60
+ ? contextsFrom(raw.contexts)
61
+ : contextsFrom(raw.chunks).length > 0
62
+ ? contextsFrom(raw.chunks)
63
+ : contextsFrom(raw.passages)
64
+ const table = stringFrom(raw.table) ?? stringFrom(raw.table_text)
65
+ const contexts = table
66
+ ? [...baseContexts, { id: 'table', title: 'Table', text: table }]
67
+ : baseContexts
68
+ const subset = stringFrom(raw.subset) ?? stringFrom(raw.dataset) ?? 'unknown'
69
+ const documentId =
70
+ stringFrom(raw.context_id) ??
71
+ stringFrom(raw.document_id) ??
72
+ stringFrom(raw.doc_id) ??
73
+ stringFrom(raw.file_name) ??
74
+ 'unknown'
75
+ const id = stringFrom(raw.id) ?? stringFrom(raw.qid) ?? stringFrom(raw.query_id) ?? `t2-ragbench-${index}`
76
+ const meta: T2RagBenchMeta = {
77
+ benchmark: 't2-ragbench',
78
+ query,
79
+ goldAnswers,
80
+ contexts,
81
+ subset,
82
+ documentId,
83
+ }
84
+ return {
85
+ id,
86
+ split: stringFrom(raw.split) ?? subset,
87
+ prompt: [
88
+ 'Answer this T2-RAGBench text-and-table financial question.',
89
+ 'Do the required numerical reasoning from the supplied context before giving the final value.',
90
+ 'End with a single final line: `FINAL ANSWER: <answer>`.',
91
+ '',
92
+ `Question: ${query}`,
93
+ `Document: ${documentId}`,
94
+ `Subset: ${subset}`,
95
+ contexts.length > 0 ? `\nContext:\n${contextBlock(contexts)}` : undefined,
96
+ ]
97
+ .filter(Boolean)
98
+ .join('\n'),
99
+ metadata: meta as unknown as Record<string, unknown>,
100
+ }
101
+ }
102
+
103
+ function readMeta(task: BenchTask): T2RagBenchMeta {
104
+ const md = task.metadata
105
+ if (!md || !Array.isArray(md.goldAnswers)) {
106
+ throw new Error(`T2-RAGBench task ${task.id} missing metadata — loadTasks did not populate it`)
107
+ }
108
+ return md as unknown as T2RagBenchMeta
109
+ }
110
+
111
+ async function loadRows(path: string): Promise<unknown[]> {
112
+ const rows = await readJsonRows(path)
113
+ if (rows.length === 0) throw new Error(`T2-RAGBench: no rows in ${path}`)
114
+ return rows
115
+ }
116
+
117
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
118
+ const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as unknown[]
119
+ console.warn(`[t2-ragbench] T2_RAGBENCH_FIXTURES=1 — loading ${rows.length} adapter fixtures`)
120
+ return selectTasks(rows.map(rowToTask), opts, 'T2-RAGBench')
121
+ }
122
+
123
+ export function createT2RagBenchAdapter(): BenchmarkAdapter {
124
+ const fixturesMode = process.env.T2_RAGBENCH_FIXTURES === '1'
125
+
126
+ return {
127
+ name: 't2-ragbench',
128
+ output: ragAnswerOutput,
129
+
130
+ async preflight() {
131
+ if (fixturesMode) {
132
+ await readFile(FIXTURES, 'utf8')
133
+ return
134
+ }
135
+ const path = dataFile()
136
+ if (!path) {
137
+ throw new Error(
138
+ 'T2_RAGBENCH_DATA_FILE is required. Fix: export T2-RAGBench rows to JSONL and set T2_RAGBENCH_DATA_FILE=/path/to/t2-ragbench.jsonl, or set T2_RAGBENCH_FIXTURES=1 for adapter plumbing.',
139
+ )
140
+ }
141
+ await loadRows(path)
142
+ },
143
+
144
+ async loadTasks(opts: LoadOptions = {}) {
145
+ if (fixturesMode) return loadFixtures(opts)
146
+ const path = dataFile()
147
+ if (!path) throw new Error('T2_RAGBENCH_DATA_FILE is required to load T2-RAGBench tasks')
148
+ return selectTasks((await loadRows(path)).map(rowToTask), opts, 'T2-RAGBench')
149
+ },
150
+
151
+ async goldArtifact(task: BenchTask) {
152
+ return `${FINAL_ANSWER_SENTINEL} ${readMeta(task).goldAnswers[0] ?? ''}`
153
+ },
154
+
155
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
156
+ const meta = readMeta(task)
157
+ const score = scoreAnswerArtifact(artifact, meta.goldAnswers, { numericTolerance: 0.01 })
158
+ return answerScoreToBenchScore(score, {
159
+ benchmark: meta.benchmark,
160
+ subset: meta.subset,
161
+ documentId: meta.documentId,
162
+ contextCount: meta.contexts.length,
163
+ })
164
+ },
165
+ }
166
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Shared tau-bench adapter spine.
3
+ *
4
+ * tau2 and tau3 live in the same upstream repository/package namespace today:
5
+ * `sierra-research/tau2-bench`, Python package `tau2`. Keep one implementation
6
+ * for task loading and reward recomputation so the domain/version adapters only
7
+ * choose env names, default domain, and fixture file.
8
+ */
9
+
10
+ import { readFile, stat } from 'node:fs/promises'
11
+ import { resolve } from 'node:path'
12
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
13
+ import { runVenvPython } from './_harness'
14
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
15
+
16
+ export interface TauBenchConfig {
17
+ name: string
18
+ fixturePath: string
19
+ fixturesEnv: string
20
+ dirEnv: string
21
+ domainEnv: string
22
+ defaultDomain: string
23
+ installHint: string
24
+ taskIntro: string
25
+ }
26
+
27
+ interface TauRow {
28
+ id: string
29
+ domain: string
30
+ user_scenario?: unknown
31
+ description?: unknown
32
+ evaluation_criteria?: unknown
33
+ }
34
+
35
+ interface TauMeta {
36
+ taskId: string
37
+ domain: string
38
+ split?: string
39
+ userScenario?: unknown
40
+ description?: unknown
41
+ evaluationCriteria?: unknown
42
+ }
43
+
44
+ export const tauResultsOutput: OutputAdapter<string> = {
45
+ parse(events) {
46
+ let text = ''
47
+ for (const ev of events) {
48
+ const d = (ev as { data?: Record<string, unknown> })?.data
49
+ const t = d?.finalText ?? d?.text ?? d?.result
50
+ if (typeof t === 'string' && t.length > 0) text = t
51
+ }
52
+ const fences = [...text.matchAll(/```(?:text|path|json)?\s*\n([\s\S]*?)```/g)]
53
+ return (fences.at(-1)?.[1] ?? text).trim()
54
+ },
55
+ }
56
+
57
+ async function assertPath(path: string, label: string, benchName: string): Promise<void> {
58
+ try {
59
+ await stat(path)
60
+ } catch (err) {
61
+ throw new Error(`${benchName}: missing ${label} at ${path} (${err instanceof Error ? err.message : err})`)
62
+ }
63
+ }
64
+
65
+ function benchDir(config: TauBenchConfig): string | undefined {
66
+ return process.env[config.dirEnv]
67
+ }
68
+
69
+ function benchDomain(config: TauBenchConfig): string {
70
+ return process.env[config.domainEnv] ?? config.defaultDomain
71
+ }
72
+
73
+ function rowToTask(row: TauRow, config: TauBenchConfig, split?: string): BenchTask {
74
+ const meta: TauMeta = {
75
+ taskId: row.id,
76
+ domain: row.domain,
77
+ split,
78
+ userScenario: row.user_scenario,
79
+ description: row.description,
80
+ evaluationCriteria: row.evaluation_criteria,
81
+ }
82
+ return {
83
+ id: row.id,
84
+ split,
85
+ prompt: [
86
+ config.taskIntro,
87
+ `Run this task in the official ${row.domain} domain.`,
88
+ 'The benchmark is a simulated multi-turn user/tool conversation.',
89
+ '',
90
+ typeof row.user_scenario === 'string' ? row.user_scenario : JSON.stringify(row.user_scenario ?? {}, null, 2),
91
+ '',
92
+ 'Return the path to the official tau results.json or trajectory file containing this task run.',
93
+ ].join('\n'),
94
+ metadata: meta as unknown as Record<string, unknown>,
95
+ }
96
+ }
97
+
98
+ function readMeta(task: BenchTask, benchName: string): TauMeta {
99
+ const md = task.metadata
100
+ if (!md || typeof md.taskId !== 'string' || typeof md.domain !== 'string') {
101
+ throw new Error(`${benchName} task ${task.id} missing metadata — loadTasks did not populate it`)
102
+ }
103
+ return md as unknown as TauMeta
104
+ }
105
+
106
+ function selectRows(rows: TauRow[], opts: LoadOptions, config: TauBenchConfig, split?: string): BenchTask[] {
107
+ let tasks = rows.map((row) => rowToTask(row, config, split))
108
+ if (opts.ids) {
109
+ const want = new Set(opts.ids)
110
+ tasks = tasks.filter((task) => want.has(task.id))
111
+ } else if (opts.limit !== undefined) {
112
+ tasks = tasks.slice(0, opts.limit)
113
+ }
114
+ if (tasks.length === 0) throw new Error(`${config.name}: no tasks matched ${JSON.stringify(opts)}`)
115
+ return tasks
116
+ }
117
+
118
+ async function loadFixtures(config: TauBenchConfig, opts: LoadOptions): Promise<BenchTask[]> {
119
+ const rows = JSON.parse(await readFile(config.fixturePath, 'utf8')) as TauRow[]
120
+ console.warn(`[${config.name}] ${config.fixturesEnv}=1 — loading ${rows.length} adapter fixtures`)
121
+ return selectRows(rows, opts, config, opts.split)
122
+ }
123
+
124
+ async function loadOfficialTasks(config: TauBenchConfig, root: string, opts: LoadOptions): Promise<BenchTask[]> {
125
+ const domain = benchDomain(config)
126
+ const script = `
127
+ import json, sys
128
+ from pathlib import Path
129
+ root = Path(sys.argv[1])
130
+ domain = sys.argv[2]
131
+ split = sys.argv[3] or None
132
+ sys.path.insert(0, str(root / "src"))
133
+ from tau2.registry import registry
134
+ loader = registry.get_tasks_loader(domain)
135
+ tasks = loader(split)
136
+ rows = []
137
+ for task in tasks:
138
+ row = task.model_dump(mode="json")
139
+ row["domain"] = domain
140
+ rows.append(row)
141
+ print(json.dumps(rows))
142
+ `
143
+ const stdout = await runVenvPython(script, [root, domain, opts.split ?? ''])
144
+ return selectRows(JSON.parse(stdout) as TauRow[], opts, config, opts.split)
145
+ }
146
+
147
+ async function scoreOfficialTrajectory(root: string, meta: TauMeta, artifactPath: string): Promise<Record<string, unknown>> {
148
+ const script = `
149
+ import json, sys
150
+ from pathlib import Path
151
+ root = Path(sys.argv[1])
152
+ task_id = sys.argv[2]
153
+ artifact = Path(sys.argv[3])
154
+ sys.path.insert(0, str(root / "src"))
155
+ from tau2.data_model.simulation import Results
156
+ from tau2.scripts.evaluate_trajectories import compute_simulation_rewards
157
+ results = Results.load(artifact)
158
+ updated = compute_simulation_rewards(results)
159
+ scores = []
160
+ for sim in updated.simulations:
161
+ if sim.task_id == task_id and sim.reward_info is not None:
162
+ scores.append(float(sim.reward_info.reward))
163
+ if not scores:
164
+ raise SystemExit(f"no scored simulations for task_id={task_id} in {artifact}")
165
+ print(json.dumps({"count": len(scores), "score": sum(scores) / len(scores), "scores": scores}))
166
+ `
167
+ const stdout = await runVenvPython(script, [root, meta.taskId, artifactPath], 0)
168
+ return JSON.parse(stdout.trim().split('\n').at(-1) ?? '{}') as Record<string, unknown>
169
+ }
170
+
171
+ export function createTauBenchAdapter(config: TauBenchConfig): BenchmarkAdapter {
172
+ const fixturesMode = process.env[config.fixturesEnv] === '1'
173
+
174
+ return {
175
+ name: config.name,
176
+ output: tauResultsOutput,
177
+
178
+ async preflight() {
179
+ if (fixturesMode) return
180
+ const dir = benchDir(config)
181
+ if (!dir) {
182
+ throw new Error(`${config.dirEnv} is required. Fix: ${config.installHint}`)
183
+ }
184
+ await assertPath(`${dir}/src/tau2/registry.py`, 'tau registry', config.name)
185
+ await loadOfficialTasks(config, dir, { limit: 1 })
186
+ },
187
+
188
+ async loadTasks(opts: LoadOptions = {}) {
189
+ if (fixturesMode) return loadFixtures(config, opts)
190
+ const dir = benchDir(config)
191
+ if (!dir) throw new Error(`${config.dirEnv} is required to load official ${config.name} tasks`)
192
+ return loadOfficialTasks(config, dir, opts)
193
+ },
194
+
195
+ async goldArtifact() {
196
+ return undefined
197
+ },
198
+
199
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
200
+ const dir = benchDir(config)
201
+ if (!dir) throw new Error(`${config.dirEnv} is required to judge ${config.name} trajectory artifacts`)
202
+ const meta = readMeta(task, config.name)
203
+ const artifactPath = resolve(artifact.trim())
204
+ await assertPath(artifactPath, 'tau results/trajectory artifact', config.name)
205
+ const report = await scoreOfficialTrajectory(dir, meta, artifactPath)
206
+ const score = typeof report.score === 'number' ? report.score : 0
207
+ return {
208
+ resolved: score === 1,
209
+ score,
210
+ detail: JSON.stringify({ taskId: meta.taskId, domain: meta.domain, count: report.count }),
211
+ }
212
+ },
213
+ }
214
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * tau2-bench adapter (Sierra tau2/tau-bench successor).
3
+ *
4
+ * Worker artifact = a tau2 `results.json`/trajectory file. Judge = tau2's own
5
+ * reward recomputation over that trajectory. A normal final-answer transcript is
6
+ * not a valid artifact for this benchmark.
7
+ */
8
+
9
+ import { join } from 'node:path'
10
+ import { benchRoot } from './_harness'
11
+ import { createTauBenchAdapter, tauResultsOutput } from './tau-bench-shared'
12
+ import type { BenchmarkAdapter } from './types'
13
+
14
+ const FIXTURES = join(benchRoot, 'fixtures', 'tau2-bench.json')
15
+
16
+ export const tau2ResultsOutput = tauResultsOutput
17
+
18
+ export function createTau2BenchAdapter(): BenchmarkAdapter {
19
+ return createTauBenchAdapter({
20
+ name: 'tau2-bench',
21
+ fixturePath: FIXTURES,
22
+ fixturesEnv: 'TAU2_FIXTURES',
23
+ dirEnv: 'TAU2_BENCH_DIR',
24
+ domainEnv: 'TAU2_DOMAIN',
25
+ defaultDomain: 'retail',
26
+ taskIntro: 'Run this tau2 task in the official tau2 text benchmark.',
27
+ installHint:
28
+ 'clone https://github.com/sierra-research/tau2-bench, install its deps in bench/.venv, and set TAU2_BENCH_DIR=/path/to/tau2-bench.',
29
+ })
30
+ }