@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.
- package/CHANGELOG.md +13 -0
- package/README.md +7 -0
- package/dist/adapters.js +23 -23
- package/dist/benchmarks/_harness.d.ts +4 -1
- package/dist/benchmarks/_harness.js +3 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-YSMEKBTD.js → chunk-5FEQDSCT.js} +2 -2
- package/dist/{chunk-PB64GYIG.js → chunk-67ACKDCX.js} +2 -2
- package/dist/{chunk-NQG5XDSB.js → chunk-7FKBWOQT.js} +2 -2
- package/dist/{chunk-K3BQGZCT.js → chunk-BEN6IF2X.js} +2 -2
- package/dist/{chunk-SHYIRB7I.js → chunk-BZY5QARD.js} +2 -2
- package/dist/{chunk-KP5KD6EN.js → chunk-CLIKAXKH.js} +2 -2
- package/dist/{chunk-XKEFIFIC.js → chunk-CWIOBFSP.js} +2 -2
- package/dist/{chunk-IA2FBTWC.js → chunk-CXDUTWQE.js} +2 -2
- package/dist/{chunk-RH5F53JT.js → chunk-DWALFME7.js} +2 -2
- package/dist/{chunk-V7AEBY6U.js → chunk-EEOC6QPJ.js} +21 -21
- package/dist/{chunk-WSKWVEQB.js → chunk-EIETHPD5.js} +7 -3
- package/dist/chunk-EIETHPD5.js.map +1 -0
- package/dist/{chunk-SHM6MRRF.js → chunk-GC2EPS6L.js} +2 -2
- package/dist/{chunk-IZ5M6OAC.js → chunk-GCHL6XPM.js} +2 -2
- package/dist/{chunk-IFVINJ4B.js → chunk-HQ5HCCKF.js} +2 -2
- package/dist/{chunk-7GRVHU22.js → chunk-HVW25KSX.js} +2 -2
- package/dist/{chunk-3U5TXJZS.js → chunk-J6BU3NTM.js} +2 -2
- package/dist/{chunk-XYA4XSNU.js → chunk-JSQOUKXS.js} +4 -3
- package/dist/chunk-JSQOUKXS.js.map +1 -0
- package/dist/{chunk-SVR2LKYI.js → chunk-NRMGT25X.js} +2 -2
- package/dist/{chunk-MQMRLGOG.js → chunk-QZZEAHWJ.js} +2 -2
- package/dist/{chunk-RCYQEFNX.js → chunk-WG7TM7UV.js} +3 -3
- package/dist/{chunk-HWST3SED.js → chunk-XXFF3RRD.js} +2 -2
- package/dist/{chunk-SFLA7OH3.js → chunk-ZFNOM7WR.js} +3 -3
- package/dist/{chunk-Z4TZ76N7.js → chunk-ZNCCYTFG.js} +2 -2
- package/dist/index.js +23 -23
- package/package.json +4 -4
- package/pier_agents/candidate_contract.py +238 -24
- package/pier_agents/tangle_candidate.py +75 -5
- package/scripts/trata-hedge/README.md +6 -5
- package/scripts/verify-packed-consumer.mjs +65 -1
- package/scripts/verify-pier-agent.mts +6 -4
- package/src/benchmarks/_harness.test.mts +16 -1
- package/src/benchmarks/_harness.ts +9 -2
- package/src/benchmarks/terminal-bench.ts +2 -1
- package/src/gate.ts +1 -1
- package/src/hev-eval.mts +5 -2
- package/src/hev-improve.mts +118 -73
- package/src/official-optimizer-config.mts +89 -0
- package/src/official-optimizer-config.test.mts +88 -0
- package/src/profiles.ts +2 -2
- package/src/rollout-ledger/backfill-swe-arena.test.mts +28 -24
- package/src/smoke-structural-rollout.mts +15 -9
- package/src/swe-arena/activation.mts +1 -4
- package/src/swe-arena/activation.test.mts +10 -13
- package/src/swe-arena/gepa-seat.mts +425 -131
- package/src/swe-arena/gepa-seat.test.mts +524 -100
- package/src/swe-arena/implementation-ref.test.mts +64 -0
- package/src/swe-arena/implementation-ref.ts +62 -0
- package/src/swe-arena/outer-loop.mts +103 -76
- package/src/swe-arena/proposer-fanout.mts +51 -36
- package/src/swe-arena/proposer-fanout.test.mts +0 -1
- package/src/swe-arena/proposer-provenance.mts +11 -16
- package/src/swe-arena/scratch-worktree.test.mts +55 -0
- package/src/swe-arena/scratch-worktree.ts +34 -0
- package/src/swe-code-improve.mts +24 -25
- package/src/swe-improve.mts +129 -96
- package/src/swe-local-proof.mts +6 -1
- package/src/swe-stream.mts +4 -2
- package/src/tb-container-executor.test.mts +30 -6
- package/src/tb-supervisor-sidecar.mts +2 -1
- package/src/trata-gepa.mts +182 -245
- package/dist/chunk-WSKWVEQB.js.map +0 -1
- package/dist/chunk-XYA4XSNU.js.map +0 -1
- package/src/live-improve-campaign-mbpp.mts +0 -641
- package/src/live-improve-campaign.mts +0 -500
- package/src/swe-arena/lineage-record.mts +0 -164
- package/src/swe-arena/lineage-record.test.mts +0 -115
- /package/dist/{chunk-YSMEKBTD.js.map → chunk-5FEQDSCT.js.map} +0 -0
- /package/dist/{chunk-PB64GYIG.js.map → chunk-67ACKDCX.js.map} +0 -0
- /package/dist/{chunk-NQG5XDSB.js.map → chunk-7FKBWOQT.js.map} +0 -0
- /package/dist/{chunk-K3BQGZCT.js.map → chunk-BEN6IF2X.js.map} +0 -0
- /package/dist/{chunk-SHYIRB7I.js.map → chunk-BZY5QARD.js.map} +0 -0
- /package/dist/{chunk-KP5KD6EN.js.map → chunk-CLIKAXKH.js.map} +0 -0
- /package/dist/{chunk-XKEFIFIC.js.map → chunk-CWIOBFSP.js.map} +0 -0
- /package/dist/{chunk-IA2FBTWC.js.map → chunk-CXDUTWQE.js.map} +0 -0
- /package/dist/{chunk-RH5F53JT.js.map → chunk-DWALFME7.js.map} +0 -0
- /package/dist/{chunk-V7AEBY6U.js.map → chunk-EEOC6QPJ.js.map} +0 -0
- /package/dist/{chunk-SHM6MRRF.js.map → chunk-GC2EPS6L.js.map} +0 -0
- /package/dist/{chunk-IZ5M6OAC.js.map → chunk-GCHL6XPM.js.map} +0 -0
- /package/dist/{chunk-IFVINJ4B.js.map → chunk-HQ5HCCKF.js.map} +0 -0
- /package/dist/{chunk-7GRVHU22.js.map → chunk-HVW25KSX.js.map} +0 -0
- /package/dist/{chunk-3U5TXJZS.js.map → chunk-J6BU3NTM.js.map} +0 -0
- /package/dist/{chunk-SVR2LKYI.js.map → chunk-NRMGT25X.js.map} +0 -0
- /package/dist/{chunk-MQMRLGOG.js.map → chunk-QZZEAHWJ.js.map} +0 -0
- /package/dist/{chunk-RCYQEFNX.js.map → chunk-WG7TM7UV.js.map} +0 -0
- /package/dist/{chunk-HWST3SED.js.map → chunk-XXFF3RRD.js.map} +0 -0
- /package/dist/{chunk-SFLA7OH3.js.map → chunk-ZFNOM7WR.js.map} +0 -0
- /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 {
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
168
|
+
id: workerId,
|
|
168
169
|
status: w.status,
|
|
169
170
|
command: out.command,
|
|
170
171
|
exitCode: out.exitCode ?? null,
|
package/src/trata-gepa.mts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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 {
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
167
|
-
const
|
|
168
|
-
const
|
|
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
|
-
//
|
|
173
|
-
|
|
174
|
-
const tasks = await adapter.loadTasks({ limit:
|
|
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,
|
|
185
|
-
const
|
|
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}
|
|
207
|
+
`[trata-gepa] worker=${model} reflect=${reflectModel} rounds=${kRounds} train=${train.length} selection=${selection.length} test=${test.length}`,
|
|
190
208
|
)
|
|
191
209
|
|
|
192
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
207
|
-
{ role: 'system', content:
|
|
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
|
|
216
|
-
{ role: 'system', content:
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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.
|
|
392
|
-
console.log(` baseline
|
|
393
|
-
console.log(` winner
|
|
394
|
-
console.log(`
|
|
395
|
-
console.log(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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.
|
|
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 (
|
|
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 {
|