@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
@@ -4,7 +4,13 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4
4
  import test from 'node:test'
5
5
  import { tmpdir } from 'node:os'
6
6
  import { join } from 'node:path'
7
- import { resolveBenchPython, runStagedJudge } from './_harness'
7
+ import {
8
+ benchRoot,
9
+ resolveBenchPython,
10
+ runStagedJudge,
11
+ venvBin,
12
+ venvBinAt,
13
+ } from './_harness'
8
14
 
9
15
  const digest = (bytes: Uint8Array): `sha256:${string}` =>
10
16
  `sha256:${createHash('sha256').update(bytes).digest('hex')}`
@@ -31,6 +37,15 @@ test('resolveBenchPython rejects relative and empty interpreter paths', () => {
31
37
  )
32
38
  })
33
39
 
40
+ test('venv executable paths support package-owned and external environments', () => {
41
+ assert.equal(venvBinAt('/srv/terminal-bench', 'tb'), join('/srv/terminal-bench', 'bin', 'tb'))
42
+ assert.equal(
43
+ venvBinAt('.venv-commit0', 'python'),
44
+ join(benchRoot, '.venv-commit0', 'bin', 'python'),
45
+ )
46
+ assert.equal(venvBin('python'), venvBinAt('.venv', 'python'))
47
+ })
48
+
34
49
  test('runStagedJudge terminates a hung evaluator at its explicit timeout', async () => {
35
50
  const started = Date.now()
36
51
  await assert.rejects(
@@ -82,13 +82,20 @@ export const venvPython = resolveBenchPython()
82
82
  /** Interpreter for a NAMED isolated venv (e.g. `.venv-commit0`). Benches whose pip
83
83
  * deps conflict with the shared `.venv` (commit0 downgrades pydantic/sqlalchemy)
84
84
  * get their own venv and pass its python explicitly — keeping the shared one clean. */
85
- export const venvPythonAt = (venvDir: string): string => join(benchRoot, venvDir, 'bin', 'python')
85
+ export const venvPythonAt = (venvDir: string): string => venvBinAt(venvDir, 'python')
86
+
87
+ /** Resolve an executable in an isolated venv. Relative venv paths are package-owned;
88
+ * absolute paths allow installed consumers to keep large environments elsewhere. */
89
+ export function venvBinAt(venvDir: string, name: string): string {
90
+ return join(resolve(benchRoot, venvDir), 'bin', name)
91
+ }
92
+
86
93
  /** Report/transcript reads are large; 256 MiB matches the SWE harness budget. */
87
94
  export const bigBuffer = 1024 * 1024 * 256
88
95
 
89
96
  /** Path to a named executable inside the bench venv (e.g. `venvBin('tb')`). */
90
97
  export function venvBin(name: string): string {
91
- return join(benchRoot, '.venv', 'bin', name)
98
+ return venvBinAt('.venv', name)
92
99
  }
93
100
 
94
101
  /**
@@ -25,6 +25,7 @@ import {
25
25
  runVenvPython,
26
26
  safeRunId,
27
27
  stageFile,
28
+ venvBinAt,
28
29
  venvPythonAt,
29
30
  } from './_harness'
30
31
  import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
@@ -34,7 +35,7 @@ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './typ
34
35
  // override the env without reloading this module.
35
36
  const terminalBenchVenvDir = (): string => process.env.TERMINAL_BENCH_VENV ?? '.venv-terminal-bench'
36
37
  const terminalBenchPython = (): string => venvPythonAt(terminalBenchVenvDir())
37
- const terminalBenchBin = (): string => join(benchRoot, terminalBenchVenvDir(), 'bin', 'tb')
38
+ const terminalBenchBin = (): string => venvBinAt(terminalBenchVenvDir(), 'tb')
38
39
 
39
40
  // Pinned dataset: the 0.1.1 core set is patched for terminal-bench >=0.2.4 (the
40
41
  // installed CLI) and is the published launch task set. name==version is what `tb
package/src/gate.ts CHANGED
@@ -427,7 +427,7 @@ export async function runGate(opts: RunGateOptions): Promise<GateReport> {
427
427
  root: a.label,
428
428
  nodes: [],
429
429
  total: acc.get(a.label)!.spend,
430
- statusCounts: { done: 0, failed: 0, cancelled: 0, pending: 0 },
430
+ statusCounts: { done: 0, failed: 0, cancelled: 0, pending: 0, waiting: 0 },
431
431
  },
432
432
  }))
433
433
  const equalK = equalKOnCost(equalKArms)
package/src/hev-eval.mts CHANGED
@@ -27,6 +27,7 @@ async function complete(base: string, key: string, model: string, prompt: string
27
27
  async function main(): Promise<void> {
28
28
  const key = process.env.TANGLE_API_KEY
29
29
  if (!key) throw new Error('TANGLE_API_KEY required')
30
+ const apiKey: string = key
30
31
  const base = process.env.ROUTER_BASE ?? 'https://api.together.xyz/v1'
31
32
  const model = process.env.WORKER_MODEL ?? 'meta-llama/Meta-Llama-3-8B-Instruct-Lite'
32
33
  const instruction = process.env.INSTRUCTION_FILE
@@ -51,8 +52,10 @@ async function main(): Promise<void> {
51
52
  let i = 0
52
53
  async function worker(): Promise<void> {
53
54
  while (i < tasks.length) {
54
- const t = tasks[i++]
55
- const reply = await complete(base, key, model, `${instruction}\n\n\`\`\`python\n${t.prompt}\`\`\``, maxTokens)
55
+ const t = tasks[i]
56
+ i += 1
57
+ if (!t) continue
58
+ const reply = await complete(base, apiKey, model, `${instruction}\n\n\`\`\`python\n${t.prompt}\`\`\``, maxTokens)
56
59
  const { pass: p } = await runChecker(t, extractCode(reply))
57
60
  if (p === 1) pass += 1
58
61
  else fails.push(t.taskId)
@@ -1,8 +1,6 @@
1
1
  /**
2
- * SELF-IMPROVEMENT on HumanEval the prompt-sensitive, VISIBLE-ORACLE counterpart
3
- * to the SWE-bench run. Same machinery (improve(surface:'prompt') + gepaProposer +
4
- * held-out gate), but the worker is a single chat completion and the judge is the
5
- * deterministic Docker checker (run the function against its own hidden unit tests).
2
+ * Official GEPA prompt optimization on HumanEval. The worker is a single chat
3
+ * completion and the judge is the deterministic Docker checker.
6
4
  *
7
5
  * WHY this exists: on SWE-bench the same GEPA loop was NULL because the grading test
8
6
  * is withheld — the worker cannot verify, so prompt wording cannot move resolve.
@@ -13,11 +11,22 @@
13
11
  * Worker + reflect models call the zai coding endpoint directly (no tangle router,
14
12
  * no WAF, no 503): TANGLE_API_KEY=$ZAI_API_KEY ROUTER_BASE=https://api.z.ai/api/coding/paas/v4
15
13
  */
16
- import { improve } from '@tangle-network/agent-runtime'
17
- import type { AgentProfile } from '@tangle-network/agent-interface'
14
+ import {
15
+ improve,
16
+ officialGepa,
17
+ type ReadonlyAgentProfile,
18
+ } from '@tangle-network/agent-runtime'
19
+ import {
20
+ canonicalCandidateDigest,
21
+ type AgentProfile,
22
+ } from '@tangle-network/agent-interface'
18
23
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
19
- import { gepaProposer } from '@tangle-network/agent-eval/campaign'
20
24
  import { extractCode, loadHumanEval, runChecker, type HumanEvalTask } from './benchmarks/humaneval'
25
+ import {
26
+ assertCompleteCost,
27
+ officialOptimizerModel,
28
+ requiredTokenPricing,
29
+ } from './official-optimizer-config.mjs'
21
30
 
22
31
  // The SEED instruction GEPA evolves. Byte-identical to humaneval.ts basePrompt's
23
32
  // solveInstruction so the baseline arm reproduces the plain-prompt denominator.
@@ -26,7 +35,6 @@ const SEED_INSTRUCTION =
26
35
 
27
36
  interface Completion {
28
37
  text: string
29
- usd: number
30
38
  tokIn: number
31
39
  tokOut: number
32
40
  }
@@ -45,10 +53,7 @@ async function complete(base: string, key: string, model: string, prompt: string
45
53
  const text = d.choices?.[0]?.message?.content ?? ''
46
54
  const tokIn = d.usage?.prompt_tokens ?? 0
47
55
  const tokOut = d.usage?.completion_tokens ?? 0
48
- // zai glm pricing is ~ $0.6/M in, $2.2/M out (coding plan); a rough cost tag so the
49
- // stub-guard sees a real backend. Exact cost is not the metric (pass-rate is).
50
- const usd = (tokIn * 0.6 + tokOut * 2.2) / 1_000_000
51
- return { text, usd, tokIn, tokOut }
56
+ return { text, tokIn, tokOut }
52
57
  }
53
58
 
54
59
  async function main(): Promise<void> {
@@ -63,51 +68,81 @@ async function main(): Promise<void> {
63
68
  const reflectBase = process.env.REFLECT_BASE ?? base
64
69
  const reflectKey = process.env.REFLECT_KEY ?? key
65
70
  const trainN = Number(process.env.TRAIN_N ?? 12)
66
- const holdoutN = Number(process.env.HOLDOUT_N ?? 12)
71
+ const selectionN = Number(process.env.SELECTION_N ?? 12)
72
+ const testN = Number(process.env.TEST_N ?? 12)
67
73
  const offset = Number(process.env.OFFSET ?? 80)
68
- // generations=1 never exercises the GEPA Pareto/combine path (the frontier
69
- // needs >=1 completed generation before combine can fire) — default to a
70
- // multi-generation budget so the default run measures the full loop.
71
- const generations = Number(process.env.GENERATIONS ?? 6)
72
- const population = Number(process.env.POPULATION ?? 4)
74
+ const maxEvaluations = Number(process.env.MAX_EVALUATIONS ?? 24)
75
+ const maxProposerCostUsd = Number(process.env.MAX_PROPOSER_COST_USD ?? 5)
73
76
  const workerMaxTokens = Number(process.env.MAX_TOKENS ?? 6000)
74
77
  const reflectMaxTokens = Number(process.env.REFLECT_MAX_TOKENS ?? 8000)
75
78
  const maxConcurrency = Number(process.env.MAX_CONCURRENCY ?? 4)
79
+ const runDir = process.env.RUN_DIR ?? '.runs/humaneval-official-gepa'
80
+ if (process.env.DRYRUN) {
81
+ console.log(
82
+ `DRYRUN: imports OK (improve=${typeof improve}, officialGepa=${typeof officialGepa})`,
83
+ )
84
+ return
85
+ }
86
+ const workerPricing = requiredTokenPricing(process.env, 'WORKER')
87
+ const optimizer = officialOptimizerModel({
88
+ env: process.env,
89
+ model: reflectModel,
90
+ baseUrl: reflectBase,
91
+ apiKey: reflectKey,
92
+ maxCostUsd: maxProposerCostUsd,
93
+ maxOutputTokensPerRequest: reflectMaxTokens,
94
+ })
76
95
 
77
- // TRAIN and HOLDOUT are DISJOINT slices of the harder middle band (offset).
96
+ // All three partitions are disjoint slices of the harder middle band.
78
97
  const train = await loadHumanEval(trainN, offset)
79
- const holdout = await loadHumanEval(holdoutN, offset + trainN)
80
- const byId = new Map<string, HumanEvalTask>([...train, ...holdout].map((t) => [t.taskId, t]))
81
- const allIds = [...byId.keys()]
98
+ const selection = await loadHumanEval(selectionN, offset + trainN)
99
+ const testCases = await loadHumanEval(testN, offset + trainN + selectionN)
100
+ const byId = new Map<string, HumanEvalTask>(
101
+ [...train, ...selection, ...testCases].map((t) => [t.taskId, t]),
102
+ )
82
103
 
83
- console.log('═══ HumanEval self-improvement VISIBLE oracle (deterministic Docker checker) ═══')
84
- console.log(`worker=${workerModel} reflect=${reflectModel} base=${base}`)
104
+ console.log('=== HumanEval prompt optimization with official GEPA ===')
105
+ console.log(`worker=${workerModel} reflect=${reflectModel} base=${base}`)
85
106
  console.log(`train=[${train.map((t) => t.taskId).join(', ')}]`)
86
- console.log(`holdout=[${holdout.map((t) => t.taskId).join(', ')}]`)
87
- console.log(`generations=${generations} population=${population} offset=${offset} maxTokens=${workerMaxTokens}`)
88
- console.log(`≈ ${trainN * (1 + generations * population) + 2 * holdoutN} cells (each = 1 completion + 1 Docker check)\n`)
107
+ console.log(`selection=[${selection.map((t) => t.taskId).join(', ')}]`)
108
+ console.log(`test=[${testCases.map((t) => t.taskId).join(', ')}]`)
109
+ console.log(`maxEvaluations=${maxEvaluations} maxProposerCostUsd=${maxProposerCostUsd} offset=${offset} maxTokens=${workerMaxTokens}`)
110
+ console.log(`runDir=${runDir}\n`)
89
111
 
90
112
  const stats = { n: 0 }
91
- const agent = async (surface: unknown, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
92
- const instr = String(surface)
113
+ const agent = async (candidate: ReadonlyAgentProfile, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
114
+ const instr = candidate.prompt?.systemPrompt
115
+ if (instr === undefined) throw new Error('agent: candidate profile has no system prompt')
93
116
  const t = byId.get(scenario.id)
94
117
  if (!t) throw new Error(`agent: unknown scenario ${scenario.id}`)
95
118
  const prompt = `${instr}\n\n\`\`\`python\n${t.prompt}\`\`\``
96
119
  const t0 = Date.now()
97
- const r = await complete(base, key, workerModel, prompt, workerMaxTokens)
98
- const zeroUsage = r.tokIn === 0 && r.tokOut === 0
120
+ const paid = await ctx.cost.runPaidCall({
121
+ channel: 'agent',
122
+ actor: 'humaneval-worker',
123
+ model: workerModel,
124
+ execute: () => complete(base, key, workerModel, prompt, workerMaxTokens),
125
+ receipt: (result) => {
126
+ const usageUnknown = result.tokIn === 0 && result.tokOut === 0
127
+ return {
128
+ model: workerModel,
129
+ inputTokens: result.tokIn,
130
+ outputTokens: result.tokOut,
131
+ customTokenPricing: workerPricing,
132
+ ...(usageUnknown ? { usageUnknown: true } : {}),
133
+ }
134
+ },
135
+ })
136
+ if (!paid.succeeded) throw paid.error
137
+ const r = paid.value
99
138
  const hasText = r.text.trim().length > 0
100
- ctx.cost.observe(zeroUsage && hasText ? Math.max(r.usd, 0.0001) : r.usd, workerModel)
101
- ctx.cost.observeTokens(
102
- zeroUsage && hasText ? { input: Math.max(r.tokIn, 1), output: Math.max(r.tokOut, 1) } : { input: r.tokIn, output: r.tokOut },
103
- )
104
139
  stats.n += 1
105
140
  const codeLen = extractCode(r.text).length
106
141
  console.log(` [agent] ${scenario.id} instr=${instr.length}c code=${codeLen}b tok=in:${r.tokIn}/out:${r.tokOut} ${Math.round((Date.now() - t0) / 1000)}s`)
107
142
  return hasText ? r.text : null
108
143
  }
109
144
 
110
- const judge: JudgeConfig<string, Scenario> = {
145
+ const judge: JudgeConfig<string | null, Scenario> = {
111
146
  name: 'humaneval-docker',
112
147
  dimensions: [{ key: 'pass', description: 'the completed function passes its hidden unit tests (deterministic Docker checker)' }],
113
148
  async score({ artifact, scenario }) {
@@ -137,49 +172,59 @@ async function main(): Promise<void> {
137
172
  }
138
173
 
139
174
  const profile: AgentProfile = { name: 'hev-solver', prompt: { systemPrompt: SEED_INSTRUCTION } }
140
- const proposer = gepaProposer({
141
- llm: { baseUrl: reflectBase, apiKey: reflectKey },
142
- model: reflectModel,
143
- target:
144
- 'the instruction/system prompt strategy for a SMALL model completing Python functions to pass hidden unit tests. ' +
145
- 'Propose SUBSTANTIALLY different strategies, not wording tweaks: e.g. require the model to first reason step-by-step ' +
146
- 'about the algorithm and edge cases (empty inputs, off-by-one, boundary values, types) in a brief plan or comments ' +
147
- 'BEFORE writing the code; provide a short worked example; or add an explicit self-check against the docstring. ' +
148
- 'Bold rewrites that change model BEHAVIOR beat cosmetic edits.',
149
- maxTokens: reflectMaxTokens,
150
- temperature: 0.7,
151
- })
152
-
153
- const scenarios: Scenario[] = allIds.map((id) => ({ id, kind: 'humaneval' }))
154
- const holdoutScenarios: Scenario[] = holdout.map((t) => ({ id: t.taskId, kind: 'humaneval' }))
175
+ const scenario = (task: HumanEvalTask): Scenario => ({ id: task.taskId, kind: 'humaneval' })
155
176
 
156
- const out = await improve(profile, [], {
177
+ const out = await improve(profile, {
157
178
  surface: 'prompt',
158
- gate: 'holdout',
159
- generator: proposer,
160
- scenarios,
161
- judge,
179
+ executionRef: canonicalCandidateDigest({
180
+ callback: 'bench/hev-improve',
181
+ model: workerModel,
182
+ endpoint: new URL(base).origin,
183
+ maxTokens: workerMaxTokens,
184
+ checker: 'local-python',
185
+ }),
186
+ method: officialGepa<Scenario, string | null>({
187
+ objective:
188
+ 'Improve the complete instruction for a small model that writes Python functions which pass hidden unit tests.',
189
+ background:
190
+ 'Prefer behavioral strategies over wording changes. Address algorithm choice, edge cases, boundary values, type behavior, and self-checking. Return only the complete instruction.',
191
+ recipe: {
192
+ kind: 'engine',
193
+ run: {
194
+ engine: 'gepa',
195
+ maxEvaluations,
196
+ maxProposerCostUsd,
197
+ },
198
+ },
199
+ optimizer,
200
+ resume: 'if-compatible',
201
+ trustResumeState: true,
202
+ describeScenario: (item) => ({ prompt: byId.get(item.id)?.prompt ?? item.id }),
203
+ }),
204
+ trainScenarios: train.map(scenario),
205
+ selectionScenarios: selection.map(scenario),
206
+ testScenarios: testCases.map(scenario),
207
+ judges: [judge],
162
208
  agent,
163
209
  expectUsage: 'warn',
164
- // rawTraceContext stays OFF deliberately: it swaps the distilled findings for
165
- // filesystem paths + grep/cat instructions (rawTraceDistiller), which only a
166
- // coding harness can execute. This run's proposer is prompt-tier (gepaProposer
167
- // — a single LLM call that cannot run grep), so the trace evidence arrives via
168
- // the judge's traceback notes + the breakdown's `emitted` excerpt instead.
169
- budget: { generations, populationSize: population, holdoutScenarios, maxConcurrency, reps: 1 },
170
- llm: { baseUrl: reflectBase, apiKey: reflectKey, model: reflectModel },
210
+ maxConcurrency,
211
+ reps: 1,
212
+ runDir,
213
+ optimizationRunOptions: {
214
+ expectUsage: 'warn',
215
+ maxConcurrency,
216
+ reps: 1,
217
+ },
171
218
  })
172
219
 
173
- console.log('\n═══ RESULT ═══')
174
- console.log(`gateDecision=${out.gateDecision} shipped=${out.shipped} lift=${out.lift}`)
175
- console.log(`baseline holdout pass-rate = ${out.raw.baseline.compositeMean}`)
176
- console.log(`winner holdout pass-rate = ${out.raw.winner.compositeMean}`)
177
- console.log(`baseline per-scenario: ${JSON.stringify(out.raw.baseline.perScenario)}`)
178
- console.log(`winner per-scenario: ${JSON.stringify(out.raw.winner.perScenario)}`)
179
- if (out.raw.winner.label) console.log(`winner label : ${out.raw.winner.label}`)
180
- if ((out.raw.winner as { surface?: unknown }).surface) {
181
- console.log(`winner instruction:\n${String((out.raw.winner as { surface?: unknown }).surface).slice(0, 1200)}`)
182
- }
220
+ assertCompleteCost('humaneval official GEPA run', out.cost)
221
+ console.log('\n=== RESULT ===')
222
+ console.log(`decision=${out.decision} lift=${out.lift} interval=[${out.liftInterval.low}, ${out.liftInterval.high}]`)
223
+ console.log(`baseline test pass-rate=${out.raw.best.baselineComposite}`)
224
+ console.log(`winner test pass-rate=${out.raw.best.winnerComposite}`)
225
+ console.log(`test scenarios=${JSON.stringify(out.raw.best.scenarioScores)}`)
226
+ console.log(`cost=${JSON.stringify(out.cost)}`)
227
+ console.log(`winner instruction:\n${String(out.candidate.value).slice(0, 2000)}`)
183
228
  }
184
229
 
185
230
  main().catch((e) => {
@@ -0,0 +1,89 @@
1
+ function requiredNonNegativeNumber(
2
+ env: NodeJS.ProcessEnv,
3
+ name: string,
4
+ ): number {
5
+ const raw = env[name]
6
+ if (raw === undefined || raw.trim() === '') {
7
+ throw new Error(`env ${name} is required`)
8
+ }
9
+ const value = Number(raw)
10
+ if (!Number.isFinite(value) || value < 0) {
11
+ throw new Error(`env ${name} must be a finite non-negative number`)
12
+ }
13
+ return value
14
+ }
15
+
16
+ function positiveInteger(
17
+ env: NodeJS.ProcessEnv,
18
+ name: string,
19
+ fallback: number,
20
+ ): number {
21
+ const value = Number(env[name] ?? fallback)
22
+ if (!Number.isSafeInteger(value) || value <= 0) {
23
+ throw new Error(`env ${name} must be a positive integer`)
24
+ }
25
+ return value
26
+ }
27
+
28
+ export function requiredTokenPricing(
29
+ env: NodeJS.ProcessEnv,
30
+ prefix: string,
31
+ ) {
32
+ return {
33
+ inputUsdPerMillion: requiredNonNegativeNumber(
34
+ env,
35
+ `${prefix}_INPUT_USD_PER_MILLION`,
36
+ ),
37
+ cachedInputUsdPerMillion: requiredNonNegativeNumber(
38
+ env,
39
+ `${prefix}_CACHED_INPUT_USD_PER_MILLION`,
40
+ ),
41
+ cacheWriteUsdPerMillion: requiredNonNegativeNumber(
42
+ env,
43
+ `${prefix}_CACHE_WRITE_USD_PER_MILLION`,
44
+ ),
45
+ outputUsdPerMillion: requiredNonNegativeNumber(
46
+ env,
47
+ `${prefix}_OUTPUT_USD_PER_MILLION`,
48
+ ),
49
+ }
50
+ }
51
+
52
+ export function officialOptimizerModel(options: {
53
+ env: NodeJS.ProcessEnv
54
+ model: string
55
+ baseUrl: string
56
+ apiKey: string
57
+ maxCostUsd: number
58
+ maxOutputTokensPerRequest: number
59
+ envPrefix?: string
60
+ }) {
61
+ const { env } = options
62
+ const envPrefix = options.envPrefix ?? 'REFLECT'
63
+ return {
64
+ model: options.model,
65
+ baseUrl: options.baseUrl,
66
+ apiKey: options.apiKey,
67
+ budget: {
68
+ maxCostUsd: options.maxCostUsd,
69
+ maxRequests: positiveInteger(env, `${envPrefix}_MAX_REQUESTS`, 100),
70
+ maxRequestBytes: positiveInteger(env, `${envPrefix}_MAX_REQUEST_BYTES`, 2_000_000),
71
+ maxResponseBytes: positiveInteger(env, `${envPrefix}_MAX_RESPONSE_BYTES`, 2_000_000),
72
+ maxOutputTokensPerRequest: options.maxOutputTokensPerRequest,
73
+ requestTimeoutMs: positiveInteger(env, `${envPrefix}_REQUEST_TIMEOUT_MS`, 300_000),
74
+ pricing: requiredTokenPricing(env, envPrefix),
75
+ },
76
+ }
77
+ }
78
+
79
+ export function assertCompleteCost(
80
+ label: string,
81
+ cost: { accountingComplete: boolean; incompleteReasons: readonly string[] },
82
+ ): void {
83
+ if (cost.accountingComplete) return
84
+ const reasons =
85
+ cost.incompleteReasons.length > 0
86
+ ? cost.incompleteReasons.join('; ')
87
+ : 'no incomplete reason was recorded'
88
+ throw new Error(`${label}: cost accounting is incomplete: ${reasons}`)
89
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ assertCompleteCost,
4
+ officialOptimizerModel,
5
+ requiredTokenPricing,
6
+ } from './official-optimizer-config.mts'
7
+
8
+ const pricingEnv = {
9
+ OPT_INPUT_USD_PER_MILLION: '1',
10
+ OPT_CACHED_INPUT_USD_PER_MILLION: '0.1',
11
+ OPT_CACHE_WRITE_USD_PER_MILLION: '1.25',
12
+ OPT_OUTPUT_USD_PER_MILLION: '5',
13
+ }
14
+
15
+ describe('official optimizer configuration', () => {
16
+ it('builds a bounded model configuration from an arbitrary environment prefix', () => {
17
+ const model = officialOptimizerModel({
18
+ env: {
19
+ ...pricingEnv,
20
+ OPT_MAX_REQUESTS: '7',
21
+ OPT_MAX_REQUEST_BYTES: '1000',
22
+ OPT_MAX_RESPONSE_BYTES: '2000',
23
+ OPT_REQUEST_TIMEOUT_MS: '3000',
24
+ },
25
+ envPrefix: 'OPT',
26
+ model: 'test-model',
27
+ baseUrl: 'http://127.0.0.1:8080/v1',
28
+ apiKey: 'test-key',
29
+ maxCostUsd: 2,
30
+ maxOutputTokensPerRequest: 4000,
31
+ })
32
+
33
+ expect(model).toEqual({
34
+ model: 'test-model',
35
+ baseUrl: 'http://127.0.0.1:8080/v1',
36
+ apiKey: 'test-key',
37
+ budget: {
38
+ maxCostUsd: 2,
39
+ maxRequests: 7,
40
+ maxRequestBytes: 1000,
41
+ maxResponseBytes: 2000,
42
+ maxOutputTokensPerRequest: 4000,
43
+ requestTimeoutMs: 3000,
44
+ pricing: {
45
+ inputUsdPerMillion: 1,
46
+ cachedInputUsdPerMillion: 0.1,
47
+ cacheWriteUsdPerMillion: 1.25,
48
+ outputUsdPerMillion: 5,
49
+ },
50
+ },
51
+ })
52
+ })
53
+
54
+ it('requires every token price instead of inventing cost data', () => {
55
+ expect(() =>
56
+ requiredTokenPricing(
57
+ {
58
+ ...pricingEnv,
59
+ OPT_OUTPUT_USD_PER_MILLION: undefined,
60
+ },
61
+ 'OPT',
62
+ ),
63
+ ).toThrow('env OPT_OUTPUT_USD_PER_MILLION is required')
64
+ })
65
+
66
+ it('rejects invalid request limits before constructing the optimizer', () => {
67
+ expect(() =>
68
+ officialOptimizerModel({
69
+ env: { ...pricingEnv, OPT_MAX_REQUESTS: '0' },
70
+ envPrefix: 'OPT',
71
+ model: 'test-model',
72
+ baseUrl: 'http://127.0.0.1:8080/v1',
73
+ apiKey: 'test-key',
74
+ maxCostUsd: 2,
75
+ maxOutputTokensPerRequest: 4000,
76
+ }),
77
+ ).toThrow('env OPT_MAX_REQUESTS must be a positive integer')
78
+ })
79
+
80
+ it('rejects incomplete cost records', () => {
81
+ expect(() =>
82
+ assertCompleteCost('official optimizer', {
83
+ accountingComplete: false,
84
+ incompleteReasons: ['provider omitted usage'],
85
+ }),
86
+ ).toThrow('official optimizer: cost accounting is incomplete: provider omitted usage')
87
+ })
88
+ })
package/src/profiles.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * "operator type" or "analyst type" — there are profiles + the operator toolbox they use to manage
6
6
  * each other (in-process via the Scope, in a sandbox via the same verbs exposed as MCP tools).
7
7
  *
8
- * The directory is OPTIMIZED over time by the agent-eval RSI loop (`runImprovementLoop`): `gepaProposer`
9
- * evolves the prompts, a `skillOptProposer` evolves `skills`, Pareto-tracked + holdout-gated.
8
+ * Complete agent-eval methods optimize these profiles over explicit train,
9
+ * selection, and final-test partitions.
10
10
  * The trace-analyst's findings are the optimizer's input — the loop improves the profile that the
11
11
  * findings say is weak.
12
12
  */
@@ -285,31 +285,35 @@ describe('backfillSweArena', () => {
285
285
  expect(stats.workerSessionsJoined).toBe(2)
286
286
  })
287
287
 
288
- it('marks a session with no readable parts as an incomplete gap line', async () => {
289
- await buildFixtureTree()
290
- await buildFixtureDb()
291
- const { DatabaseSync } = await import('node:sqlite')
292
- const db = new DatabaseSync(dbPath)
293
- // Session row survives; its message parts do not.
294
- db.exec("DELETE FROM part; DELETE FROM message")
295
- db.close()
288
+ it(
289
+ 'marks a session with no readable parts as an incomplete gap line',
290
+ async () => {
291
+ await buildFixtureTree()
292
+ await buildFixtureDb()
293
+ const { DatabaseSync } = await import('node:sqlite')
294
+ const db = new DatabaseSync(dbPath)
295
+ // Session row survives; its message parts do not.
296
+ db.exec("DELETE FROM part; DELETE FROM message")
297
+ db.close()
296
298
 
297
- const { lines, stats } = await backfillSweArena(outDir, {
298
- opencodeDb: dbPath,
299
- claudeProjectsDir: projectsDir,
300
- })
301
- const joinedCwd = lines.filter((l) => l.role === 'worker' && l.outcome.metrics.has_session === true)
302
- expect(joinedCwd.length).toBeGreaterThan(0)
303
- for (const worker of joinedCwd) {
304
- expect(worker.messages).toHaveLength(0)
305
- expect(worker.outcome.is_completed).toBe(false)
306
- expect(worker.provenance.gap).toMatch(/no readable message parts/)
307
- }
308
- // The store-integrity failure is counted apart from the cwd-join failure.
309
- expect(stats.workerSessionsEmpty).toBe(2)
310
- expect(stats.workerCwdsMissed).toBe(1)
311
- expect(stats.workerSessionsJoined).toBe(0)
312
- })
299
+ const { lines, stats } = await backfillSweArena(outDir, {
300
+ opencodeDb: dbPath,
301
+ claudeProjectsDir: projectsDir,
302
+ })
303
+ const joinedCwd = lines.filter((l) => l.role === 'worker' && l.outcome.metrics.has_session === true)
304
+ expect(joinedCwd.length).toBeGreaterThan(0)
305
+ for (const worker of joinedCwd) {
306
+ expect(worker.messages).toHaveLength(0)
307
+ expect(worker.outcome.is_completed).toBe(false)
308
+ expect(worker.provenance.gap).toMatch(/no readable message parts/)
309
+ }
310
+ // The store-integrity failure is counted apart from the cwd-join failure.
311
+ expect(stats.workerSessionsEmpty).toBe(2)
312
+ expect(stats.workerCwdsMissed).toBe(1)
313
+ expect(stats.workerSessionsJoined).toBe(0)
314
+ },
315
+ 15_000,
316
+ )
313
317
 
314
318
  it('fails loud on a cell cache that carries no scenarioId/rep identity', async () => {
315
319
  await buildFixtureTree()