@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.
- package/README.md +15 -0
- package/package.json +35 -0
- package/src/adapters.ts +60 -0
- package/src/aec-gate.mts +217 -0
- package/src/atom-humaneval.mts +197 -0
- package/src/atom-mcp-e2e.mts +223 -0
- package/src/benchmarks/_harness.ts +206 -0
- package/src/benchmarks/aec-bench.test.mts +53 -0
- package/src/benchmarks/aec-bench.ts +319 -0
- package/src/benchmarks/appworld.test.mts +45 -0
- package/src/benchmarks/appworld.ts +426 -0
- package/src/benchmarks/cad-design.ts +429 -0
- package/src/benchmarks/cadbench.ts +135 -0
- package/src/benchmarks/cadgenbench.ts +121 -0
- package/src/benchmarks/commit0.test.mts +71 -0
- package/src/benchmarks/commit0.ts +266 -0
- package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
- package/src/benchmarks/enterpriseops-gym.ts +343 -0
- package/src/benchmarks/finsearchcomp.ts +371 -0
- package/src/benchmarks/frames.ts +520 -0
- package/src/benchmarks/hotpotqa.ts +320 -0
- package/src/benchmarks/humaneval.ts +251 -0
- package/src/benchmarks/mind2web.ts +311 -0
- package/src/benchmarks/programbench.test.mts +60 -0
- package/src/benchmarks/programbench.ts +210 -0
- package/src/benchmarks/simpleqa.ts +390 -0
- package/src/benchmarks/swe-bench.ts +152 -0
- package/src/benchmarks/terminal-bench.ts +182 -0
- package/src/benchmarks/trata-hedge.ts +496 -0
- package/src/benchmarks/types.ts +58 -0
- package/src/browser/adapters/bad-design-audit.ts +153 -0
- package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
- package/src/browser/adapters/bad.ts +165 -0
- package/src/browser/agent-adapter.ts +145 -0
- package/src/browser/process-adapter.ts +146 -0
- package/src/browser/run-to-spans.ts +105 -0
- package/src/browser/run-to-spans.verify.ts +89 -0
- package/src/browser/ui-reviewer.ts +200 -0
- package/src/browser/ui-reviewer.verify.ts +94 -0
- package/src/browser/verify.ts +96 -0
- package/src/clbench-codebase-gate.mts +314 -0
- package/src/clbench-context-gate.mts +305 -0
- package/src/cloud-loop.mts +138 -0
- package/src/coding-skills/minimal-diff.md +9 -0
- package/src/coding-skills/read-before-edit.md +9 -0
- package/src/coding-skills/reproduce-first.md +10 -0
- package/src/coding-skills/run-tests-after-edit.md +9 -0
- package/src/coding-skills/trace-the-failure.md +9 -0
- package/src/commit0-env-run.mts +59 -0
- package/src/commit0-env.ts +173 -0
- package/src/commit0-gate.mts +529 -0
- package/src/commit0-prereqs.sh +48 -0
- package/src/corpus-replay.mts +300 -0
- package/src/corpus-report.mts +504 -0
- package/src/corpus.test.mts +273 -0
- package/src/corpus.ts +325 -0
- package/src/decoder-live.mts +133 -0
- package/src/directives.ts +84 -0
- package/src/diverse-gate.mjs +112 -0
- package/src/egress-probe.mts +26 -0
- package/src/eops-skills/address-every-subtask.md +5 -0
- package/src/eops-skills/exact-tools-and-args.md +5 -0
- package/src/eops-skills/full-sequence-to-goal.md +5 -0
- package/src/eops-skills/ground-every-value.md +5 -0
- package/src/eops-skills/honor-the-policies.md +5 -0
- package/src/examples/README.md +58 -0
- package/src/examples/math-demo.mts +110 -0
- package/src/examples/strategy-demo.mts +119 -0
- package/src/fleet.mts +121 -0
- package/src/gate-cli.mts +101 -0
- package/src/gate.test.mts +129 -0
- package/src/gate.ts +460 -0
- package/src/generate-eval/certify.ts +178 -0
- package/src/generate-eval/schema.ts +78 -0
- package/src/humaneval-gate.mts +204 -0
- package/src/humaneval-repair-gate.mts +143 -0
- package/src/index.ts +19 -0
- package/src/mcp-mount-probe.mts +126 -0
- package/src/profile-coordinates.ts +134 -0
- package/src/profiles.ts +128 -0
- package/src/refine-loop.test.mts +106 -0
- package/src/refine-loop.ts +106 -0
- package/src/research-gate.mts +132 -0
- package/src/research-shot.ts +134 -0
- package/src/resolve-client.ts +58 -0
- package/src/router-executor.ts +51 -0
- package/src/run-pool.ts +48 -0
- package/src/runtime-hook-recorder.ts +137 -0
- package/src/sandbox-run.ts +125 -0
- package/src/search-bench/bridge.ts +124 -0
- package/src/search-bench/export.mts +0 -0
- package/src/search-bench/parametric-check.mts +63 -0
- package/src/search-bench/profiles.ts +98 -0
- package/src/search-bench/run.mts +287 -0
- package/src/search-bench/tasks-fresh.ts +688 -0
- package/src/search-bench/tasks.ts +129 -0
- package/src/search-tool.ts +95 -0
- package/src/selector.test.mts +189 -0
- package/src/selector.ts +366 -0
- package/src/skill-sandbox-smoke.mts +100 -0
- package/src/stats.mts +90 -0
- package/src/terminal-compare.ts +519 -0
- package/src/trajectory-assemble.mjs +130 -0
- package/src/trata-gate.mts +243 -0
- package/src/trata-gepa.mts +434 -0
- package/src/worker-blender.ts +230 -0
- package/src/worker-browser.ts +102 -0
- package/src/worker-build123d.ts +143 -0
- package/src/worker-cad.ts +451 -0
- package/src/worker.ts +136 -0
- 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
|
+
}
|