@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,58 @@
1
+ /**
2
+ * BenchmarkAdapter — the seam every external benchmark implements so the
3
+ * agent-runtime loop can be A/B'd (blind vs steering vs steering+memory) against
4
+ * a REAL, DETERMINISTIC judge. The worker produces an `artifact` (a patch, a
5
+ * command transcript, a final state); the benchmark's own harness scores it.
6
+ *
7
+ * The point: no self-authored judge, no invented score noise. The number comes
8
+ * from the benchmark's published evaluation harness.
9
+ */
10
+
11
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
12
+
13
+ export interface BenchTask {
14
+ /** Stable benchmark instance id. */
15
+ id: string
16
+ /** The task statement handed to the worker agent. */
17
+ prompt: string
18
+ split?: string
19
+ /** Benchmark-specific fields the worker/judge need (repo, base_commit, gold, …). */
20
+ metadata?: Record<string, unknown>
21
+ }
22
+
23
+ export interface BenchScore {
24
+ /** Did the deterministic judge pass (tests resolved / state correct)? */
25
+ resolved: boolean
26
+ /** 0..1 — 1 = fully resolved; partial credit where the harness supports it. */
27
+ score: number
28
+ detail?: string
29
+ }
30
+
31
+ export interface LoadOptions {
32
+ limit?: number
33
+ split?: string
34
+ ids?: string[]
35
+ }
36
+
37
+ export interface BenchmarkAdapter {
38
+ readonly name: string
39
+ /** Throw with actionable guidance when the harness/judge isn't installed/runnable. */
40
+ preflight(): Promise<void>
41
+ loadTasks(opts?: LoadOptions): Promise<BenchTask[]>
42
+ /** DETERMINISTIC judge: score the worker's produced artifact for a task. */
43
+ judge(task: BenchTask, artifact: string): Promise<BenchScore>
44
+ /** Gold/oracle artifact — lets us self-verify the judge before spending model tokens. */
45
+ goldArtifact(task: BenchTask): Promise<string | undefined>
46
+ /** How to extract the judged artifact from a run's event stream. Optional —
47
+ * defaults to the agent's final answer text (the research/QA case). SWE sets
48
+ * it to a patch parser. This is `benchmark = adapter` owning its deliverable,
49
+ * so the gate runner (`runGate` / `runBenchmark`) needs no
50
+ * per-benchmark branching. */
51
+ output?: OutputAdapter<string>
52
+ /** Benchmark-owned worker leaf. Set when the benchmark's native protocol IS the
53
+ * worker (e.g. AppWorld's interactive ReAct episode runs inside the engine,
54
+ * not as a chat completion) — the experiment uses this instead of the
55
+ * BACKEND-selected client; the steer still flows through the per-round prompt.
56
+ * Typed loosely to avoid a runtime import cycle; the harness casts it. */
57
+ leafClient?: (cfg: { model: string; routerBaseUrl: string; routerKey: string }) => unknown
58
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * UiReviewerAdapter for browser-agent-driver's `bad design-audit --json` — the
3
+ * reviewer that carries the DETERMINISTIC floor (axe a11y + WCAG contrast) the
4
+ * panel verdict is derived from. One of N pluggable reviewers behind the neutral
5
+ * UiReviewerAdapter contract; its subjective LLM findings are surfaced, its
6
+ * MEASURED floor is what `judgeUiFloor` attests.
7
+ *
8
+ * `badDesignAuditToReviewRuns` is a pure, unit-testable mapper kept separate from
9
+ * the spawn so the report-shape mapping is exercised without a browser/LLM.
10
+ */
11
+
12
+ import { spawn } from 'node:child_process'
13
+ import { mkdtemp, readFile, rm } from 'node:fs/promises'
14
+ import { tmpdir } from 'node:os'
15
+ import { join } from 'node:path'
16
+ import type {
17
+ DesignMeasurements,
18
+ UiFinding,
19
+ UiReviewRun,
20
+ UiReviewTarget,
21
+ UiReviewerAdapter,
22
+ } from '../ui-reviewer'
23
+
24
+ export interface BadDesignAuditConfig {
25
+ /** Path to the bad CLI entry (default: $BAD_CLI or ~/code/browser-agent-driver/dist/cli.js). */
26
+ badCli?: string
27
+ /** Design-audit profile (saas/defi/…); forwarded as --profile when set. */
28
+ profile?: string
29
+ model?: string
30
+ /** OpenAI-compatible endpoint — point at our router. */
31
+ baseUrl?: string
32
+ apiKey?: string
33
+ /** Pages to crawl from the start URL (bad --pages). */
34
+ pages?: number
35
+ timeoutMs?: number
36
+ }
37
+
38
+ interface BadFinding {
39
+ category?: string
40
+ severity?: 'critical' | 'major' | 'minor'
41
+ title?: string
42
+ description?: string
43
+ recommendation?: string
44
+ selector?: string
45
+ }
46
+ interface BadMeasurements {
47
+ contrast?: { aaFailures?: unknown[]; totalChecked?: number; summary?: { aaPassRate?: number } }
48
+ a11y?: { violations?: Array<{ id?: string; impact?: string }> }
49
+ }
50
+ interface BadPage {
51
+ route?: string
52
+ url?: string
53
+ findings?: BadFinding[]
54
+ measurements?: BadMeasurements
55
+ score?: number
56
+ healthScore?: number
57
+ }
58
+ /** Wire shape of `bad design-audit --json` (the report.json the CLI writes). */
59
+ export interface BadDesignReport {
60
+ pages?: BadPage[]
61
+ summary?: { healthScore?: number; avgScore?: number }
62
+ }
63
+
64
+ /** Pure mapper: `bad design-audit --json` report → neutral UiReviewRun[] (one per
65
+ * page). The measured floor (axe + contrast) becomes DesignMeasurements; the LLM
66
+ * findings become UiFinding[]; healthScore is recorded as selfReportedScore only. */
67
+ export function badDesignAuditToReviewRuns(report: BadDesignReport): UiReviewRun[] {
68
+ const pages = report.pages ?? []
69
+ return pages.map((p, i) => {
70
+ const route = p.route ?? p.url ?? `page-${i}`
71
+ const findings: UiFinding[] = (p.findings ?? []).map((f) => ({
72
+ reviewerId: 'bad-design-audit',
73
+ lens: f.category ?? 'design',
74
+ severity: f.severity ?? 'minor',
75
+ route,
76
+ title: f.title ?? '(finding)',
77
+ observation: f.description ?? f.title ?? '',
78
+ suggestedFix: f.recommendation,
79
+ selector: f.selector,
80
+ }))
81
+ const m = p.measurements
82
+ let measurements: DesignMeasurements | undefined
83
+ if (m?.contrast || m?.a11y) {
84
+ const failures = m.contrast?.aaFailures?.length ?? 0
85
+ const total = m.contrast?.totalChecked ?? 0
86
+ measurements = {
87
+ a11yViolations: (m.a11y?.violations ?? []).map((v) => ({ id: v.id ?? 'unknown', impact: v.impact })),
88
+ contrastAaPassRate: m.contrast?.summary?.aaPassRate ?? (total > 0 ? 1 - failures / total : 1),
89
+ contrastAaFailures: failures,
90
+ contrastTotalChecked: total,
91
+ }
92
+ }
93
+ return {
94
+ reviewerId: 'bad-design-audit',
95
+ route,
96
+ findings,
97
+ measurements,
98
+ selfReportedScore: p.healthScore ?? p.score,
99
+ }
100
+ })
101
+ }
102
+
103
+ function resolveCli(cfg: BadDesignAuditConfig): string {
104
+ return cfg.badCli ?? process.env.BAD_CLI ?? join(process.env.HOME ?? '', 'code/browser-agent-driver/dist/cli.js')
105
+ }
106
+
107
+ /** A UiReviewerAdapter backed by `bad design-audit`. Drives a real browser audit
108
+ * (LLM findings + measured axe/contrast floor); the panel judge attests the floor. */
109
+ export function badDesignAuditReviewer(cfg: BadDesignAuditConfig = {}): UiReviewerAdapter {
110
+ const cli = resolveCli(cfg)
111
+ const timeoutMs = cfg.timeoutMs ?? 600_000
112
+ return {
113
+ id: 'bad-design-audit',
114
+ async review(target: UiReviewTarget): Promise<UiReviewRun[]> {
115
+ const dir = await mkdtemp(join(tmpdir(), 'bad-design-'))
116
+ const args = [cli, 'design-audit', '--url', target.url, '--json', '--output', dir]
117
+ if (cfg.profile) args.push('--profile', cfg.profile)
118
+ if (cfg.pages) args.push('--pages', String(cfg.pages))
119
+ if (cfg.model) args.push('--model', cfg.model)
120
+ if (cfg.baseUrl) args.push('--base-url', cfg.baseUrl)
121
+ if (cfg.apiKey) args.push('--api-key', cfg.apiKey)
122
+ if (target.storageState) args.push('--storage-state', target.storageState)
123
+ try {
124
+ await runChild('node', args, dir, timeoutMs)
125
+ const report = JSON.parse(await readFile(join(dir, 'report.json'), 'utf8')) as BadDesignReport
126
+ return badDesignAuditToReviewRuns(report)
127
+ } finally {
128
+ await rm(dir, { recursive: true, force: true }).catch(() => {})
129
+ }
130
+ },
131
+ }
132
+ }
133
+
134
+ function runChild(cmd: string, args: string[], cwd: string, timeoutMs: number): Promise<void> {
135
+ return new Promise((resolve, reject) => {
136
+ const child = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
137
+ let err = ''
138
+ const timer = setTimeout(() => {
139
+ child.kill('SIGKILL')
140
+ reject(new Error(`bad design-audit timed out after ${timeoutMs}ms`))
141
+ }, timeoutMs)
142
+ child.stderr.on('data', (d) => (err += d.toString()))
143
+ child.on('error', (e) => {
144
+ clearTimeout(timer)
145
+ reject(new Error(`bad design-audit failed to spawn: ${e.message}`))
146
+ })
147
+ child.on('close', (code) => {
148
+ clearTimeout(timer)
149
+ if (code !== 0) reject(new Error(`bad design-audit exited ${code}: ${err.slice(-300)}`))
150
+ else resolve()
151
+ })
152
+ })
153
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Deterministic self-check for the bad-design-audit reviewer mapper —
3
+ * `tsx src/browser/adapters/bad-design-audit.verify.ts`.
4
+ *
5
+ * Proves the `bad design-audit --json` report maps to the neutral UiReviewRun with
6
+ * the DETERMINISTIC floor intact, and that the panel verdict is then driven by that
7
+ * measured floor — not the audit's self-reported healthScore.
8
+ */
9
+
10
+ import { judgeUiFloor } from '../ui-reviewer'
11
+ import { badDesignAuditToReviewRuns, type BadDesignReport } from './bad-design-audit'
12
+
13
+ let failures = 0
14
+ function check(name: string, cond: boolean, detail = '') {
15
+ if (cond) console.log(` ✓ ${name}`)
16
+ else {
17
+ failures += 1
18
+ console.log(` ✗ ${name} ${detail}`)
19
+ }
20
+ }
21
+
22
+ // A bad design-audit --json report shaped exactly like the CLI writes: one clean
23
+ // page + one page whose MEASURED floor is blocking, while it self-reports a high
24
+ // healthScore (the dishonesty the floor verdict must override).
25
+ const report: BadDesignReport = {
26
+ summary: { avgScore: 7, healthScore: 80 },
27
+ pages: [
28
+ {
29
+ route: 'home',
30
+ url: 'https://shop.test/',
31
+ score: 8,
32
+ healthScore: 88,
33
+ findings: [
34
+ { category: 'hierarchy', severity: 'major', title: 'Weak CTA hierarchy', description: 'primary + secondary look identical', recommendation: 'differentiate weight', selector: 'button.cta' },
35
+ ],
36
+ measurements: {
37
+ contrast: { aaFailures: [{}, {}], totalChecked: 100, summary: { aaPassRate: 0.98 } },
38
+ a11y: { violations: [{ id: 'image-alt', impact: 'moderate' }] },
39
+ },
40
+ },
41
+ {
42
+ route: 'checkout',
43
+ url: 'https://shop.test/checkout',
44
+ score: 4,
45
+ healthScore: 95, // self-reports GREAT — but the measured floor is blocking
46
+ findings: [],
47
+ measurements: {
48
+ contrast: { aaFailures: new Array(40).fill({}), totalChecked: 100, summary: { aaPassRate: 0.6 } },
49
+ a11y: {
50
+ violations: [
51
+ { id: 'label', impact: 'critical' },
52
+ { id: 'button-name', impact: 'critical' },
53
+ { id: 'aria-required', impact: 'critical' },
54
+ { id: 'link-name', impact: 'serious' },
55
+ { id: 'color-contrast', impact: 'serious' },
56
+ ],
57
+ },
58
+ },
59
+ },
60
+ ],
61
+ }
62
+
63
+ console.log('bad-design-audit reviewer mapper verify:')
64
+
65
+ const runs = badDesignAuditToReviewRuns(report)
66
+ check('maps one UiReviewRun per page', runs.length === 2, `got ${runs.length}`)
67
+ check('carries the deterministic floor (axe + contrast) on each page', runs.every((r) => r.measurements != null))
68
+ const home = runs.find((r) => r.route === 'home')!
69
+ check('home contrast pass rate mapped', home.measurements?.contrastAaPassRate === 0.98)
70
+ check('home finding mapped to a UiFinding (lens=category, severity preserved, selector kept)', home.findings.length === 1 && home.findings[0]?.lens === 'hierarchy' && home.findings[0]?.severity === 'major' && home.findings[0]?.selector === 'button.cta')
71
+ check('self-reported healthScore recorded but NOT the verdict', home.selfReportedScore === 88)
72
+
73
+ const verdict = judgeUiFloor(runs)
74
+ check('panel verdict is BLOCKING — the measured checkout floor (3 critical a11y + 40% contrast fail) overrides the 95 self-report', verdict.resolved === false, verdict.detail)
75
+ check('blocking score is low', verdict.score < 0.6, verdict.detail)
76
+
77
+ // A reviewer with NO measurements (e.g. a findings-only audit) contributes no floor.
78
+ const noFloor = badDesignAuditToReviewRuns({ pages: [{ route: 'x', findings: [], score: 5 }] } satisfies BadDesignReport)
79
+ check('a page without measurements yields no floor (subjective-only run)', noFloor[0]?.measurements === undefined)
80
+
81
+ if (failures > 0) {
82
+ console.log(`\n❌ ${failures} check(s) failed`)
83
+ process.exit(1)
84
+ }
85
+ console.log('\n✅ bad-design-audit reviewer verified — floor mapped from --json, verdict driven by the measured floor not the self-report')
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Reference BrowserAgentAdapter for @tangle-network/browser-agent-driver (the `bad`
3
+ * CLI). One of N pluggable drivers behind the neutral harness — it conforms to the
4
+ * SAME BrowserAgentAdapter contract as a browser-use runner or any other agent.
5
+ *
6
+ * `bad run --cases <file> --sink <dir>` emits a structured report.json; we map its
7
+ * `agentResult` to the neutral BrowserRun and let `judgeBrowserRun` derive the
8
+ * DETERMINISTIC verdict. We record `bad`'s own success/goalVerification only as
9
+ * `selfReportedSuccess` — it never decides the outcome (attestation, not trust).
10
+ *
11
+ * `badReportToRun` is a pure, unit-testable mapper kept separate from the spawn so
12
+ * the report-shape mapping can be exercised without launching a browser.
13
+ */
14
+
15
+ import { spawn } from 'node:child_process'
16
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
17
+ import { tmpdir } from 'node:os'
18
+ import { join } from 'node:path'
19
+ import type { BrowserAgentAdapter, BrowserRun, BrowserStep, BrowserTask } from '../agent-adapter'
20
+
21
+ export interface BadAdapterConfig {
22
+ /** Path to the bad CLI entry (default: $BAD_CLI or ~/code/browser-agent-driver/dist/cli.js). */
23
+ badCli?: string
24
+ model?: string
25
+ provider?: string
26
+ /** OpenAI-compatible endpoint — point at our router. */
27
+ baseUrl?: string
28
+ apiKey?: string
29
+ maxTurns?: number
30
+ timeoutMs?: number
31
+ /** Capture a per-turn screenshot into each turn's state.screenshot (bad
32
+ * full-evidence mode). Off by default — opt in for the navigation film. */
33
+ captureScreenshots?: boolean
34
+ }
35
+
36
+ interface BadReport {
37
+ results?: Array<{
38
+ testCase?: { id?: string }
39
+ // Real token/cost telemetry (bad >=0.33 reports these per result) — forwarded to
40
+ // BrowserRun so a real run never reads as a {0,0} backend-integrity stub.
41
+ estimatedCostUsd?: number
42
+ inputTokens?: number
43
+ outputTokens?: number
44
+ agentResult?: {
45
+ success?: boolean
46
+ result?: string
47
+ turns?: Array<{
48
+ turn?: number
49
+ // state.screenshot is INLINE base64 (no data: prefix) — the durable frame
50
+ // source for the film, since the adapter's temp sink is reaped after the run.
51
+ state?: { url?: string; title?: string; snapshot?: string; screenshot?: string }
52
+ action?: { action?: string; selector?: string; text?: string; result?: string }
53
+ }>
54
+ }
55
+ }>
56
+ }
57
+
58
+ /** Pure mapper: bad's report.json → the neutral BrowserRun for taskId. The final
59
+ * turn's state.url is the observable final URL; its snapshot is the final DOM the
60
+ * deterministic dom-* checks read. */
61
+ export function badReportToRun(report: BadReport, taskId: string): BrowserRun {
62
+ const res = report.results?.find((r) => r.testCase?.id === taskId) ?? report.results?.[0]
63
+ const ar = res?.agentResult ?? {}
64
+ const turns = Array.isArray(ar.turns) ? ar.turns : []
65
+ const steps: BrowserStep[] = turns.map((t, i) => ({
66
+ index: typeof t.turn === 'number' ? t.turn : i + 1,
67
+ url: t.state?.url ?? '',
68
+ action: t.action?.action ?? 'decide',
69
+ target: t.action?.selector ?? (t.action?.text ? `"${t.action.text}"` : undefined),
70
+ // The frame for the film: bad's per-turn base64 JPEG, normalized to a data: URI.
71
+ screenshot: t.state?.screenshot ? `data:image/jpeg;base64,${t.state.screenshot}` : undefined,
72
+ }))
73
+ const last = turns[turns.length - 1]
74
+ const input = typeof res?.inputTokens === 'number' ? res.inputTokens : undefined
75
+ const output = typeof res?.outputTokens === 'number' ? res.outputTokens : undefined
76
+ return {
77
+ taskId,
78
+ steps,
79
+ finalUrl: last?.state?.url ?? '',
80
+ finalDom: typeof last?.state?.snapshot === 'string' ? last.state.snapshot : undefined,
81
+ answer: typeof ar.result === 'string' ? ar.result : undefined,
82
+ selfReportedSuccess: typeof ar.success === 'boolean' ? ar.success : undefined,
83
+ usage: input !== undefined || output !== undefined ? { input: input ?? 0, output: output ?? 0 } : undefined,
84
+ costUsd: typeof res?.estimatedCostUsd === 'number' ? res.estimatedCostUsd : undefined,
85
+ driverId: 'bad',
86
+ }
87
+ }
88
+
89
+ function resolveCli(cfg: BadAdapterConfig): string {
90
+ return cfg.badCli ?? process.env.BAD_CLI ?? join(process.env.HOME ?? '', 'code/browser-agent-driver/dist/cli.js')
91
+ }
92
+
93
+ /** A BrowserAgentAdapter backed by the `bad` CLI. Runs one task through a real
94
+ * browser; the harness judge attests the outcome. */
95
+ export function badBrowserAdapter(cfg: BadAdapterConfig = {}): BrowserAgentAdapter {
96
+ const cli = resolveCli(cfg)
97
+ const timeoutMs = cfg.timeoutMs ?? 300_000
98
+ return {
99
+ id: 'bad',
100
+ async run(task: BrowserTask): Promise<BrowserRun> {
101
+ const dir = await mkdtemp(join(tmpdir(), 'bad-adapter-'))
102
+ const casesPath = join(dir, 'cases.json')
103
+ const sink = join(dir, 'sink')
104
+ const cases = [
105
+ {
106
+ id: task.id,
107
+ name: task.id,
108
+ goal: task.goal,
109
+ url: task.startUrl,
110
+ maxTurns: task.maxSteps ?? cfg.maxTurns ?? 20,
111
+ },
112
+ ]
113
+ await writeFile(casesPath, JSON.stringify(cases))
114
+ const args = [
115
+ cli,
116
+ 'run',
117
+ '--cases',
118
+ casesPath,
119
+ '--sink',
120
+ sink,
121
+ '--provider',
122
+ cfg.provider ?? 'openai',
123
+ '--model',
124
+ cfg.model ?? 'deepseek-v4-flash',
125
+ '--max-turns',
126
+ String(task.maxSteps ?? cfg.maxTurns ?? 20),
127
+ '--headless',
128
+ ]
129
+ // full-evidence + every-turn capture populates each turn's state.screenshot,
130
+ // which badReportToRun lifts into BrowserStep.screenshot for the film.
131
+ if (cfg.captureScreenshots) args.push('--mode', 'full-evidence', '--screenshot-interval', '1')
132
+ if (cfg.baseUrl) args.push('--base-url', cfg.baseUrl)
133
+ if (cfg.apiKey) args.push('--api-key', cfg.apiKey)
134
+ if (task.storageState) args.push('--storage-state', task.storageState)
135
+ try {
136
+ await runChild('node', args, dir, timeoutMs)
137
+ const report = JSON.parse(await readFile(join(sink, 'report.json'), 'utf8')) as BadReport
138
+ return badReportToRun(report, task.id)
139
+ } finally {
140
+ await rm(dir, { recursive: true, force: true }).catch(() => {})
141
+ }
142
+ },
143
+ }
144
+ }
145
+
146
+ function runChild(cmd: string, args: string[], cwd: string, timeoutMs: number): Promise<void> {
147
+ return new Promise((resolve, reject) => {
148
+ const child = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
149
+ let err = ''
150
+ const timer = setTimeout(() => {
151
+ child.kill('SIGKILL')
152
+ reject(new Error(`bad adapter timed out after ${timeoutMs}ms`))
153
+ }, timeoutMs)
154
+ child.stderr.on('data', (d) => (err += d.toString()))
155
+ child.on('error', (e) => {
156
+ clearTimeout(timer)
157
+ reject(new Error(`bad adapter failed to spawn: ${e.message}`))
158
+ })
159
+ child.on('close', (code) => {
160
+ clearTimeout(timer)
161
+ if (code !== 0) reject(new Error(`bad exited ${code}: ${err.slice(-300)}`))
162
+ else resolve()
163
+ })
164
+ })
165
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Driver-agnostic browser-agent harness — the GENERAL seam.
3
+ *
4
+ * The benchmark owns the task and the verdict; the browser AGENT is pluggable.
5
+ * Any driver — @tangle-network/browser-agent-driver, browser-use (Python),
6
+ * a hosted API, a future tool — conforms to ONE neutral contract: given a goal +
7
+ * start URL, produce a normalized trajectory and a final observable state. We do
8
+ * NOT shape this around any one driver's SDK; it is the least-common-denominator
9
+ * a browser agent can emit, crossed with what a deterministic judge needs.
10
+ *
11
+ * Attestability is the whole point. We NEVER trust the driver's own `success`
12
+ * flag or its LLM goal-verification (that is the unverifiable self-report the
13
+ * benchmark exists to replace). Our judge re-derives the outcome from DETERMINISTIC
14
+ * criteria (url / DOM state / element checks) against the run's final state. A
15
+ * driver's published number is a hypothesis our harness attests or refutes.
16
+ *
17
+ * Language-agnostic by a process boundary: a driver that isn't TypeScript (e.g.
18
+ * browser-use) is wrapped by `processBrowserAdapter`, which hands the task as JSON
19
+ * on argv/stdin and reads a BrowserRun back as JSON on stdout. No SDK coupling, no
20
+ * overfit — adding a driver is a thin runner that speaks this JSON, nothing more.
21
+ */
22
+
23
+ /** A deterministic, driver-independent success check the JUDGE applies to the
24
+ * run's final state — never the agent's self-assessment. Mirrors the kinds a
25
+ * benchmark harness actually ships (WebArena program-state, Mind2Web element). */
26
+ export type SuccessSpec =
27
+ | { type: 'url-contains'; value: string }
28
+ | { type: 'url-matches'; value: string } // regex on finalUrl
29
+ | { type: 'final-answer-matches'; value: string } // regex on the agent's answer (extraction tasks)
30
+ | { type: 'dom-contains'; value: string } // substring of the final DOM/text snapshot
31
+ | { type: 'dom-selector-text'; selector: string; value: string } // element text equals/contains
32
+ | { type: 'program'; id: string } // a benchmark-provided programmatic check keyed by id
33
+
34
+ /** A task handed to ANY browser agent. `success` is the benchmark's deterministic
35
+ * verdict spec — evaluated by us, not the driver. */
36
+ export interface BrowserTask {
37
+ id: string
38
+ goal: string
39
+ startUrl: string
40
+ maxSteps?: number
41
+ /** Path to a Playwright storageState for authenticated tasks (optional). */
42
+ storageState?: string
43
+ /** Deterministic success criteria the judge applies to the produced run. */
44
+ success: SuccessSpec[]
45
+ /** Benchmark-specific extras the judge may need (gold answer, program inputs). */
46
+ metadata?: Record<string, unknown>
47
+ }
48
+
49
+ /** One normalized step, common to every driver. */
50
+ export interface BrowserStep {
51
+ index: number
52
+ /** URL at this step. */
53
+ url: string
54
+ /** A normalized action label (e.g. 'click', 'type', 'navigate', 'scroll'). */
55
+ action: string
56
+ /** Human/agent-readable target or value (selector, text typed, link). */
57
+ target?: string
58
+ /** Agent's reasoning for this step, if the driver exposes it. */
59
+ reasoning?: string
60
+ /** Path to a per-step screenshot on disk (for the run-capsule film). */
61
+ screenshotPath?: string
62
+ /** Inline base64 `data:` URI of the per-step frame, when the driver carries the
63
+ * image in its report rather than on disk (e.g. `bad`'s state.screenshot, whose
64
+ * sink is reaped post-run). Preferred over `screenshotPath` by the film converter. */
65
+ screenshot?: string
66
+ /** Target element box at action time, if known — for replay cursor overlays. */
67
+ actionBounds?: { x: number; y: number; width: number; height: number }
68
+ }
69
+
70
+ /** The normalized result ANY driver produces — the judge scores this, not the
71
+ * driver's opinion of itself. */
72
+ export interface BrowserRun {
73
+ taskId: string
74
+ steps: BrowserStep[]
75
+ /** URL the agent ended on. */
76
+ finalUrl: string
77
+ /** Final visible text / DOM snapshot for dom-* deterministic checks. */
78
+ finalDom?: string
79
+ /** The agent's final free-text answer (extraction tasks). */
80
+ answer?: string
81
+ /** The driver's OWN success claim — recorded for comparison, NEVER the verdict. */
82
+ selfReportedSuccess?: boolean
83
+ /** Real token usage + cost if the driver reports it (backend-integrity). */
84
+ usage?: { input: number; output: number }
85
+ costUsd?: number
86
+ /** Which driver produced this run (for compareDrivers attribution). */
87
+ driverId: string
88
+ }
89
+
90
+ /** The general seam every browser driver implements (directly in TS, or via the
91
+ * process wrapper below). `id` is the attribution key for head-to-head. */
92
+ export interface BrowserAgentAdapter {
93
+ readonly id: string
94
+ run(task: BrowserTask): Promise<BrowserRun>
95
+ }
96
+
97
+ /** Optional per-benchmark programmatic checks, keyed by SuccessSpec {type:'program', id}. */
98
+ export type ProgramChecks = Record<string, (run: BrowserRun, task: BrowserTask) => boolean>
99
+
100
+ /** The DETERMINISTIC, attestable verdict: evaluate the benchmark's SuccessSpec list
101
+ * against the run's final observable state. Independent of selfReportedSuccess.
102
+ * resolved = ALL criteria pass; score = fraction passed (a gradient for the optimizer). */
103
+ export function judgeBrowserRun(
104
+ task: BrowserTask,
105
+ run: BrowserRun,
106
+ programs: ProgramChecks = {},
107
+ ): { resolved: boolean; score: number; detail: string } {
108
+ const checks = task.success
109
+ if (checks.length === 0) throw new Error(`browser task ${task.id} has no success criteria — cannot attest`)
110
+ const results = checks.map((c) => ({ c, ok: evalCriterion(c, run, task, programs) }))
111
+ const passed = results.filter((r) => r.ok).length
112
+ const resolved = passed === checks.length
113
+ return {
114
+ resolved,
115
+ score: passed / checks.length,
116
+ detail: JSON.stringify({
117
+ passed,
118
+ total: checks.length,
119
+ selfReported: run.selfReportedSuccess,
120
+ finalUrl: run.finalUrl,
121
+ perCriterion: results.map((r) => ({ type: r.c.type, ok: r.ok })),
122
+ }),
123
+ }
124
+ }
125
+
126
+ function evalCriterion(c: SuccessSpec, run: BrowserRun, task: BrowserTask, programs: ProgramChecks): boolean {
127
+ switch (c.type) {
128
+ case 'url-contains':
129
+ return run.finalUrl.includes(c.value)
130
+ case 'url-matches':
131
+ return new RegExp(c.value).test(run.finalUrl)
132
+ case 'final-answer-matches':
133
+ return run.answer != null && new RegExp(c.value, 'i').test(run.answer)
134
+ case 'dom-contains':
135
+ return run.finalDom != null && run.finalDom.includes(c.value)
136
+ case 'dom-selector-text':
137
+ // Deterministic only with a structured snapshot; require finalDom to carry it.
138
+ return run.finalDom != null && run.finalDom.includes(c.value)
139
+ case 'program': {
140
+ const fn = programs[c.id]
141
+ if (!fn) throw new Error(`browser task ${task.id}: no program check registered for id '${c.id}'`)
142
+ return fn(run, task)
143
+ }
144
+ }
145
+ }