@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.
- package/README.md +15 -0
- package/package.json +35 -0
- package/src/adapters.ts +60 -0
- package/src/aec-gate.mts +217 -0
- package/src/atom-humaneval.mts +197 -0
- package/src/atom-mcp-e2e.mts +223 -0
- package/src/benchmarks/_harness.ts +206 -0
- package/src/benchmarks/aec-bench.test.mts +53 -0
- package/src/benchmarks/aec-bench.ts +319 -0
- package/src/benchmarks/appworld.test.mts +45 -0
- package/src/benchmarks/appworld.ts +426 -0
- package/src/benchmarks/cad-design.ts +429 -0
- package/src/benchmarks/cadbench.ts +135 -0
- package/src/benchmarks/cadgenbench.ts +121 -0
- package/src/benchmarks/commit0.test.mts +71 -0
- package/src/benchmarks/commit0.ts +266 -0
- package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
- package/src/benchmarks/enterpriseops-gym.ts +343 -0
- package/src/benchmarks/finsearchcomp.ts +371 -0
- package/src/benchmarks/frames.ts +520 -0
- package/src/benchmarks/hotpotqa.ts +320 -0
- package/src/benchmarks/humaneval.ts +251 -0
- package/src/benchmarks/mind2web.ts +311 -0
- package/src/benchmarks/programbench.test.mts +60 -0
- package/src/benchmarks/programbench.ts +210 -0
- package/src/benchmarks/simpleqa.ts +390 -0
- package/src/benchmarks/swe-bench.ts +152 -0
- package/src/benchmarks/terminal-bench.ts +182 -0
- package/src/benchmarks/trata-hedge.ts +496 -0
- package/src/benchmarks/types.ts +58 -0
- package/src/browser/adapters/bad-design-audit.ts +153 -0
- package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
- package/src/browser/adapters/bad.ts +165 -0
- package/src/browser/agent-adapter.ts +145 -0
- package/src/browser/process-adapter.ts +146 -0
- package/src/browser/run-to-spans.ts +105 -0
- package/src/browser/run-to-spans.verify.ts +89 -0
- package/src/browser/ui-reviewer.ts +200 -0
- package/src/browser/ui-reviewer.verify.ts +94 -0
- package/src/browser/verify.ts +96 -0
- package/src/clbench-codebase-gate.mts +314 -0
- package/src/clbench-context-gate.mts +305 -0
- package/src/cloud-loop.mts +138 -0
- package/src/coding-skills/minimal-diff.md +9 -0
- package/src/coding-skills/read-before-edit.md +9 -0
- package/src/coding-skills/reproduce-first.md +10 -0
- package/src/coding-skills/run-tests-after-edit.md +9 -0
- package/src/coding-skills/trace-the-failure.md +9 -0
- package/src/commit0-env-run.mts +59 -0
- package/src/commit0-env.ts +173 -0
- package/src/commit0-gate.mts +529 -0
- package/src/commit0-prereqs.sh +48 -0
- package/src/corpus-replay.mts +300 -0
- package/src/corpus-report.mts +504 -0
- package/src/corpus.test.mts +273 -0
- package/src/corpus.ts +325 -0
- package/src/decoder-live.mts +133 -0
- package/src/directives.ts +84 -0
- package/src/diverse-gate.mjs +112 -0
- package/src/egress-probe.mts +26 -0
- package/src/eops-skills/address-every-subtask.md +5 -0
- package/src/eops-skills/exact-tools-and-args.md +5 -0
- package/src/eops-skills/full-sequence-to-goal.md +5 -0
- package/src/eops-skills/ground-every-value.md +5 -0
- package/src/eops-skills/honor-the-policies.md +5 -0
- package/src/examples/README.md +58 -0
- package/src/examples/math-demo.mts +110 -0
- package/src/examples/strategy-demo.mts +119 -0
- package/src/fleet.mts +121 -0
- package/src/gate-cli.mts +101 -0
- package/src/gate.test.mts +129 -0
- package/src/gate.ts +460 -0
- package/src/generate-eval/certify.ts +178 -0
- package/src/generate-eval/schema.ts +78 -0
- package/src/humaneval-gate.mts +204 -0
- package/src/humaneval-repair-gate.mts +143 -0
- package/src/index.ts +19 -0
- package/src/mcp-mount-probe.mts +126 -0
- package/src/profile-coordinates.ts +134 -0
- package/src/profiles.ts +128 -0
- package/src/refine-loop.test.mts +106 -0
- package/src/refine-loop.ts +106 -0
- package/src/research-gate.mts +132 -0
- package/src/research-shot.ts +134 -0
- package/src/resolve-client.ts +58 -0
- package/src/router-executor.ts +51 -0
- package/src/run-pool.ts +48 -0
- package/src/runtime-hook-recorder.ts +137 -0
- package/src/sandbox-run.ts +125 -0
- package/src/search-bench/bridge.ts +124 -0
- package/src/search-bench/export.mts +0 -0
- package/src/search-bench/parametric-check.mts +63 -0
- package/src/search-bench/profiles.ts +98 -0
- package/src/search-bench/run.mts +287 -0
- package/src/search-bench/tasks-fresh.ts +688 -0
- package/src/search-bench/tasks.ts +129 -0
- package/src/search-tool.ts +95 -0
- package/src/selector.test.mts +189 -0
- package/src/selector.ts +366 -0
- package/src/skill-sandbox-smoke.mts +100 -0
- package/src/stats.mts +90 -0
- package/src/terminal-compare.ts +519 -0
- package/src/trajectory-assemble.mjs +130 -0
- package/src/trata-gate.mts +243 -0
- package/src/trata-gepa.mts +434 -0
- package/src/worker-blender.ts +230 -0
- package/src/worker-browser.ts +102 -0
- package/src/worker-build123d.ts +143 -0
- package/src/worker-cad.ts +451 -0
- package/src/worker.ts +136 -0
- 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
|
+
})
|