@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,243 @@
1
+ /**
2
+ * Trata hedge-bench gate — direct router completions (no sandbox/opencode).
3
+ *
4
+ * Trata is a text-analysis benchmark: each task is a financial analysis brief
5
+ * over embedded source docs. The worker is a single router completion (not an
6
+ * agentic loop), which is appropriate here: tasks are scored by an LLM judge on
7
+ * theme coverage, not on code execution or tool use.
8
+ *
9
+ * Usage:
10
+ * BENCH=trata-hedge WORKER_MODEL=deepseek-v4-flash JUDGE_MODEL=gemini-2.5-flash \
11
+ * TRATA_BENCH_ROOT=/tmp/trata-hedge-bench N=10 CONCURRENCY=5 \
12
+ * dotenvx run -f ~/company/devops/secrets/agent-state.env -- \
13
+ * pnpm exec tsx bench/src/trata-gate.mts
14
+ *
15
+ * Key env vars:
16
+ * BENCH adapter key (default: trata-hedge)
17
+ * WORKER_MODEL model for the analysis completion (default: deepseek-v4-flash)
18
+ * JUDGE_MODEL model for the 3-stage judge (default: gemini-2.5-pro)
19
+ * TRATA_BENCH_ROOT path to cloned trata-hedge-bench (default: /tmp/trata-hedge-bench)
20
+ * N how many tasks to run (default: 10; 0 = all 102)
21
+ * CONCURRENCY parallel worker slots (default: 5)
22
+ * IDS comma-separated task ids to run a specific subset
23
+ * ROUTER_BASE router base URL (default: https://router.tangle.tools/v1)
24
+ * CORPUS path to write JSONL run records (optional)
25
+ *
26
+ * Score is 0–4 / 4 = 0–100%; resolved = 4/4 (all themes + synthesis = sparse reward).
27
+ * Partial credit (1-3) is reported in the score distribution.
28
+ */
29
+
30
+ import { appendFileSync } from 'node:fs'
31
+ import { resolveAdapter } from './adapters'
32
+ import type { BenchScore, BenchTask } from './benchmarks/types'
33
+ import { runPool } from './run-pool'
34
+
35
+ function must(name: string): string {
36
+ const v = process.env[name]
37
+ if (!v) throw new Error(`env ${name} is required`)
38
+ return v
39
+ }
40
+
41
+ // GEPA-optimised surface — +8.6pp holdout lift over the minimal baseline on deepseek-v4-flash
42
+ // (2 independent runs, CI [0, 12.5pp]; gate holds for caution, surface ships as default).
43
+ const ANALYST_SYSTEM = process.env.SYSTEM_PROMPT ?? [
44
+ 'You are a senior financial analyst producing a structured investment memo.',
45
+ 'Begin your response with exactly "ANALYSIS:" on its own line (nothing before it), then write your full analysis.',
46
+ 'Structure your analysis with clearly labeled sections for each distinct analytical theme',
47
+ '(e.g., valuation, capital allocation, competitive dynamics, risk factors)',
48
+ 'so that no major investment consideration is merged or omitted.',
49
+ 'Benchmark the company explicitly against named sector peers, citing specific metrics such as',
50
+ 'EV/EBITDA, P/E ratios, margin differentials, and growth premiums from the peer financial files.',
51
+ 'Every factual claim must cite the filename (e.g., "per earnings_call/apo_q4_2025_earnings_call.txt").',
52
+ 'Identify and verbatim-cite specific numerical targets from management guidance such as',
53
+ 'earnings per share targets, margin percentages, growth rates, or AUM figures rather than paraphrasing approximately.',
54
+ 'When evaluating capital allocation options, explicitly compute implied returns or internal rates of return (IRRs),',
55
+ 'showing the arithmetic using price levels and targets from the source data.',
56
+ 'Take a clear, decisive position — do not hedge with "it depends". Reconcile conflicting data points explicitly.',
57
+ ].join(' ')
58
+
59
+ async function workerComplete(
60
+ task: BenchTask,
61
+ cfg: { routerBaseUrl: string; routerKey: string; model: string; timeoutMs: number },
62
+ ): Promise<{ answer: string; inputTokens: number; outputTokens: number; durationMs: number }> {
63
+ const startedAt = Date.now()
64
+ const res = await fetch(`${cfg.routerBaseUrl}/chat/completions`, {
65
+ method: 'POST',
66
+ signal: AbortSignal.timeout(cfg.timeoutMs),
67
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.routerKey}` },
68
+ body: JSON.stringify({
69
+ model: cfg.model,
70
+ temperature: 0,
71
+ max_tokens: 4096,
72
+ messages: [
73
+ { role: 'system', content: ANALYST_SYSTEM },
74
+ { role: 'user', content: task.prompt },
75
+ ],
76
+ }),
77
+ })
78
+ if (!res.ok) {
79
+ const body = (await res.text()).slice(0, 300)
80
+ throw new Error(`router ${res.status} for ${task.id}: ${body}`)
81
+ }
82
+ const j = (await res.json()) as {
83
+ choices?: Array<{ message?: { content?: string } }>
84
+ usage?: { prompt_tokens?: number; completion_tokens?: number }
85
+ }
86
+ const answer = j.choices?.[0]?.message?.content ?? ''
87
+ return {
88
+ answer,
89
+ inputTokens: j.usage?.prompt_tokens ?? 0,
90
+ outputTokens: j.usage?.completion_tokens ?? 0,
91
+ durationMs: Date.now() - startedAt,
92
+ }
93
+ }
94
+
95
+ interface TaskResult {
96
+ taskId: string
97
+ answer: string
98
+ score: BenchScore
99
+ rawScore: number
100
+ inputTokens: number
101
+ outputTokens: number
102
+ durationMs: number
103
+ error?: string
104
+ }
105
+
106
+ async function runTask(
107
+ task: BenchTask,
108
+ workerCfg: { routerBaseUrl: string; routerKey: string; model: string; timeoutMs: number },
109
+ adapter: ReturnType<typeof resolveAdapter>,
110
+ ): Promise<TaskResult> {
111
+ let answer = ''
112
+ let inputTokens = 0
113
+ let outputTokens = 0
114
+ let durationMs = 0
115
+ let error: string | undefined
116
+
117
+ try {
118
+ const result = await workerComplete(task, workerCfg)
119
+ answer = result.answer
120
+ inputTokens = result.inputTokens
121
+ outputTokens = result.outputTokens
122
+ durationMs = result.durationMs
123
+ } catch (err) {
124
+ error = err instanceof Error ? err.message : String(err)
125
+ }
126
+
127
+ const score = answer.trim()
128
+ ? await adapter.judge(task, answer).catch((err) => ({
129
+ resolved: false as const,
130
+ score: 0,
131
+ detail: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
132
+ }))
133
+ : { resolved: false as const, score: 0, detail: JSON.stringify({ error: error ?? 'empty answer' }) }
134
+
135
+ const detail = score.detail ? (JSON.parse(score.detail) as { rawScore?: number }) : {}
136
+ return {
137
+ taskId: task.id,
138
+ answer,
139
+ score,
140
+ rawScore: detail.rawScore ?? (score.resolved ? 4 : 0),
141
+ inputTokens,
142
+ outputTokens,
143
+ durationMs,
144
+ error,
145
+ }
146
+ }
147
+
148
+ async function main(): Promise<void> {
149
+ const benchName = process.env.BENCH ?? 'trata-hedge'
150
+ const adapter = resolveAdapter(benchName)
151
+ await adapter.preflight()
152
+
153
+ const n = Number(process.env.N ?? 10)
154
+ const concurrency = Number(process.env.CONCURRENCY ?? 5)
155
+ const model = process.env.WORKER_MODEL ?? 'deepseek-v4-flash'
156
+ const routerBase = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
157
+ const routerKey = must('TANGLE_API_KEY')
158
+ const corpusPath = process.env.CORPUS
159
+ const idsEnv = process.env.IDS
160
+
161
+ const loadOpts = idsEnv
162
+ ? { ids: idsEnv.split(',').map((s) => s.trim()).filter(Boolean) }
163
+ : n > 0
164
+ ? { limit: n }
165
+ : {}
166
+ const tasks = await adapter.loadTasks(loadOpts)
167
+ if (tasks.length === 0) throw new Error('no tasks loaded')
168
+
169
+ const workerCfg = { routerBaseUrl: routerBase, routerKey, model, timeoutMs: 180_000 }
170
+
171
+ console.log(
172
+ `trata-gate: ${tasks.length} tasks | model=${model} | judge=${process.env.JUDGE_MODEL ?? 'gemini-2.5-pro'} | concurrency=${concurrency}`,
173
+ )
174
+
175
+ let done = 0
176
+ const results: TaskResult[] = []
177
+ await runPool(tasks, concurrency, async (task) => {
178
+ const r = await runTask(task, workerCfg, adapter)
179
+ results.push(r)
180
+ done++
181
+ const icon = r.rawScore === 4 ? '✓' : r.rawScore >= 2 ? '~' : '·'
182
+ process.stdout.write(
183
+ ` [${done}/${tasks.length}] ${task.id.slice(0, 60)}: ${icon} (${r.rawScore}/4)\n`,
184
+ )
185
+ return r
186
+ })
187
+
188
+ // Score distribution
189
+ const byRaw = [0, 0, 0, 0, 0] // index = rawScore 0-4
190
+ let totalInputTok = 0
191
+ let totalOutputTok = 0
192
+ let totalMs = 0
193
+ for (const r of results) {
194
+ byRaw[Math.min(r.rawScore, 4)]++
195
+ totalInputTok += r.inputTokens
196
+ totalOutputTok += r.outputTokens
197
+ totalMs += r.durationMs
198
+ }
199
+ const resolved = byRaw[4] ?? 0
200
+ const partial = (byRaw[2] ?? 0) + (byRaw[3] ?? 0)
201
+ const n_ = results.length
202
+ const pct = (x: number) => (n_ > 0 ? `${((x / n_) * 100).toFixed(1)}%` : 'n/a')
203
+ const meanScore = n_ > 0 ? results.reduce((s, r) => s + r.score.score, 0) / n_ : 0
204
+
205
+ console.log(`\n=== ${benchName} — trata-gate (n=${n_}, model=${model}) ===`)
206
+ console.log(` resolved (4/4): ${pct(resolved)} (${resolved}/${n_})`)
207
+ console.log(` partial credit (2-3): ${pct(partial)} (${partial}/${n_})`)
208
+ console.log(` mean score (0..1): ${meanScore.toFixed(3)}`)
209
+ console.log(` score dist [0..4]: ${byRaw.join(' | ')}`)
210
+ console.log(
211
+ ` tokens (in/out): ${(totalInputTok / 1000).toFixed(0)}k / ${(totalOutputTok / 1000).toFixed(0)}k`,
212
+ )
213
+ console.log(` wall time: ${(totalMs / 1000).toFixed(0)}s total`)
214
+ console.log(` errors: ${results.filter((r) => r.error).length}`)
215
+
216
+ if (corpusPath) {
217
+ for (const r of results) {
218
+ appendFileSync(
219
+ corpusPath,
220
+ JSON.stringify({
221
+ benchmark: benchName,
222
+ instanceId: r.taskId,
223
+ condition: `blind@1`,
224
+ model,
225
+ resolved: r.score.resolved,
226
+ rawScore: r.rawScore,
227
+ score: r.score.score,
228
+ detail: r.score.detail,
229
+ inputTokens: r.inputTokens,
230
+ outputTokens: r.outputTokens,
231
+ durationMs: r.durationMs,
232
+ ...(r.error ? { error: r.error } : {}),
233
+ }) + '\n',
234
+ )
235
+ }
236
+ console.log(`\ncorpus written → ${corpusPath}`)
237
+ }
238
+ }
239
+
240
+ main().catch((err) => {
241
+ console.error(err instanceof Error ? (err.stack ?? err.message) : String(err))
242
+ process.exit(1)
243
+ })
@@ -0,0 +1,434 @@
1
+ /**
2
+ * trata-gepa — selfImprove (GEPA) outer loop for Trata hedge-bench.
3
+ *
4
+ * The optimization surface is the system prompt given to the financial analyst
5
+ * worker. GEPA reflects on which rubric themes were missed across training tasks
6
+ * and proposes improved system prompts — learning to instruct the model to
7
+ * extract specific quantitative claims, cover multiple analytical themes, and
8
+ * cite named evidence. Gated on a frozen holdout.
9
+ *
10
+ * The surface evolves beyond a bare system instruction: GEPA naturally discovers
11
+ * that it can add few-shot analytical patterns, calculation templates, and
12
+ * structured coverage checklists — effectively skill-creating without
13
+ * hand-engineering. Set K_ROUNDS=2 to add a self-critique refine pass.
14
+ *
15
+ * Usage:
16
+ * TRATA_BENCH_ROOT=/tmp/trata-hedge-bench \
17
+ * JUDGE_MODEL=gemini-2.5-flash WORKER_MODEL=deepseek-v4-flash \
18
+ * REFLECT_MODEL=gemini-2.5-pro \
19
+ * TRAIN_N=70 HOLDOUT_N=32 GENS=2 POP=3 CONCURRENCY=8 \
20
+ * dotenvx run -f ~/company/devops/secrets/agent-state.env -- \
21
+ * pnpm exec tsx bench/src/trata-gepa.mts
22
+ *
23
+ * Key env vars:
24
+ * TRATA_BENCH_ROOT cloned trata-hedge-bench (default /tmp/trata-hedge-bench)
25
+ * WORKER_MODEL analyst model (default deepseek-v4-flash)
26
+ * JUDGE_MODEL judge model in trata adapter (default gemini-2.5-flash)
27
+ * REFLECT_MODEL GEPA reflection model (default gemini-2.5-pro)
28
+ * TRAIN_N training tasks (default 70)
29
+ * HOLDOUT_N frozen holdout tasks (default 32)
30
+ * GENS optimization generations (default 2)
31
+ * POP candidates per generation (default 3)
32
+ * REPS reps per scenario (default 1)
33
+ * CONCURRENCY parallel worker slots (default 8)
34
+ * K_ROUNDS 1=single-shot, 2=analysis+self-critique (default 1)
35
+ * BASELINE_DIRECTIVE override the starting system prompt
36
+ * CORPUS path to write JSONL run records (optional)
37
+ */
38
+
39
+ import { selfImprove } from '@tangle-network/agent-eval/contract'
40
+ import type { CampaignResult, JudgeConfig, JudgeScore, Scenario } from '@tangle-network/agent-eval/campaign'
41
+ import { heldoutSignificance, inMemoryCampaignStorage, pairHoldout } from '@tangle-network/agent-eval/campaign'
42
+ import { appendFileSync, writeFileSync } from 'node:fs'
43
+ import { createTrataHedgeAdapter } from './benchmarks/trata-hedge'
44
+ import type { BenchTask } from './benchmarks/types'
45
+
46
+ interface TrataScenario extends Scenario {
47
+ task: BenchTask
48
+ }
49
+
50
+ interface DiagnosedFinding {
51
+ claim: string
52
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
53
+ area?: string
54
+ recommended_action?: string
55
+ }
56
+
57
+ function must(name: string): string {
58
+ const v = process.env[name]
59
+ if (!v) throw new Error(`env ${name} is required`)
60
+ return v
61
+ }
62
+
63
+ // GEPA-optimised baseline — the best surface found across 9 runs (+8.6pp on holdout, 2 independent
64
+ // confirmations). Future GEPA runs start from here; BASELINE_DIRECTIVE overrides if you want to
65
+ // experiment from a different starting point.
66
+ const DEFAULT_TRATA_SYSTEM = [
67
+ 'You are a senior financial analyst producing a structured investment memo.',
68
+ 'Begin your response with exactly "ANALYSIS:" on its own line (nothing before it), then write your full analysis.',
69
+ 'Structure your analysis with clearly labeled sections for each distinct analytical theme',
70
+ '(e.g., valuation, capital allocation, competitive dynamics, risk factors)',
71
+ 'so that no major investment consideration is merged or omitted.',
72
+ 'Benchmark the company explicitly against named sector peers, citing specific metrics such as',
73
+ 'EV/EBITDA, P/E ratios, margin differentials, and growth premiums from the peer financial files.',
74
+ 'Every factual claim must cite the filename (e.g., "per earnings_call/apo_q4_2025_earnings_call.txt").',
75
+ 'Identify and verbatim-cite specific numerical targets from management guidance such as',
76
+ 'earnings per share targets, margin percentages, growth rates, or AUM figures rather than paraphrasing approximately.',
77
+ 'When evaluating capital allocation options, explicitly compute implied returns or internal rates of return (IRRs),',
78
+ 'showing the arithmetic using price levels and targets from the source data.',
79
+ 'Take a clear, decisive position — do not hedge with "it depends". Reconcile conflicting data points explicitly.',
80
+ ].join(' ')
81
+
82
+ // Refine round instruction — used only when K_ROUNDS >= 2.
83
+ const REFINE_INSTRUCTION = [
84
+ 'Review your analysis above. For each distinct analytical theme in the investment brief,',
85
+ 'verify you have a dedicated section with at least one specific quantitative claim (a named figure,',
86
+ 'percentage, ratio, or calculation) from the cited data files. Add any missing themes.',
87
+ 'Rewrite any section that only gestures at a theme without a specific supporting data point.',
88
+ ].join(' ')
89
+
90
+ async function chatComplete(
91
+ baseUrl: string,
92
+ key: string,
93
+ model: string,
94
+ messages: Array<{ role: string; content: string }>,
95
+ ): Promise<{ content: string; usage?: { input: number; output: number } }> {
96
+ const res = await fetch(`${baseUrl}/chat/completions`, {
97
+ method: 'POST',
98
+ signal: AbortSignal.timeout(180_000),
99
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
100
+ body: JSON.stringify({ model, temperature: 0, max_tokens: 4096, messages }),
101
+ })
102
+ if (!res.ok) throw new Error(`router ${res.status}: ${(await res.text()).slice(0, 300)}`)
103
+ const j = (await res.json()) as {
104
+ choices?: Array<{ message?: { content?: string } }>
105
+ usage?: { prompt_tokens?: number; completion_tokens?: number }
106
+ }
107
+ const content = j.choices?.[0]?.message?.content ?? ''
108
+ const usage =
109
+ j.usage?.prompt_tokens != null
110
+ ? { input: j.usage.prompt_tokens, output: j.usage.completion_tokens ?? 0 }
111
+ : undefined
112
+ return { content, usage }
113
+ }
114
+
115
+ function parseFindings(content: string): DiagnosedFinding[] {
116
+ // Balanced-bracket scan so `]` inside string values doesn't terminate early.
117
+ const startIdx = content.indexOf('[')
118
+ if (startIdx < 0) return []
119
+ let depth = 0, inString = false, endIdx = -1
120
+ for (let i = startIdx; i < content.length; i++) {
121
+ const ch = content[i]
122
+ if (inString) {
123
+ if (ch === '\\') { i++; continue }
124
+ if (ch === '"') inString = false
125
+ } else {
126
+ if (ch === '"') inString = true
127
+ else if (ch === '[' || ch === '{') depth++
128
+ else if (ch === ']' || ch === '}') {
129
+ depth--
130
+ if (depth === 0 && ch === ']') { endIdx = i; break }
131
+ }
132
+ }
133
+ }
134
+ if (endIdx < 0) return []
135
+ const candidate = content.slice(startIdx, endIdx + 1)
136
+ let arr: unknown
137
+ try {
138
+ arr = JSON.parse(candidate)
139
+ } catch (e1) {
140
+ try { arr = JSON.parse(candidate.replace(/,(\s*[}\]])/g, '$1')) }
141
+ catch { console.error(`[trata-gepa] parseFindings failed: ${(e1 as Error).message} | head: ${candidate.slice(0, 120)}`); return [] }
142
+ }
143
+ if (!Array.isArray(arr)) return []
144
+ const sev = new Set(['critical', 'high', 'medium', 'low', 'info'])
145
+ return arr
146
+ .filter(
147
+ (x): x is Record<string, unknown> =>
148
+ typeof x === 'object' && x !== null && typeof (x as { claim?: unknown }).claim === 'string',
149
+ )
150
+ .map((x) => ({
151
+ claim: String(x.claim),
152
+ severity: (sev.has(String(x.severity)) ? String(x.severity) : 'medium') as DiagnosedFinding['severity'],
153
+ area: x.area !== undefined ? String(x.area) : 'failure-mode',
154
+ recommended_action: x.recommended_action !== undefined ? String(x.recommended_action) : undefined,
155
+ }))
156
+ }
157
+
158
+ async function main(): Promise<void> {
159
+ const adapter = createTrataHedgeAdapter()
160
+ await adapter.preflight()
161
+
162
+ const model = process.env.WORKER_MODEL ?? 'deepseek-v4-flash'
163
+ const reflectModel = process.env.REFLECT_MODEL ?? 'deepseek-v4-flash'
164
+ const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
165
+ const routerKey = must('TANGLE_API_KEY')
166
+ const trainN = Number(process.env.TRAIN_N ?? 70)
167
+ const holdoutN = Number(process.env.HOLDOUT_N ?? 32)
168
+ const kRounds = Number(process.env.K_ROUNDS ?? 1)
169
+ const corpusPath = process.env.CORPUS
170
+ const baselineSurface = process.env.BASELINE_DIRECTIVE ?? DEFAULT_TRATA_SYSTEM
171
+
172
+ // Load all tasks and split deterministically.
173
+ // Hash-shuffle by task id so both splits carry the same difficulty mix.
174
+ const tasks = await adapter.loadTasks({ limit: trainN + holdoutN })
175
+ const idHash = (s: string): number => {
176
+ let h = 2166136261
177
+ for (let i = 0; i < s.length; i += 1) {
178
+ h ^= s.charCodeAt(i)
179
+ h = Math.imul(h, 16777619)
180
+ }
181
+ return h >>> 0
182
+ }
183
+ tasks.sort((a, b) => idHash(a.id) - idHash(b.id))
184
+ const train = tasks.slice(0, Math.min(trainN, tasks.length))
185
+ const holdout = tasks.slice(train.length, train.length + Math.min(holdoutN, tasks.length - train.length))
186
+ const toScenario = (t: BenchTask): TrataScenario => ({ id: t.id, kind: 'trata-hedge', task: t })
187
+
188
+ console.log(
189
+ `[trata-gepa] worker=${model} reflect=${reflectModel} rounds=${kRounds} train=${train.length} holdout=${holdout.length}`,
190
+ )
191
+
192
+ // Domain seam: run the financial analyst worker under the candidate surface.
193
+ // For K_ROUNDS=2, a second round asks the model to review its own coverage.
194
+ // Reports real token usage to ctx.cost (never fabricated).
195
+ const runWithSurface = async (
196
+ surface: string,
197
+ scenario: TrataScenario,
198
+ ctx: {
199
+ cost: {
200
+ observe(usd: number, source: string): void
201
+ observeTokens(u: { input: number; output: number }): void
202
+ }
203
+ },
204
+ ): Promise<string> => {
205
+ // Round 1: initial analysis under the candidate system prompt.
206
+ const r1 = await chatComplete(routerBaseUrl, routerKey, model, [
207
+ { role: 'system', content: surface },
208
+ { role: 'user', content: scenario.task.prompt },
209
+ ])
210
+ if (r1.usage) ctx.cost.observeTokens(r1.usage)
211
+ let answer = r1.content
212
+
213
+ // Round 2 (optional): self-critique for rubric coverage.
214
+ if (kRounds >= 2 && answer.trim()) {
215
+ const r2 = await chatComplete(routerBaseUrl, routerKey, model, [
216
+ { role: 'system', content: surface },
217
+ { role: 'user', content: scenario.task.prompt },
218
+ { role: 'assistant', content: answer },
219
+ { role: 'user', content: REFINE_INSTRUCTION },
220
+ ])
221
+ if (r2.usage) ctx.cost.observeTokens(r2.usage)
222
+ if (r2.content.trim()) answer = r2.content
223
+ }
224
+
225
+ if (corpusPath) {
226
+ appendFileSync(
227
+ corpusPath,
228
+ JSON.stringify({ benchmark: 'trata-gepa', instanceId: scenario.task.id, model, answer: answer.slice(0, 500) }) +
229
+ '\n',
230
+ )
231
+ }
232
+ return answer
233
+ }
234
+
235
+ // Judge: wraps the Trata 3-stage judge. Returns partial-credit score (0/0.25/0.5/0.75/1.0).
236
+ const judge: JudgeConfig<string, TrataScenario> = {
237
+ name: 'trata-hedge-judge',
238
+ dimensions: [
239
+ { key: 'score', description: 'rubric theme coverage fraction (rawScore/4)' },
240
+ { key: 'resolved', description: 'all themes hit + synthesis (rawScore=4)' },
241
+ ],
242
+ async score({ artifact, scenario }): Promise<JudgeScore> {
243
+ if (!artifact.trim()) {
244
+ return { dimensions: { score: 0, resolved: 0 }, composite: 0, notes: 'empty artifact' }
245
+ }
246
+ const verdict = await adapter.judge(scenario.task, artifact)
247
+ const sc = typeof verdict.score === 'number' ? verdict.score : verdict.resolved ? 1 : 0
248
+ return {
249
+ dimensions: { score: sc, resolved: verdict.resolved ? 1 : 0 },
250
+ composite: sc,
251
+ notes: verdict.detail ?? '',
252
+ }
253
+ },
254
+ }
255
+
256
+ // EYES→HANDS: diagnose FAILED runs using themesMissed from judge detail.
257
+ // Trata's judge returns structured per-theme failure data — richer than the
258
+ // generic "wrong answer" signal that other benches feed into this hook.
259
+ const taskById = new Map(tasks.map((t) => [t.id, t]))
260
+ const analyzeGeneration = async (input: {
261
+ generation: number
262
+ runDir: string
263
+ candidates: Array<{
264
+ surfaceHash: string
265
+ campaign: CampaignResult<string, TrataScenario>
266
+ composite: number
267
+ }>
268
+ history: unknown[]
269
+ }): Promise<DiagnosedFinding[]> => {
270
+ interface FailureItem {
271
+ question: string
272
+ themesMissed: string[]
273
+ themesHit: string[]
274
+ answer: string
275
+ note: string
276
+ }
277
+ const failures = new Map<string, FailureItem>()
278
+ for (const cand of input.candidates) {
279
+ for (const cell of cand.campaign.cells) {
280
+ const js = cell.judgeScores?.[judge.name]
281
+ if ((js?.composite ?? 0) >= 1) continue
282
+ if (failures.has(cell.scenarioId)) continue
283
+ const task = taskById.get(cell.scenarioId)
284
+ if (!task) continue
285
+ let themesMissed: string[] = []
286
+ let themesHit: string[] = []
287
+ try {
288
+ const d = JSON.parse(js?.notes ?? '{}') as {
289
+ themesMissed?: string[]
290
+ themesHit?: string[]
291
+ }
292
+ themesMissed = d.themesMissed ?? []
293
+ themesHit = d.themesHit ?? []
294
+ } catch {
295
+ // no structured detail available
296
+ }
297
+ failures.set(cell.scenarioId, {
298
+ question: task.prompt.slice(0, 800),
299
+ themesMissed,
300
+ themesHit,
301
+ answer: (typeof cell.artifact === 'string' ? cell.artifact : '').slice(-1200),
302
+ note: (js?.notes ?? '').slice(0, 200),
303
+ })
304
+ }
305
+ }
306
+ const items = [...failures.values()].slice(0, 8)
307
+ if (items.length === 0) {
308
+ console.log(`[trata-gepa] gen ${input.generation}: 0 failures to diagnose`)
309
+ return []
310
+ }
311
+ const user = items
312
+ .map(
313
+ (f, i) =>
314
+ `### Failure ${i + 1}\nTASK (excerpt): ${f.question}\n` +
315
+ (f.themesMissed.length > 0 ? `MISSED THEMES: ${f.themesMissed.join(', ')}\n` : '') +
316
+ (f.themesHit.length > 0 ? `HIT THEMES: ${f.themesHit.join(', ')}\n` : '') +
317
+ `AGENT ANSWER (tail): ${f.answer}`,
318
+ )
319
+ .join('\n\n')
320
+ const system =
321
+ 'You are a failure analyst for a financial analyst agent. The agent produces investment memos ' +
322
+ 'scored by a rubric with 4-6 analytical themes, each requiring specific quantitative claims. ' +
323
+ 'Below are FAILED runs showing which themes were missed and the agent\'s answer. ' +
324
+ 'Identify the COMMON failure patterns — e.g., generic statements without specific figures, ' +
325
+ 'missing peer comparisons, no explicit calculations, ignoring certain data file types. ' +
326
+ 'For each finding, recommend a CONCRETE change to the system instruction that would fix it. ' +
327
+ 'Return ONLY a JSON array (no prose): [{"claim","severity":"high"|"medium"|"low","area","recommended_action"}]. Max 6.'
328
+ let content: string | undefined
329
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
330
+ try {
331
+ const r = await chatComplete(routerBaseUrl, routerKey, reflectModel, [
332
+ { role: 'system', content: system },
333
+ { role: 'user', content: user },
334
+ ])
335
+ content = r.content
336
+ break
337
+ } catch (err) {
338
+ const msg = (err as Error).message
339
+ if (attempt === 4) {
340
+ console.error(`[trata-gepa] analyzeGeneration failed gen ${input.generation}: ${msg}`)
341
+ return []
342
+ }
343
+ await new Promise((r) => setTimeout(r, 1000 * 2 ** (attempt - 1)))
344
+ }
345
+ }
346
+ if (!content) return []
347
+ const findings = parseFindings(content)
348
+ console.log(`[trata-gepa] gen ${input.generation}: ${items.length} failures → ${findings.length} findings`)
349
+ return findings
350
+ }
351
+
352
+ const result = await selfImprove<TrataScenario, string>({
353
+ agent: (surface, scenario, ctx) => runWithSurface(surface as string, scenario, ctx),
354
+ scenarios: train.map(toScenario),
355
+ judge,
356
+ baselineSurface,
357
+ budget: {
358
+ generations: Number(process.env.GENS ?? 2),
359
+ populationSize: Number(process.env.POP ?? 3),
360
+ maxConcurrency: Number(process.env.CONCURRENCY ?? 8),
361
+ reps: Number(process.env.REPS ?? 1),
362
+ promoteTopK: Number(process.env.TOPK ?? 1),
363
+ holdoutScenarios: holdout.map(toScenario),
364
+ },
365
+ llm: {
366
+ baseUrl: routerBaseUrl,
367
+ apiKey: routerKey,
368
+ model: reflectModel,
369
+ },
370
+ driverTarget:
371
+ 'a FINANCIAL ANALYST SYSTEM INSTRUCTION: the directive given to an agent that produces an investment memo from embedded earnings call transcripts, SEC filings, financial statements, and investor presentations. ' +
372
+ 'The memo is scored by a rubric with 4-6 analytical themes, each requiring 2-4 specific analytical moves (quantitative claims, strategic conclusions, peer comparisons, or explicit calculations). ' +
373
+ 'A theme is "hit" only when the agent makes the SPECIFIC move — not just gestures at the theme. ' +
374
+ 'The directive must make the agent: (1) extract and cite specific numerical targets from management guidance, ' +
375
+ '(2) compute implied returns/IRRs when comparing capital allocation options, ' +
376
+ '(3) cover every distinct analytical theme with a dedicated paragraph, ' +
377
+ '(4) benchmark against named peers with specific metrics. The "ANALYSIS:" sentinel must start the response.',
378
+ mutationPrimitives: [
379
+ 'instruct the agent to identify and verbatim-cite specific numerical targets in management guidance (earnings per share targets, margin percentages, growth rates, AUM figures) rather than paraphrasing in approximate terms',
380
+ 'instruct the agent to explicitly compute implied returns or IRRs when evaluating capital allocation trade-offs — show the arithmetic using the price levels and targets from the source data',
381
+ 'instruct the agent to structure the analysis with a clearly-labeled section for each distinct analytical theme (valuation, capital allocation, competitive dynamics, risk factors, etc.) so no major investment consideration is merged or omitted',
382
+ 'instruct the agent to compare the company against its NAMED sector peers with specific metrics (EV/EBITDA, P/E, margin differential, growth premium) cited from the peer financials files in the data',
383
+ ],
384
+ runDir: 'improve-prompt-trata-hedge',
385
+ storage: inMemoryCampaignStorage(),
386
+ autoOnPromote: 'none',
387
+ analyzeGeneration,
388
+ })
389
+
390
+ console.log('\n=== trata-gepa RESULT ===')
391
+ const improved = result.gateDecision === 'ship'
392
+ console.log(` baseline held-out mean: ${(result.baseline.compositeMean * 100).toFixed(1)}%`)
393
+ console.log(` winner held-out mean: ${(result.winner.compositeMean * 100).toFixed(1)}%`)
394
+ console.log(` ► held-out delta: ${(result.lift * 100).toFixed(1)} pp`)
395
+ console.log(` gate decision: ${result.gateDecision}`)
396
+
397
+ try {
398
+ const cellsToMap = (cells: ReadonlyArray<{ scenarioId: string; judgeScores: Record<string, JudgeScore> }>) => {
399
+ const m = new Map<string, Record<string, JudgeScore>>()
400
+ for (const c of cells) m.set(c.scenarioId, c.judgeScores)
401
+ return m
402
+ }
403
+ const baseMap = cellsToMap(result.raw.baselineOnHoldout.cells)
404
+ const winMap = cellsToMap(result.raw.winnerOnHoldout.cells)
405
+ const ids = new Set([...baseMap.keys()].filter((id) => winMap.has(id)))
406
+ const paired = pairHoldout(winMap, baseMap, ids, (s) => s.composite)
407
+ const sig = heldoutSignificance(paired)
408
+ console.log(
409
+ ` ► 95% CI (n=${sig.n}): [${(sig.bootstrap.low * 100).toFixed(1)}, ${(sig.bootstrap.high * 100).toFixed(1)}] pp · median ${(sig.bootstrap.median * 100).toFixed(1)}pp · significant=${sig.significant}`,
410
+ )
411
+ if (!sig.significant)
412
+ console.log(' (CI spans 0 — scale n or generations before promoting)')
413
+ } catch (err) {
414
+ console.log(` (significance unavailable: ${(err instanceof Error ? err.message : String(err)).slice(0, 80)})`)
415
+ }
416
+
417
+ const winnerSurface = result.winner.surface as string
418
+ if (improved) {
419
+ console.log(`\n PROMOTED SYSTEM PROMPT:\n${winnerSurface}`)
420
+ if (result.winner.rationale) console.log(`\n rationale: ${result.winner.rationale}`)
421
+ } else {
422
+ console.log(' kept baseline (gate did not promote)')
423
+ console.log(`\n BEST CANDIDATE SURFACE (set as BASELINE_DIRECTIVE to seed next run):\n${winnerSurface}`)
424
+ }
425
+ try {
426
+ writeFileSync('/tmp/trata-gepa-winner-surface.txt', winnerSurface)
427
+ console.log('\n (winner surface written to /tmp/trata-gepa-winner-surface.txt)')
428
+ } catch { /* non-fatal */ }
429
+ }
430
+
431
+ main().catch((err) => {
432
+ console.error(err instanceof Error ? (err.stack ?? err.message) : String(err))
433
+ process.exit(1)
434
+ })