@tangle-network/agent-bench 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Terminal-Bench adapter. Each task = a Docker environment + an English
3
+ * instruction + a per-task verifier (run-tests.sh + pytest). The worker artifact
4
+ * is the shell script the agent ran to attempt the task; the judge REPLAYS that
5
+ * script in a fresh task container via the Terminal-Bench harness (`tb run` with
6
+ * our ScriptAgent), then the task's own verifier scores the resulting state.
7
+ * Fully deterministic — no LLM judge, no self-authored score.
8
+ *
9
+ * Requires: the bench `.venv` with `terminal-bench` installed + a running Docker
10
+ * daemon (per-task images are built on first run). loadTasks caches the dataset
11
+ * from the Terminal-Bench registry on first run.
12
+ *
13
+ * Process/Docker/report plumbing is shared via ./_harness; this file owns the
14
+ * Terminal-Bench-specific pieces: the Dataset enumeration, the ScriptAgent replay
15
+ * argv, and the results.json shape.
16
+ */
17
+
18
+ import { join } from 'node:path'
19
+ import {
20
+ benchRoot,
21
+ preflightVenvImports,
22
+ readJsonReport,
23
+ runStagedJudge,
24
+ runVenvPython,
25
+ safeRunId,
26
+ stageFile,
27
+ venvBin,
28
+ } from './_harness'
29
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
30
+
31
+ const TB = venvBin('tb')
32
+
33
+ // Pinned dataset: the 0.1.1 core set is patched for terminal-bench >=0.2.4 (the
34
+ // installed CLI) and is the published launch task set. name==version is what `tb
35
+ // run -d` and `tb datasets download -d` both accept.
36
+ const DATASET = 'terminal-bench-core'
37
+ const DATASET_VERSION = '0.1.1'
38
+ const DATASET_REF = `${DATASET}==${DATASET_VERSION}`
39
+
40
+ // Bundled fixture: when no ids/limit are given, load these. hello-world is the
41
+ // fastest deterministic task (prebuilt python image, file-write verifier) so the
42
+ // adapter is runnable without a large pull.
43
+ const FIXTURE_IDS = ['hello-world']
44
+
45
+ // Import path the harness uses to load our replay agent (cwd = benchRoot).
46
+ const SCRIPT_AGENT = 'tb_agents.script_agent:ScriptAgent'
47
+
48
+ interface TbTaskRow {
49
+ id: string
50
+ instruction: string
51
+ task_dir: string
52
+ solution: string | null
53
+ }
54
+
55
+ interface TbReport {
56
+ resolved_ids?: string[]
57
+ results?: Array<{ task_id: string; is_resolved: boolean | null; parser_results?: unknown }>
58
+ }
59
+
60
+ /** Enumerate dataset tasks via tb's Dataset loader (caches from the registry on
61
+ * first run). Reads instruction from each task.yaml and the gold solution. */
62
+ async function loadRows(opts: LoadOptions): Promise<TbTaskRow[]> {
63
+ const ids = opts.ids ?? (opts.limit ? null : FIXTURE_IDS)
64
+ const limit = opts.limit ?? null
65
+ const script = `
66
+ import json, sys
67
+ from pathlib import Path
68
+ from terminal_bench.dataset.dataset import Dataset
69
+ from terminal_bench.handlers.trial_handler import TaskPaths
70
+
71
+ req_ids = json.loads(sys.argv[1]) if sys.argv[1] else None
72
+ limit = json.loads(sys.argv[2]) if sys.argv[2] else None
73
+
74
+ ds = Dataset(name=${JSON.stringify(DATASET)}, version=${JSON.stringify(DATASET_VERSION)}, task_ids=req_ids, n_tasks=limit)
75
+
76
+ import yaml
77
+ out = []
78
+ for task_dir in ds:
79
+ tp = TaskPaths(task_dir)
80
+ cfg = yaml.safe_load(tp.task_config_path.read_text())
81
+ try:
82
+ sol = tp.solution_path.read_text()
83
+ except FileNotFoundError:
84
+ sol = None
85
+ out.append({
86
+ "id": task_dir.name,
87
+ "instruction": cfg["instruction"],
88
+ "task_dir": str(task_dir),
89
+ "solution": sol if (tp.solution_path.suffix == ".sh" if sol is not None else False) else None,
90
+ })
91
+ print(json.dumps(out))
92
+ `
93
+ const stdout = await runVenvPython(script, [ids ? JSON.stringify(ids) : '', limit !== null ? String(limit) : ''])
94
+ return JSON.parse(stdout) as TbTaskRow[]
95
+ }
96
+
97
+ export function createTerminalBenchAdapter(): BenchmarkAdapter {
98
+ return {
99
+ name: 'terminal-bench',
100
+
101
+ async preflight() {
102
+ await preflightVenvImports({
103
+ modules: ['terminal_bench'],
104
+ requireDocker: true,
105
+ fix:
106
+ `Fix: (1) python3 -m venv bench/.venv && bench/.venv/bin/pip install terminal-bench ; ` +
107
+ `(2) ensure the Docker daemon is running (the judge builds per-task images on first run). ` +
108
+ `The ${DATASET_REF} dataset is cached from the Terminal-Bench registry on first loadTasks.`,
109
+ })
110
+ },
111
+
112
+ async loadTasks(opts: LoadOptions = {}) {
113
+ const rows = await loadRows(opts)
114
+ if (rows.length === 0) {
115
+ throw new Error(
116
+ `terminal-bench loadTasks returned no tasks for ${JSON.stringify(opts)} ` +
117
+ `(dataset ${DATASET_REF}). Check the requested ids exist in the dataset.`,
118
+ )
119
+ }
120
+ return rows.map(
121
+ (r): BenchTask => ({
122
+ id: r.id,
123
+ split: DATASET_VERSION,
124
+ prompt: r.instruction,
125
+ metadata: {
126
+ dataset: DATASET,
127
+ datasetVersion: DATASET_VERSION,
128
+ datasetRef: DATASET_REF,
129
+ taskDir: r.task_dir,
130
+ solution: r.solution,
131
+ instruction: r.instruction,
132
+ },
133
+ }),
134
+ )
135
+ },
136
+
137
+ async goldArtifact(task: BenchTask) {
138
+ // Gold = the task's solution.sh (the oracle script). solution.yaml tasks have
139
+ // no shell-script artifact form here, so they return undefined (cannot be
140
+ // verify-judged via the script-replay seam — use a .sh-solution task).
141
+ const sol = task.metadata?.solution
142
+ return typeof sol === 'string' ? sol : undefined
143
+ },
144
+
145
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
146
+ const runId = safeRunId('bench', `${task.id}-${Date.now()}`)
147
+ return runStagedJudge({
148
+ tmpPrefix: 'tbench-',
149
+ bin: TB,
150
+ cwd: () => benchRoot,
151
+ async stage(dir) {
152
+ await stageFile(join(dir, 'attempt.sh'), artifact)
153
+ },
154
+ // The harness builds a fresh task container, runs ScriptAgent (which replays
155
+ // the artifact script), then runs the task's verifier. --no-livestream keeps
156
+ // stdout sane; --cleanup removes the per-run images.
157
+ argv: (dir) => [
158
+ 'run',
159
+ '-d', DATASET_REF,
160
+ '-t', task.id,
161
+ '--agent-import-path', SCRIPT_AGENT,
162
+ '--agent-kwarg', `script_path=${join(dir, 'attempt.sh')}`,
163
+ '--output-path', join(dir, 'runs'),
164
+ '--run-id', runId,
165
+ '--n-concurrent', '1',
166
+ '--no-livestream',
167
+ '--cleanup',
168
+ ],
169
+ async parseReport(dir) {
170
+ const report = await readJsonReport<TbReport>(join(dir, 'runs', runId, 'results.json'))
171
+ const resolved = (report.resolved_ids ?? []).includes(task.id)
172
+ const trial = report.results?.find((r) => r.task_id === task.id)
173
+ return {
174
+ resolved,
175
+ score: resolved ? 1 : 0,
176
+ detail: JSON.stringify(trial?.parser_results ?? report.resolved_ids ?? {}),
177
+ }
178
+ },
179
+ })
180
+ },
181
+ }
182
+ }
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Trata Hedge-Bench adapter (Trata-Inc/trata-hedge-bench). 102 financial
3
+ * analysis tasks across 6 domains (private equity, managed care, industrials,
4
+ * vertical SaaS, REITs, insurance). Each task bundles real earnings-call
5
+ * transcripts, financial statements, press releases, and SEC filings under
6
+ * `environment/data/`; the agent must produce a grounded analysis citing those
7
+ * files.
8
+ *
9
+ * Harbor architecture: the original benchmark runs each task in a Docker
10
+ * container with file-read tools. This adapter skips Harbor and embeds the data
11
+ * files directly in the worker prompt so any router backend can score it. Large
12
+ * files (> 30 KB) are truncated to fit within model context limits. The judge
13
+ * reads the actual files from disk for citation verification.
14
+ *
15
+ * Judge: a 3-stage LLM cascade faithful to grade.py —
16
+ * Task 1: hallucination check (cited-file context + agent answer)
17
+ * Task 2: per-move coverage check (ground truth + agent answer + flagged claims)
18
+ * Task 3: synthesis check (ground truth + agent answer)
19
+ * Score 0–4: themes_covered == num_themes && synthesis → 4, all themes → 3,
20
+ * ≥2 themes → 2, ≥1 theme → 1, else → 0. Normalized to 0..1 for BenchScore;
21
+ * resolved = score 4 (sparse reward).
22
+ *
23
+ * Requires TANGLE_API_KEY and TRATA_BENCH_ROOT=/tmp/trata-hedge-bench.
24
+ * Clone: git clone --depth 1 https://github.com/Trata-Inc/trata-hedge-bench /tmp/trata-hedge-bench
25
+ */
26
+
27
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
28
+ import { join } from 'node:path'
29
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
30
+
31
+ const DEFAULT_BENCH_ROOT = '/tmp/trata-hedge-bench'
32
+ const MAX_FILE_BYTES = 8_000
33
+ // Sandbox API rejects messages > 100K chars; instruction + suffix adds ~500 chars overhead.
34
+ const MAX_DATA_CHARS = 85_000
35
+ const WORKER_SUFFIX = [
36
+ '',
37
+ 'The data files above are your only source. Cite each claim with the filename that supports it.',
38
+ 'Write your complete analysis to the text block that starts with "ANALYSIS:" on a line by itself.',
39
+ 'Take a clear position on the topic. Every factual claim must name the file it comes from.',
40
+ ].join('\n')
41
+
42
+ // Judge model: Gemini 2.5 Pro via router (matches gemini-3.1-pro-preview tier used by grade.py).
43
+ const JUDGE_MODEL = 'gemini-2.5-pro'
44
+
45
+ interface TrataTaskMeta {
46
+ taskDir: string
47
+ taskId: string
48
+ dataDir: string
49
+ groundTruth: string
50
+ gradingTask1: string
51
+ gradingTask2: string
52
+ gradingTask3: string
53
+ /** [(label, n_moves)] in rubric order. */
54
+ themeMoveCounts: Array<{ label: string; nMoves: number }>
55
+ }
56
+
57
+ // ── file reading helpers ──────────────────────────────────────────────────────
58
+
59
+ function readText(p: string, maxBytes = MAX_FILE_BYTES): string {
60
+ try {
61
+ const buf = readFileSync(p)
62
+ if (buf.length <= maxBytes) return buf.toString('utf8')
63
+ return buf.subarray(0, maxBytes).toString('utf8') + `\n[… truncated at ${maxBytes} bytes …]`
64
+ } catch {
65
+ return `[unreadable: ${p}]`
66
+ }
67
+ }
68
+
69
+ function listFilesRec(dir: string): string[] {
70
+ const out: string[] = []
71
+ try {
72
+ for (const name of readdirSync(dir)) {
73
+ const full = join(dir, name)
74
+ try {
75
+ if (statSync(full).isDirectory()) out.push(...listFilesRec(full))
76
+ else out.push(full)
77
+ } catch {
78
+ // skip unreadable entries
79
+ }
80
+ }
81
+ } catch {
82
+ // dir absent
83
+ }
84
+ return out.sort()
85
+ }
86
+
87
+ function buildDataBlock(dataDir: string): string {
88
+ const files = listFilesRec(dataDir)
89
+ if (files.length === 0) return '(no data files found)'
90
+ // Prioritise: company profiles → earnings calls → primary-ticker financials →
91
+ // sec filings → press releases → peers. Stop once total chars hit MAX_DATA_CHARS.
92
+ const priority = (f: string) => {
93
+ if (f.includes('company_profiles')) return 0
94
+ if (f.includes('earnings_call')) return 1
95
+ if (/financials\/(income|cash_flow|balance)/.test(f) && !/bam_|bx_|kkr_|ares_|cg_|owl_|fsk_|cat_|de_|tdg_|car_/.test(f)) return 2
96
+ if (f.includes('sec_filings')) return 3
97
+ if (f.includes('investor_pres')) return 4
98
+ if (f.includes('press_release')) return 5
99
+ return 6
100
+ }
101
+ const sorted = [...files].sort((a, b) => priority(a) - priority(b) || a.localeCompare(b))
102
+ const blocks: string[] = []
103
+ let total = 0
104
+ for (const f of sorted) {
105
+ const rel = f.slice(dataDir.length + 1)
106
+ const content = readText(f)
107
+ const chunk = `\n=== FILE: data/${rel} ===\n${content}`
108
+ if (total + chunk.length > MAX_DATA_CHARS) {
109
+ blocks.push(`\n[… ${sorted.length - blocks.length} more files omitted to stay within context limit …]`)
110
+ break
111
+ }
112
+ blocks.push(chunk)
113
+ total += chunk.length
114
+ }
115
+ return blocks.join('')
116
+ }
117
+
118
+ function buildCitedDataBlock(answerText: string, dataDir: string): string {
119
+ const files = listFilesRec(dataDir)
120
+ const cited: string[] = []
121
+ for (const f of files) {
122
+ const rel = f.slice(dataDir.length + 1)
123
+ const name = rel.split('/').pop() ?? ''
124
+ if (answerText.includes(rel) || answerText.includes(name)) cited.push(f)
125
+ }
126
+ if (cited.length === 0) {
127
+ return '(The agent did not cite any data files, or no cited file could be located.)'
128
+ }
129
+ const blocks: string[] = []
130
+ for (const f of cited) {
131
+ const rel = f.slice(dataDir.length + 1)
132
+ blocks.push(`\n=== FILE: data/${rel} ===\n${readText(f)}`)
133
+ }
134
+ return blocks.join('')
135
+ }
136
+
137
+ // ── ground-truth parser ───────────────────────────────────────────────────────
138
+
139
+ function parseThemeMoveCounts(groundTruth: string): Array<{ label: string; nMoves: number }> {
140
+ const lines = groundTruth.split('\n')
141
+ const themeStarts = lines.reduce<number[]>((acc, l, i) => {
142
+ if (/^\d+\./.test(l.trim())) acc.push(i)
143
+ return acc
144
+ }, [])
145
+ return themeStarts.map((start, k) => {
146
+ const end = k + 1 < themeStarts.length ? themeStarts[k + 1] : lines.length
147
+ const block = lines.slice(start, end).join('\n')
148
+ const label = lines[start].replace(/^\d+\.\s*/, '').trim()
149
+ const nMoves = (block.match(/^\s*\[[a-z]\]\s+/gm) ?? []).length
150
+ return { label, nMoves }
151
+ })
152
+ }
153
+
154
+ function coverageThreshold(nMoves: number): number {
155
+ return Math.max(1, Math.min(nMoves - 1, 3))
156
+ }
157
+
158
+ // ── router judge calls ────────────────────────────────────────────────────────
159
+
160
+ interface JudgeRouter {
161
+ baseUrl: string
162
+ key: string
163
+ model: string
164
+ }
165
+
166
+ function judgeRouter(): JudgeRouter {
167
+ const key = process.env.TANGLE_API_KEY
168
+ if (!key) throw new Error('TANGLE_API_KEY is required for the Trata hedge-bench judge')
169
+ const model = process.env.JUDGE_MODEL ?? JUDGE_MODEL
170
+ const baseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
171
+ return { baseUrl, key, model }
172
+ }
173
+
174
+ function parseJsonFallback(raw: string): unknown {
175
+ let s = raw.trim()
176
+ if (s.startsWith('```')) s = s.split('\n').slice(1).join('\n')
177
+ if (s.endsWith('```')) s = s.slice(0, s.lastIndexOf('```'))
178
+ s = s.replace(/,(\s*[}\]])/g, '$1').trim()
179
+ try {
180
+ return JSON.parse(s)
181
+ } catch {
182
+ try {
183
+ const m = s.match(/\{[\s\S]*\}/)
184
+ if (m) return JSON.parse(m[0])
185
+ } catch {
186
+ // fallthrough
187
+ }
188
+ }
189
+ return null
190
+ }
191
+
192
+ async function callJudge(router: JudgeRouter, prompt: string, maxAttempts = 2): Promise<unknown> {
193
+ for (let i = 0; i < maxAttempts; i++) {
194
+ const res = await fetch(`${router.baseUrl}/chat/completions`, {
195
+ method: 'POST',
196
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${router.key}` },
197
+ body: JSON.stringify({
198
+ model: router.model,
199
+ temperature: 0,
200
+ max_tokens: 16384,
201
+ messages: [{ role: 'user', content: prompt }],
202
+ }),
203
+ })
204
+ if (!res.ok) {
205
+ if (i < maxAttempts - 1) continue
206
+ throw new Error(`Trata judge HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`)
207
+ }
208
+ const body = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
209
+ const content = body.choices?.[0]?.message?.content
210
+ if (typeof content !== 'string') continue
211
+ const parsed = parseJsonFallback(content)
212
+ if (parsed !== null) return parsed
213
+ }
214
+ return null
215
+ }
216
+
217
+ interface Task1Result {
218
+ hallucinations_detected?: boolean | string
219
+ unverifiable_claims?: Array<{ claim: string; cited_file?: string; issue?: string; evidence_check?: string }>
220
+ }
221
+
222
+ interface Task2ThemeResult {
223
+ label?: string
224
+ moves_hit?: string[]
225
+ moves_missed?: string[]
226
+ moves_tainted?: string[]
227
+ move_reasoning?: Record<string, string>
228
+ }
229
+
230
+ interface Task2Result {
231
+ themes?: Task2ThemeResult[]
232
+ }
233
+
234
+ interface Task3Result {
235
+ synthesis_found?: string | null
236
+ }
237
+
238
+ async function task1HallucinationCheck(
239
+ meta: TrataTaskMeta,
240
+ answer: string,
241
+ router: JudgeRouter,
242
+ ): Promise<Task1Result> {
243
+ const citedBlock = buildCitedDataBlock(answer, meta.dataDir)
244
+ const dataContext =
245
+ 'The block below contains the contents of the data files the agent cited in their answer. ' +
246
+ 'Treat this as the ground truth for factual verification.\n\n' +
247
+ `<cited_data>\n${citedBlock}\n</cited_data>`
248
+ const prompt = meta.gradingTask1
249
+ .replace('{data_context}', dataContext)
250
+ .replace('{agent_answer}', answer)
251
+ const result = await callJudge(router, prompt)
252
+ return (result as Task1Result) ?? { hallucinations_detected: false, unverifiable_claims: [] }
253
+ }
254
+
255
+ async function task2PerMoveCheck(
256
+ meta: TrataTaskMeta,
257
+ answer: string,
258
+ flaggedClaims: Task1Result['unverifiable_claims'],
259
+ router: JudgeRouter,
260
+ ): Promise<Task2Result> {
261
+ const claimsBlock =
262
+ flaggedClaims && flaggedClaims.length > 0
263
+ ? JSON.stringify(flaggedClaims, null, 2)
264
+ : '(No claims were flagged as hallucinations.)'
265
+ const prompt = meta.gradingTask2
266
+ .replace('{ground_truth}', meta.groundTruth)
267
+ .replace('{agent_answer}', answer)
268
+ .replace('{flagged_claims_block}', claimsBlock)
269
+ .replace('{num_themes}', String(meta.themeMoveCounts.length))
270
+ const result = await callJudge(router, prompt)
271
+ return (result as Task2Result) ?? { themes: [] }
272
+ }
273
+
274
+ async function task3SynthesisCheck(
275
+ meta: TrataTaskMeta,
276
+ answer: string,
277
+ router: JudgeRouter,
278
+ ): Promise<Task3Result> {
279
+ const prompt = meta.gradingTask3
280
+ .replace('{ground_truth}', meta.groundTruth)
281
+ .replace('{agent_answer}', answer)
282
+ const result = await callJudge(router, prompt)
283
+ return (result as Task3Result) ?? { synthesis_found: null }
284
+ }
285
+
286
+ async function runTriataJudge(meta: TrataTaskMeta, answer: string, router: JudgeRouter): Promise<BenchScore> {
287
+ if (!answer.trim()) {
288
+ return { resolved: false, score: 0, detail: JSON.stringify({ error: 'empty answer' }) }
289
+ }
290
+
291
+ // Stage 1: hallucination check (serial — feeds into stage 2).
292
+ const t1 = await task1HallucinationCheck(meta, answer, router)
293
+ const flaggedClaims = t1.unverifiable_claims ?? []
294
+
295
+ // Stages 2 + 3 in parallel.
296
+ const [t2, t3] = await Promise.all([
297
+ task2PerMoveCheck(meta, answer, flaggedClaims, router),
298
+ task3SynthesisCheck(meta, answer, router),
299
+ ])
300
+
301
+ const judgeThemes = t2.themes ?? []
302
+ const themesHit: string[] = []
303
+ const themesMissed: string[] = []
304
+
305
+ for (let i = 0; i < meta.themeMoveCounts.length; i++) {
306
+ const { label, nMoves } = meta.themeMoveCounts[i]
307
+ const judge = judgeThemes[i] ?? {}
308
+ const movesHit = (judge.moves_hit ?? []).map((m: string) => m.replace(/[\[\]]/g, ''))
309
+ const tainted = new Set((judge.moves_tainted ?? []).map((m: string) => m.replace(/[\[\]]/g, '')))
310
+ const validHit = movesHit.filter((m: string) => !tainted.has(m))
311
+ const threshold = coverageThreshold(nMoves)
312
+ if (validHit.length >= threshold) themesHit.push(label)
313
+ else themesMissed.push(label)
314
+ }
315
+
316
+ const numThemes = meta.themeMoveCounts.length
317
+ const synth = t3.synthesis_found
318
+ const hasSynthesis = typeof synth === 'string' && synth.length > 0 && synth !== 'null'
319
+
320
+ let rawScore: number
321
+ if (numThemes === 0) rawScore = 0
322
+ else if (themesHit.length === numThemes && hasSynthesis) rawScore = 4
323
+ else if (themesHit.length === numThemes) rawScore = 3
324
+ else if (themesHit.length >= 2) rawScore = 2
325
+ else if (themesHit.length >= 1) rawScore = 1
326
+ else rawScore = 0
327
+
328
+ const hallRaw = t1.hallucinations_detected
329
+ const hallucinations = hallRaw === true || hallRaw === 'true'
330
+
331
+ return {
332
+ resolved: rawScore === 4,
333
+ score: numThemes > 0 ? rawScore / 4 : 0,
334
+ detail: JSON.stringify({
335
+ rawScore,
336
+ themesHit,
337
+ themesMissed,
338
+ hallucinations,
339
+ unverifiableClaims: flaggedClaims.length,
340
+ synthesis: synth ?? null,
341
+ judgeModel: router.model,
342
+ }),
343
+ }
344
+ }
345
+
346
+ // ── task loading ──────────────────────────────────────────────────────────────
347
+
348
+ function benchRoot(): string {
349
+ return process.env.TRATA_BENCH_ROOT ?? DEFAULT_BENCH_ROOT
350
+ }
351
+
352
+ function loadTask(taskId: string): { task: BenchTask; meta: TrataTaskMeta } {
353
+ const taskDir = join(benchRoot(), 'environments', taskId)
354
+ const dataDir = join(taskDir, 'environment', 'data')
355
+ const testsDir = join(taskDir, 'tests')
356
+
357
+ const instruction = readText(join(taskDir, 'instruction.md'), Infinity)
358
+ const dataBlock = buildDataBlock(dataDir)
359
+ const groundTruth = readText(join(testsDir, 'ground_truth.txt'), Infinity)
360
+ const gradingTask1 = readText(join(testsDir, 'grading_prompt_task1.md'), Infinity)
361
+ const gradingTask2 = readText(join(testsDir, 'grading_prompt_task2.md'), Infinity)
362
+ const gradingTask3 = readText(join(testsDir, 'grading_prompt_task3.md'), Infinity)
363
+
364
+ const themeMoveCounts = parseThemeMoveCounts(groundTruth)
365
+
366
+ // Rewrite the instruction so the agent knows data is inline (no /app/data/ FS).
367
+ const prompt = [
368
+ instruction
369
+ .replace(
370
+ "You are a financial analyst with access to the data in `/app/data/`.",
371
+ 'You are a financial analyst. The relevant financial data files are provided inline below.',
372
+ )
373
+ .replace(/Write your full analysis to `\/app\/answer\.txt`\.?/, 'Write your full analysis below.')
374
+ .replace(/`\/app\/data\/`/g, 'the data files below'),
375
+ '',
376
+ '## Data files (inline)',
377
+ dataBlock,
378
+ WORKER_SUFFIX,
379
+ ].join('\n')
380
+
381
+ const meta: TrataTaskMeta = {
382
+ taskDir,
383
+ taskId,
384
+ dataDir,
385
+ groundTruth,
386
+ gradingTask1,
387
+ gradingTask2,
388
+ gradingTask3,
389
+ themeMoveCounts,
390
+ }
391
+
392
+ // Store meta in the BenchTask so the judge can access it without re-reading.
393
+ const task: BenchTask = {
394
+ id: taskId,
395
+ prompt,
396
+ metadata: meta as unknown as Record<string, unknown>,
397
+ }
398
+
399
+ return { task, meta }
400
+ }
401
+
402
+ function readMeta(task: BenchTask): TrataTaskMeta {
403
+ const md = task.metadata
404
+ if (!md || typeof md.taskDir !== 'string' || typeof md.groundTruth !== 'string') {
405
+ throw new Error(`Trata task ${task.id} missing judge metadata — was it loaded by this adapter?`)
406
+ }
407
+ return md as unknown as TrataTaskMeta
408
+ }
409
+
410
+ /** List all task ids from the environments directory. */
411
+ function listTaskIds(root: string): string[] {
412
+ try {
413
+ return readdirSync(join(root, 'environments'))
414
+ .filter((name) => {
415
+ try {
416
+ return statSync(join(root, 'environments', name)).isDirectory()
417
+ } catch {
418
+ return false
419
+ }
420
+ })
421
+ .sort()
422
+ } catch (err) {
423
+ throw new Error(
424
+ `Trata benchmark environments not found at ${join(root, 'environments')}: ` +
425
+ `${err instanceof Error ? err.message : err}.\n` +
426
+ `Clone the repo: git clone --depth 1 https://github.com/Trata-Inc/trata-hedge-bench ${root}`,
427
+ )
428
+ }
429
+ }
430
+
431
+ // ── extract agent analysis from artifact ─────────────────────────────────────
432
+
433
+ function extractAnalysis(artifact: string): string {
434
+ // Model may write "ANALYSIS:" as its very first token (no leading newline) or
435
+ // after some preamble. Both are valid; fail-closed on absence.
436
+ if (artifact.startsWith('ANALYSIS:')) return artifact.slice('ANALYSIS:'.length).trim()
437
+ const marker = artifact.indexOf('\nANALYSIS:')
438
+ if (marker !== -1) return artifact.slice(marker + '\nANALYSIS:'.length).trim()
439
+ return ''
440
+ }
441
+
442
+ // ── adapter factory ───────────────────────────────────────────────────────────
443
+
444
+ export function createTrataHedgeAdapter(): BenchmarkAdapter {
445
+ return {
446
+ name: 'trata-hedge',
447
+
448
+ async preflight() {
449
+ const root = benchRoot()
450
+ const envDir = join(root, 'environments')
451
+ try {
452
+ const stat = statSync(envDir)
453
+ if (!stat.isDirectory()) throw new Error('not a directory')
454
+ } catch {
455
+ throw new Error(
456
+ `Trata hedge-bench not found at ${envDir}.\n` +
457
+ `Clone: git clone --depth 1 https://github.com/Trata-Inc/trata-hedge-bench ${root}`,
458
+ )
459
+ }
460
+ // Validate judge router connectivity.
461
+ judgeRouter()
462
+ const ids = listTaskIds(root)
463
+ if (ids.length === 0) throw new Error(`No task directories found under ${envDir}`)
464
+ console.warn(
465
+ `[trata-hedge] ${ids.length} tasks loaded from ${root}. ` +
466
+ `Worker model reads embedded data inline (Harbor/Docker not required). ` +
467
+ `Judge uses ${process.env.JUDGE_MODEL ?? JUDGE_MODEL} via Tangle router.`,
468
+ )
469
+ },
470
+
471
+ async loadTasks(opts: LoadOptions = {}): Promise<BenchTask[]> {
472
+ const root = benchRoot()
473
+ let ids = listTaskIds(root)
474
+ if (opts.ids) {
475
+ const want = new Set(opts.ids)
476
+ ids = ids.filter((id) => want.has(id))
477
+ } else if (opts.limit !== undefined) {
478
+ ids = ids.slice(0, opts.limit)
479
+ }
480
+ return ids.map((id) => loadTask(id).task)
481
+ },
482
+
483
+ async goldArtifact(_task: BenchTask): Promise<string | undefined> {
484
+ // No single gold answer: the score comes from rubric theme coverage, not
485
+ // string-match against a reference response. Return undefined — the bench
486
+ // harness will skip judge self-verification for this adapter.
487
+ return undefined
488
+ },
489
+
490
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
491
+ const meta = readMeta(task)
492
+ const answer = extractAnalysis(artifact)
493
+ return runTriataJudge(meta, answer, judgeRouter())
494
+ },
495
+ }
496
+ }