@tangle-network/agent-bench 0.7.1 → 0.8.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 (155) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/HARNESS.md +2 -2
  3. package/dist/adapters.js +6 -2
  4. package/dist/adapters.js.map +1 -1
  5. package/dist/benchmarks/appworld.d.ts +61 -2
  6. package/dist/benchmarks/appworld.js +111 -40
  7. package/dist/benchmarks/appworld.js.map +1 -1
  8. package/dist/benchmarks/cadbench.js +1 -1
  9. package/dist/benchmarks/cadgenbench.js +1 -1
  10. package/dist/benchmarks/finresearchbench.js +16 -22
  11. package/dist/benchmarks/finresearchbench.js.map +1 -1
  12. package/dist/benchmarks/finsearchcomp.js +16 -22
  13. package/dist/benchmarks/finsearchcomp.js.map +1 -1
  14. package/dist/benchmarks/frames.js +18 -20
  15. package/dist/benchmarks/frames.js.map +1 -1
  16. package/dist/benchmarks/mcad-bench.d.ts +106 -0
  17. package/dist/benchmarks/mcad-bench.js +569 -0
  18. package/dist/benchmarks/mcad-bench.js.map +1 -0
  19. package/dist/benchmarks/mcad-cq-bench.d.ts +82 -0
  20. package/dist/benchmarks/mcad-cq-bench.js +339 -0
  21. package/dist/benchmarks/mcad-cq-bench.js.map +1 -0
  22. package/dist/benchmarks/mcad-cq-golds.d.ts +36 -0
  23. package/dist/benchmarks/mcad-cq-golds.js +342 -0
  24. package/dist/benchmarks/mcad-cq-golds.js.map +1 -0
  25. package/dist/benchmarks/mcad-golds.d.ts +20 -0
  26. package/dist/benchmarks/mcad-golds.js +318 -0
  27. package/dist/benchmarks/mcad-golds.js.map +1 -0
  28. package/dist/benchmarks/mcad-tasks.d.ts +66 -0
  29. package/dist/benchmarks/mcad-tasks.js +508 -0
  30. package/dist/benchmarks/mcad-tasks.js.map +1 -0
  31. package/dist/benchmarks/simpleqa.js +15 -19
  32. package/dist/benchmarks/simpleqa.js.map +1 -1
  33. package/dist/benchmarks/trata-hedge.js +20 -23
  34. package/dist/benchmarks/trata-hedge.js.map +1 -1
  35. package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
  36. package/dist/cadbench-BLSyxR1N.js.map +1 -0
  37. package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
  38. package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
  39. package/dist/index.d.ts +0 -1
  40. package/dist/index.js +10 -9
  41. package/dist/index.js.map +1 -1
  42. package/dist/router-turn-C2wMiDoo.js +31 -0
  43. package/dist/router-turn-C2wMiDoo.js.map +1 -0
  44. package/package.json +7 -7
  45. package/scripts/appworld_driver.py +1 -170
  46. package/scripts/run-package-tests.mjs +56 -36
  47. package/scripts/run-package-tests.test.mjs +33 -0
  48. package/scripts/trata-hedge/README.md +3 -3
  49. package/scripts/trata-hedge/run.sh +6 -2
  50. package/src/adapters.ts +11 -0
  51. package/src/aec-gate.mts +30 -9
  52. package/src/agent-graphs-gen2.mts +523 -0
  53. package/src/agent-graphs-gen3.mts +660 -0
  54. package/src/agent-graphs-improve/offline-seams.mts +128 -0
  55. package/src/agent-graphs-improve.mts +747 -0
  56. package/src/atom-humaneval.mts +76 -55
  57. package/src/atom-mcp-e2e.mts +27 -13
  58. package/src/benchmarks/appworld.test.mts +119 -1
  59. package/src/benchmarks/appworld.ts +156 -33
  60. package/src/benchmarks/cadbench.ts +21 -8
  61. package/src/benchmarks/finresearchbench.ts +20 -16
  62. package/src/benchmarks/finsearchcomp.ts +20 -20
  63. package/src/benchmarks/frames.ts +19 -18
  64. package/src/benchmarks/mcad-bench.test.mts +455 -0
  65. package/src/benchmarks/mcad-bench.ts +561 -0
  66. package/src/benchmarks/mcad-cq-bench.ts +423 -0
  67. package/src/benchmarks/mcad-cq-golds.ts +374 -0
  68. package/src/benchmarks/mcad-cq.test.mts +386 -0
  69. package/src/benchmarks/mcad-golds.ts +359 -0
  70. package/src/benchmarks/mcad-tasks.ts +490 -0
  71. package/src/benchmarks/simpleqa.ts +19 -17
  72. package/src/benchmarks/trata-hedge.ts +26 -18
  73. package/src/browser/ui-reviewer.ts +1 -1
  74. package/src/clbench-context-gate.mts +45 -9
  75. package/src/cloud-loop.mts +24 -6
  76. package/src/commit0-env-run.mts +6 -3
  77. package/src/commit0-env.ts +2 -3
  78. package/src/commit0-gate.mts +53 -24
  79. package/src/corpus.ts +3 -3
  80. package/src/david-attribution.mts +28 -9
  81. package/src/david-goliath.mts +31 -12
  82. package/src/egress-probe.mts +3 -3
  83. package/src/examples/lean-proof-gate.mts +10 -3
  84. package/src/examples/math-demo.mts +10 -3
  85. package/src/examples/strategy-demo.mts +10 -4
  86. package/src/fleet.mts +30 -8
  87. package/src/gate-cli.mts +9 -4
  88. package/src/gate.test.mts +7 -2
  89. package/src/gate.ts +23 -75
  90. package/src/generate-eval/certify.ts +13 -5
  91. package/src/hev-eval.mts +39 -10
  92. package/src/hev-improve.mts +28 -16
  93. package/src/hev-structural.mts +28 -20
  94. package/src/humaneval-gate.mts +24 -8
  95. package/src/humaneval-object-ablation.mts +52 -14
  96. package/src/humaneval-repair-gate.mts +53 -14
  97. package/src/mbpp-structural.mts +28 -20
  98. package/src/mcp-mount-probe.mts +23 -11
  99. package/src/official-optimizer-config.mts +61 -12
  100. package/src/official-optimizer-config.test.mts +72 -3
  101. package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
  102. package/src/quant-arena/quant-loop.mts +88 -61
  103. package/src/research-shot.ts +21 -11
  104. package/src/resolve-client.ts +6 -8
  105. package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
  106. package/src/router-executor.ts +1 -1
  107. package/src/router-turn.ts +135 -0
  108. package/src/run-benchmarks.ts +7 -2
  109. package/src/sandbox-run.ts +23 -41
  110. package/src/search-bench/bridge.ts +66 -8
  111. package/src/search-bench/parametric-check.mts +19 -4
  112. package/src/search-bench/profiles.ts +24 -9
  113. package/src/search-bench/run.mts +3 -4
  114. package/src/smoke-structural-rollout.mts +21 -5
  115. package/src/supervisor-arena.mts +28 -20
  116. package/src/swe-arena/arms.ts +73 -15
  117. package/src/swe-arena/capacity.ts +44 -41
  118. package/src/swe-arena/diagnosis-ensemble.ts +51 -48
  119. package/src/swe-arena/execution.test.mts +12 -12
  120. package/src/swe-arena/gepa-seat.mts +12 -5
  121. package/src/swe-arena/gepa-seat.test.mts +45 -33
  122. package/src/swe-arena/outer-loop.mts +94 -70
  123. package/src/swe-arena/premeasured-from-cells.mts +16 -1
  124. package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
  125. package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
  126. package/src/swe-arena/profiles/default-author.profile.json +9 -1
  127. package/src/swe-arena/proposer-fanout.mts +52 -99
  128. package/src/swe-arena/proposer-fanout.test.mts +92 -57
  129. package/src/swe-arena/proposer-provenance.mts +1 -1
  130. package/src/swe-code-improve.mts +30 -52
  131. package/src/swe-emit-patch.mts +26 -5
  132. package/src/swe-improve.mts +27 -8
  133. package/src/swe-jail.test.ts +128 -0
  134. package/src/swe-jail.ts +106 -22
  135. package/src/swe-local-proof.mts +27 -5
  136. package/src/swe-repro-calibrate.mts +5 -0
  137. package/src/swe-self-improve.mts +45 -10
  138. package/src/swe-stream.mts +68 -6
  139. package/src/swe-structural.mts +46 -7
  140. package/src/tb-container-executor.mts +38 -6
  141. package/src/tb-container-executor.test.mts +11 -3
  142. package/src/tb-supervisor-sidecar.mts +11 -27
  143. package/src/terminal-compare.ts +38 -18
  144. package/src/trata-gate.mts +26 -26
  145. package/src/trata-gepa.mts +25 -16
  146. package/src/trata-hedge-solve.mts +80 -0
  147. package/src/worker-blender.ts +21 -11
  148. package/src/worker-browser.ts +18 -5
  149. package/src/worker-build123d.ts +17 -8
  150. package/src/worker-cad.ts +31 -12
  151. package/src/worker.ts +1 -1
  152. package/tb_agents/opencode_router_agent.py +84 -42
  153. package/dist/cadbench-BrpwOU6A.js.map +0 -1
  154. package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
  155. package/scripts/trata-hedge/solve.py +0 -83
@@ -9,17 +9,42 @@
9
9
  */
10
10
  import { mkdtempSync, rmSync } from 'node:fs'
11
11
  import { join } from 'node:path'
12
- import { createChatClient } from '@tangle-network/agent-eval'
13
- import { refine, runAgentic, runStrategyEvolution, sample } from '@tangle-network/agent-runtime/kernel'
12
+ import type { AgentProfile } from '@tangle-network/agent-interface'
13
+ import {
14
+ refine,
15
+ runAgentic,
16
+ runStrategyEvolution,
17
+ sample,
18
+ strategyAuthorSystemPrompt,
19
+ } from '@tangle-network/agent-runtime/kernel'
14
20
  import { createSweBenchEnvironment } from './swe-bench-env'
15
21
 
16
22
  async function main(): Promise<void> {
17
23
  const routerKey = process.env.TANGLE_API_KEY
18
24
  if (!routerKey) throw new Error('TANGLE_API_KEY required (worker + author call the router)')
19
25
  const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
20
- const workerModel = process.env.WORKER_MODEL ?? 'gemini-2.5-pro'
21
- const authorModel = process.env.AUTHOR_MODEL ?? 'gemini-2.5-pro'
26
+ const workerModel = process.env.WORKER_MODEL ?? 'deepseek-v4-flash'
27
+ const authorModel = process.env.AUTHOR_MODEL ?? 'deepseek-v4-flash'
22
28
  const innerTurns = Number(process.env.INNER_TURNS ?? 40)
29
+ const workerProfile: AgentProfile = {
30
+ name: 'swe-worker',
31
+ harness: 'cli-base',
32
+ model: {
33
+ provider: 'tangle-router',
34
+ default: workerModel,
35
+ metadata: { maxTokens: 8000, maxTurns: innerTurns },
36
+ },
37
+ }
38
+ const authorProfile = (model: string, name: string): AgentProfile => ({
39
+ name,
40
+ harness: 'cli-base',
41
+ model: {
42
+ provider: 'tangle-router',
43
+ default: model,
44
+ metadata: { maxTokens: 8000 },
45
+ },
46
+ prompt: { systemPrompt: strategyAuthorSystemPrompt },
47
+ })
23
48
  const { environment, tasks } = await createSweBenchEnvironment(Number(process.env.POOL_N ?? 80))
24
49
 
25
50
  if (process.env.CALIBRATE === '1') {
@@ -29,7 +54,15 @@ async function main(): Promise<void> {
29
54
  let resolved = 0
30
55
  for (const t of ts) {
31
56
  const t0 = Date.now()
32
- const r = await runAgentic({ surface: environment, task: t, strategy: refine, routerBaseUrl, routerKey, model: workerModel, maxTokens: 8000, innerTurns, budget: 1 })
57
+ const r = await runAgentic({
58
+ surface: environment,
59
+ task: t,
60
+ strategy: refine,
61
+ routerBaseUrl,
62
+ routerKey,
63
+ workerProfile,
64
+ budget: 1,
65
+ })
33
66
  if (r.resolved) resolved++
34
67
  console.log(` ${t.id.padEnd(32)} resolved=${r.resolved} completions=${r.completions} shots=${r.shots} (${Math.round((Date.now() - t0) / 1000)}s)`)
35
68
  }
@@ -46,12 +79,14 @@ async function main(): Promise<void> {
46
79
  tasks,
47
80
  trainN: Number(process.env.TRAIN_N ?? 6),
48
81
  holdoutN: Number(process.env.HOLDOUT_N ?? 8),
49
- worker: { routerBaseUrl, routerKey, model: workerModel, maxTokens: 8000, innerTurns },
82
+ worker: { routerBaseUrl, routerKey, workerProfile },
50
83
  author: {
51
- chat: createChatClient({ transport: 'router', baseUrl: routerBaseUrl, apiKey: routerKey, defaultModel: authorModel }),
52
- model: authorModel,
53
- maxTokens: 8000,
54
- fallbackModel: process.env.AUTHOR_FALLBACK ?? 'deepseek-v4-flash',
84
+ profile: authorProfile(authorModel, 'swe-strategy-author'),
85
+ executor: { backend: 'router', routerBaseUrl, routerKey },
86
+ fallbackProfile: authorProfile(
87
+ process.env.AUTHOR_FALLBACK ?? 'deepseek-v4-flash',
88
+ 'swe-strategy-author-fallback',
89
+ ),
55
90
  },
56
91
  baselines: [sample, refine],
57
92
  budget: Number(process.env.BUDGET ?? 2),
@@ -66,7 +66,7 @@ import { join } from 'node:path'
66
66
  import { promisify } from 'node:util'
67
67
  import { applyKnowledgeWriteBlocks, buildKnowledgeIndex, initKnowledgeBase, searchKnowledge } from '@tangle-network/agent-knowledge'
68
68
  import type { AgenticSurface, AgenticTask, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/kernel'
69
- import { refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
69
+ import { defaultAnalystInstruction, refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
70
70
  import type { BenchTask } from './benchmarks/types'
71
71
  import { createSweBenchEnvironment, resolveImageForMetadata, SWE_SEED_PROMPT } from './swe-bench-env'
72
72
  import {
@@ -82,6 +82,7 @@ import {
82
82
  tail,
83
83
  zaiChatRaw,
84
84
  } from './swe-jail'
85
+ import { withBenchProfile } from './router-turn'
85
86
 
86
87
  const exec = promisify(execFile)
87
88
 
@@ -274,9 +275,29 @@ const makeTransport =
274
275
  counter.guardedMsgs += assertNoHiddenLeak(marks, msgs)
275
276
  // Inject the honored reasoning-budget knob (thinking) here at the single shared worker
276
277
  // chokepoint: makeTransport is byte-identical across arms F and L, so the budget is symmetric.
278
+ const model = String(body.model ?? '')
279
+ const systemPrompt = msgs.find((message) => message.role === 'system')?.content
280
+ const toolNames = Array.isArray(body.tools)
281
+ ? (body.tools as Array<{ function?: { name?: unknown } }>).flatMap((tool) =>
282
+ typeof tool.function?.name === 'string' ? [tool.function.name] : [],
283
+ )
284
+ : []
277
285
  const { json, attempts } = await zaiChatRaw(
278
286
  { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS, deadlineAt: guard.deadlineAt },
279
287
  { ...body, ...WORKER_REASONING },
288
+ {
289
+ name: 'swe-stream-worker',
290
+ harness: 'cli-base',
291
+ model: {
292
+ provider: 'zai',
293
+ default: model,
294
+ reasoningEffort: REASONING_ON ? 'high' : 'none',
295
+ },
296
+ ...(typeof systemPrompt === 'string' ? { prompt: { systemPrompt } } : {}),
297
+ ...(toolNames.length > 0
298
+ ? { tools: Object.fromEntries(toolNames.map((name) => [name, true])) }
299
+ : {}),
300
+ },
280
301
  )
281
302
  counter.calls += 1
282
303
  counter.httpAttempts += attempts
@@ -345,10 +366,44 @@ async function emitAttempt(
345
366
  }
346
367
  const task: AgenticTask = {
347
368
  id: bt.id,
348
- systemPrompt: SWE_SEED_PROMPT,
349
369
  userPrompt: cfg.promptAppendix ? `${bt.prompt}\n\n${cfg.promptAppendix}` : bt.prompt,
350
370
  meta: { instanceId: bt.id },
351
371
  }
372
+ const workerProfile = withBenchProfile(
373
+ {
374
+ name: 'swe-stream-worker',
375
+ harness: 'cli-base',
376
+ model: {
377
+ provider: 'zai',
378
+ default: WORKER_MODEL,
379
+ reasoningEffort: REASONING_ON ? 'high' : 'none',
380
+ },
381
+ tools: { list_files: true, read_file: true, edit_file: true },
382
+ },
383
+ {
384
+ systemPrompt: SWE_SEED_PROMPT,
385
+ maxTokens: MAX_TOKENS,
386
+ maxTurns: INNER_TURNS,
387
+ temperature: cfg.temperature,
388
+ extraBody: WORKER_REASONING,
389
+ },
390
+ )
391
+ const analystProfile = withBenchProfile(
392
+ {
393
+ name: 'swe-stream-analyst',
394
+ harness: 'cli-base',
395
+ model: {
396
+ provider: 'zai',
397
+ default: WORKER_MODEL,
398
+ reasoningEffort: REASONING_ON ? 'high' : 'none',
399
+ },
400
+ },
401
+ {
402
+ systemPrompt: defaultAnalystInstruction,
403
+ maxTokens: MAX_TOKENS,
404
+ extraBody: WORKER_REASONING,
405
+ },
406
+ )
352
407
  let error: string | undefined
353
408
  try {
354
409
  const r = await runAgentic({
@@ -357,10 +412,8 @@ async function emitAttempt(
357
412
  strategy: refine,
358
413
  routerBaseUrl: 'zai-direct', // unused: the `complete` transport short-circuits the router
359
414
  routerKey: 'zai-direct',
360
- model: WORKER_MODEL,
361
- maxTokens: MAX_TOKENS,
362
- temperature: cfg.temperature,
363
- innerTurns: INNER_TURNS,
415
+ workerProfile,
416
+ analystProfile,
364
417
  budget: 1,
365
418
  complete: makeTransport(cfg.marks, counter, guard),
366
419
  })
@@ -643,6 +696,11 @@ async function acquireRepro(
643
696
  const { json } = await zaiChatRaw(
644
697
  { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS, deadlineAt },
645
698
  { model: REPRO_MODEL, max_tokens: MAX_TOKENS, temperature: 0.2, messages },
699
+ {
700
+ name: 'swe-reproduction-author',
701
+ model: { provider: 'zai', default: REPRO_MODEL, reasoningEffort: 'high' },
702
+ prompt: { systemPrompt: reproAuthorSystem(REPRO_TIMEOUT_S) },
703
+ },
646
704
  )
647
705
  const d = json as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number } }
648
706
  out.authorCalls += 1
@@ -810,6 +868,10 @@ async function superviseRepair(
810
868
  const { json, attempts } = await zaiChatRaw(
811
869
  { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS, deadlineAt },
812
870
  { model: SUPERVISOR_MODEL, max_tokens: SUPERVISOR_MAX_TOKENS, temperature: 0.2, messages },
871
+ {
872
+ name: 'swe-repair-supervisor',
873
+ model: { provider: 'zai', default: SUPERVISOR_MODEL, reasoningEffort: 'high' },
874
+ },
813
875
  )
814
876
  const d = json as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number } }
815
877
  const planRaw = d.choices?.[0]?.message?.content ?? ''
@@ -50,7 +50,7 @@ import { appendFileSync, existsSync, readFileSync, rmSync, writeFileSync } from
50
50
  import { join } from 'node:path'
51
51
  import { promisify } from 'node:util'
52
52
  import type { AgenticSurface, AgenticTask, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/kernel'
53
- import { refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
53
+ import { defaultAnalystInstruction, refine, runAgentic } from '@tangle-network/agent-runtime/kernel'
54
54
  import type { BenchTask } from './benchmarks/types'
55
55
  import { createSweBenchEnvironment, resolveImageForMetadata, SWE_SEED_PROMPT } from './swe-bench-env'
56
56
  import {
@@ -63,6 +63,7 @@ import {
63
63
  tail,
64
64
  zaiChatRaw,
65
65
  } from './swe-jail'
66
+ import { withBenchProfile } from './router-turn'
66
67
 
67
68
  const exec = promisify(execFile)
68
69
 
@@ -138,7 +139,26 @@ const makeTransport =
138
139
  async (body: Record<string, unknown>): Promise<unknown> => {
139
140
  const msgs = (body.messages ?? []) as Array<{ role?: string; content?: unknown }>
140
141
  counter.guardedMsgs += assertNoHiddenLeak(marks, msgs)
141
- const { json, attempts } = await zaiChatRaw({ base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS }, body)
142
+ const model = String(body.model ?? '')
143
+ const systemPrompt = msgs.find((message) => message.role === 'system')?.content
144
+ const toolNames = Array.isArray(body.tools)
145
+ ? (body.tools as Array<{ function?: { name?: unknown } }>).flatMap((tool) =>
146
+ typeof tool.function?.name === 'string' ? [tool.function.name] : [],
147
+ )
148
+ : []
149
+ const { json, attempts } = await zaiChatRaw(
150
+ { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS },
151
+ body,
152
+ {
153
+ name: 'swe-structural-worker',
154
+ harness: 'cli-base',
155
+ model: { provider: 'zai', default: model, reasoningEffort: 'high' },
156
+ ...(typeof systemPrompt === 'string' ? { prompt: { systemPrompt } } : {}),
157
+ ...(toolNames.length > 0
158
+ ? { tools: Object.fromEntries(toolNames.map((name) => [name, true])) }
159
+ : {}),
160
+ },
161
+ )
142
162
  counter.calls += 1
143
163
  counter.httpAttempts += attempts
144
164
  const u = (json as { usage?: { prompt_tokens?: number; completion_tokens?: number } }).usage
@@ -204,10 +224,31 @@ async function emitAttempt(
204
224
  }
205
225
  const task: AgenticTask = {
206
226
  id: bt.id,
207
- systemPrompt: SWE_SEED_PROMPT,
208
227
  userPrompt: cfg.promptAppendix ? `${bt.prompt}\n\n${cfg.promptAppendix}` : bt.prompt,
209
228
  meta: { instanceId: bt.id },
210
229
  }
230
+ const workerProfile = withBenchProfile(
231
+ {
232
+ name: 'swe-structural-worker',
233
+ harness: 'cli-base',
234
+ model: { provider: 'zai', default: MODEL, reasoningEffort: 'high' },
235
+ tools: { list_files: true, read_file: true, edit_file: true },
236
+ },
237
+ {
238
+ systemPrompt: SWE_SEED_PROMPT,
239
+ maxTokens: MAX_TOKENS,
240
+ maxTurns: INNER_TURNS,
241
+ temperature: cfg.temperature,
242
+ },
243
+ )
244
+ const analystProfile = withBenchProfile(
245
+ {
246
+ name: 'swe-structural-analyst',
247
+ harness: 'cli-base',
248
+ model: { provider: 'zai', default: MODEL, reasoningEffort: 'high' },
249
+ },
250
+ { systemPrompt: defaultAnalystInstruction, maxTokens: MAX_TOKENS },
251
+ )
211
252
  let error: string | undefined
212
253
  try {
213
254
  const r = await runAgentic({
@@ -216,10 +257,8 @@ async function emitAttempt(
216
257
  strategy: refine,
217
258
  routerBaseUrl: 'zai-direct', // unused: the `complete` transport short-circuits the router
218
259
  routerKey: 'zai-direct',
219
- model: MODEL,
220
- maxTokens: MAX_TOKENS,
221
- temperature: cfg.temperature,
222
- innerTurns: INNER_TURNS,
260
+ workerProfile,
261
+ analystProfile,
223
262
  budget: 1,
224
263
  complete: makeTransport(cfg.marks, counter),
225
264
  })
@@ -5,6 +5,7 @@
5
5
  * provided container id and reports the captured process artifact back through the runtime.
6
6
  */
7
7
  import { spawn } from 'node:child_process'
8
+ import { agentProfileSchema } from '@tangle-network/agent-interface'
8
9
  import type {
9
10
  Agent,
10
11
  AgentProfile,
@@ -17,6 +18,8 @@ import type {
17
18
  Runtime,
18
19
  Spend,
19
20
  } from '../../src/runtime/index'
21
+ import { harnessInvocation, type LocalHarness } from '../../src/mcp/local-harness'
22
+ import { assertExecutableAgentProfile } from '../../src/runtime/supervise/model-policy'
20
23
 
21
24
  export interface TbExecOutput {
22
25
  /** Primary artifact consumed by drivers that read `{ content }`. */
@@ -40,7 +43,8 @@ export interface TbContainerConfig {
40
43
  readonly workdir?: string
41
44
  readonly shell?: string
42
45
  readonly env?: Readonly<Record<string, string>>
43
- readonly wrapCommand?: (task: unknown) => string
46
+ /** Shell setup required inside the existing task container before the canonical invocation. */
47
+ readonly commandPrefix?: ReadonlyArray<string>
44
48
  readonly parseUsage?: ParseUsage
45
49
  readonly budgetExempt?: boolean
46
50
  readonly dockerBin?: string
@@ -62,7 +66,7 @@ function resolveContainerId(config: TbContainerConfig): string {
62
66
  return id.trim()
63
67
  }
64
68
 
65
- function taskToCommand(task: unknown): string {
69
+ function taskToPrompt(task: unknown): string {
66
70
  if (typeof task === 'string') return task
67
71
  if (task && typeof task === 'object') {
68
72
  const obj = task as Record<string, unknown>
@@ -73,6 +77,31 @@ function taskToCommand(task: unknown): string {
73
77
  return JSON.stringify(task)
74
78
  }
75
79
 
80
+ function shellQuote(value: string): string {
81
+ return `'${value.replace(/'/g, `'\\''`)}'`
82
+ }
83
+
84
+ function profileCommand(
85
+ profile: AgentProfile,
86
+ task: unknown,
87
+ prefix: ReadonlyArray<string> = [],
88
+ ): string {
89
+ const harness = profile.harness
90
+ if (harness !== 'claude-code' && harness !== 'codex' && harness !== 'opencode') {
91
+ throw new Error(
92
+ `tbContainerExecutor: profile harness ${JSON.stringify(harness)} is not available in the task container`,
93
+ )
94
+ }
95
+ const invocation = harnessInvocation(
96
+ harness as LocalHarness,
97
+ profile,
98
+ taskToPrompt(task),
99
+ { dangerouslySkipPermissions: true },
100
+ )
101
+ const run = [invocation.command, ...invocation.args].map(shellQuote).join(' ')
102
+ return [...prefix, `exec ${run}`].join('; ')
103
+ }
104
+
76
105
  /**
77
106
  * Compose the `docker exec` argv that runs `command` inside `containerId` via `<shell> -c`.
78
107
  * Exported so tests can verify the container path without a live Docker daemon.
@@ -113,6 +142,8 @@ export function createTbContainerExecutor(
113
142
  config: TbContainerConfig = {},
114
143
  ): ExecutorFactory<TbExecOutput> {
115
144
  return (_spec: AgentSpec, ctx: ExecutorContext): Executor<TbExecOutput> => {
145
+ const profile = agentProfileSchema.parse(_spec.profile)
146
+ assertExecutableAgentProfile(profile, 'tbContainerExecutor')
116
147
  const containerId = resolveContainerId(config)
117
148
  const dockerBin = config.dockerBin ?? 'docker'
118
149
  const metered = config.parseUsage !== undefined
@@ -133,7 +164,7 @@ export function createTbContainerExecutor(
133
164
  runtime,
134
165
  budgetExempt,
135
166
  execute(task, signal): Promise<ExecutorResult<TbExecOutput>> {
136
- const command = (config.wrapCommand ?? taskToCommand)(task)
167
+ const command = profileCommand(profile, task, config.commandPrefix)
137
168
  const args = buildTbDockerExecArgs(containerId, command, {
138
169
  ...(config.shell ? { shell: config.shell } : {}),
139
170
  ...(config.workdir ? { workdir: config.workdir } : {}),
@@ -221,9 +252,10 @@ export function createTbContainerExecutor(
221
252
  export function makeTbContainerWorkerAgent(config: TbContainerConfig = {}): MakeWorkerAgent {
222
253
  const factory = createTbContainerExecutor(config)
223
254
  return (rawProfile) => {
224
- const p = (rawProfile ?? {}) as { name?: unknown }
225
- const name = typeof p.name === 'string' && p.name.length > 0 ? p.name : 'tb-worker'
226
- const spec: AgentSpec = { profile: rawProfile as AgentProfile, harness: null }
255
+ const profile = agentProfileSchema.parse(rawProfile)
256
+ assertExecutableAgentProfile(profile, 'tbContainerWorkerAgent')
257
+ const name = profile.name ?? 'tb-worker'
258
+ const spec: AgentSpec = { profile, harness: null }
227
259
  const ctx: ExecutorContext = { signal: new AbortController().signal, seams: {} }
228
260
  const executor = factory(spec, ctx)
229
261
  return { name, act: async () => '', executorSpec: { ...spec, executor } } as Agent<
@@ -14,7 +14,14 @@ import {
14
14
  type TbExecOutput,
15
15
  } from './tb-container-executor.mts'
16
16
 
17
- const spec: AgentSpec = { profile: { name: 'tb-test-worker' }, harness: null }
17
+ const spec: AgentSpec = {
18
+ profile: {
19
+ name: 'tb-test-worker',
20
+ harness: 'opencode',
21
+ model: { provider: 'tangle-router', default: 'deepseek-v4-flash' },
22
+ },
23
+ harness: null,
24
+ }
18
25
 
19
26
  function context(): ExecutorContext {
20
27
  return { signal: new AbortController().signal, seams: {} }
@@ -78,10 +85,11 @@ printf 'argv:%s\\n' "$*"
78
85
  assert.equal(metered.budgetExempt, false, 'usage parser makes the executor metered')
79
86
  const meteredResult = await executeOneShot(metered, { command: 'echo hello' })
80
87
  assert.equal(meteredResult.out.containerId, 'cid')
81
- assert.equal(meteredResult.out.command, 'echo hello')
88
+ assert.match(meteredResult.out.command, /'echo hello'/)
89
+ assert.match(meteredResult.out.command, /'tangle-router\/deepseek-v4-flash'/)
82
90
  assert.match(
83
91
  meteredResult.out.stdout,
84
- /argv:exec -i --workdir \/work -e OPENAI_BASE_URL=http:\/\/router\.test cid \/bin\/sh -c echo hello/,
92
+ /argv:exec -i --workdir \/work -e OPENAI_BASE_URL=http:\/\/router\.test cid \/bin\/sh -c exec 'opencode' 'run' 'echo hello' '-m' 'tangle-router\/deepseek-v4-flash'/,
85
93
  )
86
94
  assert.deepEqual(meteredResult.spent.tokens, { input: 7, output: 11 })
87
95
  assert.equal(meteredResult.spent.usd, 0.004)
@@ -20,7 +20,6 @@ import { makeTbContainerWorkerAgent, type ParseUsage } from './tb-container-exec
20
20
  const CONTAINER_ID = (process.env.TB_TARGET_CONTAINER ?? '').trim()
21
21
  const ROUTER_KEY = process.env.OPENAI_API_KEY ?? ''
22
22
  const ROUTER_BASE = process.env.OPENAI_BASE_URL ?? 'https://router.tangle.tools/v1'
23
- const WORKER_MODEL = process.env.WORKER_MODEL ?? 'zai-coding-plan/glm-5.2'
24
23
  const DOCKER_BRIDGE_GATEWAY = process.env.DOCKER_BRIDGE_GATEWAY ?? '172.17.0.1'
25
24
  const PORT_FILE = process.env.TB_SIDECAR_PORT_FILE ?? '.tb-sidecar-port'
26
25
  const LOG_FILE = process.env.TB_SIDECAR_LOG ?? '.tb-sidecar-events.jsonl'
@@ -42,30 +41,6 @@ function logEvent(kind: string, payload: unknown): void {
42
41
  console.error(`[sidecar] ${kind} ${JSON.stringify(payload).slice(0, 300)}`)
43
42
  }
44
43
 
45
- function workerWrapCommand(task: unknown): string {
46
- const instr =
47
- typeof task === 'string'
48
- ? task
49
- : task && typeof task === 'object'
50
- ? ((): string => {
51
- const o = task as Record<string, unknown>
52
- for (const k of ['task', 'prompt', 'content', 'command', 'message', 'instruction']) {
53
- if (typeof o[k] === 'string') return o[k] as string
54
- }
55
- return JSON.stringify(task)
56
- })()
57
- : String(task)
58
- const q = `'${instr.replace(/'/g, `'\\''`)}'`
59
- return [
60
- 'export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"',
61
- '{ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; } 2>/dev/null || true',
62
- '{ [ -s /root/.nvm/nvm.sh ] && . /root/.nvm/nvm.sh; } 2>/dev/null || true',
63
- `export OPENCODE_CONFIG=${WORKER_CONFIG_PATH}`,
64
- `cd ${WORKER_WORKDIR} 2>/dev/null || true`,
65
- `opencode --model ${WORKER_MODEL} --format json run ${q}`,
66
- ].join('; ')
67
- }
68
-
69
44
  const parseWorkerUsage: ParseUsage = ({ stdout }) => {
70
45
  let input = 0
71
46
  let output = 0
@@ -115,9 +90,18 @@ async function main(): Promise<void> {
115
90
  const makeWorkerAgent = makeTbContainerWorkerAgent({
116
91
  containerId: CONTAINER_ID,
117
92
  workdir: WORKER_WORKDIR,
118
- wrapCommand: workerWrapCommand,
93
+ commandPrefix: [
94
+ 'export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"',
95
+ '{ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; } 2>/dev/null || true',
96
+ '{ [ -s /root/.nvm/nvm.sh ] && . /root/.nvm/nvm.sh; } 2>/dev/null || true',
97
+ ],
119
98
  parseUsage: parseWorkerUsage,
120
- env: { OPENAI_API_KEY: ROUTER_KEY, OPENAI_BASE_URL: ROUTER_BASE, HOME: '/root' },
99
+ env: {
100
+ OPENAI_API_KEY: ROUTER_KEY,
101
+ OPENAI_BASE_URL: ROUTER_BASE,
102
+ OPENCODE_CONFIG: WORKER_CONFIG_PATH,
103
+ HOME: '/root',
104
+ },
121
105
  runtime: 'tb-container',
122
106
  })
123
107
 
@@ -7,7 +7,7 @@
7
7
  * judge here.
8
8
  *
9
9
  * Per task:
10
- * ROUND 1 (blind) — `tb run --agent opencode` → parse results.json →
10
+ * ROUND 1 (blind) — Terminal-Bench runs one exact AgentProfile → parse results.json →
11
11
  * resolved_1. This IS blind pass@1.
12
12
  * ROUND r (refine, only if blind failed AND rounds>1) — extract a compact
13
13
  * summary of round 1's commands + terminal state + the
@@ -26,11 +26,12 @@
26
26
  */
27
27
 
28
28
  import { execFile } from 'node:child_process'
29
- import { mkdir, readFile, readdir, stat } from 'node:fs/promises'
29
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
30
30
  import { join } from 'node:path'
31
31
  import { fileURLToPath } from 'node:url'
32
32
 
33
33
  import { homedir } from 'node:os'
34
+ import { agentProfileSchema } from '@tangle-network/agent-interface'
34
35
 
35
36
  import { appendRunRecord, type AttemptRecord, type RunRecord } from './corpus'
36
37
  import { runPool } from './run-pool'
@@ -38,16 +39,36 @@ import { runPool } from './run-pool'
38
39
  const BENCH_ROOT = fileURLToPath(new URL('..', import.meta.url))
39
40
  const TB = join(BENCH_ROOT, '.venv', 'bin', 'tb')
40
41
  const RUNS_DIR = join(BENCH_ROOT, 'runs')
41
- const REFINE_IMPORT = 'tb_agents.opencode_refine_agent:OpenCodeRefineAgent'
42
+ const PROFILE_AGENT_IMPORT = 'tb_agents.opencode_router_agent:OpenCodeRouterAgent'
42
43
  // The durable learning-flywheel corpus (docs/learning-flywheel.md). terminal-bench
43
44
  // is bench-orchestrated (tb owns the containers) so it cannot use buildRunRecord,
44
- // which consumes runLoop Iterations; instead each task's per-round tb artifacts are
45
+ // which consumes runAgentRounds Iterations; instead each task's per-round tb artifacts are
45
46
  // folded into one RunRecord here so the corpus is genuinely cross-benchmark
46
- // (finsearch-loop.ts writes the same store from the runLoop path).
47
+ // (finsearch-loop.ts writes the same store from the runAgentRounds path).
47
48
  const CORPUS = process.env.CORPUS ?? join(BENCH_ROOT, 'corpus', 'terminal.jsonl')
48
49
 
49
50
  const DATASET = process.env.TB_DATASET ?? 'terminal-bench-core==0.1.1'
50
- const MODEL = process.env.TB_MODEL ?? 'deepseek/deepseek-v4-pro'
51
+ const MODEL = process.env.TB_MODEL ?? 'deepseek-v4-flash'
52
+ const PROVIDER = process.env.TB_PROVIDER ?? 'tangle-router'
53
+ const PROFILE = agentProfileSchema.parse({
54
+ name: 'terminal-compare-worker',
55
+ harness: 'opencode',
56
+ model: { provider: PROVIDER, default: MODEL },
57
+ prompt: {
58
+ systemPrompt:
59
+ 'Solve the Terminal-Bench task completely in the provided container and verify the result before finishing.',
60
+ },
61
+ permission: {
62
+ edit: 'allow',
63
+ bash: 'allow',
64
+ webfetch: 'allow',
65
+ read: 'allow',
66
+ write: 'allow',
67
+ external_directory: 'allow',
68
+ },
69
+ })
70
+ const PROFILE_MODEL = MODEL.includes('/') ? MODEL : `${PROVIDER}/${MODEL}`
71
+ const PROFILE_PATH = join(RUNS_DIR, 'terminal-compare.profile.json')
51
72
  const ROUNDS = Math.max(1, Number(process.env.ROUNDS ?? 2))
52
73
  const CONCURRENCY = Math.max(1, Number(process.env.CONCURRENCY ?? 2))
53
74
  // Per-round wall-clock cap (ms). tb itself enforces task timeouts; this is the
@@ -307,8 +328,8 @@ function informativeFailureMode(mode: string | undefined): string | undefined {
307
328
 
308
329
  /**
309
330
  * Build and persist one flywheel RunRecord per task from terminal-compare's own
310
- * round data — no buildRunRecord (that consumes runLoop Iterations; tb is not
311
- * runLoop-shaped). condition = 'refine@k' when a refine budget was available,
331
+ * round data — no buildRunRecord (that consumes runAgentRounds Iterations; tb is not
332
+ * runAgentRounds-shaped). condition = 'refine@k' when a refine budget was available,
312
333
  * 'blind' when only the single blind round can run (ROUNDS===1). Appended once
313
334
  * per task; never throws into the run (corpus capture must not fail the bench).
314
335
  */
@@ -330,7 +351,7 @@ async function captureRunRecord(
330
351
  benchmark: 'terminal-bench',
331
352
  instanceId: taskId,
332
353
  condition: ROUNDS > 1 ? `refine@${ROUNDS}` : 'blind',
333
- model: MODEL,
354
+ model: PROFILE.model?.default ?? MODEL,
334
355
  blindResolved,
335
356
  resolved: last ? last.resolved : blindResolved,
336
357
  attempts,
@@ -392,7 +413,7 @@ async function runRound(taskId: string, round: number, prior: string): Promise<R
392
413
  'run',
393
414
  '-d', DATASET,
394
415
  '--task-id', taskId,
395
- '-m', MODEL,
416
+ '-m', PROFILE_MODEL,
396
417
  '--output-path', RUNS_DIR,
397
418
  '--run-id', runId,
398
419
  '--n-concurrent', '1',
@@ -404,14 +425,12 @@ async function runRound(taskId: string, round: number, prior: string): Promise<R
404
425
  // literals in values. Hex-encode the prior with an 'h' sentinel so it survives
405
426
  // the split and is kept as a string; the refine agent decodes it.
406
427
  const priorHex = `h${Buffer.from(prior, 'utf8').toString('hex')}`
407
- const args =
408
- round === 1
409
- ? [...baseArgs, '--agent', 'opencode']
410
- : [
411
- ...baseArgs,
412
- '--agent-import-path', REFINE_IMPORT,
413
- '--agent-kwarg', `prior_attempt_hex=${priorHex}`,
414
- ]
428
+ const args = [
429
+ ...baseArgs,
430
+ '--agent-import-path', PROFILE_AGENT_IMPORT,
431
+ '--agent-kwarg', `profile_path=${PROFILE_PATH}`,
432
+ ...(round > 1 ? ['--agent-kwarg', `prior_attempt_hex=${priorHex}`] : []),
433
+ ]
415
434
  await runTb(args, runId)
416
435
  const resolved = await readResolved(outputDir, taskId)
417
436
  const trialDir = await findTrialDir(outputDir, taskId)
@@ -457,6 +476,7 @@ async function solveTask(taskId: string): Promise<{
457
476
 
458
477
  async function main(): Promise<void> {
459
478
  await mkdir(RUNS_DIR, { recursive: true })
479
+ await writeFile(PROFILE_PATH, JSON.stringify(PROFILE, null, 2) + '\n')
460
480
  // Validate auth up front — fail loud before spending a single container.
461
481
  must('OPENAI_API_KEY')
462
482