@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
@@ -13,11 +13,13 @@ import {
13
13
  type AgentProfile,
14
14
  type AgentRunSpec,
15
15
  type OutputAdapter,
16
- routerChatWithUsage,
17
16
  } from '@tangle-network/agent-runtime/kernel'
18
- // `BackendType` is the sandbox SDK's harness union and its canonical home. agent-runtime consumes
19
- // it from there too; it is not re-exported from the kernel barrel.
17
+ import { parseExactAgentProfile } from '@tangle-network/agent-runtime'
18
+ // `BackendType` is the sandbox SDK's harness union and its canonical home. Runtime consumes it
19
+ // from there too; benchmark profiles use the same values as their exact harness identity.
20
20
  import type { BackendType } from '@tangle-network/sandbox'
21
+ import { assertExecutableAgentProfile } from '../../src/runtime/supervise/model-policy'
22
+ import { benchRouterProfile, runBenchRouterTurn } from './router-turn'
21
23
 
22
24
  /** Parse the agent's final answer from the event stream (harness-agnostic).
23
25
  * The default deliverable; a benchmark whose artifact is a file overrides via
@@ -63,18 +65,17 @@ export const llmAnalyst = (cfg: { routerBaseUrl: string; routerKey: string; mode
63
65
  .map((e) => (typeof e === 'string' ? e : JSON.stringify(e)))
64
66
  .join('\n')
65
67
  .slice(-2000)
66
- const { content } = await routerChatWithUsage(cfg, [
68
+ const systemPrompt =
69
+ "You review an AI agent's previous attempt at a task. From the task, the attempt's output, and its execution trace ALONE, judge whether it correctly and completely solved the task. If you find a specific fault — a wrong value, a guessed API signature, a missing step, a misread requirement — name it and give the concrete correction in 1-3 sentences. Reply exactly 'no change needed' if the attempt looks correct and complete."
70
+ const turn = await runBenchRouterTurn(
67
71
  {
68
- role: 'system',
69
- content:
70
- "You review an AI agent's previous attempt at a task. From the task, the attempt's output, and its execution trace ALONE, judge whether it correctly and completely solved the task. If you find a specific fault — a wrong value, a guessed API signature, a missing step, a misread requirement — name it and give the concrete correction in 1-3 sentences. Reply exactly 'no change needed' if the attempt looks correct and complete.",
72
+ routerBaseUrl: cfg.routerBaseUrl,
73
+ routerKey: cfg.routerKey,
74
+ profile: benchRouterProfile('sandbox-run-analyst', cfg.model, { systemPrompt }),
71
75
  },
72
- {
73
- role: 'user',
74
- content: `Task:\n${task ?? '(task unavailable)'}\n\nPrevious answer:\n${last?.output ?? '(none)'}\n\nTrace tail:\n${traceTail}`,
75
- },
76
- ])
77
- return content
76
+ `Task:\n${task ?? '(task unavailable)'}\n\nPrevious answer:\n${last?.output ?? '(none)'}\n\nTrace tail:\n${traceTail}`,
77
+ )
78
+ return turn.finalText
78
79
  }
79
80
 
80
81
  /** Cost-dial backend = the SDK's canonical `BackendType` (single source of truth; no local
@@ -83,43 +84,24 @@ export const llmAnalyst = (cfg: { routerBaseUrl: string; routerKey: string; mode
83
84
  * agent runs — no per-backend worker. */
84
85
  export type WorkerBackendType = BackendType
85
86
 
86
- /** Build the standard sandbox `AgentRunSpec` for a benchmark the worker the
87
- * kernel injects. `backendType` is the cost dial. Model auth is the BOX'S OWN
88
- * provisioned credential: `backend.model` pins provider/model/baseUrl only, and
89
- * the platform generates the in-box provider config keyed to
90
- * `{env:OPENCODE_MODEL_API_KEY}`. Never pass an external router key into the
91
- * box — the egress proxy rejects foreign credentials (403, empty output). */
87
+ /** Build the standard sandbox `AgentRunSpec` for a benchmark. The complete profile is the only
88
+ * behavioral input: its harness selects the box backend and its provider/model select inference.
89
+ * Extra sandbox env remains infrastructure, not a second model-selection path. */
92
90
  export function sandboxAgentRun(opts: {
93
- model: string
94
- routerBaseUrl: string
95
- backendType?: WorkerBackendType
96
- /** In-box model provider. Default `openai` (registered models like gpt-4.1).
97
- * Cheap router models (deepseek/kimi/glm) are not in opencode's `openai`
98
- * registry and 404 in-box — pass `openai-compat` (generic passthrough). */
99
- provider?: string
100
- name?: string
91
+ profile: AgentProfile
101
92
  taskToPrompt?: (task: string) => string
102
93
  /** Extra box-level env (e.g. `TANGLE_SEARCH_DEFAULT_PROVIDER` to pin the in-box
103
94
  * agent's web-search provider, provider keys like EXA_API_KEY). Allowlisted
104
95
  * keys only reach the spawned CLI. Must NOT carry router/model credentials. */
105
96
  env?: Record<string, string>
106
- /** The developer's AgentProfile — the one knob for "which agent" (prompt / model /
107
- * tools / mcp). Spread through verbatim; the backend cost-dial is tagged into
108
- * metadata. Omitted ⇒ a minimal worker profile. */
109
- profile?: AgentProfile
110
97
  }): AgentRunSpec<string> {
111
- const backendType = opts.backendType ?? 'opencode'
112
- const name = opts.profile?.name ?? opts.name ?? `${backendType}-worker`
98
+ const profile = parseExactAgentProfile(opts.profile, 'sandboxAgentRun profile')
99
+ assertExecutableAgentProfile(profile, 'sandboxAgentRun profile')
100
+ const name = profile.name ?? 'sandbox-worker'
113
101
  return {
114
- profile: { ...opts.profile, name, metadata: { ...opts.profile?.metadata, backendType } },
102
+ profile,
115
103
  name,
116
104
  taskToPrompt: opts.taskToPrompt ?? ((t) => t),
117
- sandboxOverrides: {
118
- ...(opts.env ? { env: opts.env } : {}),
119
- backend: {
120
- type: backendType,
121
- model: { provider: opts.provider ?? 'openai', model: opts.model, baseUrl: opts.routerBaseUrl },
122
- },
123
- },
105
+ ...(opts.env ? { sandboxOverrides: { env: opts.env } } : {}),
124
106
  }
125
107
  }
@@ -13,6 +13,12 @@
13
13
  * `opencode/zai-coding-plan/glm-5.1`), so `harness` here is just the label.
14
14
  */
15
15
  import { createExecutor } from '@tangle-network/agent-runtime/kernel'
16
+ import {
17
+ defineAgentProfileSecretRef,
18
+ harnessProviders,
19
+ type AgentProfile,
20
+ type HarnessType,
21
+ } from '@tangle-network/agent-interface'
16
22
  import type { SearchArm } from './profiles'
17
23
  import { armLabel } from './profiles'
18
24
  import type { SearchCellResult } from './run.mts'
@@ -22,17 +28,63 @@ const nativeWebDisallowed = ['WebSearch', 'WebFetch', 'web_search', 'web_fetch',
22
28
 
23
29
  /** Build the cli-bridge `agent_profile` for one arm (bridge dialect: disable via
24
30
  * `metadata.disallowedTools`, search MCP via `mcp.<name>.transport:'http'`). */
25
- function bridgeProfile(arm: SearchArm, routerSearchMcp: string, tangleApiKey: string, label: string): Record<string, unknown> {
26
- if (arm === 'native') return { name: `search-bench-${label}` }
27
- const base = { name: `search-bench-${label}`, metadata: { disallowedTools: nativeWebDisallowed } }
31
+ function bridgeProfile(
32
+ arm: SearchArm,
33
+ routerSearchMcp: string,
34
+ tangleApiKey: string,
35
+ label: string,
36
+ harness: HarnessType,
37
+ wireModel: string,
38
+ ): AgentProfile {
39
+ const prefix = `${harness}/`
40
+ if (!wireModel.startsWith(prefix)) {
41
+ throw new Error(
42
+ `bridgeProfile: wire model '${wireModel}' must start with harness '${prefix}'`,
43
+ )
44
+ }
45
+ const modelPath = wireModel.slice(prefix.length)
46
+ const segments = modelPath.split('/').filter(Boolean)
47
+ if (segments.length === 0) {
48
+ throw new Error(`bridgeProfile: wire model '${wireModel}' has no model id`)
49
+ }
50
+ let provider: string
51
+ let model: string
52
+ if (segments.length === 1) {
53
+ const providers = harnessProviders(harness)
54
+ if (providers?.length !== 1) {
55
+ throw new Error(
56
+ `bridgeProfile: wire model '${wireModel}' must include a provider for harness '${harness}'`,
57
+ )
58
+ }
59
+ provider = providers[0]!
60
+ model = segments[0]!
61
+ } else {
62
+ provider = segments[0]!
63
+ model = segments.slice(1).join('/')
64
+ }
65
+ const identity: AgentProfile = {
66
+ name: `search-bench-${label}`,
67
+ harness,
68
+ model: { provider, default: model },
69
+ }
70
+ if (arm === 'native') return identity
71
+ const base: AgentProfile = {
72
+ ...identity,
73
+ metadata: { disallowedTools: nativeWebDisallowed },
74
+ }
28
75
  if (arm === 'off') return base
76
+ if (!tangleApiKey) {
77
+ throw new Error(`bridgeProfile: provider arm requires TANGLE_API_KEY`)
78
+ }
29
79
  return {
30
80
  ...base,
31
81
  mcp: {
32
82
  tangle_search: {
33
83
  transport: 'http',
34
84
  url: `${routerSearchMcp}?provider=${encodeURIComponent(arm.provider)}`,
35
- headers: { Authorization: `Bearer ${tangleApiKey}` },
85
+ headers: {
86
+ Authorization: defineAgentProfileSecretRef('TANGLE_API_KEY', 'bearer'),
87
+ },
36
88
  enabled: true,
37
89
  },
38
90
  },
@@ -58,7 +110,7 @@ export interface BridgeCfg {
58
110
  export async function runBridgeCell(
59
111
  cfg: BridgeCfg,
60
112
  task: SearchTask,
61
- harness: string,
113
+ harness: HarnessType,
62
114
  arm: SearchArm,
63
115
  ): Promise<SearchCellResult> {
64
116
  const startedAt = Date.now()
@@ -76,14 +128,20 @@ export async function runBridgeCell(
76
128
  try {
77
129
  // One harness turn through the unified bridge executor — same backend the
78
130
  // loop path uses; this cell scorer just adds oracle scoring + citations.
131
+ const profile = bridgeProfile(
132
+ arm,
133
+ cfg.routerSearchMcp,
134
+ cfg.tangleApiKey,
135
+ `${harness}-${armId}`,
136
+ harness,
137
+ cfg.bridgeModels[harness] ?? harness,
138
+ )
79
139
  const exec = createExecutor({
80
140
  backend: 'bridge',
81
141
  bridgeUrl: cfg.bridgeUrl,
82
142
  bridgeBearer: cfg.bridgeBearer,
83
- model: cfg.bridgeModels[harness] ?? harness,
84
- agentProfile: bridgeProfile(arm, cfg.routerSearchMcp, cfg.tangleApiKey, `${harness}-${armId}`),
85
143
  timeoutMs: cfg.timeoutMs ?? 300_000,
86
- })({ profile: { name: `${harness}-${armId}` }, harness: null }, { signal: controller.signal, seams: {} })
144
+ })({ profile, harness: null }, { signal: controller.signal, seams: {} })
87
145
  // bridgeExecutor is one-shot (async execute resolves an ExecutorResult).
88
146
  const artifact = (await exec.execute(taskToPrompt(task), controller.signal)) as {
89
147
  out: unknown
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { writeFileSync } from 'node:fs'
13
13
  import { runPool } from '../run-pool'
14
- import { routerChatWithUsage } from '@tangle-network/agent-runtime/kernel'
14
+ import { runBenchRouterTurn } from '../router-turn'
15
15
  import { freshTasks } from './tasks-fresh'
16
16
  import { scoreTask, taskToPrompt } from './tasks'
17
17
 
@@ -28,9 +28,24 @@ async function main(): Promise<void> {
28
28
 
29
29
  const outcomes = await runPool(freshTasks, conc, async (task) => {
30
30
  try {
31
- const res = await routerChatWithUsage(cfg, [{ role: 'user', content: taskToPrompt(task) }])
32
- const { score } = scoreTask(task, res.content)
33
- return { id: task.id, score: score as 0 | 1 | null, cost: res.costUsd, err: undefined as string | undefined }
31
+ const res = await runBenchRouterTurn(
32
+ {
33
+ routerBaseUrl: cfg.routerBaseUrl,
34
+ routerKey: cfg.routerKey,
35
+ profile: {
36
+ name: 'search-parametric-check',
37
+ model: { provider: 'tangle-router', default: model },
38
+ },
39
+ },
40
+ taskToPrompt(task),
41
+ )
42
+ const { score } = scoreTask(task, res.finalText)
43
+ return {
44
+ id: task.id,
45
+ score: score as 0 | 1 | null,
46
+ cost: res.usage.usdKnown === false ? undefined : res.usage.costUsd,
47
+ err: undefined as string | undefined,
48
+ }
34
49
  } catch (err) {
35
50
  return { id: task.id, score: null as 0 | 1 | null, cost: undefined, err: err instanceof Error ? err.message : String(err) }
36
51
  }
@@ -17,7 +17,12 @@
17
17
  * shape — a `transport:'http'` server at the router's `/v1/search/mcp` endpoint,
18
18
  * provider pinned via the `?provider=` query param.
19
19
  */
20
- import type { AgentProfile } from '@tangle-network/agent-interface'
20
+ import {
21
+ type AgentProfile,
22
+ type HarnessType,
23
+ agentProfileSchema,
24
+ defineAgentProfileSecretRef,
25
+ } from '@tangle-network/agent-interface'
21
26
 
22
27
  export type SearchArm = 'native' | 'off' | { provider: string }
23
28
 
@@ -45,37 +50,45 @@ export interface BuildArmProfileArgs {
45
50
  * foreign router credentials (403), so the provider arm needs the box-side
46
51
  * credential flow before sandbox-backed provider runs are trustworthy. */
47
52
  tangleApiKey: string
53
+ harness: HarnessType
54
+ model: string
55
+ provider: string
48
56
  /** Optional name/metadata to merge. */
49
57
  name?: string
50
58
  metadata?: Record<string, unknown>
51
59
  }
52
60
 
53
61
  /**
54
- * Build the AgentProfile fragment (tools / permission / mcp) for one search arm.
55
- * Returned as a partial profile to be spread into `sandboxAgentRun({ profile })`.
62
+ * Build the complete executable AgentProfile for one search arm.
56
63
  */
57
64
  export function buildArmProfile(args: BuildArmProfileArgs): AgentProfile {
58
65
  const { arm, routerBaseUrl, tangleApiKey } = args
59
66
  const base: AgentProfile = {
60
67
  name: args.name ?? 'search-bench-worker',
68
+ harness: args.harness,
69
+ model: { provider: args.provider, default: args.model },
61
70
  ...(args.metadata ? { metadata: args.metadata } : {}),
62
- } as AgentProfile
71
+ }
63
72
 
64
73
  if (arm === 'native') {
65
74
  // Native web tools stay on (harness default). No search MCP. For codex,
66
75
  // whose web_search ships off, explicitly enable it so the native arm is real.
67
- return { ...base, tools: { web_search: true } } as AgentProfile
76
+ return agentProfileSchema.parse({ ...base, tools: { web_search: true } })
68
77
  }
69
78
 
70
79
  if (arm === 'off') {
71
80
  // No web access at all — the parametric floor (search contributes nothing).
72
- return { ...base, tools: { ...nativeWebToolsDisabled }, permission: { webfetch: 'deny' } } as AgentProfile
81
+ return agentProfileSchema.parse({
82
+ ...base,
83
+ tools: { ...nativeWebToolsDisabled },
84
+ permission: { webfetch: 'deny' },
85
+ })
73
86
  }
74
87
 
75
88
  if (!tangleApiKey) {
76
89
  throw new Error(`buildArmProfile: provider arm "${arm.provider}" requires a tangleApiKey for the search MCP`)
77
90
  }
78
- return {
91
+ return agentProfileSchema.parse({
79
92
  ...base,
80
93
  tools: { ...nativeWebToolsDisabled },
81
94
  permission: { webfetch: 'deny' },
@@ -83,11 +96,13 @@ export function buildArmProfile(args: BuildArmProfileArgs): AgentProfile {
83
96
  tangle_search: {
84
97
  transport: 'http',
85
98
  url: routerSearchMcpUrl(arm.provider, routerBaseUrl),
86
- headers: { Authorization: `Bearer ${tangleApiKey}` },
99
+ headers: {
100
+ Authorization: defineAgentProfileSecretRef('TANGLE_API_KEY', 'bearer'),
101
+ },
87
102
  enabled: true,
88
103
  },
89
104
  },
90
- } as AgentProfile
105
+ })
91
106
  }
92
107
 
93
108
  /** Stable condition label for the corpus: `<harness>:<arm>`. */
@@ -135,14 +135,13 @@ async function runCell(
135
135
  const timer = setTimeout(() => controller.abort(), cfg.timeoutMs ?? 300_000)
136
136
  try {
137
137
  const agentRun = sandboxAgentRun({
138
- model: cfg.model,
139
- routerBaseUrl: cfg.routerBaseUrl,
140
- backendType: harness,
141
- ...(cfg.provider ? { provider: cfg.provider } : {}),
142
138
  profile: buildArmProfile({
143
139
  arm,
144
140
  routerBaseUrl: cfg.routerBaseUrl,
145
141
  tangleApiKey: cfg.tangleApiKey,
142
+ harness,
143
+ model: cfg.model,
144
+ provider: cfg.provider ?? 'openai',
146
145
  name: `search-bench-${harness}-${armId}`,
147
146
  metadata: { harness, arm: armId, taskId: task.id },
148
147
  }),
@@ -37,12 +37,14 @@ import {
37
37
  defaultStructuralRolloutPolicy,
38
38
  runAgentic,
39
39
  type StructuralRolloutResult,
40
+ defaultAnalystInstruction,
40
41
  sandboxCheckRunner,
41
42
  selectBestIndex,
42
43
  structuralRollout,
43
44
  visibleCheckScore,
44
45
  } from '../../src/runtime/index'
45
46
  import { basePrompt, type HumanEvalTask, loadHumanEval, runChecker } from './benchmarks/humaneval'
47
+ import { withBenchProfile } from './router-turn'
46
48
 
47
49
  function must(name: string): string {
48
50
  const v = process.env[name]
@@ -173,20 +175,34 @@ async function runTask(t: HumanEvalTask): Promise<TaskRow> {
173
175
  name: 'humaneval-inert',
174
176
  check: () => ({ passes: 0, total: 1, errored: 0 }),
175
177
  })
178
+ const workerProfile = withBenchProfile(
179
+ {
180
+ name: 'humaneval-structural-worker',
181
+ harness: 'cli-base',
182
+ model: { provider: 'together', default: MODEL },
183
+ tools: { submit_answer: true },
184
+ },
185
+ { systemPrompt, temperature: TEMP, maxTokens: MAX_TOKENS, maxTurns: 2 },
186
+ )
187
+ const analystProfile = withBenchProfile(
188
+ {
189
+ name: 'humaneval-structural-analyst',
190
+ harness: 'cli-base',
191
+ model: { provider: 'together', default: MODEL },
192
+ },
193
+ { systemPrompt: defaultAnalystInstruction, temperature: TEMP, maxTokens: MAX_TOKENS },
194
+ )
176
195
  const result = (await runAgentic({
177
196
  surface,
178
197
  task: {
179
198
  id: t.taskId,
180
- systemPrompt,
181
199
  userPrompt: basePrompt(t),
182
200
  meta: { entryPoint: t.entryPoint },
183
201
  },
184
202
  routerBaseUrl: BASE,
185
203
  routerKey: must('TOGETHER_API_KEY'),
186
- model: MODEL,
187
- temperature: TEMP,
188
- maxTokens: MAX_TOKENS,
189
- innerTurns: 2,
204
+ workerProfile,
205
+ analystProfile,
190
206
  strategy,
191
207
  // The strategy's documented sizing: k samples + repair rounds + the check-author consult.
192
208
  budget: policy.k + policy.repairRounds + 1,
@@ -37,6 +37,7 @@ import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from
37
37
  import { tmpdir } from 'node:os'
38
38
  import { join } from 'node:path'
39
39
  import { type HumanEvalTask, basePrompt, extractCode, loadHumanEval } from './benchmarks/humaneval'
40
+ import { runBenchRouterTurn } from './router-turn'
40
41
  import { pool } from './stats.mts'
41
42
 
42
43
  // ---------- pre-registered task sets (verbatim from the prereg; DO NOT EDIT) ----------
@@ -294,33 +295,40 @@ async function complete(cfg: ClientCfg, messages: Array<{ role: string; content:
294
295
  let lastErr = ''
295
296
  for (let attempt = 1; attempt <= 4; attempt += 1) {
296
297
  if (attempt > 1) await new Promise((r) => setTimeout(r, 2000 * 2 ** attempt))
297
- const ctl = new AbortController()
298
- const timer = setTimeout(() => ctl.abort(), Number(process.env.LLM_TIMEOUT_MS ?? 240_000))
299
298
  try {
300
- const res = await fetch(`${cfg.base}/chat/completions`, {
301
- method: 'POST',
302
- headers: { Authorization: `Bearer ${cfg.key}`, 'Content-Type': 'application/json' },
303
- body: JSON.stringify({ model: cfg.model, max_tokens: cfg.maxTokens, temperature: cfg.temperature, messages }),
304
- signal: ctl.signal,
305
- })
306
- if (!res.ok) {
307
- lastErr = `HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`
308
- continue
309
- }
310
- const d = (await res.json()) as {
311
- choices?: Array<{ message?: { content?: string } }>
312
- usage?: { prompt_tokens?: number; completion_tokens?: number }
313
- }
314
- const content = d.choices?.[0]?.message?.content ?? ''
299
+ const system = messages.find((message) => message.role === 'system')?.content
300
+ const result = await runBenchRouterTurn(
301
+ {
302
+ routerBaseUrl: cfg.base,
303
+ routerKey: cfg.key,
304
+ profile: {
305
+ name: 'supervisor-arena-agent',
306
+ harness: 'cli-base',
307
+ model: {
308
+ provider: 'tangle-router',
309
+ default: cfg.model,
310
+ metadata: { temperature: cfg.temperature, maxTokens: cfg.maxTokens },
311
+ },
312
+ ...(system ? { prompt: { systemPrompt: system } } : {}),
313
+ },
314
+ timeoutMs: Number(process.env.LLM_TIMEOUT_MS ?? 240_000),
315
+ },
316
+ { messages: messages.filter((message) => message.role !== 'system') },
317
+ )
318
+ if (result.usage.tokensKnown === false) throw new Error('provider omitted token usage')
319
+ const content = result.finalText
315
320
  if (content.trim() === '') {
316
321
  lastErr = 'empty content'
317
322
  continue
318
323
  }
319
- return { content, attempts: attempt, tokensIn: d.usage?.prompt_tokens ?? 0, tokensOut: d.usage?.completion_tokens ?? 0 }
324
+ return {
325
+ content,
326
+ attempts: attempt,
327
+ tokensIn: result.usage.input,
328
+ tokensOut: result.usage.output,
329
+ }
320
330
  } catch (e) {
321
331
  lastErr = e instanceof Error ? e.message : String(e)
322
- } finally {
323
- clearTimeout(timer)
324
332
  }
325
333
  }
326
334
  throw new Error(`completion failed after retries: ${lastErr}`)
@@ -17,6 +17,13 @@ import { chmod, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises
17
17
  import { join, resolve } from 'node:path'
18
18
  import { homedir } from 'node:os'
19
19
  import { fileURLToPath } from 'node:url'
20
+ import type { AgentProfile } from '@tangle-network/agent-interface'
21
+ import {
22
+ collectAgentTurn,
23
+ createExecutor,
24
+ streamAgentTurn,
25
+ type CollectedAgentTurn,
26
+ } from '@tangle-network/agent-runtime/kernel'
20
27
  import { run, runOk, type RunResult, shq } from './proc'
21
28
  import { materializeWorkspace } from './materialize'
22
29
  import type { ArmSpec, SoloUsage } from './types'
@@ -192,12 +199,15 @@ export interface SoloArmSpec {
192
199
  kind: 'solo'
193
200
  /** Ledger/run-dir label, e.g. 'SOLO'. */
194
201
  name: string
195
- /** opencode model id, e.g. 'zai-coding-plan/glm-5.2'. */
196
- model: string
202
+ /** Complete worker identity. Runtime reads harness/provider/model/prompt only from here. */
203
+ profile: AgentProfile
197
204
  /** Appended to the problem statement. Default: the shared worker suffix. */
198
205
  promptSuffix?: string
199
206
  /** Whole-run ceiling. solo.sh used `timeout 1000` (s). */
200
207
  timeoutMs?: number
208
+ /** Runtime bridge transport. Defaults to the CLI_BRIDGE environment variables. */
209
+ bridgeUrl?: string
210
+ bridgeBearer?: string
201
211
  }
202
212
 
203
213
  export interface SupervisorArmSpec {
@@ -271,8 +281,8 @@ export async function toArmIdentity(spec: ExecutableArmSpec): Promise<ArmSpec> {
271
281
  return {
272
282
  name: spec.name,
273
283
  kind: 'solo',
274
- env: { model: spec.model },
275
- provenance: { repo: 'opencode', commit: 'cli' },
284
+ env: { model: spec.profile.model?.default ?? 'missing' },
285
+ provenance: { repo: spec.profile.harness ?? 'missing', commit: 'runtime-bridge' },
276
286
  }
277
287
  }
278
288
  const loopsRepo = spec.loopsRepo ?? DEFAULT_LOOPS_REPO
@@ -450,7 +460,29 @@ async function verifyWorkspace(
450
460
  return res.code
451
461
  }
452
462
 
453
- /** solo.sh port: materialize → prompt → opencode run → patch extract → verify. */
463
+ function runtimeSoloUsage(turn: CollectedAgentTurn): SoloUsage {
464
+ const calls = turn.events.filter((event) => event.type === 'llm_call')
465
+ const maxCtx = calls.reduce(
466
+ (max, event) => Math.max(max, (event.tokensIn ?? 0) + (event.tokensOut ?? 0)),
467
+ 0,
468
+ )
469
+ const cacheRead = Number(turn.usage.promptCache?.readTokens ?? 0)
470
+ const cacheWrite = Number(turn.usage.promptCache?.writeTokens ?? 0)
471
+ const reasoning = turn.usage.reasoningTokens ?? 0
472
+ return {
473
+ steps: calls.length,
474
+ in: turn.usage.input,
475
+ out: turn.usage.output,
476
+ reasoning,
477
+ cache_w: Number.isFinite(cacheWrite) ? cacheWrite : 0,
478
+ cache_r: Number.isFinite(cacheRead) ? cacheRead : 0,
479
+ max_ctx: maxCtx,
480
+ oc_cost: turn.usage.costUsd ?? turn.usage.estimatedCostUsd ?? 0,
481
+ total_io: turn.usage.input + turn.usage.output + reasoning,
482
+ }
483
+ }
484
+
485
+ /** Solo arm: materialize → exact Runtime profile turn → patch extract → verify. */
454
486
  export async function runSoloArm(spec: SoloArmSpec, ctx: ArmRunContext): Promise<SoloArmResult> {
455
487
  const runDir = join(ctx.outDir, 'runs', ctx.instanceId, spec.name)
456
488
  const ws = join(runDir, 'ws')
@@ -469,18 +501,44 @@ export async function runSoloArm(spec: SoloArmSpec, ctx: ArmRunContext): Promise
469
501
  })
470
502
 
471
503
  const promptFile = join(runDir, 'prompt.txt')
472
- await writeFile(promptFile, ctx.problemStatement + (spec.promptSuffix ?? WORKER_PROMPT_SUFFIX))
504
+ const prompt = ctx.problemStatement + (spec.promptSuffix ?? WORKER_PROMPT_SUFFIX)
505
+ await writeFile(promptFile, prompt)
506
+
507
+ const bridgeUrl = spec.bridgeUrl ?? process.env.CLI_BRIDGE_URL ?? process.env.BRIDGE_URL
508
+ const bridgeBearer =
509
+ spec.bridgeBearer ?? process.env.CLI_BRIDGE_BEARER ?? process.env.BRIDGE_BEARER
510
+ if (!bridgeUrl || !bridgeBearer) {
511
+ throw new Error(
512
+ 'runSoloArm requires CLI_BRIDGE_URL/BRIDGE_URL and CLI_BRIDGE_BEARER/BRIDGE_BEARER',
513
+ )
514
+ }
473
515
 
474
516
  const t0 = Date.now()
475
- const oc = await dotenvxBash(
476
- ctx.secrets,
477
- `cd ${shq(ws)} && opencode run "$(cat ${shq(promptFile)})" -m ${shq(spec.model)} --format json`,
478
- { timeoutMs: spec.timeoutMs ?? 1_000_000, env: cell.env, signal: ctx.signal },
517
+ const timeoutMs = spec.timeoutMs ?? 1_000_000
518
+ const factory = createExecutor({
519
+ backend: 'bridge',
520
+ bridgeUrl,
521
+ bridgeBearer,
522
+ cwd: ws,
523
+ timeoutMs,
524
+ })
525
+ const turn = await collectAgentTurn(
526
+ streamAgentTurn(
527
+ { kind: 'executor', factory, profile: spec.profile },
528
+ prompt,
529
+ { timeoutMs, ...(ctx.signal ? { signal: ctx.signal } : {}) },
530
+ ),
479
531
  )
480
532
  const wall_s = Math.round((Date.now() - t0) / 1000)
481
- await writeFile(join(runDir, 'oc.jsonl'), oc.stdout)
482
- await writeFile(join(runDir, 'oc.err'), oc.stderr)
483
- assertSoloProcessCompleted(oc, `runSoloArm ${ctx.instanceId}/${spec.name}`)
533
+ await writeFile(
534
+ join(runDir, 'runtime-events.jsonl'),
535
+ turn.events.map((event) => JSON.stringify(event)).join('\n') + '\n',
536
+ )
537
+ if (turn.status !== 'completed') {
538
+ throw new Error(
539
+ `runSoloArm ${ctx.instanceId}/${spec.name}: ${turn.error?.message ?? turn.status}`,
540
+ )
541
+ }
484
542
  if (ctx.signal?.aborted) throw ctx.signal.reason
485
543
 
486
544
  const patch = await extractPatch(ws, ctx.baseCommit, ctx.excludes, ctx.signal)
@@ -492,12 +550,12 @@ export async function runSoloArm(spec: SoloArmSpec, ctx: ArmRunContext): Promise
492
550
  return {
493
551
  arm: spec.name,
494
552
  iid: ctx.instanceId,
495
- oc_rc: oc.code,
553
+ oc_rc: 0,
496
554
  wall_s,
497
555
  patch_lines: patchLineCount(patch),
498
556
  verify_rc,
499
557
  verify_pass: verify_rc === 0,
500
- usage: parseOcUsage(oc.stdout),
558
+ usage: runtimeSoloUsage(turn),
501
559
  patchPath,
502
560
  ws,
503
561
  }