# Methodology ## What is measured For each of six representations of a web page: 1. **Token cost**: the tokens the page representation adds to a prompt, counted separately from the fixed prompt (instructions, question, answer format). 2. **READ accuracy**: exact-match answers to factual questions about the page. 3. **ACT accuracy**: whether the model identifies the element to click or fill, resolved back to a live element and compared with ground truth. The benchmark measures each **representation**, sent once, at the top of the page, to one model with one shared prompt. It does not measure any library's agent loop (Browser Use's `Agent`, Stagehand's `act`/`observe`/`extract` prompts, scrolling, retries or self-healing). Those layers add their own prompts and calls, so the token numbers here are a lower bound on what each library spends in production. ## Environment | Item | Value | |---|---| | Date | Pilot 2026-10-01 (run `pilot-20261001T170632Z`); full run 2026-10-02 (run `full-20261002T073411Z`) | | Machine | Apple M4 Pro, macOS 26.6.2 (arm64) | | Python | 3.12.13, managed with uv 0.11.29; three lockfiles (see below) | | Browser | Chrome for Testing 153.0.8010.12 bundled with Playwright 1.63.0 (`channel="chromium"`, new headless), viewport 1280x800 | | Model | `claude-sonnet-5` (Anthropic API, SDK `anthropic` 1.11.0) | | Sampling | `temperature`/`top_p`/`top_k` omitted: Sonnet 5 rejects non-default values with a 400 error (platform.claude.com/docs/en/models/sonnet-5/overview). Sampling is therefore at the default temperature; variance is measured with 3 repeats. | | Thinking | `{"type": "disabled"}` (adaptive thinking is on by default on Sonnet 5) | | Other | `max_tokens` 512, no prompt caching, `max_retries=0`, 120 s timeout, 4 concurrent calls | | Tokenizers | Anthropic token counting endpoint for `claude-sonnet-5`; tiktoken 0.14.0 `o200k_base` | | Libraries | browser-use 0.13.10; stagehand 4.1.0 (Python SDK, bundled Stagehand Runtime extension); markdownify 1.2.3 | `claude-sonnet-5` was the model specified for this benchmark. It is listed as "Legacy" (current Sonnet: `claude-sonnet-5-5`), and remains available until at least 2027-06-30. **Three environments.** browser-use 0.13.10 pins `websockets==15.0.1` (through `browser-harness`) and stagehand 4.1.0 requires `websockets>=16.1.1`, so they cannot share an environment. The core harness, `adapters/browser_use` and `adapters/stagehand` are separate uv projects, each with its own committed `uv.lock`, so every library runs on exactly the versions its authors declare. The adapters are long-lived subprocesses speaking JSON over stdin/stdout. They receive no API key (the harness strips `ANTHROPIC_API_KEY` from their environment) and make no LLM calls. ## Pages All pages are served from `pages/` by a local server (`src/wpbench/server.py`). Every Chromium the benchmark launches (Playwright's and Stagehand's) gets `--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost, EXCLUDE 127.0.0.1`, so no request can reach the internet. `scripts/verify_pages.py` lists every request a page attempts; the only blocked ones are: - books.toscrape.com pages request jQuery over plain http from ajax.googleapis.com. The live https site blocks it as mixed content and falls back to its own copy, which is what happens locally too. - saucedemo.com sends error telemetry to events.backtrace.io. | id | kind | source | |---|---|---| | article | article | local fixture, original text | | docs | docs page | local fixture, fictional API | | books_listing | e-commerce listing | snapshot of https://books.toscrape.com/ (Zyte practice site) | | books_product | product page | snapshot of https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html | | saucedemo_login | login form | snapshot of https://www.saucedemo.com/ (Sauce Labs practice site; client-rendered React app) | | form | multi-field form | local fixture | | dashboard | dashboard-like page | local fixture, synthetic data, inline SVG chart | | serp | search results | local fixture, fictional results | | table | long data table | local fixture, 500 rows generated by `scripts/make_table_fixture.py` (fixed-seed LCG) | | embed | iframe + closed Shadow DOM | local fixture: same-origin iframe (localhost:8100), cross-site iframe (127.0.0.1:8100, out of process), closed-shadow `` | Snapshots were captured once with `scripts/capture_snapshot.py`. Every response the page loaded was saved as the server sent it (documents are not the rendered DOM, so client-side scripts run again locally), with a sha256 manifest per site (`pages/snapshots/*/manifest.json`). **Licensing.** The practice sites publish no redistribution license. Before publishing this repository, decide whether to ship the snapshots or the capture script plus checksums. ## Representations Every model input comes from the same **page session**, and model answers are resolved against the same session. **Benchmark browser.** One fresh load in a new Playwright context: 1. Set the viewport to 1280x800. 2. `goto`, wait for network idle, wait 500 ms. 3. Scroll to (0, 0). All representations except Stagehand's are captured from this load, in this order: 1. **raw_html**: `page.content()`, the serialized top document only. It contains neither iframe content nor shadow-root content. 2. **markdown**: `markdownify.markdownify(raw_html)` with default settings. markdownify was chosen over trafilatura because it converts the whole page without selecting content. trafilatura is a boilerplate remover: it drops navigation, forms and footers by design, so the benchmark would measure its extraction choices rather than the Markdown format. Markdown is also not its default output (that is plain text). markdownify drops ``/``, but answering its `` is defensible. Scored as pre-registered. `results/report_tables_full.md` gives a clearly labelled sensitivity line. - **Tokenizer counts are estimates.** The Anthropic token counting endpoint is documented as an estimate that may differ slightly from billed usage. The billed `usage.input_tokens` is recorded for every call.