@tangle-network/agent-bench 0.3.8 → 0.4.1

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 (115) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +7 -0
  3. package/dist/adapters.js +23 -23
  4. package/dist/benchmarks/_harness.d.ts +4 -1
  5. package/dist/benchmarks/_harness.js +3 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/nomiracl.js +2 -2
  16. package/dist/benchmarks/open-rag-bench.js +2 -2
  17. package/dist/benchmarks/programbench.js +2 -2
  18. package/dist/benchmarks/ragbench.js +2 -2
  19. package/dist/benchmarks/swe-bench.js +2 -2
  20. package/dist/benchmarks/t2-ragbench.js +2 -2
  21. package/dist/benchmarks/tau-bench-shared.js +2 -2
  22. package/dist/benchmarks/tau2-bench.js +3 -3
  23. package/dist/benchmarks/tau3-banking.js +3 -3
  24. package/dist/benchmarks/terminal-bench.js +2 -2
  25. package/dist/benchmarks/toollm.js +2 -2
  26. package/dist/benchmarks/webarena-verified.js +2 -2
  27. package/dist/{chunk-YSMEKBTD.js → chunk-5FEQDSCT.js} +2 -2
  28. package/dist/{chunk-PB64GYIG.js → chunk-67ACKDCX.js} +2 -2
  29. package/dist/{chunk-NQG5XDSB.js → chunk-7FKBWOQT.js} +2 -2
  30. package/dist/{chunk-K3BQGZCT.js → chunk-BEN6IF2X.js} +2 -2
  31. package/dist/{chunk-SHYIRB7I.js → chunk-BZY5QARD.js} +2 -2
  32. package/dist/{chunk-KP5KD6EN.js → chunk-CLIKAXKH.js} +2 -2
  33. package/dist/{chunk-XKEFIFIC.js → chunk-CWIOBFSP.js} +2 -2
  34. package/dist/{chunk-IA2FBTWC.js → chunk-CXDUTWQE.js} +2 -2
  35. package/dist/{chunk-RH5F53JT.js → chunk-DWALFME7.js} +2 -2
  36. package/dist/{chunk-V7AEBY6U.js → chunk-EEOC6QPJ.js} +21 -21
  37. package/dist/{chunk-WSKWVEQB.js → chunk-EIETHPD5.js} +7 -3
  38. package/dist/chunk-EIETHPD5.js.map +1 -0
  39. package/dist/{chunk-SHM6MRRF.js → chunk-GC2EPS6L.js} +2 -2
  40. package/dist/{chunk-IZ5M6OAC.js → chunk-GCHL6XPM.js} +2 -2
  41. package/dist/{chunk-IFVINJ4B.js → chunk-HQ5HCCKF.js} +2 -2
  42. package/dist/{chunk-7GRVHU22.js → chunk-HVW25KSX.js} +2 -2
  43. package/dist/{chunk-3U5TXJZS.js → chunk-J6BU3NTM.js} +2 -2
  44. package/dist/{chunk-XYA4XSNU.js → chunk-JSQOUKXS.js} +4 -3
  45. package/dist/chunk-JSQOUKXS.js.map +1 -0
  46. package/dist/{chunk-SVR2LKYI.js → chunk-NRMGT25X.js} +2 -2
  47. package/dist/{chunk-MQMRLGOG.js → chunk-QZZEAHWJ.js} +2 -2
  48. package/dist/{chunk-RCYQEFNX.js → chunk-WG7TM7UV.js} +3 -3
  49. package/dist/{chunk-HWST3SED.js → chunk-XXFF3RRD.js} +2 -2
  50. package/dist/{chunk-SFLA7OH3.js → chunk-ZFNOM7WR.js} +3 -3
  51. package/dist/{chunk-Z4TZ76N7.js → chunk-ZNCCYTFG.js} +2 -2
  52. package/dist/index.js +23 -23
  53. package/package.json +4 -4
  54. package/pier_agents/candidate_contract.py +238 -24
  55. package/pier_agents/tangle_candidate.py +75 -5
  56. package/scripts/trata-hedge/README.md +6 -5
  57. package/scripts/verify-packed-consumer.mjs +65 -1
  58. package/scripts/verify-pier-agent.mts +6 -4
  59. package/src/benchmarks/_harness.test.mts +16 -1
  60. package/src/benchmarks/_harness.ts +9 -2
  61. package/src/benchmarks/terminal-bench.ts +2 -1
  62. package/src/gate.ts +1 -1
  63. package/src/hev-eval.mts +5 -2
  64. package/src/hev-improve.mts +118 -73
  65. package/src/official-optimizer-config.mts +89 -0
  66. package/src/official-optimizer-config.test.mts +88 -0
  67. package/src/profiles.ts +2 -2
  68. package/src/rollout-ledger/backfill-swe-arena.test.mts +28 -24
  69. package/src/smoke-structural-rollout.mts +15 -9
  70. package/src/swe-arena/activation.mts +1 -4
  71. package/src/swe-arena/activation.test.mts +10 -13
  72. package/src/swe-arena/gepa-seat.mts +425 -131
  73. package/src/swe-arena/gepa-seat.test.mts +524 -100
  74. package/src/swe-arena/implementation-ref.test.mts +64 -0
  75. package/src/swe-arena/implementation-ref.ts +62 -0
  76. package/src/swe-arena/outer-loop.mts +103 -76
  77. package/src/swe-arena/proposer-fanout.mts +51 -36
  78. package/src/swe-arena/proposer-fanout.test.mts +0 -1
  79. package/src/swe-arena/proposer-provenance.mts +11 -16
  80. package/src/swe-arena/scratch-worktree.test.mts +55 -0
  81. package/src/swe-arena/scratch-worktree.ts +34 -0
  82. package/src/swe-code-improve.mts +24 -25
  83. package/src/swe-improve.mts +129 -96
  84. package/src/swe-local-proof.mts +6 -1
  85. package/src/swe-stream.mts +4 -2
  86. package/src/tb-container-executor.test.mts +30 -6
  87. package/src/tb-supervisor-sidecar.mts +2 -1
  88. package/src/trata-gepa.mts +182 -245
  89. package/dist/chunk-WSKWVEQB.js.map +0 -1
  90. package/dist/chunk-XYA4XSNU.js.map +0 -1
  91. package/src/live-improve-campaign-mbpp.mts +0 -641
  92. package/src/live-improve-campaign.mts +0 -500
  93. package/src/swe-arena/lineage-record.mts +0 -164
  94. package/src/swe-arena/lineage-record.test.mts +0 -115
  95. /package/dist/{chunk-YSMEKBTD.js.map → chunk-5FEQDSCT.js.map} +0 -0
  96. /package/dist/{chunk-PB64GYIG.js.map → chunk-67ACKDCX.js.map} +0 -0
  97. /package/dist/{chunk-NQG5XDSB.js.map → chunk-7FKBWOQT.js.map} +0 -0
  98. /package/dist/{chunk-K3BQGZCT.js.map → chunk-BEN6IF2X.js.map} +0 -0
  99. /package/dist/{chunk-SHYIRB7I.js.map → chunk-BZY5QARD.js.map} +0 -0
  100. /package/dist/{chunk-KP5KD6EN.js.map → chunk-CLIKAXKH.js.map} +0 -0
  101. /package/dist/{chunk-XKEFIFIC.js.map → chunk-CWIOBFSP.js.map} +0 -0
  102. /package/dist/{chunk-IA2FBTWC.js.map → chunk-CXDUTWQE.js.map} +0 -0
  103. /package/dist/{chunk-RH5F53JT.js.map → chunk-DWALFME7.js.map} +0 -0
  104. /package/dist/{chunk-V7AEBY6U.js.map → chunk-EEOC6QPJ.js.map} +0 -0
  105. /package/dist/{chunk-SHM6MRRF.js.map → chunk-GC2EPS6L.js.map} +0 -0
  106. /package/dist/{chunk-IZ5M6OAC.js.map → chunk-GCHL6XPM.js.map} +0 -0
  107. /package/dist/{chunk-IFVINJ4B.js.map → chunk-HQ5HCCKF.js.map} +0 -0
  108. /package/dist/{chunk-7GRVHU22.js.map → chunk-HVW25KSX.js.map} +0 -0
  109. /package/dist/{chunk-3U5TXJZS.js.map → chunk-J6BU3NTM.js.map} +0 -0
  110. /package/dist/{chunk-SVR2LKYI.js.map → chunk-NRMGT25X.js.map} +0 -0
  111. /package/dist/{chunk-MQMRLGOG.js.map → chunk-QZZEAHWJ.js.map} +0 -0
  112. /package/dist/{chunk-RCYQEFNX.js.map → chunk-WG7TM7UV.js.map} +0 -0
  113. /package/dist/{chunk-HWST3SED.js.map → chunk-XXFF3RRD.js.map} +0 -0
  114. /package/dist/{chunk-SFLA7OH3.js.map → chunk-ZFNOM7WR.js.map} +0 -0
  115. /package/dist/{chunk-Z4TZ76N7.js.map → chunk-ZNCCYTFG.js.map} +0 -0
@@ -2,8 +2,17 @@ import assert from 'node:assert/strict'
2
2
  import { chmod, mkdtemp, writeFile } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import path from 'node:path'
5
- import type { AgentSpec, ExecutorContext } from '@tangle-network/agent-runtime/loops'
6
- import { buildTbDockerExecArgs, createTbContainerExecutor } from './tb-container-executor.mts'
5
+ import type {
6
+ AgentSpec,
7
+ Executor,
8
+ ExecutorContext,
9
+ ExecutorResult,
10
+ } from '@tangle-network/agent-runtime/loops'
11
+ import {
12
+ buildTbDockerExecArgs,
13
+ createTbContainerExecutor,
14
+ type TbExecOutput,
15
+ } from './tb-container-executor.mts'
7
16
 
8
17
  const spec: AgentSpec = { profile: { name: 'tb-test-worker' }, harness: null }
9
18
 
@@ -11,6 +20,21 @@ function context(): ExecutorContext {
11
20
  return { signal: new AbortController().signal, seams: {} }
12
21
  }
13
22
 
23
+ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
24
+ return typeof value === 'object' && value !== null && Symbol.asyncIterator in value
25
+ }
26
+
27
+ async function executeOneShot(
28
+ executor: Executor<TbExecOutput>,
29
+ task: unknown,
30
+ ): Promise<ExecutorResult<TbExecOutput>> {
31
+ const result = executor.execute(task, new AbortController().signal)
32
+ if (isAsyncIterable(result)) {
33
+ throw new Error('tb-container-executor returned a stream instead of a one-shot result')
34
+ }
35
+ return await result
36
+ }
37
+
14
38
  async function executable(name: string, body: string): Promise<string> {
15
39
  const dir = await mkdtemp(path.join(tmpdir(), 'tb-container-executor-'))
16
40
  const file = path.join(dir, name)
@@ -52,7 +76,7 @@ printf 'argv:%s\\n' "$*"
52
76
  })(spec, context())
53
77
 
54
78
  assert.equal(metered.budgetExempt, false, 'usage parser makes the executor metered')
55
- const meteredResult = await metered.execute({ command: 'echo hello' }, new AbortController().signal)
79
+ const meteredResult = await executeOneShot(metered, { command: 'echo hello' })
56
80
  assert.equal(meteredResult.out.containerId, 'cid')
57
81
  assert.equal(meteredResult.out.command, 'echo hello')
58
82
  assert.match(
@@ -65,7 +89,7 @@ printf 'argv:%s\\n' "$*"
65
89
 
66
90
  const free = createTbContainerExecutor({ containerId: 'cid', dockerBin: fakeDocker })(spec, context())
67
91
  assert.equal(free.budgetExempt, true, 'unmetered shell commands are explicit budget-exempt work')
68
- const freeResult = await free.execute('printf ok', new AbortController().signal)
92
+ const freeResult = await executeOneShot(free, 'printf ok')
69
93
  assert.equal(freeResult.spent.iterations, 0)
70
94
  assert.deepEqual(freeResult.spent.tokens, { input: 0, output: 0 })
71
95
  assert.equal(freeResult.spent.usd, 0)
@@ -83,13 +107,13 @@ exit 7
83
107
  failOnNonZeroExit: true,
84
108
  })(spec, context())
85
109
  await assert.rejects(
86
- strict.execute('do work', new AbortController().signal),
110
+ executeOneShot(strict, 'do work'),
87
111
  /command exited 7/,
88
112
  'strict mode treats non-zero command exit as infrastructure failure',
89
113
  )
90
114
 
91
115
  const lenient = createTbContainerExecutor({ containerId: 'cid', dockerBin: failingDocker })(spec, context())
92
- const lenientResult = await lenient.execute('do work', new AbortController().signal)
116
+ const lenientResult = await executeOneShot(lenient, 'do work')
93
117
  assert.equal(lenientResult.out.exitCode, 7, 'default mode returns non-zero exits as task artifacts')
94
118
  assert.match(lenientResult.out.stderr, /failed/)
95
119
 
@@ -159,12 +159,13 @@ async function main(): Promise<void> {
159
159
  | undefined
160
160
  if (out) {
161
161
  const usage = out.stdout ? parseWorkerUsage({ stdout: out.stdout, stderr: '', exitCode: null }) : undefined
162
+ const workerId = 'id' in w && typeof w.id === 'string' ? w.id : undefined
162
163
  if (usage) {
163
164
  workerInput += usage.input
164
165
  workerOutput += usage.output
165
166
  }
166
167
  workerOutputs.push({
167
- id: w.id,
168
+ id: workerId,
168
169
  status: w.status,
169
170
  command: out.command,
170
171
  exitCode: out.exitCode ?? null,
@@ -1,22 +1,22 @@
1
1
  /**
2
- * trata-gepa — selfImprove (GEPA) outer loop for Trata hedge-bench.
2
+ * Official GEPA prompt optimization for Trata hedge-bench.
3
3
  *
4
4
  * The optimization surface is the system prompt given to the financial analyst
5
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
6
+ * and proposes improved system prompts, learning to instruct the model to
7
7
  * extract specific quantitative claims, cover multiple analytical themes, and
8
- * cite named evidence. Gated on a frozen holdout.
8
+ * cite named evidence. Candidate selection and final testing use disjoint data.
9
9
  *
10
10
  * The surface evolves beyond a bare system instruction: GEPA naturally discovers
11
11
  * that it can add few-shot analytical patterns, calculation templates, and
12
- * structured coverage checklists effectively skill-creating without
12
+ * structured coverage checklists, effectively skill-creating without
13
13
  * hand-engineering. Set K_ROUNDS=2 to add a self-critique refine pass.
14
14
  *
15
15
  * Usage:
16
16
  * TRATA_BENCH_ROOT=/tmp/trata-hedge-bench \
17
17
  * JUDGE_MODEL=gemini-2.5-flash WORKER_MODEL=deepseek-v4-flash \
18
18
  * REFLECT_MODEL=gemini-2.5-pro \
19
- * TRAIN_N=70 HOLDOUT_N=32 GENS=2 POP=3 CONCURRENCY=8 \
19
+ * TRAIN_N=70 SELECTION_N=16 TEST_N=16 MAX_EVALUATIONS=12 CONCURRENCY=8 \
20
20
  * dotenvx run -f ~/company/devops/secrets/agent-state.env -- \
21
21
  * pnpm exec tsx bench/src/trata-gepa.mts
22
22
  *
@@ -26,9 +26,10 @@
26
26
  * JUDGE_MODEL judge model in trata adapter (default gemini-2.5-flash)
27
27
  * REFLECT_MODEL GEPA reflection model (default gemini-2.5-pro)
28
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)
29
+ * SELECTION_N optimizer selection tasks (default 16)
30
+ * TEST_N untouched final comparison tasks (default 16)
31
+ * MAX_EVALUATIONS GEPA callback evaluations (default 12)
32
+ * MAX_PROPOSER_COST_USD GEPA proposal budget (default 5)
32
33
  * REPS reps per scenario (default 1)
33
34
  * CONCURRENCY parallel worker slots (default 8)
34
35
  * K_ROUNDS 1=single-shot, 2=analysis+self-critique (default 1)
@@ -36,30 +37,56 @@
36
37
  * CORPUS path to write JSONL run records (optional)
37
38
  */
38
39
 
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'
40
+ import {
41
+ canonicalCandidateDigest,
42
+ type AgentProfile,
43
+ } from '@tangle-network/agent-interface'
44
+ import type {
45
+ DispatchContext,
46
+ JudgeConfig,
47
+ JudgeScore,
48
+ Scenario,
49
+ } from '@tangle-network/agent-eval/campaign'
50
+ import {
51
+ improve,
52
+ officialGepa,
53
+ type ReadonlyAgentProfile,
54
+ } from '@tangle-network/agent-runtime'
42
55
  import { appendFileSync, writeFileSync } from 'node:fs'
43
56
  import { createTrataHedgeAdapter } from './benchmarks/trata-hedge'
44
57
  import type { BenchTask } from './benchmarks/types'
58
+ import {
59
+ assertCompleteCost,
60
+ officialOptimizerModel,
61
+ requiredTokenPricing,
62
+ } from './official-optimizer-config.mjs'
45
63
 
46
64
  interface TrataScenario extends Scenario {
47
65
  task: BenchTask
48
66
  }
49
67
 
50
- interface DiagnosedFinding {
51
- claim: string
52
- severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
53
- area?: string
54
- recommended_action?: string
55
- }
56
-
57
68
  function must(name: string): string {
58
69
  const v = process.env[name]
59
70
  if (!v) throw new Error(`env ${name} is required`)
60
71
  return v
61
72
  }
62
73
 
74
+ function positiveInteger(name: string, fallback: number): number {
75
+ const value = Number(process.env[name] ?? fallback)
76
+ if (!Number.isSafeInteger(value) || value <= 0) {
77
+ throw new Error(`env ${name} must be a positive integer`)
78
+ }
79
+ return value
80
+ }
81
+
82
+ function positiveNumber(name: string, fallback: number): number {
83
+ const value = Number(process.env[name] ?? fallback)
84
+ if (!Number.isFinite(value) || value <= 0) {
85
+ throw new Error(`env ${name} must be a positive number`)
86
+ }
87
+ return value
88
+ }
89
+
63
90
  // GEPA-optimised baseline — the best surface found across 9 runs (+8.6pp on holdout, 2 independent
64
91
  // confirmations). Future GEPA runs start from here; BASELINE_DIRECTIVE overrides if you want to
65
92
  // experiment from a different starting point.
@@ -91,13 +118,15 @@ async function chatComplete(
91
118
  baseUrl: string,
92
119
  key: string,
93
120
  model: string,
121
+ maxTokens: number,
94
122
  messages: Array<{ role: string; content: string }>,
123
+ signal: AbortSignal,
95
124
  ): Promise<{ content: string; usage?: { input: number; output: number } }> {
96
125
  const res = await fetch(`${baseUrl}/chat/completions`, {
97
126
  method: 'POST',
98
- signal: AbortSignal.timeout(180_000),
127
+ signal: AbortSignal.any([signal, AbortSignal.timeout(180_000)]),
99
128
  headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
100
- body: JSON.stringify({ model, temperature: 0, max_tokens: 4096, messages }),
129
+ body: JSON.stringify({ model, temperature: 0, max_tokens: maxTokens, messages }),
101
130
  })
102
131
  if (!res.ok) throw new Error(`router ${res.status}: ${(await res.text()).slice(0, 300)}`)
103
132
  const j = (await res.json()) as {
@@ -112,50 +141,14 @@ async function chatComplete(
112
141
  return { content, usage }
113
142
  }
114
143
 
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',
144
+ async function main(): Promise<void> {
145
+ if (process.env.DRYRUN) {
146
+ console.log(
147
+ `DRYRUN: imports OK (improve=${typeof improve}, officialGepa=${typeof officialGepa})`,
149
148
  )
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
- }
149
+ return
150
+ }
157
151
 
158
- async function main(): Promise<void> {
159
152
  const adapter = createTrataHedgeAdapter()
160
153
  await adapter.preflight()
161
154
 
@@ -163,15 +156,39 @@ async function main(): Promise<void> {
163
156
  const reflectModel = process.env.REFLECT_MODEL ?? 'deepseek-v4-flash'
164
157
  const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
165
158
  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)
159
+ const reflectBaseUrl = process.env.REFLECT_BASE ?? routerBaseUrl
160
+ const reflectKey = process.env.REFLECT_KEY ?? routerKey
161
+ const trainN = positiveInteger('TRAIN_N', 70)
162
+ const selectionN = positiveInteger('SELECTION_N', 16)
163
+ const testN = positiveInteger('TEST_N', 16)
164
+ const kRounds = positiveInteger('K_ROUNDS', 1)
165
+ const maxEvaluations = positiveInteger('MAX_EVALUATIONS', 12)
166
+ const maxProposerCostUsd = positiveNumber('MAX_PROPOSER_COST_USD', 5)
167
+ const maxConcurrency = positiveInteger('CONCURRENCY', 8)
168
+ const reps = positiveInteger('REPS', 1)
169
+ const workerMaxTokens = positiveInteger('MAX_TOKENS', 4096)
170
+ const reflectMaxTokens = positiveInteger('REFLECT_MAX_TOKENS', 8192)
169
171
  const corpusPath = process.env.CORPUS
170
172
  const baselineSurface = process.env.BASELINE_DIRECTIVE ?? DEFAULT_TRATA_SYSTEM
173
+ const runDir = process.env.RUN_DIR ?? '.runs/trata-official-gepa'
174
+ const workerPricing = requiredTokenPricing(process.env, 'WORKER')
175
+ const optimizer = officialOptimizerModel({
176
+ env: process.env,
177
+ model: reflectModel,
178
+ baseUrl: reflectBaseUrl,
179
+ apiKey: reflectKey,
180
+ maxCostUsd: maxProposerCostUsd,
181
+ maxOutputTokensPerRequest: reflectMaxTokens,
182
+ })
171
183
 
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 })
184
+ // Hash-shuffle by task id so all three partitions carry the same difficulty mix.
185
+ const requestedTasks = trainN + selectionN + testN
186
+ const tasks = await adapter.loadTasks({ limit: requestedTasks })
187
+ if (tasks.length !== requestedTasks) {
188
+ throw new Error(
189
+ `Trata returned ${tasks.length} tasks; ${requestedTasks} are required for exact train/selection/test partitions`,
190
+ )
191
+ }
175
192
  const idHash = (s: string): number => {
176
193
  let h = 2166136261
177
194
  for (let i = 0; i < s.length; i += 1) {
@@ -180,45 +197,63 @@ async function main(): Promise<void> {
180
197
  }
181
198
  return h >>> 0
182
199
  }
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))
200
+ tasks.sort((a, b) => idHash(a.id) - idHash(b.id) || a.id.localeCompare(b.id))
201
+ const train = tasks.slice(0, trainN)
202
+ const selection = tasks.slice(trainN, trainN + selectionN)
203
+ const test = tasks.slice(trainN + selectionN)
186
204
  const toScenario = (t: BenchTask): TrataScenario => ({ id: t.id, kind: 'trata-hedge', task: t })
187
205
 
188
206
  console.log(
189
- `[trata-gepa] worker=${model} reflect=${reflectModel} rounds=${kRounds} train=${train.length} holdout=${holdout.length}`,
207
+ `[trata-gepa] worker=${model} reflect=${reflectModel} rounds=${kRounds} train=${train.length} selection=${selection.length} test=${test.length}`,
190
208
  )
191
209
 
192
- // Domain seam: run the financial analyst worker under the candidate surface.
210
+ // Run the financial analyst worker under the candidate surface.
193
211
  // 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).
212
+ // Every provider call reports its returned usage through the campaign ledger.
213
+ const runPaidCompletion = async (
214
+ ctx: DispatchContext,
215
+ actor: string,
216
+ messages: Array<{ role: string; content: string }>,
217
+ ): Promise<{ content: string; usage?: { input: number; output: number } }> => {
218
+ const paid = await ctx.cost.runPaidCall({
219
+ actor,
220
+ model,
221
+ execute: (signal) =>
222
+ chatComplete(routerBaseUrl, routerKey, model, workerMaxTokens, messages, signal),
223
+ receipt: (result) => ({
224
+ model,
225
+ inputTokens: result.usage?.input ?? 0,
226
+ outputTokens: result.usage?.output ?? 0,
227
+ customTokenPricing: workerPricing,
228
+ ...(result.usage ? {} : { usageUnknown: true }),
229
+ }),
230
+ })
231
+ if (!paid.succeeded) throw paid.error
232
+ return paid.value
233
+ }
234
+
195
235
  const runWithSurface = async (
196
- surface: string,
236
+ candidate: ReadonlyAgentProfile,
197
237
  scenario: TrataScenario,
198
- ctx: {
199
- cost: {
200
- observe(usd: number, source: string): void
201
- observeTokens(u: { input: number; output: number }): void
202
- }
203
- },
238
+ ctx: DispatchContext,
204
239
  ): Promise<string> => {
240
+ const systemPrompt = candidate.prompt?.systemPrompt
241
+ if (systemPrompt === undefined) throw new Error('Trata candidate profile has no system prompt')
205
242
  // Round 1: initial analysis under the candidate system prompt.
206
- const r1 = await chatComplete(routerBaseUrl, routerKey, model, [
207
- { role: 'system', content: surface },
243
+ const r1 = await runPaidCompletion(ctx, 'trata-worker-round-1', [
244
+ { role: 'system', content: systemPrompt },
208
245
  { role: 'user', content: scenario.task.prompt },
209
246
  ])
210
- if (r1.usage) ctx.cost.observeTokens(r1.usage)
211
247
  let answer = r1.content
212
248
 
213
249
  // Round 2 (optional): self-critique for rubric coverage.
214
250
  if (kRounds >= 2 && answer.trim()) {
215
- const r2 = await chatComplete(routerBaseUrl, routerKey, model, [
216
- { role: 'system', content: surface },
251
+ const r2 = await runPaidCompletion(ctx, 'trata-worker-round-2', [
252
+ { role: 'system', content: systemPrompt },
217
253
  { role: 'user', content: scenario.task.prompt },
218
254
  { role: 'assistant', content: answer },
219
255
  { role: 'user', content: REFINE_INSTRUCTION },
220
256
  ])
221
- if (r2.usage) ctx.cost.observeTokens(r2.usage)
222
257
  if (r2.content.trim()) answer = r2.content
223
258
  }
224
259
 
@@ -253,173 +288,75 @@ async function main(): Promise<void> {
253
288
  },
254
289
  }
255
290
 
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
291
+ const profile: AgentProfile = {
292
+ name: 'trata-financial-analyst',
293
+ prompt: { systemPrompt: baselineSurface },
350
294
  }
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),
295
+ const result = await improve(profile, {
296
+ surface: 'prompt',
297
+ executionRef: canonicalCandidateDigest({
298
+ callback: 'bench/trata-gepa',
299
+ model,
300
+ endpoint: new URL(routerBaseUrl).origin,
301
+ maxTokens: workerMaxTokens,
302
+ rounds: kRounds,
303
+ judgeModel: process.env.JUDGE_MODEL ?? 'adapter-default',
304
+ }),
305
+ method: officialGepa<TrataScenario, string>({
306
+ objective:
307
+ 'Improve the complete system instruction for a financial analyst that writes evidence-backed investment memos.',
308
+ background:
309
+ 'The judge awards partial credit for covering every requested analytical theme with specific quantitative claims, named peer comparisons, explicit calculations, source citations, and a decisive synthesis. Preserve the required ANALYSIS: prefix.',
310
+ recipe: {
311
+ kind: 'engine',
312
+ run: {
313
+ engine: 'gepa',
314
+ maxEvaluations,
315
+ maxProposerCostUsd,
316
+ },
317
+ },
318
+ optimizer,
319
+ resume: 'if-compatible',
320
+ trustResumeState: true,
321
+ describeScenario: (scenario) => ({
322
+ id: scenario.id,
323
+ prompt: scenario.task.prompt,
324
+ }),
325
+ describeArtifact: (artifact) => ({ answer: artifact.slice(-4000) }),
326
+ }),
327
+ trainScenarios: train.map(toScenario),
328
+ selectionScenarios: selection.map(toScenario),
329
+ testScenarios: test.map(toScenario),
330
+ judges: [judge],
331
+ agent: runWithSurface,
332
+ expectUsage: 'warn',
333
+ maxConcurrency,
334
+ reps,
335
+ runDir,
336
+ optimizationRunOptions: {
337
+ expectUsage: 'warn',
338
+ maxConcurrency,
339
+ reps,
364
340
  },
365
- llm: {
366
- baseUrl: routerBaseUrl,
367
- apiKey: routerKey,
368
- model: reflectModel,
369
- },
370
- proposerTarget:
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
341
  })
389
342
 
343
+ assertCompleteCost('Trata official GEPA run', result.cost)
390
344
  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
- }
345
+ const improved = result.decision === 'ship'
346
+ console.log(` baseline test mean: ${(result.raw.best.baselineComposite * 100).toFixed(1)}%`)
347
+ console.log(` winner test mean: ${(result.raw.best.winnerComposite * 100).toFixed(1)}%`)
348
+ console.log(` test delta: ${(result.lift * 100).toFixed(1)} pp`)
349
+ console.log(
350
+ ` 95% interval: [${(result.liftInterval.low * 100).toFixed(1)}, ${(result.liftInterval.high * 100).toFixed(1)}] pp`,
351
+ )
352
+ console.log(` decision: ${result.decision}`)
353
+ console.log(` cost: ${JSON.stringify(result.cost)}`)
416
354
 
417
- const winnerSurface = result.winner.surface as string
355
+ const winnerSurface = String(result.candidate.value)
418
356
  if (improved) {
419
357
  console.log(`\n PROMOTED SYSTEM PROMPT:\n${winnerSurface}`)
420
- if (result.winner.rationale) console.log(`\n rationale: ${result.winner.rationale}`)
421
358
  } else {
422
- console.log(' kept baseline (gate did not promote)')
359
+ console.log(' kept baseline (the final-test interval did not clear zero)')
423
360
  console.log(`\n BEST CANDIDATE SURFACE (set as BASELINE_DIRECTIVE to seed next run):\n${winnerSurface}`)
424
361
  }
425
362
  try {