Computer-Use & Browser Agents Series — Presentation 02

Browser Agent Patterns

Browser-Use, Playwright agents, Steel, accessibility-tree extraction, WebArena/Mind2Web evals, captcha/anti-bot, stateful sessions, pricing, and failure recovery.

Browser-Use Playwright Steel AXTree WebArena Mind2Web Anti-Bot
Browser Launch → DOM / AXTree → LLM Plan → Action → Observe → Recover → Eval
00

Topics We'll Cover

01

Browser-Use, Playwright Agents, Steel

Three tiers of abstraction have emerged for browser-based LLM agents. They differ in where the DOM/screenshot extraction happens, which models they support, and how much infrastructure they manage for you.

Browser-Use Python library

browser-use v0.2.x (MIT, Jan 2025) wraps Playwright with a hybrid AXTree + screenshot perception layer. 1000+ GitHub stars within two weeks of release.

  • Auto-extracts interactive elements into a flat JSON list (role, text, bounding box)
  • Passes element list + optional screenshot to the LLM
  • Returns typed actions: click_element(index=7), input_text(index=3, text="...")
  • Built-in memory / multi-tab support
  • pip install browser-use; model-agnostic (OpenAI, Anthropic, Gemini, local)

Playwright agents DIY

Roll your own scaffolding around Playwright (Microsoft, 2020). Full control; no abstraction overhead.

  • playwright.chromium.launch(headless=True)
  • Access the full CDP: screenshots, network intercept, service-worker control, storage
  • Call page.accessibility.snapshot() for the AXTree
  • Inject <script> tags or evaluate JS for richer extraction
  • Used by OpenAI Operator internally (Playwright + CUA model)

Steel managed infra

Steel (steel.dev, 2024) is a remote browser-as-a-service: managed Chromium instances accessible via CDP, Playwright, or Puppeteer — no local browser, no CAPTCHA infrastructure to manage.

  • REST API: POST /sessions → session ID + CDP endpoint
  • Residential proxy pool built in; human-like TLS fingerprint
  • Persistent profiles across sessions (cookies, localStorage)
  • Screenshot streaming via WebSocket
  • Priced per minute of active session (see Slide 07)
Which to pick

Use Browser-Use for quick prototyping with any major LLM. Use raw Playwright when you need fine-grained CDP control, custom JS injection, or non-standard browser configurations. Use Steel when you need managed residential proxies, persistent sessions across runs, or you don't want to manage a Chromium fleet yourself.

02

DOM Extraction Strategies — Accessibility Tree, Interactive-Element Detection

The quality of the agent's world-model depends entirely on how well the page is reduced to a form the LLM can plan over. Four strategies are in common use, from least to most sophisticated:

Strategy 1 — Full inner-HTML dump

Naive. Grabs document.body.innerHTML or the Playwright content() call. Typical page: 200–800 KB raw, 30–150 KB stripped. Token count after stripping scripts and styles: 8 000–60 000. Almost always too large for a single context window. Only viable for very small pages or if you're summarising the page separately.

Strategy 2 — Interactive-element list

Playwright — collect all interactive elements with bounding boxes
async def get_interactive_elements(page):
    return await page.evaluate("""() => {
        const selectors = [
          'a[href]', 'button', 'input', 'select', 'textarea',
          '[role=button]', '[role=link]', '[role=checkbox]',
          '[role=combobox]', '[tabindex]:not([tabindex="-1"])'
        ];
        return selectors
          .flatMap(s => [...document.querySelectorAll(s)])
          .filter(el => {
              const r = el.getBoundingClientRect();
              return r.width > 0 && r.height > 0;  // visible only
          })
          .slice(0, 150)  // hard cap
          .map((el, i) => ({
              index: i,
              tag: el.tagName.toLowerCase(),
              type: el.type || null,
              text: (el.innerText || el.value || el.placeholder || '').trim().slice(0, 120),
              href: el.href || null,
              rect: el.getBoundingClientRect()
          }));
    }""")

Strategy 3 — CDP Accessibility tree

Playwright CDP session — full AXTree then filter to interactive
cdp = await page.context.new_cdp_session(page)
tree = await cdp.send("Accessibility.getFullAXTree")
# tree['nodes'] — list of dicts with role, name, nodeId, backendDOMNodeId
interactive_roles = {
    "button", "link", "textbox", "combobox", "checkbox",
    "radio", "menuitem", "tab", "listbox", "searchbox"
}
filtered = [
    n for n in tree["nodes"]
    if n.get("role", {}).get("value") in interactive_roles
    and not n.get("ignored", False)
]
# Typical result: 20–100 nodes, ~1.5 KB JSON — fits easily in context

Strategy 4 — Set-of-Marks (SOM) overlay

Draw numbered bounding boxes on the screenshot (using Canvas API or Pillow post-capture) for each interactive element. Pass the annotated image + the index→element mapping. Model says click(mark=12). Avoids raw co-ordinate hallucination. Adds ~200 ms overhead for overlay generation.

Browser-Use v0.2 implementation detail

Browser-Use combines strategies 2 and 4: it builds an interactive-element list (JavaScript enumeration), annotates the screenshot with coloured bounding boxes, and sends both the annotated image and the element list to the model. The model is prompted to reference elements by index. This is why Browser-Use consistently outperforms screenshot-only baselines on forms and multi-select widgets.

03

Screen Capture vs DOM-Only — When Each Wins

No single perception strategy dominates across all web tasks. The right choice depends on the page's structure and the nature of the task.

ScenarioBest strategyWhy
Simple web form (login, checkout) AXTree / element list only All needed info is in roles + labels; no visual parsing needed; 5× cheaper than screenshot
Data-heavy table / dashboard DOM extraction (innerText per cell) Tables render well as structured text; screenshot wastes tokens on cell borders and styling
CAPTCHA, image-based ARIA, CSS-only buttons Screenshot required AXTree shows role="img" name=""; only pixels reveal the visual content
Canvas-rendered application (Figma, Google Maps) Screenshot required DOM contains almost nothing useful; canvas content is entirely pixel-based
Single-page app with good ARIA semantics AXTree with screenshot fallback Modern SPA frameworks (React, Vue with aria-*) produce usable trees; screenshot for disambiguation
Legacy enterprise portal Screenshot + SOM overlay Broken/absent a11y tree; pixel co-ordinates are the only reliable anchor
Dynamic content: the hidden third option

For pages that update asynchronously (stock tickers, live chat, progress bars), neither a static screenshot nor a one-shot DOM extraction is sufficient. The correct pattern is a mutation observer: inject a small JS shim via page.evaluate that watches MutationObserver events and posts them to a queue. The agent polls the queue rather than re-capturing the full page, giving 50–80% reduction in per-step token cost for long-running observations.

Strategy applicability spectrum AXTree / DOM only forms, tables, SPA with ARIA Screenshot required canvas apps, legacy portals, captcha, image menus Hybrid (AXTree + screenshot) most real-world tasks — best accuracy, moderate cost
04

WebArena and Mind2Web Evals

Browser-focused evals differ from the full-desktop OSWorld in that they run against controlled web environments. This makes ground-truth verification easier but raises contamination concerns for any model trained on public web data.

WebArena live execution

812 tasks across five self-hosted web clones: Reddit (Postmill), GitLab, OneStopShop (e-commerce), CMS (Content Manager), Maps (OpenStreetMap clone).

Evaluation: rule-based functional checks — did the item get added to cart? did the GitLab issue get created with the right label? — augmented by string-match and regex verifiers.

2025 SOTA: ~35.8% (Claude 3.5 Sonnet + custom scaffolding). Human baseline: ~84%.

Setup: Docker Compose; five containers; ~12 GB download; requires manual seed scripts per task set. See web-arena-bench/webarena on GitHub.

Mind2Web offline replay

2000 tasks from 137 real websites across 31 domains. Tasks are recorded human trajectories; agents re-play against static snapshots, not live pages.

Evaluation: step-level action match (is the predicted element in the gold element set?) plus task success rate.

2025 SOTA: ~40% task success (Qwen-VL + fine-tune on Mind2Web train split). GPT-4V (zero-shot): ~18%.

Limitation: static snapshots mean the agent never needs to handle async page loads, infinite scroll, or dynamic state — easier than real browsing but useful for model development.

EvalLive execution?DatasetsVerifier typeFreshness risk
WebArenaYes (self-hosted)812 tasks, 5 app clonesRule-based + string matchLow — self-hosted clones don't change
Mind2WebNo (static replay)2000 tasks, 137 real sitesElement match at each stepMedium — snapshots from 2023 may lag real sites
VisualWebArenaYes910 tasks, adds image-grounding tasksRule-based + VQA judgeLow
WorkArenaYes (ServiceNow sandbox)33 000 tasksAPI-verified state changesLow
Why WebArena scores feel low

35% success on WebArena sounds poor but the tasks are genuinely hard: “find all open GitLab issues assigned to the user with the most closed issues, and change their username” requires chaining 6–10 navigation + form steps with zero error margin. A single wrong click fails the entire task. Score is task-success, not step-accuracy — step accuracy for the same model is typically 65–75%.

05

Captcha, Anti-Bot, Ethics

Browser agents inevitably collide with anti-bot defences. Understanding the technical layers helps both in mitigating them legitimately and in knowing when to refuse.

The anti-bot stack

Layer 1
TLS fingerprint (JA3 hash)
IP reputation / ASN block
HTTP/2 settings fingerprint (ALPN)
Layer 2
navigator.webdriver === true
missing browser plug-ins
CDP leak via window.cdc_* artefacts
headless Chrome user-agent
Layer 3
reCAPTCHA v3 behaviour score
Cloudflare Turnstile JS attestation
hCaptcha challenge
Layer 4
Mouse movement entropy analysis
Keystroke timing distribution
Scroll physics plausibility
Playwright stealth patches — playwright-stealth library
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async

async with async_playwright() as p:
    browser = await p.chromium.launch(
        headless=True,
        args=["--disable-blink-features=AutomationControlled"]
    )
    page = await browser.new_page()
    await stealth_async(page)   # patches navigator.webdriver, plugins, CDP artefacts

    # Add mouse-movement noise before clicks
    await page.mouse.move(x + random.gauss(0, 3),
                          y + random.gauss(0, 3))
    await asyncio.sleep(random.uniform(0.08, 0.35))
    await page.mouse.click(x, y)
Ethical boundary

Bypassing a site's terms of service with automation is a legal risk (Computer Fraud and Abuse Act in the US; Computer Misuse Act in the UK). The practical line: use browser agents to automate your own account's tasks. CAPTCHA-solving at scale for scraping or credential-stuffing crosses into illegal territory. Steel's residential proxies and TLS fingerprint spoofing are dual-use tools — the ethics lie in the intent and ToS, not the technique.

06

Stateful Sessions — Cookies, Auth, Persistence

Most non-trivial web tasks require authentication. The three main patterns for session persistence differ in security, portability, and implementation complexity.

Storage-state files

Playwright's context.storage_state(path="state.json") serialises cookies + localStorage + sessionStorage to JSON. Reload with browser.new_context(storage_state="state.json"). Simple; human-readable; works across sessions.

Risk: the JSON file contains live auth tokens. Treat as a secret; rotate regularly.

OAuth / PKCE flows

For services with OAuth 2.0 (Google, GitHub, Slack), have the agent complete the PKCE flow once manually, then persist only the refresh token. Re-mint access tokens programmatically; never screen-scrape the OAuth redirect.

Steel advantage: its persistent-profile feature stores cookies server-side, associated with a session ID — no local secret file needed.

Password manager integration

Inject credentials at agent launch via environment variables or a vault (HashiCorp Vault, AWS Secrets Manager). The agent calls page.fill("#username", os.environ["SITE_USER"]) directly rather than storing credentials in the browser profile.

Never pass credentials via the LLM context — they may appear in logs, traces, or be extracted by prompt injection.

Playwright — save and reload storage state
# --- Save after manual login ---
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://example.com/login")
# ... perform login manually or via the agent ...
await context.storage_state(path="example_session.json")
await context.close()

# --- Restore on next run ---
context = await browser.new_context(
    storage_state="example_session.json"
)
page = await context.new_page()
await page.goto("https://example.com/dashboard")
# Already authenticated; no login page shown
Multi-tab state

Browser-Use v0.2 supports explicit multi-tab management: open_tab(url), switch_tab(tab_id), close_tab(tab_id). This is essential for tasks that require opening a search result in a new tab while keeping the search results page for backtracking. Each tab shares the same browser context (and therefore the same cookies), but has its own navigation history and scroll position.

07

Pricing — Minutes Per Task, Infrastructure

Browser-agent costs have three components: LLM inference (per token), browser infrastructure (per minute), and operator overhead (retries, human-in-the-loop interrupts). Understanding each matters for cost-effective deployment.

ComponentTypical costNotes
LLM inference — GPT-4o screenshot mode ~$0.0019 / step (vision input)
~$0.006 / step (output ~200 tokens)
20-step task ≈ $0.16 in inference; screenshot + action output
LLM inference — Claude 3.5 Sonnet ~$0.0038 / step (vision input)
~$0.015 / step (output ~200 tokens)
Haiku 3.5 cuts input 10× to ~$0.0004/step; lower accuracy
Steel remote browser ~$0.10 / minute (2025 pricing) 20-step task takes ~3–8 min ≈ $0.30–$0.80 in browser time
Self-hosted Playwright (AWS EC2 t3.medium) ~$0.0416 / hour ≈ $0.0007 / minute 100× cheaper than managed — but you pay for ops, proxy infra, CAPTCHA handling
Residential proxy (Bright Data / Oxylabs) $8–$15 / GB traffic Most tasks use <5 MB of traffic — negligible; relevant only for data-scraping scale
Real-world task economics (2025 estimates)

Simple form fill (5 steps, AXTree only, GPT-4o mini): ~$0.03 total. Flight search & booking (30 steps, Claude 3.5 Sonnet): ~$0.80–$1.50 self-hosted, ~$2.50–$4.00 on Steel. Competitive intelligence run (100 steps, GPT-4o + proxy): ~$3–$8. Infrastructure cost dominates at scale; model cost dominates for infrequent single tasks.

08

Failure Modes & Recovery

Browser agents fail in characteristic ways. Understanding the failure taxonomy is necessary before designing a recovery strategy.

Failure modeCauseDetectionRecovery
Stale screenshot Page load in progress; spinner visible document.readyState check; AXTree loading indicators Wait for networkidle before capturing; retry
Grounding hallucination Model invents a UI element; click misses page.locator(sel).count() before click Fall back to screenshot + SOM; re-plan with fresh perception
Infinite scroll loop Model scrolls without recognising page end Hash last N screenshots; detect stable state Check scrollHeight; break when position static
Auth session expiry Cookie TTL expires; redirect to login Monitor URL changes; detect /login pattern Re-authenticate from vault; resume from checkpoint
Rate limiting / 429 Too many requests; server throttles page.on("response") status monitor Exponential back-off with jitter; respect Retry-After
Prompt injection Malicious page text hijacks agent goals Secondary classifier on page text Instruction hierarchy in system prompt; human gate before irreversible actions
Playwright — checkpoint-based recovery loop pattern
async def run_with_recovery(task, max_steps=40, max_retries=3):
    checkpoint = None   # URL + storage state at last verified good state
    for attempt in range(max_retries):
        try:
            context = await make_context(checkpoint)
            result = await run_agent(task, context, max_steps)
            if result.success:
                return result
            # Save checkpoint at last successful sub-goal
            checkpoint = await save_checkpoint(context)
        except PlaywrightTimeoutError:
            await asyncio.sleep(2 ** attempt)   # back-off
        finally:
            await context.close()
    raise AgentExhausted(f"Task failed after {max_retries} attempts")
The most important recovery rule

Never retry a write action (form submit, purchase, message send) without confirming the previous attempt failed. Network retries on idempotent GETs are safe; retrying a POST that may have succeeded silently causes duplicates. Check the resulting page state explicitly before concluding an action failed.

09

What to Take Away

Series complete

Deck 01 covered the architecture landscape: Anthropic Computer Use, OpenAI Operator, Gemini Mariner, action schemas, sandboxing, and full-desktop evals. This deck covered the browser-native layer in depth. Together they give you the vocabulary and engineering context to evaluate, deploy, and extend production computer-use systems.