@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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * QUANT-ARENA campaign loop — the improvement loop embodied for trading
3
- * strategies. One command runs: strategy authors (Claude, profile-pinned)
3
+ * strategies. One command runs: strategy authors (Runtime profile-pinned)
4
4
  * propose candidate strategies (v2 `onBar` contract, driven incrementally by
5
5
  * driver.ts) -> every candidate passes a two-stage leak audit -> survivors
6
6
  * are scored on K bootstrap in-sample windows against the pinned baselines
@@ -15,7 +15,7 @@
15
15
  * are never the acceptance currency.
16
16
  *
17
17
  * tsx src/quant-arena/quant-loop.mts --out <dir> [--candidates 2] [--seed 20260722]
18
- * [--author-model sonnet] [--audit-model haiku] [--skip-llm-audit]
18
+ * [--author-model glm-5.2] [--audit-model glm-5.2] [--skip-llm-audit]
19
19
  *
20
20
  * Kernel reuse (import, not copy — see src/swe-arena/):
21
21
  * - cost accounting: the lib's durable CostLedger (createRunCostLedger) +
@@ -38,12 +38,12 @@ import { join } from 'node:path'
38
38
  import { fileURLToPath, pathToFileURL } from 'node:url'
39
39
  import process from 'node:process'
40
40
  import { createRunCostLedger, fsCampaignStorage } from '@tangle-network/agent-eval/campaign'
41
+ import { agentProfileSchema, type AgentProfile } from '@tangle-network/agent-interface'
42
+ import { collectAgentTurn, createExecutor, streamAgentTurn } from '@tangle-network/agent-runtime/kernel'
41
43
  import { loadCampaignCells } from '../swe-arena/cell-evidence.mts'
42
44
  import { reconcileCrashOrphansOnDisk } from '../swe-arena/ledger-orphans.mts'
43
45
  import { loadLedgerReceipts } from '../swe-arena/manifest.mts'
44
- import { loadAuthorProfile, type ProposerSpec } from '../swe-arena/proposer-fanout.mts'
45
- import { proposerShotEnv } from '../swe-arena/outer-loop.mts'
46
- import { run } from '../swe-arena/proc.ts'
46
+ import { resolveAuthorProfile, type ProposerSpec } from '../swe-arena/proposer-fanout.mts'
47
47
  import { runBacktest, statsForRange, type BacktestConfig, type RangeStats } from './backtest.ts'
48
48
  import { loadInSample, type AlignedBars } from './data.ts'
49
49
  import { loadStrategyFile } from './driver.ts'
@@ -57,6 +57,9 @@ import * as equalWeight from './strategies/equal-weight/strategy.ts'
57
57
  import * as smaCrossover from './strategies/sma-crossover/strategy.ts'
58
58
 
59
59
  export const QUANT_PROFILES_DIR = fileURLToPath(new URL('./profiles', import.meta.url))
60
+ const DEFAULT_AUTHOR_PROFILE = fileURLToPath(
61
+ new URL('../swe-arena/profiles/default-author.profile.json', import.meta.url),
62
+ )
60
63
 
61
64
  // ---------------------------------------------------------------------------
62
65
  // Config.
@@ -88,11 +91,17 @@ export const PINNED_BASELINES: Record<string, GenerateSignals> = {
88
91
  /** The two demo author seats: the plain author and the quant lens. */
89
92
  export function defaultQuantProposers(): ProposerSpec[] {
90
93
  return [
91
- { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' },
94
+ {
95
+ name: 'default-author',
96
+ profile: DEFAULT_AUTHOR_PROFILE,
97
+ harness: 'pi',
98
+ model: 'glm-5.2',
99
+ },
92
100
  {
93
101
  name: 'quant-researcher',
94
102
  profile: join(QUANT_PROFILES_DIR, 'quant-researcher.profile.json'),
95
- harness: 'claude-code',
103
+ harness: 'pi',
104
+ model: 'glm-5.2',
96
105
  lens:
97
106
  'Favor ONE economically-motivated effect (trend, mean reversion, vol targeting) with few parameters. ' +
98
107
  'State the regime in which it should work and keep turnover low enough that 15bps a side cannot eat the edge.',
@@ -110,8 +119,8 @@ export function defaultConfig(outDir: string): QuantLoopConfig {
110
119
  warmupDays: 120,
111
120
  costBps: 10,
112
121
  slippageBps: 5,
113
- authorModel: 'sonnet',
114
- auditModel: 'haiku',
122
+ authorModel: 'glm-5.2',
123
+ auditModel: 'glm-5.2',
115
124
  skipLlmAudit: false,
116
125
  authorTimeoutMs: 480_000,
117
126
  auditTimeoutMs: 240_000,
@@ -183,10 +192,10 @@ export async function loadNotebookRows(notebookPath: string): Promise<Array<Reco
183
192
  const log = (msg: string): void => console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`)
184
193
 
185
194
  // ---------------------------------------------------------------------------
186
- // Claude shots (author + auditor) — metered paid calls through the run ledger.
195
+ // Exact-profile shots (author + auditor) — metered paid calls through Runtime + the run ledger.
187
196
  // ---------------------------------------------------------------------------
188
197
 
189
- interface ClaudeShotOutcome {
198
+ interface ProfileShotOutcome {
190
199
  text: string
191
200
  model: string
192
201
  inputTokens: number
@@ -197,64 +206,81 @@ interface ClaudeShotOutcome {
197
206
 
198
207
  type Ledger = ReturnType<typeof createRunCostLedger>
199
208
 
200
- async function claudeShot(opts: {
209
+ function withModel(profile: AgentProfile, model: string, name = profile.name): AgentProfile {
210
+ return agentProfileSchema.parse({
211
+ ...profile,
212
+ ...(name ? { name } : {}),
213
+ model: { ...profile.model, default: model },
214
+ })
215
+ }
216
+
217
+ function quantAuditProfile(model: string): AgentProfile {
218
+ return agentProfileSchema.parse({
219
+ name: 'quant-leak-auditor',
220
+ harness: 'pi',
221
+ model: { provider: 'tangle-router', default: model },
222
+ prompt: {
223
+ systemPrompt:
224
+ 'Audit the supplied trading strategy for look-ahead bias and nondeterminism. Follow the requested JSON response contract exactly.',
225
+ },
226
+ })
227
+ }
228
+
229
+ async function profileShot(opts: {
201
230
  prompt: string
202
- model: string
203
- systemPrompt?: string
231
+ profile: AgentProfile
204
232
  timeoutMs: number
205
233
  cwd: string
206
- }): Promise<ClaudeShotOutcome> {
207
- const argv = [
208
- '-p',
209
- '--output-format',
210
- 'json',
211
- '--model',
212
- opts.model,
213
- // The shot is pure text generation: no filesystem, no shell, no web.
214
- '--disallowed-tools',
215
- 'Bash,Edit,Write,Read,Glob,Grep,WebFetch,WebSearch,Task,NotebookEdit',
216
- ...(opts.systemPrompt ? ['--append-system-prompt', opts.systemPrompt] : []),
217
- ]
218
- const res = await run('claude', argv, {
219
- stdin: opts.prompt,
234
+ }): Promise<ProfileShotOutcome> {
235
+ const bridgeUrl = process.env.CLI_BRIDGE_URL ?? process.env.BRIDGE_URL
236
+ const bridgeBearer = process.env.CLI_BRIDGE_BEARER ?? process.env.BRIDGE_BEARER
237
+ if (!bridgeUrl || !bridgeBearer) {
238
+ throw new Error(
239
+ 'quant profile shots require CLI_BRIDGE_URL/BRIDGE_URL and CLI_BRIDGE_BEARER/BRIDGE_BEARER',
240
+ )
241
+ }
242
+ const factory = createExecutor({
243
+ backend: 'bridge',
244
+ bridgeUrl,
245
+ bridgeBearer,
220
246
  cwd: opts.cwd,
221
- env: proposerShotEnv('claude-code'),
222
247
  timeoutMs: opts.timeoutMs,
223
248
  })
224
- if (res.code !== 0) {
225
- throw new Error(`claude shot exited ${res.code}${res.timedOut ? ' (timeout)' : ''}: ${(res.stderr || res.stdout).slice(0, 800)}`)
226
- }
227
- let parsed: Record<string, unknown>
228
- try {
229
- parsed = JSON.parse(res.stdout) as Record<string, unknown>
230
- } catch {
231
- throw new Error(`claude shot: unparseable --output-format json stdout: ${res.stdout.slice(0, 400)}`)
249
+ const turn = await collectAgentTurn(
250
+ streamAgentTurn(
251
+ { kind: 'executor', factory, profile: opts.profile },
252
+ opts.prompt,
253
+ { timeoutMs: opts.timeoutMs },
254
+ ),
255
+ )
256
+ if (turn.status !== 'completed') {
257
+ throw new Error(turn.error?.message ?? `quant profile shot ended with ${turn.status}`)
232
258
  }
233
- if (parsed.is_error === true) throw new Error(`claude shot errored: ${String(parsed.result).slice(0, 800)}`)
234
- const usage = (parsed.usage ?? {}) as Record<string, unknown>
235
- const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
259
+ const cachedTokens = Number(turn.usage.promptCache?.readTokens ?? 0)
236
260
  return {
237
- text: typeof parsed.result === 'string' ? parsed.result : '',
238
- model: typeof parsed.model === 'string' ? parsed.model : opts.model,
239
- inputTokens: num(usage.input_tokens),
240
- outputTokens: num(usage.output_tokens),
241
- cachedTokens: num(usage.cache_read_input_tokens),
242
- costUsd: typeof parsed.total_cost_usd === 'number' ? parsed.total_cost_usd : null,
261
+ text: turn.finalText,
262
+ model: turn.usage.model ?? opts.profile.model?.default ?? 'unknown',
263
+ inputTokens: turn.usage.input,
264
+ outputTokens: turn.usage.output,
265
+ cachedTokens: Number.isFinite(cachedTokens) ? cachedTokens : 0,
266
+ costUsd: turn.usage.costUsd ?? null,
243
267
  }
244
268
  }
245
269
 
246
- async function meteredClaudeShot(
270
+ async function meteredProfileShot(
247
271
  ledger: Ledger,
248
272
  meta: { phase: string; actor: string; tags: Record<string, string> },
249
- opts: Parameters<typeof claudeShot>[0],
250
- ): Promise<{ outcome: ClaudeShotOutcome; costUsd: number | null }> {
251
- const paid = await ledger.runPaidCall<ClaudeShotOutcome>({
273
+ opts: Parameters<typeof profileShot>[0],
274
+ ): Promise<{ outcome: ProfileShotOutcome; costUsd: number | null }> {
275
+ const model = opts.profile.model?.default
276
+ if (!model) throw new Error('meteredProfileShot: profile.model.default is required')
277
+ const paid = await ledger.runPaidCall<ProfileShotOutcome>({
252
278
  channel: 'driver',
253
279
  phase: meta.phase,
254
280
  actor: meta.actor,
255
- model: opts.model,
281
+ model,
256
282
  tags: meta.tags,
257
- execute: () => claudeShot(opts),
283
+ execute: () => profileShot(opts),
258
284
  receipt: (v) => ({
259
285
  model: v.model,
260
286
  inputTokens: v.inputTokens,
@@ -379,12 +405,12 @@ async function llmLeakAudit(
379
405
  code: string,
380
406
  ): Promise<{ verdict: 'clean' | 'leak' | 'inconclusive'; evidence: string }> {
381
407
  for (let attempt = 0; attempt < 2; attempt++) {
382
- const { outcome } = await meteredClaudeShot(
408
+ const { outcome } = await meteredProfileShot(
383
409
  ledger,
384
- { phase: 'audit.leak', actor: 'leak-auditor:claude', tags: { candidateId, attempt: String(attempt) } },
410
+ { phase: 'audit.leak', actor: 'leak-auditor:runtime', tags: { candidateId, attempt: String(attempt) } },
385
411
  {
386
412
  prompt: AUDIT_PROMPT_HEADER + '```ts\n' + code + '\n```',
387
- model: config.auditModel,
413
+ profile: quantAuditProfile(config.auditModel),
388
414
  timeoutMs: config.auditTimeoutMs,
389
415
  cwd: config.outDir,
390
416
  },
@@ -630,7 +656,9 @@ async function runQuantCampaignWithWorker(worker: VbtWorker, config: QuantLoopCo
630
656
  const rows: CandidateRow[] = []
631
657
 
632
658
  for (const proposer of config.proposers) {
633
- const profile = loadAuthorProfile(proposer)
659
+ const sourceProfile = resolveAuthorProfile(proposer)
660
+ if (!sourceProfile) throw new Error(`quant proposer ${proposer.name}: exact profile is required`)
661
+ const profile = withModel(sourceProfile, config.authorModel, `quant-${proposer.name}`)
634
662
  for (let shot = 0; shot < config.candidatesPerProposer; shot++) {
635
663
  nTried += 1
636
664
  const candidateId = `cand-${String(nTried).padStart(3, '0')}-${proposer.name}`
@@ -642,7 +670,7 @@ async function runQuantCampaignWithWorker(worker: VbtWorker, config: QuantLoopCo
642
670
  at: new Date().toISOString(),
643
671
  candidateId,
644
672
  proposer: proposer.name,
645
- authorModel: config.authorModel,
673
+ authorModel: profile.model?.default ?? config.authorModel,
646
674
  strategyPath: null,
647
675
  sha256: null,
648
676
  authoringCostUsd: null,
@@ -663,13 +691,12 @@ async function runQuantCampaignWithWorker(worker: VbtWorker, config: QuantLoopCo
663
691
  nTried,
664
692
  ...(proposer.lens ? { lens: proposer.lens } : {}),
665
693
  })
666
- const { outcome, costUsd } = await meteredClaudeShot(
694
+ const { outcome, costUsd } = await meteredProfileShot(
667
695
  ledger,
668
696
  { phase: 'search.proposal', actor: `proposer-shot:${proposer.name}`, tags: { candidateId } },
669
697
  {
670
698
  prompt,
671
- model: config.authorModel,
672
- ...(profile?.prompt?.systemPrompt ? { systemPrompt: profile.prompt.systemPrompt } : {}),
699
+ profile,
673
700
  timeoutMs: config.authorTimeoutMs,
674
701
  cwd: config.outDir,
675
702
  },
@@ -798,7 +825,7 @@ if (isMain) {
798
825
  if (!outDir) {
799
826
  console.error(
800
827
  'usage: tsx src/quant-arena/quant-loop.mts --out <dir> [--candidates 2] [--seed 20260722] ' +
801
- '[--author-model sonnet] [--audit-model haiku] [--skip-llm-audit] # SPENDS: author + audit shots',
828
+ '[--author-model glm-5.2] [--audit-model glm-5.2] [--skip-llm-audit] # SPENDS: author + audit shots',
802
829
  )
803
830
  process.exit(2)
804
831
  }
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * One research rollout as a reusable primitive: 2-step RAG — (1) provider-pinned web
3
3
  * search via the router's proven `/v1/search?provider=<id>` + `web_fetch` of the top-K
4
- * result pages, (2) answer with that evidence via `routerChatWithUsage` (no tools on the
4
+ * result pages, (2) answer with that evidence through Runtime's profile-bound turn (no tools on the
5
5
  * answer call → `content` always present, so a search arm differs from the parametric
6
6
  * control ONLY by the evidence). Pure router HTTP (bearer `TANGLE_API_KEY`).
7
7
  *
8
8
  * Shared by the off-sandbox RAG leaderboard (`research-gate.mts`) and the router-backed
9
9
  * loop executor (`router-executor.ts`), so both score the identical retrieve→answer body
10
10
  * — the only difference is who drives the rounds (a flat best-of-k pool vs the real
11
- * `runLoop` kernel with analyst steering).
11
+ * `runAgentRounds` kernel with analyst steering).
12
12
  */
13
- import { routerChatWithUsage } from '@tangle-network/agent-runtime/kernel'
13
+ import { runBenchRouterTurn } from './router-turn'
14
14
 
15
15
  export interface ShotCfg {
16
16
  model: string
@@ -109,15 +109,25 @@ export async function runResearchShot(prompt: string, taskId: string, attempt: n
109
109
  : 'Answer from your own knowledge. ') +
110
110
  'If you are not fully certain, still COMMIT to your single best estimate — never refuse, defer, or reply with a question.'
111
111
  const userContent = useSearch && context ? `${prompt}\n\n=== WEB SEARCH RESULTS (provider: ${cfg.search}) ===\n${context}` : prompt
112
- const { content } = await routerChatWithUsage(
113
- { routerBaseUrl: cfg.routerBaseUrl, routerKey: cfg.routerKey, model: cfg.model },
114
- [
115
- { role: 'system', content: commit },
116
- { role: 'user', content: userContent },
117
- ],
118
- { temperature: cfg.temperature, ...(cfg.timeoutMs ? { signal: AbortSignal.timeout(cfg.timeoutMs) } : {}) },
112
+ const turn = await runBenchRouterTurn(
113
+ {
114
+ routerBaseUrl: cfg.routerBaseUrl,
115
+ routerKey: cfg.routerKey,
116
+ profile: {
117
+ name: 'research-shot-answerer',
118
+ harness: 'cli-base',
119
+ model: {
120
+ provider: 'tangle-router',
121
+ default: cfg.model,
122
+ metadata: { temperature: cfg.temperature },
123
+ },
124
+ prompt: { systemPrompt: commit },
125
+ },
126
+ ...(cfg.timeoutMs ? { timeoutMs: cfg.timeoutMs } : {}),
127
+ },
128
+ userContent,
119
129
  )
120
- const answer = content.trim()
130
+ const answer = turn.finalText.trim()
121
131
  const ok = answer.length > 0
122
132
  return { taskId, attempt, answer, ok, searches, wallMs: Date.now() - startedAt, ...(ok ? {} : { detail: `empty answer (searches=${searches})` }) }
123
133
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The ONE harness → worker-client mapping for the bench layer. Every bench entrypoint
3
- * (rsi.ts, run.ts, …) derives the `SandboxClient` the kernel's `runLoop` drives from a single
3
+ * (rsi.ts, run.ts, …) derives the `SandboxClient` the kernel's `runAgentRounds` drives from a single
4
4
  * selector instead of hand-rolling the branch:
5
5
  *
6
6
  * - `router` → OFF-BOX: a router chat-completion as the leaf executor, presented
@@ -11,10 +11,10 @@
11
11
  * - `bridge` → OFF-BOX: a local cli-bridge fronting a harness CLI
12
12
  * (opencode/kimi-code/…) as the leaf executor. Same resumable
13
13
  * `bridgeExecutor` the supervisor uses; harness+model ride the
14
- * bridge `model` id (`${harness}/${model}`).
14
+ * exact harness/provider/model come from the turn's AgentProfile.
15
15
  * - anything else (`sandbox`/a BackendType) → IN-BOX: a real `Sandbox`. The in-box backend
16
16
  * TYPE (opencode/codex/…) is set separately on the `AgentRunSpec`;
17
- * this only decides off-box-vs-in-box transport for `runLoop`.
17
+ * this only decides off-box-vs-in-box transport for `runAgentRounds`.
18
18
  *
19
19
  * Centralizing it here means a new entrypoint gets the full off-box/in-box matrix for free and
20
20
  * the mapping can't drift between callers.
@@ -33,7 +33,6 @@ export interface ResolveBenchClientOptions {
33
33
  backend: string
34
34
  routerBaseUrl: string
35
35
  routerKey: string
36
- model: string
37
36
  /** When set on the `router` backend, the off-box worker becomes a `router-tools` agentic loop
38
37
  * with a live `web_search` tool backed by this provider (`you`/`exa`/…). */
39
38
  searchProvider?: string
@@ -47,7 +46,7 @@ export interface ResolveBenchClientOptions {
47
46
  }
48
47
 
49
48
  export function resolveBenchClient(opts: ResolveBenchClientOptions): SandboxClient {
50
- const { backend, routerBaseUrl, routerKey, model, searchProvider } = opts
49
+ const { backend, routerBaseUrl, routerKey, searchProvider } = opts
51
50
  if (backend === 'router') {
52
51
  if (searchProvider) {
53
52
  return inlineSandboxClient(
@@ -55,13 +54,12 @@ export function resolveBenchClient(opts: ResolveBenchClientOptions): SandboxClie
55
54
  backend: 'router-tools',
56
55
  routerBaseUrl,
57
56
  routerKey,
58
- model,
59
57
  tools: [webSearchTool],
60
58
  executeToolCall: makeSearchExecutor({ routerBaseUrl, routerKey, provider: searchProvider }),
61
59
  }),
62
60
  )
63
61
  }
64
- return inlineSandboxClient(createExecutor({ backend: 'router', routerBaseUrl, routerKey, model }))
62
+ return inlineSandboxClient(createExecutor({ backend: 'router', routerBaseUrl, routerKey }))
65
63
  }
66
64
  if (backend === 'bridge') {
67
65
  // bench's bearer fallback (`?? routerKey`) resolves first, then the shared
@@ -70,7 +68,7 @@ export function resolveBenchClient(opts: ResolveBenchClientOptions): SandboxClie
70
68
  if (!bridgeBearer) throw new Error("resolveBenchClient: backend 'bridge' needs bridgeBearer or routerKey")
71
69
  return resolveSandboxClient({
72
70
  backend: 'bridge',
73
- bridge: { url: opts.bridgeUrl, bearer: bridgeBearer, model, timeoutMs: opts.timeoutMs },
71
+ bridge: { url: opts.bridgeUrl, bearer: bridgeBearer, timeoutMs: opts.timeoutMs },
74
72
  })
75
73
  }
76
74
  return new Sandbox({
@@ -2,7 +2,12 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
- import { assertRolloutLine, claudeProjectSlug, toSftRows } from '@tangle-network/agent-eval/rollout'
5
+ import {
6
+ assertMintedLines,
7
+ assertRolloutLine,
8
+ claudeProjectSlug,
9
+ toSftRows,
10
+ } from '@tangle-network/agent-eval/rollout'
6
11
  import { backfillSweArena } from './backfill-swe-arena.mts'
7
12
 
8
13
  let dir: string
@@ -262,7 +267,7 @@ describe('backfillSweArena', () => {
262
267
 
263
268
  // The dataset is immediately trainable: reward==1 trainable-split lines
264
269
  // with transcripts → SFT rows (candidate worker + proposer here).
265
- expect(toSftRows(lines)).toHaveLength(2)
270
+ expect(toSftRows(assertMintedLines(lines))).toHaveLength(2)
266
271
  })
267
272
 
268
273
  it('joins a respawned worker cwd once, not once per recovered entry', async () => {
@@ -7,7 +7,7 @@
7
7
  * Why off-sandbox: research is retrieval, not in-box code execution — it never
8
8
  * needed a box, and a real sandbox box reaches only the router (egress allowlist,
9
9
  * ops-board #976), so it cannot web-search natively anyway. Driving the loop with
10
- * this executor instead of a box gives the REAL `runLoop` kernel full `rounds` +
10
+ * this executor instead of a box gives the REAL `runAgentRounds` kernel full `rounds` +
11
11
  * analyst steering (the depth regime), search working, no sandbox dependency.
12
12
  *
13
13
  * This is a BYO `Executor` over `runResearchShot`; `inlineSandboxClient` supplies
@@ -0,0 +1,135 @@
1
+ import {
2
+ type AgentProfile,
3
+ agentProfileSchema,
4
+ type ReasoningEffort,
5
+ } from '@tangle-network/agent-interface'
6
+ import {
7
+ collectAgentTurn,
8
+ createExecutor,
9
+ streamAgentTurn,
10
+ type CollectedAgentTurn,
11
+ type ToolSpec,
12
+ } from '@tangle-network/agent-runtime/kernel'
13
+
14
+ /** Bench-local target shorthand; Runtime still executes only the exact profile below. */
15
+ export interface BenchRouterTarget {
16
+ routerBaseUrl: string
17
+ routerKey: string
18
+ profile: AgentProfile
19
+ }
20
+
21
+ export interface BenchRouterTurnConfig extends BenchRouterTarget {
22
+ tools?: ReadonlyArray<ToolSpec>
23
+ timeoutMs?: number
24
+ signal?: AbortSignal
25
+ }
26
+
27
+ export interface BenchProfileSettings {
28
+ systemPrompt?: string
29
+ temperature?: number
30
+ maxTokens?: number
31
+ retry?: {
32
+ maxAttempts?: number
33
+ initialBackoffMs?: number
34
+ maxBackoffMs?: number
35
+ jitter?: number
36
+ retryStatuses?: ReadonlyArray<number>
37
+ requestTimeoutMs?: number
38
+ }
39
+ maxTurns?: number
40
+ seed?: number
41
+ reasoningEffort?: ReasoningEffort
42
+ extraBody?: Readonly<Record<string, unknown>>
43
+ toolChoice?: 'auto' | 'required' | 'none'
44
+ }
45
+
46
+ /** Author an exact direct-Router profile for a benchmark. This is profile construction only;
47
+ * execution still accepts no model or generation fields outside the returned AgentProfile. */
48
+ export function benchRouterProfile(
49
+ name: string,
50
+ model: string,
51
+ settings: BenchProfileSettings = {},
52
+ ): AgentProfile {
53
+ return withBenchProfile(
54
+ {
55
+ name,
56
+ harness: 'cli-base',
57
+ model: { provider: 'tangle-router', default: model },
58
+ },
59
+ settings,
60
+ )
61
+ }
62
+
63
+ /** Derive another exact profile while preserving all untouched canonical axes. */
64
+ export function withBenchProfile(
65
+ base: AgentProfile,
66
+ settings: BenchProfileSettings & { name?: string },
67
+ ): AgentProfile {
68
+ const metadata = {
69
+ ...(base.model?.metadata ?? {}),
70
+ ...(settings.temperature !== undefined ? { temperature: settings.temperature } : {}),
71
+ ...(settings.maxTokens !== undefined ? { maxTokens: settings.maxTokens } : {}),
72
+ ...(settings.retry !== undefined ? { retry: settings.retry } : {}),
73
+ ...(settings.maxTurns !== undefined ? { maxTurns: settings.maxTurns } : {}),
74
+ ...(settings.seed !== undefined ? { seed: settings.seed } : {}),
75
+ ...(settings.extraBody !== undefined ? { extraBody: settings.extraBody } : {}),
76
+ ...(settings.toolChoice !== undefined ? { toolChoice: settings.toolChoice } : {}),
77
+ }
78
+ return agentProfileSchema.parse({
79
+ ...base,
80
+ ...(settings.name ? { name: settings.name } : {}),
81
+ model: {
82
+ ...base.model,
83
+ ...(settings.reasoningEffort !== undefined
84
+ ? { reasoningEffort: settings.reasoningEffort }
85
+ : {}),
86
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
87
+ },
88
+ ...(settings.systemPrompt !== undefined
89
+ ? { prompt: { ...base.prompt, systemPrompt: settings.systemPrompt } }
90
+ : {}),
91
+ })
92
+ }
93
+
94
+ export function benchProfileModel(profile: AgentProfile): string {
95
+ const model = profile.model?.default
96
+ if (typeof model !== 'string' || model.length === 0 || model === 'runtime-selected') {
97
+ throw new Error('benchmark AgentProfile.model.default must be concrete')
98
+ }
99
+ return model
100
+ }
101
+
102
+ /**
103
+ * The benchmark-side entry to Runtime's canonical one-turn path.
104
+ * It is only an ergonomic composition: Runtime still parses the exact profile,
105
+ * materializes the executor, records identity/usage/result events, and refuses
106
+ * profile axes the direct Router backend cannot carry.
107
+ */
108
+ export async function runBenchRouterTurn(
109
+ config: BenchRouterTurnConfig,
110
+ input: string | { readonly messages: ReadonlyArray<Readonly<Record<string, unknown>>> },
111
+ ): Promise<CollectedAgentTurn> {
112
+ if (!config.profile.model?.default) {
113
+ throw new Error('runBenchRouterTurn: profile.model.default is required')
114
+ }
115
+ const factory = createExecutor({
116
+ backend: 'router',
117
+ routerBaseUrl: config.routerBaseUrl,
118
+ routerKey: config.routerKey,
119
+ ...(config.tools ? { tools: config.tools } : {}),
120
+ })
121
+ const turn = await collectAgentTurn(
122
+ streamAgentTurn(
123
+ { kind: 'executor', factory, profile: config.profile },
124
+ input,
125
+ {
126
+ ...(config.timeoutMs === undefined ? {} : { timeoutMs: config.timeoutMs }),
127
+ ...(config.signal ? { signal: config.signal } : {}),
128
+ },
129
+ ),
130
+ )
131
+ if (turn.status !== 'completed') {
132
+ throw new Error(turn.error?.message ?? `Router turn ended with status ${turn.status}`)
133
+ }
134
+ return turn
135
+ }
@@ -178,7 +178,6 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
178
178
  backend: cell.backend ?? 'router',
179
179
  routerBaseUrl,
180
180
  routerKey,
181
- model: cell.model,
182
181
  ...(bridgeUrl ? { bridgeUrl } : {}),
183
182
  ...(bridgeBearer ? { bridgeBearer } : {}),
184
183
  ...(sandboxBaseUrl ? { sandboxBaseUrl } : {}),
@@ -186,7 +185,13 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
186
185
  ...(timeoutMs ? { timeoutMs } : {}),
187
186
  })
188
187
  const harness = cell.harness ?? (cell.profile?.metadata?.backendType as string | undefined) ?? 'opencode'
189
- const profile: AgentProfile = cell.profile ?? { name: cell.label, metadata: { backendType: harness } }
188
+ const profile: AgentProfile =
189
+ cell.profile ?? {
190
+ name: cell.label,
191
+ harness: harness as AgentProfile['harness'],
192
+ model: { provider: 'tangle-router', default: cell.model },
193
+ metadata: { backendType: harness },
194
+ }
190
195
  // Unique per shot: the same (adapter, task) runs concurrently across cells and reps, so the box
191
196
  // name and runId must not collide.
192
197
  const uniq = Math.random().toString(36).slice(2, 8)