@tangle-network/agent-bench 0.1.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 (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Strategy demo — the optimization suite in three layers, on a toy Environment (no gym).
3
+ *
4
+ * The whole idea in one file: you implement an `Environment` (5 hooks: open/tools/call/
5
+ * score/close), and you get optimization STRATEGIES — sample (best-of-N), refine
6
+ * (iterate-with-feedback), and any you author — compared and scored by your own check,
7
+ * for free. This uses a trivial "counter" environment so it runs with just a router key
8
+ * (no benchmark dataset, no sandbox).
9
+ *
10
+ * dotenvx run -f …/.env.keys -- env WORKER_MODEL=gpt-4o-mini tsx src/examples/strategy-demo.mts
11
+ *
12
+ * The three layers shown below:
13
+ * 1. just run it — runBenchmark(env, …) compares the default strategies, free.
14
+ * 2. pick strategies — pass [sample, refine, adaptiveRefine].
15
+ * 3. author your own — defineStrategy(name, body) in ~10 lines, no Supervisor ceremony.
16
+ */
17
+ import { adaptiveRefine, type AgenticTask, type ArtifactHandle, defineStrategy, type Environment, printBenchmarkReport, refine, runBenchmark, sample } from '@tangle-network/agent-runtime/loops'
18
+
19
+ // ── 1. Implement an Environment (the only thing a new domain writes) ──────────────
20
+ // A toy: the agent must drive a counter to exactly the target using the increment tool.
21
+ // score = how close it got. This is the seam every real benchmark (EOPS, a coding repo,
22
+ // a browser task) implements the same way — open a checkable artifact, expose tools,
23
+ // score it. Here the "artifact" is just an in-memory counter.
24
+
25
+ const target = 5
26
+ const counters = new Map<string, { count: number }>()
27
+
28
+ const counterEnv: Environment = {
29
+ name: 'counter',
30
+ async open(_task) {
31
+ const id = `counter-${Math.random().toString(36).slice(2, 8)}`
32
+ counters.set(id, { count: 0 })
33
+ return { id, surface: 'counter' } satisfies ArtifactHandle
34
+ },
35
+ async tools() {
36
+ return [
37
+ { type: 'function', function: { name: 'increment', description: 'Add 1 to the counter.', parameters: { type: 'object', properties: {} } } },
38
+ { type: 'function', function: { name: 'read_count', description: 'Read the current counter value.', parameters: { type: 'object', properties: {} } } },
39
+ ]
40
+ },
41
+ async call(handle, name) {
42
+ const c = counters.get(handle.id)
43
+ if (!c) return 'ERROR: no such counter'
44
+ if (name === 'increment') {
45
+ c.count += 1
46
+ return `count is now ${c.count}`
47
+ }
48
+ if (name === 'read_count') return `count is ${c.count}`
49
+ return `ERROR: unknown tool ${name}`
50
+ },
51
+ // The deployable CHECK: exact hits toward the target. score = passes/total.
52
+ async score(_task, handle) {
53
+ const c = counters.get(handle.id)
54
+ const count = c?.count ?? 0
55
+ return { passes: Math.min(count, target), total: target, errored: 0 }
56
+ },
57
+ async close(handle) {
58
+ counters.delete(handle.id)
59
+ },
60
+ }
61
+
62
+ const task: AgenticTask = {
63
+ id: 'counter-to-5',
64
+ systemPrompt: 'You operate a counter with tools.',
65
+ userPrompt: `Use the increment tool to bring the counter to exactly ${target}. Use read_count to verify before you finish. Reply DONE when the count equals ${target}.`,
66
+ }
67
+
68
+ // ── 3. Author your OWN strategy in ~10 lines — the lego (no Supervisor ceremony) ──
69
+ // "doubleCheck": one attempt, then critique twice (extra steering passes) before stopping.
70
+ // A strategy body composes two steps: shot() (one worker attempt) + critique() (the
71
+ // firewalled analyst → a steer). That's it. This is the skillifiable unit.
72
+ const doubleCheck = defineStrategy('doubleCheck', async ({ surface, task: t, budget, shot, critique }) => {
73
+ const handle = await surface.open(t)
74
+ const progression: number[] = []
75
+ let messages: Record<string, unknown>[] | undefined
76
+ let steer: string | undefined
77
+ let completions = 0
78
+ try {
79
+ for (let i = 0; i < budget; i += 1) {
80
+ const out = await shot({ handle, messages, steer })
81
+ if (!out) break
82
+ completions += out.completions
83
+ progression.push(out.score)
84
+ if (out.score >= 1) break
85
+ messages = out.messages
86
+ const findings = await critique(out.messages)
87
+ completions += 1
88
+ if (!findings) break
89
+ steer = `Not done yet. ${findings}`
90
+ }
91
+ const score = progression.length ? Math.max(...progression) : 0
92
+ return { score, resolved: score >= 1, completions, progression, shots: progression.length }
93
+ } finally {
94
+ await surface.close(handle)
95
+ }
96
+ })
97
+
98
+ async function main(): Promise<void> {
99
+ const worker = {
100
+ routerBaseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
101
+ routerKey: process.env.TANGLE_API_KEY ?? '',
102
+ model: process.env.WORKER_MODEL ?? 'deepseek-v4-flash',
103
+ innerTurns: 6,
104
+ }
105
+ if (!worker.routerKey) throw new Error('set TANGLE_API_KEY (the worker calls the router)')
106
+
107
+ console.log('Layer 1 — just run it (default strategies, scored by the env\'s own check):')
108
+ printBenchmarkReport(await runBenchmark({ environment: counterEnv, tasks: [task], worker, budget: 3 }))
109
+
110
+ console.log('\nLayer 2+3 — pick the built-ins AND your own authored strategy:')
111
+ printBenchmarkReport(
112
+ await runBenchmark({ environment: counterEnv, tasks: [task], worker, budget: 3, strategies: [sample, refine, adaptiveRefine, doubleCheck] }),
113
+ )
114
+ }
115
+
116
+ main().catch((e) => {
117
+ console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
118
+ process.exit(1)
119
+ })
package/src/fleet.mts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The whole vision, end to end, runnable from a laptop: a thin local driver
3
+ * fans out N workers to CLOUD sandboxes, observes each worker's trace, reports
4
+ * what to fix, and writes durable learnings to a corpus the NEXT run reads back.
5
+ *
6
+ * Local process = the driver (orchestrate + observe). All agent work runs in the
7
+ * cloud (Tangle sandbox SDK). Scale is the API key, not the laptop.
8
+ *
9
+ * dotenvx run -f ~/company/devops/secrets/.env.keys -f ~/company/devops/secrets/agent-state.env -- \
10
+ * env BACKEND=opencode MODEL=gpt-4.1 N=2 CORPUS=/tmp/fleet-corpus.jsonl \
11
+ * pnpm exec tsx src/fleet.mts
12
+ *
13
+ * Run it twice: the second run injects the first run's learnings into the workers.
14
+ */
15
+ import { createChatClient } from '@tangle-network/agent-eval'
16
+ import { FileCorpus, observe, openSandboxRun, renderReport } from '@tangle-network/agent-runtime/loops'
17
+ import { Sandbox } from '@tangle-network/sandbox'
18
+ import { answerOutput, sandboxAgentRun, type WorkerBackendType } from './sandbox-run'
19
+
20
+ function env(name: string, fallback?: string): string {
21
+ const v = process.env[name] ?? fallback
22
+ if (v === undefined) throw new Error(`missing env ${name}`)
23
+ return v
24
+ }
25
+
26
+ // The fleet's work: each worker gets one subtask. Swap for any real decomposition.
27
+ const subtasks = [
28
+ 'Write a Python function `is_prime(n)` and three asserts proving it. Run them and report PASS/FAIL.',
29
+ 'Write a Python function `fib(n)` (iterative) and three asserts proving it. Run them and report PASS/FAIL.',
30
+ 'Write a Python function `rev_words(s)` that reverses word order, with asserts. Run them and report PASS/FAIL.',
31
+ ]
32
+
33
+ interface WorkerResult {
34
+ id: string
35
+ task: string
36
+ output: string
37
+ events: unknown[]
38
+ wallMs: number
39
+ error?: string
40
+ }
41
+
42
+ async function runWorker(
43
+ client: Sandbox,
44
+ cfg: { backendType: WorkerBackendType; model: string; routerBaseUrl: string },
45
+ id: string,
46
+ task: string,
47
+ priorLearnings: string,
48
+ ): Promise<WorkerResult> {
49
+ const startedAt = Date.now()
50
+ const prompt = priorLearnings ? `${priorLearnings}\n\n---\n\n${task}` : task
51
+ const controller = new AbortController()
52
+ const timer = setTimeout(() => controller.abort(), Number(process.env.TIMEOUT_MS ?? 240_000))
53
+ try {
54
+ const agentRun = sandboxAgentRun({ ...cfg, name: id })
55
+ const run = await openSandboxRun<string>(
56
+ client,
57
+ { agentRun, signal: controller.signal },
58
+ { kind: 'events', fromEvents: (events) => answerOutput.parse(events as never) },
59
+ )
60
+ try {
61
+ const turn = await run.start(prompt)
62
+ return { id, task, output: (turn.out ?? '').trim(), events: turn.events, wallMs: Date.now() - startedAt }
63
+ } finally {
64
+ await run.close().catch(() => {})
65
+ }
66
+ } catch (err) {
67
+ return { id, task, output: '', events: [], wallMs: Date.now() - startedAt, error: err instanceof Error ? err.message : String(err) }
68
+ } finally {
69
+ clearTimeout(timer)
70
+ }
71
+ }
72
+
73
+ async function main(): Promise<void> {
74
+ const routerKey = env('TANGLE_API_KEY')
75
+ const cfg = {
76
+ backendType: env('BACKEND', 'opencode') as WorkerBackendType,
77
+ model: env('MODEL', 'gpt-4.1'),
78
+ routerBaseUrl: env('ROUTER_BASE_URL', 'https://router.tangle.tools/v1'),
79
+ }
80
+ const n = Math.min(Number(env('N', '2')), subtasks.length)
81
+ const corpus = new FileCorpus(env('CORPUS', '/tmp/fleet-corpus.jsonl'))
82
+ const observerModel = env('OBSERVER_MODEL', 'gpt-4.1')
83
+ const chat = createChatClient({ transport: 'router', apiKey: routerKey, baseUrl: cfg.routerBaseUrl, defaultModel: observerModel })
84
+ const client = new Sandbox({ baseUrl: env('SANDBOX_BASE_URL', 'https://sandbox.tangle.tools'), apiKey: routerKey })
85
+
86
+ // ── continuous: read what prior runs LEARNED, inject it into this run's workers
87
+ const prior = await corpus.query({ tags: ['audience:agent'], limit: 8 })
88
+ const priorLearnings = prior.length
89
+ ? `PRIOR LEARNINGS (from earlier runs — apply them):\n${prior.map((r) => `- ${r.claim}`).join('\n')}`
90
+ : ''
91
+ console.error(`\n=== FLEET · ${n} workers · ${cfg.backendType}/${cfg.model} · cloud ===`)
92
+ console.error(prior.length ? `carrying ${prior.length} prior learning(s) into the workers\n` : 'first run — no prior learnings yet\n')
93
+
94
+ // ── fan out N workers to cloud sandboxes, in parallel
95
+ const tasks = subtasks.slice(0, n)
96
+ const workers = await Promise.all(
97
+ tasks.map((task, i) => runWorker(client, cfg, `worker-${i + 1}`, task, priorLearnings)),
98
+ )
99
+
100
+ // ── observe each worker's trace → findings → operator report + durable learnings
101
+ let totalLearned = 0
102
+ for (const w of workers) {
103
+ console.error(`\n── ${w.id} (${Math.round(w.wallMs / 1000)}s)${w.error ? ` — ERROR: ${w.error}` : ''}`)
104
+ if (w.error) continue
105
+ const ob = await observe(
106
+ { task: w.task, output: w.output, trace: w.events, outcome: w.output ? 'passed' : 'unknown', runId: w.id },
107
+ { chat, model: observerModel, corpus, tags: [cfg.backendType, 'fleet'] },
108
+ )
109
+ totalLearned += ob.learned.length
110
+ console.error(` answer: ${w.output.slice(0, 120).replace(/\n/g, ' ')}`)
111
+ console.error(renderReport(ob.findings).split('\n').map((l) => ` ${l}`).join('\n'))
112
+ console.error(` → ${ob.learned.length} new learning(s) saved to the corpus`)
113
+ }
114
+
115
+ console.error(`\n=== fleet done: ${workers.filter((w) => !w.error).length}/${n} workers ok · ${totalLearned} learnings banked → run again to apply them ===`)
116
+ }
117
+
118
+ main().catch((e) => {
119
+ console.error(e)
120
+ process.exit(1)
121
+ })
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Run the diverse-vs-blind gate THROUGH the recursive atom, end to end, in two lines:
3
+ *
4
+ * export TANGLE_API_KEY=... # router + the deployable judge's creds
5
+ * BENCH=enterpriseops-gym EOPS_FIXTURES=1 N=20 K=4 tsx src/gate-cli.mts
6
+ *
7
+ * The arms are equal-k by construction (both open K children; blind = K identical copies,
8
+ * diverse = K distinct strategy directives). The deployable selector is the benchmark's OWN
9
+ * judge — pick a benchmark whose `adapter.judge` is a runnable checker (enterpriseops-gym,
10
+ * swe-bench, terminal-bench), NOT an LLM-judge bench (finsearchcomp), or the selector is not
11
+ * deployable and the result does not answer the gate.
12
+ *
13
+ * Output is a verdict-first report: the headline delta, the per-arm resolve table, the equal-k
14
+ * proof, and the per-task paired booleans (feed them to a paired-bootstrap + BH downstream — a
15
+ * single CLI run is the instrument, not the significance test).
16
+ */
17
+
18
+ import type { AgentProfile } from '@tangle-network/agent-runtime/loops'
19
+ import { resolveAdapter } from './adapters'
20
+ import { runGate } from './gate'
21
+
22
+ const must = (k: string): string => {
23
+ const v = process.env[k]
24
+ if (!v) throw new Error(`env ${k} is required`)
25
+ return v
26
+ }
27
+
28
+ /** Four content-free, genuinely distinct solve strategies — the diverse arm. K = this length;
29
+ * the blind arm runs K identical copies, so the two arms are equal-k. Trim/extend to change K. */
30
+ const defaultStrategies: ReadonlyArray<string> = [
31
+ 'Restate the task in your own words, then solve it directly and concisely.',
32
+ 'Decompose the task into ordered sub-steps; solve each, then assemble the final deliverable.',
33
+ 'Identify the most likely failure mode for this task FIRST, then produce a solution that explicitly avoids it.',
34
+ 'Produce a first solution, critique it against every stated requirement, then output the corrected final version only.',
35
+ ]
36
+
37
+ async function main(): Promise<void> {
38
+ const benchKey = process.env.BENCH ?? 'enterpriseops-gym'
39
+ const adapter = resolveAdapter(benchKey)
40
+ const model = process.env.WORKER_MODEL ?? 'deepseek-v4-flash'
41
+ const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
42
+ const routerKey = must('TANGLE_API_KEY')
43
+ const k = Number(process.env.K ?? defaultStrategies.length)
44
+ const strategies = defaultStrategies.slice(0, k)
45
+ if (strategies.length < 2) throw new Error('K must be >= 2')
46
+
47
+ const profile = {
48
+ name: 'gate-solver',
49
+ model: { default: model },
50
+ prompt: { systemPrompt: 'You are an expert agent. Produce the single best deliverable the task’s grader will accept.' },
51
+ } as unknown as AgentProfile
52
+
53
+ const report = await runGate({
54
+ adapter,
55
+ profile,
56
+ strategies,
57
+ routerBaseUrl,
58
+ routerKey,
59
+ model,
60
+ ...(process.env.TEMPERATURE ? { temperature: Number(process.env.TEMPERATURE) } : {}),
61
+ n: Number(process.env.N ?? 20),
62
+ ...(process.env.IDS ? { ids: process.env.IDS.split(',') } : {}),
63
+ ...(process.env.SPLIT ? { split: process.env.SPLIT } : {}),
64
+ ...(process.env.PER_CHILD_TOKENS ? { perChildTokens: Number(process.env.PER_CHILD_TOKENS) } : {}),
65
+ })
66
+
67
+ const pct = (x: number) => `${(x * 100).toFixed(1)}%`
68
+ const blind = report.arms.find((a) => a.label === 'blind')!
69
+ const diverse = report.arms.find((a) => a.label === 'diverse')!
70
+
71
+ console.log(`\n=== gate: ${report.benchmark} (k=${report.k}, n=${report.n}) ===`)
72
+ console.log(
73
+ `VERDICT: diverse ${report.deltaScorePp >= 0 ? '+' : ''}${report.deltaScorePp.toFixed(1)}pp graded-score ` +
74
+ `(binary ${report.deltaPp >= 0 ? '+' : ''}${report.deltaPp.toFixed(1)}pp) vs blind` +
75
+ ` — equal-k=${report.equalK.withinTolerance ? 'OK' : 'VIOLATED'} (token spread ${report.equalK.spread.tokens})`,
76
+ )
77
+ console.log('arm resolved errored resolveRate meanScore tokens')
78
+ for (const a of [blind, diverse]) {
79
+ const tok = a.totalSpend.tokens.input + a.totalSpend.tokens.output
80
+ console.log(
81
+ `${a.label.padEnd(8)} ${String(a.resolved).padStart(8)} ${String(a.errored).padStart(8)} ` +
82
+ `${pct(a.resolveRate).padStart(11)} ${pct(a.meanScore).padStart(9)} ${tok}`,
83
+ )
84
+ }
85
+ for (const a of [blind, diverse]) {
86
+ if (a.errored > 0) console.log(` ${a.label} first failure: ${a.sampleBlocker}`)
87
+ }
88
+ const erroredFrac = (blind.errored + diverse.errored) / (2 * report.n)
89
+ if (erroredFrac > 0.2) {
90
+ console.log(`\nWARNING: ${(erroredFrac * 100).toFixed(0)}% of runs ERRORED — this 0%/delta is NOT a clean gate result; fix the failure above first.`)
91
+ }
92
+ if (!report.equalK.withinTolerance) {
93
+ console.log('\nWARNING: arms are NOT at equal compute — the delta is confounded, not a gate result.')
94
+ }
95
+ console.log(`\npaired (id: blind|diverse): ${report.perTask.map((t) => `${t.id}:${t.blind ? 1 : 0}|${t.diverse ? 1 : 0}`).join(' ')}`)
96
+ }
97
+
98
+ main().catch((e) => {
99
+ console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
100
+ process.exit(1)
101
+ })
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Offline gate-plumbing test. A deterministic stub `ExecutorRegistry` is injected so the gate
3
+ * path runs with NO network: it proves the bridge wiring end-to-end — the persona + `fanout`
4
+ * drives the `Supervisor`, the per-child deployable verdict drives selection, the paired metric is
5
+ * derived from the run's own trajectory, and the conserved pool yields equal-k across arms.
6
+ *
7
+ * The LIVE solve-and-grade path (`benchSolverRegistry` → router + `adapter.judge`) is exercised
8
+ * by a real gate run against a deployable-checker domain, not here.
9
+ *
10
+ * tsx bench/src/gate.test.mts
11
+ */
12
+
13
+ import assert from 'node:assert/strict'
14
+ import type {
15
+ AgentSpec,
16
+ DefaultVerdict,
17
+ ExecutorContext,
18
+ ExecutorRegistry,
19
+ Executor,
20
+ ExecutorFactory,
21
+ ExecutorResult,
22
+ } from '@tangle-network/agent-runtime/loops'
23
+ import type { BenchmarkAdapter, BenchScore, BenchTask } from './benchmarks/types'
24
+ import { runGate, type SolveTask } from './gate'
25
+
26
+ /** A child whose verdict is decided purely by whether its prompt carries the STRONG marker — so a
27
+ * diverse arm that injects a STRONG strategy beats a blind arm that never does. Fixed spend per
28
+ * child (independent of prompt length) so both arms spend identically → equal-k is exact. */
29
+ function stubLeaf(_spec: AgentSpec, ctx: ExecutorContext): Executor<unknown> {
30
+ let artifact: ExecutorResult<unknown> | undefined
31
+ return {
32
+ runtime: 'stub',
33
+ execute(task): Promise<ExecutorResult<unknown>> {
34
+ void ctx.signal
35
+ const t = task as SolveTask
36
+ const strong = t.prompt.includes('STRONG')
37
+ const verdict: DefaultVerdict = { valid: strong, score: strong ? 0.9 : 0.2 }
38
+ artifact = {
39
+ outRef: `stub:${t.instance.id}:${strong ? 1 : 0}`,
40
+ out: `cand:${t.instance.id}:${strong ? 'strong' : 'weak'}`,
41
+ verdict,
42
+ spent: { iterations: 1, tokens: { input: 100, output: 20 }, usd: 0, ms: 1 },
43
+ }
44
+ return Promise.resolve(artifact)
45
+ },
46
+ teardown: () => Promise.resolve({ destroyed: true }),
47
+ resultArtifact() {
48
+ if (!artifact) throw new Error('stubLeaf: resultArtifact before execute')
49
+ return artifact
50
+ },
51
+ }
52
+ }
53
+
54
+ const stubRegistry: ExecutorRegistry = {
55
+ register() {
56
+ throw new Error('stub: register unsupported')
57
+ },
58
+ resolve<Out>(_spec: AgentSpec) {
59
+ const factory: ExecutorFactory<Out> = (s, ctx) => stubLeaf(s, ctx) as Executor<Out>
60
+ return { succeeded: true as const, value: factory }
61
+ },
62
+ }
63
+
64
+ function stubAdapter(n: number): BenchmarkAdapter {
65
+ const tasks: BenchTask[] = Array.from({ length: n }, (_v, i) => ({
66
+ id: `t${i}`,
67
+ prompt: `solve instance ${i}`,
68
+ }))
69
+ return {
70
+ name: 'stub-bench',
71
+ preflight: () => Promise.resolve(),
72
+ loadTasks: (opts) =>
73
+ Promise.resolve(opts?.limit !== undefined ? tasks.slice(0, opts.limit) : tasks),
74
+ // judge is NOT exercised on the injected-registry path (the stub leaf sets verdicts directly);
75
+ // a real run reaches the adapter judge through benchSolverRegistry.
76
+ judge: (): Promise<BenchScore> => {
77
+ throw new Error('stub judge must not be called on the injected-registry path')
78
+ },
79
+ goldArtifact: () => Promise.resolve(undefined),
80
+ }
81
+ }
82
+
83
+ const profile = { name: 'stub-solver', model: { default: 'stub-model' } } as never
84
+
85
+ const report = await runGate({
86
+ adapter: stubAdapter(5),
87
+ profile,
88
+ strategies: ['plain restate', 'use the STRONG verified approach', 'enumerate edge cases'],
89
+ routerBaseUrl: 'http://unused',
90
+ routerKey: 'unused',
91
+ model: 'stub-model',
92
+ solverRegistry: stubRegistry,
93
+ })
94
+
95
+ assert.equal(report.benchmark, 'stub-bench')
96
+ assert.equal(report.k, 3, 'k = strategies.length')
97
+ assert.equal(report.n, 5, 'n = loaded tasks')
98
+ assert.equal(report.perTask.length, 5)
99
+
100
+ const blind = report.arms.find((a) => a.label === 'blind')
101
+ const diverse = report.arms.find((a) => a.label === 'diverse')
102
+ assert.ok(blind && diverse, 'both arms present')
103
+
104
+ // Blind = k identical copies of a prompt with no STRONG marker → every child weak → never resolves.
105
+ assert.equal(blind.resolved, 0, 'blind resolves nothing (no STRONG strategy)')
106
+ assert.equal(blind.errored, 0, 'no infra-errored blind runs')
107
+ assert.equal(blind.resolveRate, 0)
108
+
109
+ // Diverse injects a STRONG strategy → the deployable selector picks that child → resolves each task.
110
+ assert.equal(diverse.resolved, 5, 'diverse resolves every task via the STRONG strategy child')
111
+ assert.equal(diverse.resolveRate, 1)
112
+
113
+ assert.equal(report.deltaPp, 100, 'diverse − blind = +100pp on this constructed signal')
114
+ for (const row of report.perTask) {
115
+ assert.equal(row.blind, false, `${row.id}: blind not resolved`)
116
+ assert.equal(row.diverse, true, `${row.id}: diverse resolved`)
117
+ }
118
+
119
+ // Both arms opened k=3 children at a fixed per-child spend → identical conserved cost → equal-k.
120
+ assert.equal(report.equalK.withinTolerance, true, 'arms are at equal compute (the equal-k proof)')
121
+ assert.equal(blind.totalSpend.iterations, 15, 'blind: 5 tasks × 3 children')
122
+ assert.equal(diverse.totalSpend.iterations, 15, 'diverse: 5 tasks × 3 children')
123
+ assert.equal(
124
+ blind.totalSpend.tokens.input + blind.totalSpend.tokens.output,
125
+ diverse.totalSpend.tokens.input + diverse.totalSpend.tokens.output,
126
+ 'identical token spend across arms',
127
+ )
128
+
129
+ console.log('gate.test: OK — gate runs through the Supervisor, deployable selection + equal-k verified')