@tangle-network/agent-bench 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +7 -0
  3. package/package.json +4 -4
  4. package/scripts/trata-hedge/README.md +6 -5
  5. package/src/gate.ts +1 -1
  6. package/src/hev-eval.mts +5 -2
  7. package/src/hev-improve.mts +118 -73
  8. package/src/official-optimizer-config.mts +89 -0
  9. package/src/official-optimizer-config.test.mts +88 -0
  10. package/src/profiles.ts +2 -2
  11. package/src/rollout-ledger/backfill-swe-arena.test.mts +28 -24
  12. package/src/smoke-structural-rollout.mts +15 -9
  13. package/src/swe-arena/activation.mts +1 -4
  14. package/src/swe-arena/activation.test.mts +10 -13
  15. package/src/swe-arena/gepa-seat.mts +425 -131
  16. package/src/swe-arena/gepa-seat.test.mts +524 -100
  17. package/src/swe-arena/implementation-ref.test.mts +64 -0
  18. package/src/swe-arena/implementation-ref.ts +62 -0
  19. package/src/swe-arena/outer-loop.mts +103 -76
  20. package/src/swe-arena/proposer-fanout.mts +51 -36
  21. package/src/swe-arena/proposer-fanout.test.mts +0 -1
  22. package/src/swe-arena/proposer-provenance.mts +11 -16
  23. package/src/swe-arena/scratch-worktree.test.mts +55 -0
  24. package/src/swe-arena/scratch-worktree.ts +34 -0
  25. package/src/swe-code-improve.mts +24 -25
  26. package/src/swe-improve.mts +129 -96
  27. package/src/swe-local-proof.mts +6 -1
  28. package/src/swe-stream.mts +4 -2
  29. package/src/tb-container-executor.test.mts +30 -6
  30. package/src/tb-supervisor-sidecar.mts +2 -1
  31. package/src/trata-gepa.mts +182 -245
  32. package/src/live-improve-campaign-mbpp.mts +0 -641
  33. package/src/live-improve-campaign.mts +0 -500
  34. package/src/swe-arena/lineage-record.mts +0 -164
  35. package/src/swe-arena/lineage-record.test.mts +0 -115
@@ -27,21 +27,19 @@ import { join } from 'node:path'
27
27
  import {
28
28
  DEFAULT_GEPA_PYTHON,
29
29
  isGepaSeat,
30
- loadGepaMethodFactory,
31
30
  probeGepaRuntime,
32
- type CampaignModuleImport,
33
31
  } from './gepa-seat.mts'
34
32
  import { run } from './proc.ts'
35
33
  import type { ProposerSpec } from './proposer-fanout.mts'
36
34
 
37
35
  export interface ProposerModelProvenance {
38
36
  name: string
39
- /** Absent on a GEN-6 engine seat (see `engine`). */
37
+ /** Absent on an engine seat (see `engine`). */
40
38
  harness: ProposerSpec['harness']
41
39
  /** Explicit model pin from the spec (threaded as `-m`), or null when the
42
40
  * seat runs the CLI's own resolved default. */
43
41
  pinnedModel: string | null
44
- /** `<harness> --version` stdout (trimmed). For a GEN-6 gepa seat this is
42
+ /** `<harness> --version` stdout (trimmed). For a GEPA seat this is
45
43
  * the bridge python's `--version` output — the runtime that authors. */
46
44
  harnessVersion: string
47
45
  /** claude seats only: the settings default model the logged-in CLI resolves
@@ -51,13 +49,13 @@ export interface ProposerModelProvenance {
51
49
  /** codex seats only: `codex login status` stdout (trimmed). */
52
50
  authStatus: string | null
53
51
  merge: boolean
54
- /** GEN-6 gepa seat: the engine name from the spec. */
52
+ /** GEPA seat: the engine name from the spec. */
55
53
  engine?: 'gepa' | 'omni'
56
- /** GEN-6 gepa seat: the ONE change-space file GEPA optimizes. */
54
+ /** GEPA seat: the one change-space file GEPA optimizes. */
57
55
  surface?: string
58
- /** GEN-6 gepa seat: installed gepa version ('source' for a source pin). */
56
+ /** GEPA seat: installed GEPA version ('source' for a source pin). */
59
57
  gepaVersion?: string
60
- /** GEN-6 gepa seat: the Python bridge module the seat runs. */
58
+ /** GEPA seat: the Python bridge module the seat runs. */
61
59
  bridge?: string
62
60
  }
63
61
 
@@ -92,17 +90,16 @@ export function claudeSettingsModel(
92
90
  }
93
91
 
94
92
  /** Capture per-proposer model provenance. Throws when any configured harness
95
- * binary is missing/broken, when a codex seat is not logged in, or — GEN-6 —
96
- * when a gepa seat's runtime is incomplete: the installed agent-eval must
97
- * export `gepaOptimizationMethod` and the Python bridge + GEPA engine must
98
- * import (probeGepaRuntime carries the exact install instructions). A dead
99
- * seat fails the launch at t=0, never a mid-run candidate slot. */
93
+ * binary is missing/broken, when a codex seat is not logged in, or when
94
+ * a GEPA seat's Python bridge or engine cannot import
95
+ * (`probeGepaRuntime` carries the exact install instructions). A dead seat
96
+ * fails the launch at t=0, never a mid-run candidate slot. The TypeScript
97
+ * adapter is a pinned package dependency and therefore checked at install. */
100
98
  export async function captureProposerProvenance(
101
99
  proposers: ProposerSpec[],
102
100
  deps: {
103
101
  exec?: VersionExec
104
102
  readSettingsModel?: () => string | null
105
- importCampaign?: CampaignModuleImport
106
103
  } = {},
107
104
  ): Promise<ProvenanceCaptureRecord> {
108
105
  const exec = deps.exec ?? defaultExec
@@ -112,8 +109,6 @@ export async function captureProposerProvenance(
112
109
  const gepaBySeat = new Map<string, { pythonVersion: string; gepaVersion: string }>()
113
110
  const gepaSeats = proposers.filter(isGepaSeat)
114
111
  if (gepaSeats.length > 0) {
115
- // Node side first: the adapter export (fails loud with upgrade hint).
116
- await loadGepaMethodFactory(...(deps.importCampaign ? [deps.importCampaign] : []))
117
112
  for (const seat of gepaSeats) {
118
113
  gepaBySeat.set(seat.name, await probeGepaRuntime(seat.python ?? DEFAULT_GEPA_PYTHON, exec, seat.name))
119
114
  }
@@ -0,0 +1,55 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { runOk } from './proc.ts'
6
+ import {
7
+ createDetachedWorktree,
8
+ pruneDetachedWorktrees,
9
+ removeDetachedWorktree,
10
+ } from './scratch-worktree.ts'
11
+
12
+ describe('scratch worktrees', () => {
13
+ const roots: string[] = []
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
17
+ })
18
+
19
+ it('creates and removes a parallel batch without pruning live metadata', async () => {
20
+ const repository = await mkdtemp(join(tmpdir(), 'scratch-worktree-repo-'))
21
+ const output = await mkdtemp(join(tmpdir(), 'scratch-worktree-out-'))
22
+ roots.push(repository, output)
23
+ await runOk('git', ['init', '-q', '-b', 'main', repository])
24
+ await runOk('git', ['-C', repository, 'config', 'user.email', 'test@example.com'])
25
+ await runOk('git', ['-C', repository, 'config', 'user.name', 'Test'])
26
+ await writeFile(join(repository, 'seed.txt'), 'seed\n')
27
+ await runOk('git', ['-C', repository, 'add', 'seed.txt'])
28
+ await runOk('git', ['-C', repository, 'commit', '-q', '-m', 'seed'])
29
+ const commit = (
30
+ await runOk('git', ['-C', repository, 'rev-parse', 'HEAD'])
31
+ ).stdout.trim()
32
+ for (let batch = 0; batch < 5; batch += 1) {
33
+ const worktrees = Array.from(
34
+ { length: 8 },
35
+ (_, index) => join(output, `batch-${batch}-candidate-${index}`),
36
+ )
37
+ await pruneDetachedWorktrees(repository)
38
+ await Promise.all(
39
+ worktrees.map((worktree) => createDetachedWorktree(repository, commit, worktree)),
40
+ )
41
+ expect(
42
+ await Promise.all(
43
+ worktrees.map((worktree) => readFile(join(worktree, 'seed.txt'), 'utf8')),
44
+ ),
45
+ ).toEqual(Array.from({ length: 8 }, () => 'seed\n'))
46
+ await Promise.all(
47
+ worktrees.map((worktree) => removeDetachedWorktree(repository, worktree)),
48
+ )
49
+ }
50
+ const listed = (
51
+ await runOk('git', ['-C', repository, 'worktree', 'list', '--porcelain'])
52
+ ).stdout
53
+ expect(listed.match(/^worktree /gmu)).toHaveLength(1)
54
+ })
55
+ })
@@ -0,0 +1,34 @@
1
+ import { rm } from 'node:fs/promises'
2
+ import { run, runOk } from './proc'
3
+
4
+ export async function createDetachedWorktree(
5
+ repository: string,
6
+ commit: string,
7
+ destination: string,
8
+ ): Promise<void> {
9
+ await run('git', ['-C', repository, 'worktree', 'remove', '--force', '--', destination])
10
+ await rm(destination, { recursive: true, force: true })
11
+ await runOk('git', ['-C', repository, 'worktree', 'add', '--detach', destination, commit])
12
+ }
13
+
14
+ export async function pruneDetachedWorktrees(repository: string): Promise<void> {
15
+ await runOk('git', ['-C', repository, 'worktree', 'prune'])
16
+ }
17
+
18
+ export async function removeDetachedWorktree(
19
+ repository: string,
20
+ destination: string,
21
+ ): Promise<void> {
22
+ const result = await run('git', [
23
+ '-C',
24
+ repository,
25
+ 'worktree',
26
+ 'remove',
27
+ '--force',
28
+ '--',
29
+ destination,
30
+ ])
31
+ if (result.code !== 0) {
32
+ await rm(destination, { recursive: true, force: true })
33
+ }
34
+ }
@@ -33,7 +33,6 @@ import { spawn, spawnSync } from 'node:child_process'
33
33
  import { existsSync, mkdirSync, symlinkSync } from 'node:fs'
34
34
  import { join } from 'node:path'
35
35
  import { improve, agenticGenerator } from '@tangle-network/agent-runtime'
36
- import type { AgentProfile } from '@tangle-network/agent-interface'
37
36
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
38
37
  import { createSweBenchAdapter } from './benchmarks/swe-bench'
39
38
  import type { BenchTask } from './benchmarks/types'
@@ -108,7 +107,6 @@ async function main(): Promise<void> {
108
107
  if (!routerKey) throw new Error('TANGLE_API_KEY required (worker calls the router)')
109
108
  const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
110
109
  const workerModel = process.env.WORKER_MODEL ?? 'glm-4.6'
111
- const reflectModel = process.env.REFLECT_MODEL ?? 'glm-4.6'
112
110
  const trainIds = (process.env.TRAIN_IDS ?? 'psf__requests-2931,pallets__flask-5014').split(',').map((s) => s.trim()).filter(Boolean)
113
111
  const holdoutIds = (process.env.HOLDOUT_IDS ?? 'psf__requests-1142,psf__requests-1921').split(',').map((s) => s.trim()).filter(Boolean)
114
112
  const generations = Number(process.env.GENERATIONS ?? 1)
@@ -127,7 +125,7 @@ async function main(): Promise<void> {
127
125
  const allIds = [...new Set([...trainIds, ...holdoutIds])]
128
126
 
129
127
  console.log('=== META-HARNESS on the SWE scaffold — improve(surface:code) ===')
130
- console.log(`worker=${workerModel} reflect=${reflectModel} router=${routerBaseUrl} runTool=${enableRun}`)
128
+ console.log(`worker=${workerModel} router=${routerBaseUrl} runTool=${enableRun}`)
131
129
  console.log(`train=[${trainIds.join(', ')}] holdout=[${holdoutIds.join(', ')}]`)
132
130
  console.log(`generations=${generations} population=${population} innerTurns=${innerTurns} maxTokens=${maxTokens}`)
133
131
  console.log(`repoRoot=${REPO_ROOT} baseRef=${baseRef}`)
@@ -156,15 +154,25 @@ async function main(): Promise<void> {
156
154
  const worktreeRef = isCode ? String((surface as { worktreeRef?: string }).worktreeRef ?? '') : ''
157
155
  const rootDir = isCode && worktreeRef && existsSync(worktreeRef) ? worktreeRef : SWE_MAIN_ROOT
158
156
  const t0 = Date.now()
159
- const r = await runEmit(rootDir, scenario.id, workerEnv, emitTimeoutMs)
157
+ const paid = await ctx.cost.runPaidCall({
158
+ channel: 'agent',
159
+ actor: 'swe-scaffold-worker',
160
+ model: workerModel,
161
+ execute: () => runEmit(rootDir, scenario.id, workerEnv, emitTimeoutMs),
162
+ receipt: (result) => {
163
+ const usageUnknown = result.tokIn === 0 && result.tokOut === 0
164
+ return {
165
+ model: workerModel,
166
+ inputTokens: result.tokIn,
167
+ outputTokens: result.tokOut,
168
+ ...(result.usd > 0 ? { actualCostUsd: result.usd } : {}),
169
+ ...(usageUnknown ? { usageUnknown: true, costUnknown: result.usd <= 0 } : {}),
170
+ }
171
+ },
172
+ })
173
+ if (!paid.succeeded) throw paid.error
174
+ const r = paid.value
160
175
  const hasPatch = r.patch.trim().length > 0
161
- // Report real usage; floor a patch-bearing zero-usage cell so the stub-guard cannot abort on a
162
- // router telemetry gap (lift is judge-derived, so this only affects cost accounting).
163
- const zeroUsage = r.tokIn === 0 && r.tokOut === 0
164
- ctx.cost.observe(zeroUsage && hasPatch ? Math.max(r.usd, 0.0001) : r.usd, workerModel)
165
- ctx.cost.observeTokens(
166
- zeroUsage && hasPatch ? { input: Math.max(r.tokIn, 1), output: Math.max(r.tokOut, 1) } : { input: r.tokIn, output: r.tokOut },
167
- )
168
176
  const files = hasPatch ? [...r.patch.matchAll(/^diff --git a\/(\S+)/gm)].map((m) => m[1]) : []
169
177
  console.log(
170
178
  ` [measure] ${isCode ? 'cand' : 'base'} ${scenario.id} patch=${r.patch.length}b files=[${files.join(', ') || 'none'}] ` +
@@ -173,7 +181,7 @@ async function main(): Promise<void> {
173
181
  return hasPatch ? r.patch : null
174
182
  }
175
183
 
176
- const judge: JudgeConfig<string, Scenario> = {
184
+ const judge: JudgeConfig<string | null, Scenario> = {
177
185
  name: 'swebench-docker',
178
186
  dimensions: [{ key: 'resolved', description: 'FAIL_TO_PASS + PASS_TO_PASS resolved by the official swebench Docker harness' }],
179
187
  async score({ artifact, scenario }) {
@@ -286,11 +294,10 @@ async function main(): Promise<void> {
286
294
  runHarness: runHarness as any,
287
295
  })
288
296
 
289
- const profile: AgentProfile = { name: 'swe-scaffold', prompt: { systemPrompt: '' } }
290
297
  const scenarios: Scenario[] = allIds.map((id) => ({ id, kind: 'swe-bench-verified' }))
291
298
  const holdoutScenarios: Scenario[] = holdoutIds.map((id) => ({ id, kind: 'swe-bench-verified' }))
292
299
 
293
- const out = await improve(profile, [], {
300
+ const out = await improve({
294
301
  surface: 'code',
295
302
  gate: 'holdout',
296
303
  code: { repoRoot: REPO_ROOT, baseRef, worktreeDir, generator },
@@ -301,25 +308,17 @@ async function main(): Promise<void> {
301
308
  agent,
302
309
  expectUsage: 'warn',
303
310
  budget: { generations, populationSize: population, holdoutScenarios, maxConcurrency: 1, reps: 1 },
304
- llm: { baseUrl: routerBaseUrl, apiKey: routerKey, model: reflectModel },
305
311
  })
306
312
 
307
313
  console.log('\n=== RESULT ===')
308
- console.log(`gateDecision=${out.gateDecision} shipped=${out.shipped} lift=${out.lift}`)
314
+ console.log(`decision=${out.decision} lift=${out.lift}`)
309
315
  console.log(`baseline holdout composite = ${out.raw.baseline.compositeMean}`)
310
316
  console.log(`winner holdout composite = ${out.raw.winner.compositeMean}`)
311
317
  console.log(`baseline per-scenario: ${JSON.stringify(out.raw.baseline.perScenario)}`)
312
318
  console.log(`winner per-scenario: ${JSON.stringify(out.raw.winner.perScenario)}`)
313
319
  if (out.raw.winner.label) console.log(`winner label: ${out.raw.winner.label}`)
314
- if (out.raw.winner.summary) console.log(`winner summary: ${out.raw.winner.summary}`)
315
- for (const gen of out.raw.generations ?? []) {
316
- console.log(`\n-- generation ${gen.record.generationIndex} candidates --`)
317
- for (const c of gen.record.candidates) {
318
- const perScenario = (c as { scenarios?: Array<{ scenarioId: string; composite: number }> }).scenarios ?? []
319
- const detail = perScenario.map((s) => `${s.scenarioId}=${s.composite}`).join(' ')
320
- console.log(` candidate ${c.surfaceHash.slice(0, 8)} composite=${c.composite}${c.label ? ` "${c.label}"` : ''} [${detail}]`)
321
- }
322
- }
320
+ if (out.raw.winner.rationale) console.log(`winner rationale: ${out.raw.winner.rationale}`)
321
+ console.log(`generations explored: ${out.generationsExplored ?? 0}`)
323
322
  }
324
323
 
325
324
  main().catch((e) => {
@@ -1,10 +1,9 @@
1
1
  /**
2
- * SELF-IMPROVEMENT on the SEE-able LOCAL SWE-bench path — NO tangle sandbox.
2
+ * Official GEPA prompt optimization on the local SWE-bench path.
3
3
  *
4
- * Composes the three proven pieces into ONE held-out-gated improvement generation:
5
- * 1. `improve({ surface: 'prompt' })` (agent-runtime) drives the loop: it asks
6
- * `gepaProposer` to EVOLVE the SWE agent's system prompt, then measures each
7
- * candidate prompt on real instances and gates the winner on a held-out split.
4
+ * Composes three pieces:
5
+ * 1. `improve({ method: officialGepa(...) })` runs GEPA's upstream
6
+ * Optimize Anything engine on explicit train and selection partitions.
8
7
  * 2. Per candidate + scenario, the `agent` fn runs the LOCAL SWE env
9
8
  * (`createSweBenchEnvironment` + `runAgentic`): clone the instance repo to a
10
9
  * host tmpdir, run the jailed list/read/edit tool loop with the CANDIDATE
@@ -16,23 +15,33 @@
16
15
  * IN-LOOP score is a cheap patch-exists proxy (NOT the Docker judge) so the ONLY
17
16
  * Docker run per cell is the improve judge — one deterministic verdict per cell.
18
17
  *
19
- * Cost per run = T·(1 + G·P) + 2·H cells, each = 1 clone + 1 runAgentic + 1 judge.
20
- *
21
- * TANGLE_API_KEY=… dotenvx run -f …/agent-state.env -- \
22
- * TRAIN_IDS=psf__requests-2931 HOLDOUT_IDS=psf__requests-1142 \
23
- * GENERATIONS=1 POPULATION=1 WORKER_MODEL=glm-4.6 REFLECT_MODEL=glm-4.6 \
18
+ * TANGLE_API_KEY=... dotenvx run -f .../agent-state.env -- \
19
+ * TRAIN_IDS=psf__requests-2931 SELECTION_IDS=pallets__flask-5014 \
20
+ * TEST_IDS=psf__requests-1142,psf__requests-1921 \
21
+ * MAX_EVALUATIONS=4 MAX_PROPOSER_COST_USD=2 \
24
22
  * node_modules/.bin/tsx bench/src/swe-improve.mts
25
23
  */
26
24
  import { execFile } from 'node:child_process'
27
25
  import { promisify } from 'node:util'
28
- import { improve } from '@tangle-network/agent-runtime'
29
- import type { AgentProfile } from '@tangle-network/agent-interface'
26
+ import {
27
+ improve,
28
+ officialGepa,
29
+ type ReadonlyAgentProfile,
30
+ } from '@tangle-network/agent-runtime'
31
+ import {
32
+ canonicalCandidateDigest,
33
+ type AgentProfile,
34
+ } from '@tangle-network/agent-interface'
30
35
  import type { AgenticSurface, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/loops'
31
36
  import { refine, runAgentic } from '@tangle-network/agent-runtime/loops'
32
37
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
33
- import { gepaProposer } from '@tangle-network/agent-eval/campaign'
34
38
  import { createSweBenchAdapter } from './benchmarks/swe-bench'
35
39
  import type { BenchTask } from './benchmarks/types'
40
+ import {
41
+ assertCompleteCost,
42
+ officialOptimizerModel,
43
+ requiredTokenPricing,
44
+ } from './official-optimizer-config.mjs'
36
45
  import { createSweBenchEnvironment, SWE_SEED_PROMPT, SWE_SEED_PROMPT_WITH_RUN } from './swe-bench-env'
37
46
 
38
47
  const exec = promisify(execFile)
@@ -42,36 +51,47 @@ async function main(): Promise<void> {
42
51
  if (!routerKey) throw new Error('TANGLE_API_KEY required (the worker + reflection call the router)')
43
52
  const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
44
53
  const workerModel = process.env.WORKER_MODEL ?? 'glm-4.6'
54
+ const reflectBase = process.env.REFLECT_BASE ?? routerBaseUrl
55
+ const reflectKey = process.env.REFLECT_KEY ?? routerKey
45
56
  const reflectModel = process.env.REFLECT_MODEL ?? 'glm-4.6'
46
57
  const trainIds = (process.env.TRAIN_IDS ?? 'psf__requests-2931').split(',').map((s) => s.trim()).filter(Boolean)
47
- const holdoutIds = (process.env.HOLDOUT_IDS ?? 'psf__requests-1142').split(',').map((s) => s.trim()).filter(Boolean)
48
- const generations = Number(process.env.GENERATIONS ?? 1)
49
- const population = Number(process.env.POPULATION ?? 1)
58
+ const selectionIds = (process.env.SELECTION_IDS ?? 'pallets__flask-5014').split(',').map((s) => s.trim()).filter(Boolean)
59
+ const testIds = (process.env.TEST_IDS ?? 'psf__requests-1142,psf__requests-1921').split(',').map((s) => s.trim()).filter(Boolean)
60
+ const maxEvaluations = Number(process.env.MAX_EVALUATIONS ?? 4)
61
+ const maxProposerCostUsd = Number(process.env.MAX_PROPOSER_COST_USD ?? 2)
50
62
  const innerTurns = Number(process.env.INNER_TURNS ?? 40)
51
63
  const workerMaxTokens = Number(process.env.MAX_TOKENS ?? 8000)
52
64
  const reflectMaxTokens = Number(process.env.REFLECT_MAX_TOKENS ?? 12000)
53
65
  const maxConcurrency = Number(process.env.MAX_CONCURRENCY ?? 1)
54
66
  const budgetShots = Number(process.env.BUDGET ?? 1)
67
+ const runDir = process.env.RUN_DIR ?? '.runs/swe-official-gepa'
55
68
  // WITH-TOOLS arm: RUN_TOOL=1 exposes the jailed `run` tool AND swaps the seed to the run-aware prompt.
56
69
  // Default OFF ⇒ reproduces the read/edit-only baseline denominator unchanged.
57
70
  const enableRun = ['1', 'true', 'yes'].includes((process.env.RUN_TOOL ?? '').toLowerCase())
58
71
  const SEED_PROMPT = enableRun ? SWE_SEED_PROMPT_WITH_RUN : SWE_SEED_PROMPT
72
+ const allIds = [...new Set([...trainIds, ...selectionIds, ...testIds])]
59
73
 
60
- const allIds = [...new Set([...trainIds, ...holdoutIds])]
61
- const cellsMax = trainIds.length * (1 + generations * population) + 2 * holdoutIds.length
62
-
63
- console.log('═══ SWE-bench self-improvement SEE-able LOCAL (no tangle sandbox) ═══')
64
- console.log(`worker=${workerModel} reflect=${reflectModel} router=${routerBaseUrl}`)
65
- console.log(`train=[${trainIds.join(', ')}] holdout=[${holdoutIds.join(', ')}]`)
66
- console.log(`generations=${generations} population=${population} innerTurns=${innerTurns} workerMaxTokens=${workerMaxTokens} reflectMaxTokens=${reflectMaxTokens} runTool=${enableRun}`)
67
- console.log(`≈ ${cellsMax} cells max (each = 1 clone + 1 runAgentic + 1 Docker judge)\n`)
74
+ console.log('=== SWE-bench prompt optimization with official GEPA ===')
75
+ console.log(`worker=${workerModel} reflect=${reflectModel} router=${routerBaseUrl}`)
76
+ console.log(`train=[${trainIds.join(', ')}] selection=[${selectionIds.join(', ')}] test=[${testIds.join(', ')}]`)
77
+ console.log(`maxEvaluations=${maxEvaluations} maxProposerCostUsd=${maxProposerCostUsd} innerTurns=${innerTurns} workerMaxTokens=${workerMaxTokens} runTool=${enableRun}`)
78
+ console.log(`runDir=${runDir}\n`)
68
79
 
69
80
  if (process.env.DRYRUN) {
70
81
  // Import + wiring smoke: prove every module resolves and the plan is well-formed
71
82
  // WITHOUT paying for a clone / model call / Docker judge.
72
- console.log(`DRYRUN: imports OK (improve=${typeof improve}, gepaProposer=${typeof gepaProposer}, runAgentic=${typeof runAgentic}, refine=${typeof refine})`)
83
+ console.log(`DRYRUN: imports OK (improve=${typeof improve}, officialGepa=${typeof officialGepa}, runAgentic=${typeof runAgentic}, refine=${typeof refine})`)
73
84
  return
74
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
+ })
75
95
 
76
96
  const { environment, adapter } = await createSweBenchEnvironment(allIds.length, { ids: allIds, enableRun })
77
97
  const pool = await adapter.loadTasks({ ids: allIds, split: 'test' })
@@ -82,8 +102,9 @@ async function main(): Promise<void> {
82
102
  // one instance, return the git-diff patch. A per-call proxy captures the patch in
83
103
  // score() BEFORE runAgentic closes (rm) the workspace; its score is a cheap
84
104
  // patch-exists proxy so the ONLY Docker run per cell is the improve judge.
85
- const agent = async (surface: unknown, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
86
- const promptText = String(surface)
105
+ const agent = async (candidate: ReadonlyAgentProfile, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
106
+ const promptText = candidate.prompt?.systemPrompt
107
+ if (promptText === undefined) throw new Error('agent: candidate profile has no system prompt')
87
108
  const bt = byId.get(scenario.id)
88
109
  if (!bt) throw new Error(`agent: unknown scenario ${scenario.id}`)
89
110
  const task = { id: bt.id, systemPrompt: promptText, userPrompt: bt.prompt, meta: { instanceId: bt.id } }
@@ -111,40 +132,44 @@ async function main(): Promise<void> {
111
132
  },
112
133
  }
113
134
  const t0 = Date.now()
114
- const r = await runAgentic({
115
- surface: proxy,
116
- task,
117
- strategy: refine,
118
- routerBaseUrl,
119
- routerKey,
135
+ const paid = await ctx.cost.runPaidCall({
136
+ channel: 'agent',
137
+ actor: 'swe-worker',
120
138
  model: workerModel,
121
- maxTokens: workerMaxTokens,
122
- innerTurns,
123
- budget: budgetShots,
139
+ execute: () =>
140
+ runAgentic({
141
+ surface: proxy,
142
+ task,
143
+ strategy: refine,
144
+ routerBaseUrl,
145
+ routerKey,
146
+ model: workerModel,
147
+ maxTokens: workerMaxTokens,
148
+ innerTurns,
149
+ budget: budgetShots,
150
+ }),
151
+ receipt: (result) => {
152
+ const inputTokens = result.tokens.input ?? 0
153
+ const outputTokens = result.tokens.output ?? 0
154
+ const usageUnknown = inputTokens === 0 && outputTokens === 0
155
+ return {
156
+ model: workerModel,
157
+ inputTokens,
158
+ outputTokens,
159
+ customTokenPricing: workerPricing,
160
+ ...(usageUnknown ? { usageUnknown: true } : {}),
161
+ }
162
+ },
124
163
  })
125
- // Report REAL cost/tokens so the backend-integrity guard sees a real backend
126
- // rather than a silent-zero stub. A glm-5.2 turn occasionally returns a real
127
- // patch with an UNPOPULATED usage block (a router telemetry gap on some
128
- // reasoning-model responses — NOT a stub: the cell made real tool calls and
129
- // produced a patch). In that gap case report a nominal floor so the stub-guard
130
- // (artifact + zero usage) cannot abort the whole campaign on a telemetry gap.
131
- // The lift metric is judge-derived, so a floored count does not distort it; only
132
- // cost accounting undercounts those few cells (disclosed). No-patch cells return
133
- // null below and are skipped by the guard's own contract, so this floor only
134
- // ever applies to a cell that genuinely produced a patch.
164
+ if (!paid.succeeded) throw paid.error
165
+ const r = paid.value
135
166
  const zeroUsage = (r.tokens.input ?? 0) === 0 && (r.tokens.output ?? 0) === 0
136
167
  const hasPatch = capturedPatch.trim().length > 0
137
- ctx.cost.observe(zeroUsage && hasPatch ? Math.max(r.usd ?? 0, 0.0001) : r.usd ?? 0, workerModel)
138
- ctx.cost.observeTokens(
139
- zeroUsage && hasPatch
140
- ? { input: Math.max(r.tokens.input ?? 0, 1), output: Math.max(r.tokens.output ?? 0, 1) }
141
- : { input: r.tokens.input, output: r.tokens.output },
142
- )
143
168
  const files = capturedPatch ? [...capturedPatch.matchAll(/^diff --git a\/(\S+)/gm)].map((m) => m[1]) : []
144
169
  console.log(
145
170
  ` [agent] ${scenario.id} prompt=${promptText.length}c tools(l/r/e+/e-/run/run!)=${stats.list}/${stats.read}/${stats.edit_ok}/${stats.edit_fail}/${stats.run}/${stats.run_err} ` +
146
- `patch=${capturedPatch.length}b files=[${files.join(', ') || 'none'}] tok=in:${r.tokens.input}/out:${r.tokens.output} usd=${r.usd} ${Math.round((Date.now() - t0) / 1000)}s` +
147
- `${zeroUsage ? (hasPatch ? ' [zero-usage telemetry gap: patch kept, usage floored]' : ' [zero-usage cell: empty completion — scored as no-patch]') : ''}`,
171
+ `patch=${capturedPatch.length}b files=[${files.join(', ') || 'none'}] tok=in:${r.tokens.input}/out:${r.tokens.output} ${Math.round((Date.now() - t0) / 1000)}s` +
172
+ `${zeroUsage ? ' [provider usage unavailable]' : ''}`,
148
173
  )
149
174
  // A cell with no patch produced NO artifact. Return null (not '') so the
150
175
  // backend-integrity guard's own contract (`artifact == null → skip`) applies:
@@ -156,7 +181,7 @@ async function main(): Promise<void> {
156
181
 
157
182
  // The judge: the OFFICIAL swebench Docker harness. Deterministic FAIL_TO_PASS +
158
183
  // PASS_TO_PASS → resolved 0/1. This is the held-out gate's scoring axis.
159
- const judge: JudgeConfig<string, Scenario> = {
184
+ const judge: JudgeConfig<string | null, Scenario> = {
160
185
  name: 'swebench-docker',
161
186
  dimensions: [{ key: 'resolved', description: 'FAIL_TO_PASS + PASS_TO_PASS resolved by the official swebench Docker harness' }],
162
187
  async score({ artifact, scenario }) {
@@ -177,53 +202,61 @@ async function main(): Promise<void> {
177
202
  }
178
203
 
179
204
  const profile: AgentProfile = { name: 'swe-agent-glm46', prompt: { systemPrompt: SEED_PROMPT } }
180
- const proposer = gepaProposer({
181
- llm: { baseUrl: routerBaseUrl, apiKey: routerKey },
182
- model: reflectModel,
183
- target: 'the system prompt of a coding agent that fixes real GitHub bugs via list_files/read_file/edit_file tools',
184
- maxTokens: reflectMaxTokens,
185
- temperature: 0.7,
186
- })
187
-
188
- const scenarios: Scenario[] = allIds.map((id) => ({ id, kind: 'swe-bench-verified' }))
189
- const holdoutScenarios: Scenario[] = holdoutIds.map((id) => ({ id, kind: 'swe-bench-verified' }))
205
+ const scenario = (id: string): Scenario => ({ id, kind: 'swe-bench-verified' })
190
206
 
191
- const out = await improve(profile, [], {
207
+ const out = await improve(profile, {
192
208
  surface: 'prompt',
193
- gate: 'holdout',
194
- generator: proposer,
195
- scenarios,
196
- judge,
209
+ executionRef: canonicalCandidateDigest({
210
+ callback: 'bench/swe-improve',
211
+ model: workerModel,
212
+ endpoint: new URL(routerBaseUrl).origin,
213
+ innerTurns,
214
+ maxTokens: workerMaxTokens,
215
+ budgetShots,
216
+ enableRun,
217
+ }),
218
+ method: officialGepa<Scenario, string | null>({
219
+ objective:
220
+ 'Improve the system prompt of a coding agent that fixes real GitHub bugs with list_files, read_file, edit_file, and optional run tools.',
221
+ background:
222
+ 'Return the complete system prompt. Preserve tool names and require evidence from repository files and tests.',
223
+ recipe: {
224
+ kind: 'engine',
225
+ run: {
226
+ engine: 'gepa',
227
+ maxEvaluations,
228
+ maxProposerCostUsd,
229
+ },
230
+ },
231
+ optimizer,
232
+ resume: 'if-compatible',
233
+ trustResumeState: true,
234
+ describeScenario: (item) => ({ prompt: byId.get(item.id)?.prompt ?? item.id }),
235
+ }),
236
+ trainScenarios: trainIds.map(scenario),
237
+ selectionScenarios: selectionIds.map(scenario),
238
+ testScenarios: testIds.map(scenario),
239
+ judges: [judge],
197
240
  agent,
198
- // glm-5.2 occasionally returns a real patch with an unpopulated usage block
199
- // (a router telemetry gap on some reasoning-model responses — NOT a stub: the
200
- // cell made real tool calls and produced a patch). 'assert' would abort the
201
- // whole campaign on such a cell; 'warn' logs it and continues. The lift metric
202
- // (resolved) is judge-derived, so a missing token count does not distort it —
203
- // only the cost accounting undercounts those cells, which is disclosed.
204
241
  expectUsage: 'warn',
205
- budget: { generations, populationSize: population, holdoutScenarios, maxConcurrency, reps: 1 },
206
- llm: { baseUrl: routerBaseUrl, apiKey: routerKey, model: reflectModel },
242
+ maxConcurrency,
243
+ reps: 1,
244
+ runDir,
245
+ optimizationRunOptions: {
246
+ expectUsage: 'warn',
247
+ maxConcurrency,
248
+ reps: 1,
249
+ },
207
250
  })
208
251
 
209
- console.log('\n═══ RESULT ═══')
210
- console.log(`gateDecision=${out.gateDecision} shipped=${out.shipped} lift=${out.lift}`)
211
- console.log(`baseline holdout composite = ${out.raw.baseline.compositeMean}`)
212
- console.log(`winner holdout composite = ${out.raw.winner.compositeMean}`)
213
- console.log(`baseline per-scenario: ${JSON.stringify(out.raw.baseline.perScenario)}`)
214
- console.log(`winner per-scenario: ${JSON.stringify(out.raw.winner.perScenario)}`)
215
- if (out.raw.winner.label) console.log(`winner label : ${out.raw.winner.label}`)
216
- if (out.raw.winner.rationale) console.log(`winner rationale: ${out.raw.winner.rationale}`)
217
-
218
- // Per-candidate verdicts on the train set (the "real swebench verdict per candidate").
219
- for (const gen of out.raw.generations ?? []) {
220
- console.log(`\n── generation ${gen.record.generationIndex} candidates ──`)
221
- for (const c of gen.record.candidates) {
222
- const perScenario = (c as { scenarios?: Array<{ scenarioId: string; composite: number }> }).scenarios ?? []
223
- const detail = perScenario.map((s) => `${s.scenarioId}=${s.composite}`).join(' ')
224
- console.log(` candidate ${c.surfaceHash.slice(0, 8)} composite=${c.composite}${c.label ? ` "${c.label}"` : ''} [${detail}]`)
225
- }
226
- }
252
+ assertCompleteCost('SWE-bench official GEPA run', out.cost)
253
+ console.log('\n=== RESULT ===')
254
+ console.log(`decision=${out.decision} lift=${out.lift} interval=[${out.liftInterval.low}, ${out.liftInterval.high}]`)
255
+ console.log(`baseline test composite=${out.raw.best.baselineComposite}`)
256
+ console.log(`winner test composite=${out.raw.best.winnerComposite}`)
257
+ console.log(`test scenarios=${JSON.stringify(out.raw.best.scenarioScores)}`)
258
+ console.log(`cost=${JSON.stringify(out.cost)}`)
259
+ console.log(`candidate prompt:\n${String(out.candidate.value).slice(0, 2000)}`)
227
260
  }
228
261
 
229
262
  main().catch((e) => {
@@ -46,6 +46,9 @@ async function main(): Promise<void> {
46
46
 
47
47
  const { environment, tasks, adapter } = await createSweBenchEnvironment(ids.length, { ids, enableRun })
48
48
  const taskList = await tasks(0, ids.length)
49
+ const benchTaskById = new Map(
50
+ (await adapter.loadTasks({ ids, split: 'test' })).map((task) => [task.id, task]),
51
+ )
49
52
 
50
53
  // One shot per pinned id: proxy score() to capture the exact judged bytes + a NON-DESTRUCTIVE
51
54
  // apply-coherence check, then delegate the verdict to the real Docker judge.
@@ -123,7 +126,9 @@ async function main(): Promise<void> {
123
126
  // Cached judge: identical patch ⇒ identical verdict; don't pay for a second Docker run.
124
127
  let s = judged.get(effPatch)
125
128
  if (!s) {
126
- s = await adapter.judge(task, effPatch)
129
+ const benchTask = benchTaskById.get(task.id)
130
+ if (!benchTask) throw new Error(`swe-local-proof: unknown benchmark task ${task.id}`)
131
+ s = await adapter.judge(benchTask, effPatch)
127
132
  judged.set(effPatch, s)
128
133
  }
129
134
  rec.score = s
@@ -700,6 +700,8 @@ async function acquireRepro(
700
700
  // hands the worker a no-code plan. Primitives replicated from supervisor-arena.mts (that file runs
701
701
  // main() on import, so it cannot be imported) — evidence from execution-verified inputs only. ----------
702
702
 
703
+ type RepairFailureKind = 'wrong-fix' | 'apply-failed' | 'empty-diff'
704
+
703
705
  /** Evidence for the supervisor: the issue, the worker's own candidate diff, and the tail of the
704
706
  * gold-verified reproduction's output on that diff. Execution-verified / model-visible ONLY — never
705
707
  * FAIL_TO_PASS, never the gold patch, never any worker self-report. Bounded to maxChars. */
@@ -709,7 +711,7 @@ function renderRepairEvidence(
709
711
  reproTail: string,
710
712
  reproExit: number | null,
711
713
  maxChars: number,
712
- failureKind: 'wrong-fix' | 'apply-failed' = 'wrong-fix',
714
+ failureKind: RepairFailureKind = 'wrong-fix',
713
715
  ): string {
714
716
  const header =
715
717
  failureKind === 'apply-failed'
@@ -789,7 +791,7 @@ async function superviseRepair(
789
791
  reproExit: number | null,
790
792
  marks: readonly string[],
791
793
  deadlineAt: number,
792
- failureKind: 'wrong-fix' | 'apply-failed' = 'wrong-fix',
794
+ failureKind: RepairFailureKind = 'wrong-fix',
793
795
  ): Promise<SupervisorPlanReceipt> {
794
796
  const md = bt.metadata as Record<string, string>
795
797
  const evidence = renderRepairEvidence(String(md.problem_statement ?? ''), candidateDiff, reproTail, reproExit, 14_000, failureKind)