@tangle-network/agent-bench 0.7.1 → 0.8.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 (131) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/HARNESS.md +1 -1
  3. package/dist/adapters.js +2 -2
  4. package/dist/benchmarks/appworld.d.ts +61 -2
  5. package/dist/benchmarks/appworld.js +111 -40
  6. package/dist/benchmarks/appworld.js.map +1 -1
  7. package/dist/benchmarks/cadbench.js +1 -1
  8. package/dist/benchmarks/cadgenbench.js +1 -1
  9. package/dist/benchmarks/finresearchbench.js +16 -22
  10. package/dist/benchmarks/finresearchbench.js.map +1 -1
  11. package/dist/benchmarks/finsearchcomp.js +16 -22
  12. package/dist/benchmarks/finsearchcomp.js.map +1 -1
  13. package/dist/benchmarks/frames.js +18 -20
  14. package/dist/benchmarks/frames.js.map +1 -1
  15. package/dist/benchmarks/simpleqa.js +15 -19
  16. package/dist/benchmarks/simpleqa.js.map +1 -1
  17. package/dist/benchmarks/trata-hedge.js +20 -23
  18. package/dist/benchmarks/trata-hedge.js.map +1 -1
  19. package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
  20. package/dist/cadbench-BLSyxR1N.js.map +1 -0
  21. package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
  22. package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
  23. package/dist/index.d.ts +0 -1
  24. package/dist/index.js +10 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/router-turn-C2wMiDoo.js +31 -0
  27. package/dist/router-turn-C2wMiDoo.js.map +1 -0
  28. package/package.json +7 -7
  29. package/scripts/appworld_driver.py +1 -170
  30. package/scripts/run-package-tests.mjs +56 -36
  31. package/scripts/run-package-tests.test.mjs +33 -0
  32. package/scripts/trata-hedge/README.md +3 -3
  33. package/scripts/trata-hedge/run.sh +6 -2
  34. package/src/aec-gate.mts +30 -9
  35. package/src/agent-graphs-gen2.mts +523 -0
  36. package/src/agent-graphs-gen3.mts +660 -0
  37. package/src/agent-graphs-improve/offline-seams.mts +128 -0
  38. package/src/agent-graphs-improve.mts +747 -0
  39. package/src/atom-humaneval.mts +76 -55
  40. package/src/atom-mcp-e2e.mts +27 -13
  41. package/src/benchmarks/appworld.test.mts +119 -1
  42. package/src/benchmarks/appworld.ts +156 -33
  43. package/src/benchmarks/cadbench.ts +21 -8
  44. package/src/benchmarks/finresearchbench.ts +20 -16
  45. package/src/benchmarks/finsearchcomp.ts +20 -20
  46. package/src/benchmarks/frames.ts +19 -18
  47. package/src/benchmarks/simpleqa.ts +19 -17
  48. package/src/benchmarks/trata-hedge.ts +26 -18
  49. package/src/browser/ui-reviewer.ts +1 -1
  50. package/src/clbench-context-gate.mts +45 -9
  51. package/src/cloud-loop.mts +24 -6
  52. package/src/commit0-env-run.mts +6 -3
  53. package/src/commit0-env.ts +2 -3
  54. package/src/commit0-gate.mts +53 -24
  55. package/src/corpus.ts +3 -3
  56. package/src/david-attribution.mts +28 -9
  57. package/src/david-goliath.mts +31 -12
  58. package/src/egress-probe.mts +3 -3
  59. package/src/examples/lean-proof-gate.mts +10 -3
  60. package/src/examples/math-demo.mts +10 -3
  61. package/src/examples/strategy-demo.mts +10 -4
  62. package/src/fleet.mts +30 -8
  63. package/src/gate-cli.mts +9 -4
  64. package/src/gate.test.mts +7 -2
  65. package/src/gate.ts +23 -75
  66. package/src/generate-eval/certify.ts +13 -5
  67. package/src/hev-eval.mts +39 -10
  68. package/src/hev-improve.mts +28 -16
  69. package/src/hev-structural.mts +28 -20
  70. package/src/humaneval-gate.mts +24 -8
  71. package/src/humaneval-object-ablation.mts +52 -14
  72. package/src/humaneval-repair-gate.mts +53 -14
  73. package/src/mbpp-structural.mts +28 -20
  74. package/src/mcp-mount-probe.mts +23 -11
  75. package/src/official-optimizer-config.mts +61 -12
  76. package/src/official-optimizer-config.test.mts +72 -3
  77. package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
  78. package/src/quant-arena/quant-loop.mts +88 -61
  79. package/src/research-shot.ts +21 -11
  80. package/src/resolve-client.ts +6 -8
  81. package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
  82. package/src/router-executor.ts +1 -1
  83. package/src/router-turn.ts +135 -0
  84. package/src/run-benchmarks.ts +7 -2
  85. package/src/sandbox-run.ts +23 -41
  86. package/src/search-bench/bridge.ts +66 -8
  87. package/src/search-bench/parametric-check.mts +19 -4
  88. package/src/search-bench/profiles.ts +24 -9
  89. package/src/search-bench/run.mts +3 -4
  90. package/src/smoke-structural-rollout.mts +21 -5
  91. package/src/supervisor-arena.mts +28 -20
  92. package/src/swe-arena/arms.ts +73 -15
  93. package/src/swe-arena/capacity.ts +44 -41
  94. package/src/swe-arena/diagnosis-ensemble.ts +51 -48
  95. package/src/swe-arena/execution.test.mts +12 -12
  96. package/src/swe-arena/gepa-seat.mts +12 -5
  97. package/src/swe-arena/gepa-seat.test.mts +45 -33
  98. package/src/swe-arena/outer-loop.mts +94 -70
  99. package/src/swe-arena/premeasured-from-cells.mts +16 -1
  100. package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
  101. package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
  102. package/src/swe-arena/profiles/default-author.profile.json +9 -1
  103. package/src/swe-arena/proposer-fanout.mts +52 -99
  104. package/src/swe-arena/proposer-fanout.test.mts +92 -57
  105. package/src/swe-arena/proposer-provenance.mts +1 -1
  106. package/src/swe-code-improve.mts +30 -52
  107. package/src/swe-emit-patch.mts +26 -5
  108. package/src/swe-improve.mts +27 -8
  109. package/src/swe-jail.test.ts +128 -0
  110. package/src/swe-jail.ts +106 -22
  111. package/src/swe-local-proof.mts +27 -5
  112. package/src/swe-repro-calibrate.mts +5 -0
  113. package/src/swe-self-improve.mts +45 -10
  114. package/src/swe-stream.mts +68 -6
  115. package/src/swe-structural.mts +46 -7
  116. package/src/tb-container-executor.mts +38 -6
  117. package/src/tb-container-executor.test.mts +11 -3
  118. package/src/tb-supervisor-sidecar.mts +11 -27
  119. package/src/terminal-compare.ts +38 -18
  120. package/src/trata-gate.mts +26 -26
  121. package/src/trata-gepa.mts +25 -16
  122. package/src/trata-hedge-solve.mts +80 -0
  123. package/src/worker-blender.ts +21 -11
  124. package/src/worker-browser.ts +18 -5
  125. package/src/worker-build123d.ts +17 -8
  126. package/src/worker-cad.ts +31 -12
  127. package/src/worker.ts +1 -1
  128. package/tb_agents/opencode_router_agent.py +84 -42
  129. package/dist/cadbench-BrpwOU6A.js.map +0 -1
  130. package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
  131. package/scripts/trata-hedge/solve.py +0 -83
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * META-HARNESS on the SWE scaffold — improve({ surface: 'code' }).
3
3
  *
4
- * A coding agent (Claude Code) REWRITES the scaffold LOGIC under bench/src (the seed prompt/playbook,
4
+ * A Pi coding agent REWRITES the scaffold LOGIC under bench/src (the seed prompt/playbook,
5
5
  * runAgentic strategy/params, context handling, retry/patch synthesis) with the MODEL (glm worker) +
6
6
  * the TOOL surface (list/read/edit[/run]) + the JUDGE held FIXED, judged on the official swebench
7
7
  * Docker verdict, gated on a held-out instance split. This is the DGM/meta-harness recipe: let the
@@ -10,13 +10,9 @@
10
10
  * Wiring (all verified in this worktree):
11
11
  * - improve()/codeProposerFor + rawTraceContext come from the LOCAL agent-runtime build, linked into
12
12
  * this bench's node_modules (bench/node_modules/@tangle-network/agent-runtime -> /home/drew/code/agent-runtime).
13
- * - The candidate proposer is agenticGenerator(harness:'claude-code'), BUT the shipped runLocalHarness
14
- * spawns `claude --headless -p` and --headless is an unknown option on the current CLI (exit 1, no
15
- * edits ever). We pass code.generator with a corrected runHarness that spawns
16
- * `claude -p <prompt> --dangerously-skip-permissions` so the coding agent can actually edit the
17
- * worktree. This is a harness-spawn fix, NOT a hand-authored scaffold edit — Claude still finds the
18
- * lever itself from the traces.
19
- * - Each candidate is a git worktree the driver forks off baseRef; Claude edits bench/src in place;
13
+ * - The candidate proposer supplies one exact Pi AgentProfile and runs it through Runtime's
14
+ * bridge executor. No local CLI shortcut or ambient model default exists.
15
+ * - Each candidate is a git worktree the driver forks off baseRef; Pi edits bench/src in place;
20
16
  * `verify` (an import smoke of the edited scaffold) gates it before the expensive measurement.
21
17
  * - MEASUREMENT: the code-aware agent fn shells into the candidate scaffold's OWN judge-free emit
22
18
  * entrypoint (swe-emit-patch.mts) with cwd = the worktree, captures the unified diff, and returns
@@ -35,6 +31,7 @@ import { join } from 'node:path'
35
31
  import { improve, agenticGenerator } from '@tangle-network/agent-runtime'
36
32
  import type { ProposalFinding } from '@tangle-network/agent-eval'
37
33
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
34
+ import { agentProfileSchema } from '@tangle-network/agent-interface'
38
35
  import { createSweBenchAdapter } from './benchmarks/swe-bench'
39
36
  import type { BenchTask } from './benchmarks/types'
40
37
 
@@ -120,6 +117,22 @@ async function main(): Promise<void> {
120
117
  const runDir = process.env.RUN_DIR ?? '/tmp/claude-1000/-home-drew-code-supervisor-lab/9ee6a456-a94f-474c-9888-b4afc9bc26bd/scratchpad/mh-run'
121
118
  const emitTimeoutMs = Number(process.env.EMIT_TIMEOUT_MS ?? 600_000)
122
119
  const harnessTimeoutMs = Number(process.env.HARNESS_TIMEOUT_MS ?? 900_000)
120
+ const bridgeUrl = process.env.CLI_BRIDGE_URL ?? process.env.BRIDGE_URL
121
+ const bridgeBearer = process.env.CLI_BRIDGE_BEARER ?? process.env.BRIDGE_BEARER
122
+ if (!bridgeUrl || !bridgeBearer) {
123
+ throw new Error('CLI_BRIDGE_URL/BRIDGE_URL and CLI_BRIDGE_BEARER/BRIDGE_BEARER are required')
124
+ }
125
+ const authorProfile = agentProfileSchema.parse({
126
+ name: 'swe-scaffold-author',
127
+ harness: 'pi',
128
+ model: {
129
+ provider: process.env.AUTHOR_PROVIDER ?? 'tangle-router',
130
+ default: process.env.AUTHOR_MODEL ?? 'glm-5.2',
131
+ },
132
+ prompt: {
133
+ systemPrompt: 'Improve the candidate worktree from measured failure evidence and verify it.',
134
+ },
135
+ })
123
136
  mkdirSync(worktreeDir, { recursive: true })
124
137
  mkdirSync(runDir, { recursive: true })
125
138
 
@@ -199,44 +212,6 @@ async function main(): Promise<void> {
199
212
  },
200
213
  }
201
214
 
202
- // The corrected coding-harness spawn: `claude -p <prompt> --dangerously-skip-permissions`. The shipped
203
- // runLocalHarness uses `claude --headless -p` (unknown option on this CLI). agenticGenerator ignores
204
- // the return value (it reads worktree dirtiness), so a minimal result shape is enough.
205
- const runHarness = (o: { cwd: string; taskPrompt: string; timeoutMs?: number; signal?: AbortSignal }): Promise<{ exitCode: number | null; stdout: string; stderr: string; killedBySignal: NodeJS.Signals | null; durationMs: number; timedOut: boolean }> => {
206
- const started = Date.now()
207
- return new Promise((resolve) => {
208
- const child = spawn('claude', ['-p', o.taskPrompt, '--dangerously-skip-permissions'], {
209
- cwd: o.cwd,
210
- env: process.env,
211
- stdio: ['ignore', 'pipe', 'pipe'],
212
- })
213
- let stdout = ''
214
- let stderr = ''
215
- let timedOut = false
216
- child.stdout?.on('data', (d) => (stdout += String(d)))
217
- child.stderr?.on('data', (d) => (stderr += String(d)))
218
- const timer = setTimeout(() => {
219
- timedOut = true
220
- if (!child.killed) child.kill('SIGTERM')
221
- }, o.timeoutMs ?? harnessTimeoutMs)
222
- ;(timer as { unref?: () => void }).unref?.()
223
- const onAbort = () => {
224
- if (!child.killed) child.kill('SIGTERM')
225
- }
226
- o.signal?.addEventListener('abort', onAbort, { once: true })
227
- child.on('error', () => {
228
- clearTimeout(timer)
229
- resolve({ exitCode: 1, stdout, stderr: `${stderr}\n[spawn error]`, killedBySignal: null, durationMs: Date.now() - started, timedOut })
230
- })
231
- child.on('close', (code, signal) => {
232
- clearTimeout(timer)
233
- o.signal?.removeEventListener('abort', onAbort)
234
- console.error(` [proposer:claude] exit=${code} wall=${Math.round((Date.now() - started) / 1000)}s out=${stdout.length}b`)
235
- resolve({ exitCode: code, stdout, stderr, killedBySignal: signal, durationMs: Date.now() - started, timedOut })
236
- })
237
- })
238
- }
239
-
240
215
  // Domain prompt: name the EDIT BOUNDARY (scaffold logic only) + keep the raw-trace evidence discipline
241
216
  // (agenticGenerator discards a raw-trace candidate that doesn't inspect a trace + write the diagnosis).
242
217
  const buildPrompt = (args: { findings: ReadonlyArray<ProposalFinding> }): string => {
@@ -286,13 +261,16 @@ async function main(): Promise<void> {
286
261
  }
287
262
 
288
263
  const generator = agenticGenerator({
289
- harness: 'claude-code',
264
+ profile: authorProfile,
265
+ executorForWorktree: (cwd) => ({
266
+ backend: 'bridge',
267
+ bridgeUrl,
268
+ bridgeBearer,
269
+ cwd,
270
+ }),
290
271
  verify,
291
272
  timeoutMs: harnessTimeoutMs,
292
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
- buildPrompt: buildPrompt as any,
294
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
- runHarness: runHarness as any,
273
+ buildPrompt,
296
274
  })
297
275
 
298
276
  const scenarios: Scenario[] = allIds.map((id) => ({ id, kind: 'swe-bench-verified' }))
@@ -301,7 +279,7 @@ async function main(): Promise<void> {
301
279
  const out = await improve({
302
280
  surface: 'code',
303
281
  gate: 'holdout',
304
- code: { repoRoot: REPO_ROOT, baseRef, worktreeDir, generator },
282
+ code: { repoRoot: REPO_ROOT, baseRef, worktreeDir, profile: authorProfile, generator },
305
283
  rawTraceContext: true,
306
284
  runDir,
307
285
  scenarios,
@@ -23,8 +23,9 @@
23
23
  import { execFile } from 'node:child_process'
24
24
  import { promisify } from 'node:util'
25
25
  import type { AgenticSurface, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/kernel'
26
- import { refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
26
+ import { defaultAnalystInstruction, refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
27
27
  import { createSweBenchEnvironment, SWE_SEED_PROMPT, SWE_SEED_PROMPT_WITH_RUN } from './swe-bench-env'
28
+ import { benchRouterProfile, withBenchProfile } from './router-turn'
28
29
 
29
30
  const exec = promisify(execFile)
30
31
 
@@ -56,10 +57,31 @@ async function main(): Promise<void> {
56
57
 
57
58
  const task = {
58
59
  id: bt.id,
59
- systemPrompt: enableRun ? SWE_SEED_PROMPT_WITH_RUN : SWE_SEED_PROMPT,
60
60
  userPrompt: bt.prompt,
61
61
  meta: { instanceId: bt.id },
62
62
  }
63
+ const workerProfile = withBenchProfile(
64
+ {
65
+ name: 'swe-emit-patch-worker',
66
+ harness: 'cli-base',
67
+ model: { provider: 'tangle-router', default: model },
68
+ tools: {
69
+ list_files: true,
70
+ read_file: true,
71
+ edit_file: true,
72
+ ...(enableRun ? { run: true } : {}),
73
+ },
74
+ },
75
+ {
76
+ systemPrompt: enableRun ? SWE_SEED_PROMPT_WITH_RUN : SWE_SEED_PROMPT,
77
+ maxTokens,
78
+ maxTurns: innerTurns,
79
+ },
80
+ )
81
+ const analystProfile = benchRouterProfile('swe-emit-patch-analyst', model, {
82
+ systemPrompt: defaultAnalystInstruction,
83
+ maxTokens,
84
+ })
63
85
 
64
86
  // Capture the patch from inside score() (called during the refine loop, BEFORE the surface closes
65
87
  // and rms the checkout). Keep the LATEST non-empty diff so accumulated refinements win and a later
@@ -85,9 +107,8 @@ async function main(): Promise<void> {
85
107
  strategy: refine,
86
108
  routerBaseUrl,
87
109
  routerKey,
88
- model,
89
- maxTokens,
90
- innerTurns,
110
+ workerProfile,
111
+ analystProfile,
91
112
  budget,
92
113
  })
93
114
  const files = capturedPatch ? [...capturedPatch.matchAll(/^diff --git a\/(\S+)/gm)].map((m) => m[1]) : []
@@ -30,10 +30,11 @@ import {
30
30
  } from '@tangle-network/agent-runtime'
31
31
  import {
32
32
  canonicalCandidateDigest,
33
+ agentProfileSchema,
33
34
  type AgentProfile,
34
35
  } from '@tangle-network/agent-interface'
35
36
  import type { AgenticSurface, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/kernel'
36
- import { refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
37
+ import { defaultAnalystInstruction, refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
37
38
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
38
39
  import { createSweBenchAdapter } from './benchmarks/swe-bench'
39
40
  import type { BenchTask } from './benchmarks/types'
@@ -43,6 +44,7 @@ import {
43
44
  requiredTokenPricing,
44
45
  } from './official-optimizer-config.mjs'
45
46
  import { createSweBenchEnvironment, SWE_SEED_PROMPT, SWE_SEED_PROMPT_WITH_RUN } from './swe-bench-env'
47
+ import { benchRouterProfile, withBenchProfile } from './router-turn'
46
48
 
47
49
  const exec = promisify(execFile)
48
50
 
@@ -70,6 +72,24 @@ async function main(): Promise<void> {
70
72
  const enableRun = ['1', 'true', 'yes'].includes((process.env.RUN_TOOL ?? '').toLowerCase())
71
73
  const SEED_PROMPT = enableRun ? SWE_SEED_PROMPT_WITH_RUN : SWE_SEED_PROMPT
72
74
  const allIds = [...new Set([...trainIds, ...selectionIds, ...testIds])]
75
+ const workerProfile = withBenchProfile(
76
+ {
77
+ name: 'swe-agent-worker',
78
+ harness: 'cli-base',
79
+ model: { provider: 'tangle-router', default: workerModel },
80
+ tools: {
81
+ list_files: true,
82
+ read_file: true,
83
+ edit_file: true,
84
+ ...(enableRun ? { run: true } : {}),
85
+ },
86
+ },
87
+ { systemPrompt: SEED_PROMPT, maxTokens: workerMaxTokens, maxTurns: innerTurns },
88
+ )
89
+ const analystProfile = benchRouterProfile('swe-agent-analyst', workerModel, {
90
+ systemPrompt: defaultAnalystInstruction,
91
+ maxTokens: workerMaxTokens,
92
+ })
73
93
 
74
94
  console.log('=== SWE-bench prompt optimization with official GEPA ===')
75
95
  console.log(`worker=${workerModel} reflect=${reflectModel} router=${routerBaseUrl}`)
@@ -103,11 +123,12 @@ async function main(): Promise<void> {
103
123
  // score() BEFORE runAgentic closes (rm) the workspace; its score is a cheap
104
124
  // patch-exists proxy so the ONLY Docker run per cell is the improve judge.
105
125
  const agent = async (candidate: ReadonlyAgentProfile, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
106
- const promptText = candidate.prompt?.systemPrompt
126
+ const exactCandidate = agentProfileSchema.parse(candidate)
127
+ const promptText = exactCandidate.prompt?.systemPrompt
107
128
  if (promptText === undefined) throw new Error('agent: candidate profile has no system prompt')
108
129
  const bt = byId.get(scenario.id)
109
130
  if (!bt) throw new Error(`agent: unknown scenario ${scenario.id}`)
110
- const task = { id: bt.id, systemPrompt: promptText, userPrompt: bt.prompt, meta: { instanceId: bt.id } }
131
+ const task = { id: bt.id, userPrompt: bt.prompt, meta: { instanceId: bt.id } }
111
132
  let capturedPatch = ''
112
133
  const stats = { list: 0, read: 0, edit_ok: 0, edit_fail: 0, run: 0, run_err: 0 }
113
134
  const proxy: AgenticSurface = {
@@ -143,9 +164,8 @@ async function main(): Promise<void> {
143
164
  strategy: refine,
144
165
  routerBaseUrl,
145
166
  routerKey,
146
- model: workerModel,
147
- maxTokens: workerMaxTokens,
148
- innerTurns,
167
+ workerProfile: exactCandidate,
168
+ analystProfile,
149
169
  budget: budgetShots,
150
170
  }),
151
171
  receipt: (result) => {
@@ -201,10 +221,9 @@ async function main(): Promise<void> {
201
221
  },
202
222
  }
203
223
 
204
- const profile: AgentProfile = { name: 'swe-agent-glm46', prompt: { systemPrompt: SEED_PROMPT } }
205
224
  const scenario = (id: string): Scenario => ({ id, kind: 'swe-bench-verified' })
206
225
 
207
- const out = await improve(profile, {
226
+ const out = await improve(workerProfile, {
208
227
  surface: 'prompt',
209
228
  executionRef: canonicalCandidateDigest({
210
229
  callback: 'bench/swe-improve',
@@ -0,0 +1,128 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import { zaiChatRaw } from './swe-jail'
4
+
5
+ test('zaiChatRaw preserves the SWE transport shape through Runtime', async () => {
6
+ const originalFetch = globalThis.fetch
7
+ let requestBody: Record<string, unknown> | undefined
8
+ let requestUrl = ''
9
+ globalThis.fetch = async (input, init) => {
10
+ requestUrl = String(input)
11
+ requestBody = JSON.parse(String(init?.body)) as Record<string, unknown>
12
+ return new Response(
13
+ JSON.stringify({
14
+ choices: [
15
+ {
16
+ message: {
17
+ content: null,
18
+ tool_calls: [
19
+ {
20
+ id: 'call-1',
21
+ type: 'function',
22
+ function: { name: 'run', arguments: '{"command":"pwd"}' },
23
+ },
24
+ ],
25
+ },
26
+ },
27
+ ],
28
+ usage: { prompt_tokens: 11, completion_tokens: 3 },
29
+ }),
30
+ { status: 200, headers: { 'content-type': 'application/json' } },
31
+ )
32
+ }
33
+
34
+ try {
35
+ const result = await zaiChatRaw(
36
+ { base: 'http://router.test/v1', key: 'secret', timeoutMs: 1_000 },
37
+ {
38
+ model: 'deepseek-v4-flash',
39
+ messages: [{ role: 'user', content: 'inspect' }],
40
+ tools: [
41
+ {
42
+ type: 'function',
43
+ function: { name: 'run', parameters: { type: 'object' } },
44
+ },
45
+ ],
46
+ tool_choice: 'required',
47
+ temperature: 0.1,
48
+ max_tokens: 32_768,
49
+ thinking: { type: 'enabled' },
50
+ },
51
+ {
52
+ name: 'swe-jail-test-worker',
53
+ harness: 'cli-base',
54
+ model: { provider: 'tangle-router', default: 'deepseek-v4-flash' },
55
+ tools: { run: true },
56
+ },
57
+ )
58
+
59
+ assert.equal(requestUrl, 'http://router.test/v1/chat/completions')
60
+ assert.deepEqual(requestBody, {
61
+ model: 'deepseek-v4-flash',
62
+ messages: [{ role: 'user', content: 'inspect' }],
63
+ tools: [
64
+ {
65
+ type: 'function',
66
+ function: { name: 'run', parameters: { type: 'object' } },
67
+ },
68
+ ],
69
+ tool_choice: 'required',
70
+ temperature: 0.1,
71
+ max_tokens: 32_768,
72
+ thinking: { type: 'enabled' },
73
+ })
74
+ assert.equal(result.attempts, 1)
75
+ assert.deepEqual(result.json, {
76
+ choices: [
77
+ {
78
+ message: {
79
+ role: 'assistant',
80
+ content: null,
81
+ tool_calls: [
82
+ {
83
+ id: 'call-1',
84
+ type: 'function',
85
+ function: { name: 'run', arguments: '{"command":"pwd"}' },
86
+ },
87
+ ],
88
+ },
89
+ finish_reason: 'tool_calls',
90
+ },
91
+ ],
92
+ usage: { prompt_tokens: 11, completion_tokens: 3 },
93
+ })
94
+ } finally {
95
+ globalThis.fetch = originalFetch
96
+ }
97
+ })
98
+
99
+ test('zaiChatRaw refuses a local profile error without entering the retry ladder', async () => {
100
+ let fetchCalls = 0
101
+ const originalFetch = globalThis.fetch
102
+ globalThis.fetch = async () => {
103
+ fetchCalls += 1
104
+ throw new Error('fetch must not run')
105
+ }
106
+
107
+ try {
108
+ await assert.rejects(
109
+ zaiChatRaw(
110
+ {
111
+ base: 'http://router.test/v1',
112
+ key: 'secret',
113
+ timeoutMs: 1_000,
114
+ maxAttempts: 7,
115
+ },
116
+ { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'inspect' }] },
117
+ {
118
+ name: 'incomplete-worker',
119
+ model: { provider: 'tangle-router', default: 'deepseek-v4-flash' },
120
+ },
121
+ ),
122
+ /AgentProfile\.harness must be explicit/,
123
+ )
124
+ assert.equal(fetchCalls, 0)
125
+ } finally {
126
+ globalThis.fetch = originalFetch
127
+ }
128
+ })
package/src/swe-jail.ts CHANGED
@@ -12,12 +12,15 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
12
12
  import { tmpdir } from 'node:os'
13
13
  import { join } from 'node:path'
14
14
  import { promisify } from 'node:util'
15
+ import type { AgentProfile } from '@tangle-network/agent-interface'
16
+ import type { ToolSpec } from '@tangle-network/agent-runtime/kernel'
17
+ import { runBenchRouterTurn, withBenchProfile } from './router-turn'
15
18
 
16
19
  const exec = promisify(execFile)
17
20
 
18
21
  export const tail = (s: string, n: number): string => (s.length > n ? `…${s.slice(s.length - n)}` : s)
19
22
 
20
- // ---------- zai chat client (plain fetch; patient 429 ladder) ----------
23
+ // ---------- zai chat client (Runtime transport; patient 429 ladder) ----------
21
24
 
22
25
  export interface ZaiCfg {
23
26
  base: string
@@ -27,12 +30,14 @@ export interface ZaiCfg {
27
30
  * runs — past this instant, so a per-instance deadline reaches INTO the 429 ladder instead of
28
31
  * letting a doomed retry sleep for another 240s after the instance was already written off. */
29
32
  deadlineAt?: number
33
+ /** Caller-owned retry ladder length. Default 7. */
34
+ maxAttempts?: number
30
35
  }
31
36
 
32
37
  export interface ZaiRaw {
33
- /** The parsed /chat/completions JSON, verbatim. */
38
+ /** The OpenAI-compatible message and usage fields expected by existing SWE callers. */
34
39
  json: Record<string, unknown>
35
- /** HTTP attempts spent (retries included). */
40
+ /** Runtime completion attempts spent (retries included). */
36
41
  attempts: number
37
42
  }
38
43
 
@@ -44,10 +49,39 @@ export interface ZaiRaw {
44
49
  * tool_calls — is the glm reasoning path starving `content` when reasoning eats max_tokens, and is
45
50
  * retried too (a tool_calls turn with empty content is a NORMAL tool-loop turn, not starvation).
46
51
  */
47
- export async function zaiChatRaw(cfg: ZaiCfg, body: Record<string, unknown>): Promise<ZaiRaw> {
52
+ export async function zaiChatRaw(
53
+ cfg: ZaiCfg,
54
+ body: Record<string, unknown>,
55
+ profile: AgentProfile,
56
+ ): Promise<ZaiRaw> {
57
+ const {
58
+ model,
59
+ messages,
60
+ tools,
61
+ temperature,
62
+ max_tokens: maxTokens,
63
+ tool_choice: toolChoice,
64
+ ...extraBody
65
+ } = body
66
+ if (typeof model !== 'string' || model.length === 0) {
67
+ throw new Error('completion body.model must be a non-empty string')
68
+ }
69
+ if (profile.model?.default !== model) {
70
+ throw new Error('completion body.model must equal AgentProfile.model.default')
71
+ }
72
+ if (!Array.isArray(messages)) throw new Error('completion body.messages must be an array')
73
+ const typedTools = Array.isArray(tools) ? (tools as ToolSpec[]) : []
74
+ const typedToolChoice =
75
+ toolChoice === 'auto' || toolChoice === 'required' || toolChoice === 'none'
76
+ ? toolChoice
77
+ : undefined
48
78
  let lastErr = ''
49
79
  let delayBase = 2_000
50
- for (let attempt = 1; attempt <= 7; attempt += 1) {
80
+ const maxAttempts = cfg.maxAttempts ?? 7
81
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
82
+ throw new Error('ZaiCfg.maxAttempts must be a positive safe integer')
83
+ }
84
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
51
85
  if (attempt > 1) {
52
86
  const delay = Math.min(delayBase * 2 ** (attempt - 2), 240_000)
53
87
  if (cfg.deadlineAt !== undefined && Date.now() + delay >= cfg.deadlineAt) {
@@ -63,30 +97,80 @@ export async function zaiChatRaw(cfg: ZaiCfg, body: Record<string, unknown>): Pr
63
97
  const ctl = new AbortController()
64
98
  const timer = setTimeout(() => ctl.abort(), perCallTimeout)
65
99
  try {
66
- const res = await fetch(`${cfg.base}/chat/completions`, {
67
- method: 'POST',
68
- headers: { Authorization: `Bearer ${cfg.key}`, 'Content-Type': 'application/json' },
69
- body: JSON.stringify(body),
70
- signal: ctl.signal,
71
- })
72
- if (!res.ok) {
73
- lastErr = `HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`
74
- delayBase = res.status === 429 ? 60_000 : 2_000
75
- continue
76
- }
77
- const json = (await res.json()) as Record<string, unknown>
78
- const msg = ((json.choices as Array<{ message?: Record<string, unknown> }> | undefined)?.[0]?.message ?? {}) as {
79
- content?: string
80
- tool_calls?: unknown[]
100
+ const result = await runBenchRouterTurn(
101
+ {
102
+ routerBaseUrl: cfg.base,
103
+ routerKey: cfg.key,
104
+ profile: withBenchProfile(profile, {
105
+ ...(typeof temperature === 'number' ? { temperature } : {}),
106
+ ...(typeof maxTokens === 'number' ? { maxTokens } : {}),
107
+ ...(typedToolChoice ? { toolChoice: typedToolChoice } : {}),
108
+ extraBody,
109
+ }),
110
+ tools: typedTools,
111
+ signal: ctl.signal,
112
+ },
113
+ { messages: messages as Array<Record<string, unknown>> },
114
+ )
115
+ const toolCalls = result.toolCalls.map((call, index) => ({
116
+ id: call.id ?? `call_${index}`,
117
+ name: call.name,
118
+ arguments: call.arguments,
119
+ }))
120
+ const message = {
121
+ role: 'assistant',
122
+ content: toolCalls.length > 0 && result.finalText === '' ? null : result.finalText,
123
+ ...(toolCalls.length > 0
124
+ ? {
125
+ tool_calls: toolCalls.map((call) => ({
126
+ id: call.id,
127
+ type: 'function',
128
+ function: { name: call.name, arguments: call.arguments },
129
+ })),
130
+ }
131
+ : {}),
81
132
  }
82
- const hasToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0
83
- if (!hasToolCalls && String(msg.content ?? '').trim() === '') {
133
+ if (toolCalls.length === 0 && result.finalText.trim() === '') {
84
134
  lastErr = 'empty content'
85
135
  continue
86
136
  }
137
+ const finalEvent = result.events.at(-1)
138
+ const finishReason =
139
+ finalEvent?.type === 'final'
140
+ ? finalEvent.reason
141
+ : toolCalls.length > 0
142
+ ? 'tool_calls'
143
+ : 'stop'
144
+ const json: Record<string, unknown> = {
145
+ choices: [
146
+ {
147
+ message,
148
+ finish_reason: finishReason,
149
+ },
150
+ ],
151
+ ...(result.usage.tokensKnown !== false
152
+ ? {
153
+ usage: {
154
+ prompt_tokens: result.usage.input,
155
+ completion_tokens: result.usage.output,
156
+ },
157
+ }
158
+ : {}),
159
+ }
87
160
  return { json, attempts: attempt }
88
161
  } catch (e) {
89
162
  lastErr = e instanceof Error ? e.message : String(e)
163
+ const status = Number(/router (\d+)/.exec(lastErr)?.[1])
164
+ const transientStatus =
165
+ Number.isFinite(status) &&
166
+ (status === 408 || status === 409 || status === 425 || status === 429 || status >= 500)
167
+ const transientTransport =
168
+ ctl.signal.aborted ||
169
+ /fetch failed|network|socket|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|operation was aborted/i.test(
170
+ lastErr,
171
+ )
172
+ if (!transientStatus && !transientTransport) throw e
173
+ delayBase = status === 429 ? 60_000 : 2_000
90
174
  } finally {
91
175
  clearTimeout(timer)
92
176
  }
@@ -21,9 +21,10 @@ import { tmpdir } from 'node:os'
21
21
  import { join } from 'node:path'
22
22
  import { promisify } from 'node:util'
23
23
  import type { AgenticSurface, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/kernel'
24
- import { refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
24
+ import { defaultAnalystInstruction, refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
25
25
  import type { BenchScore } from './benchmarks/types'
26
- import { createSweBenchEnvironment } from './swe-bench-env'
26
+ import { createSweBenchEnvironment, SWE_SEED_PROMPT, SWE_SEED_PROMPT_WITH_RUN } from './swe-bench-env'
27
+ import { benchRouterProfile, withBenchProfile } from './router-turn'
27
28
 
28
29
  const exec = promisify(execFile)
29
30
 
@@ -45,6 +46,28 @@ async function main(): Promise<void> {
45
46
  console.log(`router=${routerBaseUrl}`)
46
47
 
47
48
  const { environment, tasks, adapter } = await createSweBenchEnvironment(ids.length, { ids, enableRun })
49
+ const workerProfile = withBenchProfile(
50
+ {
51
+ name: 'swe-local-proof-worker',
52
+ harness: 'cli-base',
53
+ model: { provider: 'tangle-router', default: model },
54
+ tools: {
55
+ list_files: true,
56
+ read_file: true,
57
+ edit_file: true,
58
+ ...(enableRun ? { run: true } : {}),
59
+ },
60
+ },
61
+ {
62
+ systemPrompt: enableRun ? SWE_SEED_PROMPT_WITH_RUN : SWE_SEED_PROMPT,
63
+ maxTokens,
64
+ maxTurns: innerTurns,
65
+ },
66
+ )
67
+ const analystProfile = benchRouterProfile('swe-local-proof-analyst', model, {
68
+ systemPrompt: defaultAnalystInstruction,
69
+ maxTokens,
70
+ })
48
71
  const taskList = await tasks(0, ids.length)
49
72
  const benchTaskById = new Map(
50
73
  (await adapter.loadTasks({ ids, split: 'test' })).map((task) => [task.id, task]),
@@ -145,9 +168,8 @@ async function main(): Promise<void> {
145
168
  strategy: refine,
146
169
  routerBaseUrl,
147
170
  routerKey,
148
- model,
149
- maxTokens,
150
- innerTurns,
171
+ workerProfile,
172
+ analystProfile,
151
173
  budget,
152
174
  })
153
175
  const rec = captured.get(task.id)
@@ -89,6 +89,11 @@ async function complete(messages: ChatMsg[]): Promise<Completion> {
89
89
  const { json, attempts } = await zaiChatRaw(
90
90
  { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS },
91
91
  { model: MODEL, max_tokens: MAX_TOKENS, temperature: TEMP, messages },
92
+ {
93
+ name: 'swe-reproduction-calibrator',
94
+ model: { provider: 'zai', default: MODEL, reasoningEffort: 'high' },
95
+ prompt: { systemPrompt: AUTHOR_SYSTEM },
96
+ },
92
97
  )
93
98
  const d = json as {
94
99
  choices?: Array<{ message?: { content?: string } }>