[
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@2.1.1/schema.json\",\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"access\": \"public\",\n  \"changelog\": [\n    \"@changesets/changelog-github\",\n    {\n      \"repo\": \"browserbase/stagehand\"\n    }\n  ],\n  \"snapshot\": {\n    \"useCalculatedVersion\": true,\n    \"prereleaseTemplate\": \"alpha-{commit}\",\n    \"tag\": \"alpha\"\n  }\n}\n"
  },
  {
    "path": ".changeset/crazy-nights-prove.md",
    "content": "---\n\"@browserbasehq/stagehand\": patch\n---\n\napply user defined toolTimeout to all agent tools (other than wait & think tools)\n"
  },
  {
    "path": ".cursorrules",
    "content": "# Stagehand Project\n\nThis is a project that uses Stagehand V3, a browser automation framework with AI-powered `act`, `extract`, `observe`, and `agent` methods.\n\nThe main class can be imported as `Stagehand` from `@browserbasehq/stagehand`.\n\n**Key Classes:**\n\n- `Stagehand`: Main orchestrator class providing `act`, `extract`, `observe`, and `agent` methods\n- `context`: A `V3Context` object that manages browser contexts and pages\n- `page`: Individual page objects accessed via `stagehand.context.pages()[i]` or created with `stagehand.context.newPage()`\n\n## Initialize\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\", // or \"BROWSERBASE\"\n  verbose: 2, // 0, 1, or 2\n  model: \"openai/gpt-4.1-mini\", // or any supported model\n});\n\nawait stagehand.init();\n\n// Access the browser context and pages\nconst page = stagehand.context.pages()[0];\nconst context = stagehand.context;\n\n// Create new pages if needed\nconst page2 = await stagehand.context.newPage();\n```\n\n## Act\n\nActions are called on the `stagehand` instance (not the page). Use atomic, specific instructions:\n\n```typescript\n// Act on the current active page\nawait stagehand.act(\"click the sign in button\");\n\n// Act on a specific page (when you need to target a page that isn't currently active)\nawait stagehand.act(\"click the sign in button\", { page: page2 });\n```\n\n**Important:** Act instructions should be atomic and specific:\n\n- ✅ Good: \"Click the sign in button\" or \"Type 'hello' into the search input\"\n- ❌ Bad: \"Order me pizza\" or \"Type in the search bar and hit enter\" (multi-step)\n\n### Observe + Act Pattern (Recommended)\n\nCache the results of `observe` to avoid unexpected DOM changes:\n\n```typescript\nconst instruction = \"Click the sign in button\";\n\n// Get candidate actions\nconst actions = await stagehand.observe(instruction);\n\n// Execute the first action\nawait stagehand.act(actions[0]);\n```\n\nTo target a specific page:\n\n```typescript\nconst actions = await stagehand.observe(\"select blue as the favorite color\", {\n  page: page2,\n});\nawait stagehand.act(actions[0], { page: page2 });\n```\n\n## Extract\n\nExtract data from pages using natural language instructions. The `extract` method is called on the `stagehand` instance.\n\n### Basic Extraction (with schema)\n\n```typescript\nimport { z } from \"zod\";\n\n// Extract with explicit schema\nconst data = await stagehand.extract(\n  \"extract all apartment listings with prices and addresses\",\n  z.object({\n    listings: z.array(\n      z.object({\n        price: z.string(),\n        address: z.string(),\n      }),\n    ),\n  }),\n);\n\nconsole.log(data.listings);\n```\n\n### Simple Extraction (without schema)\n\n```typescript\n// Extract returns a default object with 'extraction' field\nconst result = await stagehand.extract(\"extract the sign in button text\");\n\nconsole.log(result);\n// Output: { extraction: \"Sign in\" }\n\n// Or destructure directly\nconst { extraction } = await stagehand.extract(\n  \"extract the sign in button text\",\n);\nconsole.log(extraction); // \"Sign in\"\n```\n\n### Targeted Extraction\n\nExtract data from a specific element using a selector:\n\n```typescript\nconst reason = await stagehand.extract(\n  \"extract the reason why script injection fails\",\n  z.string(),\n  { selector: \"/html/body/div[2]/div[3]/iframe/html/body/p[2]\" },\n);\n```\n\n### URL Extraction\n\nWhen extracting links or URLs, use `z.string().url()`:\n\n```typescript\nconst { links } = await stagehand.extract(\n  \"extract all navigation links\",\n  z.object({\n    links: z.array(z.string().url()),\n  }),\n);\n```\n\n### Extracting from a Specific Page\n\n```typescript\n// Extract from a specific page (when you need to target a page that isn't currently active)\nconst data = await stagehand.extract(\n  \"extract the placeholder text on the name field\",\n  { page: page2 },\n);\n```\n\n## Observe\n\nPlan actions before executing them. Returns an array of candidate actions:\n\n```typescript\n// Get candidate actions on the current active page\nconst [action] = await stagehand.observe(\"Click the sign in button\");\n\n// Execute the action\nawait stagehand.act(action);\n```\n\nObserving on a specific page:\n\n```typescript\n// Target a specific page (when you need to target a page that isn't currently active)\nconst actions = await stagehand.observe(\"find the next page button\", {\n  page: page2,\n});\nawait stagehand.act(actions[0], { page: page2 });\n```\n\n## Agent\n\nUse the `agent` method to autonomously execute complex, multi-step tasks.\n\n### Basic Agent Usage\n\n```typescript\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://www.google.com\");\n\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.0-flash\",\n  executionModel: \"google/gemini-2.0-flash\",\n});\n\nconst result = await agent.execute({\n  instruction: \"Search for the stock price of NVDA\",\n  maxSteps: 20,\n});\n\nconsole.log(result.message);\n```\n\n### Computer Use Agent (CUA)\n\nFor more advanced scenarios using computer-use models:\n\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\", // Enable Computer Use Agent mode\n  model: \"anthropic/claude-sonnet-4-20250514\",\n  // or \"google/gemini-2.5-computer-use-preview-10-2025\"\n  systemPrompt: `You are a helpful assistant that can use a web browser.\n    Do not ask follow up questions, the user will trust your judgement.`,\n});\n\nawait agent.execute({\n  instruction: \"Apply for a library card at the San Francisco Public Library\",\n  maxSteps: 30,\n});\n```\n\n### Agent with Custom Model Configuration\n\n```typescript\nconst agent = stagehand.agent({\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GEMINI_API_KEY,\n  },\n  systemPrompt: `You are a helpful assistant.`,\n});\n```\n\n### Agent with Integrations (MCP/External Tools)\n\n```typescript\nconst agent = stagehand.agent({\n  integrations: [`https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`],\n  systemPrompt: `You have access to the Exa search tool.`,\n});\n```\n\n## Advanced Features\n\n### DeepLocator (XPath Targeting)\n\nTarget specific elements across shadow DOM and iframes:\n\n```typescript\nawait page\n  .deepLocator(\"/html/body/div[2]/div[3]/iframe/html/body/p\")\n  .highlight({\n    durationMs: 5000,\n    contentColor: { r: 255, g: 0, b: 0 },\n  });\n```\n\n### Multi-Page Workflows\n\n```typescript\nconst page1 = stagehand.context.pages()[0];\nawait page1.goto(\"https://example.com\");\n\nconst page2 = await stagehand.context.newPage();\nawait page2.goto(\"https://example2.com\");\n\n// Act/extract/observe operate on the current active page by default\n// Pass { page } option to target a specific page\nawait stagehand.act(\"click button\", { page: page1 });\nawait stagehand.extract(\"get title\", { page: page2 });\n```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Detailed descriptions help us resolve faster\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Before submitting an issue, please:**\n\n- [ ]  Check the [documentation](https://docs.stagehand.dev/) for relevant information\n- [ ]  Search existing [issues](https://github.com/browserbase/stagehand/issues) to avoid duplicates\n\n## Environment Information\n\nPlease provide the following information to help us reproduce and resolve your issue:\n\n**Stagehand:**\n\n- Language/SDK: [TypeScript, Python, MCP…]\n- Stagehand version: [e.g., 1.0.0]\n\n**AI Provider:**\n\n- Provider: [e.g., OpenAI, Anthropic, Azure OpenAI]\n- Model: [e.g., gpt-4o, claude-sonnet-4-6]\n\n## Issue Description\n\n```\n[Describe the current behavior here]\n\n```\n\n### Steps to Reproduce\n\n1. \n2. \n3. \n\n### Minimal Reproduction Code\n\n```tsx\n// Your minimal reproduction code here\nimport { Stagehand } from '@browserbase/stagehand';\n\nconst stagehand = new Stagehand({\n  // IMPORTANT: include your stagehand config\n});\n\n// Steps that reproduce the issue\n\n```\n\n### Error Messages / Log trace\n\n```\n[Paste error messages/logs here]\n\n```\n\n### Screenshots / Videos\n\n```\n[Attach screenshots or videos here]\n\n```\n\n### Related Issues\n\nAre there any related issues or PRs?\n\n- Related to: #[issue number]\n- Duplicate of: #[issue number]\n- Blocks: #[issue number]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Are you willing to contribute to implementing this feature or fix?**\n\n- [ ]  Yes, I can submit a PR\n- [ ]  Yes, but I need guidance\n- [ ]  No, I cannot contribute at this time\n"
  },
  {
    "path": ".github/actions/select-browserbase-region/action.yml",
    "content": "name: Select Browserbase region\ndescription: Select a Browserbase region based on a weighted distribution.\ninputs:\n  distribution:\n    description: Comma-separated region=weight list (e.g. us-west-2=40,us-east-1=20).\n    required: true\noutputs:\n  region:\n    description: Selected region.\n    value: ${{ steps.select.outputs.region }}\nruns:\n  using: composite\n  steps:\n    - id: select\n      shell: bash\n      run: |\n        dist=\"${{ inputs.distribution }}\"\n        if [ -z \"$dist\" ]; then\n          echo \"BROWSERBASE_REGION_DISTRIBUTION is empty\"\n          exit 1\n        fi\n        IFS=',' read -r -a entries <<< \"$dist\"\n        total=0\n        regions=()\n        weights=()\n        for entry in \"${entries[@]}\"; do\n          region=\"${entry%%=*}\"\n          weight=\"${entry#*=}\"\n          region=\"$(printf '%s' \"$region\" | tr -d '[:space:]')\"\n          weight=\"$(printf '%s' \"$weight\" | tr -d '[:space:]')\"\n          if [ -z \"$region\" ] || [ -z \"$weight\" ]; then\n            echo \"Invalid region distribution entry: $entry\"\n            exit 1\n          fi\n          if ! [[ \"$region\" =~ ^[A-Za-z0-9-]+$ ]]; then\n            echo \"Invalid region value: $region\"\n            exit 1\n          fi\n          if ! [[ \"$weight\" =~ ^[0-9]+$ ]]; then\n            echo \"Invalid weight for region $region: $weight\"\n            exit 1\n          fi\n          regions+=(\"$region\")\n          weights+=(\"$weight\")\n          total=$((total + weight))\n        done\n        if [ \"$total\" -le 0 ]; then\n          echo \"Invalid total weight: $total\"\n          exit 1\n        fi\n        roll=$((RANDOM % total))\n        cumulative=0\n        chosen=\"\"\n        for i in \"${!regions[@]}\"; do\n          cumulative=$((cumulative + weights[i]))\n          if [ \"$roll\" -lt \"$cumulative\" ]; then\n            chosen=\"${regions[i]}\"\n            break\n          fi\n        done\n        if [ -z \"$chosen\" ]; then\n          echo \"Failed to choose Browserbase region\"\n          exit 1\n        fi\n        echo \"Selected Browserbase region: $chosen\"\n        echo \"region=$chosen\" >> \"$GITHUB_OUTPUT\"\n        echo \"BROWSERBASE_REGION=$chosen\" >> \"$GITHUB_ENV\"\n"
  },
  {
    "path": ".github/actions/setup-node-pnpm-turbo/action.yml",
    "content": "name: Setup Node, pnpm, and Turbo cache\ndescription: Configure pnpm and Node.js with caching, restore Turbo cache, and install dependencies.\ninputs:\n  node-version:\n    description: Node.js version to use.\n    required: false\n    default: \"20.x\"\n  use-prebuilt-artifacts:\n    description: Whether to download pre-built package from build artifacts.\n    required: false\n    default: \"true\"\n  restore-turbo-cache:\n    description: Whether to restore the local .turbo cache.\n    required: false\n    default: \"true\"\n\nruns:\n  using: composite\n  steps:\n    - uses: pnpm/action-setup@v4\n\n    - name: Set up Node.js\n      uses: actions/setup-node@v6\n      with:\n        node-version: ${{ inputs.node-version }}\n        cache: 'pnpm'\n        cache-dependency-path: '**/pnpm-lock.yaml'\n\n    - name: Restore Turbo cache\n      if: ${{ inputs.restore-turbo-cache == 'true' }}\n      uses: actions/cache/restore@v4\n      with:\n        path: .turbo\n        key: ${{ runner.os }}-turbo-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'package.json', 'turbo.json') }}-${{ github.sha }}\n        restore-keys: |\n          ${{ runner.os }}-turbo-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'package.json', 'turbo.json') }}-\n\n    - name: Install dependencies\n      shell: bash\n      run: pnpm install --frozen-lockfile --prefer-offline\n  \n    - name: Download build artifacts\n      if: ${{ inputs.use-prebuilt-artifacts == 'true' }}\n      uses: actions/download-artifact@v4\n      with:\n        name: build-artifacts\n        path: .\n        merge-multiple: true\n\n    - name: Prepare test output directories\n      shell: bash\n      run: |\n        mkdir -p \"${GITHUB_WORKSPACE}/ctrf\"\n        if [ -n \"${NODE_V8_COVERAGE:-}\" ]; then\n          mkdir -p \"$NODE_V8_COVERAGE\"\n        fi\n"
  },
  {
    "path": ".github/actions/upload-ctrf-report/action.yml",
    "content": "name: Upload CTRF report\ndescription: Upload CTRF report artifact.\ninputs:\n  name:\n    description: Report path (used as artifact name when sanitized).\n    required: true\n  path:\n    description: Optional explicit path (defaults to name).\n    required: false\n    default: \"\"\n\nruns:\n  using: composite\n  steps:\n    - name: Normalize inputs\n      id: normalize\n      shell: bash\n      run: |\n        name=\"${{ inputs.name }}\"\n        echo \"name=${name//\\//-}\" >> \"$GITHUB_OUTPUT\"\n        if [ -n \"${{ inputs.path }}\" ]; then\n          echo \"path=${{ inputs.path }}\" >> \"$GITHUB_OUTPUT\"\n        else\n          echo \"path=${{ inputs.name }}\" >> \"$GITHUB_OUTPUT\"\n        fi\n\n    - name: Upload CTRF report artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: ${{ steps.normalize.outputs.name }}\n        # package.json anchors uploaded paths to the repository root.\n        path: |\n          package.json\n          ${{ steps.normalize.outputs.path }}\n"
  },
  {
    "path": ".github/actions/upload-v8-coverage/action.yml",
    "content": "name: Upload V8 coverage\ndescription: Upload V8 coverage artifacts.\ninputs:\n  name:\n    description: Artifact name.\n    required: true\n  path:\n    description: Coverage path to upload (defaults to name).\n    required: false\n    default: \"\"\n\nruns:\n  using: composite\n  steps:\n    - name: Normalize artifact name\n      id: normalize\n      shell: bash\n      run: |\n        name=\"${{ inputs.name }}\"\n        echo \"name=${name//\\//-}\" >> \"$GITHUB_OUTPUT\"\n        if [ -n \"${{ inputs.path }}\" ]; then\n          echo \"path=${{ inputs.path }}\" >> \"$GITHUB_OUTPUT\"\n        else\n          echo \"path=${{ inputs.name }}\" >> \"$GITHUB_OUTPUT\"\n        fi\n\n    - name: Upload coverage artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: ${{ steps.normalize.outputs.name }}\n        # package.json anchors uploaded paths to the repository root.\n        path: |\n          package.json\n          ${{ steps.normalize.outputs.path }}\n"
  },
  {
    "path": ".github/actions/verify-chromium-launch/action.yml",
    "content": "name: Verify Chromium launch\ndescription: Validate that Chromium can start, connect to CDP, and read the page title.\ninputs:\n  chrome-path:\n    description: Path to Chromium/Chrome binary.\n    required: false\n    default: \"/usr/bin/chromium\"\n  max-attempts:\n    description: Number of launch attempts before failing.\n    required: false\n    default: \"3\"\n  timeout-ms:\n    description: Milliseconds to wait for DevTools and CDP per attempt.\n    required: false\n    default: \"30000\"\nruns:\n  using: composite\n  steps:\n    - shell: bash\n      run: |\n        set -euo pipefail\n        max_attempts=\"${{ inputs.max-attempts }}\"\n        attempt=1\n        while [ \"$attempt\" -le \"$max_attempts\" ]; do\n          if [ -n \"${{ inputs.chrome-path }}\" ]; then\n            pkill -f \"${{ inputs.chrome-path }}\" >/dev/null 2>&1 || true\n          fi\n          if node - <<'NODE'\n        const { spawn } = require(\"node:child_process\");\n        const workspace = process.env.GITHUB_WORKSPACE;\n        if (workspace) {\n          process.chdir(workspace);\n        }\n\n        const chrome = \"${{ inputs.chrome-path }}\";\n\n        const timeoutMs = Number(\"${{ inputs.timeout-ms }}\");\n        const wsPrefix = \"DevTools listening on \";\n        const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\n        let proc;\n        let wsUrl;\n\n        const waitForWsUrl = async () => {\n          const deadline = Date.now() + timeoutMs;\n          while (!wsUrl) {\n            if (Date.now() > deadline) {\n              throw new Error(\n                `❌ Chromium did not expose CDP WS URL within timeout (${timeoutMs}ms)`,\n              );\n            }\n            await sleep(250);\n          }\n          return wsUrl;\n        };\n\n        const cleanup = () => {\n          if (proc && !proc.killed) {\n            proc.kill(\"SIGKILL\");\n          }\n        };\n\n        (async () => {\n          try {\n            const startTime = Date.now();\n            const args = [\n              '--ash-no-nudges',\n              '--block-new-web-contents',\n              '--deny-permission-prompts',\n              '--disable-breakpad',\n              '--disable-client-side-phishing-detection',\n              '--disable-component-update',\n              '--disable-components=AcceptCHFrame,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold,InterestFeedContentSuggestions,CalculateNativeWinOcclusion,BackForwardCache,HeavyAdPrivacyMitigations,LazyFrameLoading,ImprovedCookieControls,PrivacySandboxSettings4,AutofillServerCommunication,CertificateTransparencyComponentUpdater,DestroyProfileOnBrowserClose,CrashReporting,OverscrollHistoryNavigation,InfiniteSessionRestore',\n              '--disable-datasaver-prompt',\n              '--disable-default-apps',\n              '--disable-desktop-notifications',\n              '--disable-domain-reliability',\n              '--disable-external-intent-requests',\n              '--disable-hang-monitor',\n              '--disable-infobars',\n              '--disable-notifications',\n              '--disable-popup-blocking',\n              '--disable-print-preview',\n              '--disable-prompt-on-repost',\n              '--disable-search-engine-choice-screen',\n              '--disable-session-crashed-bubble',\n              '--disable-speech-api',\n              '--disable-speech-synthesis-api',\n              '--hide-crash-restore-bubble',\n              '--metrics-recording-only',\n              '--no-default-browser-check',\n              '--no-first-run',\n              '--no-pings',\n              '--noerrdialogs',\n              '--safebrowsing-disable-auto-update',\n              '--silent-debugger-extension-api',\n              '--simulate-outdated-no-au=\"Tue, 31 Dec 2099 23:59:59 GMT\"',\n              '--suppress-message-center-popups',\n              \"--disable-background-networking\",\n              \"--disable-default-apps\",\n              \"--disable-dev-shm-usage\",\n              \"--disable-extensions\",\n              \"--disable-notifications\",\n              \"--disable-setuid-sandbox\",\n              \"--disable-site-isolation-trials\",\n              \"--disable-sync\",\n              \"--disable-web-security\",\n              \"--headless=new\",\n              \"--no-default-browser-check\",\n              \"--no-first-run\",\n              \"--no-sandbox\",\n              \"--no-zygote\",\n              \"--password-store=basic\",\n              \"--remote-debugging-port=0\",\n              \"--test-type=gpu\",\n              \"--use-mock-keychain\",\n              \"about:blank\",\n            ];\n            proc = spawn(chrome, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n            const lineBuffers = { stdout: \"\", stderr: \"\" };\n            const onData = (stream) => (data) => {\n              const text = data.toString();\n              if (stream === \"stderr\") {\n                process.stderr.write(text);\n              } else {\n                process.stdout.write(text);\n              }\n              lineBuffers[stream] += text;\n              const lines = lineBuffers[stream].split(/\\r?\\n/);\n              lineBuffers[stream] = lines.pop() ?? \"\";\n              for (const line of lines) {\n                const idx = line.indexOf(wsPrefix);\n                if (idx === -1) continue;\n                const rest = line.slice(idx + wsPrefix.length).trim();\n                const candidate = rest.split(/\\s+/)[0];\n                if (\n                  candidate.startsWith(\"ws://\") ||\n                  candidate.startsWith(\"wss://\")\n                ) {\n                  wsUrl = candidate;\n                }\n              }\n            };\n            proc.stdout.on(\"data\", onData(\"stdout\"));\n            proc.stderr.on(\"data\", onData(\"stderr\"));\n\n            const url = await waitForWsUrl();\n            const wsFoundMs = Date.now() - startTime;\n            const wsFoundSec = (wsFoundMs / 1000).toFixed(2);\n            const connectStart = Date.now();\n            const path = require(\"node:path\");\n            const workspaceRoot = process.env.GITHUB_WORKSPACE || process.cwd();\n            const playwrightPath = path.join(\n              workspaceRoot,\n              \"packages/core/node_modules/playwright\",\n            );\n            console.log(\n              `✅ CDP Url found after ${wsFoundSec}s, connecting with playwright...`,\n            );\n            const { chromium } = require(playwrightPath);\n            const browser = await chromium.connectOverCDP(url, {\n              timeout: timeoutMs,\n            });\n            const context = browser.contexts()[0];\n            if (!context) {\n              throw new Error(\"❌ No browser context available after CDP connect\");\n            }\n            const page = context.pages()[0];\n            if (!page) {\n              throw new Error(\"❌ No page available after CDP connect\");\n            }\n            const remainingMs = timeoutMs - (Date.now() - connectStart);\n            if (remainingMs <= 0) {\n              throw new Error(\n                `❌ CDP connect + verify timed out after ${timeoutMs}ms`,\n              );\n            }\n            const sum = await Promise.race([\n              page.evaluate(\"1 + 1\"),\n              new Promise((_, reject) =>\n                setTimeout(\n                  () =>\n                    reject(\n                      new Error(\n                        `❌ CDP connect + verify timed out after ${timeoutMs}ms`,\n                      ),\n                    ),\n                  remainingMs,\n                ),\n              ),\n            ]);\n            if (sum !== 2) {\n              throw new Error(`❌ Unexpected eval result: ${sum}`);\n            }\n            const totalMs = Date.now() - startTime;\n            const connectMs = Date.now() - connectStart;\n            const totalSec = (totalMs / 1000).toFixed(2);\n            const connectSec = (connectMs / 1000).toFixed(2);\n            console.log(\n              `✅ Chromium launched in ${wsFoundSec}s and CDP connected in ${connectSec}s (total: ${totalSec}s)`,\n            );\n            await browser.close();\n            cleanup();\n            process.exit(0);\n          } catch (err) {\n            cleanup();\n            console.error(err instanceof Error ? err.message : String(err));\n            process.exit(1);\n          }\n        })();\n        NODE\n          then\n            if [ \"$attempt\" -gt 1 ]; then\n              echo \"⚠️ Chromium launch succeeded after ${attempt} attempts; GitHub Actions runner may be constrained.\"\n            fi\n            exit 0\n          fi\n          echo \"⚠️ Chromium launch attempt ${attempt} failed.\"\n          attempt=$((attempt + 1))\n          sleep 2\n        done\n        echo \"❌ Failed to launch Chromium before running Stagehand; GitHub Actions runner is likely overloaded.\"\n        exit 1\n"
  },
  {
    "path": ".github/pull_request_template",
    "content": "# why\n\n# what changed\n\n# test plan\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Tests\n\non:\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - labeled\n      - unlabeled\n    paths-ignore:\n      - \"packages/docs/**\"\n\npermissions:\n  contents: read\n  actions: write\n\nenv:\n  BROWSERBASE_FLOW_LOGS: \"1\"\n  LLM_MAX_MS: \"15000\"\n  EVAL_MODELS: \"openai/gpt-4.1,google/gemini-2.0-flash,anthropic/claude-haiku-4-5\"\n  EVAL_AGENT_MODELS: \"computer-use-preview-2025-03-11,claude-sonnet-4-6\"\n  EVAL_CATEGORIES: \"observe,act,combination,extract,targeted_extract,agent\"\n  EVAL_MAX_CONCURRENCY: 25\n  EVAL_TRIAL_COUNT: 3\n  LOCAL_SESSION_LIMIT_PER_E2E_TEST: 2\n  BROWSERBASE_SESSION_LIMIT_PER_E2E_TEST: 3\n  BROWSERBASE_REGION_DISTRIBUTION: \"us-west-2=30,us-east-1=30,eu-central-1=20,ap-southeast-1=20\"  # percentage of load for each region when running e2e tests against prod\n  CHROME_PATH: /usr/bin/chromium # GitHub Actions runners ship with stable Chromium by default\n  BROWSERBASE_CDP_CONNECT_MAX_MS: \"10000\"\n  BROWSERBASE_SESSION_CREATE_MAX_MS: \"60000\"\n  PUPPETEER_SKIP_DOWNLOAD: \"1\"\n  PLAYWRIGHT_SKIP_DOWNLOAD: \"1\"\n  TURBO_TELEMETRY_DISABLED: \"1\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  determine-changes:\n    runs-on: ubuntu-latest\n    outputs:\n      core: ${{ steps.filter.outputs.core }}\n      cli: ${{ steps.filter.outputs.cli }}\n      evals: ${{ steps.filter.outputs.evals }}\n      server: ${{ steps.filter.outputs.server }}\n      docs-only: ${{ steps.filter.outputs.docs-only }}\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - name: Log GitHub API rate limit\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          headers_file=$(mktemp)\n          body_file=$(mktemp)\n          curl -sSL \\\n            -D \"$headers_file\" \\\n            -o \"$body_file\" \\\n            -H \"Accept: application/vnd.github+json\" \\\n            -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n            -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n            https://api.github.com/rate_limit\n          cat \"$headers_file\"\n          echo \"\"\n          cat \"$body_file\"\n          remaining=$(jq -r '.rate.remaining' \"$body_file\")\n          if [ \"$remaining\" -eq 0 ]; then\n            reset_epoch=$(jq -r '.rate.reset' \"$body_file\")\n            reset_utc=$(date -u -d \"@$reset_epoch\" +\"%Y-%m-%d %H:%M:%S\")\n            reset_pacific=$(TZ=America/Los_Angeles date -d \"@$reset_epoch\" +\"%Y-%m-%d %H:%M:%S %Z\")\n            echo \"Github API rate limited until: ${reset_pacific} (${reset_utc} UTC)\" >> \"$GITHUB_STEP_SUMMARY\"\n            echo \"GitHub API rate limit exhausted.\"\n            exit 1\n          fi\n\n      - uses: dorny/paths-filter@v3\n        id: filter\n        with:\n          filters: |\n            core:\n              - '.github/workflows/ci.yml'\n              - 'packages/core/**'\n              - 'package.json'\n              - 'pnpm-lock.yaml'\n              - 'turbo.json'\n            cli:\n              - 'packages/cli/**'\n              - 'packages/core/**'\n              - 'package.json'\n              - 'pnpm-lock.yaml'\n            evals:\n              - 'packages/evals/**'\n              - 'package.json'\n              - 'pnpm-lock.yaml'\n            server:\n              - 'packages/server-v3/**'\n              - 'packages/server-v4/**'\n              - 'packages/core/**'\n              - 'package.json'\n              - 'pnpm-lock.yaml'\n              - 'pnpm-workspace.yaml'\n              - '.github/workflows/ci.yml'\n            docs-only:\n              - '**/*.md'\n              - 'examples/**'\n              - '!packages/**/*.md'\n\n  determine-evals:\n    needs: [determine-changes]\n    runs-on: ubuntu-latest\n    outputs:\n      skip-all-evals: ${{ steps.check-labels.outputs.skip-all-evals }}\n      eval-categories: ${{ steps.check-labels.outputs.eval-categories }}\n    steps:\n      - id: check-labels\n        run: |\n          categories=()\n          declare -A seen\n          add_category() {\n            local category=\"$1\"\n            if [[ -z \"${seen[$category]:-}\" ]]; then\n              categories+=(\"$category\")\n              seen[\"$category\"]=1\n            fi\n          }\n\n          emit_categories() {\n            local json=\"[\"\n            for category in \"${categories[@]}\"; do\n              json+=\"\\\"${category}\\\",\"\n            done\n            json=\"${json%,}\"\n            json+=\"]\"\n            echo \"eval-categories=$json\" >> $GITHUB_OUTPUT\n          }\n\n          # Check if skip-evals label is present\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'skip-evals') }}\" == \"true\" ]]; then\n            echo \"skip-evals label found - skipping all evals\"\n            echo \"skip-all-evals=true\" >> $GITHUB_OUTPUT\n            emit_categories\n            exit 0\n          fi\n\n          # Skip evals if only docs/examples changed\n          if [[ \"${{ needs.determine-changes.outputs.docs-only }}\" == \"true\" && \"${{ needs.determine-changes.outputs.core }}\" == \"false\" && \"${{ needs.determine-changes.outputs.evals }}\" == \"false\" ]]; then\n            echo \"Only docs/examples changed - skipping evals\"\n            echo \"skip-all-evals=true\" >> $GITHUB_OUTPUT\n            emit_categories\n            exit 0\n          fi\n\n          # Check for skip-regression-evals label\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'skip-regression-evals') }}\" == \"true\" ]]; then\n            echo \"skip-regression-evals label found - regression evals will be skipped\"\n          else\n            echo \"Regression evals will run by default\"\n            add_category \"regression\"\n          fi\n\n          # Check for specific labels\n          echo \"skip-all-evals=false\" >> $GITHUB_OUTPUT\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'combination') }}\" == \"true\" ]]; then\n            add_category \"combination\"\n          fi\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'extract') }}\" == \"true\" ]]; then\n            add_category \"extract\"\n          fi\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'act') }}\" == \"true\" ]]; then\n            add_category \"act\"\n          fi\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'observe') }}\" == \"true\" ]]; then\n            add_category \"observe\"\n          fi\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'targeted-extract') }}\" == \"true\" ]]; then\n            add_category \"targeted_extract\"\n          fi\n          if [[ \"${{ contains(github.event.pull_request.labels.*.name, 'agent') }}\" == \"true\" ]]; then\n            add_category \"agent\"\n          fi\n          emit_categories\n      \n  run-lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    needs: [run-build]\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n          node-version: 20.x\n\n      - name: Run Lint\n        run: pnpm exec turbo run lint\n\n  cancel-after-lint-failure:\n    name: Cancel after lint failure\n    runs-on: ubuntu-latest\n    needs: [run-lint]\n    if: ${{ always() && needs.run-lint.result == 'failure' }}\n    continue-on-error: true\n    steps:\n      - name: Cancel workflow run\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          curl -sSfL -X POST \\\n            -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n            -H \"Accept: application/vnd.github+json\" \\\n            -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n            \"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/cancel\"\n\n  run-build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"false\"\n          node-version: 20.x\n\n      - name: Run Build\n        run: pnpm exec turbo run build\n\n      - name: Save Turbo cache\n        if: always()\n        uses: actions/cache/save@v4\n        with:\n          path: .turbo\n          key: ${{ runner.os }}-turbo-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'package.json', 'turbo.json') }}-${{ github.sha }}\n\n      - name: Upload build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: build-artifacts\n          include-hidden-files: true\n          # package.json is included to anchor artifact paths at repo root.\n          path: |\n            package.json\n            packages/core/dist/**\n            packages/core/lib/version.ts\n            packages/core/lib/dom/build/**\n            packages/core/lib/v3/dom/build/**\n            packages/cli/dist/**\n            packages/evals/dist/**\n            packages/server-v3/dist/**\n            packages/server-v3/openapi.v3.yaml\n            packages/server-v4/dist/**\n            packages/server-v4/openapi.v4.yaml\n          retention-days: 1\n\n  run-cli-tests:\n    name: CLI Tests\n    runs-on: ubuntu-latest\n    needs: [run-build, determine-changes]\n    if: needs.determine-changes.outputs.cli == 'true'\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - name: Run CLI Tests\n        run: pnpm exec turbo run test:cli --filter=@browserbasehq/browse-cli\n\n  discover-core-tests:\n    runs-on: ubuntu-latest\n    needs: [determine-changes]\n    if: needs.determine-changes.outputs.core == 'true'\n    outputs:\n      core-tests: ${{ steps.set-matrix.outputs.core-tests }}\n      has-core-tests: ${{ steps.set-matrix.outputs.has-core-tests }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"false\"\n          restore-turbo-cache: \"false\"\n\n      - name: Discover core test files\n        id: set-matrix\n        run: |\n          core_json=$(pnpm --filter @browserbasehq/stagehand --silent run test:core -- --list)\n          echo \"core-tests=$core_json\" >> $GITHUB_OUTPUT\n\n          if [ \"$core_json\" = \"[]\" ]; then\n            echo \"has-core-tests=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"has-core-tests=true\" >> $GITHUB_OUTPUT\n          fi\n\n          echo \"Found core tests: $core_json\"\n\n  core-unit-tests:\n    name: core/${{ matrix.test.name }}\n    runs-on: ubuntu-latest\n    needs: [run-build, discover-core-tests]\n    if: needs.discover-core-tests.outputs.has-core-tests == 'true'\n    env:\n      STAGEHAND_BROWSER_TARGET: local\n      STAGEHAND_SERVER_TARGET: local\n\n    strategy:\n      fail-fast: false\n      max-parallel: 100\n      matrix:\n        test: ${{ fromJson(needs.discover-core-tests.outputs.core-tests) }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - name: Run Vitest - ${{ matrix.test.name }}\n        run: |\n          pnpm exec turbo run test:core --only --filter=@browserbasehq/stagehand -- \"${{ matrix.test.path }}\"\n\n      - uses: ./.github/actions/upload-ctrf-report\n        if: always()\n        with:\n          name: ctrf/core-unit/${{ matrix.test.name }}.json\n\n      - uses: ./.github/actions/upload-v8-coverage\n        if: always()\n        with:\n          name: coverage/core-unit/${{ matrix.test.name }}\n\n  discover-server-tests:\n    runs-on: ubuntu-latest\n    needs: [determine-changes]\n    if: needs.determine-changes.outputs.server == 'true'\n    outputs:\n      integration-tests: ${{ steps.set-matrix.outputs.integration-tests }}\n      has-integration-tests: ${{ steps.set-matrix.outputs.has-integration-tests }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"false\"\n          restore-turbo-cache: \"false\"\n\n      - name: Discover server test files\n        id: set-matrix\n        run: |\n          int_json=$(pnpm --filter @browserbasehq/stagehand-server-v3 --silent run test:server -- --list integration)\n          echo \"integration-tests=$int_json\" >> $GITHUB_OUTPUT\n\n          if [ \"$int_json\" = \"[]\" ]; then\n            echo \"has-integration-tests=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"has-integration-tests=true\" >> $GITHUB_OUTPUT\n          fi\n\n          echo \"Found server integration tests: $int_json\"\n\n  build-server-sea:\n    name: Build SEA binary (tests, v3)\n    uses: ./.github/workflows/stagehand-server-v3-sea-build.yml\n    needs: [run-build]\n    with:\n      matrix: |\n        [\n          {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-linux-x64\",\"include_sourcemaps\":false},\n          {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-linux-arm64\",\"include_sourcemaps\":false},\n          {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-darwin-arm64\",\"include_sourcemaps\":false},\n          {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-darwin-x64\",\"include_sourcemaps\":false},\n          {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-win32-x64.exe\",\"include_sourcemaps\":false},\n          {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-win32-arm64.exe\",\"include_sourcemaps\":false},\n          {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-linux-x64-sourcemap\",\"include_sourcemaps\":true}\n        ]\n      use-prebuilt-artifacts: \"true\"\n      restore-turbo-cache: \"false\"\n      node-version: \"20.x\"\n      upload-only-binary: stagehand-server-v3-linux-x64-sourcemap\n\n  server-integration-tests:\n    name: server/v3/integration/${{ matrix.test.name }}\n    runs-on: ubuntu-latest\n    needs: [build-server-sea, discover-server-tests, run-build]\n    if: needs.discover-server-tests.outputs.has-integration-tests == 'true'\n\n    strategy:\n      fail-fast: false\n      matrix:\n        test: ${{ fromJson(needs.discover-server-tests.outputs.integration-tests) }}\n\n    env:\n      BB_ENV: local\n      STAGEHAND_BASE_URL: http://stagehand-api.localhost:3106\n      STAGEHAND_BROWSER_TARGET: local\n      STAGEHAND_SERVER_TARGET: sea\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n      GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n      # Used only for testing /start with env: BROWSERBASE remote browser\n      BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}\n      BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - name: Download SEA binary\n        uses: actions/download-artifact@v4\n        with:\n          name: stagehand-server-v3-linux-x64-sourcemap\n          path: .\n\n      - name: Ensure SEA binary is present and executable\n        shell: bash\n        run: |\n          set -euo pipefail\n          test -f packages/server-v3/dist/sea/stagehand-server-v3-linux-x64-sourcemap\n          chmod +x packages/server-v3/dist/sea/stagehand-server-v3-linux-x64-sourcemap\n\n      - name: Run server integration test - ${{ matrix.test.name }}\n        env:\n          SEA_BINARY_NAME: stagehand-server-v3-linux-x64-sourcemap\n        run: |\n          pnpm exec turbo run test:server --only --filter=@browserbasehq/stagehand-server-v3 -- \"${{ matrix.test.path }}\"\n\n      - uses: ./.github/actions/upload-ctrf-report\n        if: always()\n        with:\n          name: ctrf/server-v3-integration/${{ matrix.test.name }}.json\n\n      - uses: ./.github/actions/upload-v8-coverage\n        if: always()\n        with:\n          name: coverage/server-v3-integration/${{ matrix.test.name }}\n\n  discover-e2e-tests:\n    runs-on: ubuntu-latest\n    needs: [determine-changes]\n    if: needs.determine-changes.outputs.core == 'true'\n    outputs:\n      e2e-tests: ${{ steps.set-matrix.outputs.e2e-tests }}\n      has-e2e-tests: ${{ steps.set-matrix.outputs.has-e2e-tests }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"false\"\n          restore-turbo-cache: \"false\"\n\n      - name: Discover e2e test files\n        id: set-matrix\n        run: |\n          e2e_json=$(pnpm --filter @browserbasehq/stagehand --silent run test:e2e -- --list)\n          echo \"e2e-tests=$e2e_json\" >> $GITHUB_OUTPUT\n\n          if [ \"$e2e_json\" = \"[]\" ]; then\n            echo \"has-e2e-tests=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"has-e2e-tests=true\" >> $GITHUB_OUTPUT\n          fi\n\n          echo \"Found e2e tests: $e2e_json\"\n\n  run-e2e-local-tests:\n    name: e2e/local/${{ matrix.test.name }}\n    needs: [run-build, discover-e2e-tests]\n    runs-on: ubuntu-latest\n    timeout-minutes: 50\n    if: >\n      needs.discover-e2e-tests.outputs.has-e2e-tests == 'true' &&\n      github.event.pull_request.head.repo.full_name == github.repository\n    env:\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n      GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}\n      BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}\n      BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}\n      HEADLESS: true\n      STAGEHAND_BROWSER_TARGET: local\n      STAGEHAND_SERVER_TARGET: local\n    strategy:\n      fail-fast: false\n      max-parallel: 20\n      matrix:\n        test: ${{ fromJson(needs.discover-e2e-tests.outputs.e2e-tests) }}\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - uses: ./.github/actions/verify-chromium-launch\n\n      - name: Run local E2E Tests - ${{ matrix.test.name }}\n        run: |\n          pnpm exec turbo run test:e2e --only --filter=@browserbasehq/stagehand -- \"${{ matrix.test.path }}\"\n\n      - uses: ./.github/actions/upload-ctrf-report\n        if: always()\n        with:\n          name: ctrf/e2e-local/${{ matrix.test.name }}.json\n\n      - uses: ./.github/actions/upload-v8-coverage\n        if: always()\n        with:\n          name: coverage/e2e-local/${{ matrix.test.name }}\n\n  run-e2e-bb-tests:\n    name: e2e/bb/${{ matrix.test.name }}\n    needs: [run-build, discover-e2e-tests]\n    runs-on: ubuntu-latest\n    timeout-minutes: 50\n    if: >\n      needs.discover-e2e-tests.outputs.has-e2e-tests == 'true' &&\n      github.event.pull_request.head.repo.full_name == github.repository\n    env:\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n      GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}\n      BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}\n      BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}\n      HEADLESS: true\n      STAGEHAND_BROWSER_TARGET: browserbase\n      STAGEHAND_SERVER_TARGET: local\n    strategy:\n      fail-fast: false\n      max-parallel: 100\n      matrix:\n        test: ${{ fromJson(needs.discover-e2e-tests.outputs.e2e-tests) }}\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - name: Select Browserbase region\n        uses: ./.github/actions/select-browserbase-region\n        with:\n          distribution: ${{ env.BROWSERBASE_REGION_DISTRIBUTION }}\n\n      - name: Run E2E Tests (browserbase) - ${{ matrix.test.name }}\n        run: |\n          pnpm exec turbo run test:e2e --only --filter=@browserbasehq/stagehand -- \"${{ matrix.test.path }}\"\n\n      - uses: ./.github/actions/upload-ctrf-report\n        if: always()\n        with:\n          name: ctrf/e2e-bb/${{ matrix.test.name }}.json\n\n      - uses: ./.github/actions/upload-v8-coverage\n        if: always()\n        with:\n          name: coverage/e2e-bb/${{ matrix.test.name }}\n\n  run-evals:\n    name: evals/${{ matrix.category }}\n    needs: [run-build, determine-evals, run-e2e-bb-tests]\n    if: >-\n      ${{\n        always() &&\n        needs.run-build.result == 'success' &&\n        needs.determine-evals.result == 'success' &&\n        needs.run-e2e-bb-tests.result != 'failure' &&\n        needs.run-e2e-bb-tests.result != 'cancelled' &&\n        needs.determine-evals.outputs.skip-all-evals != 'true' &&\n        needs.determine-evals.outputs.eval-categories != '[]'\n      }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 90\n    strategy:\n      fail-fast: false\n      matrix:\n        category: ${{ fromJson(needs.determine-evals.outputs.eval-categories) }}\n    env:\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n      GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}\n      BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }}\n      BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}\n      BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}\n      STAGEHAND_BROWSER_TARGET: browserbase\n      STAGEHAND_SERVER_TARGET: local\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - name: Select Browserbase region\n        uses: ./.github/actions/select-browserbase-region\n        with:\n          distribution: ${{ env.BROWSERBASE_REGION_DISTRIBUTION }}\n\n      - name: Run Evals - ${{ matrix.category }}\n        id: run-evals\n        env:\n          NODE_V8_COVERAGE: coverage/evals/${{ matrix.category }}\n        run: |\n          log_file=\"$(mktemp)\"\n          set +e\n          pnpm exec turbo run test:evals --only --filter=@browserbasehq/stagehand-evals -- \"${{ matrix.category }}\" -t \"${EVAL_TRIAL_COUNT}\" -c \"${EVAL_MAX_CONCURRENCY}\" 2>&1 | tee \"$log_file\"\n          eval_status=${PIPESTATUS[0]}\n          set -e\n\n          summary_block=\"$(\n            awk '\n              /^=========================SUMMARY=========================$/ { capture=1 }\n              capture { print }\n              /^Evaluation summary written to / { capture=0 }\n            ' \"$log_file\"\n          )\"\n\n          if [ -n \"$summary_block\" ]; then\n            {\n              echo \"summary_text<<EOF\"\n              echo \"$summary_block\"\n              echo \"EOF\"\n            } >> \"$GITHUB_OUTPUT\"\n          fi\n\n          exit \"$eval_status\"\n\n      - name: Log Evals Performance - ${{ matrix.category }}\n        env:\n          EVAL_STDOUT_SUMMARY: ${{ steps.run-evals.outputs.summary_text }}\n        run: |\n          if [ -n \"${EVAL_STDOUT_SUMMARY:-}\" ]; then\n            echo \"### Evals Summary (${{ matrix.category }})\" >> \"$GITHUB_STEP_SUMMARY\"\n            echo '```' >> \"$GITHUB_STEP_SUMMARY\"\n            printf '%s\\n' \"$EVAL_STDOUT_SUMMARY\" >> \"$GITHUB_STEP_SUMMARY\"\n            echo '```' >> \"$GITHUB_STEP_SUMMARY\"\n          fi\n          experimentName=$(jq -r '.experimentName' eval-summary.json)\n          echo \"View results at https://www.braintrust.dev/app/Browserbase/p/stagehand/experiments/${experimentName}\"\n          if [ -f eval-summary.json ]; then\n            category_score=$(jq \".categories[\\\"${{ matrix.category }}\\\"]\" eval-summary.json)\n            echo \"${{ matrix.category }} category score: $category_score%\"\n            if (( $(echo \"$category_score < 80\" | bc -l) )); then\n              echo \"${{ matrix.category }} category score is below 80%. Failing CI.\"\n              exit 1\n            fi\n          else\n            echo \"Eval summary not found for ${{ matrix.category }} category. Failing CI.\"\n            exit 1\n          fi\n\n      - uses: ./.github/actions/upload-ctrf-report\n        if: always()\n        with:\n          name: ctrf/evals/${{ matrix.category }}.json\n\n      - uses: ./.github/actions/upload-v8-coverage\n        if: always()\n        with:\n          name: coverage/evals/${{ matrix.category }}\n\n  merge-coverage:\n    name: Code Coverage Report\n    runs-on: ubuntu-latest\n    needs:\n      - core-unit-tests\n      - run-e2e-local-tests\n      - run-e2e-bb-tests\n      - run-evals\n      - server-integration-tests\n    # if: always()\n    if: false\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"true\"\n          restore-turbo-cache: \"false\"\n\n      - name: Download V8 coverage artifacts\n        uses: actions/download-artifact@v4\n        continue-on-error: true\n        with:\n          pattern: coverage-*\n          path: .\n          merge-multiple: true\n\n      - name: Download CTRF artifacts\n        uses: actions/download-artifact@v4\n        continue-on-error: true\n        with:\n          pattern: ctrf-*\n          path: .\n          merge-multiple: true\n\n      - name: Generate merged coverage report\n        run: |\n          pnpm run coverage:merge\n\n      - name: Upload merged coverage report\n        if: always()\n        id: upload-coverage-artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: coverage-merged\n          # package.json is included to anchor artifact paths at repo root.\n          path: |\n            package.json\n            coverage/merged\n\n      - name: Add coverage summary to job summary\n        if: always()\n        shell: bash\n        run: |\n          echo \"### Code Coverage\" >> \"$GITHUB_STEP_SUMMARY\"\n          echo \"\" >> \"$GITHUB_STEP_SUMMARY\"\n          if [ -f coverage/merged/coverage-summary.txt ]; then\n            echo '```' >> \"$GITHUB_STEP_SUMMARY\"\n            cat coverage/merged/coverage-summary.txt >> \"$GITHUB_STEP_SUMMARY\"\n            echo '```' >> \"$GITHUB_STEP_SUMMARY\"\n          else\n            echo \"Coverage summary not available.\" >> \"$GITHUB_STEP_SUMMARY\"\n          fi\n          if [ -n \"${{ steps.upload-coverage-artifact.outputs.artifact-url }}\" ]; then\n            echo \"\" >> \"$GITHUB_STEP_SUMMARY\"\n            echo \"[Download full HTML coverage report](${{ steps.upload-coverage-artifact.outputs.artifact-url }})\" >> \"$GITHUB_STEP_SUMMARY\"\n          fi\n\n      - name: Publish merged CTRF report\n        if: always()\n        uses: ctrf-io/github-test-reporter@v1\n        with:\n          report-path: './ctrf/**/*.json'\n          summary: true\n          summary-report: false\n          summary-delta-report: true\n          test-report: false\n          failed-report: false\n          insights-report: true\n          flaky-rate-report: true\n          fail-rate-report: true\n          slowest-report: true\n          previous-results-report: true\n          fetch-previous-results: true\n          baseline: 1\n          previous-results-max: 1\n          max-workflow-runs-to-check: 5\n          max-previous-runs-to-fetch: 1\n          upload-artifact: true\n          artifact-name: ctrf-report-merged\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Compute coverage status metrics\n        if: always()\n        id: coverage-status\n        shell: bash\n        run: |\n          set -euo pipefail\n          shopt -s globstar nullglob\n          tests_failed=0\n          ctrf_files=(ctrf/**/*.json)\n          if [ \"${#ctrf_files[@]}\" -gt 0 ]; then\n            tests_failed=$(jq -s '[.[].results.summary.failed // 0] | add' \"${ctrf_files[@]}\")\n          fi\n          total_coverage=0\n          if [ -f coverage/merged/coverage-summary.txt ]; then\n            total_coverage=$(awk '/^Lines/ {gsub(/%/,\"\",$3); print $3}' coverage/merged/coverage-summary.txt)\n          fi\n          echo \"tests_failed=${tests_failed}\" >> \"$GITHUB_OUTPUT\"\n          echo \"total_coverage=${total_coverage}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Set coverage status\n        if: always()\n        continue-on-error: true\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          RUN_ID: ${{ github.run_id }}\n          PULL_NUMBER: ${{ github.event.pull_request.number }}\n          TESTS_FAILED: ${{ steps.coverage-status.outputs.tests_failed }}\n          TOTAL_COVERAGE: ${{ steps.coverage-status.outputs.total_coverage }}\n        run: |\n          set -euo pipefail\n          repo=\"${GITHUB_REPOSITORY}\"\n          sha=\"${GITHUB_SHA}\"\n          tests_failed=\"${TESTS_FAILED:-0}\"\n          total_coverage=\"${TOTAL_COVERAGE:-0}\"\n          state=\"success\"\n          if [ -n \"${PULL_NUMBER:-}\" ]; then\n            target_url=\"https://github.com/${repo}/pull/${PULL_NUMBER}/checks?check_run_id=${RUN_ID}\"\n          else\n            target_url=\"https://github.com/${repo}/actions/runs/${RUN_ID}\"\n          fi\n          description=\"non-blocking report: ${tests_failed} tests failed. ${total_coverage}% coverage\"\n          payload=$(jq -n \\\n            --arg state \"$state\" \\\n            --arg target_url \"$target_url\" \\\n            --arg description \"$description\" \\\n            --arg context \"Measured coverage\" \\\n            '{state: $state, target_url: $target_url, description: $description, context: $context}')\n          curl -sSfL -X POST \\\n            -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n            -H \"Accept: application/vnd.github+json\" \\\n            -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n            \"https://api.github.com/repos/${repo}/statuses/${sha}\" \\\n            -d \"$payload\"\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\nenv:\n  BROWSERBASE_FLOW_LOGS: \"1\"\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n      issues: write\n      id-token: write\n      actions: write # Required for Claude to read CI results on PRs / rerun actions that failed\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"false\"\n          restore-turbo-cache: \"false\"\n          node-version: 20.x\n\n      - name: Run Build\n        run: |\n          pnpm exec turbo run build\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          track_progress: true\n\n          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.\n          prompt: 'Make sure \"turbo run lint\" and \"turbo run build\" pass before pushing and make sure to check present CI status for the branch and fix any easy failures. Prefer using the Github MCP tools over bash for Github operations, fall back to Bash(gh) for anything not supported by the MCP tools.'\n\n          branch_prefix: 'claude-'\n          # Optional: Add claude_args to customize behavior and configuration\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://code.claude.com/docs/en/cli-reference for available options\n          claude_args: |\n            --allowed-tools mcp__github_inline_comment__create_inline_comment,Bash,View,Glob,GlobTool,GrepTool,Grep,BatchTool,WebSearch,LS,Edit,MultiEdit,Write,Read\n\n\n# consider adding in the future:\n# - https://github.com/anthropics/claude-code-action/blob/main/examples/test-failure-analysis.yml\n# - https://github.com/anthropics/claude-code-action/blob/main/examples/ci-failure-auto-fix.yml\n# - https://github.com/anthropics/claude-code-action/blob/main/examples/issue-deduplication.yml\n"
  },
  {
    "path": ".github/workflows/external-contributor-pr-approval-handoff.yml",
    "content": "name: External Contributor PR Approval Handoff\n\non:\n  pull_request_review:\n    types:\n      - submitted\n\npermissions:\n  contents: read\n  pull-requests: read\n\njobs:\n  capture-approved-review:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Write approval handoff payload\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const fs = require('fs');\n            const pr = context.payload.pull_request;\n            const review = context.payload.review;\n            const shouldClaim =\n              review.state === 'approved' &&\n              pr.head.repo.full_name !== context.payload.repository.full_name;\n\n            const payload = {\n              shouldClaim,\n              prNumber: pr.number,\n              reviewer: review.user?.login || '',\n              reviewId: review.id,\n              approvedSha: review.commit_id || pr.head.sha,\n            };\n\n            fs.writeFileSync('approval-handoff.json', JSON.stringify(payload));\n\n      - name: Upload approval handoff artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: approved-review\n          path: approval-handoff.json\n          retention-days: 1\n"
  },
  {
    "path": ".github/workflows/external-contributor-pr.yml",
    "content": "name: External Contributor PR\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - closed\n  workflow_run:\n    workflows:\n      - External Contributor PR Approval Handoff\n    types:\n      - completed\n\npermissions:\n  actions: read\n  contents: write\n  pull-requests: write\n  issues: write\n\nenv:\n  ECPR_LIB: |\n    (() => {\n      const LABELS = [\n        { name: 'external-contributor', color: '8b949e', description: 'Tracks PRs mirrored from external contributor forks.' },\n        { name: 'external-contributor:awaiting-approval', color: 'd29922', description: 'Waiting for a stagehand team member to approve the latest external commit.' },\n        { name: 'external-contributor:mirrored', color: '1f6feb', description: 'An internal mirrored PR currently exists for this external contributor PR.' },\n        { name: 'external-contributor:stale', color: 'db6d28', description: 'The mirrored PR is stale and waiting for a fresh approval to refresh.' },\n        { name: 'external-contributor:completed', color: '2da44e', description: 'The mirrored PR has been merged and the external contributor flow is complete.' },\n      ];\n      const MANAGED_LABELS = new Set(LABELS.map((label) => label.name));\n      const MANAGED_COMMENT_AUTHOR = 'github-actions[bot]';\n      const CLAIM_RE = /<!-- external-contributor-pr:claim owned-pr=(\\d+) source-sha=([0-9a-f]{40}) claimer=([A-Za-z0-9-]+) branch=([^ ]+) -->/;\n      const OWNED_RE = /<!-- external-contributor-pr:owned source-pr=(\\d+) source-sha=([0-9a-f]{40}) claimer=([A-Za-z0-9-]+) -->/;\n      const NOTICE_MARKER = '<!-- external-contributor-pr:notice -->';\n      const NOTICE_LINES = [\n        'This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.',\n        'Approving the latest commit mirrors it into an internal PR owned by the approver.',\n        'If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.',\n      ];\n\n      async function ensureLabels(github, context) {\n        for (const label of LABELS) {\n          try {\n            await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label.name });\n          } catch (error) {\n            if (error.status !== 404) throw error;\n            try {\n              await github.rest.issues.createLabel({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                name: label.name,\n                color: label.color,\n                description: label.description,\n              });\n            } catch (createError) {\n              if (createError.status !== 422) throw createError;\n            }\n          }\n        }\n      }\n\n      async function listComments(github, context, issueNumber) {\n        return github.paginate(github.rest.issues.listComments, {\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          issue_number: issueNumber,\n          per_page: 100,\n        });\n      }\n\n      function isManagedComment(comment) {\n        return comment.user?.login === MANAGED_COMMENT_AUTHOR;\n      }\n\n      function defaultManagedBranch(prNumber) {\n        return `external-contributor-pr-${prNumber}`;\n      }\n\n      function sanitizeManagedBranch(prNumber, branch) {\n        const fallback = defaultManagedBranch(prNumber);\n        if (!branch) return fallback;\n        const allowed = new RegExp(`^external-contributor-pr-${prNumber}(?:-[A-Za-z0-9._-]+)?$`);\n        return allowed.test(branch) ? branch : fallback;\n      }\n\n      async function upsertComment(github, context, issueNumber, marker, lines) {\n        const comments = await listComments(github, context, issueNumber);\n        const body = [marker, ...lines].join('\\n');\n        const existing = comments.find((comment) => isManagedComment(comment) && comment.body?.includes(marker));\n        if (!existing) {\n          await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body });\n          return;\n        }\n        if (existing.body !== body) {\n          await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body });\n        }\n      }\n\n      async function syncLabels(github, context, issueNumber, desiredLabels) {\n        const { data: issue } = await github.rest.issues.get({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          issue_number: issueNumber,\n        });\n        const existingNames = issue.labels.map((label) => typeof label === 'string' ? label : label.name).filter(Boolean);\n        const preserved = existingNames.filter((label) => !MANAGED_LABELS.has(label));\n        await github.rest.issues.setLabels({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          issue_number: issueNumber,\n          labels: [...preserved, ...desiredLabels],\n        });\n      }\n\n      async function findLatestClaim(github, context, issueNumber) {\n        const comments = await listComments(github, context, issueNumber);\n        return [...comments]\n          .reverse()\n          .map((comment) => {\n            if (!isManagedComment(comment)) return null;\n            const match = comment.body?.match(CLAIM_RE);\n            if (!match) return null;\n            const sourcePrNumber = issueNumber;\n            return {\n              ownedPrNumber: Number(match[1]),\n              sourceSha: match[2],\n              claimer: match[3],\n              branch: sanitizeManagedBranch(sourcePrNumber, match[4]),\n            };\n          })\n          .find(Boolean);\n      }\n\n      async function externalLifecycle({ github, context }) {\n        const pr = context.payload.pull_request;\n        await ensureLabels(github, context);\n\n        if (context.payload.action === 'opened' || context.payload.action === 'reopened') {\n          await upsertComment(github, context, pr.number, NOTICE_MARKER, NOTICE_LINES);\n          const latestClaim = await findLatestClaim(github, context, pr.number);\n          if (context.payload.action === 'reopened' && latestClaim && latestClaim.sourceSha === pr.head.sha) {\n            const { data: ownedPr } = await github.rest.pulls.get({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              pull_number: latestClaim.ownedPrNumber,\n            });\n            if (ownedPr.state === 'open') {\n              await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:mirrored']);\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: pr.number,\n                body: `This external contributor PR is already mirrored to ${ownedPr.html_url}. Closing it again so discussion stays on the internal PR until fresh commits require another approval.`,\n              });\n              await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, state: 'closed' });\n              return;\n            }\n          }\n          await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:awaiting-approval']);\n          return;\n        }\n\n        const latestClaim = await findLatestClaim(github, context, pr.number);\n        if (!latestClaim || latestClaim.sourceSha === pr.head.sha) return;\n\n        const { data: ownedPr } = await github.rest.pulls.get({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          pull_number: latestClaim.ownedPrNumber,\n        });\n        if (ownedPr.state !== 'open') return;\n\n        await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:awaiting-approval']);\n        await syncLabels(github, context, ownedPr.number, ['external-contributor', 'external-contributor:stale']);\n        await upsertComment(github, context, ownedPr.number, '<!-- external-contributor-pr:owned-status -->', [\n          `This mirrored PR is stale because the original external contributor PR #${pr.number} received new commits (\\`${latestClaim.sourceSha}\\` -> \\`${pr.head.sha}\\`).`,\n          `Original PR: ${pr.html_url}`,\n          '',\n          'Approve the latest external commit to refresh this same internal PR in place.',\n        ]);\n        if (pr.state === 'closed') {\n          await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, state: 'open' });\n        }\n        await github.rest.issues.createComment({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          issue_number: ownedPr.number,\n          body: `New commits landed on external contributor PR #${pr.number} (\\`${latestClaim.sourceSha}\\` -> \\`${pr.head.sha}\\`). This mirrored PR stays open but is now stale until the latest external commit is approved and copied over.`,\n        });\n        await github.rest.issues.createComment({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          issue_number: pr.number,\n          body: `New commits were pushed to this external contributor PR (\\`${latestClaim.sourceSha}\\` -> \\`${pr.head.sha}\\`). The mirrored PR ${ownedPr.html_url} remains open but is marked stale. A stagehand team member with write access must approve the latest commit to refresh that internal PR.`,\n        });\n      }\n\n      async function prepareClaim({ github, context, core, artifactPath }) {\n        const fs = require('fs');\n        const handoff = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));\n        core.setOutput('should-claim', 'false');\n        if (!handoff.shouldClaim || !handoff.prNumber || !handoff.reviewer || !handoff.approvedSha) return;\n\n        const { data: pr } = await github.rest.pulls.get({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          pull_number: Number(handoff.prNumber),\n        });\n        if (pr.head.repo.full_name === context.payload.repository.full_name || pr.state !== 'open') return;\n\n        const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          username: handoff.reviewer,\n        });\n        if (!new Set(['admin', 'maintain', 'write']).has(permission.permission)) {\n          await github.rest.issues.createComment({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            issue_number: pr.number,\n            body: `@${handoff.reviewer} submitted an approving review, but only stagehand team members with write access can claim external contributor PRs. A maintainer with write access must approve the latest commit to proceed.`,\n          });\n          return;\n        }\n        if (pr.head.sha !== handoff.approvedSha) return;\n\n        const latestClaim = await findLatestClaim(github, context, pr.number);\n        const branch = sanitizeManagedBranch(pr.number, latestClaim?.branch);\n        const title = `[Claimed #${pr.number}] ${pr.title}`;\n        const body = [\n          `Mirrored from external contributor PR #${pr.number} after approval by @${handoff.reviewer}.`,\n          '',\n          `Original author: @${pr.user.login}`,\n          `Original PR: ${pr.html_url}`,\n          `Approved source head SHA: \\`${pr.head.sha}\\``,\n          '',\n          `@${pr.user.login}, please continue any follow-up discussion on this mirrored PR. When the external PR gets new commits, this same internal PR will be marked stale until the latest external commit is approved and refreshed here.`,\n          '',\n          '## Original description',\n          pr.body?.trim() || '_No description provided._',\n          '',\n          `<!-- external-contributor-pr:owned source-pr=${pr.number} source-sha=${pr.head.sha} claimer=${handoff.reviewer} -->`,\n        ].join('\\n');\n\n        const { data: ownedPrs } = await github.rest.pulls.list({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          state: 'all',\n          head: `${context.repo.owner}:${branch}`,\n          base: 'main',\n          per_page: 100,\n        });\n\n        core.setOutput('should-claim', 'true');\n        core.setOutput('claimer', handoff.reviewer);\n        core.setOutput('pr-number', String(pr.number));\n        core.setOutput('source-sha', pr.head.sha);\n        core.setOutput('previous-source-sha', latestClaim?.sourceSha || '');\n        core.setOutput('branch', branch);\n        core.setOutput('title', title);\n        core.setOutput('body', body);\n        core.setOutput('owned-pr-number', ownedPrs[0] ? String(ownedPrs[0].number) : '');\n        core.setOutput('owned-pr-merged', ownedPrs[0]?.merged_at ? 'true' : 'false');\n      }\n\n      async function finalizeClaim({ github, context, input }) {\n        await ensureLabels(github, context);\n        const {\n          prNumber,\n          sourceSha,\n          branch,\n          claimer,\n          title,\n          body,\n          existingNumber,\n          existingMerged,\n          refreshStatus,\n          refreshReason,\n        } = input;\n\n        if (refreshStatus !== 'updated') {\n          if (existingNumber) {\n            await syncLabels(github, context, Number(existingNumber), ['external-contributor', 'external-contributor:stale']);\n            await upsertComment(github, context, Number(existingNumber), '<!-- external-contributor-pr:owned-status -->', [\n              `This mirrored PR could not be refreshed automatically after approval by @${claimer}.`,\n              '',\n              `Refresh reason: \\`${refreshReason || 'unknown'}\\``,\n              'Resolve the branch manually, then keep using this same mirrored PR.',\n            ]);\n          }\n          await syncLabels(github, context, prNumber, ['external-contributor', 'external-contributor:awaiting-approval']);\n          await github.rest.issues.createComment({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            issue_number: prNumber,\n            body: `The latest approval by @${claimer} could not refresh the mirrored PR automatically (${refreshReason || 'unknown reason'}). The external PR stays open, and the mirrored PR should be updated manually before work continues.`,\n          });\n          return;\n        }\n\n        let ownedPr;\n        if (existingNumber && !existingMerged) {\n          const { data } = await github.rest.pulls.update({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            pull_number: Number(existingNumber),\n            title,\n            body,\n            base: 'main',\n            state: 'open',\n          });\n          ownedPr = data;\n        } else {\n          const { data } = await github.rest.pulls.create({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            title,\n            body,\n            head: branch,\n            base: 'main',\n          });\n          ownedPr = data;\n        }\n\n        await github.rest.issues.addAssignees({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          issue_number: ownedPr.number,\n          assignees: [claimer],\n        });\n        await syncLabels(github, context, prNumber, ['external-contributor', 'external-contributor:mirrored']);\n        await syncLabels(github, context, ownedPr.number, ['external-contributor', 'external-contributor:mirrored']);\n        await upsertComment(github, context, ownedPr.number, '<!-- external-contributor-pr:owned-status -->', [\n          `This mirrored PR tracks external contributor PR #${prNumber} at source SHA \\`${sourceSha}\\`, approved by @${claimer}.`,\n          `Original PR: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`,\n          '',\n          'When the external PR gets new commits, this same internal PR will be refreshed in place after the latest external commit is approved.',\n        ]);\n\n        const marker = `<!-- external-contributor-pr:claim owned-pr=${ownedPr.number} source-sha=${sourceSha} claimer=${claimer} branch=${branch} -->`;\n        const comments = await listComments(github, context, prNumber);\n        if (!comments.some((comment) => comment.body?.includes(marker))) {\n          await github.rest.issues.createComment({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            issue_number: prNumber,\n            body: [marker, `This PR was approved by @${claimer} and mirrored to ${ownedPr.html_url}. All further discussion should happen on that PR.`].join('\\n'),\n          });\n        }\n\n        const { data: externalPr } = await github.rest.pulls.get({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          pull_number: prNumber,\n        });\n        if (externalPr.state !== 'closed') {\n          await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, state: 'closed' });\n        }\n      }\n\n      async function syncOwnedPr({ github, context }) {\n        const pr = context.payload.pull_request;\n        const match = pr.body?.match(OWNED_RE);\n        if (!match) return;\n\n        const sourcePrNumber = Number(match[1]);\n        const sourceSha = match[2];\n        await ensureLabels(github, context);\n\n        const { data: externalPr } = await github.rest.pulls.get({\n          owner: context.repo.owner,\n          repo: context.repo.repo,\n          pull_number: sourcePrNumber,\n        });\n\n        if (context.payload.action === 'reopened') {\n          await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:mirrored']);\n          await syncLabels(github, context, sourcePrNumber, ['external-contributor', 'external-contributor:mirrored']);\n          if (externalPr.state !== 'closed') {\n            await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: sourcePrNumber, state: 'closed' });\n          }\n          return;\n        }\n\n        if (pr.merged) {\n          await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:completed']);\n          await syncLabels(github, context, sourcePrNumber, ['external-contributor', 'external-contributor:completed']);\n          await upsertComment(github, context, pr.number, '<!-- external-contributor-pr:owned-status -->', [\n            `This mirrored PR has been merged into \\`main\\`. The original external PR ${externalPr.html_url} is now completed.`,\n          ]);\n          await upsertComment(github, context, sourcePrNumber, `<!-- external-contributor-pr:completed owned-pr=${pr.number} -->`, [\n            `The mirrored PR ${pr.html_url} has been merged into \\`main\\`. This original external contributor PR will stay closed as completed.`,\n          ]);\n          return;\n        }\n\n        await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:stale']);\n        await syncLabels(github, context, sourcePrNumber, ['external-contributor', 'external-contributor:awaiting-approval']);\n        if (externalPr.head.sha !== sourceSha) {\n          await upsertComment(github, context, pr.number, '<!-- external-contributor-pr:owned-status -->', [\n            `This mirrored PR is stale because the original external PR ${externalPr.html_url} now points at a different source SHA.`,\n            'Approve the latest external commit to refresh this same internal PR.',\n          ]);\n          return;\n        }\n\n        if (externalPr.state === 'closed') {\n          await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: sourcePrNumber, state: 'open' });\n        }\n        await upsertComment(github, context, sourcePrNumber, `<!-- external-contributor-pr:owned-closed owned-pr=${pr.number} -->`, [\n          `The mirrored PR ${pr.html_url} was closed without merge. This original PR has been reopened and is awaiting a fresh approving review from a stagehand team member with write access.`,\n        ]);\n        await upsertComment(github, context, pr.number, '<!-- external-contributor-pr:owned-status -->', [\n          `This mirrored PR was closed without merge. The original external PR ${externalPr.html_url} has been reopened and relabeled as awaiting approval.`,\n        ]);\n      }\n\n      return { externalLifecycle, prepareClaim, finalizeClaim, syncOwnedPr };\n    })()\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.id }}\n  cancel-in-progress: false\n\njobs:\n  manage-external-pr:\n    if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository\n    runs-on: ubuntu-latest\n    steps:\n      - name: Sync external PR lifecycle\n        if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const lib = eval(process.env.ECPR_LIB);\n            await lib.externalLifecycle({ github, context });\n\n  claim-approved-pr:\n    if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Download approval handoff artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: approved-review\n          path: approval-handoff\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          repository: ${{ github.repository }}\n          run-id: ${{ github.event.workflow_run.id }}\n\n      - name: Prepare approved claim\n        id: prepare-claim\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const lib = eval(process.env.ECPR_LIB);\n            await lib.prepareClaim({ github, context, core, artifactPath: 'approval-handoff/approval-handoff.json' });\n\n      - name: Checkout repository for branch operations\n        if: steps.prepare-claim.outputs.should-claim == 'true'\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          persist-credentials: true\n\n      - name: Refresh internal branch\n        if: steps.prepare-claim.outputs.should-claim == 'true'\n        id: refresh-branch\n        continue-on-error: true\n        env:\n          INTERNAL_BRANCH: ${{ steps.prepare-claim.outputs.branch }}\n          PR_NUMBER: ${{ steps.prepare-claim.outputs.pr-number }}\n          PREVIOUS_SOURCE_SHA: ${{ steps.prepare-claim.outputs.previous-source-sha }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          set -uo pipefail\n\n          refresh_status=\"conflict\"\n          refresh_reason=\"unknown\"\n\n          write_outputs() {\n            echo \"refresh-status=${refresh_status}\" >> \"$GITHUB_OUTPUT\"\n            if [ -n \"${refresh_reason}\" ]; then\n              echo \"reason=${refresh_reason}\" >> \"$GITHUB_OUTPUT\"\n            fi\n          }\n\n          trap write_outputs EXIT\n\n          if ! git config user.name \"github-actions[bot]\"; then\n            refresh_reason=\"git-config-failed\"\n            exit 0\n          fi\n\n          if ! git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"; then\n            refresh_reason=\"git-config-failed\"\n            exit 0\n          fi\n\n          if ! git remote set-url origin \"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git\"; then\n            refresh_reason=\"remote-auth-failed\"\n            exit 0\n          fi\n\n          if ! git fetch origin \"pull/${PR_NUMBER}/head:refs/remotes/origin/external-pr-head-${PR_NUMBER}\"; then\n            refresh_reason=\"fetch-external-failed\"\n            exit 0\n          fi\n\n          external_ref=\"refs/remotes/origin/external-pr-head-${PR_NUMBER}\"\n          branch_exists=false\n          if git ls-remote --exit-code --heads origin \"${INTERNAL_BRANCH}\" >/dev/null 2>&1; then\n            branch_exists=true\n            if ! git fetch origin \"${INTERNAL_BRANCH}:refs/remotes/origin/${INTERNAL_BRANCH}\"; then\n              refresh_reason=\"fetch-internal-failed\"\n              exit 0\n            fi\n          fi\n\n          if [ \"${branch_exists}\" = false ]; then\n            if ! git checkout -B \"${INTERNAL_BRANCH}\" \"${external_ref}\"; then\n              refresh_reason=\"checkout-failed\"\n              exit 0\n            fi\n\n            if ! git push --force-with-lease origin \"HEAD:refs/heads/${INTERNAL_BRANCH}\"; then\n              refresh_reason=\"push-failed\"\n              exit 0\n            fi\n\n            refresh_status=\"updated\"\n            refresh_reason=\"\"\n            exit 0\n          fi\n\n          if ! git checkout -B \"${INTERNAL_BRANCH}\" \"refs/remotes/origin/${INTERNAL_BRANCH}\"; then\n            refresh_reason=\"checkout-failed\"\n            exit 0\n          fi\n\n          if [ -z \"${PREVIOUS_SOURCE_SHA}\" ]; then\n            refresh_reason=\"missing-previous-source\"\n            exit 0\n          fi\n\n          if git rebase --onto \"${external_ref}\" \"${PREVIOUS_SOURCE_SHA}\" \"${INTERNAL_BRANCH}\"; then\n            if ! git push --force-with-lease origin \"HEAD:refs/heads/${INTERNAL_BRANCH}\"; then\n              refresh_reason=\"push-failed\"\n              exit 0\n            fi\n\n            refresh_status=\"updated\"\n            refresh_reason=\"\"\n            exit 0\n          fi\n\n          git rebase --abort || true\n          refresh_reason=\"rebase-conflict\"\n\n      - name: Finalize approved claim\n        if: always() && steps.prepare-claim.outputs.should-claim == 'true'\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const lib = eval(process.env.ECPR_LIB);\n            await lib.finalizeClaim({\n              github,\n              context,\n              input: {\n                prNumber: Number('${{ steps.prepare-claim.outputs.pr-number }}'),\n                sourceSha: ${{ toJson(steps.prepare-claim.outputs.source-sha) }},\n                branch: ${{ toJson(steps.prepare-claim.outputs.branch) }},\n                claimer: ${{ toJson(steps.prepare-claim.outputs.claimer) }},\n                title: ${{ toJson(steps.prepare-claim.outputs.title) }},\n                body: ${{ toJson(steps.prepare-claim.outputs.body) }},\n                existingNumber: ${{ toJson(steps.prepare-claim.outputs.owned-pr-number) }},\n                existingMerged: '${{ steps.prepare-claim.outputs.owned-pr-merged }}' === 'true',\n                refreshStatus: ${{ toJson(steps.refresh-branch.outputs.refresh-status) }},\n                refreshReason: ${{ toJson(steps.refresh-branch.outputs.reason) }},\n              },\n            });\n\n  sync-owned-pr:\n    if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository && (github.event.action == 'closed' || github.event.action == 'reopened')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Sync mirrored PR lifecycle\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const lib = eval(process.env.ECPR_LIB);\n            await lib.syncOwnedPr({ github, context });\n"
  },
  {
    "path": ".github/workflows/feature-parity.yml",
    "content": "name: Feature Parity\n\non:\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - labeled\n      - unlabeled\n    paths-ignore:\n      - \"packages/docs/**\"\n\njobs:\n  check-parity-label:\n    runs-on: ubuntu-latest\n    if: github.event.action == 'labeled' && github.event.label.name == 'parity'\n    permissions:\n      contents: read\n      pull-requests: write\n      issues: write\n    steps:\n      - name: Check out repository code\n        uses: actions/checkout@v4\n\n      - name: Check user permissions\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              username: context.actor\n            });\n\n            const hasWriteAccess = ['admin', 'write'].includes(permission.permission);\n\n            if (!hasWriteAccess) {\n              // Remove the parity label if user doesn't have write access\n              await github.rest.issues.removeLabel({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                name: 'parity'\n              });\n\n              // Add a comment explaining why the label was removed\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                body: `❌ **Parity Label Removed**\\n\\n@${context.actor}, you do not have sufficient permissions to add the 'parity' label. Only users with write access can trigger feature parity issues.\\n\\nIf you believe this feature should be implemented in the Python SDK, please ask a maintainer to add the label.`\n              });\n\n              throw new Error(`User ${context.actor} does not have write access to add parity label`);\n            }\n\n            console.log(`User ${context.actor} has ${permission.permission} access - proceeding with parity workflow`);\n\n      - name: Generate GitHub App token\n        id: generate-token\n        uses: actions/create-github-app-token@v1\n        with:\n          app-id: ${{ secrets.PARITY_APP_ID }}\n          private-key: ${{ secrets.PARITY_APP_PRIVATE_KEY }}\n          owner: browserbase\n          repositories: stagehand\n\n      - name: Create issue in Python SDK repository\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ steps.generate-token.outputs.token }}\n          script: |\n            const { data: pullRequest } = await github.rest.pulls.get({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              pull_number: context.issue.number,\n            });\n\n            // Get PR comments for additional context\n            const { data: comments } = await github.rest.issues.listComments({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n            });\n\n            // Format comments for the issue description\n            let commentsSection = '';\n            if (comments.length > 0) {\n              commentsSection = '\\n\\n## Recent Comments\\n\\n';\n              comments.slice(-3).forEach(comment => {\n                commentsSection += `**@${comment.user.login}** commented:\\n`;\n                commentsSection += `${comment.body.substring(0, 500)}${comment.body.length > 500 ? '...' : ''}\\n\\n`;\n              });\n            }\n\n            // Get list of changed files for context\n            const { data: files } = await github.rest.pulls.listFiles({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              pull_number: context.issue.number,\n            });\n\n            const changedFiles = files.map(file => `- \\`${file.filename}\\``).join('\\n');\n\n            const issueTitle = `[Feature Parity] ${pullRequest.title}`;\n            const issueBody = `## Feature Parity Request\n\n            This issue was automatically created from a pull request in the TypeScript Stagehand repository that was labeled with 'parity'.\n\n            ### Original PR Details\n            - **PR**: #${context.issue.number} - ${pullRequest.title}\n            - **Author**: @${pullRequest.user.login}\n            - **Link**: ${pullRequest.html_url}\n\n            ### Description\n            ${pullRequest.body || 'No description provided.'}\n\n            ### Changed Files\n            ${changedFiles}\n\n            ${commentsSection}\n\n            ### Action Required\n            Please review the changes in the original PR and implement equivalent functionality in the Python SDK if applicable.\n\n            ---\n            *This issue was automatically generated by the Feature Parity workflow.*`;\n\n            // Create the issue in the Python repository\n            const { data: issue } = await github.rest.issues.create({\n              owner: 'browserbase',\n              repo: 'stagehand-python',\n              title: issueTitle,\n              body: issueBody,\n              labels: ['parity']\n            });\n\n            console.log(`Created issue: ${issue.html_url}`);\n\n            // Add a comment to the original PR confirming the issue was created\n            await github.rest.issues.createComment({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n              body: `🔄 **Feature Parity Issue Created**\\n\\nAn issue has been automatically created in the Python SDK repository to track parity implementation:\\n${issue.html_url}`\n            });\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: write\n  pull-requests: write\n  id-token: write\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\njobs:\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repo\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        with:\n          use-prebuilt-artifacts: \"false\"\n\n      - name: Configure npm registry for Trusted Publishing\n        uses: actions/setup-node@v6\n        with:\n          node-version: 20.x\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Update npm for Trusted Publishing\n        run: npm install -g npm@latest\n\n      - name: Run Lint & Build\n        run: pnpm exec turbo run lint && pnpm exec turbo run build\n\n      - name: Create Release Pull Request or Publish to npm\n        id: changesets\n        uses: changesets/action@v1\n        with:\n          publish: pnpm run release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Publish Canary\n        if: github.ref == 'refs/heads/main'\n        run: |\n          git checkout main\n          pnpm run release-canary\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/stagehand-server-v3-release.yml",
    "content": "name: Release stagehand/server-v3\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - .changeset/**\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\nenv:\n  OAS_PATH: packages/server-v3/openapi.v3.yaml\n\njobs:\n  detect:\n    name: Detect server-v3 release (changesets)\n    runs-on: ubuntu-latest\n    outputs:\n      release: ${{ steps.meta.outputs.release }}\n      version: ${{ steps.meta.outputs.version }}\n      tag: ${{ steps.meta.outputs.tag }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n          fetch-tags: true\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        env:\n          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: \"1\"\n        with:\n          use-prebuilt-artifacts: \"false\"\n\n      - name: Determine release metadata\n        id: meta\n        shell: bash\n        run: |\n          set -euo pipefail\n\n          latest_tag=\"$(git tag -l 'stagehand-server-v3/v*' --sort=-v:refname | head -n 1 || true)\"\n          rm -f changeset-status.json\n          if [ -n \"${latest_tag}\" ]; then\n            pnpm changeset status --since \"${latest_tag}\" --output changeset-status.json\n          else\n            pnpm changeset status --output changeset-status.json\n          fi\n\n          node <<'NODE'\n          const fs = require('fs');\n\n          const status = JSON.parse(fs.readFileSync('changeset-status.json', 'utf8'));\n          const changesets = Array.isArray(status.changesets) ? status.changesets : [];\n          const releases = Array.isArray(status.releases) ? status.releases : [];\n\n          const shouldRelease = changesets.some((cs) =>\n            (cs.releases || []).some((r) => r?.name === '@browserbasehq/stagehand-server-v3')\n          );\n\n          const serverRelease = releases.find((r) => r?.name === '@browserbasehq/stagehand-server-v3');\n          if (shouldRelease && !serverRelease?.newVersion) {\n            throw new Error(\n              'Expected @browserbasehq/stagehand-server-v3 to have a computed newVersion in changeset-status.json.'\n            );\n          }\n\n          const release = shouldRelease ? 'true' : 'false';\n          const version = shouldRelease ? serverRelease.newVersion : '';\n          const tag = `stagehand-server-v3/v${version}`;\n\n          const out = process.env.GITHUB_OUTPUT;\n          fs.appendFileSync(out, `release=${release}\\n`);\n          fs.appendFileSync(out, `version=${version}\\n`);\n          fs.appendFileSync(out, `tag=${tag}\\n`);\n          NODE\n\n      - name: Create stagehand/server-v3 tag\n        if: steps.meta.outputs.release == 'true'\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        shell: bash\n        run: |\n          set -euo pipefail\n\n          TAG=\"${{ steps.meta.outputs.tag }}\"\n          VERSION=\"${{ steps.meta.outputs.version }}\"\n          TARGET_SHA=\"${{ github.sha }}\"\n\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n\n          # Try to fetch the tag if it exists on remote; ignore failure for new tags\n          git fetch --force origin \"refs/tags/${TAG}:refs/tags/${TAG}\" 2>/dev/null || true\n          if git rev-parse -q --verify \"refs/tags/${TAG}\" >/dev/null; then\n            echo \"Tag already exists: ${TAG}\"\n            exit 0\n          fi\n\n          git tag -a \"${TAG}\" \"${TARGET_SHA}\" -m \"stagehand/server-v3 v${VERSION}\"\n          git push origin \"${TAG}\"\n\n  build_binaries:\n    name: Build SEA binaries\n    needs: detect\n    if: needs.detect.outputs.release == 'true'\n    uses: ./.github/workflows/stagehand-server-v3-sea-build.yml\n    with:\n      matrix: |\n        [\n          {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-linux-x64\",\"include_sourcemaps\":false},\n          {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-linux-arm64\",\"include_sourcemaps\":false},\n          {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-darwin-arm64\",\"include_sourcemaps\":false},\n          {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-darwin-x64\",\"include_sourcemaps\":false},\n          {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-win32-x64.exe\",\"include_sourcemaps\":false},\n          {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-win32-arm64.exe\",\"include_sourcemaps\":false}\n        ]\n\n  release:\n    name: Publish GitHub Release\n    needs: [detect, build_binaries]\n    if: needs.detect.outputs.release == 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n          fetch-tags: false\n\n      - name: Prepare release assets directory\n        run: mkdir -p release-assets\n\n      - name: Prepare stagehand/server-v3 release assets\n        run: |\n          set -euo pipefail\n          cp \"${{ env.OAS_PATH }}\" \"release-assets/openapi.v3.stagehand-server-v3-${{ needs.detect.outputs.version }}.yaml\"\n\n      - name: Download SEA binary artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: stagehand-server-v3-*\n          path: .\n          merge-multiple: true\n\n      - name: Collect SEA binaries\n        shell: bash\n        run: |\n          set -euo pipefail\n          shopt -s nullglob\n          for f in packages/server-v3/dist/sea/stagehand-server-v3-*; do\n            cp \"$f\" release-assets/\n          done\n\n      - name: Create checksums\n        shell: bash\n        run: |\n          set -euo pipefail\n          cd release-assets\n          # Only checksum binaries (exclude openapi yaml). Avoid failing if no matches.\n          shopt -s nullglob\n          files=(stagehand-server-v3-*)\n          bins=()\n          for f in \"${files[@]}\"; do\n            [[ \"$f\" == *openapi* ]] && continue\n            [[ -f \"$f\" ]] && bins+=(\"$f\")\n          done\n          : > checksums.sha256\n          if [ \"${#bins[@]}\" -gt 0 ]; then\n            shasum -a 256 \"${bins[@]}\" > checksums.sha256\n          fi\n\n      - name: Publish stagehand/server-v3 GitHub release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ needs.detect.outputs.tag }}\n          name: stagehand/server-v3 v${{ needs.detect.outputs.version }}\n          generate_release_notes: true\n          files: |\n            release-assets/openapi.v3.stagehand-server-v3-${{ needs.detect.outputs.version }}.yaml\n            release-assets/stagehand-server-v3-*\n            release-assets/checksums.sha256\n"
  },
  {
    "path": ".github/workflows/stagehand-server-v3-sea-build.yml",
    "content": "name: Stagehand Server v3 SEA Build\n\non:\n  workflow_call:\n    inputs:\n      matrix:\n        description: \"JSON matrix include list for SEA binaries.\"\n        required: false\n        type: string\n        default: |\n          [\n            {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-linux-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-linux-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-darwin-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-darwin-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-win32-x64.exe\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-win32-arm64.exe\",\"include_sourcemaps\":false}\n          ]\n      use-prebuilt-artifacts:\n        description: \"Whether to download pre-built package artifacts.\"\n        required: false\n        type: string\n        default: \"false\"\n      restore-turbo-cache:\n        description: \"Whether to restore local .turbo cache.\"\n        required: false\n        type: string\n        default: \"true\"\n      node-version:\n        description: \"Node.js version for setup.\"\n        required: false\n        type: string\n        default: \"20.x\"\n      upload-only-binary:\n        description: \"Upload only this binary (empty => upload all).\"\n        required: false\n        type: string\n        default: \"\"\n  workflow_dispatch:\n    inputs:\n      matrix:\n        description: \"JSON matrix include list for SEA binaries.\"\n        required: false\n        default: |\n          [\n            {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-linux-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-linux-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-darwin-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-darwin-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v3-win32-x64.exe\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v3-win32-arm64.exe\",\"include_sourcemaps\":false}\n          ]\n      use-prebuilt-artifacts:\n        description: \"Whether to download pre-built package artifacts.\"\n        required: false\n        type: string\n        default: \"false\"\n      restore-turbo-cache:\n        description: \"Whether to restore local .turbo cache.\"\n        required: false\n        type: string\n        default: \"true\"\n      node-version:\n        description: \"Node.js version for setup.\"\n        required: false\n        type: string\n        default: \"20.x\"\n      upload-only-binary:\n        description: \"Upload only this binary (empty => upload all).\"\n        required: false\n        type: string\n        default: \"\"\n\njobs:\n  build_binaries:\n    name: Build SEA binaries (${{ matrix.binary_name }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include: ${{ fromJson(inputs.matrix) }}\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n          fetch-tags: false\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        env:\n          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: \"1\"\n          PLAYWRIGHT_SKIP_DOWNLOAD: \"1\"\n          PUPPETEER_SKIP_DOWNLOAD: \"1\"\n        with:\n          use-prebuilt-artifacts: ${{ inputs.use-prebuilt-artifacts }}\n          restore-turbo-cache: ${{ inputs.restore-turbo-cache }}\n          node-version: ${{ inputs.node-version }}\n\n      - name: Build SEA binary (ESM)\n        env:\n          SEA_TARGET_PLATFORM: ${{ matrix.platform }}\n          SEA_TARGET_ARCH: ${{ matrix.arch }}\n          SEA_BINARY_NAME: ${{ matrix.binary_name }}\n          SEA_INCLUDE_SOURCEMAPS: ${{ matrix.include_sourcemaps && '1' || '0' }}\n        run: pnpm exec turbo run build:sea:esm --filter=@browserbasehq/stagehand-server-v3\n\n      - name: Verify SEA binary exists\n        shell: bash\n        run: |\n          test -f \"packages/server-v3/dist/sea/${{ matrix.binary_name }}\"\n\n      - name: Verify SEA binary launches cleanly\n        shell: bash\n        env:\n          RUNNER_ARCH: ${{ runner.arch }}\n        run: |\n          set -euo pipefail\n\n          binary=\"packages/server-v3/dist/sea/${{ matrix.binary_name }}\"\n          matrix_arch=\"${{ matrix.arch }}\"\n          runner_arch=\"$(echo \"${RUNNER_ARCH}\" | tr '[:upper:]' '[:lower:]')\"\n\n          if [[ \"${matrix_arch}\" != \"${runner_arch}\" ]]; then\n            echo \"Runner arch (${runner_arch}) does not match matrix arch (${matrix_arch}).\"\n            echo \"Launch verification must run on same-arch runners.\"\n            exit 1\n          fi\n\n          if [[ \"${{ matrix.platform }}\" != \"win32\" ]]; then\n            chmod +x \"${binary}\"\n          fi\n\n          port=\"$((30000 + RANDOM % 10000))\"\n          log_file=\"$(mktemp)\"\n          launched=\"false\"\n\n          cleanup() {\n            if [[ -n \"${pid:-}\" ]] && kill -0 \"${pid}\" 2>/dev/null; then\n              kill \"${pid}\" 2>/dev/null || true\n              wait \"${pid}\" 2>/dev/null || true\n            fi\n          }\n          trap cleanup EXIT\n\n          PORT=\"${port}\" \"${binary}\" >\"${log_file}\" 2>&1 &\n          pid=$!\n\n          for _ in {1..30}; do\n            if ! kill -0 \"${pid}\" 2>/dev/null; then\n              wait \"${pid}\" 2>/dev/null || true\n              echo \"SEA binary exited before becoming healthy.\"\n              cat \"${log_file}\"\n              exit 1\n            fi\n\n            if curl --silent --show-error --fail \"http://127.0.0.1:${port}/healthz\" >/dev/null; then\n              launched=\"true\"\n              break\n            fi\n\n            sleep 1\n          done\n\n          if [[ \"${launched}\" != \"true\" ]]; then\n            echo \"SEA binary did not become healthy within 30 seconds.\"\n            cat \"${log_file}\"\n            exit 1\n          fi\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        if: ${{ inputs.upload-only-binary == '' || matrix.binary_name == inputs.upload-only-binary }}\n        with:\n          name: ${{ matrix.binary_name }}\n          # package.json is included to anchor artifact paths at repo root.\n          path: |\n            package.json\n            packages/server-v3/dist/sea/${{ matrix.binary_name }}\n          retention-days: 7\n"
  },
  {
    "path": ".github/workflows/stagehand-server-v4-release.yml",
    "content": "name: Release stagehand/server-v4\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - .changeset/**\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\nenv:\n  OAS_PATH: packages/server-v4/openapi.v4.yaml\n\njobs:\n  detect:\n    name: Detect server-v4 release (changesets)\n    runs-on: ubuntu-latest\n    outputs:\n      release: ${{ steps.meta.outputs.release }}\n      version: ${{ steps.meta.outputs.version }}\n      tag: ${{ steps.meta.outputs.tag }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n          fetch-tags: true\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        env:\n          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: \"1\"\n        with:\n          use-prebuilt-artifacts: \"false\"\n\n      - name: Determine release metadata\n        id: meta\n        shell: bash\n        run: |\n          set -euo pipefail\n\n          latest_tag=\"$(git tag -l 'stagehand-server-v4/v*' --sort=-v:refname | head -n 1 || true)\"\n          rm -f changeset-status.json\n          if [ -n \"${latest_tag}\" ]; then\n            pnpm changeset status --since \"${latest_tag}\" --output changeset-status.json\n          else\n            pnpm changeset status --output changeset-status.json\n          fi\n\n          node <<'NODE'\n          const fs = require('fs');\n\n          const status = JSON.parse(fs.readFileSync('changeset-status.json', 'utf8'));\n          const changesets = Array.isArray(status.changesets) ? status.changesets : [];\n          const releases = Array.isArray(status.releases) ? status.releases : [];\n\n          const shouldRelease = changesets.some((cs) =>\n            (cs.releases || []).some((r) => r?.name === '@browserbasehq/stagehand-server-v4')\n          );\n\n          const serverRelease = releases.find((r) => r?.name === '@browserbasehq/stagehand-server-v4');\n          if (shouldRelease && !serverRelease?.newVersion) {\n            throw new Error(\n              'Expected @browserbasehq/stagehand-server-v4 to have a computed newVersion in changeset-status.json.'\n            );\n          }\n\n          const release = shouldRelease ? 'true' : 'false';\n          const version = shouldRelease ? serverRelease.newVersion : '';\n          const tag = `stagehand-server-v4/v${version}`;\n\n          const out = process.env.GITHUB_OUTPUT;\n          fs.appendFileSync(out, `release=${release}\\n`);\n          fs.appendFileSync(out, `version=${version}\\n`);\n          fs.appendFileSync(out, `tag=${tag}\\n`);\n          NODE\n\n      - name: Create stagehand/server-v4 tag\n        if: steps.meta.outputs.release == 'true'\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        shell: bash\n        run: |\n          set -euo pipefail\n\n          TAG=\"${{ steps.meta.outputs.tag }}\"\n          VERSION=\"${{ steps.meta.outputs.version }}\"\n          TARGET_SHA=\"${{ github.sha }}\"\n\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n\n          # Try to fetch the tag if it exists on remote; ignore failure for new tags\n          git fetch --force origin \"refs/tags/${TAG}:refs/tags/${TAG}\" 2>/dev/null || true\n          if git rev-parse -q --verify \"refs/tags/${TAG}\" >/dev/null; then\n            echo \"Tag already exists: ${TAG}\"\n            exit 0\n          fi\n\n          git tag -a \"${TAG}\" \"${TARGET_SHA}\" -m \"stagehand/server-v4 v${VERSION}\"\n          git push origin \"${TAG}\"\n\n  build_binaries:\n    name: Build SEA binaries\n    needs: detect\n    if: needs.detect.outputs.release == 'true'\n    uses: ./.github/workflows/stagehand-server-v4-sea-build.yml\n    with:\n      matrix: |\n        [\n          {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-linux-x64\",\"include_sourcemaps\":false},\n          {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-linux-arm64\",\"include_sourcemaps\":false},\n          {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-darwin-arm64\",\"include_sourcemaps\":false},\n          {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-darwin-x64\",\"include_sourcemaps\":false},\n          {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-win32-x64.exe\",\"include_sourcemaps\":false},\n          {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-win32-arm64.exe\",\"include_sourcemaps\":false}\n        ]\n\n  release:\n    name: Publish GitHub Release\n    needs: [detect, build_binaries]\n    if: needs.detect.outputs.release == 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n          fetch-tags: false\n\n      - name: Prepare release assets directory\n        run: mkdir -p release-assets\n\n      - name: Prepare stagehand/server-v4 release assets\n        run: |\n          set -euo pipefail\n          cp \"${{ env.OAS_PATH }}\" \"release-assets/openapi.v4.stagehand-server-v4-${{ needs.detect.outputs.version }}.yaml\"\n\n      - name: Download SEA binary artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: stagehand-server-v4-*\n          path: .\n          merge-multiple: true\n\n      - name: Collect SEA binaries\n        shell: bash\n        run: |\n          set -euo pipefail\n          shopt -s nullglob\n          for f in packages/server-v4/dist/sea/stagehand-server-v4-*; do\n            cp \"$f\" release-assets/\n          done\n\n      - name: Create checksums\n        shell: bash\n        run: |\n          set -euo pipefail\n          cd release-assets\n          # Only checksum binaries (exclude openapi yaml). Avoid failing if no matches.\n          shopt -s nullglob\n          files=(stagehand-server-v4-*)\n          bins=()\n          for f in \"${files[@]}\"; do\n            [[ \"$f\" == *openapi* ]] && continue\n            [[ -f \"$f\" ]] && bins+=(\"$f\")\n          done\n          : > checksums.sha256\n          if [ \"${#bins[@]}\" -gt 0 ]; then\n            shasum -a 256 \"${bins[@]}\" > checksums.sha256\n          fi\n\n      - name: Publish stagehand/server-v4 GitHub release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ needs.detect.outputs.tag }}\n          name: stagehand/server-v4 v${{ needs.detect.outputs.version }}\n          generate_release_notes: true\n          files: |\n            release-assets/openapi.v4.stagehand-server-v4-${{ needs.detect.outputs.version }}.yaml\n            release-assets/stagehand-server-v4-*\n            release-assets/checksums.sha256\n"
  },
  {
    "path": ".github/workflows/stagehand-server-v4-sea-build.yml",
    "content": "name: Stagehand Server v4 SEA Build\n\non:\n  workflow_call:\n    inputs:\n      matrix:\n        description: \"JSON matrix include list for SEA binaries.\"\n        required: false\n        type: string\n        default: |\n          [\n            {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-linux-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-linux-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-darwin-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-darwin-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-win32-x64.exe\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-win32-arm64.exe\",\"include_sourcemaps\":false}\n          ]\n      use-prebuilt-artifacts:\n        description: \"Whether to download pre-built package artifacts.\"\n        required: false\n        type: string\n        default: \"false\"\n      restore-turbo-cache:\n        description: \"Whether to restore local .turbo cache.\"\n        required: false\n        type: string\n        default: \"true\"\n      node-version:\n        description: \"Node.js version for setup.\"\n        required: false\n        type: string\n        default: \"20.x\"\n      upload-only-binary:\n        description: \"Upload only this binary (empty => upload all).\"\n        required: false\n        type: string\n        default: \"\"\n  workflow_dispatch:\n    inputs:\n      matrix:\n        description: \"JSON matrix include list for SEA binaries.\"\n        required: false\n        default: |\n          [\n            {\"os\":\"ubuntu-latest\",\"platform\":\"linux\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-linux-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"ubuntu-24.04-arm\",\"platform\":\"linux\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-linux-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15\",\"platform\":\"darwin\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-darwin-arm64\",\"include_sourcemaps\":false},\n            {\"os\":\"macos-15-intel\",\"platform\":\"darwin\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-darwin-x64\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-latest\",\"platform\":\"win32\",\"arch\":\"x64\",\"binary_name\":\"stagehand-server-v4-win32-x64.exe\",\"include_sourcemaps\":false},\n            {\"os\":\"windows-11-arm\",\"platform\":\"win32\",\"arch\":\"arm64\",\"binary_name\":\"stagehand-server-v4-win32-arm64.exe\",\"include_sourcemaps\":false}\n          ]\n      use-prebuilt-artifacts:\n        description: \"Whether to download pre-built package artifacts.\"\n        required: false\n        type: string\n        default: \"false\"\n      restore-turbo-cache:\n        description: \"Whether to restore local .turbo cache.\"\n        required: false\n        type: string\n        default: \"true\"\n      node-version:\n        description: \"Node.js version for setup.\"\n        required: false\n        type: string\n        default: \"20.x\"\n      upload-only-binary:\n        description: \"Upload only this binary (empty => upload all).\"\n        required: false\n        type: string\n        default: \"\"\n\njobs:\n  build_binaries:\n    name: Build SEA binaries (${{ matrix.binary_name }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include: ${{ fromJson(inputs.matrix) }}\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n          fetch-tags: false\n\n      - uses: ./.github/actions/setup-node-pnpm-turbo\n        env:\n          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: \"1\"\n          PLAYWRIGHT_SKIP_DOWNLOAD: \"1\"\n          PUPPETEER_SKIP_DOWNLOAD: \"1\"\n        with:\n          use-prebuilt-artifacts: ${{ inputs.use-prebuilt-artifacts }}\n          restore-turbo-cache: ${{ inputs.restore-turbo-cache }}\n          node-version: ${{ inputs.node-version }}\n\n      - name: Build SEA binary (ESM)\n        env:\n          SEA_TARGET_PLATFORM: ${{ matrix.platform }}\n          SEA_TARGET_ARCH: ${{ matrix.arch }}\n          SEA_BINARY_NAME: ${{ matrix.binary_name }}\n          SEA_INCLUDE_SOURCEMAPS: ${{ matrix.include_sourcemaps && '1' || '0' }}\n        run: pnpm exec turbo run build:sea:esm --filter=@browserbasehq/stagehand-server-v4\n\n      - name: Verify SEA binary exists\n        shell: bash\n        run: |\n          test -f \"packages/server-v4/dist/sea/${{ matrix.binary_name }}\"\n\n      - name: Verify SEA binary launches cleanly\n        shell: bash\n        env:\n          RUNNER_ARCH: ${{ runner.arch }}\n        run: |\n          set -euo pipefail\n\n          binary=\"packages/server-v4/dist/sea/${{ matrix.binary_name }}\"\n          matrix_arch=\"${{ matrix.arch }}\"\n          runner_arch=\"$(echo \"${RUNNER_ARCH}\" | tr '[:upper:]' '[:lower:]')\"\n\n          if [[ \"${matrix_arch}\" != \"${runner_arch}\" ]]; then\n            echo \"Runner arch (${runner_arch}) does not match matrix arch (${matrix_arch}).\"\n            echo \"Launch verification must run on same-arch runners.\"\n            exit 1\n          fi\n\n          if [[ \"${{ matrix.platform }}\" != \"win32\" ]]; then\n            chmod +x \"${binary}\"\n          fi\n\n          port=\"$((30000 + RANDOM % 10000))\"\n          log_file=\"$(mktemp)\"\n          launched=\"false\"\n\n          cleanup() {\n            if [[ -n \"${pid:-}\" ]] && kill -0 \"${pid}\" 2>/dev/null; then\n              kill \"${pid}\" 2>/dev/null || true\n              wait \"${pid}\" 2>/dev/null || true\n            fi\n          }\n          trap cleanup EXIT\n\n          PORT=\"${port}\" \"${binary}\" >\"${log_file}\" 2>&1 &\n          pid=$!\n\n          for _ in {1..30}; do\n            if ! kill -0 \"${pid}\" 2>/dev/null; then\n              wait \"${pid}\" 2>/dev/null || true\n              echo \"SEA binary exited before becoming healthy.\"\n              cat \"${log_file}\"\n              exit 1\n            fi\n\n            if curl --silent --show-error --fail \"http://127.0.0.1:${port}/healthz\" >/dev/null; then\n              launched=\"true\"\n              break\n            fi\n\n            sleep 1\n          done\n\n          if [[ \"${launched}\" != \"true\" ]]; then\n            echo \"SEA binary did not become healthy within 30 seconds.\"\n            cat \"${log_file}\"\n            exit 1\n          fi\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        if: ${{ inputs.upload-only-binary == '' || matrix.binary_name == inputs.upload-only-binary }}\n        with:\n          name: ${{ matrix.binary_name }}\n          # package.json is included to anchor artifact paths at repo root.\n          path: |\n            package.json\n            packages/server-v4/dist/sea/${{ matrix.binary_name }}\n          retention-days: 7\n"
  },
  {
    "path": ".github/workflows/stainless.yml",
    "content": "name: Build SDKs for pull request\n\non:\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - reopened\n      - closed\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}\n  cancel-in-progress: true\n\nenv:\n  STAINLESS_ORG: ${{ vars.STAINLESS_ORG }}\n  STAINLESS_PROJECT: ${{ vars.STAINLESS_PROJECT }}\n  OAS_PATH: packages/server-v3/openapi.v3.yaml\n\njobs:\n  preview:\n    if: github.event.action != 'closed'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n\n      - name: Run preview builds\n        uses: stainless-api/upload-openapi-spec-action/preview@v1\n        with:\n          stainless_api_key: ${{ secrets.STAINLESS_API_KEY }}\n          org: ${{ env.STAINLESS_ORG }}\n          project: ${{ env.STAINLESS_PROJECT }}\n          oas_path: ${{ env.OAS_PATH }}\n          config_path: stainless.yml\n\n  merge:\n    if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n      - name: Run merge build\n        uses: stainless-api/upload-openapi-spec-action/merge@v1\n        with:\n          stainless_api_key: ${{ secrets.STAINLESS_API_KEY }}\n          org: ${{ env.STAINLESS_ORG }}\n          project: ${{ env.STAINLESS_PROJECT }}\n          oas_path: ${{ env.OAS_PATH }}\n          config_path: stainless.yml\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\nscreenshot.png\n.DS_STORE\n.cache/\n.env\ndownloads/\ndist/\n.browserbase/\npackages/evals/**/public\npackages/core/lib/dom/build/\npackages/core/lib/v3/dom/build/\npackages/evals/public\n*.tgz\nevals/playground.ts\ntmp/\neval-summary.json\npackage-lock.json\nevals/deterministic/tests/BrowserContext/tmp-test.har\npackages/core/lib/version.ts\npackages/core/test-results/\n/examples/inference_summary\n/inference_summary\n.turbo\n.idea\ncoverage/\nctrf/\n.stagehand-sea/\n"
  },
  {
    "path": ".prettierignore",
    "content": "pnpm-lock.yaml\nREADME.md\n**/*.json\ndocs/\n.github/\ndist/\nnode_modules/\nlib/dom/build/\nlib/v3/dom/build/\npackages/core/dist/\npackages/core/lib/dom/build/\npackages/core/lib/v3/dom/build/\npackages/cli/dist/\npackages/evals/dist/\npackages/docs/\n*.min.js\n.browserbase/\n.browserbase/**\n**/.browserbase/\n**/.browserbase/**\nstainless.yml\nopenapi.*.yaml\n"
  },
  {
    "path": ".prettierrc",
    "content": "{}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.formatOnSave\": true\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# @browserbasehq/stagehand\n\n## 3.0.0\n\n### Major Changes\n\n- Removes internal Playwright dependency\n- A generous 20-40% speed increase across `act`, `extract`, & `observe` calls\n- Compatibility with Playwright, Puppeteer, and Patchright\n- Automatic action caching (agent, stagehand.act). Go from CUA → deterministic scripts w/o inference\n- A suite of non AI primitives:\n  - `page`\n  - `locator` (built in closed mode shadow root traversal, with xpaths & css selectors)\n  - `frameLocator`\n  - `deepLocator` (crosses iframes & shadow roots)\n- bun compatibility\n- Simplified extract schemas\n- CSS selector support (id-based support coming soon)\n- Targeted extract and observe across iframes & shadow roots\n- More intuitive type names (observeResult is now action, act accepts an instruction string instead of an action string, solidified ModelConfiguration)\n\nCheck the [migration guide](https://docs.stagehand.dev/v3/migrations/v2) for more information\n\n## 2.5.0\n\n### Minor Changes\n\n- [#981](https://github.com/browserbase/stagehand/pull/981) [`8244ab2`](https://github.com/browserbase/stagehand/commit/8244ab247cd679962685ae2f7c54e874ce1fa614) Thanks [@sameelarif](https://github.com/sameelarif)! - Added support for `stagehand.agent` to interact with MCP servers as well as custom tools to be passed in. For more information, reference the [MCP integrations documentation](https://docs.stagehand.dev/best-practices/mcp-integrations)\n\n### Patch Changes\n\n- [#959](https://github.com/browserbase/stagehand/pull/959) [`09b5e1e`](https://github.com/browserbase/stagehand/commit/09b5e1e9c23c845903686db6665cc968ac34efbb) Thanks [@filip-michalsky](https://github.com/filip-michalsky)! - add webvoyager evals\n\n- [#1049](https://github.com/browserbase/stagehand/pull/1049) [`e3734b9`](https://github.com/browserbase/stagehand/commit/e3734b9c98352d5f0a4eca49791b0bbf2130ab41) Thanks [@miguelg719](https://github.com/miguelg719)! - Support local MCP server connections\n\n- [#1025](https://github.com/browserbase/stagehand/pull/1025) [`be85b19`](https://github.com/browserbase/stagehand/commit/be85b19679a826f19702e00f0aae72fce1118ec8) Thanks [@tkattkat](https://github.com/tkattkat)! - add support for custom baseUrl within openai provider\n\n- [#1040](https://github.com/browserbase/stagehand/pull/1040) [`88d1565`](https://github.com/browserbase/stagehand/commit/88d1565c65bb65a104fea2d5f5e862bbbda69677) Thanks [@miguelg719](https://github.com/miguelg719)! - Allow OpenAI CUA to take in an optional baseURL\n\n- [#1046](https://github.com/browserbase/stagehand/pull/1046) [`ab5d6ed`](https://github.com/browserbase/stagehand/commit/ab5d6ede19aabc059badc4247f1cb2c6c9e71bae) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for gpt-5 in operator agent\n\n## 2.4.4\n\n### Patch Changes\n\n- [#1012](https://github.com/browserbase/stagehand/pull/1012) [`9e8c173`](https://github.com/browserbase/stagehand/commit/9e8c17374fdc8fbe7f26e6cf802c36bd14f11039) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix disabling api validation whenever a customLLM client is provided\n\n## 2.4.3\n\n### Patch Changes\n\n- [#951](https://github.com/browserbase/stagehand/pull/951) [`f45afdc`](https://github.com/browserbase/stagehand/commit/f45afdccc8680650755fee66ffbeac32b41e075d) Thanks [@miguelg719](https://github.com/miguelg719)! - Patch GPT-5 new api format\n\n- [#954](https://github.com/browserbase/stagehand/pull/954) [`261bba4`](https://github.com/browserbase/stagehand/commit/261bba43fa79ac3af95328e673ef3e9fced3279b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support for shadow DOMs (open & closed mode) when experimental: true\n\n- [#944](https://github.com/browserbase/stagehand/pull/944) [`8de7bd8`](https://github.com/browserbase/stagehand/commit/8de7bd8635c2051cd8025e365c6c8aa83d81c7e7) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Bump zod version compatibility and add pathing spec\n\n- [#919](https://github.com/browserbase/stagehand/pull/919) [`3d80421`](https://github.com/browserbase/stagehand/commit/3d804210a106a6828c7fa50f8b765b10afd4cc6a) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - enable scrolling inside of iframes\n\n- [#963](https://github.com/browserbase/stagehand/pull/963) [`0ead63d`](https://github.com/browserbase/stagehand/commit/0ead63d6526f6c286362b74b6407c8bebc900e69) Thanks [@tkattkat](https://github.com/tkattkat)! - Properly handle images in evaluator + clean up response parsing logic\n\n- [#961](https://github.com/browserbase/stagehand/pull/961) [`8422828`](https://github.com/browserbase/stagehand/commit/8422828c4cd5fd5ebcf348cfbdb40c768bb76dd9) Thanks [@tkattkat](https://github.com/tkattkat)! - Add more evals for stagehand agent\n\n- [#946](https://github.com/browserbase/stagehand/pull/946) [`b769206`](https://github.com/browserbase/stagehand/commit/b7692060f98a2f49aeeefb90d8789ed034b08ec2) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: unable to act on/get content from some same process iframes\n\n- [#962](https://github.com/browserbase/stagehand/pull/962) [`72d2683`](https://github.com/browserbase/stagehand/commit/72d2683202af7e578d98367893964b33e0828de5) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - handle namespaced elements in xpath build step\n\n## 2.4.2\n\n### Patch Changes\n\n- [#865](https://github.com/browserbase/stagehand/pull/865) [`6b4e6e3`](https://github.com/browserbase/stagehand/commit/6b4e6e3f31d5496cf15728e9018eddeb04839542) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - improve type safety for trimTrailingTextNode\n\n- [#897](https://github.com/browserbase/stagehand/pull/897) [`e77d018`](https://github.com/browserbase/stagehand/commit/e77d0188683ebf596dfb78dfafbbca1dc32993f0) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix selfHeal to remember intially received arguments\n\n- [#920](https://github.com/browserbase/stagehand/pull/920) [`c20adb9`](https://github.com/browserbase/stagehand/commit/c20adb95539fed8c56a4aa413262a9c65a8e6474) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: tab handling on API\n\n- [#882](https://github.com/browserbase/stagehand/pull/882) [`b86df93`](https://github.com/browserbase/stagehand/commit/b86df93b9136aae96292121a29c25f3d74d84bf7) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - remove elements that don't have xpaths from observe response\n\n- [#905](https://github.com/browserbase/stagehand/pull/905) [`023c2c2`](https://github.com/browserbase/stagehand/commit/023c2c273b46d3792d7e5d3c902089487b16b531) Thanks [@tkattkat](https://github.com/tkattkat)! - Delete old images from anthropic cua client\n\n- [#925](https://github.com/browserbase/stagehand/pull/925) [`8c28647`](https://github.com/browserbase/stagehand/commit/8c2864755ecd05c8f7de235d4198deec0dd5f78e) Thanks [@miguelg719](https://github.com/miguelg719)! - Remove \\_refreshPageFromApi()\n\n- [#887](https://github.com/browserbase/stagehand/pull/887) [`87e09c6`](https://github.com/browserbase/stagehand/commit/87e09c618940f364ec8af00455a19a17ec63cbd3) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: allow xpaths with prepended 'xpath=' for targeted extract\n\n- [#864](https://github.com/browserbase/stagehand/pull/864) [`a611115`](https://github.com/browserbase/stagehand/commit/a61111525d70b450bdfc43f112380f44899c9e97) Thanks [@miguelg719](https://github.com/miguelg719)! - Temporarily patch custom clients serialization error on api\n\n- [#881](https://github.com/browserbase/stagehand/pull/881) [`69913fe`](https://github.com/browserbase/stagehand/commit/69913fe1dfb8201ae2aeffa5f049fb46ab02cbc2) Thanks [@miguelg719](https://github.com/miguelg719)! - Pass sdk version number to API for debugging\n\n- [#913](https://github.com/browserbase/stagehand/pull/913) [`b1b83a1`](https://github.com/browserbase/stagehand/commit/b1b83a1d334fe76e5f5f9dd32dc92c16b7d40ce6) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - move iframe out of 'experimental'\n\n- [#891](https://github.com/browserbase/stagehand/pull/891) [`be8497c`](https://github.com/browserbase/stagehand/commit/be8497cb6b142cc893cea9692b8c47bd19514c60) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: nested iframe xpath bug\n\n- [#883](https://github.com/browserbase/stagehand/pull/883) [`98704c9`](https://github.com/browserbase/stagehand/commit/98704c9ed225ca25bbde4bb3dc286936e9c54471) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add timeout for JS click\n\n- [#907](https://github.com/browserbase/stagehand/pull/907) [`04978bd`](https://github.com/browserbase/stagehand/commit/04978bdd30d2edcbc69eb9fd91358a16975ea2eb) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - store mapping of CDP frame ID -> page\n\n## 2.4.1\n\n### Patch Changes\n\n- [#856](https://github.com/browserbase/stagehand/pull/856) [`8a43c5a`](https://github.com/browserbase/stagehand/commit/8a43c5a86d4da40cfaedd9cf2e42186928bdf946) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - set download behaviour by default\n\n- [#857](https://github.com/browserbase/stagehand/pull/857) [`890ffcc`](https://github.com/browserbase/stagehand/commit/890ffccac5e0a60ade64a46eb550c981ffb3e84a) Thanks [@miguelg719](https://github.com/miguelg719)! - return \"not-supported\" for elements inside the shadow-dom\n\n- [#844](https://github.com/browserbase/stagehand/pull/844) [`64c1072`](https://github.com/browserbase/stagehand/commit/64c10727bda50470483a3eb175c02842db0923a1) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - don't automatically close tabs\n\n- [#860](https://github.com/browserbase/stagehand/pull/860) [`b077d3f`](https://github.com/browserbase/stagehand/commit/b077d3f48a97f47a71ccc79ae39b41e7f07f9c04) Thanks [@miguelg719](https://github.com/miguelg719)! - Set default schema on extract options with no schema\n\n- [#842](https://github.com/browserbase/stagehand/pull/842) [`8bcb5d7`](https://github.com/browserbase/stagehand/commit/8bcb5d77debf6bf7601fd5c090efd7fde75c5d5e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - improved handling for OS level dropdowns\n\n- [#846](https://github.com/browserbase/stagehand/pull/846) [`7bf10c5`](https://github.com/browserbase/stagehand/commit/7bf10c55b267078fe847c1d7f7a60d604f9c7c94) Thanks [@miguelg719](https://github.com/miguelg719)! - Filter attaching to target worker / shared_worker\n\n## 2.4.0\n\n### Minor Changes\n\n- [#819](https://github.com/browserbase/stagehand/pull/819) [`6a18c1e`](https://github.com/browserbase/stagehand/commit/6a18c1ee1e46d55c6e90c4d5572e17ed8daa140c) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - try playwright click and fall back to JS click event\n\n### Patch Changes\n\n- [#826](https://github.com/browserbase/stagehand/pull/826) [`124e0d3`](https://github.com/browserbase/stagehand/commit/124e0d3bb54ddb6738ede6d7aa99a945ef1cacd1) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where we are unable to take actions on text nodes\n\n- [#818](https://github.com/browserbase/stagehand/pull/818) [`1660751`](https://github.com/browserbase/stagehand/commit/1660751cd14cb5b27d44f8167216afb8d1c3c45c) Thanks [@miguelg719](https://github.com/miguelg719)! - Added CUA support for Claude 4 models\n\n- [#821](https://github.com/browserbase/stagehand/pull/821) [`cadac9d`](https://github.com/browserbase/stagehand/commit/cadac9da09123d12e5d496a0e8b12660964c1b33) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - use playwright instead of playwright test\n\n- [#832](https://github.com/browserbase/stagehand/pull/832) [`759da55`](https://github.com/browserbase/stagehand/commit/759da55775eb2df81d56ae18c0f386fd9b02a9f0) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix \\_refreshPageFromAPI to use parametrized apiKey\n\n- [#810](https://github.com/browserbase/stagehand/pull/810) [`a175a51`](https://github.com/browserbase/stagehand/commit/a175a519b8c14300db6f1ed30709e113d18e99db) Thanks [@miguelg719](https://github.com/miguelg719)! - Update logos\n\n- [#822](https://github.com/browserbase/stagehand/pull/822) [`8527a80`](https://github.com/browserbase/stagehand/commit/8527a80522c3eedb9516a6caa1a0e4e4be981a3d) Thanks [@miguelg719](https://github.com/miguelg719)! - Add model with date tag for OpenAI CUA\n\n- [#833](https://github.com/browserbase/stagehand/pull/833) [`55fca2f`](https://github.com/browserbase/stagehand/commit/55fca2f7da63cc0ef6e27b45a33f63c666cdce7e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - adjust stagehandLogger.warn() level to be 1 instead of 0\n\n## 2.3.1\n\n### Patch Changes\n\n- [#796](https://github.com/browserbase/stagehand/pull/796) [`12a99b3`](https://github.com/browserbase/stagehand/commit/12a99b398d8a4c3eea3ca69a3cf793faaaf4aea3) Thanks [@miguelg719](https://github.com/miguelg719)! - Added a experimental flag to enable the newest and most experimental features\n\n- [#807](https://github.com/browserbase/stagehand/pull/807) [`2451797`](https://github.com/browserbase/stagehand/commit/2451797f64c0efa4a72fd70265110003c8d0a6cd) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - include version number in StagehandDefaultError message\n\n- [#803](https://github.com/browserbase/stagehand/pull/803) [`1d631a5`](https://github.com/browserbase/stagehand/commit/1d631a57a197390f672b718ae5199991ab27cfb1) Thanks [@miguelg719](https://github.com/miguelg719)! - Enable session affinity for cache optimization\n\n- [#804](https://github.com/browserbase/stagehand/pull/804) [`9c398bb`](https://github.com/browserbase/stagehand/commit/9c398bb9ec2d10bdb53ad5aa7e3b58cce24fdb2b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - update operatorResponseSchema based on new openai spec\n\n- [#786](https://github.com/browserbase/stagehand/pull/786) [`c19ad7f`](https://github.com/browserbase/stagehand/commit/c19ad7f1e082e91fdeaa9c2ef63767a5a2b3a195) Thanks [@miguelg719](https://github.com/miguelg719)! - Handle reroute to account for rollout\n\n## 2.3.0\n\n### Minor Changes\n\n- [#737](https://github.com/browserbase/stagehand/pull/737) [`6ef6073`](https://github.com/browserbase/stagehand/commit/6ef60730cab0ad9025f44b6eeb2c83751d1dcd35) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - deprecate useTextExtract and remove functionality\n\n### Patch Changes\n\n- [#741](https://github.com/browserbase/stagehand/pull/741) [`5680d25`](https://github.com/browserbase/stagehand/commit/5680d2509352c383ad502c9f4fabde01fa638833) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - use safeparse for zod validation\n\n- [#783](https://github.com/browserbase/stagehand/pull/783) [`4de92a8`](https://github.com/browserbase/stagehand/commit/4de92a8af461fc95063faf39feee1d49259f58ba) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix the readme logo link\n\n## 2.2.1\n\n### Patch Changes\n\n- [#721](https://github.com/browserbase/stagehand/pull/721) [`be8652e`](https://github.com/browserbase/stagehand/commit/be8652e770b57fdb3299fa0b2efa4eb0e816434e) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix stagehand.close() functionality to include calling browser.close()\n\n- [#724](https://github.com/browserbase/stagehand/pull/724) [`6b413b7`](https://github.com/browserbase/stagehand/commit/6b413b7ad00b13ca0bd53ee2e7393023821408b6) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - rm refine step in extract\n\n- [#712](https://github.com/browserbase/stagehand/pull/712) [`7eafbd9`](https://github.com/browserbase/stagehand/commit/7eafbd9b1a73b37effa444929767df7c592caf02) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - deprecated `onlyVisible` param and remove its functionality\n\n- [#725](https://github.com/browserbase/stagehand/pull/725) [`1b50aa6`](https://github.com/browserbase/stagehand/commit/1b50aa61cf0a429dd6cb2760a08f7f698a50454b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - dont overwrite .describe() when user defines a zod schema with z.string().url().describe()\n\n- [#717](https://github.com/browserbase/stagehand/pull/717) [`f2b7f1f`](https://github.com/browserbase/stagehand/commit/f2b7f1f284eef1f96753319b66c7d0b273a6f8cd) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - don't publish uncompiled ts to npm\n\n- [#719](https://github.com/browserbase/stagehand/pull/719) [`c8d672f`](https://github.com/browserbase/stagehand/commit/c8d672f7c410c256defbc2e87ead99239837aa28) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix `Invalid schema for response_format` error when extracting links\n\n- [#722](https://github.com/browserbase/stagehand/pull/722) [`bebf204`](https://github.com/browserbase/stagehand/commit/bebf2044502333c694743078c5b0c9deae11fb79) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - replace NBSP with regular space & remove special characters from dom+a11y tree\n\n- [#714](https://github.com/browserbase/stagehand/pull/714) [`37d6810`](https://github.com/browserbase/stagehand/commit/37d6810a704773d0383a86f98f5f17c7d5b21975) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix the native AI SDK client implementation to optionally take in an API key\n\n## 2.2.0\n\n### Minor Changes\n\n- [#655](https://github.com/browserbase/stagehand/pull/655) [`8814af9`](https://github.com/browserbase/stagehand/commit/8814af9ece99fddc3dd9fb32671d0513a3a00c67) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - extract links\n\n- [#675](https://github.com/browserbase/stagehand/pull/675) [`35c55eb`](https://github.com/browserbase/stagehand/commit/35c55ebf6c2867801a0a6f6988a883c8cb90cf9a) Thanks [@tkattkat](https://github.com/tkattkat)! - Added Gemini 2.5 Flash to Google supported models\n\n- [#668](https://github.com/browserbase/stagehand/pull/668) [`5c6d2cf`](https://github.com/browserbase/stagehand/commit/5c6d2cf89c9fbf198485506ed9ed75e07aec5cd4) Thanks [@miguelg719](https://github.com/miguelg719)! - Added a new class - Stagehand Evaluator - that wraps around a Stagehand object to determine whether a task is successful or not. Currently used for agent evals\n\n### Patch Changes\n\n- [#706](https://github.com/browserbase/stagehand/pull/706) [`18ac6fb`](https://github.com/browserbase/stagehand/commit/18ac6fba30f45b7557cecb890f4e84c75de8383c) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - remove unused fillInVariables fn\n\n- [#692](https://github.com/browserbase/stagehand/pull/692) [`6b95248`](https://github.com/browserbase/stagehand/commit/6b95248d6e02e5304ce4dd60499e31fc42af57eb) Thanks [@miguelg719](https://github.com/miguelg719)! - Updated the list of OpenAI models (4.1, o3...)\n\n- [#688](https://github.com/browserbase/stagehand/pull/688) [`7d81b3c`](https://github.com/browserbase/stagehand/commit/7d81b3c951c1f3dfc46845aefcc26ff175299bca) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - wrap page.evaluate to make sure we have injected browser side scripts before calling them\n\n- [#664](https://github.com/browserbase/stagehand/pull/664) [`b5ca00a`](https://github.com/browserbase/stagehand/commit/b5ca00a25ad0c33a5f4d3198e1bc59edb9956e7c) Thanks [@miguelg719](https://github.com/miguelg719)! - remove unnecessary log\n\n- [#683](https://github.com/browserbase/stagehand/pull/683) [`8f0f97b`](https://github.com/browserbase/stagehand/commit/8f0f97bc491e23ff0078c802aaf509fd04173c37) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - use javsacript click instead of playwright\n\n- [#705](https://github.com/browserbase/stagehand/pull/705) [`346ef5d`](https://github.com/browserbase/stagehand/commit/346ef5d0132dc1418dac18d26640a8df0435af57) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixed removing a hanging observation map that is no longer used\n\n- [#698](https://github.com/browserbase/stagehand/pull/698) [`c145bc1`](https://github.com/browserbase/stagehand/commit/c145bc1d90ffd0d71c412de3af1c26c121e0b101) Thanks [@sameelarif](https://github.com/sameelarif)! - Fixing LLM client support to natively integrate with AI SDK\n\n- [#687](https://github.com/browserbase/stagehand/pull/687) [`edd6d3f`](https://github.com/browserbase/stagehand/commit/edd6d3feb47aac9f312a5edad78bf850ae1541db) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixed the schema input for Gemini's response model\n\n- [#678](https://github.com/browserbase/stagehand/pull/678) [`5ec43d8`](https://github.com/browserbase/stagehand/commit/5ec43d8b9568c0f86b3e24bd83d1826c837656ed) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - allow form filling when form is not top-most element\n\n- [#694](https://github.com/browserbase/stagehand/pull/694) [`b8cc164`](https://github.com/browserbase/stagehand/commit/b8cc16405b712064a54c8cd591750368a47f35ea) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add telemetry for cua agents to stagehand.metrics\n\n- [#699](https://github.com/browserbase/stagehand/pull/699) [`d9f4243`](https://github.com/browserbase/stagehand/commit/d9f4243f6a8c8d4f3003ad6589f7eb4da6d23d0f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - rm deprecated primitives from stagehand object\n\n- [#710](https://github.com/browserbase/stagehand/pull/710) [`9f4ab76`](https://github.com/browserbase/stagehand/commit/9f4ab76a0c1f0c2171290765c48c3bcea5b50e0f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - support targeted extract for domExtract\n\n- [#677](https://github.com/browserbase/stagehand/pull/677) [`bc5a731`](https://github.com/browserbase/stagehand/commit/bc5a731241f7f4c5040dd672d8e3787555766421) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixes a redundant unnecessary log\n\n## 2.1.0\n\n### Minor Changes\n\n- [#659](https://github.com/browserbase/stagehand/pull/659) [`f9a435e`](https://github.com/browserbase/stagehand/commit/f9a435e938daccfb2e54ca23fad8ef75128a4486) Thanks [@miguelg719](https://github.com/miguelg719)! - Added native support for Google Generative models (Gemini)\n\n### Patch Changes\n\n- [#647](https://github.com/browserbase/stagehand/pull/647) [`ca5467d`](https://github.com/browserbase/stagehand/commit/ca5467de7d31bfb270b6b625224a926c52c97900) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - collapse redundant text nodes into parent elements\n\n- [#636](https://github.com/browserbase/stagehand/pull/636) [`9037430`](https://github.com/browserbase/stagehand/commit/903743097367ba6bb12baa9f0fa8f7985f543fdc) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix token act metrics and inference logging being misplaced as observe metrics and inference logging\n\n- [#648](https://github.com/browserbase/stagehand/pull/648) [`169e7ea`](https://github.com/browserbase/stagehand/commit/169e7ea9e229503ae5958eaa4511531578ee3841) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add mapping of node id -> url\n\n- [#654](https://github.com/browserbase/stagehand/pull/654) [`57a9853`](https://github.com/browserbase/stagehand/commit/57a98538381e0e54fbb734b43c50d61fd0d567df) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix repeated up & down scrolling bug for clicks inside `act`\n\n- [#624](https://github.com/browserbase/stagehand/pull/624) [`cf167a4`](https://github.com/browserbase/stagehand/commit/cf167a437865e8e8bdb8739d22c3b3bb84e185de) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - export stagehand error classes so they can be referenced from @dist\n\n- [#640](https://github.com/browserbase/stagehand/pull/640) [`178f5f0`](https://github.com/browserbase/stagehand/commit/178f5f0a8fecd876adfb4e29983853bdf7ec72fd) Thanks [@yash1744](https://github.com/yash1744)! - Added support for stagehand agents to automatically redirect to https://google.com when the page URL is empty or set to about:blank, preventing empty screenshots and saving tokens.\n\n- [#661](https://github.com/browserbase/stagehand/pull/661) [`bf823a3`](https://github.com/browserbase/stagehand/commit/bf823a36930b0686b416a42302ef8c021b4aba75) Thanks [@kamath](https://github.com/kamath)! - fix press enter\n\n- [#633](https://github.com/browserbase/stagehand/pull/633) [`86724f6`](https://github.com/browserbase/stagehand/commit/86724f6fb0abc7292423ac5bd0bebcd352f95940) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix the getBrowser logic for redundant api calls and throw informed errors\n\n- [#656](https://github.com/browserbase/stagehand/pull/656) [`c630373`](https://github.com/browserbase/stagehand/commit/c630373dede4c775875834bfb860436ba2ea48d2) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - parse out % signs from variables in act\n\n- [#637](https://github.com/browserbase/stagehand/pull/637) [`944bbbf`](https://github.com/browserbase/stagehand/commit/944bbbfe8bfb357b4910584447a93f6f402c3826) Thanks [@kamath](https://github.com/kamath)! - Fix: forward along the stack trace in StagehandDefaultError\n\n## 2.0.0\n\n### Major Changes\n\n- [#591](https://github.com/browserbase/stagehand/pull/591) [`e234a0f`](https://github.com/browserbase/stagehand/commit/e234a0f80bf4c07bcc57265da216cbc4ab3bd19d) Thanks [@miguelg719](https://github.com/miguelg719)! - Announcing **Stagehand 2.0**! 🎉\n\n  We're thrilled to announce the release of Stagehand 2.0, bringing significant improvements to make browser automation more powerful, faster, and easier to use than ever before.\n\n  ### 🚀 New Features\n\n  - **Introducing `stagehand.agent`**: A powerful new way to integrate SOTA Computer use models or Browserbase's [Open Operator](https://operator.browserbase.com) into Stagehand with one line of code! Perfect for multi-step workflows and complex interactions. [Learn more](https://docs.stagehand.dev/concepts/agent)\n  - **Lightning-fast `act` and `extract`**: Major performance improvements to make your automations run significantly faster.\n  - **Enhanced Logging**: Better visibility into what's happening during automation with improved logging and debugging capabilities.\n  - **Comprehensive Documentation**: A completely revamped documentation site with better examples, guides, and best practices.\n  - **Improved Error Handling**: More descriptive errors and better error recovery to help you debug issues faster.\n\n  ### 🛠️ Developer Experience\n\n  - **Better TypeScript Support**: Enhanced type definitions and better IDE integration\n  - **Better Error Messages**: Clearer, more actionable error messages to help you debug faster\n  - **Improved Caching**: More reliable action caching for better performance\n\n  We're excited to see what you build with Stagehand 2.0! For questions or support, join our [Slack community](https://stagehand.dev/slack).\n\n  For more details, check out our [documentation](https://docs.stagehand.dev).\n\n### Minor Changes\n\n- [#588](https://github.com/browserbase/stagehand/pull/588) [`ba9efc5`](https://github.com/browserbase/stagehand/commit/ba9efc5580a536bc3c158e507a6c6695825c2834) Thanks [@sameelarif](https://github.com/sameelarif)! - Added support for offloading agent tasks to the API.\n\n- [#600](https://github.com/browserbase/stagehand/pull/600) [`11e015d`](https://github.com/browserbase/stagehand/commit/11e015daac56dc961b8c8d54ce360fd00d4fee38) Thanks [@sameelarif](https://github.com/sameelarif)! - Added a `stagehand.history` array which stores an array of `act`, `extract`, `observe`, and `goto` calls made. Since this history array is stored on the `StagehandPage` level, it will capture methods even if indirectly called by an agent.\n\n- [#601](https://github.com/browserbase/stagehand/pull/601) [`1d22604`](https://github.com/browserbase/stagehand/commit/1d2260401e27bae25779a55bb2ed7b7153c34fd0) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add custom error classes\n\n- [#599](https://github.com/browserbase/stagehand/pull/599) [`75d8fb3`](https://github.com/browserbase/stagehand/commit/75d8fb36a67cd84eb55b509bf959edc7b05059da) Thanks [@miguelg719](https://github.com/miguelg719)! - cleaner logging with pino\n\n- [#609](https://github.com/browserbase/stagehand/pull/609) [`c92295d`](https://github.com/browserbase/stagehand/commit/c92295d8424dac1a4f81066ca260ade2d5fce80b) Thanks [@kamath](https://github.com/kamath)! - Removed deprecated fields and methods from Stagehand constructor and added cdpUrl to localBrowserLaunchOptions for custom CDP URLs support.\n\n- [#571](https://github.com/browserbase/stagehand/pull/571) [`73d6736`](https://github.com/browserbase/stagehand/commit/73d67368b88002c17814e46e75a99456bf355c4e) Thanks [@miguelg719](https://github.com/miguelg719)! - You can now use Computer Using Agents (CUA) natively in Stagehand for both Anthropic and OpenAI models! This unlocks a brand new frontier of applications for Stagehand users 🤘\n\n- [#619](https://github.com/browserbase/stagehand/pull/619) [`7b0b996`](https://github.com/browserbase/stagehand/commit/7b0b9969a58014ae3e99b2054e4463b785073cfd) Thanks [@sameelarif](https://github.com/sameelarif)! - add disablePino flag to stagehand constructor params\n\n- [#620](https://github.com/browserbase/stagehand/pull/620) [`566e587`](https://github.com/browserbase/stagehand/commit/566e5877a1861e0eae5a118d34efe09d43a37098) Thanks [@kamath](https://github.com/kamath)! - You can now pass in an OpenAI instance as an `llmClient` to the Stagehand constructor! This allows you to use Stagehand with any OpenAI-compatible model, like Ollama, Gemini, etc., as well as OpenAI wrappers like Braintrust.\n\n- [#586](https://github.com/browserbase/stagehand/pull/586) [`c57dc19`](https://github.com/browserbase/stagehand/commit/c57dc19c448b8c2aab82953291f4e38f202c4729) Thanks [@sameelarif](https://github.com/sameelarif)! - Added native Stagehand agentic loop functionality. This allows you to build agentic workflows with a single prompt without using a computer-use model. To try it out, create a `stagehand.agent` without passing in a provider.\n\n### Patch Changes\n\n- [#580](https://github.com/browserbase/stagehand/pull/580) [`179e17c`](https://github.com/browserbase/stagehand/commit/179e17c2d1c9837de49c776d9850a330a759e73f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - refactor \\_performPlaywrightMethod\n\n- [#608](https://github.com/browserbase/stagehand/pull/608) [`71ee10d`](https://github.com/browserbase/stagehand/commit/71ee10d50cb46e83d43fd783e1404569e6f317cf) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - added support for \"scrolling to next/previous chunk\"\n\n- [#594](https://github.com/browserbase/stagehand/pull/594) [`e483484`](https://github.com/browserbase/stagehand/commit/e48348412a6e651967ba22d097d5308af0e8d0a8) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - pass observeHandler into actHandler\n\n- [#569](https://github.com/browserbase/stagehand/pull/569) [`17e8b40`](https://github.com/browserbase/stagehand/commit/17e8b40f94b30f6e253443a4bbb8a3e364e58e38) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - you can now call stagehand.metrics to get token usage metrics. you can also set logInferenceToFile in stagehand config to log the entire call/response history from stagehand & the LLM.\n\n- [#617](https://github.com/browserbase/stagehand/pull/617) [`affa564`](https://github.com/browserbase/stagehand/commit/affa5646658399ab71ed08c1b9ce0fd776b46fca) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - use a11y tree for default extract\n\n- [#589](https://github.com/browserbase/stagehand/pull/589) [`0c4b1e7`](https://github.com/browserbase/stagehand/commit/0c4b1e7e6ff4b8a60af4a2d0d2056bff847227d5) Thanks [@miguelg719](https://github.com/miguelg719)! - Added CDP support for screenshots, find more about the benefits here: https://docs.browserbase.com/features/screenshots#why-use-cdp-for-screenshots%3F\n\n- [#584](https://github.com/browserbase/stagehand/pull/584) [`c7c1a80`](https://github.com/browserbase/stagehand/commit/c7c1a8066be33188ba1e900828045db61410025c) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix to remove unnecessary healtcheck ping on sdk\n\n- [#616](https://github.com/browserbase/stagehand/pull/616) [`2a27e1c`](https://github.com/browserbase/stagehand/commit/2a27e1c8e967befbbbb05ea71369878ac1573658) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixed new opened tab handling for CUA models\n\n- [#582](https://github.com/browserbase/stagehand/pull/582) [`dfd24e6`](https://github.com/browserbase/stagehand/commit/dfd24e638ef3723d3a8a3a33ff7942af0ac4745f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - support api usage for extract with no args\n\n- [#563](https://github.com/browserbase/stagehand/pull/563) [`98166d7`](https://github.com/browserbase/stagehand/commit/98166d76d30bc67d6b04b3d5c39f78f92c254b49) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - support scrolling in `act`\n\n- [#598](https://github.com/browserbase/stagehand/pull/598) [`53889d4`](https://github.com/browserbase/stagehand/commit/53889d4b6e772098beaba2e1ee5a24e6f07706bb) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix the open operator handler to work with anthropic\n\n- [#605](https://github.com/browserbase/stagehand/pull/605) [`b8beaec`](https://github.com/browserbase/stagehand/commit/b8beaec451a03eaa5d12281fe7c8d4eb9c9d7e81) Thanks [@sameelarif](https://github.com/sameelarif)! - Added support for resuming a Stagehand session created on the API.\n\n- [#612](https://github.com/browserbase/stagehand/pull/612) [`cd36068`](https://github.com/browserbase/stagehand/commit/cd3606854c465747c78b44763469dfdfa16db1b0) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - remove all logic related to dom based act\n\n- [#577](https://github.com/browserbase/stagehand/pull/577) [`4fdbf63`](https://github.com/browserbase/stagehand/commit/4fdbf6324a0dc68568bba73ea4d9018b2ed67849) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - remove debugDom\n\n- [#603](https://github.com/browserbase/stagehand/pull/603) [`2a14a60`](https://github.com/browserbase/stagehand/commit/2a14a607f3e7fa3ca9a02670afdc7e60ccfbfb3f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - rm unused handlePossiblePageNavigation\n\n- [#614](https://github.com/browserbase/stagehand/pull/614) [`a59eaef`](https://github.com/browserbase/stagehand/commit/a59eaef67c2f4a0cb07bb0046fe7e93e2ba4dc41) Thanks [@kamath](https://github.com/kamath)! - override whatwg-url to avoid punycode warning\n\n- [#573](https://github.com/browserbase/stagehand/pull/573) [`c24f3c9`](https://github.com/browserbase/stagehand/commit/c24f3c9a58873c3920fab0f9891c2bf5245c9b5e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - return act result in actFromObserve\n\n## 1.14.0\n\n### Minor Changes\n\n- [#518](https://github.com/browserbase/stagehand/pull/518) [`516725f`](https://github.com/browserbase/stagehand/commit/516725fc1c5d12d22caac0078a118c77bfe033a8) Thanks [@sameelarif](https://github.com/sameelarif)! - `act()` can now use `observe()` under the hood, resulting in significant performance improvements. To opt-in to this change, set `slowDomBasedAct: false` in `ActOptions`.\n\n- [#483](https://github.com/browserbase/stagehand/pull/483) [`8c9445f`](https://github.com/browserbase/stagehand/commit/8c9445fde9724ae33eeeb1234fd5b9bbd418bfdb) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - When using `textExtract`, you can now do targetted extraction by passing an xpath string into extract via the `selector` parameter. This limits the dom processing step to a target element, reducing tokens and increasing speed. For example:\n\n  ```typescript\n  const weatherData = await stagehand.page.extract({\n    instruction: \"extract the weather data for Sun, Feb 23 at 11PM\",\n    schema: z.object({\n      temperature: z.string(),\n      weather_description: z.string(),\n      wind: z.string(),\n      humidity: z.string(),\n      barometer: z.string(),\n      visibility: z.string(),\n    }),\n    modelName,\n    useTextExtract,\n    selector: xpath, // xpath of the element to extract from\n  });\n  ```\n\n- [#556](https://github.com/browserbase/stagehand/pull/556) [`499a72d`](https://github.com/browserbase/stagehand/commit/499a72dc56009791ce065270b854b12fc5570050) Thanks [@kamath](https://github.com/kamath)! - You can now set a timeout for dom-based stagehand act! Do this in `act` with `timeoutMs` as a parameter, or set a global param to `actTimeoutMs` in Stagehand config.\n\n- [#544](https://github.com/browserbase/stagehand/pull/544) [`55c9673`](https://github.com/browserbase/stagehand/commit/55c9673c5948743b804d70646f425a61818c7789) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - you can now deterministically get the full text representation of a webpage by calling `extract()` (with no arguments)\n\n- [#538](https://github.com/browserbase/stagehand/pull/538) [`d898d5b`](https://github.com/browserbase/stagehand/commit/d898d5b9e1c3b80e62e72d36d1754b3e50d5a2b4) Thanks [@sameelarif](https://github.com/sameelarif)! - Added `gpt-4.5-preview` and `claude-3-7-sonnet-latest` as supported models.\n\n- [#523](https://github.com/browserbase/stagehand/pull/523) [`44cf7cc`](https://github.com/browserbase/stagehand/commit/44cf7cc9ac1209c97d9153281970899b10a2ddc9) Thanks [@kwt00](https://github.com/kwt00)! You can now natively run Cerebras LLMs! `cerebras-llama-3.3-70b` and `cerebras-llama-3.1-8b` are now supported models as long as `CEREBRAS_API_KEY` is set in your environment.\n\n- [#542](https://github.com/browserbase/stagehand/pull/542) [`cf7fe66`](https://github.com/browserbase/stagehand/commit/cf7fe665e6d1eeda97582ee2816f1dc3a66c6152) Thanks [@sankalpgunturi](https://github.com/sankalpgunturi)! You can now natively run Groq LLMs! `groq-llama-3.3-70b-versatile` and `groq-llama-3.3-70b-specdec` are now supported models as long as `GROQ_API_KEY` is set in your environment.\n\n### Patch Changes\n\n- [#506](https://github.com/browserbase/stagehand/pull/506) [`e521645`](https://github.com/browserbase/stagehand/commit/e5216455ce3fc2a4f4f7aa5614ecc92354eb670c) Thanks [@miguelg719](https://github.com/miguelg719)! - fixing 5s timeout on actHandler\n\n- [#535](https://github.com/browserbase/stagehand/pull/535) [`3782054`](https://github.com/browserbase/stagehand/commit/3782054734dcd0346f84003ddd8e0e484b379459) Thanks [@miguelg719](https://github.com/miguelg719)! - Adding backwards compatibility to new act->observe pipeline by accepting actOptions\n\n- [#508](https://github.com/browserbase/stagehand/pull/508) [`270f666`](https://github.com/browserbase/stagehand/commit/270f6669f1638f52fd5cd3f133f76446ced6ef9f) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixed stagehand to support multiple pages with an enhanced context\n\n- [#559](https://github.com/browserbase/stagehand/pull/559) [`18533ad`](https://github.com/browserbase/stagehand/commit/18533ad824722e4e699323248297e184bae9254e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: continuously adjusting chunk size inside `act`\n\n- [#554](https://github.com/browserbase/stagehand/pull/554) [`5f1868b`](https://github.com/browserbase/stagehand/commit/5f1868bd95478b3eb517319ebca7b0af4e91d144) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix targetted extract issue with scrollintoview and not chunking correctly\n\n- [#555](https://github.com/browserbase/stagehand/pull/555) [`fc5e8b6`](https://github.com/browserbase/stagehand/commit/fc5e8b6c5a606da96e6ed572dc8ffc6caef57576) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where processAllOfDom doesnt scroll to end of page when there is dynamic content\n\n- [#552](https://github.com/browserbase/stagehand/pull/552) [`a25a4cb`](https://github.com/browserbase/stagehand/commit/a25a4cb538d64f50b5bd834dd88e8e6086a73078) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - accept xpaths with 'xpath=' prepended to the front in addition to xpaths without\n\n- [#534](https://github.com/browserbase/stagehand/pull/534) [`f0c162a`](https://github.com/browserbase/stagehand/commit/f0c162a6b4d1ac72c42f26462d7241a08b5c4e0a) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - call this.end() if the process exists\n\n- [#528](https://github.com/browserbase/stagehand/pull/528) [`c820bfc`](https://github.com/browserbase/stagehand/commit/c820bfcfc9571fea90afd1595775c5946118cfaf) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - handle attempt to close session that has already been closed when using the api\n\n- [#520](https://github.com/browserbase/stagehand/pull/520) [`f49eebd`](https://github.com/browserbase/stagehand/commit/f49eebd98c1d61413a3ea4c798595db601d55da8) Thanks [@miguelg719](https://github.com/miguelg719)! - Performing act from a 'not-supported' ObserveResult will now throw an informed error\n\n## 1.13.1\n\n### Patch Changes\n\n- [#509](https://github.com/browserbase/stagehand/pull/509) [`a7d345e`](https://github.com/browserbase/stagehand/commit/a7d345e75434aebb656e1aa5aa61caed00dc99a8) Thanks [@miguelg719](https://github.com/miguelg719)! - Bun runs will now throw a more informed error\n\n## 1.13.0\n\n### Minor Changes\n\n- [#486](https://github.com/browserbase/stagehand/pull/486) [`33f2b3f`](https://github.com/browserbase/stagehand/commit/33f2b3f8deff86ac2073b6d35b7413b0aeaba2f9) Thanks [@sameelarif](https://github.com/sameelarif)! - [Unreleased] Parameterized offloading Stagehand method calls to the Stagehand API. In the future, this will allow for better observability and debugging experience.\n\n- [#494](https://github.com/browserbase/stagehand/pull/494) [`9ba4b0b`](https://github.com/browserbase/stagehand/commit/9ba4b0b563cbc77d40cac31c11e17e365a9d1749) Thanks [@pkiv](https://github.com/pkiv)! - Added LocalBrowserLaunchOptions to provide comprehensive configuration options for local browser instances. Deprecated the top-level headless option in favor of using localBrowserLaunchOptions.headless\n\n- [#500](https://github.com/browserbase/stagehand/pull/500) [`a683fab`](https://github.com/browserbase/stagehand/commit/a683fab9ca90c45d78f6602a228c2d3219b776dc) Thanks [@miguelg719](https://github.com/miguelg719)! - Including Iframes in ObserveResults. This appends any iframe(s) found in the page to the end of observe results on any observe call.\n\n- [#504](https://github.com/browserbase/stagehand/pull/504) [`577662e`](https://github.com/browserbase/stagehand/commit/577662e985a6a6b0477815853d98610f3a6b567d) Thanks [@sameelarif](https://github.com/sameelarif)! - Enabled support for Browserbase captcha solving after page navigations. This can be enabled with the new constructor parameter: `waitForCaptchaSolves`.\n\n- [#496](https://github.com/browserbase/stagehand/pull/496) [`28ca9fb`](https://github.com/browserbase/stagehand/commit/28ca9fbc6f3cdc88437001108a9a6c4388ba0303) Thanks [@sameelarif](https://github.com/sameelarif)! - Fixed browserbaseSessionCreateParams not being passed in to the API initialization payload.\n\n### Patch Changes\n\n- [#459](https://github.com/browserbase/stagehand/pull/459) [`62a29ee`](https://github.com/browserbase/stagehand/commit/62a29eea982bbb855e2f885c09ac4c1334f3e0dc) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - create a11y + dom hybrid input for observe\n\n- [#463](https://github.com/browserbase/stagehand/pull/463) [`e40bf6f`](https://github.com/browserbase/stagehand/commit/e40bf6f517331fc9952c3c9f2683b7e02ffb9735) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - include 'Scrollable' annotations in a11y-dom hybrid\n\n- [#480](https://github.com/browserbase/stagehand/pull/480) [`4c07c44`](https://github.com/browserbase/stagehand/commit/4c07c444f0e71faf54413b2eeab760c7916a36e3) Thanks [@miguelg719](https://github.com/miguelg719)! - Adding a fallback try on actFromObserveResult to use the description from observe and call regular act.\n\n- [#487](https://github.com/browserbase/stagehand/pull/487) [`2c855cf`](https://github.com/browserbase/stagehand/commit/2c855cffdfa2b0af9924612b9c59df7b65df6443) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - update refine extraction prompt to ensure correct schema is used\n\n- [#497](https://github.com/browserbase/stagehand/pull/497) [`945ed04`](https://github.com/browserbase/stagehand/commit/945ed0426d34d2cb833aec8ba67bd4cba6c3b660) Thanks [@kamath](https://github.com/kamath)! - add gpt 4o november snapshot\n\n## 1.12.0\n\n### Minor Changes\n\n- [#426](https://github.com/browserbase/stagehand/pull/426) [`bbbcee7`](https://github.com/browserbase/stagehand/commit/bbbcee7e7d86f5bf90cbb93f2ac9ad5935f15896) Thanks [@miguelg719](https://github.com/miguelg719)! - Observe got a major upgrade. Now it will return a suggested playwright method with any necessary arguments for the generated candidate elements. It also includes a major speedup when using a11y tree processing for context.\n\n- [#452](https://github.com/browserbase/stagehand/pull/452) [`16837ec`](https://github.com/browserbase/stagehand/commit/16837ece839e192fbf7b68bec128dd02f22c2613) Thanks [@kamath](https://github.com/kamath)! - add o3-mini to availablemodel\n\n- [#441](https://github.com/browserbase/stagehand/pull/441) [`1032d7d`](https://github.com/browserbase/stagehand/commit/1032d7d7d9c1ef8f30183c9019ea8324f1bdd5c6) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - allow act to accept observe output\n\n### Patch Changes\n\n- [#458](https://github.com/browserbase/stagehand/pull/458) [`da2e5d1`](https://github.com/browserbase/stagehand/commit/da2e5d1314b7504877fd50090e6a4b47f44fb9f6) Thanks [@miguelg719](https://github.com/miguelg719)! - Updated getAccessibilityTree() to make sure it doesn't skip useful nodes. Improved getXPathByResolvedObjectId() to account for text nodes and not skip generation\n\n- [#448](https://github.com/browserbase/stagehand/pull/448) [`b216072`](https://github.com/browserbase/stagehand/commit/b2160723923ed78eba83e75c7270634ca7d217de) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - improve handling of radio button clicks\n\n- [#445](https://github.com/browserbase/stagehand/pull/445) [`5bc514f`](https://github.com/browserbase/stagehand/commit/5bc514fc18e6634b1c81553bbc1e8b7d71b67d34) Thanks [@miguelg719](https://github.com/miguelg719)! - Adding back useAccessibilityTree param to observe with a deprecation warning/error indicating to use onlyVisible instead\n\n## 1.11.0\n\n### Minor Changes\n\n- [#428](https://github.com/browserbase/stagehand/pull/428) [`5efeb5a`](https://github.com/browserbase/stagehand/commit/5efeb5ad44852efe7b260862729a5ac74eaa0228) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - temporarily remove vision\n\n## 1.10.1\n\n### Patch Changes\n\n- [#422](https://github.com/browserbase/stagehand/pull/422) [`a2878d0`](https://github.com/browserbase/stagehand/commit/a2878d0acaf393b37763fb0c07b1a24043f7eb8d) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixing a build type error for async functions being called inside evaulate for observeHandler.\n\n## 1.10.0\n\n### Minor Changes\n\n- [#412](https://github.com/browserbase/stagehand/pull/412) [`4aa4813`](https://github.com/browserbase/stagehand/commit/4aa4813ad62cefc333a04ea6b1004f5888dec70f) Thanks [@miguelg719](https://github.com/miguelg719)! - Includes a new format to get website context using accessibility (a11y) trees. The new context is provided optionally with the flag useAccessibilityTree for observe tasks.\n\n- [#417](https://github.com/browserbase/stagehand/pull/417) [`1f2b2c5`](https://github.com/browserbase/stagehand/commit/1f2b2c57d93e3b276c61224e1e26c65c2cb50e12) Thanks [@sameelarif](https://github.com/sameelarif)! - Simplify Stagehand method calls by allowing a simple string input instead of an options object.\n\n- [#405](https://github.com/browserbase/stagehand/pull/405) [`0df1e23`](https://github.com/browserbase/stagehand/commit/0df1e233d4ad4ba39da457b6ed85916d8d20e12e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - in ProcessAllOfDom, scroll on large scrollable elements instead of just the root DOM\n\n- [#373](https://github.com/browserbase/stagehand/pull/373) [`ff00965`](https://github.com/browserbase/stagehand/commit/ff00965160d568ae0bc3ca437c01f95b5c6e9039) Thanks [@sameelarif](https://github.com/sameelarif)! - Allow the input of custom instructions into the constructor so that users can guide, or provide guardrails to, the LLM in making decisions.\n\n### Patch Changes\n\n- [#386](https://github.com/browserbase/stagehand/pull/386) [`2cee0a4`](https://github.com/browserbase/stagehand/commit/2cee0a45ae2b48d1de6543b196e338e7021e59fe) Thanks [@kamath](https://github.com/kamath)! - add demo gif\n\n- [#362](https://github.com/browserbase/stagehand/pull/362) [`9c20de3`](https://github.com/browserbase/stagehand/commit/9c20de3e66f0ac20374d5e5e02eb107c620a2263) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - reduce collisions and improve accuracy of textExtract\n\n- [#413](https://github.com/browserbase/stagehand/pull/413) [`737b4b2`](https://github.com/browserbase/stagehand/commit/737b4b208c9214e8bb22535ab7a8daccf37610d9) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - remove topMostElement check when verifying visibility of text nodes\n\n- [#388](https://github.com/browserbase/stagehand/pull/388) [`e93561d`](https://github.com/browserbase/stagehand/commit/e93561d7875210ce7bd7fe841fb52decf6011fb3) Thanks [@kamath](https://github.com/kamath)! - Export LLMClient type\n\n## 1.9.0\n\n### Minor Changes\n\n- [#374](https://github.com/browserbase/stagehand/pull/374) [`207244e`](https://github.com/browserbase/stagehand/commit/207244e3a46c4474d4d28db039eab131164790ca) Thanks [@sameelarif](https://github.com/sameelarif)! - Pass in a Stagehand Page object into the `on(\"popup\")` listener to allow for multi-page handling.\n\n- [#367](https://github.com/browserbase/stagehand/pull/367) [`75c0e20`](https://github.com/browserbase/stagehand/commit/75c0e20cde54951399753e0fa841df463e1271b8) Thanks [@kamath](https://github.com/kamath)! - Logger in LLMClient is inherited by default from Stagehand. Named rather than positional arguments are used in implemented LLMClients.\n\n- [#381](https://github.com/browserbase/stagehand/pull/381) [`db2ef59`](https://github.com/browserbase/stagehand/commit/db2ef5997664e81b1dfb5ca992392362f2d3bab1) Thanks [@kamath](https://github.com/kamath)! - make logs only sync\n\n- [#385](https://github.com/browserbase/stagehand/pull/385) [`5899ec2`](https://github.com/browserbase/stagehand/commit/5899ec2c4b73c636bfd8120ec3aac225af7dd949) Thanks [@sameelarif](https://github.com/sameelarif)! - Moved the LLMClient logger paremeter to the createChatCompletion method options.\n\n- [#364](https://github.com/browserbase/stagehand/pull/364) [`08907eb`](https://github.com/browserbase/stagehand/commit/08907ebbc2cb47cfc3151946764656a7f4ce99c6) Thanks [@kamath](https://github.com/kamath)! - exposed llmClient in stagehand constructor\n\n### Patch Changes\n\n- [#383](https://github.com/browserbase/stagehand/pull/383) [`a77efcc`](https://github.com/browserbase/stagehand/commit/a77efccfde3a3948013eda3a52935e8a21d45b3e) Thanks [@sameelarif](https://github.com/sameelarif)! - Unified LLM input/output types for reduced dependence on OpenAI types\n\n- [`b7b3701`](https://github.com/browserbase/stagehand/commit/b7b370160bf35b09f5dc132f6e86f6e34fb70a85) Thanks [@kamath](https://github.com/kamath)! - Fix $1-types exposed to the user\n\n- [#353](https://github.com/browserbase/stagehand/pull/353) [`5c6f14b`](https://github.com/browserbase/stagehand/commit/5c6f14bade201e08cb86d2e14e246cb65707f7ee) Thanks [@kamath](https://github.com/kamath)! - Throw custom error if context is referenced without initialization, remove act/extract handler from index\n\n- [#360](https://github.com/browserbase/stagehand/pull/360) [`89841fc`](https://github.com/browserbase/stagehand/commit/89841fc42ae82559baddfe2a9593bc3260c082a2) Thanks [@kamath](https://github.com/kamath)! - Remove stagehand nav entirely\n\n- [#379](https://github.com/browserbase/stagehand/pull/379) [`b1c6579`](https://github.com/browserbase/stagehand/commit/b1c657976847de86d82324030f90c2f6a1f3f976) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - dont require LLM Client to use non-ai stagehand functions\n\n- [#371](https://github.com/browserbase/stagehand/pull/371) [`30e7d09`](https://github.com/browserbase/stagehand/commit/30e7d091445004c71aec1748d3a7d75fb86d1f11) Thanks [@kamath](https://github.com/kamath)! - pretty readme :)\n\n- [#382](https://github.com/browserbase/stagehand/pull/382) [`a41271b`](https://github.com/browserbase/stagehand/commit/a41271baf351e20f4c79b4b654d8a947b615a121) Thanks [@sameelarif](https://github.com/sameelarif)! - Added example implementation of the Vercel AI SDK as an LLMClient\n\n- [#344](https://github.com/browserbase/stagehand/pull/344) [`c1cf345`](https://github.com/browserbase/stagehand/commit/c1cf34535ed30262989b1dbe262fb0414cdf8230) Thanks [@kamath](https://github.com/kamath)! - Remove duplicate logging and expose Page/BrowserContext types\n\n## 1.8.0\n\n### Minor Changes\n\n- [#324](https://github.com/browserbase/stagehand/pull/324) [`cd23fa3`](https://github.com/browserbase/stagehand/commit/cd23fa33450107f29cb1ddb6edadfc769d336aa5) Thanks [@kamath](https://github.com/kamath)! - Move stagehand.act() -> stagehand.page.act() and deprecate stagehand.act()\n\n- [#319](https://github.com/browserbase/stagehand/pull/319) [`bacbe60`](https://github.com/browserbase/stagehand/commit/bacbe608058304bfa1f0ab049da4d8aa90e8d6f7) Thanks [@kamath](https://github.com/kamath)! - We now wrap playwright page/context within StagehandPage and StagehandContext objects. This helps us augment the Stagehand experience by being able to augment the underlying Playwright\n\n- [#324](https://github.com/browserbase/stagehand/pull/324) [`cd23fa3`](https://github.com/browserbase/stagehand/commit/cd23fa33450107f29cb1ddb6edadfc769d336aa5) Thanks [@kamath](https://github.com/kamath)! - moves extract and act -> page and deprecates stagehand.extract and stagehand.observe\n\n### Patch Changes\n\n- [#320](https://github.com/browserbase/stagehand/pull/320) [`c0cdd0e`](https://github.com/browserbase/stagehand/commit/c0cdd0e985d66f0464d2e70b7d0cb343b0efbd3f) Thanks [@kamath](https://github.com/kamath)! - bug fix: set this.env to LOCAL if BROWSERBASE_API_KEY is not defined\n\n- [#325](https://github.com/browserbase/stagehand/pull/325) [`cc46f34`](https://github.com/browserbase/stagehand/commit/cc46f345c0a1dc0af4abae7e207833df17da50e7) Thanks [@pkiv](https://github.com/pkiv)! - only start domdebug if enabled\n\n## 1.7.0\n\n### Minor Changes\n\n- [#316](https://github.com/browserbase/stagehand/pull/316) [`902e633`](https://github.com/browserbase/stagehand/commit/902e633e126a58b80b757ea0ecada01a7675a473) Thanks [@kamath](https://github.com/kamath)! - rename browserbaseResumeSessionID -> browserbaseSessionID\n\n- [#296](https://github.com/browserbase/stagehand/pull/296) [`f11da27`](https://github.com/browserbase/stagehand/commit/f11da27a20409c240ceeea2003d520f676def61a) Thanks [@kamath](https://github.com/kamath)! - - Deprecate fields in `init` in favor of constructor options\n\n  - Deprecate `initFromPage` in favor of `browserbaseResumeSessionID` in constructor\n  - Rename `browserBaseSessionCreateParams` -> `browserbaseSessionCreateParams`\n\n- [#304](https://github.com/browserbase/stagehand/pull/304) [`0b72f75`](https://github.com/browserbase/stagehand/commit/0b72f75f6a62aaeb28b0c488ae96db098d6a2846) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add textExtract: an optional, text based approach to the existing extract method. textExtract often performs better on long form extraction tasks. By default `extract` uses the existing approach `domExtract`.\n\n- [#298](https://github.com/browserbase/stagehand/pull/298) [`55f0cd2`](https://github.com/browserbase/stagehand/commit/55f0cd2fe7976e800833ec6e41e9af62d88d09d5) Thanks [@kamath](https://github.com/kamath)! - Add sessionId to public params\n\n### Patch Changes\n\n- [#283](https://github.com/browserbase/stagehand/pull/283) [`b902192`](https://github.com/browserbase/stagehand/commit/b902192bc7ff8eb02c85150c1fe6f89c2a95b211) Thanks [@sameelarif](https://github.com/sameelarif)! - allowed customization of eval config via .env\n\n- [#299](https://github.com/browserbase/stagehand/pull/299) [`fbe2300`](https://github.com/browserbase/stagehand/commit/fbe23007176488043c2415519f25021612fff989) Thanks [@sameelarif](https://github.com/sameelarif)! - log playwright actions for better debugging\n\n## 1.6.0\n\n### Minor Changes\n\n- [#286](https://github.com/browserbase/stagehand/pull/286) [`9605836`](https://github.com/browserbase/stagehand/commit/9605836ee6b8207ed7dc9146e12ced1c78630d59) Thanks [@kamath](https://github.com/kamath)! - minor improvement in action + new eval case\n\n- [#279](https://github.com/browserbase/stagehand/pull/279) [`d6d7057`](https://github.com/browserbase/stagehand/commit/d6d70570623a718354797ef83aa8489eacc085d1) Thanks [@kamath](https://github.com/kamath)! - Add support for o1-mini and o1-preview in OpenAIClient\n\n- [#282](https://github.com/browserbase/stagehand/pull/282) [`5291797`](https://github.com/browserbase/stagehand/commit/529179724a53bf2fd578a4012fd6bc6b7348d1ae) Thanks [@kamath](https://github.com/kamath)! - Added eslint for stricter type checking. Streamlined most of the internal types throughout the cache, llm, and handlers. This should make it easier to add new LLMs down the line, maintain and update the existing code, and make it easier to add new features in the future. Types can be checked by running `npx eslint .` from the project directory.\n\n### Patch Changes\n\n- [#270](https://github.com/browserbase/stagehand/pull/270) [`6b10b3b`](https://github.com/browserbase/stagehand/commit/6b10b3b1160649b19f50d66588395ceb679b3d68) Thanks [@sameelarif](https://github.com/sameelarif)! - add close link to readme\n\n- [#288](https://github.com/browserbase/stagehand/pull/288) [`5afa0b9`](https://github.com/browserbase/stagehand/commit/5afa0b940a9f379a3719a5bbae249dd2a9ef8380) Thanks [@kamath](https://github.com/kamath)! - add multi-region support for browserbase\n\n- [#284](https://github.com/browserbase/stagehand/pull/284) [`474217c`](https://github.com/browserbase/stagehand/commit/474217cfaff8e68614212b66baa62d35493fd2ce) Thanks [@kamath](https://github.com/kamath)! - Build wasn't working, this addresses tsc failure.\n\n- [#236](https://github.com/browserbase/stagehand/pull/236) [`85483fe`](https://github.com/browserbase/stagehand/commit/85483fe091544fc079015c62b6923b03f8b9caa7) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - reduce chunk size\n\n## 1.5.0\n\n### Minor Changes\n\n- [#266](https://github.com/browserbase/stagehand/pull/266) [`0e8f34f`](https://github.com/browserbase/stagehand/commit/0e8f34fc15aee91c548d09534deaccc8adca7c4d) Thanks [@kamath](https://github.com/kamath)! - Install wasn't working from NPM due to misconfigured build step. This attempts to fix that.\n\n## 1.4.0\n\n### Minor Changes\n\n- [#253](https://github.com/browserbase/stagehand/pull/253) [`598cae2`](https://github.com/browserbase/stagehand/commit/598cae230c7b8d4e31ae22fd63047a91b63e51b8) Thanks [@sameelarif](https://github.com/sameelarif)! - clean up contexts after use\n\n### Patch Changes\n\n- [#225](https://github.com/browserbase/stagehand/pull/225) [`a2366fe`](https://github.com/browserbase/stagehand/commit/a2366feb023180fbb2ccc7a8379692f9f8347fe5) Thanks [@sameelarif](https://github.com/sameelarif)! - Ensuring cross-platform compatibility with tmp directories\n\n- [#249](https://github.com/browserbase/stagehand/pull/249) [`7d06d43`](https://github.com/browserbase/stagehand/commit/7d06d43f2b9a477fed35793d7479de9b183e8d53) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix broken evals\n\n- [#227](https://github.com/browserbase/stagehand/pull/227) [`647eefd`](https://github.com/browserbase/stagehand/commit/647eefd651852eec495faa1b8f4dbe6b1da17999) Thanks [@kamath](https://github.com/kamath)! - Fix debugDom still showing chunks when set to false\n\n- [#250](https://github.com/browserbase/stagehand/pull/250) [`5886620`](https://github.com/browserbase/stagehand/commit/5886620dd1b0a57c68bf810cf130df2ca0a50a69) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add ci specific evals\n\n- [#222](https://github.com/browserbase/stagehand/pull/222) [`8dff026`](https://github.com/browserbase/stagehand/commit/8dff02674df7a6448f2262c7e212b58c03be57bc) Thanks [@sameelarif](https://github.com/sameelarif)! - Streamline type definitions and fix existing typescript errors\n\n- [#232](https://github.com/browserbase/stagehand/pull/232) [`b9f9949`](https://github.com/browserbase/stagehand/commit/b9f99494021e6a9e2487b77bb64ed0a491751400) Thanks [@kamath](https://github.com/kamath)! - Minor changes to package.json and tsconfig, mainly around the build process. Also add more type defs and remove unused dependencies.\n\n## 1.3.0\n\n### Minor Changes\n\n- [#195](https://github.com/browserbase/stagehand/pull/195) [`87a6305`](https://github.com/browserbase/stagehand/commit/87a6305d9a2faf1ab5915965913bc14d5cc15772) Thanks [@kamath](https://github.com/kamath)! - - Adds structured and more standardized JSON logging\n  - Doesn't init cache if `enableCaching` is false, preventing `tmp/.cache` from being created\n  - Updates bundling for browser-side code to support NextJS and serverless\n\n## 1.2.0\n\n### Minor Changes\n\n- [#179](https://github.com/browserbase/stagehand/pull/179) [`0031871`](https://github.com/browserbase/stagehand/commit/0031871d5a6d6180f272a68b88a8634e5a991785) Thanks [@navidkpr](https://github.com/navidkpr)! - Fixes:\n\n  The last big change we pushed out, introduced a small regression. As a result, the gray outline showing the elements Stagehand is looking out is missing. This commit fixes that. We now process selectorMap properly now (using the updated type Record<number, string[]\n\n  Improved the action prompt:\n\n  Improved the structure\n  Made it more straightforward\n  Improved working for completed arg and prioritized precision over recall\n\n## 1.1.0\n\n### Minor Changes\n\n- [`9206ec6`](https://github.com/browserbase/stagehand/commit/9206ec640b2d0af9170f0a31788ab1eac448357b) Thanks [@kamath](https://github.com/kamath)! - Connect to a minor session\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Browserbase Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div id=\"toc\" align=\"center\" style=\"margin-bottom: 0;\">\n  <ul style=\"list-style: none; margin: 0; padding: 0;\">\n    <a href=\"https://stagehand.dev\">\n      <picture>\n        <source media=\"(prefers-color-scheme: dark)\" srcset=\"media/dark_logo.png\" />\n        <img alt=\"Stagehand\" src=\"media/light_logo.png\" width=\"200\" style=\"margin-right: 30px;\" />\n      </picture>\n    </a>\n  </ul>\n</div>\n<p align=\"center\">\n  <strong>The AI Browser Automation Framework</strong><br>\n  <a href=\"https://docs.stagehand.dev\">Read the Docs</a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/browserbase/stagehand/tree/main?tab=MIT-1-ov-file#MIT-1-ov-file\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"media/dark_license.svg\" />\n      <img alt=\"MIT License\" src=\"media/light_license.svg\" />\n    </picture>\n  </a>\n  <a href=\"https://stagehand.dev/discord\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"media/dark_discord.svg\" />\n      <img alt=\"Discord Community\" src=\"media/light_discord.svg\" />\n    </picture>\n  </a>\n</p>\n\n<p align=\"center\">\n\t<a href=\"https://trendshift.io/repositories/12122\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/12122\" alt=\"browserbase%2Fstagehand | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://deepwiki.com/browserbase/stagehand\">\n    <img alt=\"Ask DeepWiki\" src=\"https://deepwiki.com/badge.svg\" />\n  </a>\n</p>\n\n<p align=\"center\">\nIf you're looking for the Python implementation, you can find it \n<a href=\"https://github.com/browserbase/stagehand-python\"> here</a>\n</p>\n\n<div align=\"center\" style=\"display: flex; align-items: center; justify-content: center; gap: 4px; margin-bottom: 0;\">\n  <b>Vibe code</b>\n  <span style=\"font-size: 1.05em;\"> Stagehand with </span>\n  <a href=\"https://director.ai\" style=\"display: flex; align-items: center;\">\n    <span>Director</span>\n  </a>\n  <span> </span>\n  <picture>\n    <img alt=\"Director\" src=\"media/director_icon.svg\" width=\"25\" />\n  </picture>\n</div>\n\n## What is Stagehand?\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## Why Stagehand?\n\nMost existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.\n\n1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.\n\n2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.\n\n3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks. \n\n## Getting Started\n\nStart with Stagehand with one line of code, or check out our [Quickstart Guide](https://docs.stagehand.dev/v3/first-steps/quickstart) for more information:\n\n```bash\nnpx create-browser-app\n```\n\n## Example\n\nHere's how to build a sample browser automation with Stagehand:\n\n```typescript\n// Stagehand's CDP engine provides an optimized, low level interface to the browser built for automation\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://github.com/browserbase\");\n\n// Use act() to execute individual actions\nawait stagehand.act(\"click on the stagehand repo\");\n\n// Use agent() for multi-step tasks\nconst agent = stagehand.agent();\nawait agent.execute(\"Get to the latest PR\");\n\n// Use extract() to get structured data from the page\nconst { author, title } = await stagehand.extract(\n  \"extract the author and title of the PR\",\n  z.object({\n    author: z.string().describe(\"The username of the PR author\"),\n    title: z.string().describe(\"The title of the PR\"),\n  }),\n);\n```\n\n## Documentation\n\nVisit [docs.stagehand.dev](https://docs.stagehand.dev) to view the full documentation.\n\n\n### Build and Run from Source\n\n```bash\ngit clone https://github.com/browserbase/stagehand.git\ncd stagehand\npnpm install\npnpm run build\npnpm run example # run the blank script at ./examples/example.ts\n```\n\nStagehand is best when you have an API key for an LLM provider and Browserbase credentials. To add these to your project, run:\n\n```bash\ncp .env.example .env\nnano .env # Edit the .env file to add API keys\n```\n\n### Installing from a branch\n\nYou can install and build Stagehand directly from a github branch using [gitpkg](https://github.com/EqualMa/gitpkg)\n\nIn your project's `package.json` set:\n```json\n\"@browserbasehq/stagehand\": \"https://gitpkg.now.sh/browserbase/stagehand/packages/core?<branchName>\",\n```\n\n\n## Contributing\n\n> [!NOTE]\n> We highly value contributions to Stagehand! For questions or support, please join our [Discord community](https://stagehand.dev/discord).\n\nAt a high level, we're focused on improving reliability, extensibility, speed, and cost in that order of priority. If you're interested in contributing, **bug fixes and small improvements are the best way to get started**. For more involved features, we strongly recommend reaching out to [Miguel Gonzalez](https://x.com/miguel_gonzf) or [Paul Klein](https://x.com/pk_iv) in our [Discord community](https://stagehand.dev/discord) before starting to ensure that your contribution aligns with our goals.\n\n<!-- For more information, please see our [Contributing Guide](https://docs.stagehand.dev/examples/contributing). -->\n\n## Acknowledgements\n\nWe'd like to thank the following people for their major contributions to Stagehand:\n- [Paul Klein](https://github.com/pkiv)\n- [Sean McGuire](https://github.com/seanmcguire12)\n- [Miguel Gonzalez](https://github.com/miguelg719)\n- [Sameel Arif](https://github.com/sameelarif)\n- [Thomas Katwan](https://github.com/tkattkat)\n- [Filip Michalsky](https://github.com/filip-michalsky)\n- [Anirudh Kamath](https://github.com/kamath)\n- [Jeremy Press](https://x.com/jeremypress)\n- [Navid Pour](https://github.com/navidpour)\n\n## License\n\nLicensed under the MIT License.\n\nCopyright 2025 Browserbase, Inc.\n"
  },
  {
    "path": "claude.md",
    "content": "# Stagehand Project\n\nThis is a project that uses Stagehand V3, a browser automation framework with AI-powered `act`, `extract`, `observe`, and `agent` methods.\n\nThe main class can be imported as `Stagehand` from `@browserbasehq/stagehand`.\n\n**Key Classes:**\n\n- `Stagehand`: Main orchestrator class providing `act`, `extract`, `observe`, and `agent` methods\n- `context`: A `V3Context` object that manages browser contexts and pages\n- `page`: Individual page objects accessed via `stagehand.context.pages()[i]` or created with `stagehand.context.newPage()`\n\n## Initialize\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\", // or \"BROWSERBASE\"\n  verbose: 2, // 0, 1, or 2\n  model: \"openai/gpt-4.1-mini\", // or any supported model\n});\n\nawait stagehand.init();\n\n// Access the browser context and pages\nconst page = stagehand.context.pages()[0];\nconst context = stagehand.context;\n\n// Create new pages if needed\nconst page2 = await stagehand.context.newPage();\n```\n\n## Act\n\nActions are called on the `stagehand` instance (not the page). Use atomic, specific instructions:\n\n```typescript\n// Act on the current active page\nawait stagehand.act(\"click the sign in button\");\n\n// Act on a specific page (when you need to target a page that isn't currently active)\nawait stagehand.act(\"click the sign in button\", { page: page2 });\n```\n\n**Important:** Act instructions should be atomic and specific:\n\n- ✅ Good: \"Click the sign in button\" or \"Type 'hello' into the search input\"\n- ❌ Bad: \"Order me pizza\" or \"Type in the search bar and hit enter\" (multi-step)\n\n### Observe + Act Pattern (Recommended)\n\nCache the results of `observe` to avoid unexpected DOM changes:\n\n```typescript\nconst instruction = \"Click the sign in button\";\n\n// Get candidate actions\nconst actions = await stagehand.observe(instruction);\n\n// Execute the first action\nawait stagehand.act(actions[0]);\n```\n\nTo target a specific page:\n\n```typescript\nconst actions = await stagehand.observe(\"select blue as the favorite color\", {\n  page: page2,\n});\nawait stagehand.act(actions[0], { page: page2 });\n```\n\n## Extract\n\nExtract data from pages using natural language instructions. The `extract` method is called on the `stagehand` instance.\n\n### Basic Extraction (with schema)\n\n```typescript\nimport { z } from \"zod\";\n\n// Extract with explicit schema\nconst data = await stagehand.extract(\n  \"extract all apartment listings with prices and addresses\",\n  z.object({\n    listings: z.array(\n      z.object({\n        price: z.string(),\n        address: z.string(),\n      }),\n    ),\n  }),\n);\n\nconsole.log(data.listings);\n```\n\n### Simple Extraction (without schema)\n\n```typescript\n// Extract returns a default object with 'extraction' field\nconst result = await stagehand.extract(\"extract the sign in button text\");\n\nconsole.log(result);\n// Output: { extraction: \"Sign in\" }\n\n// Or destructure directly\nconst { extraction } = await stagehand.extract(\n  \"extract the sign in button text\",\n);\nconsole.log(extraction); // \"Sign in\"\n```\n\n### Targeted Extraction\n\nExtract data from a specific element using a selector:\n\n```typescript\nconst reason = await stagehand.extract(\n  \"extract the reason why script injection fails\",\n  z.string(),\n  { selector: \"/html/body/div[2]/div[3]/iframe/html/body/p[2]\" },\n);\n```\n\n### URL Extraction\n\nWhen extracting links or URLs, use `z.string().url()`:\n\n```typescript\nconst { links } = await stagehand.extract(\n  \"extract all navigation links\",\n  z.object({\n    links: z.array(z.string().url()),\n  }),\n);\n```\n\n### Extracting from a Specific Page\n\n```typescript\n// Extract from a specific page (when you need to target a page that isn't currently active)\nconst data = await stagehand.extract(\n  \"extract the placeholder text on the name field\",\n  { page: page2 },\n);\n```\n\n## Observe\n\nPlan actions before executing them. Returns an array of candidate actions:\n\n```typescript\n// Get candidate actions on the current active page\nconst [action] = await stagehand.observe(\"Click the sign in button\");\n\n// Execute the action\nawait stagehand.act(action);\n```\n\nObserving on a specific page:\n\n```typescript\n// Target a specific page (when you need to target a page that isn't currently active)\nconst actions = await stagehand.observe(\"find the next page button\", {\n  page: page2,\n});\nawait stagehand.act(actions[0], { page: page2 });\n```\n\n## Agent\n\nUse the `agent` method to autonomously execute complex, multi-step tasks.\n\n### Basic Agent Usage\n\n```typescript\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://www.google.com\");\n\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.0-flash\",\n  executionModel: \"google/gemini-2.0-flash\",\n});\n\nconst result = await agent.execute({\n  instruction: \"Search for the stock price of NVDA\",\n  maxSteps: 20,\n});\n\nconsole.log(result.message);\n```\n\n### Computer Use Agent (CUA)\n\nFor more advanced scenarios using computer-use models:\n\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\", // Enable Computer Use Agent mode\n  model: \"anthropic/claude-sonnet-4-20250514\",\n  // or \"google/gemini-2.5-computer-use-preview-10-2025\"\n  systemPrompt: `You are a helpful assistant that can use a web browser.\n    Do not ask follow up questions, the user will trust your judgement.`,\n});\n\nawait agent.execute({\n  instruction: \"Apply for a library card at the San Francisco Public Library\",\n  maxSteps: 30,\n});\n```\n\n### Agent with Custom Model Configuration\n\n```typescript\nconst agent = stagehand.agent({\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GEMINI_API_KEY,\n  },\n  systemPrompt: `You are a helpful assistant.`,\n});\n```\n\n### Agent with Integrations (MCP/External Tools)\n\n```typescript\nconst agent = stagehand.agent({\n  integrations: [`https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`],\n  systemPrompt: `You have access to the Exa search tool.`,\n});\n```\n\n### Agent Hybrid Mode\n\nHybrid mode uses both DOM-based and coordinate-based tools (act, click, type, dragAndDrop) for visual interactions. This requires `experimental: true` and models that support reliable coordinate-based actions.\n\n**Recommended models for hybrid mode:**\n\n- `google/gemini-3-flash-preview`\n- `anthropic/claude-sonnet-4-20250514`, `anthropic/claude-sonnet-4-5-20250929`, `anthropic/claude-haiku-4-5-20251001`\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for hybrid mode\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  mode: \"hybrid\",\n  model: \"google/gemini-3-flash-preview\",\n});\n\nawait agent.execute({\n  instruction: \"Click the submit button and fill the form\",\n  maxSteps: 20,\n  highlightCursor: true, // Enabled by default in hybrid mode\n});\n```\n\n**Agent modes:**\n\n- `\"dom\"` (default): Uses DOM-based tools (act, fillForm) - works with any model\n- `\"hybrid\"`: Uses both DOM-based and coordinate-based tools (act, click, type, dragAndDrop) - requires grounding-capable models\n- `\"cua\"`: Uses Computer Use Agent providers\n\n## Advanced Features\n\n### DeepLocator (XPath Targeting)\n\nTarget specific elements across shadow DOM and iframes:\n\n```typescript\nawait page\n  .deepLocator(\"/html/body/div[2]/div[3]/iframe/html/body/p\")\n  .highlight({\n    durationMs: 5000,\n    contentColor: { r: 255, g: 0, b: 0 },\n  });\n```\n\n### Multi-Page Workflows\n\n```typescript\nconst page1 = stagehand.context.pages()[0];\nawait page1.goto(\"https://example.com\");\n\nconst page2 = await stagehand.context.newPage();\nawait page2.goto(\"https://example2.com\");\n\n// Act/extract/observe operate on the current active page by default\n// Pass { page } option to target a specific page\nawait stagehand.act(\"click button\", { page: page1 });\nawait stagehand.extract(\"get title\", { page: page2 });\n```\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import globals from \"globals\";\nimport pluginJs from \"@eslint/js\";\nimport tseslint from \"typescript-eslint\";\nimport security from \"eslint-plugin-security\";\n\n/** @type {import('eslint').Linter.Config[]} */\nexport default [\n  { files: [\"**/*.{js,mjs,cjs,ts}\"] },\n  { languageOptions: { globals: globals.browser } },\n  {\n    files: [\"packages/core/scripts/**/*.{js,cjs,mjs}\"],\n    languageOptions: { globals: globals.node },\n  },\n  {\n    files: [\n      \"packages/server-v3/scripts/**/*.{js,cjs,mjs,ts}\",\n      \"packages/server-v4/scripts/**/*.{js,cjs,mjs,ts}\",\n    ],\n    languageOptions: { globals: globals.node },\n  },\n  {\n    files: [\"packages/cli/**/*.{js,cjs,mjs,ts}\"],\n    languageOptions: { globals: globals.node },\n  },\n  {\n    ignores: [\n      \"**/dist/**\",\n      \"**/node_modules/**\",\n      \"packages/core/lib/dom/build/**\",\n      \"packages/core/lib/v3/dom/build/**\",\n      \"packages/core/lib/v4/dom/build/**\",\n      \"packages/core/scripts/prepare.js\",\n      \"**/*.config.js\",\n      \"**/*.config.mjs\",\n      \".browserbase/**\",\n      \"**/.browserbase/**\",\n      \"**/*.json\",\n      \"stainless.yml\",\n      \"packages/server-v3/openapi.v3.yaml\",\n      \"packages/server-v4/openapi.v4.yaml\",\n    ],\n  },\n  pluginJs.configs.recommended,\n  ...tseslint.configs.recommended,\n  {\n    plugins: {\n      security,\n    },\n    rules: {\n      \"no-eval\": \"error\",\n      \"no-implied-eval\": \"error\",\n      \"no-new-func\": \"error\",\n      \"security/detect-eval-with-expression\": \"error\",\n      \"preserve-caught-error\": \"error\",\n      \"no-restricted-syntax\": [\n        \"error\",\n        {\n          selector: \"CallExpression[callee.name='Function']\",\n          message: \"Dynamic function construction is prohibited.\",\n        },\n        {\n          selector: \"NewExpression[callee.name='Function']\",\n          message: \"Dynamic function construction is prohibited.\",\n        },\n        {\n          selector:\n            \"CallExpression[callee.object.name='window'][callee.property.name='Function']\",\n          message:\n            \"Dynamic function construction via window.Function is prohibited.\",\n        },\n        {\n          selector:\n            \"CallExpression[callee.object.name='globalThis'][callee.property.name='Function']\",\n          message:\n            \"Dynamic function construction via globalThis.Function is prohibited.\",\n        },\n      ],\n    },\n  },\n  {\n    files: [\"packages/cli/**/*.{js,cjs,mjs,ts}\"],\n    rules: {\n      \"no-empty\": [\"error\", { allowEmptyCatch: true }],\n    },\n  },\n];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"stagehand-workspace\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"description\": \"Stagehand monorepo workspace\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"build:full\": \"turbo run build\",\n    \"build:cjs\": \"turbo run build:cjs\",\n    \"build:cli\": \"turbo run build:cli\",\n    \"build:esm\": \"turbo run build:esm\",\n    \"build:sea\": \"turbo run build:sea:esm\",\n    \"build:sea:esm\": \"turbo run build:sea:esm\",\n    \"build:sea:cjs\": \"turbo run build:sea:cjs\",\n    \"lint\": \"turbo run lint\",\n    \"format\": \"prettier --write .\",\n    \"prettier\": \"prettier --write .\",\n    \"eslint\": \"eslint .\",\n    \"test\": \"turbo run test:core test:e2e test:server test:evals test:cli\",\n    \"test:core\": \"turbo run test:core --\",\n    \"test:core:local\": \"STAGEHAND_BROWSER_TARGET=local pnpm run test:core --\",\n    \"test:core:bb\": \"STAGEHAND_BROWSER_TARGET=browserbase pnpm run test:core --\",\n    \"test:e2e\": \"turbo run test:e2e --\",\n    \"test:e2e:local\": \"STAGEHAND_BROWSER_TARGET=local pnpm run test:e2e --\",\n    \"test:e2e:bb\": \"STAGEHAND_BROWSER_TARGET=browserbase pnpm run test:e2e --\",\n    \"test:server\": \"turbo run test:server --\",\n    \"test:server:sea\": \"STAGEHAND_SERVER_TARGET=sea pnpm run test:server --\",\n    \"test:server:local\": \"STAGEHAND_SERVER_TARGET=local pnpm run test:server --\",\n    \"test:server:remote\": \"STAGEHAND_SERVER_TARGET=remote pnpm run test:server --\",\n    \"test:evals\": \"turbo run test:evals --\",\n    \"test:evals:local\": \"STAGEHAND_BROWSER_TARGET=local pnpm run test:evals --\",\n    \"test:evals:bb\": \"STAGEHAND_BROWSER_TARGET=browserbase pnpm run test:evals --\",\n    \"coverage:merge\": \"pnpm -w exec tsx packages/core/scripts/coverage.ts merge\",\n    \"docs\": \"turbo run docs\",\n    \"dev\": \"turbo run dev\",\n    \"example\": \"pnpm --filter @browserbasehq/stagehand run example --\",\n    \"cache:clear\": \"turbo run build --force\",\n    \"prepare\": \"node packages/core/scripts/prepare.js\",\n    \"release\": \"turbo run build && changeset publish\",\n    \"release-canary\": \"turbo run build && changeset version --snapshot && changeset publish --tag alpha\"\n  },\n  \"devDependencies\": {\n    \"@changesets/changelog-github\": \"^0.5.0\",\n    \"@changesets/cli\": \"^2.27.9\",\n    \"@eslint/js\": \"^10.0.1\",\n    \"c8\": \"^10.1.3\",\n    \"dotenv\": \"^17.3.1\",\n    \"esbuild\": \"0.27.2\",\n    \"eslint\": \"^10.0.2\",\n    \"eslint-plugin-security\": \"^3.0.1\",\n    \"globals\": \"^15.13.0\",\n    \"junit-to-ctrf\": \"^0.0.14\",\n    \"prettier\": \"^3.2.5\",\n    \"source-map\": \"^0.7.4\",\n    \"tsx\": \"^4.19.4\",\n    \"turbo\": \"^2.8.10\",\n    \"typescript\": \"5.8.3\",\n    \"typescript-eslint\": \"^8.56.1\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/browserbase/stagehand.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/browserbase/stagehand/issues\"\n  },\n  \"homepage\": \"https://stagehand.dev\",\n  \"overrides\": {\n    \"whatwg-url\": \"^14.0.0\",\n    \"jwa\": \"^2.0.1\",\n    \"zod\": \"4.2.1\",\n    \"tsx\": \"4.19.4\"\n  },\n  \"engines\": {\n    \"node\": \"^20.19.0 || >=22.12.0\"\n  },\n  \"packageManager\": \"pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c\"\n}\n"
  },
  {
    "path": "packages/README.md",
    "content": "# Stagehand Packages\n\nThis directory contains the Stagehand monorepo packages:\n\n- **core** - The main Stagehand package\n- **evals** - Evals CLI\n- **docs** - [Docs](https://docs.stagehand.dev)\n- **server** - Fastify server wrapping the core package for different language clients"
  },
  {
    "path": "packages/cli/CHANGELOG.md",
    "content": "# @browserbasehq/browse-cli\n\n## 0.2.0\n\n### Minor Changes\n\n- [#1816](https://github.com/browserbase/stagehand/pull/1816) [`687d54a`](https://github.com/browserbase/stagehand/commit/687d54addad5625f28d51c6994170c7b629871f2) Thanks [@shrey150](https://github.com/shrey150)! - Add `--context-id` and `--persist` flags to `browse open` for loading and persisting Browserbase Contexts across sessions\n\n- [#1793](https://github.com/browserbase/stagehand/pull/1793) [`e38c13b`](https://github.com/browserbase/stagehand/commit/e38c13b7526b140b693152ef1ffda88a74e9c425) Thanks [@shrey150](https://github.com/shrey150)! - Initial release of browse CLI - browser automation for AI agents\n\n### Patch Changes\n\n- [#1806](https://github.com/browserbase/stagehand/pull/1806) [`f8c7738`](https://github.com/browserbase/stagehand/commit/f8c773898f4d97e8854cc67a0b18eb7d1cdd7b75) Thanks [@shrey150](https://github.com/shrey150)! - Fix `browse env` showing stale mode after `browse env remote`\n\n- Updated dependencies [[`505e8c6`](https://github.com/browserbase/stagehand/commit/505e8c6736f3706328dbc8df670c49a018058388), [`2f43ffa`](https://github.com/browserbase/stagehand/commit/2f43ffac11778152d17e4c44405770cc32c3ec8c), [`63ee247`](https://github.com/browserbase/stagehand/commit/63ee247ac6bf2992046d4f6b2759f46b15643e36), [`7dc35f5`](https://github.com/browserbase/stagehand/commit/7dc35f5e25689e6518d68b25ef71536d2781c8aa), [`335cf47`](https://github.com/browserbase/stagehand/commit/335cf4730e73bce33e92331d04bda4b0fd42685d), [`6ba0a1d`](https://github.com/browserbase/stagehand/commit/6ba0a1db7fc2d5d5a2f8927b1417d8f1d15eda10), [`4ff3bb8`](https://github.com/browserbase/stagehand/commit/4ff3bb831a6ef6e2d57148e7afb68ea8d23e395d), [`c27054b`](https://github.com/browserbase/stagehand/commit/c27054bbd0508431ade91d655f89efc87bbf5867), [`2abf5b9`](https://github.com/browserbase/stagehand/commit/2abf5b90f1e2bb1442509ef3a686b6128c9cdcf6), [`7817fcc`](https://github.com/browserbase/stagehand/commit/7817fcc315eee4455ce04567cf56c9ec801caf0b), [`7390508`](https://github.com/browserbase/stagehand/commit/73905088c5ed5923d276da9cce2efd0a0a3a46eb), [`611f43a`](https://github.com/browserbase/stagehand/commit/611f43ac8d4c580216d55d2b217c14a9a9c11013), [`521a10e`](https://github.com/browserbase/stagehand/commit/521a10e3698fc5631e219947bc90dad0f8bddaa8), [`2402a3c`](https://github.com/browserbase/stagehand/commit/2402a3c4d50270391b3e6440f4385cdcf5e1eb64)]:\n  - @browserbasehq/stagehand@3.2.0\n"
  },
  {
    "path": "packages/cli/README.md",
    "content": "# Browse CLI\n\nBrowser automation CLI for AI agents. Built on [Stagehand](https://github.com/browserbase/stagehand), providing raw browser control without requiring LLM integration.\n\n## Installation\n\n```bash\nnpm install -g @browserbasehq/browse-cli\n```\n\nRequires Chrome/Chromium installed on the system.\n\n## Quick Start\n\n```bash\n# Navigate to a URL (auto-starts browser daemon)\nbrowse open https://example.com\n\n# Take a snapshot to get element refs\nbrowse snapshot -c\n\n# Click an element by ref\nbrowse click @0-5\n\n# Type text\nbrowse type \"Hello, world!\"\n\n# Take a screenshot\nbrowse screenshot ./page.png\n\n# Stop the browser\nbrowse stop\n```\n\n## How It Works\n\nBrowse uses a daemon architecture for fast, stateful interactions:\n\n1. **First command** auto-starts a Chrome browser daemon\n2. **Subsequent commands** reuse the same browser session\n3. **State persists** between commands (cookies, refs, etc.)\n4. **Multiple sessions** supported via `--session` or `BROWSE_SESSION` env var\n\n### Self-Healing Sessions\n\nThe CLI automatically recovers from stale sessions. If the daemon or Chrome crashes:\n1. Detects the failure\n2. Cleans up stale processes and files\n3. Restarts the daemon\n4. Retries the command\n\nAgents don't need to handle recovery - commands \"just work\".\n\n## Commands\n\n### Navigation\n\n```bash\nbrowse open <url> [--wait load|domcontentloaded|networkidle] [-t|--timeout ms]\nbrowse reload\nbrowse back\nbrowse forward\n```\n\nThe `--timeout` flag (default: 30000ms) controls how long to wait for the page load state. Use longer timeouts for slow-loading pages:\n\n```bash\nbrowse open https://slow-site.com --timeout 60000\n```\n\n### Click Actions\n\n```bash\nbrowse click <ref> [-b left|right|middle] [-c count]  # Click by ref (e.g., @0-5)\nbrowse click_xy <x> <y> [--button] [--xpath]          # Click at coordinates\n```\n\n### Coordinate Actions\n\n```bash\nbrowse hover <x> <y> [--xpath]\nbrowse scroll <x> <y> <deltaX> <deltaY> [--xpath]\nbrowse drag <fromX> <fromY> <toX> <toY> [--steps n] [--xpath]\n```\n\n### Keyboard\n\n```bash\nbrowse type <text> [-d delay] [--mistakes]\nbrowse press <key>  # e.g., Enter, Tab, Cmd+A\n```\n\n### Forms\n\n```bash\nbrowse fill <selector> <value> [--no-press-enter]\nbrowse select <selector> <values...>\nbrowse highlight <selector> [-d duration]\n```\n\n### Page Info\n\n```bash\nbrowse get url\nbrowse get title\nbrowse get text <selector>\nbrowse get html <selector>\nbrowse get value <selector>\nbrowse get box <selector>  # Returns center coordinates\n\nbrowse snapshot [-c|--compact]  # Accessibility tree with refs\nbrowse screenshot [path] [-f|--full-page] [-t png|jpeg]\n```\n\n### Waiting\n\n```bash\nbrowse wait load [state]\nbrowse wait selector <selector> [-t timeout] [-s visible|hidden|attached|detached]\nbrowse wait timeout <ms>\n```\n\n### Multi-Tab\n\n```bash\nbrowse pages          # List all tabs\nbrowse newpage [url]  # Open new tab\nbrowse tab_switch <n> # Switch to tab by index\nbrowse tab_close [n]  # Close tab (default: last)\n```\n\n### Network Capture\n\nCapture HTTP requests to the filesystem for inspection:\n\n```bash\nbrowse network on     # Start capturing requests\nbrowse network off    # Stop capturing\nbrowse network path   # Get capture directory path\nbrowse network clear  # Clear captured requests\n```\n\nCaptured requests are saved as directories:\n\n```\n/tmp/browse-default-network/\n  001-GET-api.github.com-repos/\n    request.json      # method, url, headers, body\n    response.json     # status, headers, body, duration\n```\n\n### Daemon Control\n\n```bash\nbrowse start          # Explicitly start daemon\nbrowse stop [--force] # Stop daemon\nbrowse status         # Check daemon status\nbrowse env [target]   # Show or switch environment: local | remote\n```\n\n### Environment Switching (Local vs Remote)\n\nUse environment switching when an agent should keep the same command flow, but the\nbrowser runtime needs to change:\n\n- `local` runs Chrome on your machine (best for local debugging/dev loops)\n- `remote` runs a Browserbase session (best for anti-bot hardening and cloud runs)\n\n```bash\n# Show active environment (if running) and desired environment for next start\nbrowse env\n\n# Switch current session to Browserbase (restarts daemon if needed)\nbrowse env remote\n\n# Switch back to local Chrome\nbrowse env local\n```\n\nBehavior details:\n\n- Environment is scoped per `--session`\n- `browse env <target>` persists an override and restarts the daemon\n- `browse stop` clears the override so next start falls back to env-var-based auto detection\n- Auto detection defaults to:\n  - `remote` when `BROWSERBASE_API_KEY` is set\n  - `local` otherwise\n\n## Global Options\n\n| Option | Description |\n|--------|-------------|\n| `--session <name>` | Session name for multiple browsers (default: \"default\") |\n| `--headless` | Run Chrome in headless mode |\n| `--headed` | Run Chrome with visible window (default) |\n| `--ws <url>` | Connect to existing Chrome via CDP WebSocket |\n| `--json` | Output as JSON |\n\n## Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `BROWSE_SESSION` | Default session name (alternative to `--session`) |\n| `BROWSERBASE_API_KEY` | Browserbase API key (required for `browse env remote`) |\n| `BROWSERBASE_PROJECT_ID` | Browserbase project ID (optional, passed through if set) |\n\n## Element References\n\nAfter running `browse snapshot`, you can reference elements by their ref ID:\n\n```bash\n# Get snapshot with refs\nbrowse snapshot -c\n\n# Output includes refs like [0-5], [1-2], etc.\n# RootWebArea \"Example\" url=\"https://example.com\"\n#   [0-0] link \"Home\"\n#   [0-1] link \"About\"\n#   [0-2] button \"Sign In\"\n\n# Click using ref (multiple formats supported)\nbrowse click @0-2       # @ prefix\nbrowse click 0-2        # Plain ref\nbrowse click ref=0-2    # Explicit prefix\n```\n\nThe full snapshot output includes mappings:\n- **xpathMap**: Cross-frame XPath selectors\n- **cssMap**: Fast CSS selectors when available\n- **urlMap**: Extracted URLs from links\n\n## Multiple Sessions\n\nRun multiple browser instances simultaneously:\n\n```bash\n# Terminal 1\nBROWSE_SESSION=session1 browse open https://google.com\n\n# Terminal 2\nBROWSE_SESSION=session2 browse open https://github.com\n\n# Or use --session flag\nbrowse --session work open https://slack.com\nbrowse --session personal open https://twitter.com\n```\n\n## Direct CDP Connection\n\nConnect to an existing Chrome instance:\n\n```bash\n# Start Chrome with remote debugging\ngoogle-chrome --remote-debugging-port=9222\n\n# Connect via WebSocket\nbrowse --ws ws://localhost:9222/devtools/browser/... open https://example.com\n```\n\n## Optimal AI Workflow\n\n1. **Navigate** to target page (browser auto-starts)\n2. **Snapshot** to get the accessibility tree with refs\n3. **Click/Fill** using refs directly (e.g., `@0-5`)\n4. **Re-snapshot** after actions to verify state changes\n5. **Stop** when done\n\n```bash\nbrowse open https://example.com\nbrowse snapshot -c\n# [0-5] textbox: Search\n# [0-8] button: Submit\nbrowse fill @0-5 \"my query\"\nbrowse click @0-8\nbrowse snapshot -c  # Verify result\nbrowse stop\n```\n\n## Troubleshooting\n\n### Chrome not found\n\nThe CLI uses your system Chrome/Chromium. If not found:\n\n```bash\n# macOS - Install Chrome or set path\nexport CHROME_PATH=/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome\n\n# Linux - Install chromium\nsudo apt install chromium-browser\n```\n\n### Stale daemon\n\nIf the daemon becomes unresponsive:\n\n```bash\nbrowse stop --force\n```\n\n### Permission denied on socket\n\n```bash\n# Clean up stale socket files\nrm /tmp/browse-*.sock /tmp/browse-*.pid\n```\n\n## Platform Support\n\n- macOS (Intel and Apple Silicon)\n- Linux (x64 and arm64)\n\nWindows support requires WSL or TCP socket implementation.\n\n## Development\n\n```bash\n# Clone and setup (in monorepo)\ncd packages/cli\npnpm install         # Install dependencies first!\npnpm run build       # Build the CLI\n\n# Run without building (for development)\npnpm run dev -- <command>\n\n# Or with tsx directly\nnpx tsx src/index.ts <command>\n\n# Run linting and formatting\npnpm run lint\npnpm run format\n```\n\n## License\n\nMIT - see [LICENSE](./LICENSE)\n\n## Related\n\n- [Stagehand](https://github.com/browserbase/stagehand) - AI web browser automation framework\n- [Browserbase](https://browserbase.com) - Cloud browser infrastructure\n"
  },
  {
    "path": "packages/cli/package.json",
    "content": "{\n  \"name\": \"@browserbasehq/browse-cli\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Browser automation CLI for AI agents, built on Stagehand\",\n  \"type\": \"commonjs\",\n  \"license\": \"MIT\",\n  \"author\": \"Browserbase <support@browserbase.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/browserbase/stagehand.git\",\n    \"directory\": \"packages/cli\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/browserbase/stagehand/issues\"\n  },\n  \"homepage\": \"https://github.com/browserbase/stagehand/tree/main/packages/cli#readme\",\n  \"keywords\": [\n    \"browser\",\n    \"automation\",\n    \"cli\",\n    \"ai\",\n    \"agent\",\n    \"chrome\",\n    \"cdp\",\n    \"web-scraping\",\n    \"testing\",\n    \"stagehand\"\n  ],\n  \"engines\": {\n    \"node\": \"^20.19.0 || >=22.12.0\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"bin\": {\n    \"browse\": \"./dist/index.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsx src/index.ts\",\n    \"browse\": \"tsx src/index.ts\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"eslint\": \"eslint .\",\n    \"lint\": \"cd ../.. && prettier --check packages/cli && cd packages/cli && pnpm run eslint && pnpm run typecheck\",\n    \"test\": \"vitest run\",\n    \"test:cli\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"prepublishOnly\": \"pnpm run build\"\n  },\n  \"dependencies\": {\n    \"@browserbasehq/stagehand\": \"workspace:*\",\n    \"commander\": \"^12.0.0\",\n    \"dotenv\": \"^16.4.5\",\n    \"pino\": \"^9.6.0\",\n    \"pino-pretty\": \"^13.0.0\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.11.30\",\n    \"devtools-protocol\": \"^0.0.1464554\",\n    \"eslint\": \"^10.0.2\",\n    \"tsup\": \"^8.2.1\",\n    \"tsx\": \"^4.10.5\",\n    \"typescript\": \"5.8.3\",\n    \"vitest\": \"^4.0.8\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/index.ts",
    "content": "/**\n * Browse CLI - Browser automation for AI agents\n *\n * Usage:\n *   browse [options] <command> [args...]\n *\n * The CLI runs a daemon process that maintains browser state between commands.\n * Multiple sessions can run simultaneously using --session <name> or BROWSE_SESSION env var.\n */\n\nimport { Command } from \"commander\";\nimport { Stagehand, type Page as BrowsePage } from \"@browserbasehq/stagehand\";\nimport { promises as fs } from \"fs\";\nimport * as path from \"path\";\nimport * as os from \"os\";\nimport * as net from \"net\";\nimport { spawn } from \"child_process\";\nimport * as readline from \"readline\";\nimport type { Protocol } from \"devtools-protocol\";\nimport { version as VERSION } from \"../package.json\";\n\nconst program = new Command();\n\n// Type aliases\ntype BrowseContext = Stagehand[\"context\"];\n\n// ==================== DAEMON INFRASTRUCTURE ====================\n\nconst SOCKET_DIR = os.tmpdir();\n\nfunction getSocketPath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.sock`);\n}\n\nfunction getLockPath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.lock`);\n}\n\n/**\n * Acquire an exclusive lock for daemon operations.\n * Uses O_EXCL for atomic file creation to prevent race conditions.\n */\nasync function acquireLock(\n  session: string,\n  timeoutMs: number = 10000,\n): Promise<boolean> {\n  const lockPath = getLockPath(session);\n  const startTime = Date.now();\n\n  while (Date.now() - startTime < timeoutMs) {\n    try {\n      // O_EXCL ensures atomic creation - fails if file exists\n      const handle = await fs.open(lockPath, \"wx\");\n      await handle.write(String(process.pid));\n      await handle.close();\n      return true;\n    } catch (err: unknown) {\n      if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n        // Lock exists - check if holder is still alive\n        try {\n          const holderPid = parseInt(await fs.readFile(lockPath, \"utf-8\"));\n          process.kill(holderPid, 0); // Throws if process doesn't exist\n          // Process exists, wait and retry\n          await new Promise((r) => setTimeout(r, 100));\n        } catch {\n          // Lock holder is dead, remove stale lock\n          try {\n            await fs.unlink(lockPath);\n          } catch {}\n        }\n        continue;\n      }\n      throw err;\n    }\n  }\n  return false;\n}\n\nasync function releaseLock(session: string): Promise<void> {\n  try {\n    await fs.unlink(getLockPath(session));\n  } catch {}\n}\n\n/**\n * Check if a socket is actually connectable (not just exists on disk).\n */\nasync function isSocketConnectable(\n  socketPath: string,\n  timeoutMs: number,\n): Promise<boolean> {\n  return new Promise((resolve) => {\n    const client = net.createConnection(socketPath);\n    const timeout = setTimeout(() => {\n      client.destroy();\n      resolve(false);\n    }, timeoutMs);\n\n    client.on(\"connect\", () => {\n      clearTimeout(timeout);\n      client.destroy();\n      resolve(true);\n    });\n\n    client.on(\"error\", () => {\n      clearTimeout(timeout);\n      resolve(false);\n    });\n  });\n}\n\n/**\n * Wait for socket to become connectable with exponential backoff.\n */\nasync function waitForSocketReady(\n  socketPath: string,\n  timeoutMs: number,\n): Promise<void> {\n  const startTime = Date.now();\n  let delay = 50;\n\n  while (Date.now() - startTime < timeoutMs) {\n    if (await isSocketConnectable(socketPath, 500)) return;\n    await new Promise((r) => setTimeout(r, delay));\n    delay = Math.min(delay * 1.5, 500);\n  }\n  throw new Error(`Socket not ready after ${timeoutMs}ms`);\n}\n\nfunction getPidPath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.pid`);\n}\n\nfunction getWsPath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.ws`);\n}\n\nfunction getChromePidPath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.chrome.pid`);\n}\n\nfunction getNetworkDir(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}-network`);\n}\n\nfunction getModePath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.mode`);\n}\n\nfunction getModeOverridePath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.mode-override`);\n}\n\nfunction getContextPath(session: string): string {\n  return path.join(SOCKET_DIR, `browse-${session}.context`);\n}\n\ntype BrowseMode = \"browserbase\" | \"local\";\n\nfunction hasBrowserbaseCredentials(): boolean {\n  return Boolean(process.env.BROWSERBASE_API_KEY);\n}\n\nfunction assertModeSupported(mode: BrowseMode): void {\n  if (mode === \"browserbase\" && !hasBrowserbaseCredentials()) {\n    throw new Error(\n      \"Remote mode requires BROWSERBASE_API_KEY. Set the env var or run `browse env local`.\",\n    );\n  }\n}\n\nfunction toModeTarget(mode: BrowseMode): \"local\" | \"remote\" {\n  return mode === \"browserbase\" ? \"remote\" : \"local\";\n}\n\nasync function readCurrentMode(session: string): Promise<BrowseMode | null> {\n  try {\n    const mode = (await fs.readFile(getModePath(session), \"utf-8\")).trim();\n    if (mode === \"browserbase\" || mode === \"local\") {\n      return mode;\n    }\n  } catch {\n    // File may not exist yet.\n  }\n  return null;\n}\n\n/** Determine desired mode: explicit override > env var detection */\nasync function getDesiredMode(session: string): Promise<BrowseMode> {\n  try {\n    const override = (\n      await fs.readFile(getModeOverridePath(session), \"utf-8\")\n    ).trim();\n    if (override === \"browserbase\" || override === \"local\") return override;\n  } catch {}\n  return hasBrowserbaseCredentials() ? \"browserbase\" : \"local\";\n}\n\nasync function isDaemonRunning(session: string): Promise<boolean> {\n  try {\n    const pidFile = getPidPath(session);\n    const pid = parseInt(await fs.readFile(pidFile, \"utf-8\"));\n    process.kill(pid, 0); // Check if process exists\n\n    // Also verify socket exists and is actually connectable\n    const socketPath = getSocketPath(session);\n    await fs.access(socketPath);\n\n    // Verify socket is actually connectable (not just exists on disk)\n    return await isSocketConnectable(socketPath, 500);\n  } catch {\n    return false;\n  }\n}\n\n/** Daemon state files — cleaned on both startup (stale) and shutdown. */\nconst DAEMON_STATE_FILES = (session: string) => [\n  getSocketPath(session),\n  getPidPath(session),\n  getWsPath(session),\n  getChromePidPath(session),\n  getLockPath(session),\n  getModePath(session),\n];\n\nasync function cleanupStaleFiles(session: string): Promise<void> {\n  const files = [\n    ...DAEMON_STATE_FILES(session),\n    // Context is client-written config, only cleaned on full shutdown\n    getContextPath(session),\n  ];\n\n  for (const file of files) {\n    try {\n      await fs.unlink(file);\n    } catch {}\n  }\n}\n\n/** Like cleanupStaleFiles but preserves client-written config (context). */\nasync function cleanupDaemonStateFiles(session: string): Promise<void> {\n  for (const file of DAEMON_STATE_FILES(session)) {\n    try {\n      await fs.unlink(file);\n    } catch {}\n  }\n}\n\n/** Find and kill Chrome processes for this session */\nasync function killChromeProcesses(session: string): Promise<boolean> {\n  try {\n    const { exec } = await import(\"child_process\");\n    const { promisify } = await import(\"util\");\n    const execAsync = promisify(exec);\n\n    if (process.platform === \"darwin\" || process.platform === \"linux\") {\n      // Find Chrome processes with our user data dir pattern\n      const { stdout } = await execAsync(\n        `pgrep -f \"browse-${session}\" || true`,\n      );\n      const pids = stdout.trim().split(\"\\n\").filter(Boolean);\n      for (const pid of pids) {\n        try {\n          process.kill(parseInt(pid), \"SIGTERM\");\n        } catch {}\n      }\n      return pids.length > 0;\n    }\n    return false;\n  } catch {\n    return false;\n  }\n}\n\ninterface DaemonRequest {\n  command: string;\n  args: unknown[];\n}\n\ninterface DaemonResponse {\n  success: boolean;\n  result?: unknown;\n  error?: string;\n}\n\n// ==================== DAEMON SERVER ====================\n\n// Default viewport matching Stagehand core\nconst DEFAULT_VIEWPORT = { width: 1288, height: 711 };\n\nasync function runDaemon(session: string, headless: boolean): Promise<void> {\n  // Only clean daemon state files (socket, pid, etc.), not client-written config (context)\n  await cleanupDaemonStateFiles(session);\n\n  // Write daemon PID file and initial mode so status is immediately available\n  await fs.writeFile(getPidPath(session), String(process.pid));\n  await fs.writeFile(getModePath(session), await getDesiredMode(session));\n\n  // Browser state (initialized lazily on first command)\n  let stagehand: Stagehand | null = null;\n  let context: BrowseContext | null = null;\n  let isInitializing = false;\n\n  /**\n   * Lazy browser initialization - called on first command (like agent-browser)\n   * This allows daemon to signal \"started\" immediately without waiting for browser\n   */\n  async function ensureBrowserInitialized(): Promise<{\n    stagehand: Stagehand;\n    context: BrowseContext;\n  }> {\n    if (stagehand && context) {\n      return { stagehand, context };\n    }\n\n    // Prevent concurrent initialization\n    if (isInitializing) {\n      // Wait for initialization to complete\n      while (isInitializing) {\n        await new Promise((resolve) => setTimeout(resolve, 100));\n      }\n      if (stagehand && context) {\n        return { stagehand, context };\n      }\n      throw new Error(\"Browser initialization failed\");\n    }\n\n    isInitializing = true;\n\n    try {\n      const desiredMode = await getDesiredMode(session);\n      assertModeSupported(desiredMode);\n      const useBrowserbase = desiredMode === \"browserbase\";\n\n      // Read context config if present (written by `browse open --context-id`)\n      let contextConfig: { id: string; persist?: boolean } | null = null;\n      try {\n        const raw = await fs.readFile(getContextPath(session), \"utf-8\");\n        contextConfig = JSON.parse(raw);\n      } catch {}\n\n      stagehand = new Stagehand({\n        env: useBrowserbase ? \"BROWSERBASE\" : \"LOCAL\",\n        verbose: 0,\n        disablePino: true,\n        ...(useBrowserbase\n          ? {\n              disableAPI: true,\n              ...(contextConfig\n                ? {\n                    browserbaseSessionCreateParams: {\n                      browserSettings: {\n                        context: contextConfig,\n                      },\n                    },\n                  }\n                : {}),\n            }\n          : {\n              localBrowserLaunchOptions: {\n                headless,\n                viewport: DEFAULT_VIEWPORT,\n              },\n            }),\n      });\n\n      // Persist mode so status command can report it\n      await fs.writeFile(getModePath(session), desiredMode);\n\n      await stagehand.init();\n\n      context = stagehand.context;\n\n      // Try to save Chrome info for reference (best effort)\n      try {\n        const wsUrl = stagehand.connectURL();\n        await fs.writeFile(getWsPath(session), wsUrl);\n      } catch {}\n\n      // Store session name for network capture\n      networkSession = session;\n\n      return { stagehand, context };\n    } finally {\n      isInitializing = false;\n    }\n  }\n\n  // Create Unix socket server\n  const socketPath = getSocketPath(session);\n  const server = net.createServer((conn) => {\n    const rl = readline.createInterface({ input: conn });\n\n    rl.on(\"line\", async (line) => {\n      let response: DaemonResponse;\n      try {\n        const request: DaemonRequest = JSON.parse(line);\n\n        // Lazy browser initialization on first command (like agent-browser)\n        const { stagehand: sh, context: ctx } =\n          await ensureBrowserInitialized();\n\n        const result = await executeCommand(\n          ctx,\n          request.command,\n          request.args,\n          sh,\n        );\n        response = { success: true, result };\n      } catch (e) {\n        response = {\n          success: false,\n          error: e instanceof Error ? e.message : String(e),\n        };\n      }\n      conn.write(JSON.stringify(response) + \"\\n\");\n    });\n\n    rl.on(\"close\", () => {\n      conn.destroy();\n    });\n  });\n\n  server.listen(socketPath);\n\n  // Signal daemon started immediately (before browser initialization)\n  console.log(JSON.stringify({ daemon: \"started\", session, pid: process.pid }));\n\n  // Graceful shutdown handler\n  let shuttingDown = false;\n  const shutdown = async () => {\n    if (shuttingDown) return;\n    shuttingDown = true;\n\n    server.close();\n\n    try {\n      if (stagehand) {\n        await stagehand.close();\n      }\n    } catch {}\n\n    await cleanupStaleFiles(session);\n    process.exit(0);\n  };\n\n  // Handle all termination signals\n  process.on(\"SIGTERM\", () => shutdown());\n  process.on(\"SIGINT\", () => shutdown());\n  process.on(\"SIGHUP\", () => shutdown());\n  process.on(\"uncaughtException\", (err) => {\n    console.error(\"Uncaught exception:\", err);\n    shutdown();\n  });\n  process.on(\"unhandledRejection\", (reason) => {\n    console.error(\"Unhandled rejection:\", reason);\n    shutdown();\n  });\n\n  // Keep daemon running (signal already sent above)\n}\n\n// ==================== REF MAP (cached from last snapshot) ====================\n\n/** Cached ref maps from the last snapshot - allows @ref syntax in commands */\nlet refMap: {\n  xpathMap: Record<string, string>;\n  urlMap: Record<string, string>;\n} = {\n  xpathMap: {},\n  urlMap: {},\n};\n\n// ==================== NETWORK CAPTURE STATE ====================\n\ninterface PendingRequest {\n  id: string;\n  timestamp: string;\n  method: string;\n  url: string;\n  headers: Record<string, string>;\n  body: string | null;\n  resourceType: string;\n}\n\nlet networkEnabled = false;\nlet networkDir: string | null = null;\nlet networkCounter = 0;\nlet networkSession: string | null = null;\nconst pendingRequests = new Map<string, PendingRequest>();\n\n/** Sanitize a string for use in a filename */\nfunction sanitizeForFilename(str: string, maxLen: number = 30): string {\n  return str\n    .replace(/[^a-zA-Z0-9.-]/g, \"-\")\n    .replace(/-+/g, \"-\")\n    .replace(/^-|-$/g, \"\")\n    .slice(0, maxLen);\n}\n\n/** Generate a directory name for a request */\nfunction getRequestDirName(\n  counter: number,\n  method: string,\n  url: string,\n): string {\n  try {\n    const parsed = new URL(url);\n    const domain = sanitizeForFilename(parsed.hostname, 30);\n    const pathPart = parsed.pathname.split(\"/\").filter(Boolean)[0] || \"root\";\n    const pathSlug = sanitizeForFilename(pathPart, 20);\n    return `${String(counter).padStart(3, \"0\")}-${method}-${domain}-${pathSlug}`;\n  } catch {\n    return `${String(counter).padStart(3, \"0\")}-${method}-unknown`;\n  }\n}\n\n/** Write request data to filesystem */\nasync function writeRequestToFs(\n  request: PendingRequest,\n): Promise<string | null> {\n  if (!networkDir) return null;\n\n  const dirName = getRequestDirName(\n    networkCounter++,\n    request.method,\n    request.url,\n  );\n  const requestDir = path.join(networkDir, dirName);\n\n  try {\n    await fs.mkdir(requestDir, { recursive: true });\n\n    const requestData = {\n      id: request.id,\n      timestamp: request.timestamp,\n      method: request.method,\n      url: request.url,\n      headers: request.headers,\n      body: request.body,\n      resourceType: request.resourceType,\n    };\n    await fs.writeFile(\n      path.join(requestDir, \"request.json\"),\n      JSON.stringify(requestData, null, 2),\n    );\n\n    return requestDir;\n  } catch (err) {\n    console.error(\"Failed to write request:\", err);\n    return null;\n  }\n}\n\n/** Write response data to filesystem */\nasync function writeResponseToFs(\n  requestDir: string,\n  response: {\n    id: string;\n    status: number;\n    statusText: string;\n    headers: Record<string, string>;\n    mimeType: string;\n    body: string | null;\n    duration: number;\n    error?: string;\n  },\n): Promise<void> {\n  try {\n    await fs.writeFile(\n      path.join(requestDir, \"response.json\"),\n      JSON.stringify(response, null, 2),\n    );\n  } catch (err) {\n    console.error(\"Failed to write response:\", err);\n  }\n}\n\n/**\n * Parse a ref from a selector argument.\n * Supports: @0-3, @[0-3], [0-3], 0-3, ref=0-3\n */\nfunction parseRef(selector: string): string | null {\n  if (selector.startsWith(\"@\")) {\n    const rest = selector.slice(1);\n    if (rest.startsWith(\"[\") && rest.endsWith(\"]\")) {\n      return rest.slice(1, -1);\n    }\n    return rest;\n  }\n  if (\n    selector.startsWith(\"[\") &&\n    selector.endsWith(\"]\") &&\n    /^\\[\\d+-\\d+]$/.test(selector)\n  ) {\n    return selector.slice(1, -1);\n  }\n  if (selector.startsWith(\"ref=\")) {\n    return selector.slice(4);\n  }\n  if (/^\\d+-\\d+$/.test(selector)) {\n    return selector;\n  }\n  return null;\n}\n\n/**\n * Resolve a selector - if it's a ref, look up from refMap.\n * Always uses XPath since CSS selectors cannot cross shadow DOM boundaries\n * and can cause issues with dynamically generated class names.\n */\nfunction resolveSelector(selector: string): string {\n  const ref = parseRef(selector);\n  if (ref) {\n    const xpath = refMap.xpathMap[ref];\n    if (!xpath) {\n      throw new Error(\n        `Unknown ref \"${ref}\" - run snapshot first to populate refs (have ${Object.keys(refMap.xpathMap).length} refs)`,\n      );\n    }\n    return xpath;\n  }\n  return selector;\n}\n\n// ==================== COMMAND EXECUTION ====================\n\nasync function executeCommand(\n  context: BrowseContext,\n  command: string,\n  args: unknown[],\n  stagehand?: Stagehand,\n): Promise<unknown> {\n  // Use awaitActivePage() like stagehand.act() does - handles popups and waits for page to be ready\n  const page =\n    command !== \"pages\" && command !== \"newpage\"\n      ? await context.awaitActivePage()\n      : context.activePage();\n  if (!page && command !== \"pages\" && command !== \"newpage\") {\n    throw new Error(\"No active page\");\n  }\n\n  switch (command) {\n    // Navigation\n    case \"open\": {\n      const [url, waitUntil, timeout] = args as [string, string?, number?];\n      await page!.goto(url, {\n        waitUntil: waitUntil as \"load\" | \"domcontentloaded\" | \"networkidle\",\n        timeoutMs: timeout ?? 30000,\n      });\n      return { url: page!.url() };\n    }\n    case \"reload\": {\n      await page!.reload();\n      return { url: page!.url() };\n    }\n    case \"back\": {\n      await page!.goBack();\n      return { url: page!.url() };\n    }\n    case \"forward\": {\n      await page!.goForward();\n      return { url: page!.url() };\n    }\n\n    // Click by ref - uses stagehand.act with Action type (skips LLM, uses deterministic path)\n    case \"click\": {\n      const [selector] = args as [string];\n      if (!stagehand) {\n        throw new Error(\"Stagehand instance not available\");\n      }\n      const resolved = resolveSelector(selector);\n\n      // Construct an Action object (like observe() returns) to use the deterministic path\n      const action = {\n        selector: resolved,\n        description: \"click element\",\n        method: \"click\",\n        arguments: [],\n      };\n\n      await stagehand.act(action);\n      return { clicked: true };\n    }\n\n    // Click by coordinates\n    case \"click_xy\": {\n      const [x, y, opts] = args as [\n        number,\n        number,\n        { button?: string; clickCount?: number; returnXPath?: boolean },\n      ];\n      const result = await page!.click(x, y, {\n        button: (opts?.button as \"left\" | \"right\" | \"middle\") ?? \"left\",\n        clickCount: opts?.clickCount ?? 1,\n      });\n      if (opts?.returnXPath) {\n        return { clicked: true, xpath: result };\n      }\n      return { clicked: true };\n    }\n    case \"hover\": {\n      const [x, y, opts] = args as [number, number, { returnXPath?: boolean }];\n      const result = await page!.hover(x, y);\n      if (opts?.returnXPath) {\n        return { hovered: true, xpath: result };\n      }\n      return { hovered: true };\n    }\n    case \"scroll\": {\n      const [x, y, deltaX, deltaY, opts] = args as [\n        number,\n        number,\n        number,\n        number,\n        { returnXPath?: boolean },\n      ];\n      const result = await page!.scroll(x, y, deltaX, deltaY);\n      if (opts?.returnXPath) {\n        return { scrolled: true, xpath: result };\n      }\n      return { scrolled: true };\n    }\n    case \"drag\": {\n      const [fromX, fromY, toX, toY, opts] = args as [\n        number,\n        number,\n        number,\n        number,\n        {\n          steps?: number;\n          delay?: number;\n          button?: string;\n          returnXPath?: boolean;\n        },\n      ];\n\n      const [fromXpath, toXpath] = await page!.dragAndDrop(\n        fromX,\n        fromY,\n        toX,\n        toY,\n        {\n          button: (opts?.button as \"left\" | \"right\" | \"middle\") ?? \"left\",\n          steps: opts?.steps ?? 10,\n          delay: opts?.delay ?? 0,\n          returnXpath: opts?.returnXPath,\n        },\n      );\n\n      if (opts?.returnXPath) {\n        return {\n          dragged: true,\n          xpath: fromXpath,\n          fromXpath,\n          toXpath,\n        };\n      }\n      return { dragged: true };\n    }\n    // Keyboard\n    case \"type\": {\n      const [text, opts] = args as [\n        string,\n        { delay?: number; mistakes?: boolean },\n      ];\n      await page!.type(text, {\n        delay: opts?.delay,\n        withMistakes: opts?.mistakes,\n      });\n      return { typed: true };\n    }\n    case \"press\": {\n      const [key] = args as [string];\n      await page!.keyPress(key);\n      return { pressed: key };\n    }\n\n    // Element actions - use stagehand.act with Action type for reliable interaction\n    case \"fill\": {\n      const [selector, value, opts] = args as [\n        string,\n        string,\n        { pressEnter?: boolean }?,\n      ];\n      if (!stagehand) {\n        throw new Error(\"Stagehand instance not available\");\n      }\n      const resolved = resolveSelector(selector);\n      const action = {\n        selector: resolved,\n        description: \"fill element\",\n        method: \"fill\",\n        arguments: [value],\n      };\n      await stagehand.act(action);\n      if (opts?.pressEnter) {\n        await page!.keyPress(\"Enter\");\n      }\n      return { filled: true, pressedEnter: opts?.pressEnter ?? false };\n    }\n    case \"select\": {\n      const [selector, values] = args as [string, string[]];\n      if (!stagehand) {\n        throw new Error(\"Stagehand instance not available\");\n      }\n      const resolved = resolveSelector(selector);\n      // selectOption takes the first value as argument\n      const action = {\n        selector: resolved,\n        description: \"select option\",\n        method: \"selectOption\",\n        arguments: [values[0] || \"\"],\n      };\n      await stagehand.act(action);\n      return { selected: values };\n    }\n    case \"highlight\": {\n      const [selector, duration] = args as [string, number?];\n      await page!\n        .deepLocator(resolveSelector(selector))\n        .highlight({ durationMs: duration ?? 2000 });\n      return { highlighted: true };\n    }\n    // Page info\n    case \"get\": {\n      const [what, selector] = args as [string, string?];\n      switch (what) {\n        case \"url\":\n          return { url: page!.url() };\n        case \"title\":\n          return { title: await page!.title() };\n        case \"text\":\n          return {\n            text: await page!\n              .deepLocator(resolveSelector(selector!))\n              .textContent(),\n          };\n        case \"html\":\n          return {\n            html: await page!\n              .deepLocator(resolveSelector(selector!))\n              .innerHtml(),\n          };\n        case \"value\":\n          return {\n            value: await page!\n              .deepLocator(resolveSelector(selector!))\n              .inputValue(),\n          };\n        case \"box\": {\n          const { x, y } = await page!\n            .deepLocator(resolveSelector(selector!))\n            .centroid();\n          return { x: Math.round(x), y: Math.round(y) };\n        }\n        case \"visible\":\n          return {\n            visible: await page!\n              .deepLocator(resolveSelector(selector!))\n              .isVisible(),\n          };\n        case \"checked\":\n          return {\n            checked: await page!\n              .deepLocator(resolveSelector(selector!))\n              .isChecked(),\n          };\n        default:\n          throw new Error(`Unknown get type: ${what}`);\n      }\n    }\n\n    // Screenshot\n    case \"screenshot\": {\n      const [opts] = args as [\n        {\n          path?: string;\n          fullPage?: boolean;\n          type?: string;\n          quality?: number;\n          clip?: object;\n          animations?: string;\n          caret?: string;\n        },\n      ];\n      const buffer = await page!.screenshot({\n        fullPage: opts?.fullPage,\n        type: opts?.type as \"png\" | \"jpeg\" | undefined,\n        quality: opts?.quality,\n        clip: opts?.clip as\n          | { x: number; y: number; width: number; height: number }\n          | undefined,\n        animations: opts?.animations as \"disabled\" | \"allow\" | undefined,\n        caret: opts?.caret as \"hide\" | \"initial\" | undefined,\n        timeout: 10000,\n      });\n      if (opts?.path) {\n        await fs.writeFile(opts.path, buffer);\n        return { saved: opts.path };\n      }\n      return { base64: buffer.toString(\"base64\") };\n    }\n\n    // Snapshot\n    case \"snapshot\": {\n      const [compact] = args as [boolean?];\n      const snapshot = await page!.snapshot();\n\n      refMap = {\n        xpathMap: snapshot.xpathMap ?? {},\n        urlMap: snapshot.urlMap ?? {},\n      };\n\n      if (compact) {\n        return { tree: snapshot.formattedTree };\n      }\n      return {\n        tree: snapshot.formattedTree,\n        xpathMap: snapshot.xpathMap,\n        urlMap: snapshot.urlMap,\n      };\n    }\n\n    // Viewport\n    case \"viewport\": {\n      const [width, height, scale] = args as [number, number, number?];\n      await page!.setViewportSize(width, height, {\n        deviceScaleFactor: scale ?? 1,\n      });\n      return { viewport: { width, height } };\n    }\n\n    // Eval\n    case \"eval\": {\n      const [expr] = args as [string];\n      const result = await page!.evaluate(expr);\n      return { result };\n    }\n    // Element state\n    case \"is\": {\n      const [check, selector] = args as [string, string];\n      const locator = page!.deepLocator(resolveSelector(selector));\n      switch (check) {\n        case \"visible\":\n          return { visible: await locator.isVisible() };\n        case \"checked\":\n          return { checked: await locator.isChecked() };\n        default:\n          throw new Error(`Unknown check: ${check}`);\n      }\n    }\n    // Wait\n    case \"wait\": {\n      const [type, arg, opts] = args as [\n        string,\n        string?,\n        { timeout?: number; state?: string }?,\n      ];\n      switch (type) {\n        case \"load\":\n          await page!.waitForLoadState(\n            (arg as \"load\" | \"domcontentloaded\" | \"networkidle\") ?? \"load\",\n            opts?.timeout ?? 30000,\n          );\n          break;\n        case \"selector\":\n          await page!.waitForSelector(resolveSelector(arg!), {\n            state:\n              (opts?.state as \"attached\" | \"detached\" | \"visible\" | \"hidden\") ??\n              \"visible\",\n            timeout: opts?.timeout ?? 30000,\n          });\n          break;\n        case \"timeout\":\n          await page!.waitForTimeout(parseInt(arg!));\n          break;\n        default:\n          throw new Error(`Unknown wait type: ${type}`);\n      }\n      return { waited: true };\n    }\n\n    // Cursor\n    case \"cursor\": {\n      await page!.enableCursorOverlay();\n      return { cursor: \"enabled\" };\n    }\n\n    // Multi-page\n    case \"pages\": {\n      const pages = context.pages();\n      return {\n        pages: pages.map((p: BrowsePage, i: number) => ({\n          index: i,\n          url: p.url(),\n          targetId: p.targetId(),\n        })),\n      };\n    }\n    case \"newpage\": {\n      const [url] = args as [string?];\n      const newPage = await context.newPage(url);\n      return {\n        created: true,\n        url: newPage.url(),\n        targetId: newPage.targetId(),\n      };\n    }\n    case \"tab_switch\": {\n      const [index] = args as [number];\n      const pages = context.pages();\n      if (index < 0 || index >= pages.length) {\n        throw new Error(\n          `Tab index ${index} out of range (0-${pages.length - 1})`,\n        );\n      }\n      context.setActivePage(pages[index]);\n      return { switched: true, index, url: pages[index].url() };\n    }\n    case \"tab_close\": {\n      const [index] = args as [number?];\n      const pages = context.pages();\n      const targetIndex = index ?? pages.length - 1;\n      if (targetIndex < 0 || targetIndex >= pages.length) {\n        throw new Error(\n          `Tab index ${targetIndex} out of range (0-${pages.length - 1})`,\n        );\n      }\n      if (pages.length === 1) {\n        throw new Error(\"Cannot close the last tab\");\n      }\n      await pages[targetIndex].close();\n      return { closed: true, index: targetIndex };\n    }\n\n    // Debug: show current ref map\n    case \"refs\": {\n      return {\n        count: Object.keys(refMap.xpathMap).length,\n        xpathMap: refMap.xpathMap,\n        urlMap: refMap.urlMap,\n      };\n    }\n\n    // Network capture commands\n    case \"network_enable\": {\n      if (networkEnabled && networkDir) {\n        return { enabled: true, path: networkDir, alreadyEnabled: true };\n      }\n\n      const session = networkSession || \"default\";\n      networkDir = getNetworkDir(session);\n      await fs.mkdir(networkDir, { recursive: true });\n      networkCounter = 0;\n      pendingRequests.clear();\n\n      const cdpSession = page!.mainFrame().session;\n      await cdpSession.send(\"Network.enable\", {\n        maxTotalBufferSize: 10000000,\n        maxResourceBufferSize: 5000000,\n      });\n\n      // Set up CDP event listeners for network capture\n      const requestStartTimes = new Map<string, number>();\n      const requestDirs = new Map<string, string>();\n\n      cdpSession.on(\n        \"Network.requestWillBeSent\",\n        async (params: Protocol.Network.RequestWillBeSentEvent) => {\n          if (!networkEnabled || !networkDir) return;\n\n          const request: PendingRequest = {\n            id: params.requestId,\n            timestamp: new Date().toISOString(),\n            method: params.request.method,\n            url: params.request.url,\n            headers: params.request.headers || {},\n            body: params.request.postData || null,\n            resourceType: params.type || \"Other\",\n          };\n\n          pendingRequests.set(params.requestId, request);\n          requestStartTimes.set(params.requestId, Date.now());\n\n          const requestDir = await writeRequestToFs(request);\n          if (requestDir) {\n            requestDirs.set(params.requestId, requestDir);\n          }\n        },\n      );\n\n      cdpSession.on(\n        \"Network.loadingFinished\",\n        async (params: Protocol.Network.LoadingFinishedEvent) => {\n          if (!networkEnabled) return;\n\n          const requestDir = requestDirs.get(params.requestId);\n          const pending = pendingRequests.get(params.requestId);\n          if (!requestDir || !pending) return;\n\n          const startTime =\n            requestStartTimes.get(params.requestId) || Date.now();\n          const duration = Date.now() - startTime;\n\n          let body: string | null = null;\n          try {\n            const result =\n              await cdpSession.send<Protocol.Network.GetResponseBodyResponse>(\n                \"Network.getResponseBody\",\n                {\n                  requestId: params.requestId,\n                },\n              );\n            body = result.body || null;\n            if (result.base64Encoded && body) {\n              body = `[base64] ${body.slice(0, 100)}...`;\n            }\n          } catch {\n            // Body not available (e.g., for redirects)\n          }\n\n          const responseData = {\n            id: params.requestId,\n            status: 0,\n            statusText: \"\",\n            headers: {} as Record<string, string>,\n            mimeType: \"\",\n            body,\n            duration,\n          };\n\n          await writeResponseToFs(requestDir, responseData);\n\n          pendingRequests.delete(params.requestId);\n          requestStartTimes.delete(params.requestId);\n          requestDirs.delete(params.requestId);\n        },\n      );\n\n      cdpSession.on(\n        \"Network.loadingFailed\",\n        async (params: Protocol.Network.LoadingFailedEvent) => {\n          if (!networkEnabled) return;\n\n          const requestDir = requestDirs.get(params.requestId);\n          if (!requestDir) return;\n\n          const startTime =\n            requestStartTimes.get(params.requestId) || Date.now();\n          const duration = Date.now() - startTime;\n\n          const responseData = {\n            id: params.requestId,\n            status: 0,\n            statusText: \"Failed\",\n            headers: {},\n            mimeType: \"\",\n            body: null,\n            duration,\n            error: params.errorText || \"Unknown error\",\n          };\n\n          await writeResponseToFs(requestDir, responseData);\n\n          pendingRequests.delete(params.requestId);\n          requestStartTimes.delete(params.requestId);\n          requestDirs.delete(params.requestId);\n        },\n      );\n\n      networkEnabled = true;\n      return { enabled: true, path: networkDir };\n    }\n\n    case \"network_disable\": {\n      if (!networkEnabled) {\n        return { enabled: false, alreadyDisabled: true };\n      }\n\n      try {\n        await page!.mainFrame().session.send(\"Network.disable\");\n      } catch {}\n\n      networkEnabled = false;\n      return { enabled: false, path: networkDir };\n    }\n\n    case \"network_path\": {\n      if (!networkDir) {\n        const session = networkSession || \"default\";\n        return { path: getNetworkDir(session), enabled: false };\n      }\n      return { path: networkDir, enabled: networkEnabled };\n    }\n\n    case \"network_clear\": {\n      if (!networkDir) {\n        return { cleared: false, error: \"Network capture not enabled\" };\n      }\n\n      try {\n        const entries = await fs.readdir(networkDir, { withFileTypes: true });\n        for (const entry of entries) {\n          if (entry.isDirectory()) {\n            await fs.rm(path.join(networkDir, entry.name), { recursive: true });\n          }\n        }\n        networkCounter = 0;\n        pendingRequests.clear();\n        return { cleared: true, path: networkDir };\n      } catch (err) {\n        return {\n          cleared: false,\n          error: err instanceof Error ? err.message : String(err),\n        };\n      }\n    }\n\n    // Daemon control\n    case \"stop\": {\n      process.nextTick(() => {\n        process.emit(\"SIGTERM\");\n      });\n      return { stopping: true };\n    }\n\n    default:\n      throw new Error(`Unknown command: ${command}`);\n  }\n}\n\n// ==================== CLIENT ====================\n\nasync function sendCommandOnce(\n  session: string,\n  command: string,\n  args: unknown[],\n): Promise<unknown> {\n  return new Promise((resolve, reject) => {\n    const socketPath = getSocketPath(session);\n    const client = net.createConnection(socketPath);\n    let done = false;\n\n    const timeout = setTimeout(() => {\n      cleanup();\n      reject(new Error(\"Command timeout\"));\n    }, 60000);\n\n    const cleanup = () => {\n      if (!done) {\n        done = true;\n        clearTimeout(timeout);\n        rl.close();\n        client.destroy();\n      }\n    };\n\n    const rl = readline.createInterface({ input: client });\n\n    rl.on(\"line\", (line) => {\n      const response: DaemonResponse = JSON.parse(line);\n      cleanup();\n      if (response.success) {\n        resolve(response.result);\n      } else {\n        reject(new Error(response.error));\n      }\n    });\n\n    rl.on(\"error\", () => {});\n\n    client.on(\"connect\", () => {\n      const request: DaemonRequest = { command, args };\n      client.write(JSON.stringify(request) + \"\\n\");\n    });\n\n    client.on(\"error\", (err) => {\n      cleanup();\n      reject(new Error(`Connection failed: ${err.message}`));\n    });\n  });\n}\n\n/** Send command with automatic retry and daemon restart on connection failure */\nasync function sendCommand(\n  session: string,\n  command: string,\n  args: unknown[],\n  headless: boolean = false,\n): Promise<unknown> {\n  const maxRetries = 3;\n\n  for (let attempt = 0; attempt < maxRetries; attempt++) {\n    try {\n      return await sendCommandOnce(session, command, args);\n    } catch (err) {\n      const errMsg = err instanceof Error ? err.message : String(err);\n\n      if (command === \"stop\") {\n        throw err;\n      }\n\n      const isConnectionError =\n        errMsg.includes(\"ENOENT\") ||\n        errMsg.includes(\"ECONNREFUSED\") ||\n        errMsg.includes(\"Connection failed\");\n\n      if (!isConnectionError) {\n        throw err;\n      }\n\n      // Attempt 0: Brief wait and retry (socket might be temporarily unavailable)\n      if (attempt === 0) {\n        await new Promise((r) => setTimeout(r, 200));\n        continue;\n      }\n\n      // Attempt 1: Try to restart daemon without cleanup\n      if (attempt === 1) {\n        await ensureDaemon(session, headless);\n        continue;\n      }\n\n      // Final attempt: Full cleanup and restart\n      await killChromeProcesses(session);\n      await cleanupStaleFiles(session);\n      await ensureDaemon(session, headless);\n    }\n  }\n\n  throw new Error(\n    `Max retries exceeded for command ${command} on session ${session}`,\n  );\n}\n\nasync function stopDaemonAndCleanup(session: string): Promise<void> {\n  try {\n    await sendCommandOnce(session, \"stop\", []);\n  } catch {\n    // Daemon may already be down.\n  }\n  await new Promise((r) => setTimeout(r, 500));\n  await cleanupStaleFiles(session);\n}\n\nasync function ensureDaemon(session: string, headless: boolean): Promise<void> {\n  const wantMode = await getDesiredMode(session);\n  assertModeSupported(wantMode);\n\n  if (await isDaemonRunning(session)) {\n    // Missing mode file means daemon predates mode support, which was local-only.\n    const currentMode = (await readCurrentMode(session)) ?? \"local\";\n    if (currentMode === wantMode) {\n      return;\n    }\n    await stopDaemonAndCleanup(session);\n  }\n\n  // Acquire lock before spawning to prevent race conditions\n  const locked = await acquireLock(session);\n  if (!locked) {\n    throw new Error(`Timeout acquiring lock for session ${session}`);\n  }\n\n  try {\n    // Re-check after acquiring lock (another process may have started daemon)\n    if (await isDaemonRunning(session)) {\n      const currentMode = (await readCurrentMode(session)) ?? \"local\";\n      if (currentMode === wantMode) {\n        return;\n      }\n      await stopDaemonAndCleanup(session);\n    }\n\n    const args = [\"--session\", session, \"daemon\"];\n    if (headless) args.push(\"--headless\");\n\n    const child = spawn(process.argv[0], [process.argv[1], ...args], {\n      detached: true,\n      // Avoid piping stdout for detached daemon startup. Deep-locator internals\n      // can log via console fallback, and writing to a broken pipe crashes daemon.\n      stdio: [\"ignore\", \"ignore\", \"ignore\"],\n    });\n    child.unref();\n\n    await new Promise<void>((resolve, reject) => {\n      let settled = false;\n\n      const finish = (err?: Error) => {\n        if (settled) return;\n        settled = true;\n        clearTimeout(timeout);\n        child.off(\"error\", onError);\n        child.off(\"exit\", onExit);\n        if (err) reject(err);\n        else resolve();\n      };\n\n      const onError = (err: Error) => {\n        finish(err);\n      };\n\n      const onExit = (code: number | null, signal: string | null) => {\n        finish(\n          new Error(\n            `Daemon exited before ready (code=${code ?? \"null\"}, signal=${signal ?? \"null\"})`,\n          ),\n        );\n      };\n\n      const timeout = setTimeout(() => {\n        finish(new Error(\"Timeout waiting for daemon to start\"));\n      }, 30000);\n\n      child.once(\"error\", onError);\n      child.once(\"exit\", onExit);\n\n      // Readiness is determined by socket connectivity, not daemon stdout.\n      waitForSocketReady(getSocketPath(session), 28000)\n        .then(() => finish())\n        .catch((err) =>\n          finish(err instanceof Error ? err : new Error(String(err))),\n        );\n    });\n  } finally {\n    await releaseLock(session);\n  }\n}\n\n// ==================== CLI INTERFACE ====================\n\ninterface GlobalOpts {\n  ws?: string;\n  headless?: boolean;\n  headed?: boolean;\n  json?: boolean;\n  session?: string;\n}\n\nfunction getSession(opts: GlobalOpts): string {\n  return opts.session ?? process.env.BROWSE_SESSION ?? \"default\";\n}\n\nfunction isHeadless(opts: GlobalOpts): boolean {\n  return opts.headless === true && opts.headed !== true;\n}\n\nfunction output(data: unknown, json: boolean): void {\n  if (json) {\n    console.log(JSON.stringify(data, null, 2));\n  } else if (typeof data === \"string\") {\n    console.log(data);\n  } else {\n    console.log(JSON.stringify(data, null, 2));\n  }\n}\n\nasync function runCommand(command: string, args: unknown[]): Promise<unknown> {\n  const opts = program.opts<GlobalOpts>();\n  const session = getSession(opts);\n  const headless = isHeadless(opts);\n  // If --ws provided, bypass daemon and connect directly\n  if (opts.ws) {\n    const stagehand = new Stagehand({\n      env: \"LOCAL\",\n      verbose: 0,\n      disablePino: true,\n      localBrowserLaunchOptions: {\n        cdpUrl: opts.ws,\n      },\n    });\n    await stagehand.init();\n    try {\n      return await executeCommand(stagehand.context, command, args);\n    } finally {\n      await stagehand.close();\n    }\n  }\n\n  await ensureDaemon(session, headless);\n  return sendCommand(session, command, args, headless);\n}\n\nprogram\n  .name(\"browse\")\n  .description(\"Browser automation CLI for AI agents\")\n  .version(VERSION)\n  .option(\n    \"--ws <url>\",\n    \"CDP WebSocket URL (bypasses daemon, direct connection)\",\n  )\n  .option(\"--headless\", \"Run Chrome in headless mode\")\n  .option(\"--headed\", \"Run Chrome with visible window (default)\")\n  .option(\"--json\", \"Output as JSON\", false)\n  .option(\n    \"--session <name>\",\n    \"Session name for multiple browsers (or use BROWSE_SESSION env var)\",\n  );\n\n// ==================== DAEMON COMMANDS ====================\n\nprogram\n  .command(\"start\")\n  .description(\"Start browser daemon (auto-started by other commands)\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    const session = getSession(opts);\n    if (await isDaemonRunning(session)) {\n      console.log(JSON.stringify({ status: \"already running\", session }));\n      return;\n    }\n    await ensureDaemon(session, isHeadless(opts));\n    console.log(JSON.stringify({ status: \"started\", session }));\n  });\n\nprogram\n  .command(\"stop\")\n  .description(\"Stop browser daemon\")\n  .option(\"--force\", \"Force kill Chrome processes if daemon is unresponsive\")\n  .action(async (cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    const session = getSession(opts);\n    // Clear any explicit env override so next start uses env var detection\n    try {\n      await fs.unlink(getModeOverridePath(session));\n    } catch {}\n    try {\n      await sendCommand(session, \"stop\", []);\n      console.log(JSON.stringify({ status: \"stopped\", session }));\n    } catch {\n      if (cmdOpts.force) {\n        await killChromeProcesses(session);\n        await cleanupStaleFiles(session);\n        console.log(JSON.stringify({ status: \"force stopped\", session }));\n      } else {\n        console.log(JSON.stringify({ status: \"not running\", session }));\n      }\n    }\n  });\n\nprogram\n  .command(\"status\")\n  .description(\"Check daemon status\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    const session = getSession(opts);\n    const running = await isDaemonRunning(session);\n    let wsUrl = null;\n    let mode: BrowseMode | null = null;\n    if (running) {\n      try {\n        wsUrl = await fs.readFile(getWsPath(session), \"utf-8\");\n      } catch {}\n      mode = await readCurrentMode(session);\n    }\n    console.log(JSON.stringify({ running, session, wsUrl, mode }));\n  });\n\nprogram\n  .command(\"env [target]\")\n  .description(\"Show or switch browser environment (local | remote)\")\n  .action(async (target?: string) => {\n    const opts = program.opts<GlobalOpts>();\n    const session = getSession(opts);\n\n    if (!target) {\n      let mode: string | null = null;\n      const desiredMode = await getDesiredMode(session);\n      if (await isDaemonRunning(session)) {\n        mode = toModeTarget((await readCurrentMode(session)) ?? desiredMode);\n      }\n      console.log(\n        JSON.stringify({\n          mode: mode ?? \"not running\",\n          desired: toModeTarget(desiredMode),\n          session,\n        }),\n      );\n      return;\n    }\n\n    const modeMap: Record<string, BrowseMode> = {\n      local: \"local\",\n      remote: \"browserbase\",\n    };\n    const mapped = modeMap[target];\n    if (!mapped) {\n      console.error(\"Usage: browse env [local|remote]\");\n      process.exit(1);\n    }\n\n    try {\n      assertModeSupported(mapped);\n    } catch (err) {\n      console.error(err instanceof Error ? err.message : String(err));\n      process.exit(1);\n    }\n\n    await fs.writeFile(getModeOverridePath(session), mapped);\n\n    if (await isDaemonRunning(session)) {\n      const currentMode = (await readCurrentMode(session)) ?? \"local\";\n      if (currentMode === mapped) {\n        console.log(\n          JSON.stringify({\n            mode: toModeTarget(mapped),\n            session,\n            restarted: false,\n          }),\n        );\n        return;\n      }\n      await stopDaemonAndCleanup(session);\n    }\n\n    await ensureDaemon(session, isHeadless(opts));\n\n    console.log(\n      JSON.stringify({\n        mode: toModeTarget(mapped),\n        session,\n        restarted: true,\n      }),\n    );\n  });\n\nprogram\n  .command(\"refs\")\n  .description(\"Show cached ref map from last snapshot\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"refs\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"daemon\")\n  .description(\"Run as daemon (internal use)\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    await runDaemon(getSession(opts), isHeadless(opts));\n  });\n\n// ==================== NAVIGATION ====================\n\nprogram\n  .command(\"open <url>\")\n  .alias(\"goto\")\n  .description(\"Navigate to URL\")\n  .option(\n    \"--wait <state>\",\n    \"Wait state: load, domcontentloaded, networkidle\",\n    \"load\",\n  )\n  .option(\"-t, --timeout <ms>\", \"Navigation timeout in milliseconds\", \"30000\")\n  .option(\n    \"--context-id <id>\",\n    \"Browserbase context ID to load browser state (remote mode only)\",\n  )\n  .option(\n    \"--persist\",\n    \"Persist context changes back after session ends (requires --context-id)\",\n    false,\n  )\n  .action(async (url: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      // Validate context flags\n      if (cmdOpts.persist && !cmdOpts.contextId) {\n        console.error(\"Error: --persist requires --context-id\");\n        process.exit(1);\n      }\n\n      const session = getSession(opts);\n\n      if (cmdOpts.contextId) {\n        // Contexts only work with Browserbase remote sessions\n        const desiredMode = await getDesiredMode(session);\n        if (desiredMode === \"local\") {\n          console.error(\n            \"Error: --context-id is only supported in remote mode. Run `browse env remote` first.\",\n          );\n          process.exit(1);\n        }\n\n        const newConfig = JSON.stringify({\n          id: cmdOpts.contextId,\n          persist: cmdOpts.persist ?? false,\n        });\n\n        // If daemon is already running with a different context, restart it\n        // (context is baked into the Browserbase session at creation time)\n        if (await isDaemonRunning(session)) {\n          let currentConfig: string | null = null;\n          try {\n            currentConfig = await fs.readFile(getContextPath(session), \"utf-8\");\n          } catch {}\n          if (currentConfig !== newConfig) {\n            await stopDaemonAndCleanup(session);\n          }\n        }\n\n        await fs.writeFile(getContextPath(session), newConfig);\n      } else {\n        // No --context-id: clear any stale context file so the daemon starts clean\n        try {\n          await fs.unlink(getContextPath(session));\n        } catch {}\n      }\n\n      const result = await runCommand(\"open\", [\n        url,\n        cmdOpts.wait,\n        parseInt(cmdOpts.timeout),\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"reload\")\n  .description(\"Reload current page\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"reload\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"back\")\n  .description(\"Go back in history\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"back\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"forward\")\n  .description(\"Go forward in history\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"forward\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== CLICK ACTIONS ====================\n\nprogram\n  .command(\"click <ref>\")\n  .description(\"Click element by ref (e.g., @0-5, 0-5, or CSS/XPath selector)\")\n  .option(\"-b, --button <btn>\", \"Mouse button: left, right, middle\", \"left\")\n  .option(\"-c, --count <n>\", \"Click count\", \"1\")\n  .option(\n    \"-f, --force\",\n    \"Force click even if element has no layout (uses synthetic event)\",\n  )\n  .action(async (ref: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"click\", [\n        ref,\n        {\n          button: cmdOpts.button,\n          clickCount: parseInt(cmdOpts.count),\n          force: cmdOpts.force,\n        },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"click_xy <x> <y>\")\n  .description(\"Click at exact coordinates\")\n  .option(\"-b, --button <btn>\", \"Mouse button: left, right, middle\", \"left\")\n  .option(\"-c, --count <n>\", \"Click count\", \"1\")\n  .option(\"--xpath\", \"Return XPath of clicked element\")\n  .action(async (x: string, y: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"click_xy\", [\n        parseFloat(x),\n        parseFloat(y),\n        {\n          button: cmdOpts.button,\n          clickCount: parseInt(cmdOpts.count),\n          returnXPath: cmdOpts.xpath,\n        },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== COORDINATE ACTIONS ====================\n\nprogram\n  .command(\"hover <x> <y>\")\n  .description(\"Hover at coordinates\")\n  .option(\"--xpath\", \"Return XPath of hovered element\")\n  .action(async (x: string, y: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"hover\", [\n        parseFloat(x),\n        parseFloat(y),\n        { returnXPath: cmdOpts.xpath },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"scroll <x> <y> <deltaX> <deltaY>\")\n  .description(\"Scroll at coordinates\")\n  .option(\"--xpath\", \"Return XPath of scrolled element\")\n  .action(async (x: string, y: string, dx: string, dy: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"scroll\", [\n        parseFloat(x),\n        parseFloat(y),\n        parseFloat(dx),\n        parseFloat(dy),\n        { returnXPath: cmdOpts.xpath },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"drag <fromX> <fromY> <toX> <toY>\")\n  .description(\"Drag from one point to another\")\n  .option(\"-b, --button <btn>\", \"Mouse button: left, right, middle\", \"left\")\n  .option(\"--steps <n>\", \"Number of intermediate drag steps\", \"10\")\n  .option(\"--delay <ms>\", \"Delay between drag steps in milliseconds\", \"0\")\n  .option(\"--xpath\", \"Return XPath of source and target elements\")\n  .action(async (fx: string, fy: string, tx: string, ty: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"drag\", [\n        parseFloat(fx),\n        parseFloat(fy),\n        parseFloat(tx),\n        parseFloat(ty),\n        {\n          button: cmdOpts.button,\n          steps: parseInt(cmdOpts.steps, 10),\n          delay: parseInt(cmdOpts.delay, 10),\n          returnXPath: cmdOpts.xpath,\n        },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== KEYBOARD ====================\n\nprogram\n  .command(\"type <text>\")\n  .description(\"Type text\")\n  .option(\"-d, --delay <ms>\", \"Delay between keystrokes\")\n  .option(\"--mistakes\", \"Enable human-like typing with mistakes\")\n  .action(async (text: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"type\", [\n        text,\n        {\n          delay: cmdOpts.delay ? parseInt(cmdOpts.delay) : undefined,\n          mistakes: cmdOpts.mistakes,\n        },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"press <key>\")\n  .alias(\"key\")\n  .description(\"Press key (e.g., Enter, Tab, Escape, Cmd+A)\")\n  .action(async (key: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"press\", [key]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== ELEMENT ACTIONS ====================\n\nprogram\n  .command(\"fill <selector> <value>\")\n  .description(\"Fill input element (presses Enter by default)\")\n  .option(\"--no-press-enter\", \"Don't press Enter after filling\")\n  .action(async (selector: string, value: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const pressEnter = cmdOpts.pressEnter !== false;\n      const result = await runCommand(\"fill\", [\n        selector,\n        value,\n        { pressEnter },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"select <selector> <values...>\")\n  .description(\"Select option(s)\")\n  .action(async (selector: string, values: string[]) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"select\", [selector, values]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"highlight <selector>\")\n  .description(\"Highlight element\")\n  .option(\"-d, --duration <ms>\", \"Duration\", \"2000\")\n  .action(async (selector: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"highlight\", [\n        selector,\n        parseInt(cmdOpts.duration),\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== PAGE INFO ====================\n\nprogram\n  .command(\"get <what> [selector]\")\n  .description(\n    \"Get page info: url, title, text, html, value, box, visible, checked\",\n  )\n  .action(async (what: string, selector?: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"get\", [what, selector]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== SCREENSHOT ====================\n\nprogram\n  .command(\"screenshot [path]\")\n  .description(\"Take screenshot\")\n  .option(\"-f, --full-page\", \"Full page screenshot\")\n  .option(\"-t, --type <type>\", \"Image type: png, jpeg\", \"png\")\n  .option(\"-q, --quality <n>\", \"JPEG quality (0-100)\")\n  .option(\"--clip <json>\", \"Clip region as JSON\")\n  .option(\"--no-animations\", \"Disable animations\")\n  .option(\"--hide-caret\", \"Hide text caret\")\n  .action(async (filePath: string | undefined, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"screenshot\", [\n        {\n          path: filePath,\n          fullPage: cmdOpts.fullPage,\n          type: cmdOpts.type,\n          quality: cmdOpts.quality ? parseInt(cmdOpts.quality) : undefined,\n          clip: cmdOpts.clip ? JSON.parse(cmdOpts.clip) : undefined,\n          animations: cmdOpts.animations === false ? \"disabled\" : \"allow\",\n          caret: cmdOpts.hideCaret ? \"hide\" : \"initial\",\n        },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== SNAPSHOT ====================\n\nprogram\n  .command(\"snapshot\")\n  .description(\"Get accessibility tree snapshot\")\n  .option(\"-c, --compact\", \"Output tree only (no xpath map)\")\n  .action(async (cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = (await runCommand(\"snapshot\", [cmdOpts.compact])) as {\n        tree: string;\n        xpathMap?: Record<string, string>;\n        urlMap?: Record<string, string>;\n      };\n      if (cmdOpts.compact && !opts.json) {\n        console.log(result.tree);\n      } else {\n        output(result, opts.json ?? false);\n      }\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== VIEWPORT ====================\n\nprogram\n  .command(\"viewport <width> <height>\")\n  .description(\"Set viewport size\")\n  .option(\"-s, --scale <n>\", \"Device scale factor\", \"1\")\n  .action(async (w: string, h: string, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"viewport\", [\n        parseInt(w),\n        parseInt(h),\n        parseFloat(cmdOpts.scale),\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== EVAL ====================\n\nprogram\n  .command(\"eval <expression>\")\n  .description(\"Evaluate JavaScript in page\")\n  .action(async (expr: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"eval\", [expr]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== WAIT ====================\n\nprogram\n  .command(\"wait <type> [arg]\")\n  .description(\"Wait for: load, selector, timeout\")\n  .option(\"-t, --timeout <ms>\", \"Timeout\", \"30000\")\n  .option(\n    \"-s, --state <state>\",\n    \"Element state: visible, hidden, attached, detached\",\n    \"visible\",\n  )\n  .action(async (type: string, arg: string | undefined, cmdOpts) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"wait\", [\n        type,\n        arg,\n        { timeout: parseInt(cmdOpts.timeout), state: cmdOpts.state },\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== ELEMENT STATE CHECKS ====================\n\nprogram\n  .command(\"is <check> <selector>\")\n  .description(\"Check element state: visible, checked\")\n  .action(async (check: string, selector: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"is\", [check, selector]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== CURSOR ====================\n\nprogram\n  .command(\"cursor\")\n  .description(\"Enable visual cursor overlay\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"cursor\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== MULTI-PAGE ====================\n\nprogram\n  .command(\"pages\")\n  .description(\"List all open pages\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"pages\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"newpage [url]\")\n  .description(\"Create a new page/tab\")\n  .action(async (url?: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"newpage\", [url]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"tab_switch <index>\")\n  .alias(\"switch\")\n  .description(\"Switch to tab by index\")\n  .action(async (index: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"tab_switch\", [parseInt(index)]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nprogram\n  .command(\"tab_close [index]\")\n  .alias(\"close\")\n  .description(\"Close tab by index (defaults to last tab)\")\n  .action(async (index?: string) => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"tab_close\", [\n        index ? parseInt(index) : undefined,\n      ]);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== NETWORK CAPTURE ====================\n\nconst networkCmd = program\n  .command(\"network\")\n  .description(\n    \"Network capture commands (writes to filesystem for agent inspection)\",\n  );\n\nnetworkCmd\n  .command(\"on\")\n  .description(\"Enable network capture (creates temp directory for requests)\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"network_enable\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nnetworkCmd\n  .command(\"off\")\n  .description(\"Disable network capture\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"network_disable\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nnetworkCmd\n  .command(\"path\")\n  .description(\"Get network capture directory path\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"network_path\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\nnetworkCmd\n  .command(\"clear\")\n  .description(\"Clear all captured requests\")\n  .action(async () => {\n    const opts = program.opts<GlobalOpts>();\n    try {\n      const result = await runCommand(\"network_clear\", []);\n      output(result, opts.json ?? false);\n    } catch (e) {\n      console.error(\"Error:\", e instanceof Error ? e.message : e);\n      process.exit(1);\n    }\n  });\n\n// ==================== RUN ====================\n\nprogram.parse();\n"
  },
  {
    "path": "packages/cli/tests/cli.test.ts",
    "content": "/**\n * Browse CLI Tests\n *\n * Comprehensive test suite covering:\n * - Daemon lifecycle\n * - Navigation commands\n * - Actions (click, type, fill)\n * - Information retrieval (snapshot, screenshot, get)\n * - Multi-tab operations\n * - Network capture\n * - Error handling\n */\n\nimport { describe, it, expect, beforeAll, afterAll, afterEach } from \"vitest\";\nimport { exec } from \"child_process\";\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport * as os from \"os\";\n\n// CLI executable path - use the built dist for testing (daemon spawns via process.argv[0])\nconst CLI_PATH = path.join(__dirname, \"../dist/index.js\");\n\n// Test session name to avoid conflicts\nconst TEST_SESSION = `test-${Date.now()}`;\n\n// Helper to run CLI commands\nasync function browse(\n  args: string,\n  options: { timeout?: number; session?: string } = {},\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n  const session = options.session ?? TEST_SESSION;\n  const timeout = options.timeout ?? 30000;\n\n  return new Promise((resolve) => {\n    const fullArgs = `node ${CLI_PATH} --headless --session ${session} ${args}`;\n    exec(fullArgs, { timeout }, (error, stdout, stderr) => {\n      resolve({\n        stdout: stdout.trim(),\n        stderr: stderr.trim(),\n        exitCode: error?.code ?? 0,\n      });\n    });\n  });\n}\n\n// Helper to parse JSON output\nfunction parseJson<T = Record<string, unknown>>(output: string): T {\n  try {\n    return JSON.parse(output) as T;\n  } catch {\n    throw new Error(`Failed to parse JSON: ${output}`);\n  }\n}\n\n// Cleanup helper\nasync function cleanupSession(session: string): Promise<void> {\n  const tmpDir = os.tmpdir();\n  const patterns = [\n    `browse-${session}.sock`,\n    `browse-${session}.pid`,\n    `browse-${session}.ws`,\n    `browse-${session}.chrome.pid`,\n    `browse-${session}.mode`,\n    `browse-${session}.mode-override`,\n  ];\n\n  for (const pattern of patterns) {\n    try {\n      await fs.unlink(path.join(tmpDir, pattern));\n    } catch {}\n  }\n\n  // Clean network dir\n  try {\n    await fs.rm(path.join(tmpDir, `browse-${session}-network`), {\n      recursive: true,\n    });\n  } catch {}\n}\n\ndescribe(\"Browse CLI\", () => {\n  // Cleanup before and after all tests\n  beforeAll(async () => {\n    await cleanupSession(TEST_SESSION);\n  });\n\n  afterAll(async () => {\n    // Stop daemon if running\n    await browse(\"stop --force\");\n    await cleanupSession(TEST_SESSION);\n  });\n\n  describe(\"Daemon Lifecycle\", () => {\n    afterEach(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should start daemon on first command\", async () => {\n      const result = await browse(\"status\");\n      const data = parseJson(result.stdout);\n      // Initially not running\n      expect(data.running).toBe(false);\n\n      // Start via command\n      const startResult = await browse(\"start\");\n      expect(startResult.stdout).toContain(\"started\");\n\n      // Now should be running\n      const statusResult = await browse(\"status\");\n      const statusData = parseJson(statusResult.stdout);\n      expect(statusData.running).toBe(true);\n    });\n\n    it(\"should stop daemon gracefully\", async () => {\n      await browse(\"start\");\n\n      const stopResult = await browse(\"stop\");\n      const data = parseJson(stopResult.stdout);\n      expect(data.status).toBe(\"stopped\");\n\n      // Verify stopped\n      const statusResult = await browse(\"status\");\n      const statusData = parseJson(statusResult.stdout);\n      expect(statusData.running).toBe(false);\n    });\n\n    it(\"should force stop unresponsive daemon\", async () => {\n      await browse(\"start\");\n\n      const result = await browse(\"stop --force\");\n      const data = parseJson(result.stdout);\n      expect([\"stopped\", \"force stopped\", \"not running\"]).toContain(\n        data.status,\n      );\n    });\n\n    it(\"should support multiple sessions\", async () => {\n      const session1 = `${TEST_SESSION}-1`;\n      const session2 = `${TEST_SESSION}-2`;\n\n      try {\n        // Start both sessions\n        await browse(\"start\", { session: session1 });\n        await browse(\"start\", { session: session2 });\n\n        // Both should be running\n        const status1 = parseJson(\n          (await browse(\"status\", { session: session1 })).stdout,\n        );\n        const status2 = parseJson(\n          (await browse(\"status\", { session: session2 })).stdout,\n        );\n\n        expect(status1.running).toBe(true);\n        expect(status2.running).toBe(true);\n      } finally {\n        await browse(\"stop --force\", { session: session1 });\n        await browse(\"stop --force\", { session: session2 });\n        await cleanupSession(session1);\n        await cleanupSession(session2);\n      }\n    });\n  });\n\n  describe(\"Navigation\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should navigate to URL\", async () => {\n      const result = await browse(\"open https://example.com\");\n      const data = parseJson(result.stdout);\n      expect(data.url).toContain(\"example.com\");\n    });\n\n    it(\"should get current URL\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"get url\");\n      const data = parseJson(result.stdout);\n      expect(data.url).toContain(\"example.com\");\n    });\n\n    it(\"should get page title\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"get title\");\n      const data = parseJson(result.stdout);\n      expect(data.title).toBeTruthy();\n    });\n\n    it(\"should reload page\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"reload\");\n      const data = parseJson(result.stdout);\n      expect(data.url).toContain(\"example.com\");\n    });\n  });\n\n  describe(\"Snapshot\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n      await browse(\"open https://example.com\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should take snapshot with refs\", async () => {\n      const result = await browse(\"snapshot\");\n      const data = parseJson(result.stdout);\n\n      expect(data.tree).toBeTruthy();\n      expect(data.xpathMap).toBeTruthy();\n      expect(typeof data.xpathMap).toBe(\"object\");\n    });\n\n    it(\"should take compact snapshot\", async () => {\n      const result = await browse(\"snapshot -c\");\n      // Compact mode outputs tree directly (not JSON when not --json)\n      expect(result.stdout).toContain(\"RootWebArea\");\n    });\n\n    it(\"should populate refs for subsequent commands\", async () => {\n      await browse(\"snapshot\");\n      const refsResult = await browse(\"refs\");\n      const data = parseJson(refsResult.stdout);\n\n      expect(data.count).toBeGreaterThan(0);\n      expect(data.xpathMap).toBeTruthy();\n    });\n  });\n\n  describe(\"Screenshot\", () => {\n    const screenshotPath = path.join(\n      os.tmpdir(),\n      `browse-test-${Date.now()}.png`,\n    );\n\n    beforeAll(async () => {\n      await browse(\"start\");\n      await browse(\"open https://example.com\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n      try {\n        await fs.unlink(screenshotPath);\n      } catch {}\n    });\n\n    it(\"should take screenshot and return base64\", async () => {\n      const result = await browse(\"screenshot\");\n      const data = parseJson<{ base64: string }>(result.stdout);\n      expect(data.base64).toBeTruthy();\n      expect(data.base64.length).toBeGreaterThan(100);\n    });\n\n    it(\"should save screenshot to file\", async () => {\n      const result = await browse(`screenshot ${screenshotPath}`);\n      const data = parseJson(result.stdout);\n      expect(data.saved).toBe(screenshotPath);\n\n      // Verify file exists\n      const stat = await fs.stat(screenshotPath);\n      expect(stat.size).toBeGreaterThan(0);\n    });\n  });\n\n  describe(\"Actions\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should click by coordinates\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"click_xy 100 100\");\n      const data = parseJson(result.stdout);\n      expect(data.clicked).toBe(true);\n    });\n\n    it(\"should click by ref after snapshot\", async () => {\n      await browse(\"open https://example.com\");\n      await browse(\"snapshot\");\n\n      // Find a clickable ref\n      const refsResult = await browse(\"refs\");\n      const refs = parseJson<{\n        count: number;\n        xpathMap: Record<string, string>;\n      }>(refsResult.stdout);\n\n      if (refs.count > 0) {\n        const firstRef = Object.keys(refs.xpathMap)[0];\n        const result = await browse(`click @${firstRef}`);\n        const data = parseJson(result.stdout);\n        expect(data.clicked).toBe(true);\n      }\n    });\n\n    it(\"should type text\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse('type \"Hello World\"');\n      const data = parseJson(result.stdout);\n      expect(data.typed).toBe(true);\n    });\n\n    it(\"should press keys\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"press Tab\");\n      const data = parseJson(result.stdout);\n      expect(data.pressed).toBe(\"Tab\");\n    });\n\n    it(\"should hover at coordinates\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"hover 200 200\");\n      const data = parseJson(result.stdout);\n      expect(data.hovered).toBe(true);\n    });\n\n    it(\"should scroll\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"scroll 400 400 0 100\");\n      const data = parseJson(result.stdout);\n      expect(data.scrolled).toBe(true);\n    });\n\n    it(\"should drag and drop between coordinates\", async () => {\n      const html = `<!doctype html><html><body style=\"margin:0\"><div id=\"source\" draggable=\"true\" style=\"position:absolute;left:40px;top:40px;width:80px;height:80px;background:#e66;cursor:move\"></div><div id=\"target\" style=\"position:absolute;left:250px;top:40px;width:120px;height:120px;background:#ddd\"></div><div id=\"status\" style=\"position:absolute;left:40px;top:180px\">Not dropped</div><script>const source=document.getElementById('source');const target=document.getElementById('target');const status=document.getElementById('status');source.addEventListener('dragstart',e=>{e.dataTransfer.setData('text/plain','dragged')});target.addEventListener('dragover',e=>{e.preventDefault()});target.addEventListener('drop',e=>{e.preventDefault();status.textContent='Dropped'})</script></body></html>`;\n      const dataUrl = `data:text/html,${encodeURIComponent(html)}`;\n\n      await browse(`open \"${dataUrl}\"`);\n      const dragResult = await browse(\"drag 80 80 310 100 --steps 8 --xpath\");\n      const dragData = parseJson(dragResult.stdout);\n      expect(dragData.dragged).toBe(true);\n      expect(typeof dragData.fromXpath).toBe(\"string\");\n      expect(typeof dragData.toXpath).toBe(\"string\");\n\n      const statusResult = await browse(\n        'eval \"document.getElementById(\\\\\"status\\\\\").textContent\"',\n      );\n      const statusData = parseJson(statusResult.stdout);\n      expect(statusData.result).toBe(\"Dropped\");\n    });\n  });\n\n  describe(\"Multi-Tab\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should list pages\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"pages\");\n      const data = parseJson<{ pages: { index: number; url: string }[] }>(\n        result.stdout,\n      );\n\n      expect(data.pages).toBeInstanceOf(Array);\n      expect(data.pages.length).toBeGreaterThan(0);\n      expect(data.pages[0]).toHaveProperty(\"index\");\n      expect(data.pages[0]).toHaveProperty(\"url\");\n    });\n\n    it(\"should create new page\", async () => {\n      const beforeResult = await browse(\"pages\");\n      const beforeData = parseJson<{ pages: unknown[] }>(beforeResult.stdout);\n      const beforeCount = beforeData.pages.length;\n\n      const newResult = await browse(\"newpage https://github.com\");\n      const newData = parseJson(newResult.stdout);\n      expect(newData.created).toBe(true);\n\n      const afterResult = await browse(\"pages\");\n      const afterData = parseJson<{ pages: unknown[] }>(afterResult.stdout);\n      expect(afterData.pages.length).toBe(beforeCount + 1);\n    });\n\n    it(\"should switch tabs\", async () => {\n      await browse(\"open https://example.com\");\n      await browse(\"newpage https://github.com\");\n\n      const result = await browse(\"tab_switch 0\");\n      const data = parseJson(result.stdout);\n      expect(data.switched).toBe(true);\n      expect(data.index).toBe(0);\n    });\n\n    it(\"should close tab\", async () => {\n      await browse(\"open https://example.com\");\n      await browse(\"newpage https://github.com\");\n\n      const beforeResult = await browse(\"pages\");\n      const beforeCount = parseJson<{ pages: unknown[] }>(beforeResult.stdout)\n        .pages.length;\n\n      const closeResult = await browse(\"tab_close\");\n      const closeData = parseJson(closeResult.stdout);\n      expect(closeData.closed).toBe(true);\n\n      const afterResult = await browse(\"pages\");\n      const afterCount = parseJson<{ pages: unknown[] }>(afterResult.stdout)\n        .pages.length;\n      expect(afterCount).toBe(beforeCount - 1);\n    });\n  });\n\n  describe(\"Waiting\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should wait for timeout\", async () => {\n      await browse(\"open https://example.com\");\n      const start = Date.now();\n      const result = await browse(\"wait timeout 500\");\n      const elapsed = Date.now() - start;\n\n      const data = parseJson(result.stdout);\n      expect(data.waited).toBe(true);\n      expect(elapsed).toBeGreaterThanOrEqual(450);\n    });\n\n    it(\"should wait for load state\", async () => {\n      await browse(\"open https://example.com\");\n      const result = await browse(\"wait load\");\n      const data = parseJson(result.stdout);\n      expect(data.waited).toBe(true);\n    });\n  });\n\n  describe(\"Network Capture\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should enable network capture\", async () => {\n      const result = await browse(\"network on\");\n      const data = parseJson(result.stdout);\n      expect(data.enabled).toBe(true);\n      expect(data.path).toBeTruthy();\n    });\n\n    it(\"should return network path\", async () => {\n      await browse(\"network on\");\n      const result = await browse(\"network path\");\n      const data = parseJson(result.stdout);\n      expect(data.path).toBeTruthy();\n      expect(data.enabled).toBe(true);\n    });\n\n    it(\"should capture requests to filesystem\", async () => {\n      await browse(\"network on\");\n      const pathResult = await browse(\"network path\");\n      const networkDir = parseJson<{ path: string }>(pathResult.stdout).path;\n\n      // Navigate to trigger requests\n      await browse(\"open https://example.com\");\n\n      // Wait for requests to be written\n      await browse(\"wait timeout 1000\");\n\n      // Check if directory has content\n      try {\n        const entries = await fs.readdir(networkDir);\n        // May or may not have captured requests depending on timing\n        expect(Array.isArray(entries)).toBe(true);\n      } catch {\n        // Directory may not exist if no requests captured\n      }\n    });\n\n    it(\"should disable network capture\", async () => {\n      await browse(\"network on\");\n      const result = await browse(\"network off\");\n      const data = parseJson(result.stdout);\n      expect(data.enabled).toBe(false);\n    });\n\n    it(\"should clear network captures\", async () => {\n      await browse(\"network on\");\n      await browse(\"open https://example.com\");\n      await browse(\"wait timeout 500\");\n\n      const result = await browse(\"network clear\");\n      const data = parseJson(result.stdout);\n      expect(data.cleared).toBe(true);\n    });\n  });\n\n  describe(\"Viewport\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n      await browse(\"open https://example.com\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should set viewport size\", async () => {\n      const result = await browse(\"viewport 1920 1080\");\n      const data = parseJson<{ viewport: { width: number; height: number } }>(\n        result.stdout,\n      );\n      expect(data.viewport.width).toBe(1920);\n      expect(data.viewport.height).toBe(1080);\n    });\n  });\n\n  describe(\"Eval\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n      await browse(\"open https://example.com\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should evaluate JavaScript\", async () => {\n      const result = await browse('eval \"document.title\"');\n      const data = parseJson(result.stdout);\n      expect(data.result).toBeTruthy();\n    });\n\n    it(\"should return computed values\", async () => {\n      const result = await browse('eval \"1 + 1\"');\n      const data = parseJson(result.stdout);\n      expect(data.result).toBe(2);\n    });\n  });\n\n  describe(\"Error Handling\", () => {\n    beforeAll(async () => {\n      await browse(\"start\");\n    });\n\n    afterAll(async () => {\n      await browse(\"stop --force\");\n    });\n\n    it(\"should error on invalid ref\", async () => {\n      await browse(\"open https://example.com\");\n      // Don't run snapshot, so refs are empty\n      const result = await browse(\"click @99-99\");\n      expect(result.stderr).toContain(\"Error\");\n    });\n\n    it(\"should error on unknown command\", async () => {\n      const result = await browse(\"nonexistent\");\n      expect(result.exitCode).not.toBe(0);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/cli/tests/mode.test.ts",
    "content": "import { describe, it, expect, afterEach } from \"vitest\";\nimport { exec } from \"child_process\";\nimport { promises as fs } from \"fs\";\nimport * as path from \"path\";\nimport * as os from \"os\";\n\nconst CLI_PATH = path.join(__dirname, \"../dist/index.js\");\nconst TEST_SESSION = `env-test-${Date.now()}`;\n\nasync function browse(\n  args: string,\n  options: { timeout?: number; env?: NodeJS.ProcessEnv } = {},\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n  const timeout = options.timeout ?? 30000;\n  const env = { ...process.env, ...options.env };\n\n  return new Promise((resolve) => {\n    const fullArgs = `node ${CLI_PATH} --headless --session ${TEST_SESSION} ${args}`;\n    exec(fullArgs, { timeout, env }, (error, stdout, stderr) => {\n      resolve({\n        stdout: stdout.trim(),\n        stderr: stderr.trim(),\n        exitCode: error?.code ?? 0,\n      });\n    });\n  });\n}\n\nfunction parseJson<T = Record<string, unknown>>(output: string): T {\n  try {\n    return JSON.parse(output) as T;\n  } catch {\n    throw new Error(`Failed to parse JSON: ${output}`);\n  }\n}\n\nasync function cleanupSession(session: string): Promise<void> {\n  const tmpDir = os.tmpdir();\n  const patterns = [\n    `browse-${session}.sock`,\n    `browse-${session}.pid`,\n    `browse-${session}.ws`,\n    `browse-${session}.chrome.pid`,\n    `browse-${session}.mode`,\n    `browse-${session}.mode-override`,\n  ];\n\n  for (const pattern of patterns) {\n    try {\n      await fs.unlink(path.join(tmpDir, pattern));\n    } catch {\n      // Ignore missing files.\n    }\n  }\n\n  try {\n    await fs.rm(path.join(tmpDir, `browse-${session}-network`), {\n      recursive: true,\n    });\n  } catch {\n    // Ignore missing directory.\n  }\n}\n\ndescribe(\"Browse CLI env command\", () => {\n  afterEach(async () => {\n    await browse(\"stop --force\");\n    await cleanupSession(TEST_SESSION);\n  });\n\n  it(\"shows desired env even when daemon is not running\", async () => {\n    const result = await browse(\"env\");\n    expect(result.exitCode).toBe(0);\n\n    const data = parseJson(result.stdout);\n    expect(data.mode).toBe(\"not running\");\n    expect([\"local\", \"remote\"]).toContain(data.desired);\n  });\n\n  it(\"rejects unsupported env target\", async () => {\n    const result = await browse(\"env invalid-target\");\n    expect(result.exitCode).not.toBe(0);\n    expect(result.stderr).toContain(\"Usage: browse env [local|remote]\");\n  });\n\n  it(\"rejects remote env without Browserbase credentials\", async () => {\n    const result = await browse(\"env remote\", {\n      env: {\n        ...process.env,\n        BROWSERBASE_API_KEY: \"\",\n      },\n    });\n    expect(result.exitCode).not.toBe(0);\n    expect(result.stderr).toContain(\"Remote mode requires BROWSERBASE_API_KEY\");\n  });\n});\n"
  },
  {
    "path": "packages/cli/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \".\",\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"src/**/*\", \"tests/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/cli/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\"],\n  target: \"node20\",\n  clean: true,\n  shims: true,\n  banner: {\n    js: \"#!/usr/bin/env node\",\n  },\n  // Bundle everything possible, only externalize what truly can't be bundled\n  noExternal: [/@browserbasehq\\/stagehand/],\n  external: [\n    // Browser automation - user must install playwright to use the CLI\n    \"playwright\",\n    \"playwright-core\",\n    // CJS packages with dynamic requires that break in ESM bundles\n    \"pino\",\n    \"pino-pretty\",\n    \"ws\",\n    \"dotenv\",\n  ],\n});\n"
  },
  {
    "path": "packages/cli/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    testTimeout: 60000,\n    hookTimeout: 60000,\n    include: [\"tests/**/*.test.ts\"],\n    // Run tests sequentially since they share browser state\n    pool: \"forks\",\n    poolOptions: {\n      forks: {\n        singleFork: true,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/core/CHANGELOG.md",
    "content": "# @browserbasehq/stagehand\n\n## 3.2.0\n\n### Minor Changes\n\n- [#1779](https://github.com/browserbase/stagehand/pull/1779) [`2f43ffa`](https://github.com/browserbase/stagehand/commit/2f43ffac11778152d17e4c44405770cc32c3ec8c) Thanks [@shrey150](https://github.com/shrey150)! - feat: add `cdpHeaders` option to `localBrowserLaunchOptions` for passing custom HTTP headers when connecting to an existing browser via CDP URL\n\n- [#1834](https://github.com/browserbase/stagehand/pull/1834) [`63ee247`](https://github.com/browserbase/stagehand/commit/63ee247ac6bf2992046d4f6b2759f46b15643e36) Thanks [@tkattkat](https://github.com/tkattkat)! - Update stagehand agents search tool\n\n- [#1774](https://github.com/browserbase/stagehand/pull/1774) [`521a10e`](https://github.com/browserbase/stagehand/commit/521a10e3698fc5631e219947bc90dad0f8bddaa8) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add new page.setExtraHTTPHeaders() method\n\n### Patch Changes\n\n- [#1759](https://github.com/browserbase/stagehand/pull/1759) [`505e8c6`](https://github.com/browserbase/stagehand/commit/505e8c6736f3706328dbc8df670c49a018058388) Thanks [@shrey150](https://github.com/shrey150)! - Add bedrock to the provider enum in model configuration schemas and regenerate OpenAPI spec.\n\n- [#1814](https://github.com/browserbase/stagehand/pull/1814) [`7dc35f5`](https://github.com/browserbase/stagehand/commit/7dc35f5e25689e6518d68b25ef71536d2781c8aa) Thanks [@tkattkat](https://github.com/tkattkat)! - Change usage of openai provider in agent to default to store:false\n\n- [#1846](https://github.com/browserbase/stagehand/pull/1846) [`335cf47`](https://github.com/browserbase/stagehand/commit/335cf4730e73bce33e92331d04bda4b0fd42685d) Thanks [@aq17](https://github.com/aq17)! - Fix streaming finished event being silently dropped. The final SSE event containing the result payload (success status, message, actions, usage, and messages) was previously discarded instead of being yielded to the caller.\n\n- [#1764](https://github.com/browserbase/stagehand/pull/1764) [`6ba0a1d`](https://github.com/browserbase/stagehand/commit/6ba0a1db7fc2d5d5a2f8927b1417d8f1d15eda10) Thanks [@shrey150](https://github.com/shrey150)! - Expose `headers` in `GoogleVertexProviderSettings` so model configs can pass custom provider headers (for example `X-Goog-Priority`) without TypeScript errors.\n\n- [#1847](https://github.com/browserbase/stagehand/pull/1847) [`4ff3bb8`](https://github.com/browserbase/stagehand/commit/4ff3bb831a6ef6e2d57148e7afb68ea8d23e395d) Thanks [@miguelg719](https://github.com/miguelg719)! - Enable FlowLogger on BROWSERBASE_FLOW_LOGS=1\n\n- [#1752](https://github.com/browserbase/stagehand/pull/1752) [`c27054b`](https://github.com/browserbase/stagehand/commit/c27054bbd0508431ade91d655f89efc87bbf5867) Thanks [@derekmeegan](https://github.com/derekmeegan)! - fix: pause Browserbase agents while captcha solving is active and improve CUA recovery after the solve completes\n\n- [#1800](https://github.com/browserbase/stagehand/pull/1800) [`2abf5b9`](https://github.com/browserbase/stagehand/commit/2abf5b90f1e2bb1442509ef3a686b6128c9cdcf6) Thanks [@shrey150](https://github.com/shrey150)! - Make projectId optional for Browserbase sessions — only BROWSERBASE_API_KEY is required\n\n- [#1766](https://github.com/browserbase/stagehand/pull/1766) [`7817fcc`](https://github.com/browserbase/stagehand/commit/7817fcc315eee4455ce04567cf56c9ec801caf0b) Thanks [@tkattkat](https://github.com/tkattkat)! - Add configurable timeout to tools in agent\n\n- [#1749](https://github.com/browserbase/stagehand/pull/1749) [`7390508`](https://github.com/browserbase/stagehand/commit/73905088c5ed5923d276da9cce2efd0a0a3a46eb) Thanks [@pirate](https://github.com/pirate)! - When connecting to a browser session that has zero open tabs, Stagehand now automatically creates an initial `about:blank` tab so the connection can continue.\n\n- [#1761](https://github.com/browserbase/stagehand/pull/1761) [`611f43a`](https://github.com/browserbase/stagehand/commit/611f43ac8d4c580216d55d2b217c14a9a9c11013) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where handlePossibleNavigation was producing unnecessary error logs on clicks that trigger page close\n\n- [#1817](https://github.com/browserbase/stagehand/pull/1817) [`2402a3c`](https://github.com/browserbase/stagehand/commit/2402a3c4d50270391b3e6440f4385cdcf5e1eb64) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for passing custom headers in clientOptions\n\n## 3.1.0\n\n### Minor Changes\n\n- [#1681](https://github.com/browserbase/stagehand/pull/1681) [`e3db9aa`](https://github.com/browserbase/stagehand/commit/e3db9aa863f44270792215801fe6e3a02a1321aa) Thanks [@tkattkat](https://github.com/tkattkat)! - Add cookie management APIs: `context.addCookies()`, `context.clearCookies()`, & `context.cookies()`\n\n- [#1672](https://github.com/browserbase/stagehand/pull/1672) [`b65756e`](https://github.com/browserbase/stagehand/commit/b65756e9e85643055446aa4a51956f7d6627c89f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add boolean keepAlive parameter to allow for configuring whether the browser should be closed when stagehand.close() is called.\n\n- [#1708](https://github.com/browserbase/stagehand/pull/1708) [`176d420`](https://github.com/browserbase/stagehand/commit/176d42002cc0a2c7d13b4c0ffbbd56b70fdc49e8) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add context.setExtraHTTPHeaders()\n\n- [#1611](https://github.com/browserbase/stagehand/pull/1611) [`8a3c066`](https://github.com/browserbase/stagehand/commit/8a3c06600a9ba98485db7e9ed5c3cc43ea180334) Thanks [@monadoid](https://github.com/monadoid)! - Using `mode` enum instead of old `cua` boolean in openapi spec\n\n### Patch Changes\n\n- [#1683](https://github.com/browserbase/stagehand/pull/1683) [`7584f3e`](https://github.com/browserbase/stagehand/commit/7584f3e92e60a557d2b3e0e0d2a2af04c3527523) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: include shadow DOM in .count() & .nth() & support xpath predicates\n\n- [#1644](https://github.com/browserbase/stagehand/pull/1644) [`1e1c9c1`](https://github.com/browserbase/stagehand/commit/1e1c9c15773e49d5c3cd36021dbc1d23495c1bce) Thanks [@monadoid](https://github.com/monadoid)! - Fix unhandled CDP detaches by returning the original sendCDP promise\n\n- [#1729](https://github.com/browserbase/stagehand/pull/1729) [`6bef890`](https://github.com/browserbase/stagehand/commit/6bef89090ebd231e77d8092b2c32a0f06303d5a9) Thanks [@shrey150](https://github.com/shrey150)! - fix: support Claude 4.6 (Opus and Sonnet) in CUA mode by using the correct `computer_20251124` tool version and `computer-use-2025-11-24` beta header\n\n- [#1647](https://github.com/browserbase/stagehand/pull/1647) [`ffd4b33`](https://github.com/browserbase/stagehand/commit/ffd4b335a873d0f4dcd76ea22d44f47919bf8e49) Thanks [@tkattkat](https://github.com/tkattkat)! - Fix [Agent] - Address bug causing issues with continuing a conversation from past messages in dom mode\n\n- [#1614](https://github.com/browserbase/stagehand/pull/1614) [`677bff5`](https://github.com/browserbase/stagehand/commit/677bff5834c879a2d95f7dbff918b8e1510516b3) Thanks [@miguelg719](https://github.com/miguelg719)! - Enforce <number>-<number> regex validation on act/observe for elementId\n\n- [#1580](https://github.com/browserbase/stagehand/pull/1580) [`65ff464`](https://github.com/browserbase/stagehand/commit/65ff464bc13388eb109eba0a2cf533c1cc202854) Thanks [@tkattkat](https://github.com/tkattkat)! - Add unified variables support across act and agent with a single VariableValue type\n\n- [#1666](https://github.com/browserbase/stagehand/pull/1666) [`101bcf2`](https://github.com/browserbase/stagehand/commit/101bcf2da8b527fd6ace6aa291ada5d0f2d90344) Thanks [@Kylejeong2](https://github.com/Kylejeong2)! - add support for codex models\n\n- [#1728](https://github.com/browserbase/stagehand/pull/1728) [`0a94301`](https://github.com/browserbase/stagehand/commit/0a94301caa991d1aa4cdade6e28a065b1aefb3e2) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - handle potential race condition on `.close()` when using the Stagehand API\n\n- [#1664](https://github.com/browserbase/stagehand/pull/1664) [`b27c04d`](https://github.com/browserbase/stagehand/commit/b27c04d278c290364347acd0c354a878ea9b7c2d) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fixes issue with context.addInitScript() where scripts were not being applied to out of process iframes (OOPIFs), and popup pages with same process iframes (SPIFs)\n\n- [#1632](https://github.com/browserbase/stagehand/pull/1632) [`afbd08b`](https://github.com/browserbase/stagehand/commit/afbd08bb6367a9c9f65f67e453667987e4659918) Thanks [@pirate](https://github.com/pirate)! - Remove automatic `.env` loading via `dotenv`.\n\n  If your app relies on `.env` files, install `dotenv` and load it explicitly in your code:\n\n  ```ts\n  import dotenv from \"dotenv\";\n  dotenv.config({ path: \".env\" });\n  ```\n\n- [#1624](https://github.com/browserbase/stagehand/pull/1624) [`0e8d569`](https://github.com/browserbase/stagehand/commit/0e8d5695f662040f7384e64f46301152802e3c62) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where screenshot masks were not being applied to dialog elements\n\n- [#1596](https://github.com/browserbase/stagehand/pull/1596) [`ff0f979`](https://github.com/browserbase/stagehand/commit/ff0f9795f3b2c1cf4f2610a80ebcb3341a24f987) Thanks [@tkattkat](https://github.com/tkattkat)! - Update usage/metrics handling in agent\n\n- [#1631](https://github.com/browserbase/stagehand/pull/1631) [`2d89d2b`](https://github.com/browserbase/stagehand/commit/2d89d2b35ce812431956b28e0c8b52d32ddc7a27) Thanks [@miguelg719](https://github.com/miguelg719)! - Add right and middle click support to act and observe\n\n- [#1697](https://github.com/browserbase/stagehand/pull/1697) [`aac9a19`](https://github.com/browserbase/stagehand/commit/aac9a19bdfbe62e4508631337ab0bfbcf8ae62b2) Thanks [@shrey150](https://github.com/shrey150)! - fix: support `<frame>` elements in XPath frame boundary detection so `act()` works on legacy `<frameset>` pages\n\n- [#1692](https://github.com/browserbase/stagehand/pull/1692) [`06de50f`](https://github.com/browserbase/stagehand/commit/06de50ff377fd31f1b0fcf79adb996d04562d2c0) Thanks [@shrey150](https://github.com/shrey150)! - fix: skip piercer injection for chrome-extension:// and other non-HTML targets\n\n- [#1613](https://github.com/browserbase/stagehand/pull/1613) [`aa4d981`](https://github.com/browserbase/stagehand/commit/aa4d981e440bdd0e3d3f42ccc310d5958aa25cc6) Thanks [@miguelg719](https://github.com/miguelg719)! - SupportedUnderstudyAction Enum validation for 'method' on act/observe inference\n\n- [#1652](https://github.com/browserbase/stagehand/pull/1652) [`18b1e3b`](https://github.com/browserbase/stagehand/commit/18b1e3bd2b16b721845d52fcf1a45c6158e2403f) Thanks [@miguelg719](https://github.com/miguelg719)! - Add support for gemini 3 flash and pro in hybrid/cua agent\n\n- [#1706](https://github.com/browserbase/stagehand/pull/1706) [`957d82b`](https://github.com/browserbase/stagehand/commit/957d82b9845b4413b123539e81a2e4a490e74a8a) Thanks [@chrisreadsf](https://github.com/chrisreadsf)! - Add GLM to prompt-based JSON fallback for models without native structured output support\n\n- [#1633](https://github.com/browserbase/stagehand/pull/1633) [`22e371a`](https://github.com/browserbase/stagehand/commit/22e371ae4c25deb6350328fe02832bf2b2197b94) Thanks [@tkattkat](https://github.com/tkattkat)! - Add warning when incorrect models are used with agents hybrid mode\n\n- [#1673](https://github.com/browserbase/stagehand/pull/1673) [`d29b91f`](https://github.com/browserbase/stagehand/commit/d29b91fa506636ca36f724fcf106320de54ec3f3) Thanks [@miguelg719](https://github.com/miguelg719)! - Add multi-region support for Stagehand API with region-specific endpoints\n\n- [#1695](https://github.com/browserbase/stagehand/pull/1695) [`7b4f817`](https://github.com/browserbase/stagehand/commit/7b4f817cafb9829ac81c4b5890c318c7f9521fe4) Thanks [@tkattkat](https://github.com/tkattkat)! - Fix: zod bug when pinning zod to v3 and using structured output in agent\n\n- [#1609](https://github.com/browserbase/stagehand/pull/1609) [`3f9ca4d`](https://github.com/browserbase/stagehand/commit/3f9ca4d9acc109101357378d29cf969168991608) Thanks [@miguelg719](https://github.com/miguelg719)! - Add SupportedUnderstudyActions to observe system prompt\n\n- [#1581](https://github.com/browserbase/stagehand/pull/1581) [`49ead1e`](https://github.com/browserbase/stagehand/commit/49ead1e1e8678a8da0f87ad2042491dacc6b01d7) Thanks [@sameelarif](https://github.com/sameelarif)! - **Server-side caching is now available.**\n\n  When running `env: \"BROWSERBASE\"`, Stagehand automatically caches `act()`, `extract()`, and `observe()` results server-side — repeated calls with the same inputs return instantly without consuming LLM tokens.\n\n  Caching is enabled by default and can be disabled via `serverCache: false` on the Stagehand instance or per individual call. Check out the [browserbase blog](https://www.browserbase.com/blog/stagehand-caching) for more details.\n\n- [#1642](https://github.com/browserbase/stagehand/pull/1642) [`3673369`](https://github.com/browserbase/stagehand/commit/36733691f90c15386cf2a7b47d04ef429b7195ae) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where scripts added via context.addInitScripts() were not being injected into new pages that were opened via popups (eg, clicking a link that opens a new page) and/or calling context.newPage(url)\n\n- [#1735](https://github.com/browserbase/stagehand/pull/1735) [`c465e87`](https://github.com/browserbase/stagehand/commit/c465e87ab41942435132c76338518fb3fa8e7896) Thanks [@monadoid](https://github.com/monadoid)! - Supports request header authentication with connectToMCPServer\n\n- [#1705](https://github.com/browserbase/stagehand/pull/1705) [`ae533e4`](https://github.com/browserbase/stagehand/commit/ae533e40195181b53833f8055b1259fb360a927b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - include error cause in UnderstudyCommandException\n\n- [#1636](https://github.com/browserbase/stagehand/pull/1636) [`ea33052`](https://github.com/browserbase/stagehand/commit/ea330520a325583b71b87d85beb740df4bdb9b2d) Thanks [@miguelg719](https://github.com/miguelg719)! - Include executionModel on the AgentConfigSchema\n\n- [#1679](https://github.com/browserbase/stagehand/pull/1679) [`5764ede`](https://github.com/browserbase/stagehand/commit/5764edee7aab00ef1aafafb68fc56eb26c0a70b2) Thanks [@shrey150](https://github.com/shrey150)! - fix issue where locator.count() was not working with xpaths that have attribute predicates\n\n- [#1646](https://github.com/browserbase/stagehand/pull/1646) [`f09b184`](https://github.com/browserbase/stagehand/commit/f09b184cc5e774736280ae8c94ba3f4f13adda80) Thanks [@miguelg719](https://github.com/miguelg719)! - Add user-agent to CDP connections\n\n- [#1637](https://github.com/browserbase/stagehand/pull/1637) [`a7d29de`](https://github.com/browserbase/stagehand/commit/a7d29decee0f7d12e2437267b9eef1795d3b4e3a) Thanks [@miguelg719](https://github.com/miguelg719)! - Improve error and warning message for legacy model format\n\n- [#1685](https://github.com/browserbase/stagehand/pull/1685) [`d334399`](https://github.com/browserbase/stagehand/commit/d3343990041bf9cd5613569840afb0c17131e33c) Thanks [@tkattkat](https://github.com/tkattkat)! - Bump ai sdk & google provider version\n\n- [#1662](https://github.com/browserbase/stagehand/pull/1662) [`44416da`](https://github.com/browserbase/stagehand/commit/44416da7ff33301bb32d3811e6c3be8782a7d168) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where locator.fill() was not working on elements that require direct value setting\n\n- [#1612](https://github.com/browserbase/stagehand/pull/1612) [`bdd8b4e`](https://github.com/browserbase/stagehand/commit/bdd8b4ee3c697a02728375510ab7fae764990576) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue where screenshot mask was only being applied to the first element that the locator resolved to. masks now apply to all matching elements.\n\n## 3.0.8\n\n### Patch Changes\n\n- [#1514](https://github.com/browserbase/stagehand/pull/1514) [`40ce5cc`](https://github.com/browserbase/stagehand/commit/40ce5cc83ec758f4e8c37132a7f4ac8eeea7ca34) Thanks [@tkattkat](https://github.com/tkattkat)! - Rename the close tool in agent to \"done\"\n\n- [#1574](https://github.com/browserbase/stagehand/pull/1574) [`5506f41`](https://github.com/browserbase/stagehand/commit/5506f416d2609d112b553263984e21d7a30e32b1) Thanks [@tkattkat](https://github.com/tkattkat)! - fix(server): pass cdpUrl to localBrowserLaunchOptions when launchOptions absent\n\n- [#1521](https://github.com/browserbase/stagehand/pull/1521) [`84c05ca`](https://github.com/browserbase/stagehand/commit/84c05ca8de4587181faf128e5c7464fd960caacc) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: get agent cache working in API mode\n\n- [#1486](https://github.com/browserbase/stagehand/pull/1486) [`692ffa0`](https://github.com/browserbase/stagehand/commit/692ffa0346ad3d121686aba503c0a22844293efa) Thanks [@tkattkat](https://github.com/tkattkat)! - improve logging in agent\n\n- [#1551](https://github.com/browserbase/stagehand/pull/1551) [`1ef8901`](https://github.com/browserbase/stagehand/commit/1ef8901e1314e90f43b36be20192e652d3b5598f) Thanks [@miguelg719](https://github.com/miguelg719)! - move extract handler response log to after URL injection\n\n- [#1495](https://github.com/browserbase/stagehand/pull/1495) [`72ac775`](https://github.com/browserbase/stagehand/commit/72ac775a831d6f0f376ceda4426525f93cc21452) Thanks [@tkattkat](https://github.com/tkattkat)! - export tool function & type to simplify defining custom tools\n\n- [#1481](https://github.com/browserbase/stagehand/pull/1481) [`3d5af07`](https://github.com/browserbase/stagehand/commit/3d5af07f66d6d26d1f5ac4bd9be7183c3381dd92) Thanks [@tkattkat](https://github.com/tkattkat)! - add waitForTimeout to page\n\n- [#1423](https://github.com/browserbase/stagehand/pull/1423) [`40e1d80`](https://github.com/browserbase/stagehand/commit/40e1d80776b9216422a25a81070ccb3105e56ec2) Thanks [@miguelg719](https://github.com/miguelg719)! - Improve benchmark handling and add metadata\n\n- [#1588](https://github.com/browserbase/stagehand/pull/1588) [`56c0d24`](https://github.com/browserbase/stagehand/commit/56c0d244f9b2431218bfa832ddfc0587930ae038) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add SnapshotOptions to page.snapshot()\n\n- [#1483](https://github.com/browserbase/stagehand/pull/1483) [`16d72fb`](https://github.com/browserbase/stagehand/commit/16d72fb4c4081dd33bf45605d75c27644ea4c00e) Thanks [@tkattkat](https://github.com/tkattkat)! - Optimize screenshot handling in agent hybrid mode\n\n- [#1498](https://github.com/browserbase/stagehand/pull/1498) [`088c4cc`](https://github.com/browserbase/stagehand/commit/088c4cc31dc924bb232a9d5a09ab42cd961c2d36) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: replaying cached actions (for agent & act) now uses the originally defined model, (instead of default model) when action fails and rerunning inference is needed\n\n- [#1575](https://github.com/browserbase/stagehand/pull/1575) [`4276f4a`](https://github.com/browserbase/stagehand/commit/4276f4abc8bbde215faac6c0321bf243484c376b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - expose port param in localBrowserLaunchOptions\n\n- [#1544](https://github.com/browserbase/stagehand/pull/1544) [`6005786`](https://github.com/browserbase/stagehand/commit/600578637e65f6fd18b0cdb322b9e0b857708b2f) Thanks [@tkattkat](https://github.com/tkattkat)! - Recommend hybrid mode over DOM mode in agent, which is now considered legacy\n\n- [#1505](https://github.com/browserbase/stagehand/pull/1505) [`6fbf5fc`](https://github.com/browserbase/stagehand/commit/6fbf5fc811e5e5d9d22f10c5309fbd336892263a) Thanks [@tkattkat](https://github.com/tkattkat)! - Add structured output to agent result + ensure close tool is always called\n\n- [#1511](https://github.com/browserbase/stagehand/pull/1511) [`704cf18`](https://github.com/browserbase/stagehand/commit/704cf18cb2bdd187ba06c35f05ccb47317a7668c) Thanks [@shrey150](https://github.com/shrey150)! - Fix ControlOrMeta keypress event\n\n- [#1480](https://github.com/browserbase/stagehand/pull/1480) [`091296e`](https://github.com/browserbase/stagehand/commit/091296e438bb2374c8bb10ef6c08283978145ebf) Thanks [@tkattkat](https://github.com/tkattkat)! - Update agent to only calculate xpath when caching is enabled\n\n- [#1509](https://github.com/browserbase/stagehand/pull/1509) [`e56c6eb`](https://github.com/browserbase/stagehand/commit/e56c6eb139bf3aad37e98b16626fff13a6c671d0) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support for page.waitForSelector()\n\n- [#1478](https://github.com/browserbase/stagehand/pull/1478) [`2cb78d0`](https://github.com/browserbase/stagehand/commit/2cb78d0f5ddef9f7337a9a2fe3137f1421df700a) Thanks [@tkattkat](https://github.com/tkattkat)! - update agent message handling\n\n- [#1518](https://github.com/browserbase/stagehand/pull/1518) [`5dad639`](https://github.com/browserbase/stagehand/commit/5dad63938f08d968d434bb1ee2804f1e54fb836a) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add page.snapshot() for capturing a stringified DOM snapshot of the page, including an xpath map & url map\n\n- [#1576](https://github.com/browserbase/stagehand/pull/1576) [`b7c2571`](https://github.com/browserbase/stagehand/commit/b7c2571ad4ac563f3ca0518e1f29a40da93e33bc) Thanks [@tkattkat](https://github.com/tkattkat)! - utilize waitForSelector when running agent cache\n\n- [#1560](https://github.com/browserbase/stagehand/pull/1560) [`4c69117`](https://github.com/browserbase/stagehand/commit/4c6911748953199dc9aad3eabe98bcf325f871e4) Thanks [@tkattkat](https://github.com/tkattkat)! - Update coordinate handling in cua and hybrid\n\n## 3.0.7\n\n### Patch Changes\n\n- [#1461](https://github.com/browserbase/stagehand/pull/1461) [`0f3991e`](https://github.com/browserbase/stagehand/commit/0f3991eedc0aaff72ef718dda3ddb0839cf4a464) Thanks [@tkattkat](https://github.com/tkattkat)! - Move hybrid mode out of experimental\n\n- [#1433](https://github.com/browserbase/stagehand/pull/1433) [`e0e22e0`](https://github.com/browserbase/stagehand/commit/e0e22e06bc752a8ffde30f3dbfa58d91e24e6c09) Thanks [@tkattkat](https://github.com/tkattkat)! - Put hybrid mode behind experimental\n\n- [#1456](https://github.com/browserbase/stagehand/pull/1456) [`f261051`](https://github.com/browserbase/stagehand/commit/f2610517d74774374de9ee93191e663439ef55e5) Thanks [@shrey150](https://github.com/shrey150)! - Invoke page.hover for agent move action\n\n- [#1473](https://github.com/browserbase/stagehand/pull/1473) [`e021674`](https://github.com/browserbase/stagehand/commit/e021674f9641c1c5f9d0c1817c3fdf599eea124d) Thanks [@shrey150](https://github.com/shrey150)! - Add safety confirmation support for OpenAI + Google CUA\n\n- [#1399](https://github.com/browserbase/stagehand/pull/1399) [`6a5496f`](https://github.com/browserbase/stagehand/commit/6a5496f17dbb716be1ee1aaa4e5ba9d8c723b30b) Thanks [@tkattkat](https://github.com/tkattkat)! - Ensure cua agent is killed when stagehand.close is called\n\n- [#1436](https://github.com/browserbase/stagehand/pull/1436) [`fea1700`](https://github.com/browserbase/stagehand/commit/fea1700552af3319052f463685752501c8e71de3) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix auto-load key for act/extract/observe parametrized models on api\n\n- [#1439](https://github.com/browserbase/stagehand/pull/1439) [`5b288d9`](https://github.com/browserbase/stagehand/commit/5b288d9ac37406ff22460ac8050bea26b87a378e) Thanks [@tkattkat](https://github.com/tkattkat)! - Remove base64 from agent actions array ( still present in messages object )\n\n- [#1408](https://github.com/browserbase/stagehand/pull/1408) [`e822f5a`](https://github.com/browserbase/stagehand/commit/e822f5a8898df9eb48ca32c321025f0c74b638f0) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - allow for act() cache hit when variable values change\n\n- [#1472](https://github.com/browserbase/stagehand/pull/1472) [`638efc7`](https://github.com/browserbase/stagehand/commit/638efc7fea401bc43dd05dceedf4c13a3495a728) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: agent cache not refreshed on action failure\n\n- [#1424](https://github.com/browserbase/stagehand/pull/1424) [`a890f16`](https://github.com/browserbase/stagehand/commit/a890f16fa3a752f308f858e5ab9c9a0faf6b3b34) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: \"Error: -32000 Failed to convert response to JSON: CBOR: stack limit exceeded\"\n\n- [#1418](https://github.com/browserbase/stagehand/pull/1418) [`934f492`](https://github.com/browserbase/stagehand/commit/934f492ec587bef81f0ce75b45a35b44ab545712) Thanks [@miguelg719](https://github.com/miguelg719)! - Cleanup handlers and bus listeners on close\n\n- [#1430](https://github.com/browserbase/stagehand/pull/1430) [`bd2db92`](https://github.com/browserbase/stagehand/commit/bd2db925f66a826d61d58be1611d55646cbdb560) Thanks [@shrey150](https://github.com/shrey150)! - Fix CUA model coordinate translation\n\n- [#1465](https://github.com/browserbase/stagehand/pull/1465) [`51e0170`](https://github.com/browserbase/stagehand/commit/51e01709ce1c947c1947b4e2cb0b1f4f97b77182) Thanks [@miguelg719](https://github.com/miguelg719)! - Add media resolution high provider option to gemini 3 hybrid agent\n\n- [#1431](https://github.com/browserbase/stagehand/pull/1431) [`05f5580`](https://github.com/browserbase/stagehand/commit/05f5580937c3c157550e3c25ae6671f44f562211) Thanks [@tkattkat](https://github.com/tkattkat)! - Update the cache handling for agent\n\n- [#1432](https://github.com/browserbase/stagehand/pull/1432) [`f56a9c2`](https://github.com/browserbase/stagehand/commit/f56a9c296d4ddce25a405358c66837f8ce4d679f) Thanks [@tkattkat](https://github.com/tkattkat)! - Deprecate cua: true in favor of mode: \"cua\"\n\n- [#1406](https://github.com/browserbase/stagehand/pull/1406) [`b40ae11`](https://github.com/browserbase/stagehand/commit/b40ae11391af49c3581fce27faa1b7483fc4a169) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for hovering with coordinates ( page.hover )\n\n- [#1407](https://github.com/browserbase/stagehand/pull/1407) [`0d2b398`](https://github.com/browserbase/stagehand/commit/0d2b398cd40b32a9ecaf28ede70853036b7c91bd) Thanks [@tkattkat](https://github.com/tkattkat)! - Clean up page methods\n\n- [#1412](https://github.com/browserbase/stagehand/pull/1412) [`cd01f29`](https://github.com/browserbase/stagehand/commit/cd01f290578eac703521f801ba3712f5332918f3) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: load GOOGLE_API_KEY from .env\n\n- [#1462](https://github.com/browserbase/stagehand/pull/1462) [`a734fca`](https://github.com/browserbase/stagehand/commit/a734fca0b4573753767d3ebc48ec414baf4f23e1) Thanks [@shrey150](https://github.com/shrey150)! - fix: correctly pass userDataDir to chrome launcher\n\n- [#1466](https://github.com/browserbase/stagehand/pull/1466) [`b342acf`](https://github.com/browserbase/stagehand/commit/b342acfaae058127fb57664644c5fd965db02bf2) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - move playwright to optional dependencies\n\n- [#1440](https://github.com/browserbase/stagehand/pull/1440) [`2987cd1`](https://github.com/browserbase/stagehand/commit/2987cd1e5ffabefa9411936609635d4a638faed5) Thanks [@tkattkat](https://github.com/tkattkat)! - [Feature] support excluding tools from agent\n\n- [#1455](https://github.com/browserbase/stagehand/pull/1455) [`dfab1d5`](https://github.com/browserbase/stagehand/commit/dfab1d566299c8c5a63f20565a6da07dc8f61ccd) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - update aisdk client to better enforce structured output with deepseek models\n\n- [#1428](https://github.com/browserbase/stagehand/pull/1428) [`4d71162`](https://github.com/browserbase/stagehand/commit/4d71162beb119635b69b17637564a2bbd0e373e7) Thanks [@tkattkat](https://github.com/tkattkat)! - Add \"hybrid\" mode to stagehand agent\n\n## 3.0.6\n\n### Patch Changes\n\n- [#1388](https://github.com/browserbase/stagehand/pull/1388) [`605ed6b`](https://github.com/browserbase/stagehand/commit/605ed6b81a3ff8f25d4022f1e5fce6b42aecfc19) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix multiple click event dispatches on CDP and Anthropic CUA handling (double clicks)\n\n- [#1400](https://github.com/browserbase/stagehand/pull/1400) [`34e7e5b`](https://github.com/browserbase/stagehand/commit/34e7e5b292f5e6af6efc0da60118663310c5f718) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - don't write base64 encoded screenshots to disk when caching agent actions\n\n- [#1345](https://github.com/browserbase/stagehand/pull/1345) [`943d2d7`](https://github.com/browserbase/stagehand/commit/943d2d79d0f289ac41c9164578f2f1dd876058f2) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for aborting / stopping an agent run & continuing an agent run using messages from prior runs\n\n- [#1334](https://github.com/browserbase/stagehand/pull/1334) [`0e95cd2`](https://github.com/browserbase/stagehand/commit/0e95cd2f67672f64f0017024fd47d8b3aef59a95) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for google vertex provider\n\n- [#1410](https://github.com/browserbase/stagehand/pull/1410) [`d4237e4`](https://github.com/browserbase/stagehand/commit/d4237e40951ecd10abfdbe766672d498f8806484) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: include extract in stagehand.history()\n\n- [#1315](https://github.com/browserbase/stagehand/pull/1315) [`86975e7`](https://github.com/browserbase/stagehand/commit/86975e795db7505804949a267b20509bd16b5256) Thanks [@tkattkat](https://github.com/tkattkat)! - Add streaming support to agent through stream:true in the agent config\n\n- [#1304](https://github.com/browserbase/stagehand/pull/1304) [`d5e119b`](https://github.com/browserbase/stagehand/commit/d5e119be5eec84915a79f8d611b6ba0546f48c99) Thanks [@miguelg719](https://github.com/miguelg719)! - Add support for Microsoft's Fara-7B\n\n- [#1346](https://github.com/browserbase/stagehand/pull/1346) [`4e051b2`](https://github.com/browserbase/stagehand/commit/4e051b23add7ae276b0dbead38b4587838cfc1c1) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: don't attach to targets twice\n\n- [#1327](https://github.com/browserbase/stagehand/pull/1327) [`6b5a3c9`](https://github.com/browserbase/stagehand/commit/6b5a3c9035654caaed2da375085b465edda97de4) Thanks [@miguelg719](https://github.com/miguelg719)! - Informed error parsing from api\n\n- [#1335](https://github.com/browserbase/stagehand/pull/1335) [`bb85ad9`](https://github.com/browserbase/stagehand/commit/bb85ad912738623a7a866f0cb6e8d5807c6c2738) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support for page.addInitScript()\n\n- [#1331](https://github.com/browserbase/stagehand/pull/1331) [`88d28cc`](https://github.com/browserbase/stagehand/commit/88d28cc6f31058d1cf6ec6dc948a4ae77a926b3c) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: page.evaluate() now works with scripts injected via context.addInitScript()\n\n- [#1316](https://github.com/browserbase/stagehand/pull/1316) [`45bcef0`](https://github.com/browserbase/stagehand/commit/45bcef0e5788b083f9e38dfd7c3bc63afcd4b6dd) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for callbacks in stagehand agent\n\n- [#1374](https://github.com/browserbase/stagehand/pull/1374) [`6aa9d45`](https://github.com/browserbase/stagehand/commit/6aa9d455aa5836ec2ee8ab2e8b9df3fb218e5381) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix key action mapping in Anthropic CUA\n\n- [#1330](https://github.com/browserbase/stagehand/pull/1330) [`d382084`](https://github.com/browserbase/stagehand/commit/d382084745fff98c3e71413371466394a2625429) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: make act, extract, and observe respect user defined timeout param\n\n- [#1336](https://github.com/browserbase/stagehand/pull/1336) [`1df08cc`](https://github.com/browserbase/stagehand/commit/1df08ccb0a2cf73b5c37a91c129721114ff6371c) Thanks [@tkattkat](https://github.com/tkattkat)! - Patch agent on api\n\n- [#1358](https://github.com/browserbase/stagehand/pull/1358) [`2b56600`](https://github.com/browserbase/stagehand/commit/2b566009606fcbba987260f21b075b318690ce99) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for 4.5 opus in cua agent\n\n## 3.0.4\n\n### Patch Changes\n\n- [#1281](https://github.com/browserbase/stagehand/pull/1281) [`fa18cfd`](https://github.com/browserbase/stagehand/commit/fa18cfdc45f28e35e6566587b54612396e6ece45) Thanks [@monadoid](https://github.com/monadoid)! - Add Browserbase session URL and debug URL accessors\n\n- [#1264](https://github.com/browserbase/stagehand/pull/1264) [`767d168`](https://github.com/browserbase/stagehand/commit/767d1686285cf9c57675595f553f8a891f13c63b) Thanks [@Kylejeong2](https://github.com/Kylejeong2)! - feat: adding gpt 5.1 to stagehand\n\n- [#1282](https://github.com/browserbase/stagehand/pull/1282) [`f27a99c`](https://github.com/browserbase/stagehand/commit/f27a99c11b020b33736fe67af8f7f0e663c6f45f) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for zod 4, while maintaining backwards compatibility for zod 3\n\n- [#1295](https://github.com/browserbase/stagehand/pull/1295) [`91a1ca0`](https://github.com/browserbase/stagehand/commit/91a1ca07d9178c46269bfb951abb20a215eb7c29) Thanks [@tkattkat](https://github.com/tkattkat)! - Patch zod handling of non objects in extract\n\n- [#1298](https://github.com/browserbase/stagehand/pull/1298) [`1dd7d43`](https://github.com/browserbase/stagehand/commit/1dd7d4330de9022dc6cd45a8b5c86cb9e1b575ec) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - log Browserbase session status when websocket is closed due to session timeout\n\n- [#1284](https://github.com/browserbase/stagehand/pull/1284) [`c0f3b98`](https://github.com/browserbase/stagehand/commit/c0f3b98277c15c77b2b4c3f55503e61ef3d27cf3) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: waitForDomNetworkQuiet() causing `act()` to hang indefinitely\n\n- [#1246](https://github.com/browserbase/stagehand/pull/1246) [`44bb4f5`](https://github.com/browserbase/stagehand/commit/44bb4f51dcccbdca8df07e4d7f8d28a7e6e793ec) Thanks [@filip-michalsky](https://github.com/filip-michalsky)! - make ci faster\n\n- [#1300](https://github.com/browserbase/stagehand/pull/1300) [`2b70347`](https://github.com/browserbase/stagehand/commit/2b7034771bc6d6b1fabb13deaa56c299881b3728) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support for context.addInitScript()\n\n## 3.0.3\n\n### Patch Changes\n\n- [#1273](https://github.com/browserbase/stagehand/pull/1273) [`ab51232`](https://github.com/browserbase/stagehand/commit/ab51232db428be048957c0f5d67f2176eb7a5194) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: trigger shadow root rerender in OOPIFs by cloning & replacing instead of reloading\n\n- [#1268](https://github.com/browserbase/stagehand/pull/1268) [`c76ade0`](https://github.com/browserbase/stagehand/commit/c76ade009ef81208accae6475ec4707d3906e566) Thanks [@tkattkat](https://github.com/tkattkat)! - Expose reasoning, and cached input tokens in stagehand metrics\n\n- [#1267](https://github.com/browserbase/stagehand/pull/1267) [`ffb5e5d`](https://github.com/browserbase/stagehand/commit/ffb5e5d2ab49adcb2efdfc9e5c76e8c96268b5b3) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: file uploads failing on Browserbase\n\n- [#1269](https://github.com/browserbase/stagehand/pull/1269) [`772e735`](https://github.com/browserbase/stagehand/commit/772e73543e45106d7fa0fafd95ade46ae11023bc) Thanks [@tkattkat](https://github.com/tkattkat)! - Add example using playwright screen recording\n\n## 3.0.2\n\n### Patch Changes\n\n- [#1245](https://github.com/browserbase/stagehand/pull/1245) [`a224b33`](https://github.com/browserbase/stagehand/commit/a224b3371b6c1470baf342742fb745c7192b52c6) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - allow act() to call hover()\n\n- [#1234](https://github.com/browserbase/stagehand/pull/1234) [`6fc9de2`](https://github.com/browserbase/stagehand/commit/6fc9de2a1079e4f2fb0b1633d8df0bb7a9f7f89f) Thanks [@miguelg719](https://github.com/miguelg719)! - Add a page.sendCDP method\n\n- [#1233](https://github.com/browserbase/stagehand/pull/1233) [`4935be7`](https://github.com/browserbase/stagehand/commit/4935be788b3431527f3d110864c0fd7060cfaf7c) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - extend page.screenshot() options to mirror playwright\n\n- [#1232](https://github.com/browserbase/stagehand/pull/1232) [`bdd76fc`](https://github.com/browserbase/stagehand/commit/bdd76fcd1e48079fc5ab8cf040ebb5997dfc6c99) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - export Page type\n\n- [#1229](https://github.com/browserbase/stagehand/pull/1229) [`7ea18a4`](https://github.com/browserbase/stagehand/commit/7ea18a420fc033d1b72556db83a1f41735e5a022) Thanks [@tkattkat](https://github.com/tkattkat)! - Adjust extract tool + expose extract response in agent result\n\n- [#1239](https://github.com/browserbase/stagehand/pull/1239) [`d4de014`](https://github.com/browserbase/stagehand/commit/d4de014235a18f9e1089240bc72e28cbfe77ca1c) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix stagehand.metrics on api mode\n\n- [#1241](https://github.com/browserbase/stagehand/pull/1241) [`2d1b573`](https://github.com/browserbase/stagehand/commit/2d1b5732dc441a3331f5743cdfed3e1037d8b3b5) Thanks [@miguelg719](https://github.com/miguelg719)! - Return response on page.goto api mode\n\n- [#1253](https://github.com/browserbase/stagehand/pull/1253) [`5556041`](https://github.com/browserbase/stagehand/commit/5556041e2deaed5012363303fd7a8ac00e3242cd) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix missing page issue when connecting to existing browser\n\n- [#1235](https://github.com/browserbase/stagehand/pull/1235) [`7e4b43e`](https://github.com/browserbase/stagehand/commit/7e4b43ed46fbdd2074827e87d9a245e2dc96456b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - make page.goto() return a Response object\n\n- [#1254](https://github.com/browserbase/stagehand/pull/1254) [`7e72adf`](https://github.com/browserbase/stagehand/commit/7e72adfd7e4af5ec49ac2f552e7f1f57c1acc554) Thanks [@sameelarif](https://github.com/sameelarif)! - Added custom error types to allow for a smoother debugging experience.\n\n- [#1227](https://github.com/browserbase/stagehand/pull/1227) [`9bf09d0`](https://github.com/browserbase/stagehand/commit/9bf09d041111870d71cb9ffcb3ac5fa2c4b1399d) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix readme's media links and add instructions for installing from a branch\n\n- [#1257](https://github.com/browserbase/stagehand/pull/1257) [`92d32ea`](https://github.com/browserbase/stagehand/commit/92d32eafe91a4241615cc65501b8461c6074a02b) Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for a custom baseUrl with google cua client\n\n- [#1230](https://github.com/browserbase/stagehand/pull/1230) [`ebcf3a1`](https://github.com/browserbase/stagehand/commit/ebcf3a1ffa859374d71de4931c6a9b982a565e46) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add stagehand.browserbaseSessionID getter\n\n- [#1262](https://github.com/browserbase/stagehand/pull/1262) [`c29a4f2`](https://github.com/browserbase/stagehand/commit/c29a4f2eca91ae2902ed9d48b2385b4436f7b664) Thanks [@miguelg719](https://github.com/miguelg719)! - Remove error throwing when api and experimental are both set\n\n- [#1223](https://github.com/browserbase/stagehand/pull/1223) [`6d21efa`](https://github.com/browserbase/stagehand/commit/6d21efa8b30317aa3ce3e37ac6c2222af3b967b5) Thanks [@miguelg719](https://github.com/miguelg719)! - Disable api mode when using custom LLM clients\n\n- [#1228](https://github.com/browserbase/stagehand/pull/1228) [`525ef0c`](https://github.com/browserbase/stagehand/commit/525ef0c1243aaf3452ee7e4ea81b4208f4c2efd1) Thanks [@Kylejeong2](https://github.com/Kylejeong2)! - update slack link in docs\n\n- [#1226](https://github.com/browserbase/stagehand/pull/1226) [`9ddb872`](https://github.com/browserbase/stagehand/commit/9ddb872e350358214e12a91cf6a614fd2ec1f74c) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support for page.on('console') events\n\n## 3.0.1\n\n### Patch Changes\n\n- [#1207](https://github.com/browserbase/stagehand/pull/1207) [`55da8c6`](https://github.com/browserbase/stagehand/commit/55da8c6e9575cbad3246c55b17650cf6b293ddbe) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix broken links to quickstart docs\n\n- [#1200](https://github.com/browserbase/stagehand/pull/1200) [`0a5ee63`](https://github.com/browserbase/stagehand/commit/0a5ee638bde051d109eb2266e665934a12f3dc31) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - log info when scope narrowing selector fails\n\n- [#1205](https://github.com/browserbase/stagehand/pull/1205) [`ee76881`](https://github.com/browserbase/stagehand/commit/ee7688156cb67a9f0f90dfe0dbab77423693a332) Thanks [@miguelg719](https://github.com/miguelg719)! - Update README.md, add Changelog for v3\n\n- [#1209](https://github.com/browserbase/stagehand/pull/1209) [`9e95add`](https://github.com/browserbase/stagehand/commit/9e95add37eb30db4f85e73df7760c7e63fb4131e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix circular import in exported aisdk example client\n\n- [#1211](https://github.com/browserbase/stagehand/pull/1211) [`98e212b`](https://github.com/browserbase/stagehand/commit/98e212b27887241879608c6c1b6c2524477a40d7) Thanks [@miguelg719](https://github.com/miguelg719)! - Add an example for passing custom tools to agent\n\n- [#1206](https://github.com/browserbase/stagehand/pull/1206) [`d5ecbfc`](https://github.com/browserbase/stagehand/commit/d5ecbfc8e419a59b91c2115fd7f984378381d3d0) Thanks [@miguelg719](https://github.com/miguelg719)! - Export example AISdkClient properly from the stagehand package\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "<div id=\"toc\" align=\"center\" style=\"margin-bottom: 0;\">\n  <ul style=\"list-style: none; margin: 0; padding: 0;\">\n    <a href=\"https://stagehand.dev\">\n      <picture>\n        <source media=\"(prefers-color-scheme: dark)\" srcset=\"../../media/dark_logo.png\" />\n        <img alt=\"Stagehand\" src=\"../../media/light_logo.png\" width=\"200\" style=\"margin-right: 30px;\" />\n      </picture>\n    </a>\n  </ul>\n</div>\n<p align=\"center\">\n  <strong>The AI Browser Automation Framework</strong><br>\n  <a href=\"https://docs.stagehand.dev\">Read the Docs</a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/browserbase/stagehand/tree/main?tab=MIT-1-ov-file#MIT-1-ov-file\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"../../media/dark_license.svg\" />\n      <img alt=\"MIT License\" src=\"../../media/light_license.svg\" />\n    </picture>\n  </a>\n  <a href=\"https://stagehand.dev/discord\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"../../media/dark_discord.svg\" />\n      <img alt=\"Discord Community\" src=\"../../media/light_discord.svg\" />\n    </picture>\n  </a>\n</p>\n\n<p align=\"center\">\n\t<a href=\"https://trendshift.io/repositories/12122\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/12122\" alt=\"browserbase%2Fstagehand | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://deepwiki.com/browserbase/stagehand\">\n    <img alt=\"Ask DeepWiki\" src=\"https://deepwiki.com/badge.svg\" />\n  </a>\n</p>\n\n<p align=\"center\">\nIf you're looking for the Python implementation, you can find it \n<a href=\"https://github.com/browserbase/stagehand-python\"> here</a>\n</p>\n\n<div align=\"center\" style=\"display: flex; align-items: center; justify-content: center; gap: 4px; margin-bottom: 0;\">\n  <b>Vibe code</b>\n  <span style=\"font-size: 1.05em;\"> Stagehand with </span>\n  <a href=\"https://director.ai\" style=\"display: flex; align-items: center;\">\n    <span>Director</span>\n  </a>\n  <span> </span>\n  <picture>\n    <img alt=\"Director\" src=\"../../media/director_icon.svg\" width=\"25\" />\n  </picture>\n</div>\n\n## What is Stagehand?\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## Why Stagehand?\n\nMost existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.\n\n1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.\n\n2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.\n\n3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks.\n\n## Getting Started\n\nStart with Stagehand with one line of code, or check out our [Quickstart Guide](https://docs.stagehand.dev/v3/first-steps/quickstart) for more information:\n\n```bash\nnpx create-browser-app\n```\n\n## Example\n\nHere's how to build a sample browser automation with Stagehand:\n\n```typescript\n// Stagehand's CDP engine provides an optimized, low level interface to the browser built for automation\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://github.com/browserbase\");\n\n// Use act() to execute individual actions\nawait stagehand.act(\"click on the stagehand repo\");\n\n// Use agent() for multi-step tasks\nconst agent = stagehand.agent();\nawait agent.execute(\"Get to the latest PR\");\n\n// Use extract() to get structured data from the page\nconst { author, title } = await stagehand.extract(\n  \"extract the author and title of the PR\",\n  z.object({\n    author: z.string().describe(\"The username of the PR author\"),\n    title: z.string().describe(\"The title of the PR\"),\n  }),\n);\n```\n\n## Documentation\n\nVisit [docs.stagehand.dev](https://docs.stagehand.dev) to view the full documentation.\n\n### Build and Run from Source\n\n```bash\ngit clone https://github.com/browserbase/stagehand.git\ncd stagehand\npnpm install\npnpm run build\npnpm run example # run the blank script at ./examples/example.ts\n```\n\nStagehand is best when you have an API key for an LLM provider and Browserbase credentials. To add these to your project, run:\n\n```bash\ncp .env.example .env\nnano .env # Edit the .env file to add API keys\n```\n\n### Installing from a branch\n\nYou can install and build Stagehand directly from a github branch using [gitpkg](https://github.com/EqualMa/gitpkg)\n\nIn your project's `package.json` set:\n\n```json\n\"@browserbasehq/stagehand\": \"https://gitpkg.now.sh/browserbase/stagehand/packages/core?<branchName>\",\n```\n\n## Contributing\n\n> [!NOTE]\n> We highly value contributions to Stagehand! For questions or support, please join our [Discord community](https://stagehand.dev/discord).\n\nAt a high level, we're focused on improving reliability, extensibility, speed, and cost in that order of priority. If you're interested in contributing, **bug fixes and small improvements are the best way to get started**. For more involved features, we strongly recommend reaching out to [Miguel Gonzalez](https://x.com/miguel_gonzf) or [Paul Klein](https://x.com/pk_iv) in our [Discord community](https://stagehand.dev/discord) before starting to ensure that your contribution aligns with our goals.\n\n<!-- For more information, please see our [Contributing Guide](https://docs.stagehand.dev/examples/contributing). -->\n\n## Acknowledgements\n\nWe'd like to thank the following people for their major contributions to Stagehand:\n\n- [Paul Klein](https://github.com/pkiv)\n- [Sean McGuire](https://github.com/seanmcguire12)\n- [Miguel Gonzalez](https://github.com/miguelg719)\n- [Sameel Arif](https://github.com/sameelarif)\n- [Thomas Katwan](https://github.com/tkattkat)\n- [Filip Michalsky](https://github.com/filip-michalsky)\n- [Anirudh Kamath](https://github.com/kamath)\n- [Jeremy Press](https://x.com/jeremypress)\n- [Navid Pour](https://github.com/navidpour)\n\n## License\n\nLicensed under the MIT License.\n\nCopyright 2025 Browserbase, Inc.\n"
  },
  {
    "path": "packages/core/examples/2048.ts",
    "content": "import { Stagehand } from \"../lib/v3/index.js\";\nimport { z } from \"zod\";\n\nasync function example() {\n  console.log(\"🎮 Starting 2048 bot...\");\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 1,\n  });\n\n  console.log(\"🌟 Initializing Stagehand...\");\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n  try {\n    console.log(\"🌐 Navigating to 2048...\");\n    await page.goto(\"https://ovolve.github.io/2048-AI/\");\n    // Main game loop\n    while (true) {\n      console.log(\"🔄 Game loop iteration...\");\n      // Add a small delay for UI updates\n      await new Promise((resolve) => setTimeout(resolve, 300));\n      // Get current game state\n      const gameState = await stagehand.extract(\n        `Extract the current game state:\n          1. Score from the score counter\n          2. All tile values in the 4x4 grid (empty spaces as 0)\n          3. Highest tile value present`,\n        z.object({\n          score: z.number(),\n          highestTile: z.number(),\n          grid: z.array(z.array(z.number())),\n        }),\n      );\n      const transposedGrid = gameState.grid[0].map((_, colIndex) =>\n        gameState.grid.map((row) => row[colIndex]),\n      );\n      const grid = transposedGrid.map((row, rowIndex) => ({\n        [`row${rowIndex + 1}`]: row,\n      }));\n      console.log(\"Game State:\", {\n        score: gameState.score,\n        highestTile: gameState.highestTile,\n        grid: grid,\n      });\n      // Analyze board and decide next move\n      const analysis = await stagehand.extract(\n        `Based on the current game state:\n          - Score: ${gameState.score}\n          - Highest tile: ${gameState.highestTile}\n          - Grid: This is a 4x4 matrix ordered by row (top to bottom) and column (left to right). The rows are stacked vertically, and tiles can move vertically between rows or horizontally between columns:\\n${grid\n            .map((row) => {\n              const rowName = Object.keys(row)[0];\n              return `             ${rowName}: ${row[rowName].join(\", \")}`;\n            })\n            .join(\"\\n\")}\n          What is the best move (up/down/left/right)? Consider:\n          1. Keeping high value tiles in corners (bottom left, bottom right, top left, top right)\n          2. Maintaining a clear path to merge tiles\n          3. Avoiding moves that could block merges\n          4. Only adjacent tiles of the same value can merge\n          5. Making a move will move all tiles in that direction until they hit a tile of a different value or the edge of the board\n          6. Tiles cannot move past the edge of the board\n          7. Each move must move at least one tile`,\n        z.object({\n          move: z.enum([\"up\", \"down\", \"left\", \"right\"]),\n          confidence: z.number(),\n          reasoning: z.string(),\n        }),\n      );\n      console.log(\"Move Analysis:\", analysis);\n      const moveKey = {\n        up: \"ArrowUp\",\n        down: \"ArrowDown\",\n        left: \"ArrowLeft\",\n        right: \"ArrowRight\",\n      }[analysis.move];\n      await page.keyPress(moveKey);\n      console.log(\"🎯 Executed move:\", analysis.move);\n    }\n  } catch (error) {\n    console.error(\"❌ Error in game loop:\", error);\n    const isGameOver = await page.evaluate(() => {\n      return document.querySelector(\".game-over\") !== null;\n    });\n    if (isGameOver) {\n      console.log(\"🏁 Game Over!\");\n      return;\n    }\n    throw error; // Re-throw non-game-over errors\n  }\n}\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/CHANGELOG.md",
    "content": "# @browserbasehq/stagehand-examples\n\n## 1.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`09b5e1e`](https://github.com/browserbase/stagehand/commit/09b5e1e9c23c845903686db6665cc968ac34efbb), [`e3734b9`](https://github.com/browserbase/stagehand/commit/e3734b9c98352d5f0a4eca49791b0bbf2130ab41), [`8244ab2`](https://github.com/browserbase/stagehand/commit/8244ab247cd679962685ae2f7c54e874ce1fa614), [`be85b19`](https://github.com/browserbase/stagehand/commit/be85b19679a826f19702e00f0aae72fce1118ec8), [`88d1565`](https://github.com/browserbase/stagehand/commit/88d1565c65bb65a104fea2d5f5e862bbbda69677), [`ab5d6ed`](https://github.com/browserbase/stagehand/commit/ab5d6ede19aabc059badc4247f1cb2c6c9e71bae)]:\n  - @browserbasehq/stagehand@2.5.0\n\n## 1.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`9e8c173`](https://github.com/browserbase/stagehand/commit/9e8c17374fdc8fbe7f26e6cf802c36bd14f11039)]:\n  - @browserbasehq/stagehand@2.4.4\n\n## 1.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`f45afdc`](https://github.com/browserbase/stagehand/commit/f45afdccc8680650755fee66ffbeac32b41e075d), [`261bba4`](https://github.com/browserbase/stagehand/commit/261bba43fa79ac3af95328e673ef3e9fced3279b), [`8de7bd8`](https://github.com/browserbase/stagehand/commit/8de7bd8635c2051cd8025e365c6c8aa83d81c7e7), [`3d80421`](https://github.com/browserbase/stagehand/commit/3d804210a106a6828c7fa50f8b765b10afd4cc6a), [`0ead63d`](https://github.com/browserbase/stagehand/commit/0ead63d6526f6c286362b74b6407c8bebc900e69), [`8422828`](https://github.com/browserbase/stagehand/commit/8422828c4cd5fd5ebcf348cfbdb40c768bb76dd9), [`b769206`](https://github.com/browserbase/stagehand/commit/b7692060f98a2f49aeeefb90d8789ed034b08ec2), [`72d2683`](https://github.com/browserbase/stagehand/commit/72d2683202af7e578d98367893964b33e0828de5)]:\n  - @browserbasehq/stagehand@2.4.3\n\n## 1.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`6b4e6e3`](https://github.com/browserbase/stagehand/commit/6b4e6e3f31d5496cf15728e9018eddeb04839542), [`e77d018`](https://github.com/browserbase/stagehand/commit/e77d0188683ebf596dfb78dfafbbca1dc32993f0), [`c20adb9`](https://github.com/browserbase/stagehand/commit/c20adb95539fed8c56a4aa413262a9c65a8e6474), [`b86df93`](https://github.com/browserbase/stagehand/commit/b86df93b9136aae96292121a29c25f3d74d84bf7), [`023c2c2`](https://github.com/browserbase/stagehand/commit/023c2c273b46d3792d7e5d3c902089487b16b531), [`8c28647`](https://github.com/browserbase/stagehand/commit/8c2864755ecd05c8f7de235d4198deec0dd5f78e), [`87e09c6`](https://github.com/browserbase/stagehand/commit/87e09c618940f364ec8af00455a19a17ec63cbd3), [`a611115`](https://github.com/browserbase/stagehand/commit/a61111525d70b450bdfc43f112380f44899c9e97), [`69913fe`](https://github.com/browserbase/stagehand/commit/69913fe1dfb8201ae2aeffa5f049fb46ab02cbc2), [`b1b83a1`](https://github.com/browserbase/stagehand/commit/b1b83a1d334fe76e5f5f9dd32dc92c16b7d40ce6), [`be8497c`](https://github.com/browserbase/stagehand/commit/be8497cb6b142cc893cea9692b8c47bd19514c60), [`98704c9`](https://github.com/browserbase/stagehand/commit/98704c9ed225ca25bbde4bb3dc286936e9c54471), [`04978bd`](https://github.com/browserbase/stagehand/commit/04978bdd30d2edcbc69eb9fd91358a16975ea2eb)]:\n  - @browserbasehq/stagehand@2.4.2\n\n## 1.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`8a43c5a`](https://github.com/browserbase/stagehand/commit/8a43c5a86d4da40cfaedd9cf2e42186928bdf946), [`890ffcc`](https://github.com/browserbase/stagehand/commit/890ffccac5e0a60ade64a46eb550c981ffb3e84a), [`64c1072`](https://github.com/browserbase/stagehand/commit/64c10727bda50470483a3eb175c02842db0923a1), [`b077d3f`](https://github.com/browserbase/stagehand/commit/b077d3f48a97f47a71ccc79ae39b41e7f07f9c04), [`8bcb5d7`](https://github.com/browserbase/stagehand/commit/8bcb5d77debf6bf7601fd5c090efd7fde75c5d5e), [`7bf10c5`](https://github.com/browserbase/stagehand/commit/7bf10c55b267078fe847c1d7f7a60d604f9c7c94)]:\n  - @browserbasehq/stagehand@2.4.1\n\n## 1.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`124e0d3`](https://github.com/browserbase/stagehand/commit/124e0d3bb54ddb6738ede6d7aa99a945ef1cacd1), [`6a18c1e`](https://github.com/browserbase/stagehand/commit/6a18c1ee1e46d55c6e90c4d5572e17ed8daa140c), [`1660751`](https://github.com/browserbase/stagehand/commit/1660751cd14cb5b27d44f8167216afb8d1c3c45c), [`cadac9d`](https://github.com/browserbase/stagehand/commit/cadac9da09123d12e5d496a0e8b12660964c1b33), [`759da55`](https://github.com/browserbase/stagehand/commit/759da55775eb2df81d56ae18c0f386fd9b02a9f0), [`a175a51`](https://github.com/browserbase/stagehand/commit/a175a519b8c14300db6f1ed30709e113d18e99db), [`8527a80`](https://github.com/browserbase/stagehand/commit/8527a80522c3eedb9516a6caa1a0e4e4be981a3d), [`55fca2f`](https://github.com/browserbase/stagehand/commit/55fca2f7da63cc0ef6e27b45a33f63c666cdce7e)]:\n  - @browserbasehq/stagehand@2.4.0\n\n## 1.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`12a99b3`](https://github.com/browserbase/stagehand/commit/12a99b398d8a4c3eea3ca69a3cf793faaaf4aea3), [`2451797`](https://github.com/browserbase/stagehand/commit/2451797f64c0efa4a72fd70265110003c8d0a6cd), [`1d631a5`](https://github.com/browserbase/stagehand/commit/1d631a57a197390f672b718ae5199991ab27cfb1), [`9c398bb`](https://github.com/browserbase/stagehand/commit/9c398bb9ec2d10bdb53ad5aa7e3b58cce24fdb2b), [`c19ad7f`](https://github.com/browserbase/stagehand/commit/c19ad7f1e082e91fdeaa9c2ef63767a5a2b3a195)]:\n  - @browserbasehq/stagehand@2.3.1\n\n## 1.0.2\n\n### Patch Changes\n\n- Updated dependencies [[`5680d25`](https://github.com/browserbase/stagehand/commit/5680d2509352c383ad502c9f4fabde01fa638833), [`4de92a8`](https://github.com/browserbase/stagehand/commit/4de92a8af461fc95063faf39feee1d49259f58ba), [`6ef6073`](https://github.com/browserbase/stagehand/commit/6ef60730cab0ad9025f44b6eeb2c83751d1dcd35)]:\n  - @browserbasehq/stagehand@2.3.0\n\n## 1.0.1\n\n### Patch Changes\n\n- Updated dependencies [[`be8652e`](https://github.com/browserbase/stagehand/commit/be8652e770b57fdb3299fa0b2efa4eb0e816434e), [`6b413b7`](https://github.com/browserbase/stagehand/commit/6b413b7ad00b13ca0bd53ee2e7393023821408b6), [`7eafbd9`](https://github.com/browserbase/stagehand/commit/7eafbd9b1a73b37effa444929767df7c592caf02), [`1b50aa6`](https://github.com/browserbase/stagehand/commit/1b50aa61cf0a429dd6cb2760a08f7f698a50454b), [`f2b7f1f`](https://github.com/browserbase/stagehand/commit/f2b7f1f284eef1f96753319b66c7d0b273a6f8cd), [`c8d672f`](https://github.com/browserbase/stagehand/commit/c8d672f7c410c256defbc2e87ead99239837aa28), [`bebf204`](https://github.com/browserbase/stagehand/commit/bebf2044502333c694743078c5b0c9deae11fb79), [`37d6810`](https://github.com/browserbase/stagehand/commit/37d6810a704773d0383a86f98f5f17c7d5b21975)]:\n  - @browserbasehq/stagehand@2.2.1\n"
  },
  {
    "path": "packages/core/examples/actionable_observe_example.ts",
    "content": "/**\n * This example shows how to use actionable observe()\n *\n * You can use observe to get a cache-able Playwright action as JSON, then pass that JSON to act() to perform the action.\n *\n * This is useful for:\n * - Previewing actions before running them\n * - Saving actions to a file and replaying them later\n * - Hiding sensitive information from LLMs\n *\n * For more on caching, see: https://docs.stagehand.dev/examples/caching\n * Also check out the form_filling_sensible.ts example for a more complex example of using observe() to fill out a form.\n */\n\nimport { Action, Stagehand } from \"../lib/v3/index.js\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n  });\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n\n  await page.goto(\"https://www.apartments.com/san-francisco-ca/\");\n\n  let observation: Action;\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n  [observation] = await stagehand.observe(\"find the 'all filters' button\");\n  await stagehand.act(observation);\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n  [observation] = await stagehand.observe(\n    \"find the '1+' button in the 'beds' section\",\n  );\n  await stagehand.act(observation);\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n  [observation] = await stagehand.observe(\n    \"find the 'apartments' button in the 'home type' section\",\n  );\n  await stagehand.act(observation);\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n  [observation] = await stagehand.observe(\n    \"find the pet policy dropdown to click on.\",\n  );\n  await stagehand.act(observation);\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n  [observation] = await stagehand.observe(\n    \"find the 'Dog Friendly' option to click on\",\n  );\n  await stagehand.act(observation);\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n  [observation] = await stagehand.observe(\"find the 'see results' section\");\n  await stagehand.act(observation);\n\n  const currentUrl = page.url();\n  await stagehand.close();\n  if (\n    currentUrl.includes(\n      \"https://www.apartments.com/apartments/san-francisco-ca/min-1-bedrooms-pet-friendly-dog/\",\n    )\n  ) {\n    console.log(\"✅ Success! we made it to the correct page\");\n  } else {\n    console.log(\n      \"❌ Whoops, looks like we didn't make it to the correct page. \" +\n        \"\\nThanks for testing out this new Stagehand feature!\" +\n        \"\\nReach us on Discord if you have any feedback/questions/suggestions!\",\n    );\n  }\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/agent-custom-tools.ts",
    "content": "/**\n * This example shows how to pass custom tools to stagehand agent (both CUA and non-CUA)\n */\nimport { z } from \"zod\";\nimport { tool } from \"ai\";\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport chalk from \"chalk\";\n\n// Mock weather API, replace with your own API/tool logic\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst fetchWeatherAPI = async (location: string) => {\n  return {\n    temp: 70,\n    conditions: \"sunny\",\n  };\n};\n\n// Define the tool in an AI SDK format\nconst getWeather = tool({\n  description: \"Get the current weather in a location\",\n  inputSchema: z.object({\n    location: z.string().describe(\"The location to get weather for\"),\n  }),\n  execute: async ({ location }) => {\n    // Your custom logic here\n    const weather = await fetchWeatherAPI(location);\n    return {\n      location,\n      temperature: weather.temp,\n      conditions: weather.conditions,\n    };\n  },\n});\n\nasync function main() {\n  console.log(\n    `\\n${chalk.bold(\"Stagehand 🤘 Computer Use Agent (CUA) Demo\")}\\n`,\n  );\n\n  // Initialize Stagehand\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 2,\n    experimental: true, // You must enable experimental mode to use custom tools / MCP integrations\n    model: \"anthropic/claude-sonnet-4-5\",\n  });\n  await stagehand.init();\n\n  try {\n    const page = stagehand.context.pages()[0];\n\n    // Create a computer use agent\n    const agent = stagehand.agent({\n      mode: \"cua\",\n      model: {\n        modelName: \"anthropic/claude-sonnet-4-5-20250929\",\n        apiKey: process.env.ANTHROPIC_API_KEY,\n      },\n      systemPrompt: `You are a helpful assistant that can use a web browser.\n      You are currently on the following page: ${page.url()}.\n      Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,\n      tools: {\n        getWeather, // Pass the tools to the agent\n      },\n    });\n\n    // const agent = stagehand.agent({\n    //   systemPrompt: `You are a helpful assistant that can use a web browser.\n    //   You are currently on the following page: ${page.url()}.\n    //   Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,\n    //   // Pass the tools to the agent\n    //   tools: {\n    //     getWeather: getWeather,\n    //   },\n    // });\n\n    // Navigate to the Browserbase careers page\n    await page.goto(\"https://www.google.com\");\n\n    // Define the instruction for the CUA\n    const instruction = \"What's the weather in San Francisco?\";\n    console.log(`Instruction: ${chalk.white(instruction)}`);\n\n    // Execute the instruction\n    const result = await agent.execute({\n      instruction,\n      maxSteps: 20,\n    });\n\n    console.log(`${chalk.green(\"✓\")} Execution complete`);\n    console.log(`${chalk.yellow(\"⤷\")} Result:`);\n    console.log(chalk.white(JSON.stringify(result, null, 2)));\n  } catch (error) {\n    console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n    if (error instanceof Error && error.stack) {\n      console.log(chalk.dim(error.stack.split(\"\\n\").slice(1).join(\"\\n\")));\n    }\n  } finally {\n    // Close the browser\n    await stagehand.close();\n  }\n}\n\nmain().catch((error) => {\n  console.log(`${chalk.red(\"✗\")} Unhandled error in main function`);\n  console.log(chalk.red(error));\n});\n"
  },
  {
    "path": "packages/core/examples/agent_stream_example.ts",
    "content": "import { Stagehand } from \"../lib/v3/index.js\";\nimport chalk from \"chalk\";\n\n// Load environment variables\nasync function main() {\n  console.log(`\\n${chalk.bold(\"Stagehand 🤘 Agent Streaming Example\")}\\n`);\n  // Initialize Stagehand\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    cacheDir: \"stagehand-agent-cache\",\n    logInferenceToFile: false,\n    experimental: true,\n  });\n\n  await stagehand.init();\n\n  try {\n    const page = stagehand.context.pages()[0];\n    await page.goto(\"https://amazon.com\");\n\n    // Create a streaming agent with stream: true in the config\n    const agent = stagehand.agent({\n      model: \"anthropic/claude-sonnet-4-5-20250929\",\n      stream: true, // This makes execute() return AgentStreamResult\n    });\n\n    const agentRun = await agent.execute({\n      instruction: \"go to amazon, and search for shampoo, stop after searching\",\n      maxSteps: 20,\n    });\n    // stream the text\n    for await (const delta of agentRun.textStream) {\n      process.stdout.write(delta);\n    }\n    // stream everything ( toolcalls, messages, etc.)\n    // for await (const delta of result.fullStream) {\n    //   console.log(delta);\n    // }\n\n    const finalResult = await agentRun.result;\n    console.log(\"Final Result:\", finalResult);\n  } catch (error) {\n    console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n  }\n}\nmain();\n"
  },
  {
    "path": "packages/core/examples/cua-example.ts",
    "content": "/**\n * This example shows how to use a computer use agent (CUA) to navigate a web page and extract data.\n *\n * To learn more about the CUA, see: https://docs.stagehand.dev/examples/computer_use\n *\n * NOTE: YOU MUST CONFIGURE BROWSER DIMENSIONS TO USE COMPUTER USE!\n * Check out stagehand.config.ts for more information.\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport chalk from \"chalk\";\n\nasync function main() {\n  console.log(\n    `\\n${chalk.bold(\"Stagehand 🤘 Computer Use Agent (CUA) Demo\")}\\n`,\n  );\n\n  // Initialize Stagehand\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 2,\n  });\n  await stagehand.init();\n\n  try {\n    const page = stagehand.context.pages()[0];\n\n    // Create a computer use agent\n    const agent = stagehand.agent({\n      mode: \"cua\",\n      model: {\n        modelName: \"google/gemini-3-flash-preview\",\n        apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,\n      },\n      systemPrompt: `You are a helpful assistant that can use a web browser.\n      You are currently on the following page: ${page.url()}.\n      Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,\n    });\n\n    // Navigate to the Browserbase careers page\n    await page.goto(\"https://www.browserbase.com/careers\");\n\n    // Define the instruction for the CUA\n    const instruction =\n      \"Apply for the first engineer position with mock data. Don't submit the form. You're on the right page\";\n    console.log(`Instruction: ${chalk.white(instruction)}`);\n\n    // Execute the instruction\n    const result = await agent.execute({\n      instruction,\n      maxSteps: 20,\n    });\n    await new Promise((resolve) => setTimeout(resolve, 30000));\n\n    console.log(`${chalk.green(\"✓\")} Execution complete`);\n    console.log(`${chalk.yellow(\"⤷\")} Result:`);\n    console.log(chalk.white(JSON.stringify(result, null, 2)));\n  } catch (error) {\n    console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n    if (error instanceof Error && error.stack) {\n      console.log(chalk.dim(error.stack.split(\"\\n\").slice(1).join(\"\\n\")));\n    }\n  } finally {\n    // Close the browser\n    await stagehand.close();\n  }\n}\n\nmain().catch((error) => {\n  console.log(`${chalk.red(\"✗\")} Unhandled error in main function`);\n  console.log(chalk.red(error));\n});\n"
  },
  {
    "path": "packages/core/examples/custom_client_aisdk.ts",
    "content": "/**\n * This example shows how to use the Vercel AI SDK to power the Stagehand LLM Client.\n *\n * You will need to reference the AI SDK Client in /external_clients/aisdk.ts\n *\n * To learn more about the Vercel AI SDK, see: https://sdk.vercel.ai/docs\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport { AISdkClient } from \"./external_clients/aisdk.js\";\nimport { z } from \"zod\";\nimport { openai } from \"@ai-sdk/openai\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    llmClient: new AISdkClient({\n      model: openai(\"gpt-4o\"),\n    }),\n  });\n\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n\n  await page.goto(\"https://news.ycombinator.com\");\n\n  const { story } = await stagehand.extract(\n    \"extract the title of the top story on the page\",\n    z.object({\n      story: z.string().describe(\"the top story on the page\"),\n    }),\n  );\n\n  console.log(\"The top story is:\", story);\n  await stagehand.act(\"click the first story\");\n\n  await stagehand.close();\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/custom_client_langchain.ts",
    "content": "/**\n * This example shows how to use the Langchain client with Stagehand.\n *\n * You will need to reference the Langchain Client in /external_clients/langchain.ts\n */\nimport { z } from \"zod\";\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport { LangchainClient } from \"./external_clients/langchain.js\";\nimport { ChatOpenAI } from \"@langchain/openai\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    llmClient: new LangchainClient(\n      new ChatOpenAI({\n        model: \"gpt-4o\",\n      }),\n    ),\n  });\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://news.ycombinator.com\");\n  const { story } = await stagehand.extract(\n    \"extract the title of the top story on the page\",\n    z.object({\n      story: z.string().describe(\"the top story on the page\"),\n    }),\n  );\n  console.log(\"The top story is:\", story);\n  await stagehand.act(\"click the first story\");\n  await stagehand.close();\n}\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/custom_client_openai.ts",
    "content": "/**\n * This example shows how to use a custom OpenAI client with Stagehand.\n *\n * The OpenAI API provides a simple, type-safe, and composable way to build AI applications.\n *\n * You will need to reference the Custom OpenAI Client in /external_clients/customOpenAI.ts\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport { z } from \"zod\";\nimport { CustomOpenAIClient } from \"./external_clients/customOpenAI.js\";\nimport OpenAI from \"openai\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    llmClient: new CustomOpenAIClient({\n      modelName: \"gpt-4o-mini\",\n      client: new OpenAI({\n        apiKey: process.env.OPENAI_API_KEY,\n      }),\n    }),\n  });\n  await stagehand.init();\n\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://news.ycombinator.com\");\n  await stagehand.act(\"click on the 'new' link\");\n\n  const headlines = await stagehand.extract(\n    \"Extract the top 3 stories from the Hacker News homepage.\",\n    z.object({\n      stories: z.array(\n        z.object({\n          title: z.string(),\n          url: z.string(),\n          points: z.number(),\n        }),\n      ),\n    }),\n  );\n\n  console.log(headlines);\n\n  await stagehand.close();\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/example.ts",
    "content": "import { Stagehand } from \"../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  /**\n   * Add your code here!\n   */\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/\",\n  );\n\n  const { extraction } = await stagehand.extract(\n    \"grab the the first title from inside the iframe\",\n  );\n  console.log(extraction);\n\n  const page2 = await stagehand.context.newPage();\n  await page2.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-same-proc/\",\n  );\n  await stagehand.extract(\n    \"extract the placeholder text on the your name field\",\n    { page: page2 },\n  );\n  await stagehand.act(\"fill the your name field with the text 'John Doe'\", {\n    page: page2,\n  });\n  const action2 = await stagehand.observe(\n    \"select blue as the favorite color on the dropdown\",\n    { page: page2 },\n  );\n  for (const action of action2) {\n    await stagehand.act(action, { page: page2, timeout: 30_000 });\n  }\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    apiKey: process.env.BROWSERBASE_API_KEY,\n    projectId: process.env.BROWSERBASE_PROJECT_ID,\n    model: {\n      modelName: \"openai/gpt-5\",\n      apiKey: process.env.MODEL_API_KEY,\n    },\n    verbose: 2,\n  });\n  try {\n    await stagehand.init();\n    await example(stagehand);\n  } finally {\n    await stagehand.close();\n  }\n})();\n"
  },
  {
    "path": "packages/core/examples/external_clients/aisdk.ts",
    "content": "export { AISdkClient } from \"../../lib/v3/external_clients/aisdk.js\";\n"
  },
  {
    "path": "packages/core/examples/external_clients/customOpenAI.ts",
    "content": "export { CustomOpenAIClient } from \"../../lib/v3/external_clients/customOpenAI.js\";\n"
  },
  {
    "path": "packages/core/examples/external_clients/langchain.ts",
    "content": "import { BaseChatModel } from \"@langchain/core/language_models/chat_models\";\nimport {\n  CreateChatCompletionOptions,\n  LLMClient,\n  AvailableModel,\n} from \"../../lib/v3/index.js\";\nimport {\n  AIMessage,\n  BaseMessageLike,\n  HumanMessage,\n  SystemMessage,\n} from \"@langchain/core/messages\";\nimport { ChatCompletion } from \"openai/resources\";\nimport { toJsonSchema } from \"../../lib/v3/zodCompat.js\";\n\nexport class LangchainClient extends LLMClient {\n  public type = \"langchainClient\" as const;\n  private model: BaseChatModel;\n\n  constructor(model: BaseChatModel) {\n    super(model.name as AvailableModel);\n    this.model = model;\n  }\n\n  async createChatCompletion<T = ChatCompletion>({\n    options,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const formattedMessages: BaseMessageLike[] = options.messages.map(\n      (message) => {\n        if (Array.isArray(message.content)) {\n          if (message.role === \"system\") {\n            return new SystemMessage(\n              message.content\n                .map((c) => (\"text\" in c ? c.text : \"\"))\n                .join(\"\\n\"),\n            );\n          }\n\n          const content = message.content.map((content) =>\n            \"image_url\" in content\n              ? { type: \"image\", image: content.image_url.url }\n              : { type: \"text\", text: content.text },\n          );\n\n          if (message.role === \"user\") return new HumanMessage({ content });\n\n          const textOnlyParts = content.map((part) => ({\n            type: \"text\" as const,\n            text: part.type === \"image\" ? \"[Image]\" : part.text,\n          }));\n\n          return new AIMessage({ content: textOnlyParts });\n        }\n\n        return {\n          role: message.role,\n          content: message.content,\n        };\n      },\n    );\n\n    if (options.response_model) {\n      //ref string no longer needed, this is now default behavior\n      const responseSchema = toJsonSchema(options.response_model.schema);\n      const structuredModel = this.model.withStructuredOutput(responseSchema);\n      const response = await structuredModel.invoke(formattedMessages);\n\n      return {\n        data: response,\n        usage: {\n          prompt_tokens: 0, // Langchain doesn't provide token counts by default\n          completion_tokens: 0,\n          total_tokens: 0,\n        },\n      } as T;\n    }\n\n    const modelWithTools = this.model.bindTools(options.tools);\n    const response = await modelWithTools.invoke(formattedMessages);\n\n    return {\n      data: response,\n      usage: {\n        prompt_tokens: 0, // Langchain doesn't provide token counts by default\n        completion_tokens: 0,\n        total_tokens: 0,\n      },\n    } as T;\n  }\n}\n"
  },
  {
    "path": "packages/core/examples/form_filling_sensible.ts",
    "content": "/**\n * This example shows you how to use observe() to get a cacheable Playwright action as JSON, then pass that JSON to act() to perform the action.\n *\n * In this specific example, we use observe() to get multiple actions, then iterate through each action to fill the form with sensitive data at lightning speed.\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport chalk from \"chalk\";\n\nasync function formFillingSensible() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n  });\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n\n  // Go to the website and wait for it to load\n  await page.goto(\"https://file.1040.com/estimate/\", {\n    waitUntil: \"networkidle\",\n    timeoutMs: 30000,\n  });\n\n  // Observe the form fields with suggested actions\n  const observed = await stagehand.observe(\n    \"fill all the form fields in the page with mock data. In the description include the field name\",\n  );\n\n  // Uncomment the following snippet to see the stagehand candidate suggestions (initial)\n  console.log(\n    `${chalk.green(\"Observe:\")} Form fields found:\\n${observed\n      .map((r) => `${chalk.yellow(r.description)} -> ${chalk.gray(r.selector)}`)\n      .join(\"\\n\")}`,\n  );\n\n  // Create a mapping of 1+ keywords in the form fields to standardize field names\n  const mapping = (description: string): string | null => {\n    const keywords: { [key: string]: string[] } = {\n      age: [\"old\"],\n      dependentsUnder17: [\"under age 17\", \"child\", \"minor\"],\n      dependents17to23: [\"17-23\", \"school\", \"student\"],\n      wages: [\"wages\", \"W-2 Box 1\"],\n      federalTax: [\"federal tax\", \"Box 2\"],\n      stateTax: [\"state tax\", \"Box 17\"],\n    };\n\n    for (const [key, terms] of Object.entries(keywords)) {\n      if (terms.some((term) => description.toLowerCase().includes(term))) {\n        return key;\n      }\n    }\n    return null;\n  };\n\n  // Fill the form fields with sensible data. This data will only be used in your session and not be shared with LLM providers/external APIs.\n  const userInputs: { [key: string]: string } = {\n    age: \"26\",\n    dependentsUnder17: \"1\",\n    wages: \"54321\",\n    federalTax: \"8345\",\n    stateTax: \"2222\",\n  };\n\n  const updatedFields = observed.map((candidate) => {\n    const key = mapping(candidate.description);\n    if (key && userInputs[key]) {\n      candidate.arguments = [userInputs[key]];\n    }\n    return candidate;\n  });\n  // List of sensible-data candidates\n  console.log(\n    `\\n${chalk.green(\"Sensible Data form inputs:\")} Form fields to be filled:\\n${updatedFields\n      .map(\n        (r) =>\n          `${chalk.yellow(r.description)} -> ${chalk.blue(r.arguments?.[0] || \"no value\")}`,\n      )\n      .join(\"\\n\")}`,\n  );\n\n  // Fill all the form fields with the sensible candidates\n  for (const candidate of updatedFields) {\n    await stagehand.act(candidate);\n  }\n}\n\n(async () => {\n  await formFillingSensible();\n})();\n"
  },
  {
    "path": "packages/core/examples/google_enter.ts",
    "content": "/**\n * This example shows how to use the Stagehand agent to navigate to Google and search for \"Browserbase\".\n *\n * It's mainly meant to sanity check using page.act() to press enter, since some LLMs have issues with it.\n */\n\nimport { Stagehand } from \"../lib/v3/index.js\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n  });\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://google.com\");\n  await stagehand.act(\"type in 'Browserbase'\");\n  await stagehand.act(\"press enter\");\n  await stagehand.close();\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/instructions.ts",
    "content": "/**\n * This example shows how to use custom system prompts with Stagehand.\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    systemPrompt:\n      \"if the users says `secret12345`, click on the 'getting started' tab. additionally, if the user says to type something, translate their input into french and type it.\",\n  });\n  await stagehand.init();\n\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://docs.browserbase.com/\");\n\n  await stagehand.act(\"secret12345\");\n\n  await stagehand.act(\"search for 'how to use browserbase'\");\n\n  await stagehand.close();\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/integrations/exa.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://www.google.com\");\n\n  const agent = stagehand.agent({\n    integrations: [\n      `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n    ],\n    // Optional: Add custom instructions\n    systemPrompt: `You are a helpful assistant that can use a browser as well as external tools such as web search.\n    You have access to the Exa search tool to find information on the web.\n    When looking for products to buy, make sure to search for current and reliable information.\n    Be thorough in your research before making purchase decisions.`,\n  });\n\n  const result = await agent.execute(\n    \"Use one of the tools from Exa to search for the top headphones of 2025. After doing so, use the browser and go through the checkout flow for the best one.\",\n  );\n\n  console.log(result);\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    model: \"openai/gpt-4.1\",\n    verbose: 1,\n    logInferenceToFile: true,\n    experimental: true,\n  });\n\n  try {\n    await stagehand.init();\n    await example(stagehand);\n  } catch (error) {\n    console.error(\"Error running example:\", error);\n  } finally {\n    await stagehand.close();\n  }\n})();\n"
  },
  {
    "path": "packages/core/examples/integrations/supabase.ts",
    "content": "import { connectToMCPServer, Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://www.opentable.com/\");\n\n  const supabaseClient = await connectToMCPServer(\n    `https://server.smithery.ai/@supabase-community/supabase-mcp/mcp?api_key=${process.env.SMITHERY_API_KEY}`,\n  );\n\n  const agent = stagehand.agent({\n    model: \"openai/computer-use-preview\",\n    integrations: [supabaseClient],\n  });\n\n  const result = await agent.execute(\n    \"Search for restaurants in New Brunswick, NJ. Then, use the Supabase tools to insert the name of the first result of the search into a table called 'restaurants'.\",\n  );\n\n  console.log(result);\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 1,\n  });\n\n  try {\n    await stagehand.init();\n    await example(stagehand);\n  } catch (error) {\n    console.error(\"Error running example:\", error);\n  } finally {\n    await stagehand.close();\n  }\n})();\n"
  },
  {
    "path": "packages/core/examples/mcp.ts",
    "content": "// import { Stagehand } from \"../lib/v3\";\n// import StagehandConfig from \"@/stagehand.config\";\n// import chalk from \"chalk\";\n// import { connectToMCPServer } from \"../lib/mcp/connection\";\n\n// async function main() {\n//   console.log(`\\n${chalk.bold(\"Stagehand 🤘 MCP Demo\")}\\n`);\n//   console.log(process.env.NOTION_TOKEN);\n\n//   // Initialize Stagehand\n//   const stagehand = new Stagehand({\n//     ...StagehandConfig,\n//     env: \"LOCAL\",\n//     experimental: true,\n//   });\n//   await stagehand.init();\n\n//   const notionClient = await connectToMCPServer({\n//     command: \"npx\",\n//     args: [\"-y\", \"@notionhq/notion-mcp-server\"],\n//     env: {\n//       NOTION_TOKEN: process.env.NOTION_TOKEN,\n//     },\n//   });\n\n//   try {\n//     const page = stagehand.page;\n\n//     // Create a computer use agent\n//     const agent = stagehand.agent({\n//       provider: \"anthropic\",\n//       // For Anthropic, use claude-sonnet-4-6 or claude-sonnet-4-5-20250929\n//       model: \"claude-sonnet-4-6\",\n//       instructions: `You are a helpful assistant that can use a web browser.\n//       You are currently on the following page: ${page.url()}.\n//       Do not ask follow up questions, the user will trust your judgement.\n//       You have access to the Notion MCP.`,\n//       options: {\n//         apiKey: process.env.ANTHROPIC_API_KEY,\n//       },\n//       integrations: [notionClient],\n//     });\n\n//     // Navigate to the Browserbase careers page\n//     await page.goto(\"https://www.google.com\");\n\n//     // Define the instruction for the CUA\n//     const instruction =\n//       \"Check the Agent Tasks page in notion, read your tasks, perform them and update the notion page with the results.\";\n//     console.log(`Instruction: ${chalk.white(instruction)}`);\n\n//     // Execute the instruction\n//     const result = await agent.execute({\n//       instruction,\n//       maxSteps: 50,\n//     });\n\n//     console.log(`${chalk.green(\"✓\")} Execution complete`);\n//     console.log(`${chalk.yellow(\"⤷\")} Result:`);\n//     console.log(chalk.white(JSON.stringify(result, null, 2)));\n//   } catch (error) {\n//     console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n//     if (error instanceof Error && error.stack) {\n//       console.log(chalk.dim(error.stack.split(\"\\n\").slice(1).join(\"\\n\")));\n//     }\n//   } finally {\n//     // Close the browser\n//     await stagehand.close();\n//   }\n// }\n\n// main().catch((error) => {\n//   console.log(`${chalk.red(\"✗\")} Unhandled error in main function`);\n//   console.log(chalk.red(error));\n// });\n"
  },
  {
    "path": "packages/core/examples/operator-example.ts",
    "content": "/**\n * This example shows how to use the Stagehand operator to do simple autonomous tasks.\n *\n * This is built off of our open source project, Open Operator: https://operator.browserbase.com\n *\n * To learn more about Stagehand Agents, see: https://docs.stagehand.dev/concepts/agent\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport chalk from \"chalk\";\n\n// Load environment variables\n\nasync function main() {\n  console.log(`\\n${chalk.bold(\"Stagehand 🤘 Operator Example\")}\\n`);\n  // Initialize Stagehand\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 2,\n    cacheDir: \"stagehand-agent-cache\",\n    logInferenceToFile: false,\n  });\n\n  await stagehand.init();\n\n  try {\n    const page = stagehand.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/shadow-dom/\",\n    );\n    const agent = stagehand.agent();\n\n    const result = await agent.execute({\n      instruction: \"click the button\",\n      maxSteps: 20,\n    });\n\n    console.log(`${chalk.green(\"✓\")} Execution complete`);\n    console.log(`${chalk.yellow(\"⤷\")} Result:`);\n    console.log(JSON.stringify(result, null, 2));\n    console.log(chalk.white(result.message));\n  } catch (error) {\n    console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n  } finally {\n    // await stagehand.close();\n  }\n}\nmain();\n"
  },
  {
    "path": "packages/core/examples/oss-cua-example.ts",
    "content": "/**\n * This example shows how to use a computer use agent (CUA) to navigate a web page and extract data.\n *\n * To learn more about the CUA, see: https://docs.stagehand.dev/examples/computer_use\n *\n * NOTE: YOU MUST CONFIGURE BROWSER DIMENSIONS TO USE COMPUTER USE!\n * Check out stagehand.config.ts for more information.\n */\nimport { Stagehand } from \"../lib/v3/index.js\";\nimport chalk from \"chalk\";\n\nasync function main() {\n  console.log(\n    `\\n${chalk.bold(\"Stagehand 🤘 Computer Use Agent (CUA) Demo\")}\\n`,\n  );\n\n  // Initialize Stagehand\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 2,\n    localBrowserLaunchOptions: {\n      viewport: {\n        width: 1288,\n        height: 711,\n      },\n      deviceScaleFactor: 1,\n    },\n  });\n  await stagehand.init();\n\n  try {\n    const page = stagehand.context.pages()[0];\n\n    // Create a computer use agent\n    const agent = stagehand.agent({\n      mode: \"cua\",\n      model: {\n        modelName: \"microsoft/fara-7b\",\n        apiKey: process.env.AZURE_API_KEY,\n        baseURL: process.env.AZURE_ENDPOINT,\n        /** Alternative model configuration for Fireworks Deployments */\n        // modelName: \"accounts/...\",\n        // apiKey: process.env.FIREWORKS_API_KEY,\n        // baseURL: \"https://api.fireworks.ai/inference/v1\",\n        // provider: \"microsoft\", // Important: this routes to the MicrosoftCUAClient\n      },\n      systemPrompt: `You are a helpful assistant that can use a web browser.\n      You are currently on the following page: ${page.url()}.\n      Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}. Remember apply buttons are there for a reason.`,\n    });\n\n    // Navigate to the Browserbase careers page\n    await page.goto(\"https://www.browserbase.com/careers\");\n\n    // Define the instruction for the CUA\n    const instruction = `Apply for the first engineer position with mock data on the ${page.url()} page. Don't submit the form.`;\n    console.log(`Instruction: ${chalk.white(instruction)}`);\n\n    // Execute the instruction\n    const result = await agent.execute({\n      instruction,\n      maxSteps: 20,\n    });\n    await new Promise((resolve) => setTimeout(resolve, 30000));\n\n    console.log(`${chalk.green(\"✓\")} Execution complete`);\n    console.log(`${chalk.yellow(\"⤷\")} Result:`);\n    console.log(chalk.white(JSON.stringify(result, null, 2)));\n  } catch (error) {\n    console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n    if (error instanceof Error && error.stack) {\n      console.log(chalk.dim(error.stack.split(\"\\n\").slice(1).join(\"\\n\")));\n    }\n  } finally {\n    // Close the browser\n    await stagehand.close();\n  }\n}\n\nmain().catch((error) => {\n  console.log(`${chalk.red(\"✗\")} Unhandled error in main function`);\n  console.log(chalk.red(error));\n});\n"
  },
  {
    "path": "packages/core/examples/parameterizeApiKey.ts",
    "content": "import { Stagehand } from \"../lib/v3/index.js\";\nimport { z } from \"zod\";\n\n/**\n * This example shows how to parameterize the API key for the LLM provider.\n *\n * In order to best demonstrate, unset the OPENAI_API_KEY environment variable and\n * set the USE_OPENAI_API_KEY environment variable to your OpenAI API key.\n *\n * export USE_OPENAI_API_KEY=$OPENAI_API_KEY\n * unset OPENAI_API_KEY\n */\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 1,\n    model: {\n      modelName: \"gpt-4o\",\n      apiKey: process.env.USE_OPENAI_API_KEY,\n    },\n  });\n\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://github.com/browserbase/stagehand\");\n  await stagehand.act(\"click on the contributors\");\n  const contributor = await stagehand.extract(\n    \"extract the top contributor\",\n    z.object({\n      username: z.string(),\n      url: z.string(),\n    }),\n  );\n  console.log(`Our favorite contributor is ${contributor.username}`);\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/examples/persist_logs_example.ts",
    "content": "/**\n * Example: Run a Stagehand agent and persist structured logging events to a user-specified dir.\n */\nimport path from \"node:path\";\nimport { Stagehand } from \"../lib/v3/index.js\";\n\nasync function main() {\n  const logsRoot = path.resolve(process.cwd(), \"examples\", \"logs\");\n  process.env.BROWSERBASE_CONFIG_DIR = logsRoot;\n\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 1,\n  });\n\n  await stagehand.init();\n\n  try {\n    const page = stagehand.context.pages()[0];\n    await page.goto(\"https://www.google.com\");\n\n    const agent = stagehand.agent();\n    await agent.execute({\n      instruction:\n        \"Search for Browserbase and stop after the results are visible.\",\n      maxSteps: 10,\n    });\n  } finally {\n    // All logs can be found at logs/sessions/$SESSION_ID/session.json, or agent_events.log etc\n    await stagehand.close();\n  }\n}\n\nmain().catch((error) => {\n  console.error(error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "packages/core/examples/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"include\": [\"*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/core/examples/v3/cuaReplay.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\nimport { v3Logger } from \"../../lib/v3/logger.js\";\n\nasync function runDemo(runNumber: number) {\n  const startTime = Date.now();\n\n  v3Logger({\n    level: 1,\n    category: \"demo\",\n    message: `RUN ${runNumber}: ${runNumber === 1 ? \"BUILDING CACHE\" : \"USING CACHE\"}`,\n  });\n\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    disableAPI: false,\n    verbose: 1,\n    cacheDir: \"cua-agent-cache\",\n  });\n\n  await stagehand.init();\n\n  const page = stagehand.context.pages()[0];\n\n  await page.goto(\"https://v0-modern-login-flow.vercel.app/\", {\n    waitUntil: \"networkidle\",\n  });\n\n  const agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n      modelName: \"anthropic/claude-sonnet-4-20250514\",\n      apiKey: process.env.ANTHROPIC_API_KEY!,\n    },\n  });\n\n  const result = await agent.execute({\n    instruction: `Sign in with the email address 'test@browserbaser.com' and the password 'stagehand=goated'`,\n    maxSteps: 20,\n  });\n\n  const endTime = Date.now();\n  const duration = (endTime - startTime) / 1000;\n\n  await stagehand.close();\n\n  return {\n    duration,\n    success: result.success,\n    result,\n  };\n}\n\nasync function main() {\n  const metrics1 = await runDemo(1);\n\n  v3Logger({\n    level: 1,\n    category: \"demo\",\n    message: \"⏳ Waiting 2 seconds before cached run...\",\n  });\n  await new Promise((resolve) => setTimeout(resolve, 2000));\n\n  v3Logger({\n    level: 1,\n    category: \"demo\",\n    message: \"Starting second run with cache...\",\n  });\n  const metrics2 = await runDemo(2);\n\n  const duration1 = `${metrics1.duration.toFixed(2)}s`;\n  const duration2 = `${metrics2.duration.toFixed(2)}s`;\n\n  v3Logger({\n    level: 1,\n    category: \"demo\",\n    message: `\n╔════════════════════════════════════════════════════════════╗\n║                  📊 PERFORMANCE COMPARISON                 ║\n╚════════════════════════════════════════════════════════════╝\n\n┌─────────────────────┬──────────────────┬──────────────────┐\n│     Metric          │   Run 1 (Cold)   │  Run 2 (Cached)  │\n├─────────────────────┼──────────────────┼──────────────────┤\n│ Duration            │ ${duration1.padEnd(16)} │ ${duration2.padEnd(16)} │\n└─────────────────────┴──────────────────┴──────────────────┘\n\n Performance Comparison:\n   • Speed: ${((1 - metrics2.duration / metrics1.duration) * 100).toFixed(1)}% faster with cache\n   • Time saved: ${(metrics1.duration - metrics2.duration).toFixed(2)} seconds\n\n Insights:\n   • First run establishes the CUA action cache\n   • Second run reuses cached actions for instant execution\n   • Zero LLM tokens used on cached run`,\n  });\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "packages/core/examples/v3/deepLocator.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n  );\n\n  // crossing OOPIF & shadow root boundaries with deep locator\n  await page\n    .deepLocator(\n      \"/html/body/shadow-host//section/iframe/html/body/main/section[1]/form/div/div[1]/input\",\n    )\n    .fill(\"nunya\");\n  await page\n    .deepLocator(\n      \"/html/body/shadow-host//section/iframe/html/body/main/section[1]/form/div/div[2]/input\",\n    )\n    .fill(\"business\");\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"openai/gpt-4.1\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/dropdown.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/scroll-dropdown/\",\n  );\n\n  const actResult = await stagehand.act(\n    \"choose 'Peach' from the favorite colour dropdown\",\n  );\n\n  const numSteps = actResult.actions.length;\n\n  console.log(\n    `\\n\\nThis act() call took ${numSteps} steps. Here are the actions:`,\n  );\n\n  for (const action of actResult.actions) {\n    console.log(`\\naction: `, action);\n  }\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"google/gemini-2.5-flash\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/highlight.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-root-in-oopif/\",\n  );\n\n  await page\n    .deepLocator(\n      \"xpath=/html/body/main/section/iframe/html/body/shadow-demo//div/button\",\n    )\n    .highlight({\n      durationMs: 20000,\n      contentColor: { r: 255, g: 0, b: 0 },\n    });\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"google/gemini-2.5-flash\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/patchright.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\nimport { chromium } from \"patchright-core\";\nimport { z } from \"zod\";\n\nasync function example(stagehand: Stagehand) {\n  const browser = await chromium.connectOverCDP({\n    wsEndpoint: stagehand.connectURL(),\n  });\n\n  const prContext = browser.contexts()[0];\n  const prPage = prContext.pages()[0];\n  await prPage.goto(\"https://github.com/microsoft/playwright/issues/30261\");\n\n  await stagehand.act(\"scroll to the bottom of the page\", { page: prPage });\n\n  const reason = await stagehand.extract(\n    \"extract the reason why playwright doesn't expose frame IDs\",\n    z.string(),\n    // page arg not required\n  );\n  console.log(reason);\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"openai/gpt-4.1\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/playwright.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\nimport { chromium } from \"playwright-core\";\nimport { z } from \"zod\";\n\nasync function example(stagehand: Stagehand) {\n  const browser = await chromium.connectOverCDP({\n    wsEndpoint: stagehand.connectURL(),\n  });\n  const pwContext = browser.contexts()[0];\n  const pwPage1 = pwContext.pages()[0];\n  await pwPage1.goto(\"https://docs.stagehand.dev/first-steps/introduction\");\n\n  const pwPage2 = await pwContext.newPage();\n  await pwPage2.goto(\"https://docs.stagehand.dev/configuration/observability\");\n\n  const [page1Extraction, page2Extraction] = await Promise.all([\n    stagehand.extract(\n      \"extract the names of the four stagehand primitives\",\n      z.array(z.string()),\n      { page: pwPage1 },\n    ),\n    stagehand.extract(\n      \"extract the list of session dashboard features\",\n      z.array(z.string()),\n      { page: pwPage2 },\n    ),\n  ]);\n\n  console.log(page1Extraction);\n  console.log(page2Extraction);\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    model: \"openai/gpt-4.1\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/puppeteer.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\nimport puppeteer from \"puppeteer-core\";\n\nasync function example(stagehand: Stagehand) {\n  const browser = await puppeteer.connect({\n    browserWSEndpoint: stagehand.connectURL(),\n    defaultViewport: null,\n  });\n  const ppPages = await browser.pages();\n  const ppPage = ppPages[0];\n\n  await ppPage.goto(\"https://www.browserbase.com/blog\");\n\n  const actions = await stagehand.observe(\"find the next page button\", {\n    page: ppPage,\n  });\n\n  await stagehand.act(actions[0]);\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"openai/gpt-4.1\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/recordVideo.ts",
    "content": "import path from \"node:path\";\nimport { mkdir } from \"node:fs/promises\";\nimport { Stagehand } from \"../../lib/v3/index.js\";\nimport { chromium } from \"playwright-core\";\nimport { z } from \"zod\";\n\nasync function recordPlaywrightVideo(stagehand: Stagehand): Promise<void> {\n  const browser = await chromium.connectOverCDP({\n    wsEndpoint: stagehand.connectURL(),\n  });\n\n  const videoDir = path.resolve(process.cwd(), \"artifacts\", \"stagehand-videos\");\n  await mkdir(videoDir, { recursive: true });\n\n  const context = await browser.newContext({\n    recordVideo: {\n      dir: videoDir,\n      size: { width: 1280, height: 720 },\n    },\n  });\n\n  const page = await context.newPage();\n  await page.goto(\"https://docs.stagehand.dev/first-steps/quickstart\", {\n    waitUntil: \"domcontentloaded\",\n  });\n\n  await stagehand.act(\"click the introduction div in the first steps section\");\n\n  const { primitives } = await stagehand.extract(\n    \"list the four Stagehand primitives that are described on the page\",\n    z.object({\n      primitives: z.array(z.string()),\n    }),\n    { page },\n  );\n\n  console.log(\"Stagehand primitives:\", primitives.join(\", \"));\n\n  // Capture the handle before closing the context so we can read the video path afterwards.\n  const video = page.video();\n\n  await context.close();\n\n  if (video) {\n    const videoPath = await video.path();\n    console.log(`Playwright saved the video to ${videoPath}`);\n  } else {\n    console.log(\"Video recording was not enabled for this context.\");\n  }\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 1,\n    model: \"google/gemini-2.5-flash\",\n  });\n\n  try {\n    await stagehand.init();\n    await recordPlaywrightVideo(stagehand);\n  } finally {\n    await stagehand.close().catch(() => {});\n  }\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/returnXpath.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n  );\n\n  const xpath = await page.click(286, 628, { returnXpath: true });\n\n  // use the xpath that was returned from out coord click\n  await page.deepLocator(xpath).fill(\"hellooooooooo\");\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"openai/gpt-4.1\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/shadowRoot.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/shadow-dom-closed/\",\n  );\n\n  // clicking in closed mode shadow root with an xpath\n  await page.locator(\"/html/body/shadow-demo//div/button\").click();\n\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n\n  await page.reload();\n  await new Promise((resolve) => setTimeout(resolve, 3000));\n\n  // clicking in closed mode shadow root with css selector\n  await page.locator(\"div > button\").click();\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"openai/gpt-4.1\",\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/targetedExtract.ts",
    "content": "import { Stagehand } from \"../../lib/v3/index.js\";\nimport { z } from \"zod\";\n\nasync function example(stagehand: Stagehand) {\n  const page = stagehand.context.pages()[0];\n  await page.goto(\n    \"https://ambarc.github.io/web-element-test/stagehand-breaking-test.html\",\n  );\n\n  await page\n    .deepLocator(\"/html/body/div[2]/div[3]/iframe/html/body/p\")\n    .highlight({\n      durationMs: 5000,\n      contentColor: { r: 255, g: 0, b: 0 },\n    });\n\n  const reason = await stagehand.extract(\n    \"extract the reason why script injection fails\",\n    z.string(),\n    // selector: \"// body > div.test-container > div:nth-child(3) > iframe >> body > p:nth-child(3)\",\n    { selector: \"/html/body/div[2]/div[3]/iframe/html/body/p[2]\" },\n  );\n  console.log(reason);\n}\n\n(async () => {\n  const stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 0,\n    model: \"openai/gpt-4.1\",\n    logInferenceToFile: true,\n  });\n  await stagehand.init();\n  await example(stagehand);\n})();\n"
  },
  {
    "path": "packages/core/examples/v3/v3_agent.ts",
    "content": "import chalk from \"chalk\";\nimport { V3 } from \"../../lib/v3/index.js\";\n\nconst INSTRUCTION = \"scroll down and click on the last hn story\";\n\nasync function main() {\n  console.log(`\\n${chalk.bold(\"Stagehand V3 🤘 Operator Example\")}\\n`);\n\n  // Initialize Stagehand\n  const v3 = new V3({\n    env: \"LOCAL\",\n    verbose: 2,\n  });\n\n  await v3.init();\n\n  try {\n    const startPage = v3.context.pages()[0];\n    await startPage.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/\",\n    );\n    const agent = v3.agent({\n      cua: false,\n      model: \"google/gemini-2.0-flash\",\n      executionModel: \"google/gemini-2.0-flash\",\n    });\n    // {\n    //   model: \"computer-use-preview-2025-03-11\",\n    //   provider: \"openai\",\n    // }\n\n    // Execute the agent\n    console.log(`${chalk.cyan(\"↳\")} Instruction: ${INSTRUCTION}`);\n    const result = await agent.execute({\n      instruction: INSTRUCTION,\n      maxSteps: 20,\n    });\n\n    console.log(`${chalk.green(\"✓\")} Execution complete`);\n    console.log(`${chalk.yellow(\"⤷\")} Result:`);\n    console.log(JSON.stringify(result, null, 2));\n    console.log(chalk.white(result.message));\n  } catch (error) {\n    console.log(`${chalk.red(\"✗\")} Error: ${error}`);\n  } finally {\n    // await v3.close();\n  }\n}\n\nmain();\n"
  },
  {
    "path": "packages/core/examples/v3_example.ts",
    "content": "import { V3 } from \"../lib/v3/index.js\";\nimport { z } from \"zod\";\n\nasync function example(v3: V3) {\n  const page = v3.context.pages()[0];\n  await page.goto(\"https://www.apartments.com/san-francisco-ca/2-bedrooms/\", {\n    waitUntil: \"load\",\n  });\n  const apartment_listings = await v3.extract(\n    \"Extract all the apartment listings with their prices and their addresses.\",\n    z.object({\n      listings: z.array(\n        z.object({\n          price: z.string().describe(\"The price of the listing\"),\n          address: z.string().describe(\"The address of the listing\"),\n        }),\n      ),\n    }),\n  );\n\n  const listings = apartment_listings.listings;\n  console.log(listings);\n  console.log(`found ${listings.length} listings`);\n}\n\n(async () => {\n  const v3 = new V3({\n    env: \"LOCAL\",\n    verbose: 2,\n    logInferenceToFile: false,\n    model: \"google/gemini-2.0-flash\",\n    cacheDir: \"stagehand-extract-cache\",\n  });\n  await v3.init();\n  await example(v3);\n})();\n"
  },
  {
    "path": "packages/core/examples/wordle.ts",
    "content": "import { Stagehand } from \"../lib/v3/index.js\";\n\nasync function example() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n  });\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n  await page.goto(\"https://www.nytimes.com/games/wordle/index.html\");\n  await stagehand.act(\"click 'Continue'\");\n  await stagehand.act(\"click 'Play'\");\n  await stagehand.act(\"click cross sign on top right of 'How To Play' card\");\n  const word = \"WORDS\";\n  for (const letter of word) {\n    await stagehand.act(`press ${letter}`);\n  }\n  await stagehand.act(\"press enter\");\n  await stagehand.close();\n}\n\n(async () => {\n  await example();\n})();\n"
  },
  {
    "path": "packages/core/lib/CHANGELOG.md",
    "content": "# @browserbasehq/stagehand-lib\n\n## 2.4.1\n\n### Patch Changes\n\n- [#1027](https://github.com/browserbase/stagehand/pull/1027) [`455b61f`](https://github.com/browserbase/stagehand/commit/455b61fb6f7a34ae50d7e7c76c1d639241e213d6) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Fixed small issue with module-level state guard for the Playwright selectors.register call\n\n## 2.4.0\n\n### Minor Changes\n\n- [#778](https://github.com/browserbase/stagehand/pull/778) [`df570b6`](https://github.com/browserbase/stagehand/commit/df570b67e46febcaf7282ffb65dd5707e2808152) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - iframe support\n\n### Patch Changes\n\n- [#809](https://github.com/browserbase/stagehand/pull/809) [`03ebebc`](https://github.com/browserbase/stagehand/commit/03ebebc0317f92d8de77285cc2e66dc0131fe9fe) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - log NoObjectGenerated error details\n\n- [#801](https://github.com/browserbase/stagehand/pull/801) [`1d4f0ab`](https://github.com/browserbase/stagehand/commit/1d4f0abca47bf47ae8b7aeb53f3cd1155a7e5448) Thanks [@miguelg719](https://github.com/miguelg719)! - Default use API to true\n\n- [#798](https://github.com/browserbase/stagehand/pull/798) [`d86200b`](https://github.com/browserbase/stagehand/commit/d86200bd5bde4c5ba113ca89e28ab86c14a8304e) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix pino logging memory leak by reusing worker\n\n## 2.3.0\n\n### Minor Changes\n\n- [#731](https://github.com/browserbase/stagehand/pull/731) [`393c8e0`](https://github.com/browserbase/stagehand/commit/393c8e05d016086e481c0043ee6b084c61886cad) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - make extract() with no arguments return the hybrid tree instead of text-rendered webpage\n\n- [#737](https://github.com/browserbase/stagehand/pull/737) [`6ef6073`](https://github.com/browserbase/stagehand/commit/6ef60730cab0ad9025f44b6eeb2c83751d1dcd35) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - deprecate useTextExtract and remove functionality\n\n### Patch Changes\n\n- [#741](https://github.com/browserbase/stagehand/pull/741) [`5680d25`](https://github.com/browserbase/stagehand/commit/5680d2509352c383ad502c9f4fabde01fa638833) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - use safeparse for zod validation\n\n- [#740](https://github.com/browserbase/stagehand/pull/740) [`28840a7`](https://github.com/browserbase/stagehand/commit/28840a7d3fec89a490984582fb37fa3d007c0349) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - dont log deprecation warning when onlyVisible is undefined\n\n- [#755](https://github.com/browserbase/stagehand/pull/755) [`ba687ab`](https://github.com/browserbase/stagehand/commit/ba687abdfb598f839ddfec0442d3d7b6b696b0a3) Thanks [@miguelg719](https://github.com/miguelg719)! - Fix context init error on undefined context\n\n- [#789](https://github.com/browserbase/stagehand/pull/789) [`c5ff8ce`](https://github.com/browserbase/stagehand/commit/c5ff8ce2d7467b70a450ca52bc3e03b15280ce1b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix noisy useTextExtract deprecation log\n\n- [#757](https://github.com/browserbase/stagehand/pull/757) [`628e534`](https://github.com/browserbase/stagehand/commit/628e534ea6d7ca081bad6c32167c7d53d4772eed) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - optimize CDP calls when building hybrid tree\n\n- [#772](https://github.com/browserbase/stagehand/pull/772) [`64d331d`](https://github.com/browserbase/stagehand/commit/64d331dc2eba86675a8b148d361897f55f170703) Thanks [@miguelg719](https://github.com/miguelg719)! - Fixes an issue with the new tab intercepts for invalid urls\n\n- [#770](https://github.com/browserbase/stagehand/pull/770) [`d312a43`](https://github.com/browserbase/stagehand/commit/d312a43672fe2865abcf184a712a759a12f5b9d1) Thanks [@miguelg719](https://github.com/miguelg719)! - Removed default chromium flags that delay browser launching\n\n- [#753](https://github.com/browserbase/stagehand/pull/753) [`fbca400`](https://github.com/browserbase/stagehand/commit/fbca4003a547dc5eee0c0be5edc5e98c1f4d8c22) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix `stagehand.history`\n\n- [#745](https://github.com/browserbase/stagehand/pull/745) [`c54afab`](https://github.com/browserbase/stagehand/commit/c54afab0e43a2144eecbc56df7f33c5e444ceed5) Thanks [@miguelg719](https://github.com/miguelg719)! - Add an identifier for client language/runtime\n\n- [#768](https://github.com/browserbase/stagehand/pull/768) [`58b06eb`](https://github.com/browserbase/stagehand/commit/58b06eb2fdfb1a9cd84c03f46655ab0ea00ee07f) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix: page.evaluate: Execution context was destroyed, most likely because of a navigation\n\n- [#758](https://github.com/browserbase/stagehand/pull/758) [`98e1356`](https://github.com/browserbase/stagehand/commit/98e13566846a547003e4c9aebbe4f95eff653bba) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - rm unused functions\n\n- [#781](https://github.com/browserbase/stagehand/pull/781) [`8d239ce`](https://github.com/browserbase/stagehand/commit/8d239cec7a835d35243b2b00c3c00c1b66c05b5e) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix variable parsing issue with gpt-4.1\n\n- [#761](https://github.com/browserbase/stagehand/pull/761) [`e1f7074`](https://github.com/browserbase/stagehand/commit/e1f7074be23c82ae897386d5e5e132ff8cb4120a) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - build xpaths on node side instead of using injected JS\n\n## 2.2.1\n\n### Patch Changes\n\n- [#729](https://github.com/browserbase/stagehand/pull/729) [`fc24f84`](https://github.com/browserbase/stagehand/commit/fc24f848ee0f300182e88993dfe8d68025d69fcb) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix \"failed to inject helper scripts\" log on stagehand.close()\n"
  },
  {
    "path": "packages/core/lib/inference.ts",
    "content": "import { z } from \"zod\";\nimport { LogLine } from \"./v3/types/public/logs.js\";\nimport { ChatMessage, LLMClient } from \"./v3/llm/LLMClient.js\";\nimport { getEnvTimeoutMs, withTimeout } from \"./v3/timeoutConfig.js\";\nimport {\n  buildActSystemPrompt,\n  buildExtractSystemPrompt,\n  buildExtractUserPrompt,\n  buildMetadataPrompt,\n  buildMetadataSystemPrompt,\n  buildObserveSystemPrompt,\n  buildObserveUserMessage,\n} from \"./prompt.js\";\nimport { appendSummary, writeTimestampedTxtFile } from \"./inferenceLogUtils.js\";\nimport type {\n  InferStagehandSchema,\n  StagehandZodObject,\n} from \"./v3/zodCompat.js\";\nimport { SupportedUnderstudyAction } from \"./v3/types/private/handlers.js\";\n\n// Re-export for backward compatibility\nexport type { LLMParsedResponse, LLMUsage } from \"./v3/llm/LLMClient.js\";\n\nfunction withLlmTimeout<T>(promise: Promise<T>, operation: string): Promise<T> {\n  return withTimeout(\n    promise,\n    getEnvTimeoutMs(\"LLM_MAX_MS\"),\n    `LLM ${operation}`,\n  );\n}\n\nexport async function extract<T extends StagehandZodObject>({\n  instruction,\n  domElements,\n  schema,\n  llmClient,\n  logger,\n  userProvidedInstructions,\n  logInferenceToFile = false,\n}: {\n  instruction: string;\n  domElements: string;\n  schema: T;\n  llmClient: LLMClient;\n  userProvidedInstructions?: string;\n  logger: (message: LogLine) => void;\n  logInferenceToFile?: boolean;\n}) {\n  const metadataSchema = z.object({\n    progress: z\n      .string()\n      .describe(\n        \"progress of what has been extracted so far, as concise as possible\",\n      ),\n    completed: z\n      .boolean()\n      .describe(\n        \"true if the goal is now accomplished. Use this conservatively, only when sure that the goal has been completed.\",\n      ),\n  });\n\n  type ExtractionResponse = InferStagehandSchema<T>;\n  type MetadataResponse = z.infer<typeof metadataSchema>;\n\n  const isUsingAnthropic = llmClient.type === \"anthropic\";\n  const isGPT5 = llmClient.modelName.includes(\"gpt-5\"); // TODO: remove this as we update support for gpt-5 configuration options\n\n  const extractCallMessages: ChatMessage[] = [\n    buildExtractSystemPrompt(isUsingAnthropic, userProvidedInstructions),\n    buildExtractUserPrompt(instruction, domElements, isUsingAnthropic),\n  ];\n\n  let extractCallFile = \"\";\n  let extractCallTimestamp = \"\";\n  if (logInferenceToFile) {\n    const { fileName, timestamp } = writeTimestampedTxtFile(\n      \"extract_summary\",\n      \"extract_call\",\n      {\n        modelCall: \"extract\",\n        messages: extractCallMessages,\n      },\n    );\n    extractCallFile = fileName;\n    extractCallTimestamp = timestamp;\n  }\n\n  const extractStartTime = Date.now();\n  const extractionResponse = await withLlmTimeout(\n    llmClient.createChatCompletion<ExtractionResponse>({\n      options: {\n        messages: extractCallMessages,\n        response_model: {\n          schema,\n          name: \"Extraction\",\n        },\n        temperature: isGPT5 ? 1 : 0.1,\n        top_p: 1,\n        frequency_penalty: 0,\n        presence_penalty: 0,\n      },\n      logger,\n    }),\n    \"extract\",\n  );\n  const extractEndTime = Date.now();\n\n  const { data: extractedData, usage: extractUsage } = extractionResponse;\n\n  let extractResponseFile: string;\n  if (logInferenceToFile) {\n    const { fileName } = writeTimestampedTxtFile(\n      \"extract_summary\",\n      \"extract_response\",\n      {\n        modelResponse: \"extract\",\n        rawResponse: extractedData,\n      },\n    );\n    extractResponseFile = fileName;\n\n    appendSummary(\"extract\", {\n      extract_inference_type: \"extract\",\n      timestamp: extractCallTimestamp,\n      LLM_input_file: extractCallFile,\n      LLM_output_file: extractResponseFile,\n      prompt_tokens: extractUsage?.prompt_tokens ?? 0,\n      completion_tokens: extractUsage?.completion_tokens ?? 0,\n      reasoning_tokens: extractUsage?.reasoning_tokens ?? 0,\n      cached_input_tokens: extractUsage?.cached_input_tokens ?? 0,\n      inference_time_ms: extractEndTime - extractStartTime,\n    });\n  }\n\n  const metadataCallMessages: ChatMessage[] = [\n    buildMetadataSystemPrompt(),\n    buildMetadataPrompt(instruction, extractedData),\n  ];\n\n  let metadataCallFile = \"\";\n  let metadataCallTimestamp = \"\";\n  if (logInferenceToFile) {\n    const { fileName, timestamp } = writeTimestampedTxtFile(\n      \"extract_summary\",\n      \"metadata_call\",\n      {\n        modelCall: \"metadata\",\n        messages: metadataCallMessages,\n      },\n    );\n    metadataCallFile = fileName;\n    metadataCallTimestamp = timestamp;\n  }\n\n  const metadataStartTime = Date.now();\n  const metadataResponse = await withLlmTimeout(\n    llmClient.createChatCompletion<MetadataResponse>({\n      options: {\n        messages: metadataCallMessages,\n        response_model: {\n          name: \"Metadata\",\n          schema: metadataSchema,\n        },\n        temperature: isGPT5 ? 1 : 0.1,\n        top_p: 1,\n        frequency_penalty: 0,\n        presence_penalty: 0,\n      },\n      logger,\n    }),\n    \"extract metadata\",\n  );\n  const metadataEndTime = Date.now();\n\n  const {\n    data: {\n      completed: metadataResponseCompleted,\n      progress: metadataResponseProgress,\n    },\n    usage: metadataResponseUsage,\n  } = metadataResponse;\n\n  let metadataResponseFile: string;\n  if (logInferenceToFile) {\n    const { fileName } = writeTimestampedTxtFile(\n      \"extract_summary\",\n      \"metadata_response\",\n      {\n        modelResponse: \"metadata\",\n        completed: metadataResponseCompleted,\n        progress: metadataResponseProgress,\n      },\n    );\n    metadataResponseFile = fileName;\n\n    appendSummary(\"extract\", {\n      extract_inference_type: \"metadata\",\n      timestamp: metadataCallTimestamp,\n      LLM_input_file: metadataCallFile,\n      LLM_output_file: metadataResponseFile,\n      prompt_tokens: metadataResponseUsage?.prompt_tokens ?? 0,\n      completion_tokens: metadataResponseUsage?.completion_tokens ?? 0,\n      reasoning_tokens: metadataResponseUsage?.reasoning_tokens ?? 0,\n      cached_input_tokens: metadataResponseUsage?.cached_input_tokens ?? 0,\n      inference_time_ms: metadataEndTime - metadataStartTime,\n    });\n  }\n\n  const totalPromptTokens =\n    (extractUsage?.prompt_tokens ?? 0) +\n    (metadataResponseUsage?.prompt_tokens ?? 0);\n\n  const totalCompletionTokens =\n    (extractUsage?.completion_tokens ?? 0) +\n    (metadataResponseUsage?.completion_tokens ?? 0);\n\n  const totalInferenceTimeMs =\n    extractEndTime - extractStartTime + (metadataEndTime - metadataStartTime);\n  const totalReasoningTokens =\n    (extractUsage?.reasoning_tokens ?? 0) +\n    (metadataResponseUsage?.reasoning_tokens ?? 0);\n  const totalCachedInputTokens =\n    (extractUsage?.cached_input_tokens ?? 0) +\n    (metadataResponseUsage?.cached_input_tokens ?? 0);\n\n  return {\n    ...extractedData,\n    metadata: {\n      completed: metadataResponseCompleted,\n      progress: metadataResponseProgress,\n    },\n    prompt_tokens: totalPromptTokens,\n    completion_tokens: totalCompletionTokens,\n    reasoning_tokens: totalReasoningTokens,\n    cached_input_tokens: totalCachedInputTokens,\n    inference_time_ms: totalInferenceTimeMs,\n  };\n}\n\nexport async function observe({\n  instruction,\n  domElements,\n  llmClient,\n  userProvidedInstructions,\n  logger,\n  logInferenceToFile = false,\n  supportedActions,\n}: {\n  instruction: string;\n  domElements: string;\n  llmClient: LLMClient;\n  userProvidedInstructions?: string;\n  logger: (message: LogLine) => void;\n  logInferenceToFile?: boolean;\n  supportedActions?: string[];\n}) {\n  const isGPT5 = llmClient.modelName.includes(\"gpt-5\"); // TODO: remove this as we update support for gpt-5 configuration options\n\n  const observeSchema = z.object({\n    elements: z\n      .array(\n        z.object({\n          elementId: z\n            .string()\n            .regex(/^\\d+-\\d+$/)\n            .describe(\n              \"the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.\",\n            ),\n          description: z\n            .string()\n            .describe(\n              \"a description of the accessible element and its purpose\",\n            ),\n          method: z\n            .enum(\n              // Use Object.values() for Zod v3 compatibility - z.enum() in v3 doesn't accept TypeScript enums directly\n              Object.values(SupportedUnderstudyAction) as unknown as readonly [\n                string,\n                ...string[],\n              ],\n            )\n            .describe(\n              `the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.`,\n            ),\n          arguments: z.array(\n            z\n              .string()\n              .describe(\n                \"the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.\",\n              ),\n          ),\n        }),\n      )\n      .describe(\"an array of accessible elements that match the instruction\"),\n  });\n\n  type ObserveResponse = z.infer<typeof observeSchema>;\n\n  const messages: ChatMessage[] = [\n    buildObserveSystemPrompt(userProvidedInstructions, supportedActions),\n    buildObserveUserMessage(instruction, domElements),\n  ];\n\n  let callTimestamp = \"\";\n  let callFile = \"\";\n  if (logInferenceToFile) {\n    const { fileName, timestamp } = writeTimestampedTxtFile(\n      `observe_summary`,\n      `observe_call`,\n      {\n        modelCall: \"observe\",\n        messages,\n      },\n    );\n    callFile = fileName;\n    callTimestamp = timestamp;\n  }\n\n  const start = Date.now();\n  const rawResponse = await llmClient.createChatCompletion<ObserveResponse>({\n    options: {\n      messages,\n      response_model: {\n        schema: observeSchema,\n        name: \"Observation\",\n      },\n      temperature: isGPT5 ? 1 : 0.1,\n      top_p: 1,\n      frequency_penalty: 0,\n      presence_penalty: 0,\n    },\n    logger,\n  });\n  const end = Date.now();\n  const usageTimeMs = end - start;\n\n  const { data: observeData, usage: observeUsage } = rawResponse;\n  const promptTokens = observeUsage?.prompt_tokens ?? 0;\n  const completionTokens = observeUsage?.completion_tokens ?? 0;\n  const reasoningTokens = observeUsage?.reasoning_tokens ?? 0;\n  const cachedInputTokens = observeUsage?.cached_input_tokens ?? 0;\n\n  let responseFile: string;\n  if (logInferenceToFile) {\n    const { fileName: responseFileName } = writeTimestampedTxtFile(\n      `observe_summary`,\n      `observe_response`,\n      {\n        modelResponse: \"observe\",\n        rawResponse: observeData,\n      },\n    );\n    responseFile = responseFileName;\n\n    appendSummary(\"observe\", {\n      [`observe_inference_type`]: \"observe\",\n      timestamp: callTimestamp,\n      LLM_input_file: callFile,\n      LLM_output_file: responseFile,\n      prompt_tokens: promptTokens,\n      completion_tokens: completionTokens,\n      reasoning_tokens: reasoningTokens,\n      cached_input_tokens: cachedInputTokens,\n      inference_time_ms: usageTimeMs,\n    });\n  }\n\n  const parsedElements =\n    observeData.elements?.map((el) => {\n      const base = {\n        elementId: el.elementId,\n        description: String(el.description),\n        method: String(el.method),\n        arguments: el.arguments,\n      };\n      return base;\n    }) ?? [];\n\n  return {\n    elements: parsedElements,\n    prompt_tokens: promptTokens,\n    completion_tokens: completionTokens,\n    reasoning_tokens: reasoningTokens,\n    cached_input_tokens: cachedInputTokens,\n    inference_time_ms: usageTimeMs,\n  };\n}\n\nexport async function act({\n  instruction,\n  domElements,\n  llmClient,\n  userProvidedInstructions,\n  logger,\n  logInferenceToFile = false,\n}: {\n  instruction: string;\n  domElements: string;\n  llmClient: LLMClient;\n  userProvidedInstructions?: string;\n  logger: (message: LogLine) => void;\n  logInferenceToFile?: boolean;\n}) {\n  const isGPT5 = llmClient.modelName.includes(\"gpt-5\"); // TODO: remove this as we update support for gpt-5 configuration options\n\n  const actSchema = z.object({\n    elementId: z\n      .string()\n      .regex(/^\\d+-\\d+$/)\n      .describe(\n        \"the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.\",\n      ),\n    description: z\n      .string()\n      .describe(\"a description of the accessible element and its purpose\"),\n    method: z\n      .enum(\n        // Use Object.values() for Zod v3 compatibility - z.enum() in v3 doesn't accept TypeScript enums directly\n        Object.values(SupportedUnderstudyAction) as unknown as readonly [\n          string,\n          ...string[],\n        ],\n      )\n      .describe(\n        \"the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.\",\n      ),\n    arguments: z.array(\n      z\n        .string()\n        .describe(\n          \"the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.\",\n        ),\n    ),\n    twoStep: z.boolean(),\n  });\n\n  type ActResponse = z.infer<typeof actSchema>;\n\n  const messages: ChatMessage[] = [\n    buildActSystemPrompt(userProvidedInstructions),\n    buildObserveUserMessage(instruction, domElements),\n  ];\n\n  let callTimestamp = \"\";\n  let callFile = \"\";\n  if (logInferenceToFile) {\n    const { fileName, timestamp } = writeTimestampedTxtFile(\n      `act_summary`,\n      `act_call`,\n      {\n        modelCall: \"act\",\n        messages,\n      },\n    );\n    callFile = fileName;\n    callTimestamp = timestamp;\n  }\n\n  const start = Date.now();\n  const rawResponse = await llmClient.createChatCompletion<ActResponse>({\n    options: {\n      messages,\n      response_model: {\n        schema: actSchema,\n        name: \"act\",\n      },\n      temperature: isGPT5 ? 1 : 0.1,\n      top_p: 1,\n      frequency_penalty: 0,\n      presence_penalty: 0,\n    },\n    logger,\n  });\n  const end = Date.now();\n  const usageTimeMs = end - start;\n\n  const { data: actData, usage: actUsage } = rawResponse;\n  const promptTokens = actUsage?.prompt_tokens ?? 0;\n  const completionTokens = actUsage?.completion_tokens ?? 0;\n  const reasoningTokens = actUsage?.reasoning_tokens ?? 0;\n  const cachedInputTokens = actUsage?.cached_input_tokens ?? 0;\n\n  let responseFile: string;\n  if (logInferenceToFile) {\n    const { fileName: responseFileName } = writeTimestampedTxtFile(\n      `act_summary`,\n      `act_response`,\n      {\n        modelResponse: \"act\",\n        rawResponse: actData,\n      },\n    );\n    responseFile = responseFileName;\n\n    appendSummary(\"act\", {\n      [`act_inference_type`]: \"act\",\n      timestamp: callTimestamp,\n      LLM_input_file: callFile,\n      LLM_output_file: responseFile,\n      prompt_tokens: promptTokens,\n      completion_tokens: completionTokens,\n      reasoning_tokens: reasoningTokens,\n      cached_input_tokens: cachedInputTokens,\n      inference_time_ms: usageTimeMs,\n    });\n  }\n\n  const parsedElement = {\n    elementId: actData.elementId,\n    description: String(actData.description),\n    method: String(actData.method),\n    arguments: actData.arguments,\n  };\n\n  return {\n    element: parsedElement,\n    prompt_tokens: promptTokens,\n    completion_tokens: completionTokens,\n    reasoning_tokens: reasoningTokens,\n    cached_input_tokens: cachedInputTokens,\n    inference_time_ms: usageTimeMs,\n    twoStep: actData.twoStep,\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/inferenceLogUtils.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\n\n/**\n * Create (or ensure) a parent directory named \"inference_summary\".\n */\nfunction ensureInferenceSummaryDir(): string {\n  const inferenceDir = path.join(process.cwd(), \"inference_summary\");\n  if (!fs.existsSync(inferenceDir)) {\n    fs.mkdirSync(inferenceDir, { recursive: true });\n  }\n  return inferenceDir;\n}\n\n/**\n * Appends a new entry to the act_summary.json file, then writes the file back out.\n */\nexport function appendSummary<T>(inferenceType: string, entry: T) {\n  const summaryPath = getSummaryJsonPath(inferenceType);\n  const arrayKey = `${inferenceType}_summary`;\n\n  const existingData = readSummaryFile<T>(inferenceType);\n  existingData[arrayKey].push(entry);\n\n  fs.writeFileSync(summaryPath, JSON.stringify(existingData, null, 2));\n}\n\n/** A simple timestamp utility for filenames. */\nfunction getTimestamp(): string {\n  return new Date()\n    .toISOString()\n    .replace(/[^0-9T]/g, \"\")\n    .replace(\"T\", \"_\");\n}\n\n/**\n * Writes `data` as JSON into a file in `directory`, using a prefix plus timestamp.\n * Returns both the file name and the timestamp used, so you can log them.\n */\nexport function writeTimestampedTxtFile(\n  directory: string,\n  prefix: string,\n  data: unknown,\n): { fileName: string; timestamp: string } {\n  const baseDir = ensureInferenceSummaryDir();\n\n  const subDir = path.join(baseDir, directory);\n  if (!fs.existsSync(subDir)) {\n    fs.mkdirSync(subDir, { recursive: true });\n  }\n\n  const timestamp = getTimestamp();\n  const fileName = `${timestamp}_${prefix}.txt`;\n  const filePath = path.join(subDir, fileName);\n\n  fs.writeFileSync(\n    filePath,\n    JSON.stringify(data, null, 2).replace(/\\\\n/g, \"\\n\"),\n  );\n\n  return { fileName, timestamp };\n}\n\n/**\n * Returns the path to the `<inferenceType>_summary.json` file.\n *\n * For example, if `inferenceType = \"act\"`, this will be:\n *   `./inference_summary/act_summary/act_summary.json`\n */\nfunction getSummaryJsonPath(inferenceType: string): string {\n  const baseDir = ensureInferenceSummaryDir();\n  const subDir = path.join(baseDir, `${inferenceType}_summary`);\n  if (!fs.existsSync(subDir)) {\n    fs.mkdirSync(subDir, { recursive: true });\n  }\n  return path.join(subDir, `${inferenceType}_summary.json`);\n}\n\n/**\n * Reads the `<inferenceType>_summary.json` file, returning an object\n * with the top-level array named `<inferenceType>_summary`, if it exists.\n *\n * E.g. if inferenceType is \"act\", we expect a shape like:\n * {\n *   \"act_summary\": [ ... ]\n * }\n *\n * If the file or array is missing, returns { \"<inferenceType>_summary\": [] }.\n */\nfunction readSummaryFile<T>(inferenceType: string): Record<string, T[]> {\n  const summaryPath = getSummaryJsonPath(inferenceType);\n\n  // The top-level array key, e.g. \"act_summary\", \"observe_summary\", \"extract_summary\"\n  const arrayKey = `${inferenceType}_summary`;\n\n  if (!fs.existsSync(summaryPath)) {\n    return { [arrayKey]: [] };\n  }\n\n  try {\n    const raw = fs.readFileSync(summaryPath, \"utf8\");\n    const parsed = JSON.parse(raw);\n    if (\n      parsed &&\n      typeof parsed === \"object\" &&\n      Array.isArray(parsed[arrayKey])\n    ) {\n      return parsed;\n    }\n  } catch {\n    // If we fail to parse for any reason, fall back to empty array\n  }\n  return { [arrayKey]: [] };\n}\n"
  },
  {
    "path": "packages/core/lib/logger.ts",
    "content": "import pino from \"pino\";\nimport { LogLine } from \"./v3/types/public/logs.js\";\n\n// Map our existing levels to Pino's standard levels\nconst levelMapping: Record<number, pino.Level> = {\n  0: \"error\", // Critical/important messages\n  1: \"info\", // Standard information\n  2: \"debug\", // Detailed debugging information\n};\n\n// Define configuration options\nexport interface LoggerOptions {\n  pretty?: boolean;\n  level?: pino.Level;\n  destination?: pino.DestinationStream;\n  usePino?: boolean; // Whether to use pino (default: true)\n}\n\n/**\n * Creates a configured Pino logger instance\n */\nexport function createLogger(options: LoggerOptions = {}) {\n  const loggerConfig: pino.LoggerOptions = {\n    level: options.level || \"info\",\n    base: undefined, // Don't include pid and hostname\n    browser: {\n      asObject: true,\n    },\n    // Disable worker threads to avoid issues in tests\n    transport: undefined,\n  };\n\n  // Add pretty printing for dev environments only if explicitly requested\n  // and not in a test environment\n  if (options.pretty && !isTestEnvironment()) {\n    try {\n      // Use require for dynamic import\n      const transport = {\n        transport: {\n          target: \"pino-pretty\",\n          options: {\n            colorize: true,\n            translateTime: \"SYS:standard\",\n            ignore: \"pid,hostname\",\n          },\n        },\n      };\n      Object.assign(loggerConfig, transport);\n    } catch {\n      console.warn(\n        \"pino-pretty not available, falling back to standard logging\",\n      );\n    }\n  }\n\n  return pino(loggerConfig, options.destination);\n}\n\n/**\n * Check if we're running in a test environment\n */\nfunction isTestEnvironment(): boolean {\n  return (\n    process.env.NODE_ENV === \"test\" ||\n    process.env.JEST_WORKER_ID !== undefined ||\n    process.env.PLAYWRIGHT_TEST_BASE_DIR !== undefined ||\n    // Check if we're in a CI environment\n    process.env.CI === \"true\"\n  );\n}\n\n/**\n * StagehandLogger class that wraps Pino for our specific needs\n *\n * LOGGING PRECEDENCE:\n *\n * Test environments:\n *   - External logger provided -> external logger only.\n *   - No external logger -> console fallback only (Pino disabled).\n *\n * Non-test environments:\n *   - usePino === true -> emit via Pino and also call the external logger when present.\n *   - usePino === false -> disable Pino; use the external logger when present, otherwise console fallback.\n *   - usePino === undefined -> prefer the external logger when present; otherwise use Pino.\n *\n * SHARED PINO OPTIMIZATION:\n * We maintain a single shared Pino instance when `usePino` is enabled.\n * This prevents spawning a new worker thread for every Stagehand instance\n * (which happens when `pino-pretty` transport is used), eliminating the\n * memory/RSS growth observed when many Stagehand objects are created and\n * disposed within the same process (e.g. a request-per-instance API).\n */\nexport class StagehandLogger {\n  /**\n   * Shared Pino logger instance across all StagehandLogger instances.\n   * First instance to enable Pino creates it, subsequent instances reuse it.\n   */\n  private static sharedPinoLogger: pino.Logger | null = null;\n\n  private logger?: pino.Logger;\n  private verbose: 0 | 1 | 2;\n  private externalLogger?: (logLine: LogLine) => void;\n  private usePino: boolean;\n  private isTest: boolean;\n\n  constructor(\n    options: LoggerOptions = {},\n    externalLogger?: (logLine: LogLine) => void,\n  ) {\n    this.isTest = isTestEnvironment();\n    this.externalLogger = externalLogger;\n\n    const externalProvided = typeof externalLogger === \"function\";\n    const explicitUsePino = options.usePino;\n\n    if (this.isTest) {\n      this.usePino = false;\n    } else if (explicitUsePino === true) {\n      this.usePino = true;\n    } else if (explicitUsePino === false) {\n      this.usePino = false;\n    } else {\n      this.usePino = !externalProvided;\n    }\n\n    if (this.usePino) {\n      // Re-use (or create) a single shared Pino logger instance\n      if (!StagehandLogger.sharedPinoLogger) {\n        StagehandLogger.sharedPinoLogger = createLogger(options);\n      }\n      this.logger = StagehandLogger.sharedPinoLogger;\n    }\n\n    this.verbose = 1; // Default verbosity level\n  }\n\n  /**\n   * Set the verbosity level\n   */\n  setVerbosity(level: 0 | 1 | 2) {\n    this.verbose = level;\n\n    if (this.usePino && this.logger) {\n      // Map our verbosity levels to Pino log levels\n      switch (level) {\n        case 0:\n          this.logger.level = \"error\";\n          break;\n        case 1:\n          this.logger.level = \"info\";\n          break;\n        case 2:\n          this.logger.level = \"debug\";\n          break;\n      }\n    }\n  }\n\n  /**\n   * Log a message using our LogLine format\n   */\n  log(logLine: LogLine): void {\n    // Skip logs above verbosity level\n    if ((logLine.level ?? 1) > this.verbose) {\n      return;\n    }\n\n    // For test environments WITHOUT an external logger OR for cases where Pino\n    // is disabled and no external logger is provided, fall back to console.* so\n    // users still see logs (non-colourised).\n    const shouldFallbackToConsole =\n      (!this.usePino && !this.externalLogger) ||\n      (this.isTest && !this.externalLogger);\n\n    if (shouldFallbackToConsole) {\n      const level = logLine.level ?? 1;\n      const ts = logLine.timestamp ?? new Date().toISOString();\n      const levelStr = level === 0 ? \"ERROR\" : level === 2 ? \"DEBUG\" : \"INFO\";\n\n      // Format like Pino: [timestamp] LEVEL: message\n      let output = `[${ts}] ${levelStr}: ${logLine.message}`;\n\n      // Add auxiliary data on separate indented lines (like Pino pretty format)\n      if (logLine.auxiliary) {\n        const formattedData = this.formatAuxiliaryData(logLine.auxiliary);\n        for (const [key, value] of Object.entries(formattedData)) {\n          let formattedValue: string;\n          if (typeof value === \"object\" && value !== null) {\n            // Pretty print objects with indentation\n            formattedValue = JSON.stringify(value, null, 2)\n              .split(\"\\n\")\n              .map((line, i) => (i === 0 ? line : `    ${line}`))\n              .join(\"\\n\");\n          } else {\n            formattedValue = String(value);\n          }\n          output += `\\n    ${key}: ${formattedValue}`;\n        }\n      }\n\n      switch (level) {\n        case 0:\n          console.error(output);\n          break;\n        case 1:\n          console.log(output);\n          break;\n        case 2:\n          console.debug(output);\n          break;\n      }\n\n      return; // already handled via console output, avoid duplicate logging\n    }\n\n    if (this.usePino && this.logger) {\n      // Determine the Pino log level\n      const pinoLevel = levelMapping[logLine.level ?? 1] || \"info\";\n\n      // Structure the log data\n      const logData = {\n        category: logLine.category,\n        timestamp: logLine.timestamp || new Date().toISOString(),\n        ...this.formatAuxiliaryData(logLine.auxiliary),\n      };\n\n      // Log through Pino with the appropriate level\n      if (pinoLevel === \"error\") {\n        this.logger.error(logData, logLine.message);\n      } else if (pinoLevel === \"info\") {\n        this.logger.info(logData, logLine.message);\n      } else if (pinoLevel === \"debug\") {\n        this.logger.debug(logData, logLine.message);\n      } else if (pinoLevel === \"warn\") {\n        this.logger.warn(logData, logLine.message);\n      } else if (pinoLevel === \"trace\") {\n        this.logger.trace(logData, logLine.message);\n      } else {\n        this.logger.info(logData, logLine.message);\n      }\n    }\n\n    // IMPORTANT: External logger receives logs ALWAYS when provided (takes precedence)\n    // This ensures user-provided loggers (e.g., EvalLogger, custom loggers) capture all logs\n    // regardless of Pino configuration. Pino is used for console output, external logger\n    // is used for programmatic log capture.\n    if (this.externalLogger) {\n      this.externalLogger(logLine);\n    }\n  }\n\n  /**\n   * Helper to format auxiliary data for structured logging\n   */\n  private formatAuxiliaryData(auxiliary?: LogLine[\"auxiliary\"]) {\n    if (!auxiliary) return {};\n\n    const formattedData: Record<string, unknown> = {};\n\n    for (const [key, { value, type }] of Object.entries(auxiliary)) {\n      let formattedValue: unknown;\n\n      // Convert values based on their type\n      switch (type) {\n        case \"integer\":\n          formattedValue = parseInt(value, 10);\n          break;\n        case \"float\":\n          formattedValue = parseFloat(value);\n          break;\n        case \"boolean\":\n          formattedValue = value === \"true\";\n          break;\n        case \"object\":\n          try {\n            formattedValue = JSON.parse(value);\n          } catch {\n            formattedValue = value;\n          }\n          break;\n        default:\n          formattedValue = value;\n      }\n\n      // Skip undefined values and empty objects/arrays\n      if (formattedValue === undefined) continue;\n      if (typeof formattedValue === \"object\" && formattedValue !== null) {\n        const isEmpty = Array.isArray(formattedValue)\n          ? formattedValue.length === 0\n          : Object.keys(formattedValue).length === 0;\n        if (isEmpty) continue;\n      }\n\n      formattedData[key] = formattedValue;\n    }\n\n    return formattedData;\n  }\n\n  /**\n   * Convenience methods for different log levels\n   */\n  error(message: string, data?: Record<string, unknown>): void {\n    this.log({\n      message,\n      level: 0,\n      auxiliary: this.convertToAuxiliary(data),\n    });\n  }\n\n  warn(message: string, data?: Record<string, unknown>): void {\n    this.log({\n      message,\n      level: 1,\n      category: \"warning\",\n      auxiliary: this.convertToAuxiliary(data),\n    });\n  }\n\n  info(message: string, data?: Record<string, unknown>): void {\n    this.log({\n      message,\n      level: 1,\n      auxiliary: this.convertToAuxiliary(data),\n    });\n  }\n\n  debug(message: string, data?: Record<string, unknown>): void {\n    this.log({\n      message,\n      level: 2,\n      auxiliary: this.convertToAuxiliary(data),\n    });\n  }\n\n  /**\n   * Convert a plain object to our auxiliary format\n   */\n  private convertToAuxiliary(\n    data?: Record<string, unknown>,\n  ): LogLine[\"auxiliary\"] {\n    if (!data) return undefined;\n\n    const auxiliary: LogLine[\"auxiliary\"] = {};\n\n    for (const [key, value] of Object.entries(data)) {\n      if (value === undefined) continue;\n\n      const type = typeof value;\n\n      auxiliary[key] = {\n        value: type === \"object\" ? JSON.stringify(value) : String(value),\n        type:\n          type === \"number\"\n            ? Number.isInteger(value)\n              ? \"integer\"\n              : \"float\"\n            : type === \"boolean\"\n              ? \"boolean\"\n              : type === \"object\"\n                ? \"object\"\n                : \"string\",\n      };\n    }\n\n    return auxiliary;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/modelUtils.ts",
    "content": "import { ClientOptions, ModelConfiguration } from \"./v3/types/public/model.js\";\nimport {\n  AVAILABLE_CUA_MODELS,\n  AvailableCuaModel,\n} from \"./v3/types/public/agent.js\";\n\n//useful when resolving a model from string or object formats we accept\nexport function extractModelName(\n  model?: string | { modelName: string; [key: string]: unknown },\n): string | undefined {\n  if (!model) return undefined;\n  return typeof model === \"string\" ? model : model.modelName;\n}\n\nexport function splitModelName(model: string): {\n  provider: string;\n  modelName: string;\n} {\n  const firstSlashIndex = model.indexOf(\"/\");\n  const provider = model.substring(0, firstSlashIndex);\n  const modelName = model.substring(firstSlashIndex + 1);\n  return { provider, modelName };\n}\n\nexport function resolveModel(model: string | ModelConfiguration): {\n  provider: string;\n  modelName: string;\n  clientOptions: ClientOptions;\n  isCua: boolean;\n} {\n  const modelString = extractModelName(model)!;\n  const clientOptions =\n    typeof model === \"string\"\n      ? {}\n      : (() => {\n          // eslint-disable-next-line @typescript-eslint/no-unused-vars\n          const { modelName: _, ...rest } = model;\n          return rest;\n        })();\n\n  // Check if provider is explicitly set in clientOptions\n  const hasExplicitProvider = clientOptions.provider !== undefined;\n\n  // If provider is explicitly set, don't split the model name - pass it through as-is\n  let provider: string;\n  let parsedModelName: string;\n\n  if (hasExplicitProvider) {\n    provider = clientOptions.provider as string;\n    parsedModelName = modelString; // Keep the full model name\n  } else {\n    // Parse the model string normally\n    const split = splitModelName(modelString);\n    provider = split.provider;\n    parsedModelName = split.modelName;\n  }\n\n  // Check if it's a CUA model\n  const isCua =\n    hasExplicitProvider ||\n    AVAILABLE_CUA_MODELS.includes(modelString as AvailableCuaModel);\n\n  return {\n    provider,\n    modelName: parsedModelName,\n    clientOptions,\n    isCua,\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/prompt.ts",
    "content": "import { ChatMessage } from \"./v3/llm/LLMClient.js\";\nimport type { Variables } from \"./v3/types/public/agent.js\";\n\nexport function buildUserInstructionsString(\n  userProvidedInstructions?: string,\n): string {\n  if (!userProvidedInstructions) {\n    return \"\";\n  }\n\n  return `\\n\\n# Custom Instructions Provided by the User\n    \nPlease keep the user's instructions in mind when performing actions. If the user's instructions are not relevant to the current task, ignore them.\n\nUser Instructions:\n${userProvidedInstructions}`;\n}\n\n// extract\nexport function buildExtractSystemPrompt(\n  isUsingPrintExtractedDataTool: boolean = false,\n  userProvidedInstructions?: string,\n): ChatMessage {\n  const baseContent = `You are extracting content on behalf of a user.\n  If a user asks you to extract a 'list' of information, or 'all' information, \n  YOU MUST EXTRACT ALL OF THE INFORMATION THAT THE USER REQUESTS.\n   \n  You will be given:\n1. An instruction\n2. `;\n\n  const contentDetail = `A list of DOM elements to extract from.`;\n\n  const instructions = `\nPrint the exact text from the DOM elements with all symbols, characters, and endlines as is.\nPrint null or an empty string if no new information is found.\n  `.trim();\n\n  const toolInstructions = isUsingPrintExtractedDataTool\n    ? `\nONLY print the content using the print_extracted_data tool provided.\nONLY print the content using the print_extracted_data tool provided.\n  `.trim()\n    : \"\";\n\n  const additionalInstructions =\n    \"If a user is attempting to extract links or URLs, you MUST respond with ONLY the IDs of the link elements. \\n\" +\n    \"Do not attempt to extract links directly from the text unless absolutely necessary. \";\n\n  const userInstructions = buildUserInstructionsString(\n    userProvidedInstructions,\n  );\n\n  const content =\n    `${baseContent}${contentDetail}\\n\\n${instructions}\\n${toolInstructions}${\n      additionalInstructions ? `\\n\\n${additionalInstructions}` : \"\"\n    }${userInstructions ? `\\n\\n${userInstructions}` : \"\"}`.replace(/\\s+/g, \" \");\n\n  return {\n    role: \"system\",\n    content,\n  };\n}\n\nexport function buildExtractUserPrompt(\n  instruction: string,\n  domElements: string,\n  isUsingPrintExtractedDataTool: boolean = false,\n): ChatMessage {\n  let content = `Instruction: ${instruction}\nDOM: ${domElements}`;\n\n  if (isUsingPrintExtractedDataTool) {\n    content += `\nONLY print the content using the print_extracted_data tool provided.\nONLY print the content using the print_extracted_data tool provided.`;\n  }\n\n  return {\n    role: \"user\",\n    content,\n  };\n}\n\nconst metadataSystemPrompt = `You are an AI assistant tasked with evaluating the progress and completion status of an extraction task.\nAnalyze the extraction response and determine if the task is completed or if more information is needed.\nStrictly abide by the following criteria:\n1. Once the instruction has been satisfied by the current extraction response, ALWAYS set completion status to true and stop processing, regardless of remaining chunks.\n2. Only set completion status to false if BOTH of these conditions are true:\n   - The instruction has not been satisfied yet\n   - There are still chunks left to process (chunksTotal > chunksSeen)`;\n\nexport function buildMetadataSystemPrompt(): ChatMessage {\n  return {\n    role: \"system\",\n    content: metadataSystemPrompt,\n  };\n}\n\nexport function buildMetadataPrompt(\n  instruction: string,\n  extractionResponse: object,\n): ChatMessage {\n  return {\n    role: \"user\",\n    content: `Instruction: ${instruction}\nExtracted content: ${JSON.stringify(extractionResponse, null, 2)}`,\n  };\n}\n\n// observe\nexport function buildObserveSystemPrompt(\n  userProvidedInstructions?: string,\n  supportedActions?: string[],\n): ChatMessage {\n  const actionsString = supportedActions?.length\n    ? `\\n\\nSupported actions: ${supportedActions.join(\", \")}`\n    : \"\";\n\n  const observeSystemPrompt = `\nYou are helping the user automate the browser by finding elements based on what the user wants to observe in the page.\n\nYou will be given:\n1. a instruction of elements to observe\n2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree.\n\nReturn an array of elements that match the instruction if they exist, otherwise return an empty array.\nWhen returning elements, include the appropriate method from the supported actions list.${actionsString}. When choosing non-left click actions, provide right or middle as the argument.`;\n  const content = observeSystemPrompt.replace(/\\s+/g, \" \");\n\n  return {\n    role: \"system\",\n    content: [content, buildUserInstructionsString(userProvidedInstructions)]\n      .filter(Boolean)\n      .join(\"\\n\\n\"),\n  };\n}\n\nexport function buildObserveUserMessage(\n  instruction: string,\n  domElements: string,\n): ChatMessage {\n  return {\n    role: \"user\",\n    content: `instruction: ${instruction}\nAccessibility Tree: \\n${domElements}\\n`,\n  };\n}\n\nexport function buildActSystemPrompt(\n  userProvidedInstructions?: string,\n): ChatMessage {\n  const actSystemPrompt = `\nYou are helping the user automate the browser by finding elements based on what action the user wants to take on the page\n\nYou will be given:\n1. a user defined instruction about what action to take\n2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree.\n\nReturn the element that matches the instruction if it exists. Otherwise, return an empty object.`;\n  const content = actSystemPrompt.replace(/\\s+/g, \" \");\n\n  return {\n    role: \"system\",\n    content: [content, buildUserInstructionsString(userProvidedInstructions)]\n      .filter(Boolean)\n      .join(\"\\n\\n\"),\n  };\n}\n\nexport function buildActPrompt(\n  action: string,\n  supportedActions: string[],\n  variables?: Variables,\n): string {\n  // Base instruction\n  let instruction = `Find the most relevant element to perform an action on given the following action: ${action}.  \n  IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n  \n  General Instructions: \n    Provide an action for this element such as ${supportedActions.join(\", \")}. Remember that to users, buttons and links look the same in most cases.\n    When choosing non-left click actions, provide right or middle as the argument\n    If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n    ONLY return one action. If multiple actions are relevant, return the most relevant one. \n    If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n    If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n    If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n  \n  Dropdown Specific Instructions:\n    For interacting with dropdowns, there are two specific cases that you need to handle. \n    \n    CASE 1: the element is a 'select' element. \n      - choose the selectOptionFromDropdown method,\n      - set the argument to the exact text of the option that should be selected,\n      - set twoStep to false.\n    CASE 2: the element is NOT a 'select' element:\n      - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n        - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable.  \n        - choose the 'click' method\n        - set twoStep to true.\n  `;\n\n  // Add variable names (not values) to the instruction if any\n  if (variables && Object.keys(variables).length > 0) {\n    const variableNames = Object.keys(variables)\n      .map((key) => `%${key}%`)\n      .join(\", \");\n    const variablesPrompt = `The following variables are available to use in the action: ${variableNames}. Fill the argument variables with the variable name.`;\n    instruction += ` ${variablesPrompt}`;\n  }\n\n  return instruction;\n}\n\nexport function buildStepTwoPrompt(\n  originalUserAction: string,\n  previousAction: string,\n  supportedActions: string[],\n  variables?: Variables,\n): string {\n  // Base instruction\n  let instruction = `\n  The original user action was: ${originalUserAction}.\n  You have just taken the following action which completed step 1 of 2: ${previousAction}.\n  \n  Now, you must find the most relevant element to perform an action on in order to complete step 2 of 2. \n  \n  General Instructions: \n  Provide an action for this element such as ${supportedActions.join(\", \")}. Remember that to users, buttons and links look the same in most cases.\n  If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n  ONLY return one action. If multiple actions are relevant, return the most relevant one. \n  If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n  If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n  If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n  `;\n\n  // Add variable names (not values) to the instruction if any\n  if (variables && Object.keys(variables).length > 0) {\n    const variableNames = Object.keys(variables)\n      .map((key) => `%${key}%`)\n      .join(\", \");\n    const variablesPrompt = `The following variables are available to use in the action: ${variableNames}. Fill the argument variables with the variable name.`;\n    instruction += ` ${variablesPrompt}`;\n  }\n\n  return instruction;\n}\n\nexport function buildOperatorSystemPrompt(goal: string): ChatMessage {\n  return {\n    role: \"system\",\n    content: `You are a general-purpose agent whose job is to accomplish the user's goal across multiple model calls by running actions on the page.\n\nYou will be given a goal and a list of steps that have been taken so far. Your job is to determine if either the user's goal has been completed or if there are still steps that need to be taken.\n\n# Your current goal\n${goal}\n\n# CRITICAL: You MUST use the provided tools to take actions. Do not just describe what you want to do - actually call the appropriate tools.\n\n# Available tools and when to use them:\n- \\`act\\`: Use this to interact with the page (click, type, navigate, etc.)\n- \\`extract\\`: Use this to get information from the page\n- \\`goto\\`: Use this to navigate to a specific URL\n- \\`wait\\`: Use this to wait for a period of time\n- \\`navback\\`: Use this to go back to the previous page\n- \\`refresh\\`: Use this to refresh the current page\n- \\`close\\`: Use this ONLY when the task is complete or cannot be achieved\n- External tools: Use any additional tools (like search tools) as needed for your goal\n\n# Important guidelines\n1. ALWAYS use tools - never just provide text responses about what you plan to do\n2. Break down complex actions into individual atomic steps\n3. For \\`act\\` commands, use only one action at a time, such as:\n   - Single click on a specific element\n   - Type into a single input field\n   - Select a single option\n4. Avoid combining multiple actions in one instruction\n5. If multiple actions are needed, they should be separate steps\n6. Only use \\`close\\` when the task is genuinely complete or impossible to achieve`,\n  };\n}\n\nexport function buildCuaDefaultSystemPrompt(): string {\n  return `You are a helpful assistant that can use a web browser.\\nDo not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toISOString().split(\"T\")[0]}.`;\n}\n\nexport function buildGoogleCUASystemPrompt(): ChatMessage {\n  return {\n    role: \"system\",\n    content: `You are a general-purpose browser agent whose job is to accomplish the user's goal.\nToday's date is ${new Date().toISOString().split(\"T\")[0]}.\nYou have access to a search tool; however, in most cases you should operate within the page/url the user has provided. ONLY use the search tool if you're stuck or the task is impossible to complete within the current page.\nYou will be given a goal and a list of steps that have been taken so far. Avoid requesting the user for input as much as possible. Good luck!\n`,\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/utils.ts",
    "content": "import { ZodSchemaValidationError } from \"./v3/types/public/sdkErrors.js\";\nimport { Schema, Type } from \"@google/genai\";\nimport { z, ZodTypeAny } from \"zod\";\nimport z3 from \"zod/v3\";\nimport { LogLine } from \"./v3/types/public/logs.js\";\nimport { ModelProvider } from \"./v3/types/public/model.js\";\nimport { ZodPathSegments } from \"./v3/types/private/internal.js\";\nimport type { StagehandZodSchema } from \"./v3/zodCompat.js\";\nimport { isZod4Schema } from \"./v3/zodCompat.js\";\n\nconst ID_PATTERN = /^\\d+-\\d+$/;\n\nconst zFactories = {\n  v4: z,\n  v3: z3 as unknown as typeof z,\n};\n\nexport function getZFactory(schema: StagehandZodSchema): typeof z {\n  return isZod4Schema(schema) ? zFactories.v4 : zFactories.v3;\n}\n\nconst TYPE_NAME_MAP: Record<string, string> = {\n  ZodString: \"string\",\n  string: \"string\",\n  ZodNumber: \"number\",\n  number: \"number\",\n  ZodBoolean: \"boolean\",\n  boolean: \"boolean\",\n  ZodObject: \"object\",\n  object: \"object\",\n  ZodArray: \"array\",\n  array: \"array\",\n  ZodUnion: \"union\",\n  union: \"union\",\n  ZodIntersection: \"intersection\",\n  intersection: \"intersection\",\n  ZodOptional: \"optional\",\n  optional: \"optional\",\n  ZodNullable: \"nullable\",\n  nullable: \"nullable\",\n  ZodLiteral: \"literal\",\n  literal: \"literal\",\n  ZodEnum: \"enum\",\n  enum: \"enum\",\n  ZodDefault: \"default\",\n  default: \"default\",\n  ZodEffects: \"effects\",\n  effects: \"effects\",\n  pipe: \"pipe\",\n};\n\nfunction getZ4Def(schema: StagehandZodSchema) {\n  return (schema as SchemaInternals)._zod?.def as\n    | Record<string, unknown>\n    | undefined;\n}\n\nfunction getZ4Bag(schema: StagehandZodSchema) {\n  return (schema as SchemaInternals)._zod?.bag as\n    | Record<string, unknown>\n    | undefined;\n}\n\nfunction getZ3Def(schema: StagehandZodSchema) {\n  return (schema as SchemaInternals)._def as\n    | Record<string, unknown>\n    | undefined;\n}\n\nfunction getObjectShape(\n  schema: StagehandZodSchema,\n): Record<string, StagehandZodSchema> | undefined {\n  const z4Shape = getZ4Def(schema)?.shape as\n    | Record<string, StagehandZodSchema>\n    | undefined;\n  if (z4Shape) {\n    return z4Shape;\n  }\n\n  const z3Shape = getZ3Def(schema)?.shape;\n  if (!z3Shape) {\n    return undefined;\n  }\n\n  if (typeof z3Shape === \"function\") {\n    return (z3Shape as () => Record<string, StagehandZodSchema>)();\n  }\n\n  return z3Shape as Record<string, StagehandZodSchema>;\n}\n\nfunction getArrayElement(\n  schema: StagehandZodSchema,\n): StagehandZodSchema | undefined {\n  return (getZ4Def(schema)?.element ?? getZ3Def(schema)?.type) as\n    | StagehandZodSchema\n    | undefined;\n}\n\nfunction getInnerType(\n  schema: StagehandZodSchema,\n): StagehandZodSchema | undefined {\n  return (getZ4Def(schema)?.innerType ?? getZ3Def(schema)?.innerType) as\n    | StagehandZodSchema\n    | undefined;\n}\n\nfunction getUnionOptions(\n  schema: StagehandZodSchema,\n): StagehandZodSchema[] | undefined {\n  const z4Options = getZ4Def(schema)?.options;\n  if (Array.isArray(z4Options)) {\n    return z4Options as StagehandZodSchema[];\n  }\n  const z3Options = getZ3Def(schema)?.options;\n  return Array.isArray(z3Options)\n    ? (z3Options as StagehandZodSchema[])\n    : undefined;\n}\n\nfunction getIntersectionSides(schema: StagehandZodSchema): {\n  left?: StagehandZodSchema;\n  right?: StagehandZodSchema;\n} {\n  const z4Def = getZ4Def(schema);\n  if (z4Def?.left || z4Def?.right) {\n    return {\n      left: z4Def?.left as StagehandZodSchema | undefined,\n      right: z4Def?.right as StagehandZodSchema | undefined,\n    };\n  }\n  const z3Def = getZ3Def(schema);\n  return {\n    left: z3Def?.left as StagehandZodSchema | undefined,\n    right: z3Def?.right as StagehandZodSchema | undefined,\n  };\n}\n\nfunction getEnumValues(schema: StagehandZodSchema): string[] | undefined {\n  const z4Entries = getZ4Def(schema)?.entries;\n  if (z4Entries && typeof z4Entries === \"object\") {\n    return Object.values(z4Entries as Record<string, string>);\n  }\n  const z3Values = getZ3Def(schema)?.values;\n  return Array.isArray(z3Values) ? (z3Values as string[]) : undefined;\n}\n\nfunction getLiteralValues(schema: StagehandZodSchema): unknown[] {\n  const z4Values = getZ4Def(schema)?.values;\n  if (Array.isArray(z4Values)) {\n    return z4Values as unknown[];\n  }\n  const value = getZ3Def(schema)?.value;\n  return typeof value !== \"undefined\" ? [value] : [];\n}\n\nfunction getStringChecks(schema: StagehandZodSchema): unknown[] {\n  const z4Checks = getZ4Def(schema)?.checks;\n  if (Array.isArray(z4Checks)) {\n    return z4Checks;\n  }\n  const z3Checks = getZ3Def(schema)?.checks;\n  return Array.isArray(z3Checks) ? z3Checks : [];\n}\n\nfunction getStringFormat(schema: StagehandZodSchema): string | undefined {\n  const bagFormat = getZ4Bag(schema)?.format;\n  if (typeof bagFormat === \"string\") {\n    return bagFormat;\n  }\n  const z4Format = getZ4Def(schema)?.format;\n  if (typeof z4Format === \"string\") {\n    return z4Format;\n  }\n  const z3Format = getZ3Def(schema)?.format;\n  return typeof z3Format === \"string\" ? z3Format : undefined;\n}\n\nfunction getPipeEndpoints(schema: StagehandZodSchema): {\n  in?: StagehandZodSchema;\n  out?: StagehandZodSchema;\n} {\n  const z4Def = getZ4Def(schema);\n  if (z4Def?.in || z4Def?.out) {\n    return {\n      in: z4Def?.in as StagehandZodSchema | undefined,\n      out: z4Def?.out as StagehandZodSchema | undefined,\n    };\n  }\n  return {};\n}\n\nfunction getEffectsBaseSchema(\n  schema: StagehandZodSchema,\n): StagehandZodSchema | undefined {\n  return getZ3Def(schema)?.schema as StagehandZodSchema | undefined;\n}\n\ntype SchemaInternals = {\n  _zod?: { def?: Record<string, unknown>; bag?: Record<string, unknown> };\n  _def?: Record<string, unknown>;\n};\n\nexport function validateZodSchema(schema: StagehandZodSchema, data: unknown) {\n  const result = schema.safeParse(data);\n\n  if (result.success) {\n    return true;\n  }\n  throw new ZodSchemaValidationError(data, result.error.format());\n}\n\n/**\n * Detects if the code is running in the Bun runtime environment.\n * @returns {boolean} True if running in Bun, false otherwise.\n */\nexport function isRunningInBun(): boolean {\n  return (\n    typeof process !== \"undefined\" &&\n    typeof process.versions !== \"undefined\" &&\n    \"bun\" in process.versions\n  );\n}\n\n/*\n * Helper functions for converting between Gemini and Zod schemas\n */\nfunction decorateGeminiSchema(\n  geminiSchema: Schema,\n  zodSchema: z.ZodTypeAny,\n): Schema {\n  if (geminiSchema.nullable === undefined) {\n    geminiSchema.nullable = zodSchema.isOptional();\n  }\n\n  if (zodSchema.description) {\n    geminiSchema.description = zodSchema.description;\n  }\n\n  return geminiSchema;\n}\n\nexport function toGeminiSchema(zodSchema: StagehandZodSchema): Schema {\n  const normalizedSchema = zodSchema as z.ZodTypeAny;\n  const zodType = getZodType(zodSchema);\n  switch (zodType) {\n    case \"array\": {\n      const element = getArrayElement(zodSchema) ?? z.any();\n      return decorateGeminiSchema(\n        {\n          type: Type.ARRAY,\n          items: toGeminiSchema(element),\n        },\n        normalizedSchema,\n      );\n    }\n    case \"object\": {\n      const properties: Record<string, Schema> = {};\n      const required: string[] = [];\n\n      const shape = getObjectShape(zodSchema);\n      if (shape) {\n        Object.entries(shape).forEach(\n          ([key, value]: [string, StagehandZodSchema]) => {\n            properties[key] = toGeminiSchema(value);\n            if (getZodType(value) !== \"optional\") {\n              required.push(key);\n            }\n          },\n        );\n      }\n\n      return decorateGeminiSchema(\n        {\n          type: Type.OBJECT,\n          properties,\n          required: required.length > 0 ? required : undefined,\n        },\n        normalizedSchema,\n      );\n    }\n    case \"string\":\n      return decorateGeminiSchema(\n        {\n          type: Type.STRING,\n        },\n        normalizedSchema,\n      );\n    case \"number\":\n      return decorateGeminiSchema(\n        {\n          type: Type.NUMBER,\n        },\n        normalizedSchema,\n      );\n    case \"boolean\":\n      return decorateGeminiSchema(\n        {\n          type: Type.BOOLEAN,\n        },\n        normalizedSchema,\n      );\n    case \"enum\": {\n      const values = getEnumValues(zodSchema);\n      return decorateGeminiSchema(\n        {\n          type: Type.STRING,\n          enum: values,\n        },\n        normalizedSchema,\n      );\n    }\n    case \"default\":\n    case \"nullable\":\n    case \"optional\": {\n      const innerType = getInnerType(zodSchema) ?? z.any();\n      const innerSchema = toGeminiSchema(innerType);\n      return decorateGeminiSchema(\n        {\n          ...innerSchema,\n          nullable: true,\n        },\n        normalizedSchema,\n      );\n    }\n    case \"literal\": {\n      const values = getLiteralValues(zodSchema);\n      return decorateGeminiSchema(\n        {\n          type: Type.STRING,\n          enum: values as string[],\n        },\n        normalizedSchema,\n      );\n    }\n    case \"pipe\": {\n      const endpoints = getPipeEndpoints(zodSchema);\n      if (endpoints.in) {\n        return toGeminiSchema(endpoints.in);\n      }\n      return decorateGeminiSchema(\n        {\n          type: Type.STRING,\n        },\n        normalizedSchema,\n      );\n    }\n    // Standalone transforms and any unknown types fall through to default\n    default:\n      return decorateGeminiSchema(\n        {\n          type: Type.STRING,\n        },\n        normalizedSchema,\n      );\n  }\n}\n\n// Helper function to check the type of Zod schema\nexport function getZodType(schema: StagehandZodSchema): string {\n  const schemaWithDef = schema as SchemaInternals & {\n    _zod?: { def?: { type?: string } };\n  };\n  const rawType =\n    (schemaWithDef._zod?.def?.type as string | undefined) ??\n    (schemaWithDef._def?.typeName as string | undefined) ??\n    (schemaWithDef._def?.type as string | undefined);\n\n  if (!rawType) {\n    return \"unknown\";\n  }\n\n  return TYPE_NAME_MAP[rawType] ?? rawType;\n}\n\n/**\n * Recursively traverses a given Zod schema, scanning for any fields of type `z.string().url()`.\n * For each such field, it replaces the `z.string().url()` with `z.number()`.\n *\n * This function is used internally by higher-level utilities (e.g., transforming entire object schemas)\n * and handles nested objects, arrays, unions, intersections, optionals.\n *\n * @param schema - The Zod schema to transform.\n * @param currentPath - An array of string/number keys representing the current schema path (used internally for recursion).\n * @returns A two-element tuple:\n *   1. The updated Zod schema, with any `.url()` fields replaced by `z.number()`.\n *   2. An array of {@link ZodPathSegments} objects representing each replaced field, including the path segments.\n */\nexport function transformSchema(\n  schema: StagehandZodSchema,\n  currentPath: Array<string | number>,\n): [StagehandZodSchema, ZodPathSegments[]] {\n  if (isKind(schema, \"string\")) {\n    const checks = getStringChecks(schema);\n    const format = getStringFormat(schema);\n    const hasUrlCheck =\n      checks.some((check) => {\n        const candidate = check as {\n          kind?: string;\n          format?: string;\n          _zod?: { def?: { check?: string; format?: string } };\n        };\n        return (\n          candidate.kind === \"url\" ||\n          candidate.format === \"url\" ||\n          candidate._zod?.def?.check === \"url\" ||\n          candidate._zod?.def?.format === \"url\"\n        );\n      }) || format === \"url\";\n\n    if (hasUrlCheck) {\n      return [makeIdStringSchema(schema), [{ segments: [] }]];\n    }\n    return [schema, []];\n  }\n\n  if (isKind(schema, \"object\")) {\n    const shape = getObjectShape(schema);\n    if (!shape) {\n      return [schema, []];\n    }\n    const newShape: Record<string, StagehandZodSchema> = {};\n    const urlPaths: ZodPathSegments[] = [];\n    let changed = false;\n\n    for (const key of Object.keys(shape)) {\n      const child = shape[key];\n      const [transformedChild, childPaths] = transformSchema(child, [\n        ...currentPath,\n        key,\n      ]);\n      if (transformedChild !== child) {\n        changed = true;\n      }\n      newShape[key] = transformedChild;\n      childPaths.forEach((cp) => {\n        urlPaths.push({ segments: [key, ...cp.segments] });\n      });\n    }\n\n    if (changed) {\n      const factory = getZFactory(schema);\n      return [\n        factory.object(newShape as Record<string, z.ZodTypeAny>),\n        urlPaths,\n      ];\n    }\n    return [schema, urlPaths];\n  }\n\n  if (isKind(schema, \"array\")) {\n    const itemType = getArrayElement(schema);\n    if (!itemType) {\n      return [schema, []];\n    }\n    const [transformedItem, childPaths] = transformSchema(itemType, [\n      ...currentPath,\n      \"*\",\n    ]);\n    const arrayPaths: ZodPathSegments[] = childPaths.map((cp) => ({\n      segments: [\"*\", ...cp.segments],\n    }));\n    if (transformedItem !== itemType) {\n      const factory = getZFactory(schema);\n      return [\n        factory.array(transformedItem as unknown as z.ZodTypeAny),\n        arrayPaths,\n      ];\n    }\n    return [schema, arrayPaths];\n  }\n\n  if (isKind(schema, \"union\")) {\n    const unionOptions = getUnionOptions(schema);\n    if (!unionOptions || unionOptions.length === 0) {\n      return [schema, []];\n    }\n    const newOptions: StagehandZodSchema[] = [];\n    let changed = false;\n    let allPaths: ZodPathSegments[] = [];\n\n    unionOptions.forEach((option, idx) => {\n      const [newOption, childPaths] = transformSchema(option, [\n        ...currentPath,\n        `union_${idx}`,\n      ]);\n      if (newOption !== option) {\n        changed = true;\n      }\n      newOptions.push(newOption);\n      allPaths = [...allPaths, ...childPaths];\n    });\n\n    if (changed) {\n      const factory = getZFactory(schema);\n      return [\n        factory.union(\n          newOptions as unknown as [\n            z.ZodTypeAny,\n            z.ZodTypeAny,\n            ...z.ZodTypeAny[],\n          ],\n        ),\n        allPaths,\n      ];\n    }\n    return [schema, allPaths];\n  }\n\n  if (isKind(schema, \"intersection\")) {\n    const { left, right } = getIntersectionSides(schema);\n    if (!left || !right) {\n      return [schema, []];\n    }\n    const [newLeft, leftPaths] = transformSchema(left, [\n      ...currentPath,\n      \"intersection_left\",\n    ]);\n    const [newRight, rightPaths] = transformSchema(right, [\n      ...currentPath,\n      \"intersection_right\",\n    ]);\n    const changed = newLeft !== left || newRight !== right;\n    const allPaths = [...leftPaths, ...rightPaths];\n    if (changed) {\n      const factory = getZFactory(schema);\n      return [\n        factory.intersection(\n          newLeft as unknown as z.ZodTypeAny,\n          newRight as unknown as z.ZodTypeAny,\n        ),\n        allPaths,\n      ];\n    }\n    return [schema, allPaths];\n  }\n\n  if (isKind(schema, \"optional\")) {\n    const innerType = getInnerType(schema);\n    if (!innerType) {\n      return [schema, []];\n    }\n    const [inner, innerPaths] = transformSchema(innerType, currentPath);\n    if (inner !== innerType) {\n      return [\n        (inner as z.ZodTypeAny).optional() as unknown as StagehandZodSchema,\n        innerPaths,\n      ];\n    }\n    return [schema, innerPaths];\n  }\n\n  if (isKind(schema, \"nullable\")) {\n    const innerType = getInnerType(schema);\n    if (!innerType) {\n      return [schema, []];\n    }\n    const [inner, innerPaths] = transformSchema(innerType, currentPath);\n    if (inner !== innerType) {\n      return [\n        (inner as z.ZodTypeAny).nullable() as unknown as StagehandZodSchema,\n        innerPaths,\n      ];\n    }\n    return [schema, innerPaths];\n  }\n\n  if (isKind(schema, \"pipe\") && isZod4Schema(schema)) {\n    const { in: inSchema, out: outSchema } = getPipeEndpoints(schema);\n    if (!inSchema || !outSchema) {\n      return [schema, []];\n    }\n\n    const [newIn, inPaths] = transformSchema(inSchema, currentPath);\n    const [newOut, outPaths] = transformSchema(outSchema, currentPath);\n    const allPaths = [...inPaths, ...outPaths];\n\n    if (newIn !== inSchema || newOut !== outSchema) {\n      const result = z.pipe(\n        newIn as unknown as z.ZodTypeAny,\n        newOut as unknown as z.ZodTypeAny,\n      ) as StagehandZodSchema;\n      return [result, allPaths];\n    }\n    return [schema, allPaths];\n  }\n\n  if (isKind(schema, \"effects\")) {\n    const baseSchema = getEffectsBaseSchema(schema);\n    if (!baseSchema) {\n      return [schema, []];\n    }\n    return transformSchema(baseSchema, currentPath);\n  }\n\n  return [schema, []];\n}\n\n/**\n * Once we get the final extracted object that has numeric IDs in place of URLs,\n * use `injectUrls` to walk the object and replace numeric IDs\n * with the real URL strings from idToUrlMapping. The `path` may include `*`\n * for array indices (indicating \"all items in the array\").\n */\nexport function injectUrls(\n  obj: unknown,\n  path: Array<string | number>,\n  idToUrlMapping: Record<string, string>,\n): void {\n  if (path.length === 0) return;\n  const toId = (value: unknown): string | undefined => {\n    if (typeof value === \"number\") {\n      return String(value);\n    }\n    if (typeof value === \"string\" && ID_PATTERN.test(value)) {\n      return value;\n    }\n    return undefined;\n  };\n  const [key, ...rest] = path;\n\n  if (key === \"*\") {\n    if (Array.isArray(obj)) {\n      if (rest.length === 0) {\n        for (let i = 0; i < obj.length; i += 1) {\n          const id = toId(obj[i]);\n          if (id !== undefined) {\n            obj[i] = idToUrlMapping[id] ?? \"\";\n          }\n        }\n      } else {\n        for (const item of obj) injectUrls(item, rest, idToUrlMapping);\n      }\n    }\n    return;\n  }\n\n  if (obj && typeof obj === \"object\") {\n    const record = obj as Record<string | number, unknown>;\n    if (path.length === 1) {\n      const fieldValue = record[key];\n      const id = toId(fieldValue);\n      if (id !== undefined) {\n        record[key] = idToUrlMapping[id] ?? \"\";\n      }\n    } else {\n      injectUrls(record[key], rest, idToUrlMapping);\n    }\n  }\n}\n\n// Helper to check if a schema is of a specific type\nfunction isKind(s: StagehandZodSchema, kind: string): boolean {\n  try {\n    return getZodType(s) === kind;\n  } catch {\n    return false;\n  }\n}\n\nfunction makeIdStringSchema(orig: StagehandZodSchema): StagehandZodSchema {\n  const userDesc =\n    (orig as unknown as { description?: string }).description ?? \"\";\n\n  const base =\n    \"This field must be the element-ID in the form 'frameId-backendId' \" +\n    '(e.g. \"0-432\").';\n  const composed =\n    userDesc.trim().length > 0\n      ? `${base} that follows this user-defined description: ${userDesc}`\n      : base;\n\n  const factory = getZFactory(orig);\n  return factory.string().regex(ID_PATTERN).describe(composed);\n}\n\n/**\n * Mapping from LLM provider names to their corresponding environment variable names for API keys.\n */\nexport const providerEnvVarMap: Partial<\n  Record<ModelProvider | string, string | Array<string>>\n> = {\n  openai: \"OPENAI_API_KEY\",\n  anthropic: \"ANTHROPIC_API_KEY\",\n  google: [\"GEMINI_API_KEY\", \"GOOGLE_GENERATIVE_AI_API_KEY\", \"GOOGLE_API_KEY\"],\n  vertex: \"GOOGLE_VERTEX_AI_API_KEY\",\n  groq: \"GROQ_API_KEY\",\n  cerebras: \"CEREBRAS_API_KEY\",\n  togetherai: \"TOGETHER_AI_API_KEY\",\n  mistral: \"MISTRAL_API_KEY\",\n  deepseek: \"DEEPSEEK_API_KEY\",\n  perplexity: \"PERPLEXITY_API_KEY\",\n  azure: \"AZURE_API_KEY\",\n  xai: \"XAI_API_KEY\",\n  google_legacy: \"GOOGLE_API_KEY\",\n};\n\nconst providersWithoutApiKey = new Set([\"bedrock\", \"ollama\"]);\n\n/**\n * Loads an API key for a provider, checking environment variables.\n * @param provider The name of the provider (e.g., 'openai', 'anthropic')\n * @param logger Optional logger for info/error messages\n * @returns The API key if found, undefined otherwise\n */\nexport function loadApiKeyFromEnv(\n  provider: string | undefined,\n  logger: (logLine: LogLine) => void,\n): string | undefined {\n  if (!provider) {\n    return undefined;\n  }\n\n  const envVarName = providerEnvVarMap[provider];\n  if (!envVarName) {\n    if (!providersWithoutApiKey.has(provider)) {\n      logger({\n        category: \"init\",\n        message: `No known environment variable for provider '${provider}'`,\n        level: 0,\n      });\n    }\n    return undefined;\n  }\n\n  const apiKeyFromEnv = Array.isArray(envVarName)\n    ? envVarName\n        .map((name) => process.env[name])\n        .find((key) => key && key.length > 0)\n    : process.env[envVarName as string];\n  if (typeof apiKeyFromEnv === \"string\" && apiKeyFromEnv.length > 0) {\n    return apiKeyFromEnv;\n  }\n\n  // Don't log - this is expected when llmClient is provided or API key will be set later\n  return undefined;\n}\n\nexport function trimTrailingTextNode(\n  path: string | undefined,\n): string | undefined {\n  return path?.replace(/\\/text\\(\\)(\\[\\d+\\])?$/iu, \"\");\n}\n\nexport function toTitleCase(str: string): string {\n  return str.replace(\n    /\\w\\S*/g,\n    (text) => text.charAt(0).toUpperCase() + text.substring(1),\n  );\n}\n\n// TODO: move to separate types file\nexport interface JsonSchemaProperty {\n  type: string;\n  enum?: unknown[];\n  items?: JsonSchemaProperty;\n  properties?: Record<string, JsonSchemaProperty>;\n  required?: string[];\n  minimum?: number;\n  maximum?: number;\n  description?: string;\n  format?: string; // JSON Schema format field (e.g., \"uri\", \"url\", \"email\", etc.)\n}\nexport interface JsonSchema extends JsonSchemaProperty {\n  type: string;\n}\n\n/**\n * Converts a JSON Schema object to a Zod schema\n * @param schema The JSON Schema object to convert\n * @returns A Zod schema equivalent to the input JSON Schema\n */\nexport function jsonSchemaToZod(schema: JsonSchema): ZodTypeAny {\n  switch (schema.type) {\n    case \"object\":\n      if (schema.properties) {\n        const shape: Record<string, ZodTypeAny> = {};\n        for (const key in schema.properties) {\n          shape[key] = jsonSchemaToZod(schema.properties[key]);\n        }\n        let zodObject = z.object(shape);\n        if (schema.required && Array.isArray(schema.required)) {\n          const requiredFields = schema.required.reduce<Record<string, true>>(\n            (acc, field) => ({ ...acc, [field]: true }),\n            {},\n          );\n          zodObject = zodObject.partial().required(requiredFields);\n        }\n        if (schema.description) {\n          zodObject = zodObject.describe(schema.description);\n        }\n        return zodObject;\n      } else {\n        return z.object({});\n      }\n    case \"array\":\n      if (schema.items) {\n        let zodArray = z.array(jsonSchemaToZod(schema.items));\n        if (schema.description) {\n          zodArray = zodArray.describe(schema.description);\n        }\n        return zodArray;\n      } else {\n        return z.array(z.any());\n      }\n    case \"string\": {\n      if (schema.enum) {\n        return z.string().refine((val) => schema.enum!.includes(val));\n      }\n      let zodString = z.string();\n\n      // Handle JSON Schema format field\n      if (schema.format === \"uri\" || schema.format === \"url\") {\n        zodString = zodString.url();\n      } else if (schema.format === \"email\") {\n        zodString = zodString.email();\n      } else if (schema.format === \"uuid\") {\n        zodString = zodString.uuid();\n      }\n      // Add more format handlers as needed\n\n      if (schema.description) {\n        zodString = zodString.describe(schema.description);\n      }\n      return zodString;\n    }\n    case \"number\": {\n      let zodNumber = z.number();\n      if (schema.minimum !== undefined) {\n        zodNumber = zodNumber.min(schema.minimum);\n      }\n      if (schema.maximum !== undefined) {\n        zodNumber = zodNumber.max(schema.maximum);\n      }\n      if (schema.description) {\n        zodNumber = zodNumber.describe(schema.description);\n      }\n      return zodNumber;\n    }\n    case \"boolean\": {\n      let zodBoolean = z.boolean();\n      if (schema.description) {\n        zodBoolean = zodBoolean.describe(schema.description);\n      }\n      return zodBoolean;\n    }\n    default:\n      return z.any();\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/AgentClient.ts",
    "content": "import {\n  AgentAction,\n  AgentResult,\n  AgentType,\n  AgentExecutionOptions,\n} from \"../types/public/agent.js\";\nimport { ClientOptions } from \"../types/public/model.js\";\n\n/**\n * Abstract base class for agent clients\n * This provides a common interface for all agent implementations\n */\nexport abstract class AgentClient {\n  public type: AgentType;\n  public modelName: string;\n  public clientOptions: ClientOptions;\n  public userProvidedInstructions?: string;\n\n  constructor(\n    type: AgentType,\n    modelName: string,\n    userProvidedInstructions?: string,\n  ) {\n    this.type = type;\n    this.modelName = modelName;\n    this.userProvidedInstructions = userProvidedInstructions;\n    this.clientOptions = {};\n  }\n\n  abstract execute(options: AgentExecutionOptions): Promise<AgentResult>;\n\n  abstract captureScreenshot(\n    options?: Record<string, unknown>,\n  ): Promise<unknown>;\n\n  abstract setViewport(width: number, height: number): void;\n\n  abstract setCurrentUrl(url: string): void;\n\n  abstract setScreenshotProvider(provider: () => Promise<string>): void;\n\n  abstract setActionHandler(\n    handler: (action: AgentAction) => Promise<void>,\n  ): void;\n\n  /** Optional hook called at the top of every step in the agent loop. */\n  protected preStepHook?: () => Promise<void>;\n\n  setPreStepHook(handler: () => Promise<void>): void {\n    this.preStepHook = handler;\n  }\n\n  /**\n   * Optional ephemeral context note that should be sent to the next model turn.\n   * Clients that do not support this can ignore it.\n   */\n  addContextNote(note: string): void {\n    void note;\n    // no-op by default\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/AgentProvider.ts",
    "content": "import { ToolSet } from \"ai/dist\";\nimport { AgentProviderType } from \"../types/public/agent.js\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { ClientOptions } from \"../types/public/model.js\";\nimport {\n  UnsupportedModelError,\n  UnsupportedModelProviderError,\n} from \"../types/public/sdkErrors.js\";\nimport { AgentClient } from \"./AgentClient.js\";\nimport { AnthropicCUAClient } from \"./AnthropicCUAClient.js\";\nimport { OpenAICUAClient } from \"./OpenAICUAClient.js\";\nimport { GoogleCUAClient } from \"./GoogleCUAClient.js\";\nimport { MicrosoftCUAClient } from \"./MicrosoftCUAClient.js\";\n\n// Map model names to their provider types\nexport const modelToAgentProviderMap: Record<string, AgentProviderType> = {\n  \"computer-use-preview\": \"openai\",\n  \"computer-use-preview-2025-03-11\": \"openai\",\n  \"claude-sonnet-4-20250514\": \"anthropic\",\n  \"claude-sonnet-4-5-20250929\": \"anthropic\",\n  \"claude-opus-4-5-20251101\": \"anthropic\",\n  \"claude-opus-4-6\": \"anthropic\",\n  \"claude-sonnet-4-6\": \"anthropic\",\n  \"claude-haiku-4-5-20251001\": \"anthropic\",\n  \"gemini-2.5-computer-use-preview-10-2025\": \"google\",\n  \"gemini-3-flash-preview\": \"google\",\n  \"gemini-3-pro-preview\": \"google\",\n  \"fara-7b\": \"microsoft\",\n};\n\n/**\n * Provider for agent clients\n * This class is responsible for creating the appropriate agent client\n * based on the provider type\n */\nexport class AgentProvider {\n  private logger: (message: LogLine) => void;\n\n  /**\n   * Create a new agent provider\n   */\n  constructor(logger: (message: LogLine) => void) {\n    this.logger = logger;\n  }\n\n  getClient(\n    modelName: string,\n    clientOptions?: ClientOptions,\n    userProvidedInstructions?: string,\n    tools?: ToolSet,\n  ): AgentClient {\n    // Check if provider is explicitly set in clientOptions\n    const explicitProvider = clientOptions?.provider as\n      | AgentProviderType\n      | undefined;\n    const type = explicitProvider || AgentProvider.getAgentProvider(modelName);\n\n    this.logger({\n      category: \"agent\",\n      message: `Getting agent client for type: ${type}, model: ${modelName}${explicitProvider ? \" (explicit provider)\" : \"\"}`,\n      level: 2,\n    });\n\n    try {\n      switch (type) {\n        case \"openai\":\n          return new OpenAICUAClient(\n            type,\n            modelName,\n            userProvidedInstructions,\n            clientOptions,\n            tools,\n          );\n        case \"anthropic\":\n          return new AnthropicCUAClient(\n            type,\n            modelName,\n            userProvidedInstructions,\n            clientOptions,\n            tools,\n          );\n        case \"google\":\n          return new GoogleCUAClient(\n            type,\n            modelName,\n            userProvidedInstructions,\n            clientOptions,\n            tools,\n          );\n        case \"microsoft\":\n          return new MicrosoftCUAClient(\n            type,\n            modelName,\n            userProvidedInstructions,\n            clientOptions,\n          );\n        default:\n          throw new UnsupportedModelProviderError(\n            [\"openai\", \"anthropic\", \"google\", \"microsoft\"],\n            \"Computer Use Agent\",\n          );\n      }\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      this.logger({\n        category: \"agent\",\n        message: `Error creating agent client: ${errorMessage}`,\n        level: 0,\n      });\n      throw error;\n    }\n  }\n\n  static getAgentProvider(modelName: string): AgentProviderType {\n    const normalized = modelName.includes(\"/\")\n      ? modelName.split(\"/\")[1]\n      : modelName;\n\n    if (normalized in modelToAgentProviderMap) {\n      return modelToAgentProviderMap[normalized];\n    }\n\n    throw new UnsupportedModelError(\n      Object.keys(modelToAgentProviderMap),\n      \"Computer Use Agent\",\n    );\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/AnthropicCUAClient.ts",
    "content": "import {\n  AgentAction,\n  AgentResult,\n  AgentType,\n  AnthropicContentBlock,\n  AnthropicMessage,\n  AnthropicTextBlock,\n  AnthropicToolResult,\n  AgentExecutionOptions,\n  ToolUseItem,\n} from \"../types/public/agent.js\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { ClientOptions } from \"../types/public/model.js\";\nimport {\n  AgentScreenshotProviderError,\n  StagehandClosedError,\n} from \"../types/public/sdkErrors.js\";\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { ToolSet } from \"ai\";\nimport { AgentClient } from \"./AgentClient.js\";\nimport { compressConversationImages } from \"./utils/imageCompression.js\";\nimport { toJsonSchema } from \"../zodCompat.js\";\nimport type { StagehandZodSchema } from \"../zodCompat.js\";\nimport {\n  FlowLogger,\n  extractLlmCuaPromptSummary,\n  extractLlmCuaResponseSummary,\n} from \"../flowlogger/FlowLogger.js\";\nimport { v7 as uuidv7 } from \"uuid\";\n\nexport type ResponseInputItem = AnthropicMessage | AnthropicToolResult;\n\n/**\n * Client for Anthropic's Computer Use API\n * This implementation uses the official Anthropic Messages API for Computer Use\n */\nexport class AnthropicCUAClient extends AgentClient {\n  private apiKey: string;\n  private baseURL?: string;\n  private client: Anthropic;\n  public lastMessageId?: string;\n  private currentViewport = { width: 1288, height: 711 };\n  private currentUrl?: string;\n  private screenshotProvider?: () => Promise<string>;\n  private actionHandler?: (action: AgentAction) => Promise<void>;\n  private thinkingBudget: number | null = null;\n  private tools?: ToolSet;\n\n  constructor(\n    type: AgentType,\n    modelName: string,\n    userProvidedInstructions?: string,\n    clientOptions?: ClientOptions,\n    tools?: ToolSet,\n  ) {\n    super(type, modelName, userProvidedInstructions);\n\n    // Process client options\n    this.apiKey =\n      (clientOptions?.apiKey as string) || process.env.ANTHROPIC_API_KEY || \"\";\n    this.baseURL = (clientOptions?.baseURL as string) || undefined;\n\n    // Get thinking budget if specified\n    if (\n      clientOptions?.thinkingBudget &&\n      typeof clientOptions.thinkingBudget === \"number\"\n    ) {\n      this.thinkingBudget = clientOptions.thinkingBudget;\n    }\n\n    // Store client options for reference\n    this.clientOptions = {\n      apiKey: this.apiKey,\n    };\n\n    if (this.baseURL) {\n      this.clientOptions.baseURL = this.baseURL;\n    }\n\n    // Initialize the Anthropic client\n    this.client = new Anthropic(this.clientOptions);\n\n    this.tools = tools;\n  }\n\n  setViewport(width: number, height: number): void {\n    this.currentViewport = { width, height };\n  }\n\n  setCurrentUrl(url: string): void {\n    this.currentUrl = url;\n  }\n\n  setScreenshotProvider(provider: () => Promise<string>): void {\n    this.screenshotProvider = provider;\n  }\n\n  setActionHandler(handler: (action: AgentAction) => Promise<void>): void {\n    this.actionHandler = handler;\n  }\n\n  setTools(tools: ToolSet): void {\n    this.tools = tools;\n  }\n\n  /**\n   * Execute a task with the Anthropic CUA\n   * This is the main entry point for the agent\n   * @implements AgentClient.execute\n   */\n  async execute(executionOptions: AgentExecutionOptions): Promise<AgentResult> {\n    const { options, logger } = executionOptions;\n    const { instruction } = options;\n    const maxSteps = options.maxSteps || 10;\n\n    let currentStep = 0;\n    let completed = false;\n    const actions: AgentAction[] = [];\n    const messageList: string[] = [];\n    let finalMessage = \"\";\n\n    // Start with the initial instruction\n    let inputItems: ResponseInputItem[] =\n      this.createInitialInputItems(instruction);\n\n    logger({\n      category: \"agent\",\n      message: `Starting Anthropic agent execution with instruction: ${instruction}`,\n      level: 1,\n    });\n\n    let totalInputTokens = 0;\n    let totalOutputTokens = 0;\n    let totalInferenceTime = 0;\n\n    try {\n      // Execute steps until completion or max steps reached\n      while (!completed && currentStep < maxSteps) {\n        await this.preStepHook?.();\n\n        logger({\n          category: \"agent\",\n          message: `Executing step ${currentStep + 1}/${maxSteps}`,\n          level: 1,\n        });\n\n        const result = await this.executeStep(inputItems, logger);\n        totalInputTokens += result.usage.input_tokens;\n        totalOutputTokens += result.usage.output_tokens;\n        totalInferenceTime += result.usage.inference_time_ms;\n\n        // Add actions to the list\n        if (result.actions.length > 0) {\n          logger({\n            category: \"agent\",\n            message: `Step ${currentStep + 1} performed ${result.actions.length} actions`,\n            level: 2,\n          });\n          actions.push(...result.actions);\n        }\n\n        // Update completion status\n        completed = result.completed;\n\n        // Update the input items for the next step if we're continuing\n        if (!completed) {\n          inputItems = result.nextInputItems;\n        }\n\n        // Record any message for this step\n        if (result.message) {\n          messageList.push(result.message);\n          finalMessage = result.message;\n        }\n\n        // Increment step counter\n        currentStep++;\n      }\n\n      logger({\n        category: \"agent\",\n        message: `Anthropic agent execution completed: ${completed}, with ${actions.length} total actions performed`,\n        level: 1,\n      });\n\n      // Return the final result\n      return {\n        success: completed,\n        actions,\n        message: finalMessage,\n        completed,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      logger({\n        category: \"agent\",\n        message: `Error executing agent task: ${errorMessage}`,\n        level: 0,\n      });\n\n      return {\n        success: false,\n        actions,\n        message: `Failed to execute task: ${errorMessage}`,\n        completed: false,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    }\n  }\n\n  async executeStep(\n    inputItems: ResponseInputItem[],\n    logger: (message: LogLine) => void,\n  ): Promise<{\n    actions: AgentAction[];\n    message: string;\n    completed: boolean;\n    nextInputItems: ResponseInputItem[];\n    usage: {\n      input_tokens: number;\n      output_tokens: number;\n      inference_time_ms: number;\n    };\n  }> {\n    try {\n      // Get response from the model\n      const result = await this.getAction(inputItems);\n      const content = result.content;\n      const usage = {\n        input_tokens: result.usage.input_tokens,\n        output_tokens: result.usage.output_tokens,\n        inference_time_ms: result.usage.inference_time_ms,\n      };\n\n      logger({\n        category: \"agent\",\n        message: `Received response with ${content.length} content blocks`,\n        level: 2,\n      });\n\n      // Extract actions from the content\n      const stepActions: AgentAction[] = [];\n      const toolUseItems: ToolUseItem[] = [];\n      let message = \"\";\n\n      // Process content blocks to find tool use items and text content\n      for (const block of content) {\n        logger({\n          category: \"agent\",\n          message: `Processing block type: ${block.type}, id: ${block.id || \"unknown\"}`,\n          level: 2,\n        });\n\n        if (block.type === \"tool_use\") {\n          // Direct handling of tool_use type\n          logger({\n            category: \"agent\",\n            message: `Found tool_use block: ${JSON.stringify(block)}`,\n            level: 2,\n          });\n\n          // Cast to ToolUseItem and add to list\n          const toolUseItem = block as ToolUseItem;\n          toolUseItems.push(toolUseItem);\n\n          logger({\n            category: \"agent\",\n            message: `Added tool_use item: ${toolUseItem.name}, action: ${JSON.stringify(toolUseItem.input)}`,\n            level: 2,\n          });\n\n          // Convert tool use to action and add to actions list\n          const action = this.convertToolUseToAction(toolUseItem);\n          if (action) {\n            logger({\n              category: \"agent\",\n              message: `Created action from tool_use: ${toolUseItem.name}, action: ${action.type}`,\n              level: 2,\n            });\n            stepActions.push(action);\n          } else if (this.tools && toolUseItem.name in this.tools) {\n            stepActions.push({\n              type: \"custom_tool\",\n              tool: toolUseItem.name,\n              input: toolUseItem.input,\n            } as AgentAction);\n          }\n        } else if (block.type === \"text\") {\n          // Safe to cast here since we've verified it's a text block\n          const textBlock = block as unknown as AnthropicTextBlock;\n          message += textBlock.text + \"\\n\";\n\n          logger({\n            category: \"agent\",\n            message: `Found text block: ${textBlock.text}`,\n            level: 2,\n          });\n        } else {\n          logger({\n            category: \"agent\",\n            message: `Found unknown block type: ${block.type}`,\n            level: 2,\n          });\n        }\n      }\n\n      // Execute actions if an action handler is provided\n      if (this.actionHandler && stepActions.length > 0) {\n        for (const action of stepActions) {\n          try {\n            logger({\n              category: \"agent\",\n              message: `Executing action: ${action.type}`,\n              level: 1,\n            });\n            await this.actionHandler(action);\n          } catch (error) {\n            if (error instanceof StagehandClosedError) {\n              throw error;\n            }\n            const errorMessage =\n              error instanceof Error ? error.message : String(error);\n            logger({\n              category: \"agent\",\n              message: `Error executing action ${action.type}: ${errorMessage}`,\n              level: 0,\n            });\n          }\n        }\n      }\n\n      // Create the assistant response message with all content blocks\n      const assistantMessage: AnthropicMessage = {\n        role: \"assistant\",\n        content: content as unknown as AnthropicContentBlock[],\n      };\n\n      // Keep track of the conversation history by preserving all previous messages\n      // and adding new messages at the end\n      const nextInputItems: ResponseInputItem[] = [...inputItems];\n\n      // Add the assistant message with tool_use blocks to the history\n      compressConversationImages(nextInputItems);\n\n      nextInputItems.push(assistantMessage);\n\n      // Generate tool results and add them as a user message\n      if (toolUseItems.length > 0) {\n        const toolResults = await this.takeAction(toolUseItems, logger);\n\n        if (toolResults.length > 0) {\n          // Tool results are AnthropicToolResult[] which are compatible with AnthropicContentBlock[]\n          const userToolResultsMessage: AnthropicMessage = {\n            role: \"user\",\n            content: toolResults as unknown as AnthropicContentBlock[],\n          };\n          nextInputItems.push(userToolResultsMessage);\n        }\n      }\n\n      // The step is completed only if there were no tool_use items\n      const completed = toolUseItems.length === 0;\n\n      logger({\n        category: \"agent\",\n        message: `Step processed ${toolUseItems.length} tool use items, completed: ${completed}`,\n        level: 2,\n      });\n\n      return {\n        actions: stepActions,\n        message: message.trim(),\n        completed,\n        nextInputItems,\n        usage: usage,\n      };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      logger({\n        category: \"agent\",\n        message: `Error executing step: ${errorMessage}`,\n        level: 0,\n      });\n\n      throw error;\n    }\n  }\n\n  private createInitialInputItems(instruction: string): AnthropicMessage[] {\n    // For the initial request, we use a simple array with the user's instruction\n    return [\n      {\n        role: \"system\",\n        content: this.userProvidedInstructions,\n      },\n      {\n        role: \"user\",\n        content: instruction,\n      },\n    ];\n  }\n\n  async getAction(inputItems: ResponseInputItem[]): Promise<{\n    content: AnthropicContentBlock[];\n    id: string;\n    usage: Record<string, number>;\n  }> {\n    try {\n      // For the API request, we use the inputItems directly\n      // These should already be properly formatted as a sequence of user/assistant messages\n      const messages: AnthropicMessage[] = [];\n\n      for (const item of inputItems) {\n        if (\"role\" in item) {\n          // Skip system messages as Anthropic requires system as a top-level parameter\n          if (item.role !== \"system\") {\n            messages.push(item);\n          }\n        }\n        // Note: We don't need special handling for tool_result items here anymore\n        // as they should already be properly wrapped in user messages\n      }\n\n      // Configure thinking capability if available\n      const thinking = this.thinkingBudget\n        ? { type: \"enabled\" as const, budget_tokens: this.thinkingBudget }\n        : undefined;\n\n      // Claude 4.6+ models require the newer computer_20251124 tool version\n      const modelBase = this.modelName.includes(\"/\")\n        ? this.modelName.split(\"/\")[1]\n        : this.modelName;\n      const shouldUseNewToolVersion = [\n        \"claude-opus-4-6\",\n        \"claude-sonnet-4-6\",\n        \"claude-opus-4-5-20251101\",\n      ].includes(modelBase);\n\n      const computerToolType = shouldUseNewToolVersion\n        ? \"computer_20251124\"\n        : \"computer_20250124\";\n      const betaFlag = shouldUseNewToolVersion\n        ? \"computer-use-2025-11-24\"\n        : \"computer-use-2025-01-24\";\n\n      // Create the request parameters\n      const requestParams: Record<string, unknown> = {\n        model: this.modelName,\n        max_tokens: 4096,\n        messages: messages,\n        tools: [\n          {\n            type: computerToolType,\n            name: \"computer\",\n            display_width_px: this.currentViewport.width,\n            display_height_px: this.currentViewport.height,\n            display_number: 1,\n          },\n        ],\n        betas: [betaFlag],\n      };\n\n      // Add custom tools if available\n      if (this.tools && Object.keys(this.tools).length > 0) {\n        const customTools = Object.entries(this.tools).map(([name, tool]) => {\n          const schema = tool.inputSchema as StagehandZodSchema;\n\n          // Convert Zod schema to proper JSON schema format for Anthropic\n          const jsonSchema = toJsonSchema(schema) as {\n            properties?: Record<string, unknown>;\n            required?: string[];\n          };\n\n          const inputSchema = {\n            type: \"object\",\n            properties: jsonSchema.properties || {},\n            required: jsonSchema.required || [],\n          };\n\n          return {\n            name,\n            description: tool.description,\n            input_schema: inputSchema,\n          };\n        });\n\n        requestParams.tools = [\n          ...(requestParams.tools as Record<string, unknown>[]),\n          ...customTools,\n        ];\n      }\n\n      // Add system parameter if provided\n      if (this.userProvidedInstructions) {\n        requestParams.system = this.userProvidedInstructions;\n      }\n\n      // Add thinking parameter if available\n      if (thinking) {\n        requestParams.thinking = thinking;\n      }\n\n      // Log LLM request\n      const llmRequestId = uuidv7();\n      FlowLogger.logLlmRequest({\n        requestId: llmRequestId,\n        model: this.modelName,\n        prompt: extractLlmCuaPromptSummary(messages),\n      });\n\n      const startTime = Date.now();\n      // Create the message using the Anthropic Messages API\n      // @ts-expect-error - The Anthropic SDK types are stricter than what we need\n      const response = await this.client.beta.messages.create(requestParams);\n      const endTime = Date.now();\n      const elapsedMs = endTime - startTime;\n      const usage = {\n        input_tokens: response.usage.input_tokens,\n        output_tokens: response.usage.output_tokens,\n        inference_time_ms: elapsedMs,\n      };\n\n      // Log LLM response\n      FlowLogger.logLlmResponse({\n        requestId: llmRequestId,\n        model: this.modelName,\n        output: extractLlmCuaResponseSummary(response.content),\n        inputTokens: response.usage.input_tokens,\n        outputTokens: response.usage.output_tokens,\n      });\n\n      // Store the message ID for future use\n      this.lastMessageId = response.id;\n\n      // Return the content and message ID\n      return {\n        // Cast the response content to our internal type\n        content: response.content as unknown as AnthropicContentBlock[],\n        id: response.id,\n        usage,\n      };\n    } catch (error) {\n      console.error(\"Error getting action from Anthropic:\", error);\n      throw error;\n    }\n  }\n\n  async takeAction(\n    toolUseItems: ToolUseItem[],\n    logger: (message: LogLine) => void,\n  ): Promise<AnthropicToolResult[]> {\n    const toolResults: AnthropicToolResult[] = [];\n\n    logger({\n      category: \"agent\",\n      message: `Taking action on ${toolUseItems.length} tool use items`,\n      level: 2,\n    });\n\n    // Process each tool use item\n    for (const item of toolUseItems) {\n      try {\n        logger({\n          category: \"agent\",\n          message: `Processing tool use: ${item.name}, id: ${item.id}, action: ${JSON.stringify(item.input)}`,\n          level: 2,\n        });\n\n        // TODO: Normalize and migrate to agentHandler\n\n        // For computer tool, capture screenshot and return image\n        if (item.name === \"computer\") {\n          // Get action type\n          const action = item.input.action as string;\n          logger({\n            category: \"agent\",\n            message: `Computer action type: ${action}`,\n            level: 2,\n          });\n\n          // Capture a screenshot for the response\n          const screenshot = await this.captureScreenshot();\n          logger({\n            category: \"agent\",\n            message: `Screenshot captured, length: ${screenshot.length}`,\n            level: 2,\n          });\n\n          // Create proper image content block for Anthropic\n          const imageContent = [\n            {\n              type: \"image\",\n              source: {\n                type: \"base64\",\n                media_type: \"image/png\",\n                data: screenshot.replace(/^data:image\\/png;base64,/, \"\"),\n              },\n            },\n          ];\n\n          // Add current URL if available\n          if (this.currentUrl) {\n            toolResults.push({\n              type: \"tool_result\",\n              tool_use_id: item.id,\n              content: [\n                ...imageContent,\n                {\n                  type: \"text\",\n                  text: `Current URL: ${this.currentUrl}`,\n                },\n              ],\n            });\n          } else {\n            toolResults.push({\n              type: \"tool_result\",\n              tool_use_id: item.id,\n              content: imageContent,\n            });\n          }\n\n          logger({\n            category: \"agent\",\n            message: `Added computer tool result for tool_use_id: ${item.id}`,\n            level: 2,\n          });\n        } else {\n          // Handle custom tools\n          let toolResult = \"Tool executed successfully\";\n          if (this.tools && item.name in this.tools) {\n            try {\n              const tool = this.tools[item.name];\n\n              logger({\n                category: \"agent\",\n                message: `Executing tool call: ${item.name} with args: ${JSON.stringify(item.input)}`,\n                level: 1,\n              });\n\n              const result = await tool.execute(item.input, {\n                toolCallId: item.id,\n                messages: [],\n              });\n              toolResult = JSON.stringify(result);\n\n              logger({\n                category: \"agent\",\n                message: `Tool ${item.name} completed successfully. Result: ${toolResult}`,\n                level: 1,\n              });\n            } catch (toolError) {\n              const errorMessage =\n                toolError instanceof Error\n                  ? toolError.message\n                  : String(toolError);\n              toolResult = `Error executing tool: ${errorMessage}`;\n\n              logger({\n                category: \"agent\",\n                message: `Error executing tool ${item.name}: ${errorMessage}`,\n                level: 0,\n              });\n            }\n          }\n\n          toolResults.push({\n            type: \"tool_result\",\n            tool_use_id: item.id,\n            content: [\n              {\n                type: \"text\",\n                text: toolResult,\n              },\n            ],\n          });\n\n          logger({\n            category: \"agent\",\n            message: `Added custom tool result for tool ${item.name}, tool_use_id: ${item.id}`,\n            level: 2,\n          });\n        }\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n\n        logger({\n          category: \"agent\",\n          message: `Error executing tool use: ${errorMessage}`,\n          level: 0,\n        });\n\n        try {\n          // For computer tool, try to capture a screenshot even on error\n          if (item.name === \"computer\") {\n            const screenshot = await this.captureScreenshot();\n\n            toolResults.push({\n              type: \"tool_result\",\n              tool_use_id: item.id,\n              content: [\n                {\n                  type: \"image\",\n                  source: {\n                    type: \"base64\",\n                    media_type: \"image/png\",\n                    data: screenshot.replace(/^data:image\\/png;base64,/, \"\"),\n                  },\n                },\n                {\n                  type: \"text\",\n                  text: `Error: ${errorMessage}`,\n                },\n              ],\n            });\n\n            logger({\n              category: \"agent\",\n              message: `Added error tool result with screenshot for tool_use_id: ${item.id}`,\n              level: 1,\n            });\n          } else {\n            // For other tools, return an error message as a text content block\n            toolResults.push({\n              type: \"tool_result\",\n              tool_use_id: item.id,\n              content: [\n                {\n                  type: \"text\",\n                  text: `Error: ${errorMessage}`,\n                },\n              ],\n            });\n\n            logger({\n              category: \"agent\",\n              message: `Added error tool result for tool_use_id: ${item.id}`,\n              level: 1,\n            });\n          }\n        } catch (screenshotError) {\n          // If we can't capture a screenshot, just send the error\n          logger({\n            category: \"agent\",\n            message: `Error capturing screenshot: ${String(screenshotError)}`,\n            level: 0,\n          });\n\n          toolResults.push({\n            type: \"tool_result\",\n            tool_use_id: item.id,\n            content: [\n              {\n                type: \"text\",\n                text: `Error: ${errorMessage}`,\n              },\n            ],\n          });\n\n          logger({\n            category: \"agent\",\n            message: `Added text error tool result for tool_use_id: ${item.id}`,\n            level: 1,\n          });\n        }\n      }\n    }\n\n    logger({\n      category: \"agent\",\n      message: `Prepared ${toolResults.length} tool results for next request`,\n      level: 2,\n    });\n\n    return toolResults;\n  }\n\n  private convertToolUseToAction(item: ToolUseItem): AgentAction | null {\n    try {\n      const { name, input } = item;\n\n      if (name === \"computer\") {\n        // For computer actions, format according to the action type\n        const action = input.action as string;\n\n        if (!action) {\n          console.warn(\"Missing action in tool use item:\", item);\n          return null;\n        }\n\n        // Handle different action types specifically\n        if (action === \"screenshot\") {\n          return {\n            type: \"screenshot\",\n            ...input,\n          };\n        } else if (action === \"click\") {\n          return {\n            type: \"click\",\n            x: input.x as number,\n            y: input.y as number,\n            button: (input.button as string) || \"left\",\n            ...input,\n          };\n        } else if (action === \"type\") {\n          return {\n            type: \"type\",\n            text: input.text as string,\n            ...input,\n          };\n        } else if (action === \"keypress\" || action === \"key\") {\n          return {\n            type: \"keypress\",\n            keys: [input.text as string],\n            ...input,\n          };\n        } else if (action === \"double_click\" || action === \"doubleClick\") {\n          return {\n            type: \"doubleClick\",\n            x:\n              (input.x as number) ||\n              (input.coordinate ? (input.coordinate as number[])[0] : 0),\n            y:\n              (input.y as number) ||\n              (input.coordinate ? (input.coordinate as number[])[1] : 0),\n            ...input,\n          };\n        } else if (action === \"scroll\") {\n          // Convert Anthropic's coordinate, scroll_amount and scroll_direction into scroll_x and scroll_y\n          const x =\n            (input.x as number) ||\n            (input.coordinate ? (input.coordinate as number[])[0] : 0);\n          const y =\n            (input.y as number) ||\n            (input.coordinate ? (input.coordinate as number[])[1] : 0);\n\n          // Calculate scroll_x and scroll_y based on scroll_amount and scroll_direction\n          let scroll_x = 0;\n          let scroll_y = 0;\n\n          const scrollAmount = (input.scroll_amount as number) || 5;\n          const scrollMultiplier = 100; // Pixels per unit of scroll_amount\n\n          if (input.scroll_direction) {\n            const direction = input.scroll_direction as string;\n            if (direction === \"down\") {\n              scroll_y = scrollAmount * scrollMultiplier;\n            } else if (direction === \"up\") {\n              scroll_y = -scrollAmount * scrollMultiplier;\n            } else if (direction === \"right\") {\n              scroll_x = scrollAmount * scrollMultiplier;\n            } else if (direction === \"left\") {\n              scroll_x = -scrollAmount * scrollMultiplier;\n            }\n          } else {\n            // Use direct scroll_x and scroll_y if provided\n            scroll_x = (input.scroll_x as number) || 0;\n            scroll_y = (input.scroll_y as number) || 0;\n          }\n\n          return {\n            type: \"scroll\",\n            x: x,\n            y: y,\n            scroll_x: scroll_x,\n            scroll_y: scroll_y,\n            ...input,\n          };\n        } else if (action === \"move\") {\n          // Handle Anthropic's coordinate format\n          const coordinates = input.coordinate as number[] | undefined;\n          const x = coordinates ? coordinates[0] : (input.x as number) || 0;\n          const y = coordinates ? coordinates[1] : (input.y as number) || 0;\n\n          return {\n            type: \"move\",\n            x: x,\n            y: y,\n            ...input,\n          };\n        } else if (action === \"drag\" || action === \"left_click_drag\") {\n          // Make sure path is properly formatted\n          const path =\n            (input.path as { x: number; y: number }[]) ||\n            (input.coordinate\n              ? [\n                  {\n                    x: (input.start_coordinate as number[])[0],\n                    y: (input.start_coordinate as number[])[1],\n                  },\n                  {\n                    x: (input.coordinate as number[])[0],\n                    y: (input.coordinate as number[])[1],\n                  },\n                ]\n              : []);\n\n          return {\n            type: \"drag\",\n            path: path,\n            ...input,\n          };\n        } else if (action === \"wait\") {\n          return {\n            type: \"wait\",\n            ...input,\n          };\n        } else if (action === \"left_click\") {\n          // Convert left_click to regular click\n          const coordinates = input.coordinate as number[] | undefined;\n          const x = coordinates ? coordinates[0] : (input.x as number) || 0;\n          const y = coordinates ? coordinates[1] : (input.y as number) || 0;\n\n          return {\n            type: \"click\",\n            x: x,\n            y: y,\n            button: \"left\",\n            ...input,\n          };\n        } else {\n          // For other computer actions, use the action type directly\n          return {\n            type: action,\n            ...input,\n          };\n        }\n      } else if (name === \"str_replace_editor\" || name === \"bash\") {\n        // For editor or bash tools\n        return {\n          type: name,\n          params: input,\n        };\n      } else if (this.tools && name in this.tools) {\n        return null;\n      }\n\n      console.warn(`Unknown tool name: ${name}`);\n      return null;\n    } catch (error) {\n      console.error(\"Error converting tool use to action:\", error);\n      return null;\n    }\n  }\n\n  async captureScreenshot(options?: {\n    base64Image?: string;\n    currentUrl?: string;\n  }): Promise<string> {\n    // Use provided options if available\n    if (options?.base64Image) {\n      return `data:image/png;base64,${options.base64Image}`;\n    }\n\n    // Use the screenshot provider if available\n    if (this.screenshotProvider) {\n      try {\n        const base64Image = await this.screenshotProvider();\n        return `data:image/png;base64,${base64Image}`;\n      } catch (error) {\n        console.error(\"Error capturing screenshot:\", error);\n        throw error;\n      }\n    }\n\n    throw new AgentScreenshotProviderError(\n      \"`screenshotProvider` has not been set. \" +\n        \"Please call `setScreenshotProvider()` with a valid function that returns a base64-encoded image\",\n    );\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/GoogleCUAClient.ts",
    "content": "import {\n  GoogleGenAI,\n  Content,\n  Part,\n  GenerateContentResponse,\n  FunctionCall,\n  GenerateContentConfig,\n  Tool,\n  GoogleGenAIOptions,\n} from \"@google/genai\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport {\n  AgentAction,\n  AgentResult,\n  AgentType,\n  AgentExecutionOptions,\n  SafetyCheck,\n  SafetyConfirmationHandler,\n} from \"../types/public/agent.js\";\nimport { ClientOptions } from \"../types/public/model.js\";\nimport { AgentClient } from \"./AgentClient.js\";\nimport {\n  AgentScreenshotProviderError,\n  LLMResponseError,\n  StagehandClosedError,\n} from \"../types/public/sdkErrors.js\";\nimport { buildGoogleCUASystemPrompt } from \"../../prompt.js\";\nimport { compressGoogleConversationImages } from \"./utils/imageCompression.js\";\nimport { mapKeyToPlaywright } from \"./utils/cuaKeyMapping.js\";\nimport {\n  executeGoogleCustomTool,\n  isCustomTool,\n  convertToolSetToFunctionDeclarations,\n} from \"./utils/googleCustomToolHandler.js\";\nimport { ToolSet } from \"ai\";\nimport {\n  FlowLogger,\n  extractLlmCuaPromptSummary,\n  extractLlmCuaResponseSummary,\n} from \"../flowlogger/FlowLogger.js\";\nimport { v7 as uuidv7 } from \"uuid\";\n\n/**\n * Client for Google's Computer Use Assistant API\n * This implementation uses the Google Generative AI SDK for Computer Use\n */\nexport class GoogleCUAClient extends AgentClient {\n  private apiKey: string;\n  private client: GoogleGenAI;\n  private currentViewport = { width: 1288, height: 711 };\n  private currentUrl?: string;\n  private screenshotProvider?: () => Promise<string>;\n  private actionHandler?: (action: AgentAction) => Promise<void>;\n  private history: Content[] = [];\n  private environment: \"ENVIRONMENT_BROWSER\" | \"ENVIRONMENT_DESKTOP\" =\n    \"ENVIRONMENT_BROWSER\";\n  private generateContentConfig: GenerateContentConfig;\n  private tools?: ToolSet;\n  private baseURL?: string;\n  private safetyConfirmationHandler?: SafetyConfirmationHandler;\n  constructor(\n    type: AgentType,\n    modelName: string,\n    userProvidedInstructions?: string,\n    clientOptions?: ClientOptions,\n    tools?: ToolSet,\n  ) {\n    super(type, modelName, userProvidedInstructions);\n\n    this.tools = tools;\n    // Process client options\n    this.apiKey =\n      (clientOptions?.apiKey as string) ||\n      process.env.GEMINI_API_KEY ||\n      process.env.GOOGLE_GENERATIVE_AI_API_KEY ||\n      process.env.GOOGLE_API_KEY ||\n      \"\";\n    this.baseURL = clientOptions?.baseURL as string | undefined;\n\n    // Initialize the Google Generative AI client\n    const genAIOptions: GoogleGenAIOptions = {\n      apiKey: this.apiKey,\n      ...(this.baseURL ? { httpOptions: { baseUrl: this.baseURL } } : {}),\n    };\n    this.client = new GoogleGenAI(genAIOptions);\n\n    // Get environment if specified\n    if (\n      clientOptions?.environment &&\n      typeof clientOptions.environment === \"string\"\n    ) {\n      this.environment = clientOptions.environment as typeof this.environment;\n    }\n\n    this.generateContentConfig = {\n      temperature: 1,\n      topP: 0.95,\n      topK: 40,\n      maxOutputTokens: 8192,\n      // systemInstruction: this.userProvidedInstructions\n      //   ? { parts: [{ text: this.userProvidedInstructions }] }\n      //   : { parts: [{ text: buildGoogleCUASystemPrompt() }] },\n      tools: [\n        {\n          computerUse: {\n            environment: this.environment,\n          },\n        } as Tool,\n      ],\n    };\n\n    // Store client options for reference\n    this.clientOptions = {\n      apiKey: this.apiKey,\n      ...(this.baseURL ? { baseURL: this.baseURL } : {}),\n    };\n\n    // Initialize tools if provided\n    if (this.tools && Object.keys(this.tools).length > 0) {\n      this.updateGenerateContentConfig();\n    }\n  }\n\n  public setViewport(width: number, height: number): void {\n    this.currentViewport = { width, height };\n  }\n\n  setCurrentUrl(url: string): void {\n    this.currentUrl = url;\n  }\n\n  setScreenshotProvider(provider: () => Promise<string>): void {\n    this.screenshotProvider = provider;\n  }\n\n  setActionHandler(handler: (action: AgentAction) => Promise<void>): void {\n    this.actionHandler = handler;\n  }\n\n  setTools(tools: ToolSet): void {\n    this.tools = tools;\n    this.updateGenerateContentConfig();\n  }\n\n  setSafetyConfirmationHandler(handler?: SafetyConfirmationHandler): void {\n    this.safetyConfirmationHandler = handler;\n  }\n\n  private async handleSafetyConfirmation(\n    safetyDecision: unknown,\n    logger: (message: LogLine) => void,\n  ): Promise<string | undefined> {\n    const safetyMessage =\n      typeof safetyDecision === \"object\"\n        ? JSON.stringify(safetyDecision, null, 2)\n        : String(safetyDecision);\n\n    const safetyChecks: SafetyCheck[] = [\n      {\n        id: \"google-safety-decision\",\n        code: \"safety_decision\",\n        message: safetyMessage,\n      },\n    ];\n\n    if (this.safetyConfirmationHandler) {\n      logger({\n        category: \"agent\",\n        message: `Requesting safety confirmation for Google safety decision: ${safetyMessage}`,\n        level: 1,\n      });\n\n      const response = await this.safetyConfirmationHandler(safetyChecks);\n\n      if (response.acknowledged) {\n        logger({\n          category: \"agent\",\n          message: `Safety decision acknowledged by user`,\n          level: 1,\n        });\n        return \"true\";\n      } else {\n        logger({\n          category: \"agent\",\n          message: `Safety decision rejected by user`,\n          level: 1,\n        });\n        return undefined;\n      }\n    }\n\n    logger({\n      category: \"agent\",\n      message: `Auto-acknowledging Google safety decision`,\n      level: 2,\n    });\n    return \"true\";\n  }\n\n  /**\n   * Update the generateContentConfig with current tools\n   */\n  private updateGenerateContentConfig(): void {\n    const functionDeclarations =\n      this.tools && Object.keys(this.tools).length > 0\n        ? convertToolSetToFunctionDeclarations(this.tools)\n        : [];\n\n    this.generateContentConfig = {\n      ...this.generateContentConfig,\n      tools: [\n        {\n          computerUse: {\n            environment: this.environment,\n          },\n          ...(functionDeclarations.length > 0 ? { functionDeclarations } : {}),\n        } as Tool,\n      ],\n    };\n  }\n\n  /**\n   * Execute a task with the Google CUA\n   * This is the main entry point for the agent\n   * @implements AgentClient.execute\n   */\n  async execute(executionOptions: AgentExecutionOptions): Promise<AgentResult> {\n    const { options, logger } = executionOptions;\n    const { instruction } = options;\n    const maxSteps = options.maxSteps || 10;\n\n    let currentStep = 0;\n    let completed = false;\n    const actions: AgentAction[] = [];\n    const messageList: string[] = [];\n    let finalMessage = \"\";\n    this.history = []; // Clear history for new execution\n\n    // Start with the initial instruction\n    await this.initializeHistory(instruction);\n\n    let totalInputTokens = 0;\n    let totalOutputTokens = 0;\n    let totalInferenceTime = 0;\n\n    try {\n      // Execute steps until completion or max steps reached\n      while (!completed && currentStep < maxSteps) {\n        await this.preStepHook?.();\n\n        logger({\n          category: \"agent\",\n          message: `Executing step ${currentStep + 1}/${maxSteps}`,\n          level: 1,\n        });\n\n        const result = await this.executeStep(logger);\n        totalInputTokens += result.usage.input_tokens;\n        totalOutputTokens += result.usage.output_tokens;\n        totalInferenceTime += result.usage.inference_time_ms;\n\n        // Add actions to the list\n        actions.push(...result.actions);\n\n        // Update completion status\n        completed = result.completed;\n\n        // Record any message for this step\n        if (result.message) {\n          messageList.push(result.message);\n          finalMessage = result.message;\n        }\n\n        // Increment step counter\n        currentStep++;\n      }\n\n      // Return the final result\n      return {\n        success: completed,\n        actions,\n        message: finalMessage,\n        completed,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      logger({\n        category: \"agent\",\n        message: `Error executing agent task: ${errorMessage}`,\n        level: 0,\n      });\n\n      return {\n        success: false,\n        actions,\n        message: `Failed to execute task: ${errorMessage}`,\n        completed: false,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    }\n  }\n\n  /**\n   * Initialize conversation history with the initial instruction\n   */\n  private async initializeHistory(instruction: string): Promise<void> {\n    const parts: Part[] = [{ text: instruction }];\n\n    // Note: The Python implementation doesn't include the initial screenshot\n    // Following the same pattern here\n\n    const systemPromptContent = this.userProvidedInstructions\n      ? this.userProvidedInstructions\n      : buildGoogleCUASystemPrompt().content;\n\n    this.history = [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: \"System prompt: \" + systemPromptContent,\n          },\n        ],\n      },\n      {\n        role: \"user\",\n        parts,\n      },\n    ];\n  }\n\n  /**\n   * Execute a single step of the agent\n   */\n  async executeStep(logger: (message: LogLine) => void): Promise<{\n    actions: AgentAction[];\n    message: string;\n    completed: boolean;\n    usage: {\n      input_tokens: number;\n      output_tokens: number;\n      inference_time_ms: number;\n    };\n  }> {\n    try {\n      const startTime = Date.now();\n\n      // Compress images in conversation history before sending to the model\n      const compressedResult = compressGoogleConversationImages(\n        this.history,\n        2,\n      );\n      const compressedHistory = compressedResult.items;\n\n      // Use the SDK's generateContent method with retry logic (matching Python's get_model_response)\n      const maxRetries = 5;\n      const baseDelayS = 1;\n      let lastError: Error | null = null;\n      let response: GenerateContentResponse | null = null;\n\n      // Log LLM request\n      const llmRequestId = uuidv7();\n      FlowLogger.logLlmRequest({\n        requestId: llmRequestId,\n        model: this.modelName,\n        prompt: extractLlmCuaPromptSummary(compressedHistory),\n      });\n\n      for (let attempt = 0; attempt < maxRetries; attempt++) {\n        try {\n          // Add exponential backoff delay for retries\n          if (attempt > 0) {\n            const delay = baseDelayS * Math.pow(2, attempt) * 1000; // Convert to ms\n            logger({\n              category: \"agent\",\n              message: `Generating content failed on attempt ${attempt + 1}. Retrying in ${delay / 1000} seconds...`,\n              level: 2,\n            });\n            await new Promise((resolve) => setTimeout(resolve, delay));\n          }\n\n          // Use the SDK's generateContent method - following Python SDK pattern\n          response = await this.client.models.generateContent({\n            model: this.modelName,\n            contents: compressedHistory,\n            config: this.generateContentConfig,\n          });\n\n          // Check if we have valid response content\n          if (!response.candidates || response.candidates.length === 0) {\n            throw new LLMResponseError(\"agent\", \"Response has no candidates!\");\n          }\n\n          const candidate = response.candidates[0];\n          if (!candidate.content || !candidate.content.parts) {\n            const reason = candidate.finishReason || \"unknown\";\n            throw new LLMResponseError(\n              \"agent\",\n              `Response has no content (finish reason: ${reason})`,\n            );\n          }\n\n          // Success - we have a valid response\n          break;\n        } catch (error) {\n          lastError = error instanceof Error ? error : new Error(String(error));\n          logger({\n            category: \"agent\",\n            message: `API call error: ${lastError.message}`,\n            level: 2,\n          });\n\n          // If this was the last attempt, throw the error\n          if (attempt === maxRetries - 1) {\n            logger({\n              category: \"agent\",\n              message: `Generating content failed after ${maxRetries} attempts.`,\n              level: 0,\n            });\n            throw lastError;\n          }\n        }\n      }\n\n      if (!response) {\n        throw (\n          lastError || new Error(\"Failed to get response after all retries\")\n        );\n      }\n\n      const endTime = Date.now();\n      const elapsedMs = endTime - startTime;\n      const { usageMetadata } = response;\n\n      // Log LLM response\n      FlowLogger.logLlmResponse({\n        requestId: llmRequestId,\n        model: this.modelName,\n        output: extractLlmCuaResponseSummary(response),\n        inputTokens: usageMetadata?.promptTokenCount,\n        outputTokens: usageMetadata?.candidatesTokenCount,\n      });\n\n      // Process the response\n      const result = await this.processResponse(response, logger);\n\n      // Add model response to history\n      if (response.candidates && response.candidates[0]) {\n        // Sanitize any out-of-range coordinates in function calls before adding to history\n        const sanitizedContent = JSON.parse(\n          JSON.stringify(response.candidates[0].content),\n        );\n        if (sanitizedContent.parts) {\n          for (const part of sanitizedContent.parts) {\n            if (part.functionCall?.args) {\n              if (\n                typeof part.functionCall.args.x === \"number\" &&\n                part.functionCall.args.x > 999\n              ) {\n                part.functionCall.args.x = 999;\n              }\n              if (\n                typeof part.functionCall.args.y === \"number\" &&\n                part.functionCall.args.y > 999\n              ) {\n                part.functionCall.args.y = 999;\n              }\n            }\n          }\n        }\n        this.history.push(sanitizedContent);\n      }\n\n      // Execute actions and collect function responses\n      const functionResponses: Part[] = [];\n\n      if (result.actions.length > 0) {\n        let hasError = false;\n\n        // Execute all actions\n        for (let i = 0; i < result.actions.length; i++) {\n          const action = result.actions[i];\n\n          logger({\n            category: \"agent\",\n            message: `Executing action ${i + 1}/${result.actions.length}: ${action.type}`,\n            level: 2,\n          });\n\n          // Special handling for open_web_browser - don't execute it\n          if (action.type === \"open_web_browser\") {\n            // Set pageUrl for open_web_browser since it doesn't go through action handler\n            action.pageUrl = this.currentUrl;\n            logger({\n              category: \"agent\",\n              message: \"Skipping open_web_browser action\",\n              level: 2,\n            });\n          } else if (action.type === \"custom_tool\") {\n            const toolName = action.name as string;\n            const toolArgs = action.arguments as Record<string, unknown>;\n\n            if (this.tools && toolName in this.tools) {\n              const correspondingFunctionCall = result.functionCalls.find(\n                (fc) => fc.name === toolName,\n              );\n\n              if (correspondingFunctionCall) {\n                const executionResult = await executeGoogleCustomTool(\n                  toolName,\n                  toolArgs,\n                  this.tools,\n                  correspondingFunctionCall,\n                  logger,\n                );\n\n                functionResponses.push(executionResult.functionResponse);\n\n                if (!executionResult.success) {\n                  hasError = true;\n                }\n              }\n            }\n          } else if (this.actionHandler) {\n            try {\n              await this.actionHandler(action);\n\n              // Add a delay between actions to ensure they complete properly\n              // Longer delay for typing actions to ensure fields are ready\n              if (i < result.actions.length - 1) {\n                const nextAction = result.actions[i + 1];\n                const isTypingAction =\n                  action.type === \"type\" || nextAction.type === \"type\";\n                const delay = isTypingAction ? 500 : 200;\n                await new Promise((resolve) => setTimeout(resolve, delay));\n              }\n            } catch (actionError) {\n              if (actionError instanceof StagehandClosedError) {\n                throw actionError;\n              }\n              logger({\n                category: \"agent\",\n                message: `Error executing action ${action.type}: ${actionError}`,\n                level: 0,\n              });\n              hasError = true;\n              // Continue processing other actions even if one fails\n            }\n          }\n        }\n\n        // Create function responses for computer use actions (non-custom tools)\n        // We need exactly one response per function call, regardless of how many actions were generated\n        if (result.functionCalls.length > 0 || hasError) {\n          // Filter out custom tool function calls as they've already been handled\n          const computerUseFunctionCalls = result.functionCalls.filter(\n            (fc) => !isCustomTool(fc, this.tools),\n          );\n\n          if (computerUseFunctionCalls.length > 0) {\n            try {\n              logger({\n                category: \"agent\",\n                message: `Taking screenshot after executing ${result.actions.length} actions${hasError ? \" (with errors)\" : \"\"}`,\n                level: 2,\n              });\n\n              const screenshot = await this.captureScreenshot();\n              const base64Data = screenshot.replace(\n                /^data:image\\/png;base64,/,\n                \"\",\n              );\n\n              // Create one function response for each computer use function call\n              // Following Python SDK pattern: FunctionResponse with parts containing inline_data\n              for (const functionCall of computerUseFunctionCalls) {\n                let safetyAcknowledgement: string | undefined;\n                if (functionCall.args?.safety_decision) {\n                  safetyAcknowledgement = await this.handleSafetyConfirmation(\n                    functionCall.args.safety_decision,\n                    logger,\n                  );\n                }\n\n                const functionResponsePart: Part = {\n                  functionResponse: {\n                    name: functionCall.name,\n                    response: {\n                      url: this.currentUrl || \"\",\n                      ...(safetyAcknowledgement !== undefined\n                        ? {\n                            safety_acknowledgement: safetyAcknowledgement,\n                          }\n                        : {}),\n                    },\n                    parts: [\n                      {\n                        inlineData: {\n                          mimeType: \"image/png\",\n                          data: base64Data,\n                        },\n                      },\n                    ],\n                  },\n                };\n                functionResponses.push(functionResponsePart);\n              }\n            } catch (error) {\n              logger({\n                category: \"agent\",\n                message: `Error capturing screenshot: ${error}`,\n                level: 0,\n              });\n            }\n          }\n        }\n\n        // Add all function responses to history in a single user message\n        if (functionResponses.length > 0) {\n          logger({\n            category: \"agent\",\n            message: `Adding ${functionResponses.length} function responses to history`,\n            level: 2,\n          });\n          this.history.push({\n            role: \"user\",\n            parts: functionResponses,\n          });\n        }\n      }\n\n      return {\n        actions: result.actions,\n        message: result.message,\n        completed: result.completed,\n        usage: {\n          input_tokens: usageMetadata?.promptTokenCount || 0,\n          output_tokens: usageMetadata?.candidatesTokenCount || 0,\n          inference_time_ms: elapsedMs,\n        },\n      };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      logger({\n        category: \"agent\",\n        message: `Error executing step: ${errorMessage}`,\n        level: 0,\n      });\n\n      throw error;\n    }\n  }\n\n  /**\n   * Process the response from Google's API\n   */\n  private async processResponse(\n    response: GenerateContentResponse,\n    logger: (message: LogLine) => void,\n  ): Promise<{\n    actions: AgentAction[];\n    message: string;\n    completed: boolean;\n    functionCalls: FunctionCall[];\n  }> {\n    const actions: AgentAction[] = [];\n    let message = \"\";\n    const functionCalls: FunctionCall[] = [];\n\n    if (!response.candidates || response.candidates.length === 0) {\n      return {\n        actions: [],\n        message: \"No candidates in response\",\n        completed: true,\n        functionCalls: [],\n      };\n    }\n    const candidate = response.candidates[0];\n\n    // Log the raw response for debugging\n    logger({\n      category: \"agent\",\n      message: `Raw response from Google: ${JSON.stringify(candidate.content, null, 2)}`,\n      level: 2,\n    });\n\n    // Process all parts - Google can send multiple function calls\n    for (const part of candidate.content.parts) {\n      if (part.text) {\n        message += part.text + \"\\n\";\n        logger({\n          category: \"agent\",\n          message: `Reasoning: ${part.text}`,\n          level: 1,\n        });\n      }\n      if (part.functionCall) {\n        functionCalls.push(part.functionCall);\n        logger({\n          category: \"agent\",\n          message: `Found function call: ${part.functionCall.name} with args: ${JSON.stringify(part.functionCall.args)}`,\n          level: 2,\n        });\n\n        // Convert function call to action(s)\n        const action = this.convertFunctionCallToAction(part.functionCall);\n        if (action) {\n          // Special handling for type_text_at - we need to click first\n          if (\n            part.functionCall.name === \"type_text_at\" &&\n            action.type === \"type\"\n          ) {\n            logger({\n              category: \"agent\",\n              message: `Adding action: ${JSON.stringify(action)}`,\n              level: 2,\n            });\n            // First add a click action at the same coordinates\n            actions.push({\n              type: \"click\",\n              x: action.x,\n              y: action.y,\n              button: \"left\",\n            });\n\n            // If clear_before_typing is true (default), add a select all\n            if (action.clearBeforeTyping) {\n              // Select all text in the field\n              actions.push({\n                type: \"keypress\",\n                keys: [\"ControlOrMeta+A\"],\n              });\n              actions.push({\n                type: \"keypress\",\n                keys: [\"Backspace\"],\n              });\n            }\n\n            // Then add the type action\n            actions.push(action);\n            if (action.pressEnter) {\n              actions.push({\n                type: \"keypress\",\n                keys: [\"Enter\"],\n              });\n            }\n          } else {\n            actions.push(action);\n          }\n        } else {\n          logger({\n            category: \"agent\",\n            message: `Warning: Could not convert function call ${part.functionCall.name} to action`,\n            level: 1,\n          });\n        }\n      }\n    }\n\n    // Log summary of what we found\n    logger({\n      category: \"agent\",\n      message: `Found ${functionCalls.length} function calls, converted to ${actions.length} actions`,\n      level: 2,\n    });\n\n    // Check if task is completed\n    const completed =\n      functionCalls.length === 0 ||\n      (candidate.finishReason && candidate.finishReason !== \"STOP\");\n\n    return {\n      actions,\n      message: message.trim(),\n      completed,\n      functionCalls,\n    };\n  }\n\n  /**\n   * Convert Google function call to Stagehand action\n   */\n  private convertFunctionCallToAction(\n    functionCall: FunctionCall,\n  ): AgentAction | null {\n    const { name, args } = functionCall;\n\n    if (!name || !args) {\n      return null;\n    }\n\n    switch (name) {\n      case \"open_web_browser\":\n        return {\n          type: \"open_web_browser\",\n          timestamp: Date.now(),\n        };\n\n      case \"click_at\": {\n        const { x, y } = this.normalizeCoordinates(\n          args.x as number,\n          args.y as number,\n        );\n        return {\n          type: \"click\",\n          x,\n          y,\n          button: args.button || \"left\",\n        };\n      }\n\n      case \"type_text_at\": {\n        const { x, y } = this.normalizeCoordinates(\n          args.x as number,\n          args.y as number,\n        );\n        // Google's type_text_at includes press_enter and clear_before_typing parameters\n        const pressEnter = (args.press_enter as boolean) ?? false;\n        const clearBeforeTyping = (args.clear_before_typing as boolean) ?? true;\n\n        // For type_text_at, we need to click first then type\n        // This matches the behavior expected by Google's CUA\n        // We'll handle this in the executeStep method by converting to two actions\n        return {\n          type: \"type\",\n          text: args.text as string,\n          x,\n          y,\n          pressEnter,\n          clearBeforeTyping,\n        };\n      }\n\n      case \"key_combination\": {\n        const keys = (args.keys as string)\n          .split(\"+\")\n          .map((key: string) => key.trim())\n          .map((key: string) => mapKeyToPlaywright(key));\n        return {\n          type: \"keypress\",\n          keys,\n        };\n      }\n\n      case \"scroll_document\": {\n        const direction = (args.direction as string).toLowerCase();\n        return {\n          type: \"keypress\",\n          keys: [direction === \"up\" ? \"PageUp\" : \"PageDown\"],\n        };\n      }\n\n      case \"scroll_at\": {\n        const { x, y } = this.normalizeCoordinates(\n          args.x as number,\n          args.y as number,\n        );\n        const direction = ((args.direction as string) || \"down\").toLowerCase();\n        const magnitude =\n          typeof args.magnitude === \"number\" ? (args.magnitude as number) : 800;\n\n        let scroll_x = 0;\n        let scroll_y = 0;\n        if (direction === \"up\") {\n          scroll_y = -magnitude;\n        } else if (direction === \"down\") {\n          scroll_y = magnitude;\n        } else if (direction === \"left\") {\n          scroll_x = -magnitude;\n        } else if (direction === \"right\") {\n          scroll_x = magnitude;\n        } else {\n          // Default to down if unknown direction\n          scroll_y = magnitude;\n        }\n\n        return {\n          type: \"scroll\",\n          x,\n          y,\n          scroll_x,\n          scroll_y,\n        };\n      }\n\n      case \"navigate\":\n        return {\n          type: \"goto\",\n          url: args.url as string,\n        };\n\n      case \"go_back\":\n        return {\n          type: \"back\",\n        };\n\n      case \"go_forward\":\n        return {\n          type: \"forward\",\n        };\n\n      case \"wait_5_seconds\":\n        return {\n          type: \"wait\",\n          timeMs: 5000, // Google CUA waits for 5 seconds\n        };\n\n      case \"hover_at\": {\n        const { x, y } = this.normalizeCoordinates(\n          args.x as number,\n          args.y as number,\n        );\n        return {\n          type: \"move\",\n          x,\n          y,\n        };\n      }\n\n      case \"search\":\n        return {\n          type: \"goto\",\n          url: \"https://www.google.com\",\n        };\n\n      case \"drag_and_drop\": {\n        const startPoint = this.normalizeCoordinates(\n          args.x as number,\n          args.y as number,\n        );\n        const endPoint = this.normalizeCoordinates(\n          args.destination_x as number,\n          args.destination_y as number,\n        );\n        return {\n          type: \"drag\",\n          path: [\n            { x: startPoint.x, y: startPoint.y },\n            { x: endPoint.x, y: endPoint.y },\n          ],\n        };\n      }\n\n      default:\n        if (isCustomTool(functionCall, this.tools)) {\n          return {\n            type: \"custom_tool\",\n            name,\n            arguments: args,\n            timestamp: Date.now(),\n            pageUrl: this.currentUrl,\n          };\n        }\n        console.warn(`Unsupported Google CUA function: ${name}`);\n        return null;\n    }\n  }\n\n  /**\n   * Normalize coordinates from Google's 0-1000 range to viewport dimensions\n   */\n  private normalizeCoordinates(x: number, y: number): { x: number; y: number } {\n    const clampedX = Math.min(999, Math.max(0, x));\n    const clampedY = Math.min(999, Math.max(0, y));\n    return {\n      x: Math.floor((clampedX / 1000) * this.currentViewport.width),\n      y: Math.floor((clampedY / 1000) * this.currentViewport.height),\n    };\n  }\n\n  async captureScreenshot(options?: {\n    base64Image?: string;\n    currentUrl?: string;\n  }): Promise<string> {\n    // Update current URL if provided\n    if (options?.currentUrl) {\n      this.currentUrl = options.currentUrl;\n    }\n\n    // Use provided options if available\n    if (options?.base64Image) {\n      return `data:image/png;base64,${options.base64Image}`;\n    }\n\n    // Use the screenshot provider if available\n    if (this.screenshotProvider) {\n      try {\n        const base64Image = await this.screenshotProvider();\n        return `data:image/png;base64,${base64Image}`;\n      } catch (error) {\n        console.error(\"Error capturing screenshot:\", error);\n        throw error;\n      }\n    }\n\n    throw new AgentScreenshotProviderError(\n      \"`screenshotProvider` has not been set. \" +\n        \"Please call `setScreenshotProvider()` with a valid function that returns a base64-encoded image\",\n    );\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/MicrosoftCUAClient.ts",
    "content": "import OpenAI from \"openai\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport {\n  AgentAction,\n  AgentResult,\n  AgentType,\n  AgentExecutionOptions,\n} from \"../types/public/agent.js\";\nimport { ClientOptions } from \"../types/public/model.js\";\nimport { AgentClient } from \"./AgentClient.js\";\nimport { AgentScreenshotProviderError } from \"../types/public/sdkErrors.js\";\nimport { mapKeyToPlaywright } from \"./utils/cuaKeyMapping.js\";\nimport { ChatCompletionMessageParam } from \"openai/resources/chat/completions\";\n\n/**\n * Message types for FARA agent\n */\ninterface FaraMessage {\n  role: \"system\" | \"user\" | \"assistant\";\n  content: string | FaraMessageContent[];\n}\n\ninterface FaraMessageContent {\n  type: \"text\" | \"image_url\";\n  text?: string;\n  image_url?: {\n    url: string; // data:image/png;base64,...\n  };\n}\n\n/**\n * FARA function call structure (parsed from XML tags)\n */\ninterface FaraFunctionCall {\n  name: string; // Always \"computer_use\"\n  arguments: {\n    action: string;\n    thoughts?: string;\n    [key: string]: unknown;\n  };\n}\n\n/**\n * Client for FARA (Function-based Autonomous Research Agent) by Microsoft\n * This implementation uses OpenAI-compatible API with XML-based tool calling\n */\nexport class MicrosoftCUAClient extends AgentClient {\n  private apiKey: string;\n  private baseURL: string;\n  private client: OpenAI;\n  private currentViewport = { width: 1288, height: 711 };\n  private currentUrl?: string;\n  private screenshotProvider?: () => Promise<string>;\n  private actionHandler?: (action: AgentAction) => Promise<void>;\n\n  // Dual history system\n  private conversationHistory: FaraMessage[] = []; // Conceptual flow\n  private actionHistory: FaraMessage[] = []; // Raw model responses\n\n  private maxImages: number = 3;\n  private temperature: number = 0;\n  private facts: string[] = [];\n\n  // FARA-specific MLM processor config\n  private readonly MLM_PROCESSOR_IM_CFG = {\n    min_pixels: 3136,\n    max_pixels: 12845056,\n    patch_size: 14,\n    merge_size: 2,\n  };\n\n  // Resized dimensions for model input\n  private resizedViewport = { width: 1288, height: 711 };\n\n  constructor(\n    type: AgentType,\n    modelName: string,\n    userProvidedInstructions?: string,\n    clientOptions?: ClientOptions,\n  ) {\n    super(type, modelName || \"fara-7b\", userProvidedInstructions);\n\n    // Process client options\n    this.apiKey =\n      (clientOptions?.apiKey as string) ||\n      process.env.AZURE_API_KEY ||\n      process.env.FIREWORKS_API_KEY ||\n      \"\";\n    this.baseURL =\n      (clientOptions?.baseURL as string) ||\n      process.env.AZURE_ENDPOINT ||\n      process.env.FIREWORKS_ENDPOINT ||\n      \"\";\n\n    // Store client options for reference\n    this.clientOptions = {\n      apiKey: this.apiKey,\n      baseURL: this.baseURL,\n    };\n\n    // Validate API key\n    if (!this.apiKey || this.apiKey === \"\") {\n      throw new Error(\n        \"API key is required. Please provide it via clientOptions.apiKey or AZURE_API_KEY or FIREWORKS_API_KEY environment variables.\",\n      );\n    }\n\n    // Initialize the OpenAI client (FARA uses OpenAI-compatible API)\n    this.client = new OpenAI({\n      apiKey: this.apiKey,\n      baseURL: this.baseURL,\n    });\n\n    // Max images to keep in history\n    if (clientOptions?.maxImages !== undefined) {\n      this.maxImages = clientOptions.maxImages as number;\n    }\n\n    // Temperature\n    if (clientOptions?.temperature !== undefined) {\n      this.temperature = clientOptions.temperature as number;\n    }\n  }\n\n  setViewport(width: number, height: number): void {\n    this.currentViewport = { width, height };\n    // Compute resized viewport using smart_resize logic\n    this.resizedViewport = this.smartResize(width, height);\n  }\n\n  setCurrentUrl(url: string): void {\n    this.currentUrl = url;\n  }\n\n  setScreenshotProvider(provider: () => Promise<string>): void {\n    this.screenshotProvider = provider;\n  }\n\n  setActionHandler(handler: (action: AgentAction) => Promise<void>): void {\n    this.actionHandler = handler;\n  }\n\n  /**\n   * Smart resize algorithm from FARA\n   * Ensures dimensions are divisible by factor and within pixel limits\n   */\n  private smartResize(\n    width: number,\n    height: number,\n  ): { width: number; height: number } {\n    const { patch_size, merge_size, min_pixels, max_pixels } =\n      this.MLM_PROCESSOR_IM_CFG;\n    const factor = patch_size * merge_size;\n\n    const roundByFactor = (num: number, f: number) => Math.round(num / f) * f;\n    const ceilByFactor = (num: number, f: number) => Math.ceil(num / f) * f;\n    const floorByFactor = (num: number, f: number) => Math.floor(num / f) * f;\n\n    let h_bar = Math.max(factor, roundByFactor(height, factor));\n    let w_bar = Math.max(factor, roundByFactor(width, factor));\n\n    if (h_bar * w_bar > max_pixels) {\n      const beta = Math.sqrt((height * width) / max_pixels);\n      h_bar = floorByFactor(height / beta, factor);\n      w_bar = floorByFactor(width / beta, factor);\n    } else if (h_bar * w_bar < min_pixels) {\n      const beta = Math.sqrt(min_pixels / (height * width));\n      h_bar = ceilByFactor(height * beta, factor);\n      w_bar = ceilByFactor(width * beta, factor);\n    }\n\n    return { width: w_bar, height: h_bar };\n  }\n\n  /**\n   * Generate system prompt with tool description\n   * Simplified to match Python's minimal approach\n   */\n  private generateSystemPrompt(): string {\n    const { width, height } = this.resizedViewport;\n\n    // Base prompt - Minimalist like Python\n    let basePrompt = \"You are a helpful assistant.\";\n\n    // Add user-provided instructions if available\n    if (this.userProvidedInstructions) {\n      basePrompt = `${basePrompt}\\n\\n${this.userProvidedInstructions}`;\n    }\n\n    // Tool description from FaraComputerUse\n    const toolDescription = `Use a mouse and keyboard to interact with a computer, and take screenshots.\n* This is an interface to a desktop GUI. You do not have access to a terminal or applications menu. You must click on desktop icons to start applications.\n* Some applications may take time to start or process actions, so you may need to wait and take successive screenshots to see the results of your actions. E.g. if you click on Firefox and a window doesn't open, try wait and taking another screenshot.\n* The screen's resolution is ${width}x${height}.\n* Whenever you intend to move the cursor to click on an element like an icon, you should consult a screenshot to determine the coordinates of the element before moving the cursor.\n* If you tried clicking on a program or link but it failed to load, even after waiting, try adjusting your cursor position so that the tip of the cursor visually falls on the element that you want to click.\n* Make sure to click any buttons, links, icons, etc with the cursor tip in the center of the element. Don't click boxes on their edges unless asked.\n* When a separate scrollable container prominently overlays the webpage, if you want to scroll within it, you typically need to mouse_move() over it first and then scroll().\n* If a popup window appears that you want to close, if left_click() on the 'X' or close button doesn't work, try key(keys=['Escape']) to close it.\n* On some search bars, when you type(), you may need to press_enter=False and instead separately call left_click() on the search button to submit the search query. This is especially true of search bars that have auto-suggest popups for e.g. locations\n* For calendar widgets, you usually need to left_click() on arrows to move between months and left_click() on dates to select them; type() is not typically used to input dates there.`;\n\n    // Tool parameters description\n    const actionsDescription = `The action to perform. The available actions are:\n* \\`key\\`: Performs key down presses on the arguments passed in order, then performs key releases in reverse order. Includes \"Enter\", \"Alt\", \"Shift\", \"Tab\", \"Control\", \"Backspace\", \"Delete\", \"Escape\", \"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\", \"PageDown\", \"PageUp\", \"Shift\", etc.\n* \\`type\\`: Type a string of text on the keyboard.\n* \\`mouse_move\\`: Move the cursor to a specified (x, y) pixel coordinate on the screen.\n* \\`left_click\\`: Click the left mouse button.\n* \\`scroll\\`: Performs a scroll of the mouse scroll wheel.\n* \\`history_back\\`: Go back to the previous page in the browser history.\n* \\`pause_and_memorize_fact\\`: Pause and memorize a fact for future reference.\n* \\`visit_url\\`: Visit a specified URL.\n* \\`web_search\\`: Perform a web search with a specified query.\n* \\`wait\\`: Wait specified seconds for the change to happen.\n* \\`terminate\\`: Terminate the current task and report its completion status.`;\n\n    // Tool JSON schema\n    const toolSchema = {\n      name: \"computer_use\",\n      description: toolDescription,\n      parameters: {\n        type: \"object\",\n        required: [\"action\"],\n        properties: {\n          action: {\n            type: \"string\",\n            description: actionsDescription,\n            enum: [\n              \"key\",\n              \"type\",\n              \"mouse_move\",\n              \"left_click\",\n              \"scroll\",\n              \"visit_url\",\n              \"web_search\",\n              \"history_back\",\n              \"pause_and_memorize_fact\",\n              \"wait\",\n              \"terminate\",\n            ],\n          },\n          keys: {\n            type: \"array\",\n            description: \"Required only by `action=key`.\",\n          },\n          text: {\n            type: \"string\",\n            description: \"Required only by `action=type`.\",\n          },\n          press_enter: {\n            type: \"boolean\",\n            description:\n              \"Whether to press the Enter key after typing. Required only by `action=type`.\",\n          },\n          delete_existing_text: {\n            type: \"boolean\",\n            description:\n              \"Whether to delete existing text before typing. Required only by `action=type`.\",\n          },\n          coordinate: {\n            type: \"array\",\n            description:\n              \"(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to move the mouse to. Required only by `action=left_click`, `action=mouse_move`, and `action=type`.\",\n          },\n          pixels: {\n            type: \"number\",\n            description:\n              \"The amount of scrolling to perform. Positive values scroll up, negative values scroll down. Required only by `action=scroll`.\",\n          },\n          fact: {\n            type: \"string\",\n            description:\n              \"The fact to remember for the future. Required only by `action=pause_and_memorize_fact`.\",\n          },\n          time: {\n            type: \"number\",\n            description: \"The seconds to wait. Required only by `action=wait`.\",\n          },\n          status: {\n            type: \"string\",\n            description:\n              \"The status of the task. Required only by `action=terminate`.\",\n            enum: [\"success\", \"failure\"],\n          },\n        },\n      },\n    };\n\n    // Format as FARA function calling template (FN_CALL_TEMPLATE format)\n    const toolDescs = JSON.stringify(toolSchema, null, 2);\n    const functionCallTemplate = `\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n${toolDescs}\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{{\"name\": <function-name>, \"arguments\": <args-json-object>}}\n</tool_call>`;\n\n    return `${basePrompt}\\n\\n${functionCallTemplate}`;\n  }\n\n  /**\n   * Parse thoughts and action from model response\n   * FARA uses XML-based tool calling: <tool_call>\\n{...}\\n</tool_call>\n   */\n  private parseThoughtsAndAction(response: string): {\n    thoughts: string;\n    functionCall: FaraFunctionCall;\n  } {\n    try {\n      const parts = response.split(\"<tool_call>\\n\");\n      const thoughts = parts[0].trim();\n      const actionText = parts[1].split(\"\\n</tool_call>\")[0].trim();\n\n      let parsedAction;\n      try {\n        parsedAction = JSON.parse(actionText);\n      } catch (jsonError) {\n        // Fix common malformed JSON: double opening brackets {{\"name\": ...}}\n        // This happens when the model adds an extra opening brace\n        if (actionText.startsWith(\"{{\") && actionText.endsWith(\"}\")) {\n          // Remove the extra opening brace\n          const fixedText = actionText.slice(1);\n          try {\n            parsedAction = JSON.parse(fixedText);\n          } catch (retryError) {\n            throw new Error(\n              `Failed to parse action text even after fixing double brackets. Original: ${actionText}. Fixed: ${fixedText}. Error: ${retryError}`,\n              { cause: retryError },\n            );\n          }\n        } else {\n          throw new Error(\n            `Failed to parse action text as JSON: ${actionText}. Error: ${jsonError}`,\n            { cause: jsonError },\n          );\n        }\n      }\n\n      return {\n        thoughts,\n        functionCall: {\n          name: parsedAction.name || \"computer_use\",\n          arguments: {\n            ...parsedAction.arguments,\n            thoughts,\n          },\n        },\n      };\n    } catch (error) {\n      throw new Error(\n        `Failed to parse FARA tool call from response: ${response}. Error: ${error}`,\n        { cause: error },\n      );\n    }\n  }\n\n  /**\n   * Convert FARA function call to Stagehand AgentAction\n   */\n  private convertFunctionCallToAction(\n    functionCall: FaraFunctionCall,\n  ): AgentAction {\n    const args = functionCall.arguments;\n    const action = args.action as string;\n\n    // Transform coordinates from resized to original viewport\n    const transformCoordinate = (coord: number[]): number[] => {\n      if (!coord || coord.length !== 2) return coord;\n      const [x, y] = coord;\n      const scaleX = this.currentViewport.width / this.resizedViewport.width;\n      const scaleY = this.currentViewport.height / this.resizedViewport.height;\n      return [Math.round(x * scaleX), Math.round(y * scaleY)];\n    };\n\n    const baseAction = {\n      type: action,\n      reasoning: args.thoughts as string,\n    };\n\n    switch (action) {\n      case \"left_click\": {\n        const clickCoord = transformCoordinate(args.coordinate as number[]);\n        return {\n          ...baseAction,\n          type: \"click\",\n          x: clickCoord[0],\n          y: clickCoord[1],\n          button: \"left\" as const,\n        };\n      }\n\n      case \"mouse_move\": {\n        const moveCoord = transformCoordinate(args.coordinate as number[]);\n        return {\n          ...baseAction,\n          type: \"move\",\n          coordinate: moveCoord,\n        };\n      }\n\n      case \"type\": {\n        const typeCoord = args.coordinate\n          ? transformCoordinate(args.coordinate as number[])\n          : undefined;\n        return {\n          ...baseAction,\n          text: args.text as string,\n          ...(typeCoord && { x: typeCoord[0], y: typeCoord[1] }),\n          press_enter:\n            args.press_enter !== undefined\n              ? (args.press_enter as boolean)\n              : true,\n          ...(args.delete_existing_text !== undefined && {\n            delete_existing_text: args.delete_existing_text as boolean,\n          }),\n        };\n      }\n\n      case \"key\":\n      case \"keypress\": {\n        const keys = (args.keys as string[]) || [];\n        // Normalize keys to Playwright format\n        const normalizedKeys = keys.map((k) => mapKeyToPlaywright(k));\n        return {\n          ...baseAction,\n          type: \"keypress\",\n          keys: normalizedKeys,\n        };\n      }\n\n      case \"scroll\": {\n        const pixels = (args.pixels as number) || 0;\n        // FARA: positive = scroll up, negative = scroll down\n        // Convert to scroll_x/scroll_y\n        return {\n          ...baseAction,\n          scroll_x: 0,\n          scroll_y: -pixels, // Invert: negative pixels = scroll down\n        };\n      }\n\n      case \"visit_url\": {\n        let url = args.url as string;\n        // Enhanced URL processing like Python\n        if (\n          !url.startsWith(\"https://\") &&\n          !url.startsWith(\"http://\") &&\n          !url.startsWith(\"file://\") &&\n          !url.startsWith(\"about:\")\n        ) {\n          // If URL contains space, treat as search query\n          if (url.includes(\" \")) {\n            url = `https://www.bing.com/search?q=${encodeURIComponent(url)}&FORM=QBLH`;\n          } else {\n            // Otherwise prefix with https://\n            url = \"https://\" + url;\n          }\n        }\n        return {\n          ...baseAction,\n          type: \"goto\",\n          url,\n        };\n      }\n\n      case \"web_search\": {\n        // Convert web search to visit_url with Bing search\n        const query = args.query as string;\n        const searchUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&FORM=QBLH`;\n        return {\n          ...baseAction,\n          type: \"goto\",\n          url: searchUrl,\n        };\n      }\n\n      case \"history_back\":\n        return {\n          ...baseAction,\n          type: \"back\",\n        };\n\n      case \"wait\": {\n        // Support both 'time' and 'duration' parameters with default (matches Python)\n        const durationSeconds =\n          (args.time as number) || (args.duration as number) || 3.0;\n        return {\n          ...baseAction,\n          timeMs: durationSeconds * 1000, // Convert seconds to ms\n        };\n      }\n\n      case \"pause_and_memorize_fact\": {\n        // Store the fact for future reference (matches Python)\n        const fact = args.fact as string;\n        this.facts.push(fact);\n        return {\n          ...baseAction,\n          fact,\n        };\n      }\n\n      case \"terminate\":\n        return {\n          ...baseAction,\n          status: args.status as string,\n        };\n\n      default:\n        return {\n          ...baseAction,\n          ...args,\n        };\n    }\n  }\n\n  /**\n   * Capture a screenshot and return as base64 data URL\n   */\n  async captureScreenshot(): Promise<string> {\n    if (!this.screenshotProvider) {\n      throw new AgentScreenshotProviderError(\"Screenshot provider not set\");\n    }\n\n    const base64Screenshot = await this.screenshotProvider();\n    return `data:image/png;base64,${base64Screenshot}`;\n  }\n\n  /**\n   * Remove old screenshots from history\n   * Matches Python's maybe_remove_old_screenshots\n   */\n  private maybeRemoveOldScreenshots(\n    history: FaraMessage[],\n    includesCurrent: boolean = false,\n  ): FaraMessage[] {\n    if (this.maxImages <= 0) {\n      return history;\n    }\n\n    const maxImages = includesCurrent ? this.maxImages : this.maxImages - 1;\n    const newHistory: FaraMessage[] = [];\n    let nImages = 0;\n\n    // Iterate backwards\n    for (let i = history.length - 1; i >= 0; i--) {\n      const msg = history[i];\n\n      // Check if message has image\n      let hasImage = false;\n      if (Array.isArray(msg.content)) {\n        hasImage = msg.content.some((c) => c.type === \"image_url\");\n      }\n\n      if (i === 0 && nImages >= maxImages) {\n        // First message (task) - preserve text, remove image\n        if (Array.isArray(msg.content)) {\n          const newContent = msg.content.filter((c) => c.type !== \"image_url\");\n          // If no content left, skip (unless it's the only message, but Python logic says continue)\n          if (newContent.length === 0) {\n            continue;\n          }\n          newHistory.push({ ...msg, content: newContent });\n        } else {\n          newHistory.push(msg);\n        }\n        continue;\n      }\n\n      if (hasImage) {\n        if (nImages < maxImages) {\n          newHistory.push(msg);\n          nImages++;\n        } else {\n          // Remove image, keep text\n          if (Array.isArray(msg.content)) {\n            const newContent = msg.content.filter(\n              (c) => c.type !== \"image_url\",\n            );\n            // If content becomes empty, we can skip this message entirely (unless it's meaningful text)\n            // Python logic: if msg is None continue.\n            if (newContent.length > 0) {\n              newHistory.push({ ...msg, content: newContent });\n            }\n          } else {\n            newHistory.push(msg);\n          }\n        }\n      } else {\n        newHistory.push(msg);\n      }\n    }\n\n    return newHistory.reverse();\n  }\n\n  /**\n   * Reconstruct history for API call\n   * Merges conceptual chat history with raw action history\n   */\n  private reconstructHistory(): FaraMessage[] {\n    const history: FaraMessage[] = [];\n    let actionTurn = 0;\n\n    for (let i = 0; i < this.conversationHistory.length; i++) {\n      const m = this.conversationHistory[i];\n      if (m.role === \"assistant\") {\n        if (actionTurn >= this.actionHistory.length) {\n          // Should not happen if synced correctly\n          console.warn(\"OUT OF SYNC: Action history shorter than chat history\");\n          history.push(m);\n        } else {\n          history.push(this.actionHistory[actionTurn]);\n          actionTurn++;\n        }\n      } else {\n        history.push(m);\n      }\n    }\n\n    return this.maybeRemoveOldScreenshots(history);\n  }\n\n  /**\n   * Execute a single step\n   */\n  private async executeStep(\n    logger: (message: LogLine) => void,\n    isFirstRound: boolean = false,\n  ): Promise<{\n    actions: AgentAction[];\n    completed: boolean;\n    usage: {\n      input_tokens: number;\n      output_tokens: number;\n      inference_time_ms: number;\n    };\n  }> {\n    // Capture screenshot\n    const screenshotDataUrl = await this.captureScreenshot();\n\n    // Update conversation history with new screenshot/message\n    if (isFirstRound) {\n      // First round: modify the last message (initial user instruction) to include screenshot\n      const lastMessage =\n        this.conversationHistory[this.conversationHistory.length - 1];\n      if (lastMessage && lastMessage.role === \"user\") {\n        const originalContent =\n          typeof lastMessage.content === \"string\"\n            ? lastMessage.content\n            : (lastMessage.content.find((c) => c.type === \"text\")?.text ??\n              \"Start task\");\n\n        lastMessage.content = [\n          {\n            type: \"image_url\",\n            image_url: { url: screenshotDataUrl },\n          },\n          {\n            type: \"text\",\n            text: originalContent,\n          },\n        ];\n      }\n    } else {\n      // Subsequent rounds: add new user message with screenshot\n      const userContent: FaraMessageContent[] = [\n        {\n          type: \"image_url\",\n          image_url: { url: screenshotDataUrl },\n        },\n      ];\n\n      // Add current URL if available\n      let textPrompt =\n        \"Here is the next screenshot. Think about what to do next.\";\n      if (this.currentUrl) {\n        const trimmedUrl =\n          this.currentUrl.length > 100\n            ? this.currentUrl.slice(0, 100) + \"...\"\n            : this.currentUrl;\n        textPrompt = `Current URL: ${trimmedUrl}\\n${textPrompt}`;\n      }\n\n      userContent.push({\n        type: \"text\",\n        text: textPrompt,\n      });\n\n      this.conversationHistory.push({\n        role: \"user\",\n        content: userContent,\n      });\n    }\n\n    // Reconstruct history for model call\n    let history = this.reconstructHistory();\n\n    // Prepend system prompt (generated fresh)\n    const systemMessage: FaraMessage = {\n      role: \"system\",\n      content: this.generateSystemPrompt(),\n    };\n    history = [systemMessage, ...history];\n\n    // Make API call\n    logger({\n      category: \"agent\",\n      message: `Making API call to FARA model with ${history.length} messages`,\n      level: 2,\n    });\n\n    const startTime = Date.now();\n    let response;\n    try {\n      response = await this.client.chat.completions.create({\n        model: this.modelName,\n        messages: history as unknown as ChatCompletionMessageParam[],\n        temperature: this.temperature,\n      });\n    } catch (apiError) {\n      logger({\n        category: \"agent\",\n        message: `API call failed: ${apiError instanceof Error ? apiError.message : String(apiError)}`,\n        level: 0,\n      });\n      throw apiError;\n    }\n    const inferenceTime = Date.now() - startTime;\n\n    logger({\n      category: \"agent\",\n      message: `API call completed in ${inferenceTime}ms`,\n      level: 2,\n    });\n\n    const content = response.choices[0].message.content || \"\";\n    const usage = response.usage || {\n      prompt_tokens: 0,\n      completion_tokens: 0,\n      total_tokens: 0,\n    };\n\n    // Add assistant response to both histories\n    const assistantMsg: FaraMessage = {\n      role: \"assistant\",\n      content,\n    };\n    this.conversationHistory.push(assistantMsg);\n    this.actionHistory.push(assistantMsg);\n\n    logger({\n      category: \"agent\",\n      message: `Model response: ${content}`,\n      level: 2,\n    });\n\n    // Parse tool call\n    const { thoughts, functionCall } = this.parseThoughtsAndAction(content);\n\n    logger({\n      category: \"agent\",\n      message: `Thoughts: ${thoughts}`,\n      level: 2,\n    });\n\n    logger({\n      category: \"agent\",\n      message: `Action: ${JSON.stringify(functionCall.arguments)}`,\n      level: 2,\n    });\n\n    // Convert to AgentAction\n    const agentAction = this.convertFunctionCallToAction(functionCall);\n\n    // Expand type action into multiple actions if it has coordinates\n    const actions: AgentAction[] = [];\n    if (\n      agentAction.type === \"type\" &&\n      typeof agentAction.x === \"number\" &&\n      typeof agentAction.y === \"number\"\n    ) {\n      // First, click at the coordinates to focus the field\n      actions.push({\n        type: \"click\",\n        x: agentAction.x,\n        y: agentAction.y,\n        button: \"left\",\n      });\n\n      // If delete_existing_text is true, clear the field first\n      if (agentAction.delete_existing_text) {\n        actions.push({\n          type: \"keypress\",\n          keys: [\"Command+A\"],\n        });\n        actions.push({\n          type: \"keypress\",\n          keys: [\"Backspace\"],\n        });\n      }\n\n      // Add the type action (without coordinates since we already clicked)\n      actions.push({\n        type: \"type\",\n        text: agentAction.text,\n      });\n\n      // If press_enter is true (default), press Enter after typing\n      if (agentAction.press_enter !== false) {\n        actions.push({\n          type: \"keypress\",\n          keys: [\"Enter\"],\n        });\n      }\n    } else {\n      // For all other actions, just add as-is\n      actions.push(agentAction);\n    }\n\n    // Execute all actions if handler is available\n    if (this.actionHandler && agentAction.type !== \"terminate\") {\n      for (const action of actions) {\n        await this.actionHandler(action);\n      }\n    }\n\n    // Check if completed\n    const completed = functionCall.arguments.action === \"terminate\";\n\n    return {\n      actions,\n      completed,\n      usage: {\n        input_tokens: usage.prompt_tokens,\n        output_tokens: usage.completion_tokens,\n        inference_time_ms: inferenceTime,\n      },\n    };\n  }\n\n  /**\n   * Execute a task with the FARA CUA\n   * This is the main entry point for the agent\n   * @implements AgentClient.execute\n   */\n  async execute(executionOptions: AgentExecutionOptions): Promise<AgentResult> {\n    const { options, logger } = executionOptions;\n    const { instruction } = options;\n    const maxSteps = options.maxSteps || 10;\n\n    let currentStep = 0;\n    let completed = false;\n    const actions: AgentAction[] = [];\n    const messageList: string[] = [];\n    let finalMessage: string;\n    let totalInputTokens = 0;\n    let totalOutputTokens = 0;\n    let totalInferenceTime = 0;\n\n    // Initialize conversation with user instruction\n    // System prompt is NOT added here, it's added dynamically in executeStep\n    this.conversationHistory = [\n      {\n        role: \"user\",\n        content: instruction,\n      },\n    ];\n    this.actionHistory = [];\n\n    try {\n      // Execute steps until completion or max steps reached\n      while (!completed && currentStep < maxSteps) {\n        await this.preStepHook?.();\n\n        logger({\n          category: \"agent\",\n          message: `Executing step ${currentStep + 1}/${maxSteps}`,\n          level: 1,\n        });\n\n        const isFirstRound = currentStep === 0;\n        const result = await this.executeStep(logger, isFirstRound);\n        totalInputTokens += result.usage.input_tokens;\n        totalOutputTokens += result.usage.output_tokens;\n        totalInferenceTime += result.usage.inference_time_ms;\n\n        // Add actions to the list\n        actions.push(...result.actions);\n\n        // Update completion status\n        completed = result.completed;\n\n        currentStep++;\n\n        // Record message for this step\n        const lastAction = result.actions[result.actions.length - 1];\n        if (lastAction?.reasoning) {\n          messageList.push(lastAction.reasoning);\n        }\n      }\n\n      // Generate final message\n      if (completed) {\n        const lastAction = actions[actions.length - 1];\n        finalMessage =\n          (lastAction as { status?: string })?.status === \"success\"\n            ? \"Task completed successfully.\"\n            : \"Task completed with failures.\";\n      } else {\n        finalMessage = `Reached maximum steps (${maxSteps}) without completion.`;\n      }\n\n      if (messageList.length > 0) {\n        finalMessage = `${messageList.join(\"\\n\\n\")}\\n\\n${finalMessage}`;\n      }\n\n      return {\n        success: completed,\n        completed,\n        message: finalMessage,\n        actions,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    } catch (error) {\n      logger({\n        category: \"agent\",\n        message: `Error during execution: ${error}`,\n        level: 0,\n      });\n\n      // Rethrow to allow eval runner's retry logic to handle transient errors\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/OpenAICUAClient.ts",
    "content": "import OpenAI from \"openai\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport {\n  AgentAction,\n  AgentResult,\n  AgentType,\n  AgentExecutionOptions,\n  ResponseInputItem,\n  ResponseItem,\n  ComputerCallItem,\n  FunctionCallItem,\n  SafetyCheck,\n  SafetyConfirmationHandler,\n} from \"../types/public/agent.js\";\nimport { ClientOptions } from \"../types/public/model.js\";\nimport { AgentClient } from \"./AgentClient.js\";\nimport {\n  AgentScreenshotProviderError,\n  StagehandClosedError,\n} from \"../types/public/sdkErrors.js\";\nimport { ToolSet } from \"ai\";\nimport {\n  FlowLogger,\n  extractLlmCuaPromptSummary,\n  extractLlmCuaResponseSummary,\n} from \"../flowlogger/FlowLogger.js\";\nimport { v7 as uuidv7 } from \"uuid\";\n\n/**\n * Client for OpenAI's Computer Use Assistant API\n * This implementation uses the official OpenAI Responses API for Computer Use\n */\nconst CAPTCHA_PROCEED_TOOL = \"captchaSolvedProceed\";\n\nexport class OpenAICUAClient extends AgentClient {\n  private pendingContextNotes: string[] = [];\n  private captchaSolvedToolActive = false;\n  private apiKey: string;\n  private organization?: string;\n  private baseURL: string;\n  private client: OpenAI;\n  public lastResponseId?: string;\n  private currentViewport = { width: 1288, height: 711 };\n  private currentUrl?: string;\n  private screenshotProvider?: () => Promise<string>;\n  private actionHandler?: (action: AgentAction) => Promise<void>;\n  private reasoningItems: Map<string, ResponseItem> = new Map();\n  private environment: string = \"browser\"; // \"browser\", \"mac\", \"windows\", or \"ubuntu\"\n  private tools?: ToolSet;\n  private safetyConfirmationHandler?: SafetyConfirmationHandler;\n\n  constructor(\n    type: AgentType,\n    modelName: string,\n    userProvidedInstructions?: string,\n    clientOptions?: ClientOptions,\n    tools?: ToolSet,\n  ) {\n    super(type, modelName, userProvidedInstructions);\n\n    // Process client options\n    this.apiKey =\n      (clientOptions?.apiKey as string) || process.env.OPENAI_API_KEY || \"\";\n    this.baseURL = (clientOptions?.baseURL as string) || undefined;\n    this.organization =\n      (clientOptions?.organization as string) || process.env.OPENAI_ORG;\n\n    // Get environment if specified\n    if (\n      clientOptions?.environment &&\n      typeof clientOptions.environment === \"string\"\n    ) {\n      this.environment = clientOptions.environment;\n    }\n\n    // Store client options for reference\n    this.clientOptions = {\n      apiKey: this.apiKey,\n    };\n\n    if (this.baseURL) {\n      this.clientOptions.baseURL = this.baseURL;\n    }\n\n    // Initialize the OpenAI client\n    this.client = new OpenAI(this.clientOptions);\n\n    this.tools = tools;\n  }\n\n  setViewport(width: number, height: number): void {\n    this.currentViewport = { width, height };\n  }\n\n  setCurrentUrl(url: string): void {\n    this.currentUrl = url;\n  }\n\n  setScreenshotProvider(provider: () => Promise<string>): void {\n    this.screenshotProvider = provider;\n  }\n\n  setActionHandler(handler: (action: AgentAction) => Promise<void>): void {\n    this.actionHandler = handler;\n  }\n\n  setTools(tools: ToolSet): void {\n    this.tools = tools;\n  }\n\n  setSafetyConfirmationHandler(handler?: SafetyConfirmationHandler): void {\n    this.safetyConfirmationHandler = handler;\n  }\n\n  addContextNote(note: string): void {\n    this.pendingContextNotes.push(note);\n\n    // When a captcha-related note arrives, expose a tool that the model can\n    // call instead of asking the user for confirmation.  This replaces\n    // fragile English-phrase parsing with a structured tool call.\n    if (note.toLowerCase().includes(\"captcha\")) {\n      this.captchaSolvedToolActive = true;\n    }\n  }\n\n  /**\n   * Execute a task with the OpenAI CUA\n   * This is the main entry point for the agent\n   * @implements AgentClient.execute\n   */\n  async execute(executionOptions: AgentExecutionOptions): Promise<AgentResult> {\n    const { options, logger } = executionOptions;\n    const { instruction } = options;\n    const maxSteps = options.maxSteps || 10;\n\n    let currentStep = 0;\n    let completed = false;\n    const actions: AgentAction[] = [];\n    const messageList: string[] = [];\n    let finalMessage = \"\";\n    this.reasoningItems.clear(); // Clear any previous reasoning items\n\n    // Start with the initial instruction\n    let inputItems = this.createInitialInputItems(instruction);\n    let previousResponseId: string | undefined = undefined;\n    let totalInputTokens = 0;\n    let totalOutputTokens = 0;\n    let totalInferenceTime = 0;\n\n    try {\n      // Execute steps until completion or max steps reached\n      while (!completed && currentStep < maxSteps) {\n        await this.preStepHook?.();\n\n        logger({\n          category: \"agent\",\n          message: `Executing step ${currentStep + 1}/${maxSteps}`,\n          level: 1,\n        });\n\n        const result = await this.executeStep(\n          inputItems,\n          previousResponseId,\n          logger,\n        );\n        totalInputTokens += result.usage.input_tokens;\n        totalOutputTokens += result.usage.output_tokens;\n        totalInferenceTime += result.usage.inference_time_ms;\n\n        // Add actions to the list\n        actions.push(...result.actions);\n\n        // Update completion status\n        completed = result.completed;\n\n        // Store the previous response ID for the next request\n        previousResponseId = result.responseId;\n\n        // Update the input items for the next step if we're continuing\n        if (!completed) {\n          inputItems = result.nextInputItems;\n          const contextNotes = this.drainContextNotes();\n          if (contextNotes.length > 0) {\n            inputItems = [\n              ...inputItems,\n              ...contextNotes.map((note) => ({\n                role: \"user\" as const,\n                content: note,\n              })),\n            ];\n          }\n        }\n\n        // Record any message for this step\n        if (result.message) {\n          messageList.push(result.message);\n          finalMessage = result.message;\n        }\n\n        // Increment step counter\n        currentStep++;\n      }\n\n      // Return the final result\n      return {\n        success: completed,\n        actions,\n        message: finalMessage,\n        completed,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      logger({\n        category: \"agent\",\n        message: `Error executing agent task: ${errorMessage}`,\n        level: 0,\n      });\n\n      return {\n        success: false,\n        actions,\n        message: `Failed to execute task: ${errorMessage}`,\n        completed: false,\n        usage: {\n          input_tokens: totalInputTokens,\n          output_tokens: totalOutputTokens,\n          inference_time_ms: totalInferenceTime,\n        },\n      };\n    }\n  }\n\n  /**\n   * Execute a single step of the agent\n   * This coordinates the flow: Request → Get Action → Execute Action\n   */\n  async executeStep(\n    inputItems: ResponseInputItem[],\n    previousResponseId: string | undefined,\n    logger: (message: LogLine) => void,\n  ): Promise<{\n    actions: AgentAction[];\n    message: string;\n    completed: boolean;\n    nextInputItems: ResponseInputItem[];\n    responseId: string;\n    usage: {\n      input_tokens: number;\n      output_tokens: number;\n      inference_time_ms: number;\n    };\n  }> {\n    try {\n      // Get response from the model\n      const result = await this.getAction(inputItems, previousResponseId);\n      const output = result.output;\n      const responseId = result.responseId;\n      const usage = {\n        input_tokens: result.usage.input_tokens,\n        output_tokens: result.usage.output_tokens,\n        inference_time_ms: result.usage.inference_time_ms,\n      };\n\n      // Add any reasoning items to our map\n      for (const item of output) {\n        if (item.type === \"reasoning\") {\n          this.reasoningItems.set(item.id, item);\n          logger({\n            category: \"agent\",\n            message: `Reasoning: ${String(item.content || \"\")}`,\n            level: 1,\n          });\n        }\n      }\n\n      // Extract actions from the output\n      const stepActions: AgentAction[] = [];\n      for (const item of output) {\n        if (item.type === \"computer_call\" && this.isComputerCallItem(item)) {\n          logger({\n            category: \"agent\",\n            message: `Found computer_call: ${item.action.type}, payload: ${JSON.stringify(item.action)}, call_id: ${item.call_id}`,\n            level: 2,\n          });\n          const action = this.convertComputerCallToAction(item);\n          if (action) {\n            stepActions.push(action);\n            logger({\n              category: \"agent\",\n              message: `Converted computer_call to action: ${action.type}`,\n              level: 2,\n            });\n          }\n        } else if (\n          item.type === \"function_call\" &&\n          this.isFunctionCallItem(item)\n        ) {\n          logger({\n            category: \"agent\",\n            message: `Found function_call: ${item.name}, call_id: ${item.call_id}`,\n            level: 2,\n          });\n          const action = this.convertFunctionCallToAction(item);\n          if (action) {\n            stepActions.push(action);\n            logger({\n              category: \"agent\",\n              message: `Converted function_call to action: ${action.type}`,\n              level: 2,\n            });\n          }\n        }\n      }\n\n      // Extract message text\n      let message = \"\";\n      for (const item of output) {\n        if (item.type === \"message\") {\n          logger({\n            category: \"agent\",\n            message: `Found message block`,\n            level: 2,\n          });\n          if (item.content && Array.isArray(item.content)) {\n            for (const content of item.content) {\n              if (content.type === \"output_text\" && content.text) {\n                message += content.text + \"\\n\";\n                logger({\n                  category: \"agent\",\n                  message: `Message text: ${String(content.text || \"\")}`,\n                  level: 1,\n                });\n              }\n            }\n          }\n        }\n      }\n\n      // Take actions and get results\n      const nextInputItems = await this.takeAction(output, logger);\n\n      // Check if completed\n      const completed =\n        output.length === 0 ||\n        output.every(\n          (item) => item.type === \"message\" || item.type === \"reasoning\",\n        );\n\n      return {\n        actions: stepActions,\n        message: message.trim(),\n        completed,\n        nextInputItems,\n        responseId,\n        usage: usage,\n      };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      logger({\n        category: \"agent\",\n        message: `Error executing step: ${errorMessage}`,\n        level: 0,\n      });\n\n      throw error;\n    }\n  }\n\n  private isComputerCallItem(item: ResponseItem): item is ComputerCallItem {\n    return (\n      item.type === \"computer_call\" &&\n      \"call_id\" in item &&\n      \"action\" in item &&\n      typeof item.action === \"object\"\n    );\n  }\n\n  private async handleSafetyConfirmation(\n    pendingSafetyChecks: SafetyCheck[],\n    logger: (message: LogLine) => void,\n  ): Promise<SafetyCheck[] | undefined> {\n    if (this.safetyConfirmationHandler) {\n      logger({\n        category: \"agent\",\n        message: `Requesting safety confirmation for ${pendingSafetyChecks.length} check(s): ${pendingSafetyChecks.map((c) => c.code).join(\", \")}`,\n        level: 1,\n      });\n\n      const response =\n        await this.safetyConfirmationHandler(pendingSafetyChecks);\n\n      if (response.acknowledged) {\n        logger({\n          category: \"agent\",\n          message: `Safety checks acknowledged by user`,\n          level: 1,\n        });\n        return pendingSafetyChecks;\n      } else {\n        logger({\n          category: \"agent\",\n          message: `Safety checks rejected by user`,\n          level: 1,\n        });\n        return undefined;\n      }\n    }\n\n    logger({\n      category: \"agent\",\n      message: `Auto-acknowledging ${pendingSafetyChecks.length} safety check(s)`,\n      level: 2,\n    });\n    return pendingSafetyChecks;\n  }\n\n  private isFunctionCallItem(item: ResponseItem): item is FunctionCallItem {\n    return (\n      item.type === \"function_call\" &&\n      \"call_id\" in item &&\n      \"name\" in item &&\n      \"arguments\" in item\n    );\n  }\n\n  private createInitialInputItems(instruction: string): ResponseInputItem[] {\n    // For the initial request, we use a simple array with the user's instruction\n    return [\n      {\n        role: \"system\",\n        content: this.userProvidedInstructions,\n      },\n      {\n        role: \"user\",\n        content: instruction,\n      },\n    ];\n  }\n\n  async getAction(\n    inputItems: ResponseInputItem[],\n    previousResponseId?: string,\n  ): Promise<{\n    output: ResponseItem[];\n    responseId: string;\n    usage: Record<string, number>;\n  }> {\n    try {\n      // Create the request parameters\n      const requestParams: Record<string, unknown> = {\n        model: this.modelName,\n        tools: [\n          {\n            type: \"computer_use_preview\",\n            display_width: this.currentViewport.width,\n            display_height: this.currentViewport.height,\n            environment: this.environment,\n          },\n        ],\n        input: inputItems,\n        truncation: \"auto\",\n      };\n\n      // Add custom tools if available\n      if (this.tools && Object.keys(this.tools).length > 0) {\n        const customTools = Object.entries(this.tools).map(([name, tool]) => ({\n          type: \"function\" as const,\n          name,\n          function: {\n            name,\n            description: tool.description,\n            parameters: tool.inputSchema,\n          },\n        }));\n\n        requestParams.tools = [\n          ...(requestParams.tools as Record<string, unknown>[]),\n          ...customTools,\n        ];\n      }\n\n      // When a captcha was just solved, expose a tool the model can call\n      // to confirm it should proceed.  This avoids fragile English-phrase\n      // parsing and works regardless of the model's output language.\n      if (this.captchaSolvedToolActive) {\n        requestParams.tools = [\n          ...(requestParams.tools as Record<string, unknown>[]),\n          {\n            type: \"function\" as const,\n            name: CAPTCHA_PROCEED_TOOL,\n            function: {\n              name: CAPTCHA_PROCEED_TOOL,\n              description:\n                \"The captcha on this page was solved automatically. \" +\n                \"Call this tool to confirm and continue with your task \" +\n                \"instead of asking the user for permission.\",\n              parameters: { type: \"object\", properties: {}, required: [] },\n            },\n          },\n        ];\n      }\n\n      // Add previous_response_id if available\n      if (previousResponseId) {\n        requestParams.previous_response_id = previousResponseId;\n      }\n\n      // Log LLM request\n      const llmRequestId = uuidv7();\n      FlowLogger.logLlmRequest({\n        requestId: llmRequestId,\n        model: this.modelName,\n        prompt: extractLlmCuaPromptSummary(inputItems),\n      });\n\n      const startTime = Date.now();\n      // Create the response using the OpenAI Responses API\n      // @ts-expect-error - Force type to match what the OpenAI SDK expects\n      const response = await this.client.responses.create(requestParams);\n      const endTime = Date.now();\n      const elapsedMs = endTime - startTime;\n\n      // Extract only the input_tokens and output_tokens\n      const usage = {\n        input_tokens: response.usage.input_tokens,\n        output_tokens: response.usage.output_tokens,\n        inference_time_ms: elapsedMs,\n      };\n\n      // Log LLM response\n      FlowLogger.logLlmResponse({\n        requestId: llmRequestId,\n        model: this.modelName,\n        output: extractLlmCuaResponseSummary(response.output),\n        inputTokens: response.usage.input_tokens,\n        outputTokens: response.usage.output_tokens,\n      });\n\n      // Store the response ID for future use\n      this.lastResponseId = response.id;\n\n      // Return the output and response ID\n      return {\n        output: response.output as unknown as ResponseItem[],\n        responseId: response.id,\n        usage,\n      };\n    } catch (error) {\n      console.error(\"Error getting action from OpenAI:\", error);\n      throw error;\n    }\n  }\n\n  async takeAction(\n    output: ResponseItem[],\n    logger: (message: LogLine) => void,\n  ): Promise<ResponseInputItem[]> {\n    const nextInputItems: ResponseInputItem[] = [];\n\n    // Process each output item\n    for (const item of output) {\n      if (item.type === \"computer_call\" && this.isComputerCallItem(item)) {\n        // Handle computer calls\n        try {\n          const action = this.convertComputerCallToAction(item);\n\n          if (action && this.actionHandler) {\n            logger({\n              category: \"agent\",\n              message: `Executing computer action: ${action.type}`,\n              level: 1,\n            });\n            await this.actionHandler(action);\n          }\n\n          // Capture a screenshot\n          const screenshot = await this.captureScreenshot();\n\n          // Create a computer_call_output for the next request\n          const outputItem = {\n            type: \"computer_call_output\" as const,\n            call_id: item.call_id,\n            output: {\n              type: \"input_image\" as const,\n              image_url: screenshot,\n            },\n          } as ResponseInputItem;\n\n          logger({\n            category: \"agent\",\n            message: `Added computer_call_output for call_id: ${item.call_id}`,\n            level: 2,\n          });\n\n          // Add current URL if available\n          if (this.currentUrl) {\n            const computerCallOutput = outputItem as {\n              type: \"computer_call_output\";\n              call_id: string;\n              output: {\n                type: \"input_image\";\n                image_url: string;\n                current_url?: string;\n              };\n              acknowledged_safety_checks?: SafetyCheck[];\n            };\n            computerCallOutput.output.current_url = this.currentUrl;\n          }\n\n          if (\n            item.pending_safety_checks &&\n            item.pending_safety_checks.length > 0\n          ) {\n            const acknowledgedChecks = await this.handleSafetyConfirmation(\n              item.pending_safety_checks,\n              logger,\n            );\n\n            if (acknowledgedChecks) {\n              const computerCallOutput = outputItem as {\n                type: \"computer_call_output\";\n                call_id: string;\n                output: {\n                  type: \"input_image\";\n                  image_url: string;\n                };\n                acknowledged_safety_checks?: SafetyCheck[];\n              };\n              computerCallOutput.acknowledged_safety_checks =\n                acknowledgedChecks;\n            }\n          }\n\n          nextInputItems.push(outputItem);\n        } catch (error) {\n          if (error instanceof StagehandClosedError) {\n            throw error;\n          }\n          const errorMessage =\n            error instanceof Error ? error.message : String(error);\n\n          logger({\n            category: \"agent\",\n            message: `Error executing computer call: ${errorMessage}`,\n            level: 0,\n          });\n\n          try {\n            // Capture a screenshot even on error\n            const screenshot = await this.captureScreenshot();\n\n            const errorOutputItem = {\n              type: \"computer_call_output\" as const,\n              call_id: item.call_id,\n              output: {\n                type: \"input_image\" as const,\n                image_url: screenshot,\n                error: errorMessage,\n              },\n            } as ResponseInputItem;\n\n            // Add current URL if available\n            if (this.currentUrl) {\n              const computerCallOutput = errorOutputItem as {\n                type: \"computer_call_output\";\n                call_id: string;\n                output: {\n                  type: \"input_image\";\n                  image_url: string;\n                  current_url?: string;\n                };\n                acknowledged_safety_checks?: SafetyCheck[];\n              };\n              computerCallOutput.output.current_url = this.currentUrl;\n            }\n\n            if (\n              item.pending_safety_checks &&\n              item.pending_safety_checks.length > 0\n            ) {\n              const acknowledgedChecks = await this.handleSafetyConfirmation(\n                item.pending_safety_checks,\n                logger,\n              );\n\n              if (acknowledgedChecks) {\n                const computerCallOutput = errorOutputItem as {\n                  type: \"computer_call_output\";\n                  call_id: string;\n                  output: {\n                    type: \"input_image\";\n                    image_url: string;\n                  };\n                  acknowledged_safety_checks?: SafetyCheck[];\n                };\n                computerCallOutput.acknowledged_safety_checks =\n                  acknowledgedChecks;\n              }\n            }\n\n            nextInputItems.push(errorOutputItem);\n          } catch (screenshotError) {\n            if (screenshotError instanceof StagehandClosedError) {\n              throw screenshotError;\n            }\n            // If we can't capture a screenshot, just send the error\n            logger({\n              category: \"agent\",\n              message: `Error capturing screenshot: ${String(screenshotError)}`,\n              level: 0,\n            });\n\n            // For error cases without a screenshot, we need to use a string output\n            nextInputItems.push({\n              type: \"computer_call_output\",\n              call_id: item.call_id,\n              output: `Error: ${errorMessage}`,\n            } as ResponseInputItem);\n          }\n        }\n      } else if (\n        item.type === \"function_call\" &&\n        this.isFunctionCallItem(item)\n      ) {\n        // Handle the captcha-proceed tool — just return a confirmation and\n        // deactivate the tool so it doesn't appear on subsequent steps.\n        if (item.name === CAPTCHA_PROCEED_TOOL) {\n          this.captchaSolvedToolActive = false;\n          nextInputItems.push({\n            type: \"function_call_output\",\n            call_id: item.call_id,\n            output:\n              \"Confirmed. The captcha is solved. Continue completing the original task autonomously without asking for further confirmation.\",\n          } as ResponseInputItem);\n          continue;\n        }\n\n        // Handle function calls (tool calls)\n        try {\n          const action = this.convertFunctionCallToAction(item);\n\n          if (action && this.actionHandler) {\n            await this.actionHandler(action);\n          }\n\n          // Execute the tool if available\n          let toolResult = \"Tool executed successfully\";\n          if (this.tools && item.name in this.tools) {\n            try {\n              const tool = this.tools[item.name];\n              const args = JSON.parse(item.arguments);\n\n              logger({\n                category: \"agent\",\n                message: `Executing tool call: ${item.name} with args: ${item.arguments}`,\n                level: 1,\n              });\n\n              const result = await tool.execute(args, {\n                toolCallId: item.call_id,\n                messages: [],\n              });\n              toolResult = JSON.stringify(result);\n\n              logger({\n                category: \"agent\",\n                message: `Tool ${item.name} completed successfully. Result: ${toolResult}`,\n                level: 1,\n              });\n            } catch (toolError) {\n              const errorMessage =\n                toolError instanceof Error\n                  ? toolError.message\n                  : String(toolError);\n              toolResult = `Error executing tool: ${errorMessage}`;\n\n              logger({\n                category: \"agent\",\n                message: `Error executing tool ${item.name}: ${errorMessage}`,\n                level: 0,\n              });\n            }\n          }\n\n          // Create a function_call_output for the next request\n          const outputItem: ResponseInputItem = {\n            type: \"function_call_output\",\n            call_id: item.call_id,\n            output: toolResult,\n          };\n\n          nextInputItems.push(outputItem);\n        } catch (error) {\n          if (error instanceof StagehandClosedError) {\n            throw error;\n          }\n          const errorMessage =\n            error instanceof Error ? error.message : String(error);\n\n          logger({\n            category: \"agent\",\n            message: `Error executing function call: ${errorMessage}`,\n            level: 0,\n          });\n\n          // Send error result back\n          const errorOutputItem: ResponseInputItem = {\n            type: \"function_call_output\",\n            call_id: item.call_id,\n            output: `Error: ${errorMessage}`,\n          };\n\n          nextInputItems.push(errorOutputItem);\n        }\n      }\n    }\n\n    return nextInputItems;\n  }\n\n  private convertComputerCallToAction(\n    call: ComputerCallItem,\n  ): AgentAction | null {\n    const { action } = call;\n\n    // Instead of wrapping the action in a params object, spread the action properties directly\n    // This ensures properties like x, y, button, etc. are directly accessible on the AgentAction\n    return {\n      type: action.type as string,\n      ...action, // Spread all properties from the action\n    };\n  }\n\n  private drainContextNotes(): string[] {\n    if (this.pendingContextNotes.length === 0) {\n      return [];\n    }\n\n    const notes = [...this.pendingContextNotes];\n    this.pendingContextNotes = [];\n    return notes;\n  }\n\n  private convertFunctionCallToAction(\n    call: FunctionCallItem,\n  ): AgentAction | null {\n    try {\n      const args = JSON.parse(call.arguments);\n\n      return {\n        type: call.name,\n        params: args,\n      };\n    } catch (error) {\n      console.error(\"Error parsing function call arguments:\", error);\n      return null;\n    }\n  }\n\n  async captureScreenshot(options?: {\n    base64Image?: string;\n    currentUrl?: string;\n  }): Promise<string> {\n    // Use provided options if available\n    if (options?.base64Image) {\n      return `data:image/png;base64,${options.base64Image}`;\n    }\n\n    // Use the screenshot provider if available\n    if (this.screenshotProvider) {\n      try {\n        const base64Image = await this.screenshotProvider();\n        return `data:image/png;base64,${base64Image}`;\n      } catch (error) {\n        console.error(\"Error capturing screenshot:\", error);\n        throw error;\n      }\n    }\n\n    throw new AgentScreenshotProviderError(\n      \"`screenshotProvider` has not been set. \" +\n        \"Please call `setScreenshotProvider()` with a valid function that returns a base64-encoded image\",\n    );\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/prompts/agentSystemPrompt.ts",
    "content": "import type { AgentToolMode, Variables } from \"../../types/public/agent.js\";\nimport { CAPTCHA_SYSTEM_PROMPT_NOTE } from \"../utils/captchaSolver.js\";\n\nexport interface AgentSystemPromptOptions {\n  url: string;\n  executionInstruction: string;\n  mode: AgentToolMode;\n  systemInstructions?: string;\n  /** Whether captchas are automatically solved by the browser environment */\n  captchasAutoSolve?: boolean;\n  /** Tools to exclude from the system prompt */\n  excludeTools?: string[];\n  /** Variables available to the agent for use in act/type tools */\n  variables?: Variables;\n  /** Whether the search tool is enabled for this execution */\n  useSearch?: boolean;\n}\n\n/**\n * Builds the system prompt for the agent based on the tool mode.\n *\n * @param options - The prompt configuration options\n * @returns The formatted system prompt string\n */\ninterface ToolDefinition {\n  name: string;\n  description: string;\n}\n\nfunction buildToolsSection(\n  isHybridMode: boolean,\n  hasSearch: boolean,\n  excludeTools?: string[],\n): string {\n  const excludeSet = new Set(excludeTools ?? []);\n\n  const hybridTools: ToolDefinition[] = [\n    {\n      name: \"screenshot\",\n      description: \"Take a compressed JPEG screenshot for quick visual context\",\n    },\n    {\n      name: \"ariaTree\",\n      description:\n        \"Get an accessibility (ARIA) hybrid tree for full page context\",\n    },\n    {\n      name: \"click\",\n      description:\n        \"Click on an element (PREFERRED - more reliable when element is visible in viewport)\",\n    },\n    {\n      name: \"type\",\n      description:\n        \"Type text into an element (PREFERRED - more reliable when element is visible in viewport)\",\n    },\n    {\n      name: \"act\",\n      description:\n        \"Perform a specific atomic action (click, type, etc.) - ONLY use when element is in ariaTree but NOT visible in screenshot. Less reliable but can interact with out-of-viewport elements.\",\n    },\n    { name: \"dragAndDrop\", description: \"Drag and drop an element\" },\n    { name: \"clickAndHold\", description: \"Click and hold on an element\" },\n    { name: \"keys\", description: \"Press a keyboard key\" },\n    {\n      name: \"fillFormVision\",\n      description: \"Fill out a form using coordinates\",\n    },\n    { name: \"think\", description: \"Think about the task\" },\n    { name: \"extract\", description: \"Extract structured data\" },\n    { name: \"goto\", description: \"Navigate to a URL\" },\n    { name: \"wait\", description: \"Wait for a specified time\" },\n    { name: \"navback\", description: \"Navigate back in browser history\" },\n    { name: \"scroll\", description: \"Scroll the page x pixels up or down\" },\n  ];\n\n  const domTools: ToolDefinition[] = [\n    {\n      name: \"screenshot\",\n      description: \"Take a compressed JPEG screenshot for quick visual context\",\n    },\n    {\n      name: \"ariaTree\",\n      description:\n        \"Get an accessibility (ARIA) hybrid tree for full page context\",\n    },\n    {\n      name: \"act\",\n      description: \"Perform a specific atomic action (click, type)\",\n    },\n    { name: \"keys\", description: \"Press a keyboard key\" },\n    { name: \"fillForm\", description: \"Fill out a form\" },\n    { name: \"think\", description: \"Think about the task\" },\n    { name: \"extract\", description: \"Extract structured data\" },\n    { name: \"goto\", description: \"Navigate to a URL\" },\n    { name: \"wait\", description: \"Wait for a specified time\" },\n    { name: \"navback\", description: \"Navigate back in browser history\" },\n    { name: \"scroll\", description: \"Scroll the page x pixels up or down\" },\n  ];\n\n  const baseTools = isHybridMode ? hybridTools : domTools;\n\n  if (hasSearch) {\n    baseTools.push({\n      name: \"search\",\n      description:\n        \"Perform a web search and return results. Prefer this over navigating to Google and searching within the page for reliability and efficiency.\",\n    });\n  }\n\n  const filteredTools = baseTools.filter((tool) => !excludeSet.has(tool.name));\n\n  const toolLines = filteredTools\n    .map((tool) => `    <tool name=\"${tool.name}\">${tool.description}</tool>`)\n    .join(\"\\n\");\n\n  return `<tools>\\n${toolLines}\\n  </tools>`;\n}\n\nexport function buildAgentSystemPrompt(\n  options: AgentSystemPromptOptions,\n): string {\n  const {\n    url,\n    executionInstruction,\n    mode,\n    systemInstructions,\n    captchasAutoSolve = false,\n    excludeTools,\n    variables,\n    useSearch = false,\n  } = options;\n  const localeDate = new Date().toLocaleDateString();\n  const isoDate = new Date().toISOString();\n  const cdata = (text: string) => `<![CDATA[${text}]]>`;\n\n  const isHybridMode = mode === \"hybrid\";\n  const hasSearch = useSearch || Boolean(process.env.BRAVE_API_KEY);\n\n  // Tools section differs based on mode and excluded tools\n  const toolsSection = buildToolsSection(isHybridMode, hasSearch, excludeTools);\n\n  // Strategy differs based on mode\n  const strategyItems = isHybridMode\n    ? [\n        `<item>Tool selection priority: Use specific tools (click, type) when elements are visible in viewport for maximum reliability.</item>`,\n        `<item>Always use screenshot to get proper grounding of the coordinates you want to type/click into.</item>`,\n        `<item>When interacting with an input, always use the type tool to type into the input, over clicking and then typing into it.</item>`,\n        `<item>Use ariaTree as a secondary tool when elements aren't visible in screenshot or to get full page context.</item>`,\n        `<item>Only use act when element is in ariaTree but NOT visible in screenshot.</item>`,\n      ]\n    : [\n        `<item>Tool selection priority: Use act tool for all clicking and typing on a page.</item>`,\n        `<item>Always check ariaTree first to understand full page content without scrolling - it shows all elements including those below the fold.</item>`,\n        `<item>When interacting with an input, always use the act tool to type into the input, over clicking and then typing.</item>`,\n        `<item>If an element is present in the ariaTree, use act to interact with it directly - this eliminates the need to scroll.</item>`,\n        `<item>Use screenshot for visual confirmation when needed, but rely primarily on ariaTree for element detection.</item>`,\n      ];\n\n  const strategySection = strategyItems.join(\"\\n    \");\n\n  const commonStrategyItems = `\n    <item>CRITICAL: Use extract ONLY when the task explicitly requires structured data output (e.g., \"get job listings\", \"extract product details\"). For reading page content or understanding elements, always use ${isHybridMode ? \"screenshot or ariaTree\" : \"ariaTree\"} instead - it's faster and more reliable.</item>\n    <item>Keep actions atomic and verify outcomes before proceeding.</item>\n    <item>For each action, provide clear reasoning about why you're taking that step.</item>\n    <item>When you need to input text that could be entered character-by-character or through multiple separate inputs, prefer using the keys tool to type the entire sequence at once. This is more efficient for scenarios like verification codes split across multiple fields, or when virtual keyboards are present but direct typing would be faster.</item>\n    `;\n\n  // Page understanding protocol differs based on mode\n  const pageUnderstandingProtocol = isHybridMode\n    ? `<page_understanding_protocol>\n    <step_1>\n      <title>UNDERSTAND THE PAGE</title>\n      <primary_tool>\n        <name>screenshot</name>\n        <usage>Visual confirmation when needed. Ideally after navigating to a new page.</usage>\n        </primary_tool>\n      <secondary_tool>\n        <name>ariaTree</name>\n        <usage>Get complete page context before taking actions</usage>\n        <benefit>Eliminates the need to scroll and provides full accessible content</benefit>\n      </secondary_tool>\n    </step_1>\n  </page_understanding_protocol>`\n    : `<page_understanding_protocol>\n    <step_1>\n      <title>UNDERSTAND THE PAGE</title>\n      <primary_tool>\n        <name>ariaTree</name>\n        <usage>Get complete page context before taking actions</usage>\n        <benefit>Eliminates the need to scroll and provides full accessible content</benefit>\n        </primary_tool>\n      <secondary_tool>\n        <name>screenshot</name>\n        <usage>Visual confirmation when needed. Ideally after navigating to a new page.</usage>\n      </secondary_tool>\n    </step_1>\n  </page_understanding_protocol>`;\n\n  // Roadblocks section only shown when captchas are auto-solved\n  const roadblocksSection = captchasAutoSolve\n    ? `<roadblocks>\n    <note>${CAPTCHA_SYSTEM_PROMPT_NOTE}</note>\n  </roadblocks>`\n    : \"\";\n\n  // Build customInstructions block only if provided\n  const customInstructionsBlock = systemInstructions\n    ? `<customInstructions>${cdata(systemInstructions)}</customInstructions>\\n  `\n    : \"\";\n\n  // Build variables section only if variables are provided\n  const hasVariables = variables && Object.keys(variables).length > 0;\n  const variableToolsNote = isHybridMode\n    ? \"Use %variableName% syntax in the type, fillFormVision, or act tool's value/text/action fields.\"\n    : \"Use %variableName% syntax in the act or fillForm tool's action fields.\";\n  const variablesSection = hasVariables\n    ? `<variables>\n    <note>You have access to the following variables. Use %variableName% syntax to substitute variable values. This is especially important for sensitive data like passwords.</note>\n    <usage>${variableToolsNote}</usage>\n    <example>To type a password, use: type %password% into the password field</example>\n    ${Object.entries(variables)\n      .map(([name, v]) => {\n        const description =\n          typeof v === \"object\" && v !== null && \"value\" in v\n            ? v.description\n            : undefined;\n        return description\n          ? `<variable name=\"${name}\">${description}</variable>`\n          : `<variable name=\"${name}\" />`;\n      })\n      .join(\"\\n    \")}\n  </variables>`\n    : \"\";\n\n  return `<system>\n  <identity>You are a web automation assistant using browser automation tools to accomplish the user's goal.</identity>\n  ${customInstructionsBlock}<task>\n    <goal>${cdata(executionInstruction)}</goal>\n    <date display=\"local\" iso=\"${isoDate}\">${localeDate}</date>\n    <note>You may think the date is different due to knowledge cutoff, but this is the actual date.</note>\n  </task>\n  <page>\n    <startingUrl>you are starting your task on this url: ${url}</startingUrl>\n  </page>\n  <mindset>\n    <note>Be very intentional about your action. The initial instruction is very important, and slight variations of the actual goal can lead to failures.</note>\n    <importantNote>If something fails to meet a single condition of the task, move on from it rather than seeing if it meets other criteria. We only care that it meets all of it</importantNote>\n    <note>When the task is complete, do not seek more information; you have completed the task.</note>\n  </mindset>\n  <guidelines>\n    <item>Always start by understanding the current page state</item>\n    <item>Use the screenshot tool to verify page state when needed</item>\n    <item>Use appropriate tools for each action</item>\n  </guidelines>\n  ${pageUnderstandingProtocol}\n  <navigation>\n    <rule>If you are confident in the URL, navigate directly to it.</rule>\n    ${hasSearch ? `<rule>If you are not confident in the URL, use the search tool to find it.</rule>` : ``}\n  </navigation>\n  ${toolsSection}\n  <strategy>\n    ${strategySection}\n    ${commonStrategyItems}\n  </strategy>\n  ${roadblocksSection}\n  ${variablesSection}\n  <completion>\n    <note>When you complete the task, explain any information that was found that was relevant to the original task.</note>\n    <examples>\n      <example>If you were asked for specific flights, list the flights you found.</example>\n      <example>If you were asked for information about a product, list the product information you were asked for.</example>\n    </examples>\n  </completion>\n</system>`;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/README.md",
    "content": "This folder provides v3-native agent tools for the AISDK-based agent flow.\nThey mirror the v2 tools but operate on the V3 CDP-native APIs.\n\nFiles are placed under lib/v3/agent/tools and consumed by V3AgentHandler.\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/act.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport type { AgentModelConfig, Variables } from \"../../types/public/agent.js\";\nimport { TimeoutError } from \"../../types/public/sdkErrors.js\";\n\nexport const actTool = (\n  v3: V3,\n  executionModel?: string | AgentModelConfig,\n  variables?: Variables,\n  toolTimeout?: number,\n) => {\n  const hasVariables = variables && Object.keys(variables).length > 0;\n  const actionDescription = hasVariables\n    ? `Describe what to click or type, e.g. \"click the Login button\" or \"type %variableName% into the input\". Available variables: ${Object.keys(variables).join(\", \")}`\n    : 'Describe what to click or type, e.g. \"click the Login button\" or \"type \"John\" into the first name input\"';\n\n  return tool({\n    description:\n      \"Perform an action on the page (click, type). Provide a short, specific phrase that mentions the element type.\",\n    inputSchema: z.object({\n      action: z.string().describe(actionDescription),\n    }),\n    execute: async ({ action }) => {\n      try {\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: act`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: action,\n              type: \"string\",\n            },\n          },\n        });\n        const options = executionModel\n          ? { model: executionModel, variables, timeout: toolTimeout }\n          : { variables, timeout: toolTimeout };\n\n        const result = await v3.act(action, options);\n        const actions = (result.actions as Action[] | undefined) ?? [];\n        v3.recordAgentReplayStep({\n          type: \"act\",\n          instruction: action,\n          actions,\n          actionDescription: result.actionDescription,\n          message: result.message,\n        });\n        // Only include playwrightArguments when actions exist\n        // (undefined is not valid JSON and breaks AI SDK validation)\n        const response: {\n          success: boolean;\n          action: string;\n          playwrightArguments?: Action;\n        } = {\n          success: result.success ?? true,\n          action: result?.actionDescription ?? action,\n        };\n        if (actions.length > 0) {\n          response.playwrightArguments = actions[0];\n        }\n        return response;\n      } catch (error) {\n        if (error instanceof TimeoutError) {\n          throw error;\n        }\n        return {\n          success: false,\n          error: error?.message ?? String(error),\n        };\n      }\n    },\n  });\n};\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/ariaTree.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport { TimeoutError } from \"../../types/public/sdkErrors.js\";\n\nexport const ariaTreeTool = (v3: V3, toolTimeout?: number) =>\n  tool({\n    description:\n      \"gets the accessibility (ARIA) hybrid tree text for the current page. use this to understand structure and content.\",\n    inputSchema: z.object({}),\n    execute: async () => {\n      try {\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: ariaTree`,\n          level: 1,\n        });\n        const page = await v3.context.awaitActivePage();\n        const extractOptions = toolTimeout\n          ? { timeout: toolTimeout }\n          : undefined;\n        const { pageText } = (await v3.extract(extractOptions)) as {\n          pageText: string;\n        };\n        const pageUrl = page.url();\n\n        let content = pageText;\n        const MAX_TOKENS = 70000; // rough cap, assume ~4 chars per token for conservative truncation\n        const estimatedTokens = Math.ceil(content.length / 4);\n        if (estimatedTokens > MAX_TOKENS) {\n          const maxChars = MAX_TOKENS * 4;\n          content =\n            content.substring(0, maxChars) +\n            \"\\n\\n[CONTENT TRUNCATED: Exceeded 70,000 token limit]\";\n        }\n\n        return { success: true, content, pageUrl };\n      } catch (error) {\n        if (error instanceof TimeoutError) {\n          throw error;\n        }\n        return {\n          content: \"\",\n          error: error?.message ?? String(error),\n          success: false,\n          pageUrl: \"\",\n        };\n      }\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      return {\n        type: \"content\",\n        value: [\n          { type: \"text\", text: `Accessibility Tree:\\n${result.content}` },\n        ],\n      };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/braveSearch.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\n\nexport interface BraveSearchResult {\n  title: string;\n  url: string;\n  description?: string;\n}\n\ninterface SearchResponse {\n  data?: {\n    results: BraveSearchResult[];\n  };\n  error?: string;\n}\n\ninterface BraveWebResult {\n  title?: string;\n  url?: string;\n  description?: string;\n  age?: string;\n  meta_url?: {\n    favicon?: string;\n  };\n}\n\ninterface BraveApiResponse {\n  web?: {\n    results?: BraveWebResult[];\n  };\n}\n\nasync function performBraveSearch(query: string): Promise<SearchResponse> {\n  try {\n    const encodedQuery = encodeURIComponent(query);\n    const response = await fetch(\n      `https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`,\n      {\n        method: \"GET\",\n        headers: {\n          Accept: \"application/json\",\n          \"Accept-Encoding\": \"gzip\",\n          \"X-Subscription-Token\": process.env.BRAVE_API_KEY!,\n        },\n      },\n    );\n\n    if (!response.ok) {\n      return {\n        error: `Brave API error: ${response.status} ${response.statusText}`,\n        data: { results: [] },\n      };\n    }\n\n    const data = (await response.json()) as BraveApiResponse;\n    const results: BraveSearchResult[] = [];\n\n    if (data?.web?.results && Array.isArray(data.web.results)) {\n      for (const item of data.web.results.slice(0, 5)) {\n        if (item.title && item.url) {\n          results.push({\n            title: item.title,\n            url: item.url,\n            description: item.description,\n          });\n        }\n      }\n    }\n\n    return { data: { results } };\n  } catch (error) {\n    console.error(\"Search error\", error);\n    return {\n      error: `Error performing search: ${error.message}`,\n      data: { results: [] },\n    };\n  }\n}\n\nexport const searchTool = (v3: V3) =>\n  tool({\n    description:\n      \"Perform a web search and returns results. Use this tool when you need information from the web or when you are unsure of the exact URL you want to navigate to. This can be used to find the ideal entry point, resulting in a task that is easier to complete due to starting further in the process.\",\n    inputSchema: z.object({\n      query: z.string().describe(\"The search query to look for on the web\"),\n    }),\n    execute: async ({ query }) => {\n      v3.logger({\n        category: \"agent\",\n        message: `Agent calling tool: search`,\n        level: 1,\n        auxiliary: {\n          arguments: {\n            value: JSON.stringify({ query }),\n            type: \"object\",\n          },\n        },\n      });\n\n      const result = await performBraveSearch(query);\n\n      v3.recordAgentReplayStep({\n        type: \"search\",\n        instruction: query,\n        playwrightArguments: { query },\n        message:\n          result.error ?? `Found ${result.data?.results.length ?? 0} results`,\n      });\n\n      return {\n        ...result,\n        timestamp: Date.now(),\n      };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/browserbaseSearch.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\n\nexport interface SearchResult {\n  title: string;\n  url: string;\n  publishedDate?: string;\n}\n\ninterface BrowserbaseRawResult {\n  title?: string;\n  url?: string;\n  publishedDate?: string;\n}\n\ninterface BrowserbaseApiResponse {\n  results?: BrowserbaseRawResult[];\n}\n\nasync function performBrowserbaseSearch(\n  v3: V3,\n  query: string,\n  apiKey: string,\n  numResults: number = 5,\n): Promise<{ results: SearchResult[]; error?: string }> {\n  try {\n    const response = await fetch(\"https://api.browserbase.com/v1/search\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"x-bb-api-key\": apiKey,\n      },\n      body: JSON.stringify({ query, numResults }),\n    });\n\n    if (!response.ok) {\n      return {\n        results: [],\n        error: `Browserbase Search API error: ${response.status} ${response.statusText}`,\n      };\n    }\n\n    const data = (await response.json()) as BrowserbaseApiResponse;\n    const results: SearchResult[] = (data?.results ?? []).map(\n      ({ title, url, publishedDate }) => ({\n        title: title,\n        url: url,\n        ...(publishedDate && { publishedDate }),\n      }),\n    );\n\n    return { results };\n  } catch (error) {\n    v3.logger({\n      category: \"agent\",\n      message: `Search error: ${error.message}`,\n      level: 0,\n    });\n    return {\n      results: [],\n      error: `Error performing search: ${error.message}`,\n    };\n  }\n}\n\nexport const searchTool = (v3: V3, apiKey: string) =>\n  tool({\n    description:\n      \"Perform a web search and returns results. Use this tool when you need information from the web or when you are unsure of the exact URL you want to navigate to. This can be used to find the ideal entry point, resulting in a task that is easier to complete due to starting further in the process.\",\n    inputSchema: z.object({\n      query: z.string().describe(\"The search query to look for on the web\"),\n    }),\n    execute: async ({ query }) => {\n      v3.logger({\n        category: \"agent\",\n        message: `Agent calling tool: search`,\n        level: 1,\n        auxiliary: {\n          arguments: {\n            value: JSON.stringify({ query }),\n            type: \"object\",\n          },\n        },\n      });\n\n      const result = await performBrowserbaseSearch(v3, query, apiKey);\n\n      v3.recordAgentReplayStep({\n        type: \"search\",\n        instruction: query,\n        playwrightArguments: { query },\n        message: result.error ?? `Found ${result.results.length} results`,\n      });\n\n      return { ...result, timestamp: Date.now() };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/click.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport type {\n  ClickToolResult,\n  ModelOutputContentItem,\n} from \"../../types/public/agent.js\";\nimport { processCoordinates } from \"../utils/coordinateNormalization.js\";\nimport { ensureXPath } from \"../utils/xpath.js\";\nimport { waitAndCaptureScreenshot } from \"../utils/screenshotHandler.js\";\n\nexport const clickTool = (v3: V3, provider?: string) =>\n  tool({\n    description:\n      \"Click on an element using its coordinates (this is the most reliable way to click on an element, always use this over act, unless the element is not visible in the screenshot, but shown in ariaTree)\",\n    inputSchema: z.object({\n      describe: z\n        .string()\n        .describe(\n          \"Describe the element to click on in a short, specific phrase that mentions the element type and a good visual description\",\n        ),\n      coordinates: z\n        .array(z.number())\n        .describe(\"The (x, y) coordinates to click on\"),\n    }),\n    execute: async ({ describe, coordinates }): Promise<ClickToolResult> => {\n      try {\n        const page = await v3.context.awaitActivePage();\n        const processed = processCoordinates(\n          coordinates[0],\n          coordinates[1],\n          provider,\n          v3,\n        );\n\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: click`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify({ describe }),\n              type: \"object\",\n            },\n          },\n        });\n\n        // Only request XPath when caching is enabled to avoid unnecessary computation\n        const shouldCollectXpath = v3.isAgentReplayActive();\n        const xpath = await page.click(processed.x, processed.y, {\n          returnXpath: shouldCollectXpath,\n        });\n\n        const screenshotBase64 = await waitAndCaptureScreenshot(page);\n\n        // Record as an \"act\" step with proper Action for deterministic replay (only when caching)\n        if (shouldCollectXpath) {\n          const normalizedXpath = ensureXPath(xpath);\n          if (normalizedXpath) {\n            const action: Action = {\n              selector: normalizedXpath,\n              description: describe,\n              method: \"click\",\n              arguments: [],\n            };\n            v3.recordAgentReplayStep({\n              type: \"act\",\n              instruction: describe,\n              actions: [action],\n              actionDescription: describe,\n            });\n          }\n        }\n\n        return {\n          success: true,\n          describe,\n          coordinates: [processed.x, processed.y],\n          screenshotBase64,\n        };\n      } catch (error) {\n        return {\n          success: false,\n          error: `Error clicking: ${error.message}`,\n        };\n      }\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      const content: ModelOutputContentItem[] = [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            success: result.success,\n            describe: result.describe,\n            coordinates: result.coordinates,\n          }),\n        },\n      ];\n      if (result.screenshotBase64) {\n        content.push({\n          type: \"media\",\n          mediaType: \"image/png\",\n          data: result.screenshotBase64,\n        });\n      }\n      return { type: \"content\", value: content };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/clickAndHold.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport { processCoordinates } from \"../utils/coordinateNormalization.js\";\nimport { ensureXPath } from \"../utils/xpath.js\";\n\nexport const clickAndHoldTool = (v3: V3, provider?: string) =>\n  tool({\n    description: \"Click and hold on an element using its coordinates\",\n    inputSchema: z.object({\n      describe: z\n        .string()\n        .describe(\n          \"Describe the element to click on in a short, specific phrase that mentions the element type and a good visual description\",\n        ),\n      duration: z\n        .number()\n        .describe(\"The duration to hold the element in milliseconds\"),\n      coordinates: z\n        .array(z.number())\n        .describe(\"The (x, y) coordinates to click on\"),\n    }),\n    execute: async ({ describe, coordinates, duration }) => {\n      try {\n        const page = await v3.context.awaitActivePage();\n        const processed = processCoordinates(\n          coordinates[0],\n          coordinates[1],\n          provider,\n          v3,\n        );\n\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: clickAndHold`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify({\n                describe,\n                duration,\n              }),\n              type: \"object\",\n            },\n          },\n        });\n\n        // Only request XPath when caching is enabled to avoid unnecessary computation\n        const shouldCollectXpath = v3.isAgentReplayActive();\n\n        // Use dragAndDrop from same point to same point with delay to simulate click and hold\n        const [xpath] = await page.dragAndDrop(\n          processed.x,\n          processed.y,\n          processed.x,\n          processed.y,\n          { delay: duration, returnXpath: shouldCollectXpath },\n        );\n\n        // Record as \"act\" step with proper Action for deterministic replay (only when caching)\n        if (shouldCollectXpath) {\n          const normalizedXpath = ensureXPath(xpath);\n          if (normalizedXpath) {\n            const action: Action = {\n              selector: normalizedXpath,\n              description: describe,\n              method: \"clickAndHold\",\n              arguments: [String(duration)],\n            };\n            v3.recordAgentReplayStep({\n              type: \"act\",\n              instruction: describe,\n              actions: [action],\n              actionDescription: describe,\n            });\n          }\n        }\n\n        return { success: true, describe };\n      } catch (error) {\n        return {\n          success: false,\n          error: `Error clicking and holding: ${error.message}`,\n        };\n      }\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/dragAndDrop.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport type {\n  DragAndDropToolResult,\n  ModelOutputContentItem,\n} from \"../../types/public/agent.js\";\nimport { processCoordinates } from \"../utils/coordinateNormalization.js\";\nimport { ensureXPath } from \"../utils/xpath.js\";\nimport { waitAndCaptureScreenshot } from \"../utils/screenshotHandler.js\";\n\nexport const dragAndDropTool = (v3: V3, provider?: string) =>\n  tool({\n    description:\n      \"Drag and drop an element using its coordinates (this is the most reliable way to drag and drop an element, always use this over act, unless the element is not visible in the screenshot, but shown in ariaTree)\",\n    inputSchema: z.object({\n      describe: z.string().describe(\"Describe the element to drag and drop\"),\n      startCoordinates: z\n        .array(z.number())\n        .describe(\"The (x, y) coordinates to start the drag and drop from\"),\n      endCoordinates: z\n        .array(z.number())\n        .describe(\"The (x, y) coordinates to end the drag and drop at\"),\n    }),\n    execute: async ({\n      describe,\n      startCoordinates,\n      endCoordinates,\n    }): Promise<DragAndDropToolResult> => {\n      try {\n        const page = await v3.context.awaitActivePage();\n        const processedStart = processCoordinates(\n          startCoordinates[0],\n          startCoordinates[1],\n          provider,\n          v3,\n        );\n        const processedEnd = processCoordinates(\n          endCoordinates[0],\n          endCoordinates[1],\n          provider,\n          v3,\n        );\n\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: dragAndDrop`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify({\n                describe,\n              }),\n              type: \"object\",\n            },\n          },\n        });\n\n        // Only request XPath when caching is enabled to avoid unnecessary computation\n        const shouldCollectXpath = v3.isAgentReplayActive();\n        const [fromXpath, toXpath] = await page.dragAndDrop(\n          processedStart.x,\n          processedStart.y,\n          processedEnd.x,\n          processedEnd.y,\n          { returnXpath: shouldCollectXpath },\n        );\n\n        const screenshotBase64 = await waitAndCaptureScreenshot(page);\n\n        // Record as \"act\" step with proper Action for deterministic replay (only when caching)\n        if (shouldCollectXpath) {\n          const normalizedFrom = ensureXPath(fromXpath);\n          const normalizedTo = ensureXPath(toXpath);\n          if (normalizedFrom && normalizedTo) {\n            const action: Action = {\n              selector: normalizedFrom,\n              description: describe,\n              method: \"dragAndDrop\",\n              arguments: [normalizedTo],\n            };\n            v3.recordAgentReplayStep({\n              type: \"act\",\n              instruction: describe,\n              actions: [action],\n              actionDescription: describe,\n            });\n          }\n        }\n\n        return {\n          success: true,\n          describe,\n          screenshotBase64,\n        };\n      } catch (error) {\n        return {\n          success: false,\n          error: `Error dragging: ${error.message}`,\n        };\n      }\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      const content: ModelOutputContentItem[] = [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            success: result.success,\n            describe: result.describe,\n          }),\n        },\n      ];\n      if (result.screenshotBase64) {\n        content.push({\n          type: \"media\",\n          mediaType: \"image/png\",\n          data: result.screenshotBase64,\n        });\n      }\n      return { type: \"content\", value: content };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/extract.ts",
    "content": "import { tool } from \"ai\";\nimport { z, ZodTypeAny } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { AgentModelConfig } from \"../../types/public/agent.js\";\nimport { TimeoutError } from \"../../types/public/sdkErrors.js\";\n\ninterface JsonSchema {\n  type?: string;\n  properties?: Record<string, JsonSchema>;\n  items?: JsonSchema;\n  enum?: string[];\n  format?: \"url\" | \"email\" | \"uuid\";\n}\n\nfunction jsonSchemaToZod(schema: JsonSchema): ZodTypeAny {\n  switch (schema.type) {\n    case \"object\": {\n      const shape: Record<string, ZodTypeAny> = {};\n      if (schema.properties) {\n        for (const [key, value] of Object.entries(schema.properties)) {\n          shape[key] = jsonSchemaToZod(value);\n        }\n      }\n      return z.object(shape);\n    }\n    case \"array\":\n      return z.array(schema.items ? jsonSchemaToZod(schema.items) : z.any());\n    case \"string\": {\n      let s = z.string();\n      if (schema.format === \"url\") s = s.url();\n      if (schema.format === \"email\") s = s.email();\n      if (schema.format === \"uuid\") s = s.uuid();\n      if (schema.enum && schema.enum.length > 0)\n        return z.enum(schema.enum as [string, ...string[]]);\n      return s;\n    }\n    case \"number\":\n    case \"integer\":\n      return z.number();\n    case \"boolean\":\n      return z.boolean();\n    case \"null\":\n      return z.null();\n    default:\n      return z.any();\n  }\n}\n\nexport const extractTool = (\n  v3: V3,\n  executionModel?: string | AgentModelConfig,\n  toolTimeout?: number,\n) =>\n  tool({\n    description: `Extract structured data from the current page based on a provided schema.\n    \n    USAGE GUIDELINES:\n    - Keep schemas MINIMAL - only include fields essential for the task\n    - IMPORTANT: only use this if explicitly asked for structured output. In most scenarios, you should use the aria tree tool over this.\n    - For URL fields, use format: \"url\"\n    \n    EXAMPLES:\n    1. Extract a single value:\n       instruction: \"extract the product price\"\n       schema: { type: \"object\", properties: { price: { type: \"number\" } } }\n    \n    2. Extract multiple fields:\n       instruction: \"extract product name and price\"\n       schema: { type: \"object\", properties: { name: { type: \"string\" }, price: { type: \"number\" } } }\n    \n    3. Extract arrays:\n       instruction: \"extract all product names and prices\"\n       schema: { type: \"object\", properties: { products: { type: \"array\", items: { type: \"object\", properties: { name: { type: \"string\" }, price: { type: \"number\" } } } } } }\n    \n    4. Extract a URL:\n       instruction: \"extract the link\"\n       schema: { type: \"object\", properties: { url: { type: \"string\", format: \"url\" } } }`,\n    inputSchema: z.object({\n      instruction: z.string(),\n      schema: z\n        .object({\n          type: z.string().optional(),\n          properties: z.record(z.string(), z.unknown()).optional(),\n          items: z.unknown().optional(),\n          enum: z.array(z.string()).optional(),\n          format: z.enum([\"url\", \"email\", \"uuid\"]).optional(),\n        })\n        .passthrough()\n        .optional()\n        .describe(\"JSON Schema object describing the structure to extract\"),\n    }),\n    execute: async ({ instruction, schema }) => {\n      try {\n        const parsedSchema = schema\n          ? jsonSchemaToZod(schema as JsonSchema)\n          : undefined;\n        const result = await v3.extract(instruction, parsedSchema, {\n          ...(executionModel ? { model: executionModel } : {}),\n          timeout: toolTimeout,\n        });\n        return { success: true, result };\n      } catch (error) {\n        if (error instanceof TimeoutError) {\n          throw error;\n        }\n        return { success: false, error: error?.message ?? String(error) };\n      }\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/fillFormVision.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport type {\n  FillFormVisionToolResult,\n  ModelOutputContentItem,\n  Variables,\n} from \"../../types/public/agent.js\";\nimport { processCoordinates } from \"../utils/coordinateNormalization.js\";\nimport { ensureXPath } from \"../utils/xpath.js\";\nimport { waitAndCaptureScreenshot } from \"../utils/screenshotHandler.js\";\nimport { substituteVariables } from \"../utils/variables.js\";\n\nexport const fillFormVisionTool = (\n  v3: V3,\n  provider?: string,\n  variables?: Variables,\n) => {\n  const hasVariables = variables && Object.keys(variables).length > 0;\n  const valueDescription = hasVariables\n    ? `Text to type into the target field. Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(\", \")}`\n    : \"Text to type into the target field\";\n\n  return tool({\n    description: `FORM FILL - SPECIALIZED MULTI-FIELD INPUT TOOL\n\nCRITICAL: Use this for ANY form with 2+ input fields (text inputs, textareas, etc.)\nIMPORTANT: Ensure the fields are visible within the current viewport\n\nWHY THIS TOOL EXISTS:\n- Forms are the #1 use case for multi-field input\n- Optimized specifically for input/textarea elements\n- 4-6x faster than individual typing actions\n\nUse fillFormVision: Pure form filling (inputs, textareas only)\nMANDATORY USE CASES (always use fillFormVision for these):\n- Registration forms: name, email, password fields\n- Contact forms: name, email, message fields\n- Checkout forms: address, payment info fields\n- Profile updates: multiple user data fields\n- Search filters: multiple criteria inputs`,\n    inputSchema: z.object({\n      fields: z\n        .array(\n          z.object({\n            action: z\n              .string()\n              .describe(\n                \"Description of the typing action, e.g. 'type foo into the bar field'\",\n              ),\n            value: z.string().describe(valueDescription),\n            coordinates: z\n              .object({\n                x: z.number(),\n                y: z.number(),\n              })\n              .describe(\"Coordinates of the target field\"),\n          }),\n        )\n        .min(2, \"Provide at least two fields to fill\"),\n    }),\n    execute: async ({ fields }): Promise<FillFormVisionToolResult> => {\n      try {\n        const page = await v3.context.awaitActivePage();\n\n        // Process coordinates and substitute variables for each field\n        // Keep original values (with %tokens%) for logging/caching, substituted values for typing\n        const processedFields = fields.map((field) => {\n          const processed = processCoordinates(\n            field.coordinates.x,\n            field.coordinates.y,\n            provider,\n            v3,\n          );\n          return {\n            ...field,\n            originalValue: field.value, // Keep original with %tokens% for cache\n            value: substituteVariables(field.value, variables),\n            coordinates: { x: processed.x, y: processed.y },\n          };\n        });\n\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: fillFormVision`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify({ fields }), // Don't log substituted values\n              type: \"object\",\n            },\n          },\n        });\n\n        // Only request XPath when caching is enabled to avoid unnecessary computation\n        const shouldCollectXpath = v3.isAgentReplayActive();\n        const actions: Action[] = [];\n\n        for (const field of processedFields) {\n          // Click the field, only requesting XPath when caching is enabled\n          const xpath = await page.click(\n            field.coordinates.x,\n            field.coordinates.y,\n            {\n              returnXpath: shouldCollectXpath,\n            },\n          );\n          await page.type(field.value);\n\n          // Build Action with XPath for deterministic replay (only when caching)\n          // Use originalValue (with %tokens%) so cache stores references, not sensitive values\n          if (shouldCollectXpath) {\n            const normalizedXpath = ensureXPath(xpath);\n            if (normalizedXpath) {\n              actions.push({\n                selector: normalizedXpath,\n                description: field.action,\n                method: \"type\",\n                arguments: [field.originalValue],\n              });\n            }\n          }\n\n          // Small delay between fields\n          await new Promise((resolve) => setTimeout(resolve, 100));\n        }\n\n        const screenshotBase64 = await waitAndCaptureScreenshot(page, 100);\n\n        // Record as \"act\" step with proper Actions for deterministic replay (only when caching)\n        if (shouldCollectXpath && actions.length > 0) {\n          v3.recordAgentReplayStep({\n            type: \"act\",\n            instruction: `Fill ${fields.length} form fields`,\n            actions,\n            actionDescription: `Fill ${fields.length} form fields`,\n          });\n        }\n\n        return {\n          success: true,\n          playwrightArguments: processedFields,\n          screenshotBase64,\n        };\n      } catch (error) {\n        return {\n          success: false,\n          error: `Error filling form: ${error.message}`,\n        };\n      }\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [\n            {\n              type: \"text\",\n              text: JSON.stringify({\n                success: result.success,\n                error: result.error,\n              }),\n            },\n          ],\n        };\n      }\n\n      const content: ModelOutputContentItem[] = [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            success: result.success,\n            fieldsCount: result.playwrightArguments?.length ?? 0,\n          }),\n        },\n      ];\n      if (result.screenshotBase64) {\n        content.push({\n          type: \"media\",\n          mediaType: \"image/png\",\n          data: result.screenshotBase64,\n        });\n      }\n      return { type: \"content\", value: content };\n    },\n  });\n};\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/fillform.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport type { AgentModelConfig, Variables } from \"../../types/public/agent.js\";\nimport { TimeoutError } from \"../../types/public/sdkErrors.js\";\n\nexport const fillFormTool = (\n  v3: V3,\n  executionModel?: string | AgentModelConfig,\n  variables?: Variables,\n  toolTimeout?: number,\n) => {\n  const hasVariables = variables && Object.keys(variables).length > 0;\n  const actionDescription = hasVariables\n    ? `Must follow the pattern: \"type <exact value> into the <field name> <fieldType>\". Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(\", \")}. Examples: \"type %email% into the email input\", \"type %password% into the password input\"`\n    : 'Must follow the pattern: \"type <exact value> into the <field name> <fieldType>\". Examples: \"type john@example.com into the email input\", \"type John into the first name input\"';\n\n  return tool({\n    description:\n      'FORM FILL - MULTI-FIELD INPUT TOOL\\nFill 2+ form inputs/textareas at once. Each action MUST include the exact text to type and the target field, e.g. \"type john@example.com into the email field\".',\n    inputSchema: z.object({\n      fields: z\n        .array(\n          z.object({\n            action: z.string().describe(actionDescription),\n          }),\n        )\n        .min(1, \"Provide at least one field to fill\"),\n    }),\n    execute: async ({ fields }) => {\n      try {\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: fillForm`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify(fields),\n              type: \"object\",\n            },\n          },\n        });\n        const instruction = `Return observation results for the following actions: ${fields\n          .map((f) => f.action)\n          .join(\", \")}`;\n\n        const observeOptions = executionModel\n          ? { model: executionModel, timeout: toolTimeout }\n          : { timeout: toolTimeout };\n        const observeResults = await v3.observe(instruction, observeOptions);\n\n        const completed = [] as unknown[];\n        const replayableActions: Action[] = [];\n        for (const res of observeResults) {\n          const actOptions = variables\n            ? { variables, timeout: toolTimeout }\n            : { timeout: toolTimeout };\n          const actResult = await v3.act(res, actOptions);\n          completed.push(actResult);\n          if (Array.isArray(actResult.actions)) {\n            replayableActions.push(...(actResult.actions as Action[]));\n          }\n        }\n        v3.recordAgentReplayStep({\n          type: \"fillForm\",\n          fields,\n          observeResults,\n          actions: replayableActions,\n        });\n        return {\n          success: true,\n          actions: completed,\n          playwrightArguments: replayableActions,\n        };\n      } catch (error) {\n        if (error instanceof TimeoutError) {\n          throw error;\n        }\n        return {\n          success: false,\n          error: error?.message ?? String(error),\n        };\n      }\n    },\n  });\n};\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/goto.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\n\nexport const gotoTool = (v3: V3) =>\n  tool({\n    description: \"Navigate to a specific URL\",\n    inputSchema: z.object({\n      url: z.string().describe(\"The URL to navigate to\"),\n    }),\n    execute: async ({ url }) => {\n      try {\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: goto`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: url,\n              type: \"string\",\n            },\n          },\n        });\n        const page = await v3.context.awaitActivePage();\n        await page.goto(url, { waitUntil: \"load\" });\n        v3.recordAgentReplayStep({ type: \"goto\", url, waitUntil: \"load\" });\n        return { success: true, url };\n      } catch (error) {\n        return { success: false, error: error?.message ?? String(error) };\n      }\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/index.ts",
    "content": "import { gotoTool } from \"./goto.js\";\nimport { actTool } from \"./act.js\";\nimport { screenshotTool } from \"./screenshot.js\";\nimport { waitTool } from \"./wait.js\";\nimport { navBackTool } from \"./navback.js\";\nimport { ariaTreeTool } from \"./ariaTree.js\";\nimport { fillFormTool } from \"./fillform.js\";\nimport { scrollTool, scrollVisionTool } from \"./scroll.js\";\nimport { extractTool } from \"./extract.js\";\nimport { clickTool } from \"./click.js\";\nimport { typeTool } from \"./type.js\";\nimport { dragAndDropTool } from \"./dragAndDrop.js\";\nimport { clickAndHoldTool } from \"./clickAndHold.js\";\nimport { keysTool } from \"./keys.js\";\nimport { fillFormVisionTool } from \"./fillFormVision.js\";\nimport { thinkTool } from \"./think.js\";\nimport { searchTool as browserbaseSearchTool } from \"./browserbaseSearch.js\";\nimport { searchTool as braveSearchTool } from \"./braveSearch.js\";\n\nimport type { ToolSet, InferUITools } from \"ai\";\nimport type { V3 } from \"../../v3.js\";\nimport type { LogLine } from \"../../types/public/logs.js\";\nimport type {\n  AgentToolMode,\n  AgentModelConfig,\n  Variables,\n} from \"../../types/public/agent.js\";\nimport { withTimeout } from \"../../timeoutConfig.js\";\nimport { TimeoutError } from \"../../types/public/sdkErrors.js\";\n\nexport interface V3AgentToolOptions {\n  executionModel?: string | AgentModelConfig;\n  logger?: (message: LogLine) => void;\n  /**\n   * Tool mode determines which set of tools are available.\n   * - 'dom' (default): Uses DOM-based tools (act, fillForm) - removes coordinate-based tools\n   * - 'hybrid': Uses coordinate-based tools (click, type, dragAndDrop, etc.) - removes fillForm\n   */\n  mode?: AgentToolMode;\n  /**\n   * The model provider. Used for model-specific coordinate handling\n   */\n  provider?: string;\n  /**\n   * Tools to exclude from the available toolset.\n   * These tools will be filtered out after mode-based filtering.\n   */\n  excludeTools?: string[];\n  /**\n   * Variables available to the agent for use in act/type tools.\n   * When provided, these tools will have an optional useVariable field.\n   */\n  variables?: Variables;\n  /**\n   * Timeout in milliseconds for async tool calls.\n   * Applied to all tools that perform I/O (except wait and think).\n   */\n  toolTimeout?: number;\n  /**\n   * Whether to enable the Browserbase-powered web search tool.\n   * Requires a valid Browserbase API key.\n   */\n  useSearch?: boolean;\n  /**\n   * The Browserbase API key used for the search tool.\n   * Resolved from BROWSERBASE_API_KEY env var or the Stagehand constructor.\n   */\n  browserbaseApiKey?: string;\n}\n\n/**\n * Filters tools based on mode and explicit exclusions.\n * - 'dom' mode: Removes coordinate-based tools (click, type, dragAndDrop, clickAndHold, fillFormVision)\n * - 'hybrid' mode: Removes DOM-based form tool (fillForm) in favor of coordinate-based fillFormVision\n * - excludeTools: Additional tools to remove from the toolset\n */\nfunction filterTools(\n  tools: ToolSet,\n  mode: AgentToolMode,\n  excludeTools?: string[],\n): ToolSet {\n  const filtered: ToolSet = { ...tools };\n\n  // Mode-based filtering\n  if (mode === \"hybrid\") {\n    delete filtered.fillForm;\n  } else {\n    // DOM mode (default)\n    delete filtered.click;\n    delete filtered.type;\n    delete filtered.dragAndDrop;\n    delete filtered.clickAndHold;\n    delete filtered.fillFormVision;\n  }\n\n  if (excludeTools) {\n    for (const toolName of excludeTools) {\n      delete filtered[toolName];\n    }\n  }\n\n  return filtered;\n}\n\n/**\n * Wraps an AI SDK tool's execute function with a timeout guard.\n * On timeout, returns `{ success: false, error: \"TimeoutError: ...\" }` to the LLM\n * and logs the error. Also acts as a safety net for any uncaught errors.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction wrapToolWithTimeout<T extends Record<string, any>>(\n  agentTool: T,\n  toolName: string,\n  v3: V3,\n  timeoutMs?: number,\n  timeoutHint?: string,\n): T {\n  if (!timeoutMs || !agentTool.execute) return agentTool;\n\n  const originalExecute = agentTool.execute;\n  return {\n    ...agentTool,\n    execute: async (...args: unknown[]) => {\n      try {\n        return await withTimeout(originalExecute(...args), timeoutMs, toolName);\n      } catch (error) {\n        if (error instanceof TimeoutError) {\n          const message = `TimeoutError: ${error.message}${timeoutHint ? ` ${timeoutHint}` : \"\"}`;\n          v3.logger({\n            category: \"agent\",\n            message,\n            level: 0,\n          });\n          return {\n            success: false,\n            error: message,\n          };\n        }\n        throw error;\n      }\n    },\n  } as T;\n}\n\nexport function createAgentTools(v3: V3, options?: V3AgentToolOptions) {\n  const executionModel = options?.executionModel;\n  const mode = options?.mode ?? \"dom\";\n  const provider = options?.provider;\n  const excludeTools = options?.excludeTools;\n  const variables = options?.variables;\n  const toolTimeout = options?.toolTimeout;\n\n  const timeoutHints: Record<string, string> = {\n    act: \"(it may continue executing in the background) — try using a different description for the action\",\n    ariaTree: \"— the page may be too large\",\n    extract: \"— try using a smaller or simpler schema\",\n    fillForm:\n      \"(it may continue executing in the background) — try filling fewer fields at once or use a different tool\",\n  };\n\n  const unwrappedTools: ToolSet = {\n    act: actTool(v3, executionModel, variables, toolTimeout),\n    ariaTree: ariaTreeTool(v3, toolTimeout),\n    click: clickTool(v3, provider),\n    clickAndHold: clickAndHoldTool(v3, provider),\n    dragAndDrop: dragAndDropTool(v3, provider),\n    extract: extractTool(v3, executionModel, toolTimeout),\n    fillForm: fillFormTool(v3, executionModel, variables, toolTimeout),\n    fillFormVision: fillFormVisionTool(v3, provider, variables),\n    goto: gotoTool(v3),\n    keys: keysTool(v3),\n    navback: navBackTool(v3),\n    screenshot: screenshotTool(v3),\n    scroll: mode === \"hybrid\" ? scrollVisionTool(v3, provider) : scrollTool(v3),\n    type: typeTool(v3, provider, variables),\n  };\n\n  if (options?.useSearch && options.browserbaseApiKey) {\n    unwrappedTools.search = browserbaseSearchTool(\n      v3,\n      options.browserbaseApiKey,\n    );\n  } else if (process.env.BRAVE_API_KEY) {\n    unwrappedTools.search = braveSearchTool(v3);\n  }\n\n  const allTools: ToolSet = {\n    ...Object.fromEntries(\n      Object.entries(unwrappedTools).map(([name, t]) => [\n        name,\n        wrapToolWithTimeout(\n          t,\n          `${name}()`,\n          v3,\n          toolTimeout,\n          timeoutHints[name],\n        ),\n      ]),\n    ),\n    think: thinkTool(),\n    wait: waitTool(v3, mode),\n  };\n\n  return filterTools(allTools, mode, excludeTools);\n}\n\nexport type AgentTools = ReturnType<typeof createAgentTools>;\n\n/**\n * Type map of all agent tools for strong typing of tool calls and results.\n * Note: `search` is optional — enabled via useSearch: true (Browserbase) or BRAVE_API_KEY env var (legacy).\n */\nexport type AgentToolTypesMap = {\n  act: ReturnType<typeof actTool>;\n  ariaTree: ReturnType<typeof ariaTreeTool>;\n  click: ReturnType<typeof clickTool>;\n  clickAndHold: ReturnType<typeof clickAndHoldTool>;\n  dragAndDrop: ReturnType<typeof dragAndDropTool>;\n  extract: ReturnType<typeof extractTool>;\n  fillForm: ReturnType<typeof fillFormTool>;\n  fillFormVision: ReturnType<typeof fillFormVisionTool>;\n  goto: ReturnType<typeof gotoTool>;\n  keys: ReturnType<typeof keysTool>;\n  navback: ReturnType<typeof navBackTool>;\n  screenshot: ReturnType<typeof screenshotTool>;\n  scroll: ReturnType<typeof scrollTool> | ReturnType<typeof scrollVisionTool>;\n  search?:\n    | ReturnType<typeof browserbaseSearchTool>\n    | ReturnType<typeof braveSearchTool>;\n  think: ReturnType<typeof thinkTool>;\n  type: ReturnType<typeof typeTool>;\n  wait: ReturnType<typeof waitTool>;\n};\n\n/**\n * Inferred UI tools type for type-safe tool inputs and outputs.\n * Use with UIMessage for full type safety in UI contexts.\n */\nexport type AgentUITools = InferUITools<AgentToolTypesMap>;\n\n/**\n * Union type for all possible agent tool calls.\n * Provides type-safe access to tool call arguments.\n */\nexport type AgentToolCall = {\n  [K in keyof AgentToolTypesMap]: {\n    toolName: K;\n    toolCallId: string;\n    args: AgentUITools[K][\"input\"];\n  };\n}[keyof AgentToolTypesMap];\n\n/**\n * Union type for all possible agent tool results.\n * Provides type-safe access to tool result values.\n */\nexport type AgentToolResult = {\n  [K in keyof AgentToolTypesMap]: {\n    toolName: K;\n    toolCallId: string;\n    result: AgentUITools[K][\"output\"];\n  };\n}[keyof AgentToolTypesMap];\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/keys.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\n\nexport const keysTool = (v3: V3) =>\n  tool({\n    description: `Send keyboard input to the page without targeting a specific element. Unlike the type tool which clicks then types into coordinates, this sends keystrokes directly to wherever focus currently is.\n\nUse method=\"type\" to enter text into the currently focused element. Preferred when: input is already focused, text needs to flow across multiple fields (e.g., verification codes)\n\nUse method=\"press\" for navigation keys (Enter, Tab, Escape, Backspace, arrows) and keyboard shortcuts (Cmd+A, Ctrl+C, Shift+Tab).`,\n    inputSchema: z.object({\n      method: z.enum([\"press\", \"type\"]),\n      value: z\n        .string()\n        .describe(\n          \"The text to type, or the key/combo to press (Enter, Tab, Cmd+A)\",\n        ),\n      repeat: z.number().optional(),\n    }),\n    execute: async ({ method, value, repeat }) => {\n      try {\n        const page = await v3.context.awaitActivePage();\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: keys`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify({ method, value, repeat }),\n              type: \"object\",\n            },\n          },\n        });\n\n        const times = Math.max(1, repeat ?? 1);\n\n        if (method === \"type\") {\n          for (let i = 0; i < times; i++) {\n            await page.type(value, { delay: 100 });\n          }\n          v3.recordAgentReplayStep({\n            type: \"keys\",\n            instruction: `type \"${value}\"`,\n            playwrightArguments: { method, text: value, times },\n          });\n          return { success: true, method, value, times };\n        }\n\n        if (method === \"press\") {\n          for (let i = 0; i < times; i++) {\n            await page.keyPress(value, { delay: 100 });\n          }\n          v3.recordAgentReplayStep({\n            type: \"keys\",\n            instruction: `press ${value}`,\n            playwrightArguments: { method, keys: value, times },\n          });\n          return { success: true, method, value, times };\n        }\n\n        return { success: false, error: `Unsupported method: ${method}` };\n      } catch (error) {\n        return { success: false, error: error.message };\n      }\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/navback.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\n\nexport const navBackTool = (v3: V3) =>\n  tool({\n    description: \"Navigate back to the previous page\",\n    inputSchema: z.object({\n      reasoningText: z.string().describe(\"Why you're going back\"),\n    }),\n    execute: async () => {\n      v3.logger({\n        category: \"agent\",\n        message: `Agent calling tool: navback`,\n        level: 1,\n      });\n      const page = await v3.context.awaitActivePage();\n      await page.goBack({ waitUntil: \"domcontentloaded\" });\n      v3.recordAgentReplayStep({\n        type: \"navback\",\n        waitUntil: \"domcontentloaded\",\n      });\n      return { success: true };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/screenshot.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\n\nexport const screenshotTool = (v3: V3) =>\n  tool({\n    description:\n      \"Takes a screenshot (PNG) of the current page. Use this to quickly verify page state.\",\n    inputSchema: z.object({}),\n    execute: async () => {\n      try {\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: screenshot`,\n          level: 1,\n        });\n        const page = await v3.context.awaitActivePage();\n        const buffer = await page.screenshot({ fullPage: false });\n        const pageUrl = page.url();\n        return {\n          success: true,\n          base64: buffer.toString(\"base64\"),\n          timestamp: Date.now(),\n          pageUrl,\n        };\n      } catch (error) {\n        return {\n          success: false,\n          error: `Error taking screenshot: ${(error as Error).message}`,\n        };\n      }\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      return {\n        type: \"content\",\n        value: [{ type: \"media\", mediaType: \"image/png\", data: result.base64 }],\n      };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/scroll.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type {\n  ScrollToolResult,\n  ScrollVisionToolResult,\n  ModelOutputContentItem,\n} from \"../../types/public/agent.js\";\nimport { processCoordinates } from \"../utils/coordinateNormalization.js\";\nimport { waitAndCaptureScreenshot } from \"../utils/screenshotHandler.js\";\n\n/**\n * Simple scroll tool for DOM mode (non-grounding models).\n * No coordinates - scrolls from viewport center.\n */\nexport const scrollTool = (v3: V3) =>\n  tool({\n    description:\n      \"Scroll the page up or down by a percentage of the viewport height. Default is 80%, and what should be typically used for general page scrolling\",\n    inputSchema: z.object({\n      direction: z.enum([\"up\", \"down\"]),\n      percentage: z.number().min(1).max(200).optional(),\n    }),\n    execute: async ({\n      direction,\n      percentage = 80,\n    }): Promise<ScrollToolResult> => {\n      v3.logger({\n        category: \"agent\",\n        message: `Agent calling tool: scroll`,\n        level: 1,\n        auxiliary: {\n          arguments: {\n            value: JSON.stringify({ direction, percentage }),\n            type: \"object\",\n          },\n        },\n      });\n\n      const page = await v3.context.awaitActivePage();\n\n      const { w, h } = await page.mainFrame().evaluate<{\n        w: number;\n        h: number;\n      }>(\"({ w: window.innerWidth, h: window.innerHeight })\");\n\n      const scrollDistance = Math.round((h * percentage) / 100);\n      const cx = Math.floor(w / 2);\n      const cy = Math.floor(h / 2);\n      const deltaY = direction === \"up\" ? -scrollDistance : scrollDistance;\n\n      await page.scroll(cx, cy, 0, deltaY);\n\n      v3.recordAgentReplayStep({\n        type: \"scroll\",\n        deltaX: 0,\n        deltaY,\n        anchor: { x: cx, y: cy },\n      });\n\n      return {\n        success: true,\n        message: `Scrolled ${percentage}% ${direction} (${scrollDistance}px)`,\n        scrolledPixels: scrollDistance,\n      };\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      return {\n        type: \"json\",\n        value: {\n          success: result.success,\n          message: result.message,\n          scrolledPixels: result.scrolledPixels,\n        },\n      };\n    },\n  });\n\n/**\n * Scroll tool for hybrid mode (grounding models).\n * Supports optional coordinates for scrolling within nested scrollable elements.\n */\nexport const scrollVisionTool = (v3: V3, provider?: string) =>\n  tool({\n    description: `Scroll the page up or down. For general page scrolling, no coordinates needed. Only provide coordinates when scrolling inside a nested scrollable element (e.g., a dropdown menu, modal with overflow, or scrollable sidebar). Default is 80%, and what should be typically used for general page scrolling`,\n    inputSchema: z.object({\n      direction: z.enum([\"up\", \"down\"]),\n      coordinates: z\n        .array(z.number())\n        .optional()\n        .describe(\n          \"Only use coordinates for scrolling inside a nested scrollable element - provide (x, y) within that element\",\n        ),\n      percentage: z.number().min(1).max(200).optional(),\n    }),\n    execute: async ({\n      direction,\n      coordinates,\n      percentage = 80,\n    }): Promise<ScrollVisionToolResult> => {\n      const page = await v3.context.awaitActivePage();\n\n      const { w, h } = await page.mainFrame().evaluate<{\n        w: number;\n        h: number;\n      }>(\"({ w: window.innerWidth, h: window.innerHeight })\");\n\n      // Process coordinates if provided, otherwise use viewport center\n      let cx: number;\n      let cy: number;\n      if (coordinates) {\n        const processed = processCoordinates(\n          coordinates[0],\n          coordinates[1],\n          provider,\n          v3,\n        );\n        cx = processed.x;\n        cy = processed.y;\n      } else {\n        cx = Math.floor(w / 2);\n        cy = Math.floor(h / 2);\n      }\n\n      v3.logger({\n        category: \"agent\",\n        message: `Agent calling tool: scroll`,\n        level: 1,\n        auxiliary: {\n          arguments: {\n            value: JSON.stringify({\n              direction,\n              coordinates,\n              percentage,\n              processed: { cx, cy },\n            }),\n            type: \"object\",\n          },\n        },\n      });\n\n      const scrollDistance = Math.round((h * percentage) / 100);\n      const deltaY = direction === \"up\" ? -scrollDistance : scrollDistance;\n\n      await page.scroll(cx, cy, 0, deltaY);\n\n      const screenshotBase64 = await waitAndCaptureScreenshot(page, 100);\n\n      v3.recordAgentReplayStep({\n        type: \"scroll\",\n        deltaX: 0,\n        deltaY,\n        anchor: { x: cx, y: cy },\n      });\n\n      return {\n        success: true,\n        message: coordinates\n          ? `Scrolled ${percentage}% ${direction} at (${cx}, ${cy})`\n          : `Scrolled ${percentage}% ${direction}`,\n        scrolledPixels: scrollDistance,\n        screenshotBase64,\n      };\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      const content: ModelOutputContentItem[] = [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            success: result.success,\n            message: result.message,\n            scrolledPixels: result.scrolledPixels,\n          }),\n        },\n      ];\n      if (result.screenshotBase64) {\n        content.push({\n          type: \"media\",\n          mediaType: \"image/png\",\n          data: result.screenshotBase64,\n        });\n      }\n      return { type: \"content\", value: content };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/think.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport const thinkTool = () =>\n  tool({\n    description: `Use this tool to think through complex problems or plan a sequence of steps. This is for internal reasoning only and doesn't perform any actions. Use this to:\n\n1. Plan a multi-step approach before taking action\n2. Break down complex tasks\n3. Reason through edge cases\n4. Evaluate options when you're unsure what to do next\n\nThe output is only visible to you; use it to track your own reasoning process.`,\n    inputSchema: z.object({\n      reasoning: z\n        .string()\n        .describe(\n          \"Your step-by-step reasoning or planning process. Be as detailed as needed.\",\n        ),\n    }),\n    execute: async ({ reasoning }) => {\n      return {\n        acknowledged: true,\n        message: reasoning,\n      };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/type.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type { Action } from \"../../types/public/methods.js\";\nimport type {\n  TypeToolResult,\n  ModelOutputContentItem,\n  Variables,\n} from \"../../types/public/agent.js\";\nimport { processCoordinates } from \"../utils/coordinateNormalization.js\";\nimport { ensureXPath } from \"../utils/xpath.js\";\nimport { waitAndCaptureScreenshot } from \"../utils/screenshotHandler.js\";\nimport { substituteVariables } from \"../utils/variables.js\";\n\nexport const typeTool = (v3: V3, provider?: string, variables?: Variables) => {\n  const hasVariables = variables && Object.keys(variables).length > 0;\n  const textDescription = hasVariables\n    ? `The text to type into the element. Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(\", \")}`\n    : \"The text to type into the element\";\n\n  return tool({\n    description:\n      \"Type text into an element using its coordinates. This will click the element and then type the text into it (this is the most reliable way to type into an element, always use this over act, unless the element is not visible in the screenshot, but shown in ariaTree)\",\n    inputSchema: z.object({\n      describe: z\n        .string()\n        .describe(\n          \"Describe the element to type into in a short, specific phrase that mentions the element type and a good visual description\",\n        ),\n      text: z.string().describe(textDescription),\n      coordinates: z\n        .array(z.number())\n        .describe(\"The (x, y) coordinates to type into the element\"),\n    }),\n    execute: async ({\n      describe,\n      coordinates,\n      text,\n    }): Promise<TypeToolResult> => {\n      try {\n        const page = await v3.context.awaitActivePage();\n        const processed = processCoordinates(\n          coordinates[0],\n          coordinates[1],\n          provider,\n          v3,\n        );\n\n        // Substitute any %variableName% tokens in the text\n        const actualText = substituteVariables(text, variables);\n\n        v3.logger({\n          category: \"agent\",\n          message: `Agent calling tool: type`,\n          level: 1,\n          auxiliary: {\n            arguments: {\n              value: JSON.stringify({ describe, text }),\n              type: \"object\",\n            },\n          },\n        });\n\n        // Only request XPath when caching is enabled to avoid unnecessary computation\n        const shouldCollectXpath = v3.isAgentReplayActive();\n        const xpath = await page.click(processed.x, processed.y, {\n          returnXpath: shouldCollectXpath,\n        });\n\n        await page.type(actualText);\n\n        const screenshotBase64 = await waitAndCaptureScreenshot(page);\n\n        // Record as an \"act\" step with proper Action for deterministic replay (only when caching)\n        if (shouldCollectXpath) {\n          const normalizedXpath = ensureXPath(xpath);\n          if (normalizedXpath) {\n            const action: Action = {\n              selector: normalizedXpath,\n              description: describe,\n              method: \"type\",\n              arguments: [text],\n            };\n            v3.recordAgentReplayStep({\n              type: \"act\",\n              instruction: describe,\n              actions: [action],\n              actionDescription: describe,\n            });\n          }\n        }\n\n        return {\n          success: true,\n          describe,\n          text, // Return original text (with %variableName% tokens) to avoid exposing sensitive values to LLM\n          screenshotBase64,\n        };\n      } catch (error) {\n        return {\n          success: false,\n          error: `Error typing: ${error.message}`,\n        };\n      }\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      const content: ModelOutputContentItem[] = [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            success: result.success,\n            describe: result.describe,\n            text: result.text,\n          }),\n        },\n      ];\n      if (result.screenshotBase64) {\n        content.push({\n          type: \"media\",\n          mediaType: \"image/png\",\n          data: result.screenshotBase64,\n        });\n      }\n      return { type: \"content\", value: content };\n    },\n  });\n};\n"
  },
  {
    "path": "packages/core/lib/v3/agent/tools/wait.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\nimport type { V3 } from \"../../v3.js\";\nimport type {\n  AgentToolMode,\n  WaitToolResult,\n  ModelOutputContentItem,\n} from \"../../types/public/agent.js\";\nimport { waitAndCaptureScreenshot } from \"../utils/screenshotHandler.js\";\n\nexport const waitTool = (v3: V3, mode?: AgentToolMode) =>\n  tool({\n    description: \"Wait for a specified time\",\n    inputSchema: z.object({\n      timeMs: z.number().describe(\"Time in milliseconds\"),\n    }),\n    execute: async ({ timeMs }): Promise<WaitToolResult> => {\n      v3.logger({\n        category: \"agent\",\n        message: `Agent calling tool: wait`,\n        level: 1,\n        auxiliary: {\n          arguments: {\n            value: `Waiting for ${timeMs} milliseconds`,\n            type: \"string\",\n          },\n        },\n      });\n      await new Promise((resolve) => setTimeout(resolve, timeMs));\n      if (timeMs > 0) {\n        v3.recordAgentReplayStep({ type: \"wait\", timeMs });\n      }\n\n      // Take screenshot after wait in hybrid mode for visual feedback\n      if (mode === \"hybrid\") {\n        const page = await v3.context.awaitActivePage();\n        const screenshotBase64 = await waitAndCaptureScreenshot(page, 0);\n        return { success: true, waited: timeMs, screenshotBase64 };\n      }\n\n      return { success: true, waited: timeMs };\n    },\n    toModelOutput: (result) => {\n      if (result.success === false || result.error !== undefined) {\n        return {\n          type: \"content\",\n          value: [{ type: \"text\", text: JSON.stringify(result) }],\n        };\n      }\n\n      const content: ModelOutputContentItem[] = [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            success: result.success,\n            waited: result.waited,\n          }),\n        },\n      ];\n      if (result.screenshotBase64) {\n        content.push({\n          type: \"media\",\n          mediaType: \"image/png\",\n          data: result.screenshotBase64,\n        });\n      }\n      return { type: \"content\", value: content };\n    },\n  });\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/actionMapping.ts",
    "content": "import { AgentAction } from \"../../types/public/agent.js\";\nimport { ActionMappingOptions } from \"../../types/private/agent.js\";\n\n/**\n * Keys to exclude from tool outputs when mapping to actions.\n * These are large data fields that shouldn't be included in the actions array.\n * Users can access this data through result.messages if needed.\n */\nconst EXCLUDED_OUTPUT_KEYS = [\"screenshotBase64\"] as const;\n\n/**\n * Strips excluded keys (like screenshotBase64) from a tool output object.\n */\nfunction stripExcludedKeys(\n  output: Record<string, unknown>,\n): Record<string, unknown> {\n  const result: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(output)) {\n    if (\n      !EXCLUDED_OUTPUT_KEYS.includes(\n        key as (typeof EXCLUDED_OUTPUT_KEYS)[number],\n      )\n    ) {\n      result[key] = value;\n    }\n  }\n  return result;\n}\n\nexport function mapToolResultToActions({\n  toolCallName,\n  toolResult,\n  args,\n  reasoning,\n}: ActionMappingOptions): AgentAction[] {\n  switch (toolCallName) {\n    case \"act\":\n      return mapActToolResult(toolResult, args, reasoning);\n    case \"fillForm\":\n      return mapFillFormToolResult(toolResult, args, reasoning);\n    default:\n      return [createStandardAction(toolCallName, toolResult, args, reasoning)];\n  }\n}\n\nfunction mapActToolResult(\n  toolResult: unknown,\n  args: Record<string, unknown>,\n  reasoning?: string,\n): AgentAction[] {\n  if (!toolResult || typeof toolResult !== \"object\") {\n    return [createStandardAction(\"act\", toolResult, args, reasoning)];\n  }\n\n  const result = toolResult as Record<string, unknown>;\n\n  // AI SDK wraps the tool result in an output property\n  const output = (result.output as Record<string, unknown>) || result;\n\n  // Extract playwright arguments if they exist\n  const action: AgentAction = {\n    type: \"act\",\n    reasoning,\n    taskCompleted: false,\n    ...args,\n  };\n\n  if (output.playwrightArguments) {\n    action.playwrightArguments = output.playwrightArguments;\n  }\n\n  return [action];\n}\n\nfunction mapFillFormToolResult(\n  toolResult: unknown,\n  args: Record<string, unknown>,\n  reasoning?: string,\n): AgentAction[] {\n  if (!toolResult || typeof toolResult !== \"object\") {\n    return [createStandardAction(\"fillForm\", toolResult, args, reasoning)];\n  }\n\n  const result = toolResult as Record<string, unknown>;\n\n  // AI SDK wraps the tool result in an output property\n  const output = (result.output as Record<string, unknown>) || result;\n\n  const observeResults = Array.isArray(output?.playwrightArguments)\n    ? output.playwrightArguments\n    : [];\n\n  const actions: AgentAction[] = [];\n\n  actions.push({\n    type: \"fillForm\",\n    reasoning,\n    taskCompleted: false,\n    ...args,\n  });\n\n  for (const observeResult of observeResults) {\n    actions.push({\n      type: \"act\",\n      reasoning: \"acting from fillform tool\",\n      taskCompleted: false,\n      playwrightArguments: observeResult,\n    });\n  }\n\n  return actions;\n}\n\nfunction createStandardAction(\n  toolCallName: string,\n  toolResult: unknown,\n  args: Record<string, unknown>,\n  reasoning?: string,\n): AgentAction {\n  const action: AgentAction = {\n    type: toolCallName,\n    reasoning,\n    taskCompleted:\n      toolCallName === \"done\" ? (args?.taskComplete as boolean) : false,\n    ...args,\n  };\n\n  // For screenshot tool, exclude base64 data and just indicate a screenshot was taken,\n  // if somebody really wants the base64 data, they can access it through messages\n  if (toolCallName === \"screenshot\") {\n    action.result = \"screenshotTaken\";\n    return action;\n  }\n\n  // Spread the output from the tool result if it exists\n  // Exclude ariaTree tool result as it is very large and unnecessary\n  if (toolCallName !== \"ariaTree\" && toolResult) {\n    const result = toolResult as { output?: unknown };\n    const output = result.output;\n\n    if (output && typeof output === \"object\" && !Array.isArray(output)) {\n      const cleanedOutput = stripExcludedKeys(\n        output as Record<string, unknown>,\n      );\n      Object.assign(action, cleanedOutput);\n    }\n  }\n\n  return action;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/captchaSolver.ts",
    "content": "import type { Page } from \"../../understudy/page.js\";\nimport type { ConsoleMessage } from \"../../understudy/consoleMessage.js\";\n\nconst SOLVING_STARTED = \"browserbase-solving-started\";\nconst SOLVING_FINISHED = \"browserbase-solving-finished\";\nconst SOLVING_ERRORED = \"browserbase-solving-errored\";\n\n/** Maximum time (ms) to wait for the captcha solver before giving up. */\nconst SOLVE_TIMEOUT_MS = 90_000;\n\n// ---------------------------------------------------------------------------\n// Shared captcha notification strings\n// ---------------------------------------------------------------------------\n\n/** Injected into the agent message stream after a successful captcha solve. */\nexport const CAPTCHA_SOLVED_MSG =\n  \"A captcha was automatically detected and solved — no further interaction with the captcha is needed, even if it does not visually appear solved. Do not click the captcha checkbox, widget, or challenge again. Continue with your task.\";\n\n/** Injected into the agent message stream when the captcha solver fails. */\nexport const CAPTCHA_ERRORED_MSG =\n  \"A captcha was detected but the automatic captcha solver failed to solve it. You may need to try a different approach or navigate around the captcha.\";\n\n/** Appended to the system prompt (DOM/hybrid agents) when captchas auto-solve. */\nexport const CAPTCHA_SYSTEM_PROMPT_NOTE =\n  \"Captchas on this page are automatically detected and solved by the browser environment. Do not interact with or attempt to solve any captchas yourself — they will be handled for you. Do not click the captcha checkbox, widget, or challenge again after it has been solved, even if it still looks unresolved. Continue with your task as if the captcha does not exist.\";\n\n/** Appended to the CUA system prompt when captchas auto-solve. */\nexport const CAPTCHA_CUA_SYSTEM_PROMPT_NOTE =\n  \"\\n\\nCaptchas on this page are automatically detected and solved by the browser environment. Do not interact with or attempt to solve any captchas yourself — they will be handled for you. Continue with your task as if the captcha does not exist.\";\n\n/**\n * Tracks Browserbase captcha solver state via console messages and provides\n * a blocking `waitIfSolving()` that agents call before each step/action.\n *\n * Accepts a page-provider callback so the listener is automatically\n * re-attached when the active page changes (e.g. popup / new tab).\n *\n * All concurrent callers of `waitIfSolving()` share the same underlying\n * promise, so multiple waiters are safely resolved together.\n */\nexport class CaptchaSolver {\n  private solving = false;\n  private _solvedSinceLastConsume = false;\n  private _erroredSinceLastConsume = false;\n  private listener: ((msg: ConsoleMessage) => void) | null = null;\n  private attachedPage: Page | null = null;\n  private pageProvider: (() => Promise<Page>) | null = null;\n\n  /** Shared promise that all concurrent waitIfSolving() callers await. */\n  private waitPromise: Promise<void> | null = null;\n  /** Resolves the shared waitPromise. */\n  private resolveWait: (() => void) | null = null;\n  /** Timeout handle for the 90s deadline. */\n  private waitTimer: ReturnType<typeof setTimeout> | null = null;\n\n  /**\n   * Initialise with a callback that returns the current active page.\n   * The listener is lazily (re-)attached whenever the active page changes.\n   */\n  init(pageProvider: () => Promise<Page>): void {\n    this.pageProvider = pageProvider;\n  }\n\n  /** Whether a captcha solve is currently in progress. */\n  isSolving(): boolean {\n    return this.solving;\n  }\n\n  /**\n   * Ensure the console listener is attached to the current active page.\n   * If the active page has changed since the last call, the old listener\n   * is removed and a new one is installed.\n   */\n  async ensureAttached(): Promise<void> {\n    if (!this.pageProvider) return;\n    const page = await this.pageProvider();\n    if (page === this.attachedPage) return;\n\n    // Detach from the old page\n    this.detachListener();\n\n    this.attachedPage = page;\n    this.listener = (msg: ConsoleMessage) => {\n      const text = msg.text();\n      if (text === SOLVING_STARTED) {\n        this.solving = true;\n      } else if (text === SOLVING_FINISHED) {\n        this.solving = false;\n        this._solvedSinceLastConsume = true;\n        this.settle();\n      } else if (text === SOLVING_ERRORED) {\n        this.solving = false;\n        this._erroredSinceLastConsume = true;\n        this.settle();\n      }\n    };\n    page.on(\"console\", this.listener);\n  }\n\n  /**\n   * Returns a promise that resolves immediately if no captcha is being\n   * solved, or blocks until the solver finishes, errors, or the 90s\n   * timeout is reached.\n   *\n   * Also re-attaches the listener to the current active page if it has\n   * changed since the last call.\n   *\n   * All concurrent callers share the same promise, so no waiter is\n   * orphaned.\n   */\n  async waitIfSolving(): Promise<void> {\n    await this.ensureAttached();\n\n    if (!this.solving) return;\n\n    // Return the existing shared promise if one is already pending\n    if (this.waitPromise) return this.waitPromise;\n\n    this.waitPromise = new Promise<void>((resolve) => {\n      this.resolveWait = resolve;\n      this.waitTimer = setTimeout(() => {\n        this.solving = false;\n        this._erroredSinceLastConsume = true;\n        this.settle();\n      }, SOLVE_TIMEOUT_MS);\n    });\n\n    return this.waitPromise;\n  }\n\n  /**\n   * Returns and resets the solve event flags.\n   * Call after `waitIfSolving()` to check whether a captcha was solved\n   * (or errored) since the last consume.  This captures events even if\n   * the solve completed between two `waitIfSolving()` calls.\n   */\n  consumeSolveResult(): { solved: boolean; errored: boolean } {\n    const result = {\n      solved: this._solvedSinceLastConsume,\n      errored: this._erroredSinceLastConsume,\n    };\n    this._solvedSinceLastConsume = false;\n    this._erroredSinceLastConsume = false;\n    return result;\n  }\n\n  /**\n   * Remove the console listener and reset all state.\n   */\n  dispose(): void {\n    this.detachListener();\n    this.attachedPage = null;\n    this.pageProvider = null;\n    this.solving = false;\n    this._solvedSinceLastConsume = false;\n    this._erroredSinceLastConsume = false;\n    this.settle();\n  }\n\n  // ------------------------------------------------------------------\n  // Internal helpers\n  // ------------------------------------------------------------------\n\n  /** Remove the console listener from the currently attached page. */\n  private detachListener(): void {\n    if (this.attachedPage && this.listener) {\n      this.attachedPage.off(\"console\", this.listener);\n    }\n    this.listener = null;\n    // If a solve was in progress, mark it as errored so consumers\n    // know it was interrupted (consistent with the timeout path).\n    if (this.solving) {\n      this._erroredSinceLastConsume = true;\n    }\n    // Reset solving state so waiters aren't stuck waiting for events\n    // that can never arrive from the detached page.\n    this.solving = false;\n    this.settle();\n  }\n\n  /** Resolve the shared wait promise and clear the timeout. */\n  private settle(): void {\n    if (this.waitTimer) {\n      clearTimeout(this.waitTimer);\n      this.waitTimer = null;\n    }\n    if (this.resolveWait) {\n      const resolve = this.resolveWait;\n      this.resolveWait = null;\n      this.waitPromise = null;\n      resolve();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/coordinateNormalization.ts",
    "content": "import type { V3 } from \"../../v3.js\";\n\n// Default viewport for advancedStealth mode\nconst STEALTH_VIEWPORT = { width: 1288, height: 711 };\n\nexport function isGoogleProvider(provider?: string): boolean {\n  if (!provider) return false;\n  return provider.toLowerCase().includes(\"google\");\n}\n\n// Google returns coordinates in a 0-1000 range, we need to normalize\n// them to the viewport dimensions\nexport function normalizeGoogleCoordinates(\n  x: number,\n  y: number,\n  viewport: { width: number; height: number },\n): { x: number; y: number } {\n  const clampedX = Math.min(999, Math.max(0, x));\n  const clampedY = Math.min(999, Math.max(0, y));\n  return {\n    x: Math.floor((clampedX / 1000) * viewport.width),\n    y: Math.floor((clampedY / 1000) * viewport.height),\n  };\n}\n\nexport function processCoordinates(\n  x: number,\n  y: number,\n  provider?: string,\n  v3?: V3,\n): { x: number; y: number } {\n  if (isGoogleProvider(provider) && v3) {\n    // advancedStealth uses fixed viewport, otherwise use configured viewport\n    const viewport = v3.isAdvancedStealth\n      ? STEALTH_VIEWPORT\n      : v3.configuredViewport;\n    return normalizeGoogleCoordinates(x, y, viewport);\n  }\n  return { x, y };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/cuaKeyMapping.ts",
    "content": "/**\n * Universal key mapping utility for converting various key representations\n * to Playwright-compatible key names. Used by all CUA clients and handlers.\n */\n\n/**\n * map of key variations to Playwright key names\n * This handles keys from both Anthropic and OpenAI CUA APIs\n */\nconst KEY_MAP: Record<string, string> = {\n  ENTER: \"Enter\",\n  RETURN: \"Enter\",\n  ESCAPE: \"Escape\",\n  ESC: \"Escape\",\n  BACKSPACE: \"Backspace\",\n  TAB: \"Tab\",\n  SPACE: \" \",\n  DELETE: \"Delete\",\n  DEL: \"Delete\",\n  ARROWUP: \"ArrowUp\",\n  ARROWDOWN: \"ArrowDown\",\n  ARROWLEFT: \"ArrowLeft\",\n  ARROWRIGHT: \"ArrowRight\",\n  ARROW_UP: \"ArrowUp\",\n  ARROW_DOWN: \"ArrowDown\",\n  ARROW_LEFT: \"ArrowLeft\",\n  ARROW_RIGHT: \"ArrowRight\",\n  UP: \"ArrowUp\",\n  DOWN: \"ArrowDown\",\n  LEFT: \"ArrowLeft\",\n  RIGHT: \"ArrowRight\",\n  SHIFT: \"Shift\",\n  CONTROL: \"Control\",\n  CTRL: \"Control\",\n  ALT: \"Alt\",\n  OPTION: \"Alt\", // macOS alternative name\n  META: \"Meta\",\n  COMMAND: \"Meta\", // macOS\n  CMD: \"Meta\", // macOS shorthand\n  SUPER: \"Meta\", // Linux\n  WINDOWS: \"Meta\", // Windows\n  WIN: \"Meta\", // Windows shorthand\n  HOME: \"Home\",\n  END: \"End\",\n  PAGEUP: \"PageUp\",\n  PAGEDOWN: \"PageDown\",\n  PAGE_UP: \"PageUp\",\n  PAGE_DOWN: \"PageDown\",\n  PGUP: \"PageUp\",\n  PGDN: \"PageDown\",\n};\n\n/**\n * Maps a key name from various formats to Playwright-compatible format\n * @param key The key name in any supported format\n * @returns The Playwright-compatible key name\n */\nexport function mapKeyToPlaywright(key: string): string {\n  if (!key) return key;\n  const upperKey = key.toUpperCase();\n  return KEY_MAP[upperKey] || key;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/googleCustomToolHandler.ts",
    "content": "import { Part, FunctionCall, FunctionDeclaration, Type } from \"@google/genai\";\nimport { ToolSet } from \"ai\";\nimport { LogLine } from \"../../types/public/logs.js\";\nimport { toJsonSchema } from \"../../zodCompat.js\";\nimport type { StagehandZodSchema } from \"../../zodCompat.js\";\n\n/**\n * Result of executing a custom tool for Google CUA\n */\nexport interface CustomToolExecutionResult {\n  functionResponse: Part;\n  success: boolean;\n}\n\n/**\n * Execute a custom tool and format the response for Google's API\n * This handles tool execution, result formatting, and error handling\n * specific to Google's function response format\n */\nexport async function executeGoogleCustomTool(\n  toolName: string,\n  toolArgs: Record<string, unknown>,\n  tools: ToolSet,\n  functionCall: FunctionCall,\n  logger: (message: LogLine) => void,\n): Promise<CustomToolExecutionResult> {\n  try {\n    logger({\n      category: \"agent\",\n      message: `Executing custom tool: ${toolName} with args: ${JSON.stringify(toolArgs)}`,\n      level: 1,\n    });\n\n    const tool = tools[toolName];\n    const toolResult = await tool.execute(toolArgs, {\n      toolCallId: `tool_${Date.now()}`,\n      messages: [],\n    });\n\n    logger({\n      category: \"agent\",\n      message: `Tool ${toolName} completed successfully. Result: ${JSON.stringify(toolResult)}`,\n      level: 1,\n    });\n\n    // Create function response with the result\n    const functionResponsePart: Part = {\n      functionResponse: {\n        name: toolName,\n        response: {\n          result: JSON.stringify(toolResult),\n        },\n      },\n    };\n\n    return {\n      functionResponse: functionResponsePart,\n      success: true,\n    };\n  } catch (toolError) {\n    const errorMessage =\n      toolError instanceof Error ? toolError.message : String(toolError);\n\n    logger({\n      category: \"agent\",\n      message: `Error executing custom tool ${toolName}: ${errorMessage}`,\n      level: 0,\n    });\n\n    // Create error function response\n    const functionResponsePart: Part = {\n      functionResponse: {\n        name: toolName,\n        response: {\n          error: errorMessage,\n        },\n      },\n    };\n\n    return {\n      functionResponse: functionResponsePart,\n      success: false,\n    };\n  }\n}\n\n/**\n * Check if a function call is a custom tool\n */\nexport function isCustomTool(\n  functionCall: FunctionCall,\n  tools?: ToolSet,\n): boolean {\n  return !!(tools && functionCall.name && functionCall.name in tools);\n}\n\n/**\n * Convert ToolSet to Google's FunctionDeclaration array\n * Handles the conversion of Zod schemas to Google's parameter format\n */\nexport function convertToolSetToFunctionDeclarations(\n  tools: ToolSet,\n): FunctionDeclaration[] {\n  const functionDeclarations: FunctionDeclaration[] = [];\n\n  for (const [name, tool] of Object.entries(tools)) {\n    const functionDeclaration = convertToolToFunctionDeclaration(name, tool);\n    if (functionDeclaration) {\n      functionDeclarations.push(functionDeclaration);\n    }\n  }\n\n  return functionDeclarations;\n}\n\n/**\n * Convert a single ToolSet tool to Google's FunctionDeclaration format\n */\nfunction convertToolToFunctionDeclaration(\n  name: string,\n  tool: { description?: string; inputSchema: unknown },\n): FunctionDeclaration | null {\n  try {\n    // Convert Zod schema to JSON schema\n    const schema = tool.inputSchema as StagehandZodSchema;\n    const jsonSchema = toJsonSchema(schema) as {\n      properties?: Record<string, unknown>;\n      required?: string[];\n      type?: string;\n    };\n\n    const parameters = convertJsonSchemaToGoogleParameters(jsonSchema);\n\n    return {\n      name,\n      description: tool.description || `Execute ${name}`,\n      parameters,\n    };\n  } catch (error) {\n    console.error(\n      `Error converting tool ${name} to function declaration:`,\n      error,\n    );\n    return null;\n  }\n}\n\n/**\n * Convert JSON schema to Google's parameter format\n */\nfunction convertJsonSchemaToGoogleParameters(schema: {\n  properties?: Record<string, unknown>;\n  required?: string[];\n  type?: string;\n}): {\n  type: Type;\n  properties: Record<string, { type: Type; description?: string }>;\n  required?: string[];\n} {\n  const properties: Record<string, { type: Type; description?: string }> = {};\n\n  if (schema.properties) {\n    for (const [key, value] of Object.entries(schema.properties)) {\n      const propSchema = value as {\n        type?: string;\n        description?: string;\n        items?: { type?: string };\n      };\n      properties[key] = {\n        type: mapJsonTypeToGoogleType(propSchema.type || \"string\"),\n        ...(propSchema.description\n          ? { description: propSchema.description }\n          : {}),\n      };\n    }\n  }\n\n  return {\n    type: Type.OBJECT,\n    properties,\n    ...(schema.required && schema.required.length > 0\n      ? { required: schema.required }\n      : {}),\n  };\n}\n\n/**\n * Map JSON schema types to Google's Type enum\n */\nfunction mapJsonTypeToGoogleType(jsonType: string): Type {\n  switch (jsonType.toLowerCase()) {\n    case \"string\":\n      return Type.STRING;\n    case \"number\":\n    case \"integer\":\n      return Type.NUMBER;\n    case \"boolean\":\n      return Type.BOOLEAN;\n    case \"array\":\n      return Type.ARRAY;\n    case \"object\":\n      return Type.OBJECT;\n    default:\n      return Type.STRING;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/handleDoneToolCall.ts",
    "content": "import { generateText, ModelMessage, LanguageModel, ToolSet } from \"ai\";\nimport { z } from \"zod\";\nimport { tool } from \"ai\";\nimport { LogLine } from \"../../types/public/logs.js\";\nimport { StagehandZodObject } from \"../../zodCompat.js\";\nimport { getZFactory } from \"../../../utils.js\";\nimport type { StagehandZodSchema } from \"../../zodCompat.js\";\n\ninterface DoneResult {\n  reasoning: string;\n  taskComplete: boolean;\n  messages: ModelMessage[];\n  output?: Record<string, unknown>;\n}\n\nfunction buildBaseDoneSchema(factory: typeof z) {\n  return factory.object({\n    reasoning: factory\n      .string()\n      .describe(\"Brief summary of what actions were taken and the outcome\"),\n    taskComplete: factory\n      .boolean()\n      .describe(\"true if the task was fully completed, false otherwise\"),\n  });\n}\n\n/**\n * Force a done tool call at the end of an agent run.\n * This ensures we always get a structured final response,\n * even if the main loop ended without calling done.\n */\nexport async function handleDoneToolCall(options: {\n  model: LanguageModel;\n  inputMessages: ModelMessage[];\n  instruction: string;\n  outputSchema?: StagehandZodObject;\n  logger: (message: LogLine) => void;\n}): Promise<DoneResult> {\n  const { model, inputMessages, instruction, outputSchema, logger } = options;\n\n  logger({\n    category: \"agent\",\n    message: \"Agent calling tool: done\",\n    level: 1,\n  });\n  // Use the same Zod version as the user's outputSchema to avoid v3/v4 mixing\n  const factory = outputSchema\n    ? getZFactory(outputSchema as StagehandZodSchema)\n    : z;\n  const baseDoneSchema = buildBaseDoneSchema(factory);\n\n  // Merge base done schema with user-provided output schema if present\n  const doneToolSchema = outputSchema\n    ? baseDoneSchema.extend({\n        output: outputSchema.describe(\n          \"The specific data the user requested from this task\",\n        ),\n      })\n    : baseDoneSchema;\n\n  const outputInstructions = outputSchema\n    ? `\\n\\nThe user also requested the following information from this task. Provide it in the \"output\" field:\\n${JSON.stringify(\n        Object.fromEntries(\n          Object.entries(outputSchema.shape).map(\n            ([key, value]: [string, StagehandZodSchema]) => [\n              key,\n              value.description || \"no description\",\n            ],\n          ),\n        ),\n        null,\n        2,\n      )}`\n    : \"\";\n\n  const systemPrompt = `You are a web automation assistant that was tasked with completing a task.\n\nThe task was:\n\"${instruction}\"\n\nReview what was accomplished and provide your final assessment in whether the task was completed successfully. you have been provided with the history of the actions taken so far, use this to determine if the task was completed successfully.${outputInstructions}\n\nCall the \"done\" tool with:\n1. A brief summary of what was done\n2. Whether the task was completed successfully${outputSchema ? \"\\n3. The requested output data based on what you found\" : \"\"}`;\n\n  const doneTool = tool({\n    description: outputSchema\n      ? \"Complete the task with your assessment and the requested output data.\"\n      : \"Complete the task with your final assessment.\",\n    inputSchema: doneToolSchema,\n    execute: async (params) => {\n      return { success: true, ...params };\n    },\n  });\n\n  const userPrompt: ModelMessage = {\n    role: \"user\",\n    content: outputSchema\n      ? \"Provide your final assessment and the requested output data.\"\n      : \"Provide your final assessment.\",\n  };\n\n  const result = await generateText({\n    model,\n    system: systemPrompt,\n    messages: [...inputMessages, userPrompt],\n    tools: { done: doneTool } as ToolSet,\n    toolChoice: { type: \"tool\", toolName: \"done\" },\n    providerOptions: {\n      google: { mediaResolution: \"MEDIA_RESOLUTION_HIGH\" },\n      openai: { store: false },\n    },\n  });\n\n  const doneToolCall = result.toolCalls.find((tc) => tc.toolName === \"done\");\n  const outputMessages: ModelMessage[] = [\n    userPrompt,\n    ...(result.response?.messages || []),\n  ];\n\n  if (!doneToolCall) {\n    return {\n      reasoning: result.text || \"Task execution completed\",\n      taskComplete: false,\n      messages: outputMessages,\n    };\n  }\n\n  const input = doneToolCall.input as {\n    reasoning: string;\n    taskComplete: boolean;\n    output?: Record<string, unknown>;\n  };\n  logger({\n    category: \"agent\",\n    message: `Task completed`,\n    level: 1,\n  });\n\n  return {\n    reasoning: input.reasoning,\n    taskComplete: input.taskComplete,\n    messages: outputMessages,\n    output: input.output,\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/imageCompression.ts",
    "content": "import {\n  AnthropicMessage,\n  AnthropicContentBlock,\n  AnthropicToolResult,\n  ResponseInputItem as OpenAIResponseInputItem,\n} from \"../../types/public/agent.js\";\nimport type {\n  Content as GoogleContent,\n  Part as GooglePart,\n} from \"@google/genai\";\n\nexport type ResponseInputItem = AnthropicMessage | AnthropicToolResult;\n\ninterface FunctionResponseData {\n  inlineData?: {\n    mimeType?: string;\n    data?: string;\n  };\n}\nexport type AnthropicResponseInputItem = AnthropicMessage | AnthropicToolResult;\nexport type SupportedInputItem =\n  | AnthropicResponseInputItem\n  | OpenAIResponseInputItem\n  | GoogleContent;\n\n/**\n * Finds all items in the conversation history that contain images\n * @param items - Array of conversation items to check\n * @returns Array of indices where images were found\n */\nexport function findItemsWithImages(items: ResponseInputItem[]): number[] {\n  const itemsWithImages: number[] = [];\n\n  items.forEach((item, index) => {\n    let hasImage = false;\n\n    if (Array.isArray(item.content)) {\n      hasImage = item.content.some(\n        (contentItem: AnthropicContentBlock) =>\n          contentItem.type === \"tool_result\" &&\n          \"content\" in contentItem &&\n          Array.isArray(contentItem.content) &&\n          (contentItem.content as AnthropicContentBlock[]).some(\n            (nestedItem: AnthropicContentBlock) => nestedItem.type === \"image\",\n          ),\n      );\n    }\n\n    if (hasImage) {\n      itemsWithImages.push(index);\n    }\n  });\n\n  return itemsWithImages;\n}\n\n/**\n * Compresses conversation history by removing images from older items\n * while keeping the most recent images intact\n * @param items - Array of conversation items to process\n * @param keepMostRecentCount - Number of most recent image-containing items to preserve (default: 2)\n * @returns Object with processed items\n */\nexport function compressConversationImages(\n  items: ResponseInputItem[],\n  keepMostRecentCount: number = 2,\n): { items: ResponseInputItem[] } {\n  const itemsWithImages = findItemsWithImages(items);\n\n  items.forEach((item, index) => {\n    const imageIndex = itemsWithImages.indexOf(index);\n    const shouldCompress =\n      imageIndex >= 0 &&\n      imageIndex < itemsWithImages.length - keepMostRecentCount;\n\n    if (shouldCompress) {\n      if (Array.isArray(item.content)) {\n        item.content = item.content.map(\n          (contentItem: AnthropicContentBlock) => {\n            if (\n              contentItem.type === \"tool_result\" &&\n              \"content\" in contentItem &&\n              Array.isArray(contentItem.content) &&\n              (contentItem.content as AnthropicContentBlock[]).some(\n                (nestedItem: AnthropicContentBlock) =>\n                  nestedItem.type === \"image\",\n              )\n            ) {\n              return {\n                ...contentItem,\n                content: \"screenshot taken\",\n              } as AnthropicContentBlock;\n            }\n            return contentItem;\n          },\n        );\n      }\n    }\n  });\n\n  return {\n    items,\n  };\n}\n\n/**\n * Finds all items in the conversation history that contain images (Google format)\n * @param items - Array of conversation items to check\n * @returns Array of indices where images were found\n */\nexport function findGoogleItemsWithImages(items: GoogleContent[]): number[] {\n  const itemsWithImages: number[] = [];\n\n  items.forEach((item, index) => {\n    let hasImage = false;\n\n    if (item.parts && Array.isArray(item.parts)) {\n      hasImage = item.parts.some((part: GooglePart) => {\n        // Check for functionResponse with data containing images\n        if (part.functionResponse?.response?.data) {\n          const data = part.functionResponse.response\n            .data as FunctionResponseData[];\n          return data.some((dataItem) =>\n            dataItem.inlineData?.mimeType?.startsWith(\"image/\"),\n          );\n        }\n\n        // Check for functionResponse with parts containing images\n        if (part.functionResponse?.parts) {\n          return part.functionResponse.parts.some((responsePart) =>\n            responsePart.inlineData?.mimeType?.startsWith(\"image/\"),\n          );\n        }\n\n        // Check for direct inline data\n        return part.inlineData?.mimeType?.startsWith(\"image/\");\n      });\n    }\n\n    if (hasImage) {\n      itemsWithImages.push(index);\n    }\n  });\n\n  return itemsWithImages;\n}\n\n/**\n * Finds all items in the conversation history that contain images (OpenAI format)\n * @param items - Array of conversation items to check\n * @returns Array of indices where images were found\n */\nexport function findOpenAIItemsWithImages(\n  items: OpenAIResponseInputItem[],\n): number[] {\n  const itemsWithImages: number[] = [];\n\n  items.forEach((item, index) => {\n    let hasImage = false;\n\n    // Check for computer_call_output with image\n    if (\n      \"type\" in item &&\n      item.type === \"computer_call_output\" &&\n      \"output\" in item\n    ) {\n      const output = item.output as unknown as {\n        type: string;\n        image_url: string;\n      };\n      hasImage = output?.type === \"input_image\" && !!output?.image_url;\n    }\n\n    if (hasImage) {\n      itemsWithImages.push(index);\n    }\n  });\n\n  return itemsWithImages;\n}\n\n/**\n * Compresses OpenAI conversation history by removing images from older items\n * while keeping the most recent images intact\n * @param items - Array of conversation items to process\n * @param keepMostRecentCount - Number of most recent image-containing items to preserve (default: 2)\n * @returns Object with processed items\n */\nexport function compressOpenAIConversationImages(\n  items: OpenAIResponseInputItem[],\n  keepMostRecentCount: number = 2,\n): { items: OpenAIResponseInputItem[] } {\n  const itemsWithImages = findOpenAIItemsWithImages(items);\n\n  items.forEach((item, index) => {\n    const imageIndex = itemsWithImages.indexOf(index);\n    const shouldCompress =\n      imageIndex >= 0 &&\n      imageIndex < itemsWithImages.length - keepMostRecentCount;\n\n    if (shouldCompress) {\n      // For computer_call_output with image, replace with text\n      if (\n        \"type\" in item &&\n        item.type === \"computer_call_output\" &&\n        \"output\" in item\n      ) {\n        const output = item.output as unknown as { type: string };\n        if (output?.type === \"input_image\") {\n          // Replace the image with a text message\n          (item as unknown as { output: string }).output = \"screenshot taken\";\n        }\n      }\n    }\n  });\n\n  return {\n    items,\n  };\n}\n\n/**\n * Compresses Google conversation history by removing images from older items\n * while keeping the most recent images intact\n * @param items - Array of conversation items to process\n * @param keepMostRecentCount - Number of most recent image-containing items to preserve (default: 2)\n * @returns Object with processed items\n */\nexport function compressGoogleConversationImages(\n  items: GoogleContent[],\n  keepMostRecentCount: number = 2,\n): { items: GoogleContent[] } {\n  const itemsWithImages = findGoogleItemsWithImages(items);\n\n  items.forEach((item, index) => {\n    const imageIndex = itemsWithImages.indexOf(index);\n    const shouldCompress =\n      imageIndex >= 0 &&\n      imageIndex < itemsWithImages.length - keepMostRecentCount;\n\n    if (shouldCompress && item.parts && Array.isArray(item.parts)) {\n      item.parts = item.parts.map((part: GooglePart) => {\n        // Replace functionResponse with data containing images\n        if (part.functionResponse?.response?.data) {\n          const data = part.functionResponse.response\n            .data as FunctionResponseData[];\n          const hasImage = data.some((dataItem) =>\n            dataItem.inlineData?.mimeType?.startsWith(\"image/\"),\n          );\n          if (hasImage) {\n            return {\n              ...part,\n              functionResponse: {\n                ...part.functionResponse,\n                data: [] as FunctionResponseData[],\n                response: {\n                  ...part.functionResponse.response,\n                  compressed: \"screenshot taken\",\n                },\n              },\n            };\n          }\n        }\n\n        // Replace functionResponse with parts containing images\n        if (part.functionResponse?.parts) {\n          const hasImageInParts = part.functionResponse.parts.some(\n            (responsePart) =>\n              responsePart.inlineData?.mimeType?.startsWith(\"image/\"),\n          );\n          if (hasImageInParts) {\n            return {\n              ...part,\n              functionResponse: {\n                ...part.functionResponse,\n                parts: part.functionResponse.parts.filter(\n                  (responsePart) =>\n                    !responsePart.inlineData?.mimeType?.startsWith(\"image/\"),\n                ),\n                response: {\n                  ...part.functionResponse.response,\n                  compressed: \"screenshot taken\",\n                },\n              },\n            };\n          }\n        }\n\n        // Replace direct inline data images\n        if (part.inlineData?.mimeType?.startsWith(\"image/\")) {\n          return {\n            text: \"screenshot taken\",\n          };\n        }\n        return part;\n      });\n    }\n  });\n\n  return {\n    items,\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/messageProcessing.ts",
    "content": "import type { ModelMessage } from \"ai\";\n\n// Vision action tools that include screenshots in their results\nconst VISION_ACTION_TOOLS = [\n  \"click\",\n  \"type\",\n  \"dragAndDrop\",\n  \"wait\",\n  \"fillFormVision\",\n  \"scroll\",\n];\n\nfunction isToolMessage(\n  message: unknown,\n): message is { role: \"tool\"; content: unknown[] } {\n  return (\n    !!message &&\n    typeof message === \"object\" &&\n    (message as { role?: unknown }).role === \"tool\" &&\n    Array.isArray((message as { content?: unknown }).content)\n  );\n}\n\nfunction isScreenshotPart(part: unknown): boolean {\n  return (\n    !!part &&\n    typeof part === \"object\" &&\n    (part as { toolName?: unknown }).toolName === \"screenshot\"\n  );\n}\n\nfunction isVisionActionPart(part: unknown): boolean {\n  if (!part || typeof part !== \"object\") return false;\n  const toolName = (part as { toolName?: unknown }).toolName;\n  return typeof toolName === \"string\" && VISION_ACTION_TOOLS.includes(toolName);\n}\n\nfunction isVisionPart(part: unknown): boolean {\n  return isScreenshotPart(part) || isVisionActionPart(part);\n}\n\nfunction isAriaTreePart(part: unknown): boolean {\n  return (\n    !!part &&\n    typeof part === \"object\" &&\n    (part as { toolName?: unknown }).toolName === \"ariaTree\"\n  );\n}\n\n/**\n * Compress old screenshot/ariaTree data in messages in-place.\n *\n * Strategy:\n * - Keep only the 2 most recent vision results (screenshots OR vision action tools like click/type/etc)\n * - Keep only the 1 most recent ariaTree (replace older ones with placeholder)\n *\n * @param messages - The messages array to modify in-place\n * @returns Number of items compressed\n */\nexport function processMessages(messages: ModelMessage[]): number {\n  let compressedCount = 0;\n\n  // Find indices of all vision-related tool results (screenshots + vision actions)\n  // and ariaTree results\n  const visionIndices: number[] = [];\n  const ariaTreeIndices: number[] = [];\n\n  for (let i = 0; i < messages.length; i++) {\n    const message = messages[i];\n    if (isToolMessage(message)) {\n      const content = message.content as unknown[];\n      if (content.some(isVisionPart)) {\n        visionIndices.push(i);\n      }\n      if (content.some(isAriaTreePart)) {\n        ariaTreeIndices.push(i);\n      }\n    }\n  }\n\n  // Compress old vision results (keep 2 most recent across all vision tools)\n  if (visionIndices.length > 2) {\n    const toCompress = visionIndices.slice(0, visionIndices.length - 2);\n    for (const index of toCompress) {\n      const message = messages[index];\n      if (isToolMessage(message)) {\n        // Both functions are safe to call - they only modify their respective part types\n        compressScreenshotMessage(message);\n        compressVisionActionMessage(message);\n        compressedCount++;\n      }\n    }\n  }\n\n  // Compress old ariaTree results (keep 1 most recent)\n  if (ariaTreeIndices.length > 1) {\n    const toCompress = ariaTreeIndices.slice(0, ariaTreeIndices.length - 1);\n    for (const idx of toCompress) {\n      const message = messages[idx];\n      if (isToolMessage(message)) {\n        compressAriaTreeMessage(message);\n        compressedCount++;\n      }\n    }\n  }\n\n  return compressedCount;\n}\n\n/**\n * Tool result part structure from AI SDK.\n * The output field uses a discriminated union - type determines value format:\n * - type: \"content\" -> value: Array<{type: \"text\", ...} | {type: \"media\", ...}>\n * - type: \"text\" -> value: string\n * - type: \"json\" -> value: JSONValue\n * - type: \"error-text\" -> value: string\n * - type: \"error-json\" -> value: JSONValue\n */\ninterface ToolResultPart {\n  output?: {\n    type: string;\n    value?: unknown;\n  };\n}\n\n/**\n * Check if output has type \"content\" (array-based value format).\n * Only outputs with type \"content\" should have array values.\n */\nfunction isContentTypeOutput(output: {\n  type: string;\n  value?: unknown;\n}): boolean {\n  return output.type === \"content\";\n}\n\n/**\n * Compress screenshot message content in-place.\n * Only modifies outputs with type \"content\" to maintain schema validity.\n * Replaces entire output object to ensure type/value consistency.\n */\nfunction compressScreenshotMessage(message: {\n  role: \"tool\";\n  content: unknown[];\n}): void {\n  for (const part of message.content) {\n    if (isScreenshotPart(part)) {\n      const typedPart = part as ToolResultPart;\n      // Only compress if output exists and has type \"content\"\n      if (typedPart.output && isContentTypeOutput(typedPart.output)) {\n        // Replace entire output to ensure type/value consistency\n        typedPart.output = {\n          type: \"content\",\n          value: [{ type: \"text\", text: \"screenshot taken\" }],\n        };\n      }\n    }\n  }\n}\n\n/**\n * Compress vision action message content in-place by removing the screenshot\n * but keeping the action result text.\n * Only modifies outputs with type \"content\" to maintain schema validity.\n */\nfunction compressVisionActionMessage(message: {\n  role: \"tool\";\n  content: unknown[];\n}): void {\n  for (const part of message.content) {\n    if (isVisionActionPart(part)) {\n      const typedPart = part as ToolResultPart;\n\n      // Only compress if output is type \"content\" (array-based value)\n      if (\n        typedPart.output &&\n        isContentTypeOutput(typedPart.output) &&\n        Array.isArray(typedPart.output.value)\n      ) {\n        // Filter out media content but keep text results\n        const filteredValue = (\n          typedPart.output.value as Array<{ type?: string }>\n        ).filter(\n          (item) => item && typeof item === \"object\" && item.type !== \"media\",\n        );\n        // Replace entire output to ensure type/value consistency\n        typedPart.output = {\n          type: \"content\",\n          value: filteredValue,\n        };\n      }\n    }\n  }\n}\n\n/**\n * Compress ariaTree message content in-place.\n * Only modifies outputs with type \"content\" to maintain schema validity.\n * Replaces entire output object to ensure type/value consistency.\n */\nfunction compressAriaTreeMessage(message: {\n  role: \"tool\";\n  content: unknown[];\n}): void {\n  for (const part of message.content) {\n    if (isAriaTreePart(part)) {\n      const typedPart = part as ToolResultPart;\n      // Only compress if output exists and has type \"content\"\n      if (typedPart.output && isContentTypeOutput(typedPart.output)) {\n        typedPart.output = {\n          type: \"content\",\n          value: [\n            {\n              type: \"text\",\n              text: \"ARIA tree extracted for context of page elements\",\n            },\n          ],\n        };\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/screenshotHandler.ts",
    "content": "import type { Page } from \"../../understudy/page.js\";\n\n/**\n * Default delay in milliseconds to wait after vision actions before capturing screenshot.\n * Allows the page to settle after interactions.\n */\nconst DEFAULT_DELAY_MS = 500;\n\n/**\n * Waits for the page to settle and captures a screenshot.\n * If the screenshot fails (e.g., page closed, navigation in progress),\n * returns undefined instead of throwing - allowing the action to still succeed.\n *\n * @param page - The page to capture\n * @param delayMs - Delay before capturing (default: 500ms, pass 0 to skip delay)\n */\nexport async function waitAndCaptureScreenshot(\n  page: Page,\n  delayMs: number = DEFAULT_DELAY_MS,\n): Promise<string | undefined> {\n  if (delayMs > 0) {\n    await page.waitForTimeout(delayMs);\n  }\n\n  try {\n    const buffer = await page.screenshot({ fullPage: false });\n    return buffer.toString(\"base64\");\n  } catch {\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/validateExperimentalFeatures.ts",
    "content": "import {\n  ExperimentalNotConfiguredError,\n  StagehandInvalidArgumentError,\n} from \"../../types/public/sdkErrors.js\";\nimport type {\n  AgentConfig,\n  AgentExecuteOptionsBase,\n} from \"../../types/public/index.js\";\n\nexport interface AgentValidationOptions {\n  /** Whether experimental mode is enabled */\n  isExperimental: boolean;\n  /** Agent config options (integrations, tools, stream, cua, etc.) */\n  agentConfig?: Partial<AgentConfig>;\n  /** Execute options (callbacks, signal, messages, etc.) */\n  executeOptions?:\n    | (Partial<AgentExecuteOptionsBase> & { callbacks?: unknown })\n    | null;\n  /** Whether this is streaming mode (can be derived from agentConfig.stream) */\n  isStreaming?: boolean;\n}\n\n/**\n * Validates agent configuration and experimental feature usage.\n *\n * This utility consolidates all validation checks for both CUA and non-CUA agent paths:\n * - Invalid argument errors for CUA (streaming, abort signal, message continuation, excludeTools, output schema are not supported)\n * - Experimental feature checks for integrations and tools (both CUA and non-CUA)\n * - Experimental feature checks for hybrid mode (requires experimental: true)\n * - Experimental feature checks for non-CUA only (callbacks, signal, messages, streaming, excludeTools, output schema)\n *\n * Throws StagehandInvalidArgumentError for invalid/unsupported configurations.\n * Throws ExperimentalNotConfiguredError if experimental features are used without experimental mode.\n */\nexport function validateExperimentalFeatures(\n  options: AgentValidationOptions,\n): void {\n  const { isExperimental, agentConfig, executeOptions, isStreaming } = options;\n\n  // Check if CUA mode is enabled (via mode: \"cua\" or deprecated cua: true)\n  const isCuaMode =\n    agentConfig?.mode !== undefined\n      ? agentConfig.mode === \"cua\"\n      : agentConfig?.cua === true;\n\n  // CUA-specific validation: certain features are not available at all\n  if (isCuaMode) {\n    const unsupportedFeatures: string[] = [];\n\n    if (agentConfig?.stream) {\n      unsupportedFeatures.push(\"streaming\");\n    }\n    if (executeOptions?.signal) {\n      unsupportedFeatures.push(\"abort signal\");\n    }\n    if (executeOptions?.messages) {\n      unsupportedFeatures.push(\"message continuation\");\n    }\n    if (\n      executeOptions?.excludeTools &&\n      executeOptions.excludeTools.length > 0\n    ) {\n      unsupportedFeatures.push(\"excludeTools\");\n    }\n    if (executeOptions?.output) {\n      unsupportedFeatures.push(\"output schema\");\n    }\n    if (\n      executeOptions?.variables &&\n      Object.keys(executeOptions.variables).length > 0\n    ) {\n      unsupportedFeatures.push(\"variables\");\n    }\n\n    if (unsupportedFeatures.length > 0) {\n      throw new StagehandInvalidArgumentError(\n        `${unsupportedFeatures.join(\", \")} ${unsupportedFeatures.length === 1 ? \"is\" : \"are\"} not supported with CUA (Computer Use Agent) mode.`,\n      );\n    }\n  }\n\n  // Skip experimental checks if already in experimental mode\n  if (isExperimental) return;\n\n  const features: string[] = [];\n\n  // Check agent config features (check array length to avoid false positives for empty arrays)\n  const hasIntegrations =\n    agentConfig?.integrations && agentConfig.integrations.length > 0;\n  const hasTools =\n    agentConfig?.tools && Object.keys(agentConfig.tools).length > 0;\n  if (hasIntegrations || hasTools) {\n    features.push(\"MCP integrations and custom tools\");\n  }\n\n  // Check streaming mode (either explicit or derived from config) - only for non-CUA\n  if (!isCuaMode && (isStreaming || agentConfig?.stream)) {\n    features.push(\"streaming\");\n  }\n\n  // Check execute options features - only for non-CUA\n  if (executeOptions && !isCuaMode) {\n    if (executeOptions.callbacks) {\n      features.push(\"callbacks\");\n    }\n    if (executeOptions.signal) {\n      features.push(\"abort signal\");\n    }\n    if (executeOptions.messages) {\n      features.push(\"message continuation\");\n    }\n    if (executeOptions.excludeTools && executeOptions.excludeTools.length > 0) {\n      features.push(\"excludeTools\");\n    }\n    if (executeOptions.output) {\n      features.push(\"output schema\");\n    }\n    if (\n      executeOptions.variables &&\n      Object.keys(executeOptions.variables).length > 0\n    ) {\n      features.push(\"variables\");\n    }\n  }\n\n  if (features.length > 0) {\n    throw new ExperimentalNotConfiguredError(`Agent ${features.join(\", \")}`);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/variables.ts",
    "content": "import type { Variables, VariableValue } from \"../../types/public/agent.js\";\n\n/**\n * Resolves a VariableValue to its primitive string value.\n * Handles both simple primitives (\"secret\") and rich objects ({ value: \"secret\", description: \"...\" }).\n */\nexport function resolveVariableValue(v: VariableValue): string {\n  if (typeof v === \"object\" && v !== null && \"value\" in v) {\n    return String(v.value);\n  }\n  return String(v);\n}\n\n/**\n * Extracts the optional description from a VariableValue.\n * Returns undefined for simple primitive values.\n */\nexport function getVariableDescription(v: VariableValue): string | undefined {\n  if (typeof v === \"object\" && v !== null && \"value\" in v) {\n    return v.description;\n  }\n  return undefined;\n}\n\n/**\n * Substitutes %variableName% tokens in text with resolved variable values.\n * Works with both simple and rich variable formats.\n */\nexport function substituteVariables(\n  text: string,\n  variables?: Variables,\n): string {\n  if (!variables) return text;\n  let result = text;\n  for (const [key, v] of Object.entries(variables)) {\n    const token = `%${key}%`;\n    result = result.split(token).join(resolveVariableValue(v));\n  }\n  return result;\n}\n\n/**\n * Flattens Variables to Record<string, string> for internal consumers\n * that only need key→value mappings (e.g., actHandler, cache replay).\n */\nexport function flattenVariables(\n  variables?: Variables,\n): Record<string, string> | undefined {\n  if (!variables || Object.keys(variables).length === 0) return undefined;\n  const result: Record<string, string> = {};\n  for (const [key, v] of Object.entries(variables)) {\n    result[key] = resolveVariableValue(v);\n  }\n  return result;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/agent/utils/xpath.ts",
    "content": "/**\n * Utility functions for XPath handling in agent tools.\n */\n\n/**\n * Ensures a value is properly formatted as an XPath selector.\n * Returns null if the value is not a valid string.\n *\n * @param value - The value to normalize as an XPath\n * @returns The normalized XPath string prefixed with \"xpath=\" or null\n */\nexport function ensureXPath(value: unknown): string | null {\n  if (typeof value !== \"string\") return null;\n  const trimmed = value.trim();\n  if (!trimmed) return null;\n  return trimmed.startsWith(\"xpath=\") ? trimmed : `xpath=${trimmed}`;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/api.ts",
    "content": "import makeFetchCookie from \"fetch-cookie\";\nimport { loadApiKeyFromEnv } from \"../utils.js\";\nimport { STAGEHAND_VERSION } from \"../version.js\";\nimport {\n  StagehandAPIError,\n  StagehandAPIUnauthorizedError,\n  StagehandHttpError,\n  StagehandResponseBodyError,\n  StagehandResponseParseError,\n  StagehandServerError,\n  ExperimentalNotConfiguredError,\n} from \"./types/public/index.js\";\nimport type {\n  ActResult,\n  AgentConfig,\n  AgentExecuteOptions,\n  AgentResult,\n  ExtractResult,\n  ObserveResult,\n  LogLine,\n  StagehandMetrics,\n  BrowserbaseRegion,\n  ActOptions,\n  ExtractOptions,\n  ObserveOptions,\n  Api,\n} from \"./types/public/index.js\";\nimport type {\n  SerializableResponse,\n  AgentCacheTransferPayload,\n} from \"./types/private/index.js\";\nimport type { ModelConfiguration } from \"./types/public/model.js\";\nimport { toJsonSchema } from \"./zodCompat.js\";\nimport type { StagehandZodSchema } from \"./zodCompat.js\";\n\n// =============================================================================\n// Multi-region API URL mapping\n// =============================================================================\n\n/**\n * Mapping of Browserbase regions to their corresponding Stagehand API base URLs.\n * Users should configure their client to hit the API endpoint that matches\n * the region where their browser session is running.\n */\nexport const REGION_API_URLS: Record<BrowserbaseRegion, string> = {\n  \"us-west-2\": \"https://api.stagehand.browserbase.com\",\n  \"us-east-1\": \"https://api.use1.stagehand.browserbase.com\",\n  \"eu-central-1\": \"https://api.euc1.stagehand.browserbase.com\",\n  \"ap-southeast-1\": \"https://api.apse1.stagehand.browserbase.com\",\n};\n\n/**\n * Returns the full API URL (with /v1 suffix) for a given Browserbase region.\n * If no region is specified or the region is unknown, defaults to us-west-2.\n *\n * @param region - The Browserbase region (e.g., \"us-west-2\", \"eu-central-1\")\n * @returns The full API URL including /v1 suffix\n */\nexport function getApiUrlForRegion(\n  region: BrowserbaseRegion | undefined,\n): string {\n  const baseUrl =\n    REGION_API_URLS[region as BrowserbaseRegion] ??\n    REGION_API_URLS[\"us-west-2\"];\n  return `${baseUrl}/v1`;\n}\n\n// =============================================================================\n// Client-specific types (can't be Zod schemas due to functions/Page objects)\n// =============================================================================\n//\n// These types mirror the Api.* schemas from types/public/api.ts but include\n// non-serializable SDK fields (like Page objects) that get stripped before\n// sending requests over the wire.\n//\n// Relationship to wire format:\n// - Client accepts: SDK types (ActOptions, ExtractOptions, etc.) with optional `page`\n// - Wire sends: Api.* types (page stripped, Zod schema converted to JSON schema)\n// - Client returns: SDK result types (ActResult, ExtractResult, etc.)\n// =============================================================================\n\n/**\n * Constructor parameters for StagehandAPIClient\n */\ninterface StagehandAPIConstructorParams {\n  apiKey: string;\n  projectId?: string;\n  logger: (message: LogLine) => void;\n  /**\n   * When true, enables server-side caching by default for all requests.\n   * When false, disables server-side caching.\n   * Defaults to true (caching enabled).\n   * Can be overridden per-method in act(), extract(), and observe() options.\n   */\n  serverCache?: boolean;\n}\n\n/**\n * Parameters for starting a session via the API client.\n * Extends Api.SessionStartRequest with client-specific field (modelApiKey).\n *\n * Wire format: Api.SessionStartRequest (modelApiKey sent via header, not body)\n */\ninterface ClientSessionStartParams extends Api.SessionStartRequest {\n  /** Model API key - sent via x-model-api-key header, not in request body */\n  modelApiKey: string;\n}\n\n/**\n * Generic API response wrapper matching Api.*Response schemas\n */\ntype ApiResponse<T> =\n  | { success: true; data: T }\n  | { success: false; message: string };\n\n/**\n * Union of all API request body types for type-safe execute() calls\n */\ntype ApiRequestBody =\n  | Api.ActRequest\n  | Api.ExtractRequest\n  | Api.ObserveRequest\n  | Api.NavigateRequest\n  | Api.AgentExecuteRequest;\n\n/**\n * Parameters for executing an action via the streaming API\n */\ninterface ExecuteActionParams {\n  method: \"act\" | \"extract\" | \"observe\" | \"navigate\" | \"end\" | \"agentExecute\";\n  args?: ApiRequestBody;\n  params?: Record<string, string>;\n  /**\n   * Override the instance-level serverCache setting for this request.\n   * When true, enables server-side caching.\n   * When false, disables server-side caching.\n   */\n  serverCache?: boolean;\n}\n\n/**\n * Client parameters for act() method.\n * Derives structure from Api.ActRequest but uses SDK's ActOptions (which includes `page`).\n * Before serialization, `page` is stripped to produce Api.ActRequest wire format.\n */\ninterface ClientActParameters {\n  input: Api.ActRequest[\"input\"];\n  options?: ActOptions;\n  frameId?: Api.ActRequest[\"frameId\"];\n}\n\n/**\n * Client parameters for extract() method.\n * Derives structure from Api.ExtractRequest but uses SDK's ExtractOptions (which includes `page`)\n * and accepts Zod schema (converted to JSON schema for wire format).\n */\ninterface ClientExtractParameters {\n  instruction?: Api.ExtractRequest[\"instruction\"];\n  schema?: StagehandZodSchema;\n  options?: ExtractOptions;\n  frameId?: Api.ExtractRequest[\"frameId\"];\n}\n\n/**\n * Client parameters for observe() method.\n * Derives structure from Api.ObserveRequest but uses SDK's ObserveOptions (which includes `page`).\n * Before serialization, `page` is stripped to produce Api.ObserveRequest wire format.\n */\ninterface ClientObserveParameters {\n  instruction?: Api.ObserveRequest[\"instruction\"];\n  options?: ObserveOptions;\n  frameId?: Api.ObserveRequest[\"frameId\"];\n}\n\nexport class StagehandAPIClient {\n  private apiKey: string;\n  private projectId?: string;\n  private sessionId?: string;\n  private modelApiKey: string;\n  private modelProvider?: string;\n  private region?: BrowserbaseRegion;\n  private logger: (message: LogLine) => void;\n  private fetchWithCookies;\n  private serverCache: boolean;\n  private lastFinishedEventData: Record<string, unknown> | null = null;\n  private latestAgentCacheEntry: AgentCacheTransferPayload | null = null;\n\n  constructor({\n    apiKey,\n    projectId,\n    logger,\n    serverCache,\n  }: StagehandAPIConstructorParams) {\n    this.apiKey = apiKey;\n    this.projectId = projectId;\n    this.logger = logger;\n    this.serverCache = serverCache ?? true;\n    // Create a single cookie jar instance that will persist across all requests\n    this.fetchWithCookies = makeFetchCookie(fetch);\n  }\n\n  async init({\n    modelName,\n    modelApiKey,\n    domSettleTimeoutMs,\n    verbose,\n    systemPrompt,\n    selfHeal,\n    browserbaseSessionCreateParams,\n    browserbaseSessionID,\n    // browser,  TODO for local browsers\n  }: ClientSessionStartParams): Promise<Api.SessionStartResult> {\n    if (!modelApiKey) {\n      throw new StagehandAPIError(\"modelApiKey is required\");\n    }\n    this.modelApiKey = modelApiKey;\n    // Extract provider from modelName (e.g., \"openai/gpt-5-nano\" -> \"openai\")\n    this.modelProvider = modelName?.includes(\"/\")\n      ? modelName.split(\"/\")[0]\n      : undefined;\n\n    // Store the region for multi-region API URL resolution\n    this.region = browserbaseSessionCreateParams?.region;\n\n    this.logger({\n      category: \"init\",\n      message: \"Creating new browserbase session...\",\n      level: 1,\n    });\n\n    // Build wire-format request body (Api.SessionStartRequest shape)\n    const requestBody: Api.SessionStartRequest = {\n      modelName,\n      domSettleTimeoutMs,\n      verbose,\n      systemPrompt,\n      selfHeal,\n      browserbaseSessionCreateParams,\n      browserbaseSessionID,\n      // browser, TODO: only send when connected to local fastify\n    };\n\n    const sessionResponse = await this.request(\"/sessions/start\", {\n      method: \"POST\",\n      body: JSON.stringify(requestBody),\n    });\n\n    if (sessionResponse.status === 401) {\n      throw new StagehandAPIUnauthorizedError(\n        \"Unauthorized. Ensure you provided a valid API key.\",\n      );\n    } else if (sessionResponse.status !== 200) {\n      const errorText = await sessionResponse.text();\n      this.logger({\n        category: \"api\",\n        message: `API error (${sessionResponse.status}): ${errorText}`,\n        level: 0,\n      });\n      throw new StagehandHttpError(`Unknown error: ${sessionResponse.status}`);\n    }\n\n    const sessionResponseBody =\n      (await sessionResponse.json()) as ApiResponse<Api.SessionStartResult>;\n\n    if (sessionResponseBody.success === false) {\n      throw new StagehandAPIError(sessionResponseBody.message);\n    }\n\n    // Temporary reroute for rollout\n    if (!sessionResponseBody.data?.available && browserbaseSessionID) {\n      sessionResponseBody.data.sessionId = browserbaseSessionID;\n    }\n\n    this.sessionId = sessionResponseBody.data.sessionId;\n\n    return sessionResponseBody.data;\n  }\n\n  async act({\n    input,\n    options,\n    frameId,\n  }: ClientActParameters): Promise<ActResult> {\n    // Strip non-serializable `page` and SDK-only fields from options before wire serialization\n    let wireOptions: Api.ActRequest[\"options\"];\n    let serverCache: boolean | undefined;\n    if (options) {\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      const { page: _, serverCache: enableCache, ...restOptions } = options;\n      serverCache = enableCache;\n      if (Object.keys(restOptions).length > 0) {\n        if (restOptions.model) {\n          restOptions.model = this.prepareModelConfig(restOptions.model);\n        }\n        wireOptions = restOptions as unknown as Api.ActRequest[\"options\"];\n      }\n    }\n\n    // Build wire-format request body\n    const requestBody: Api.ActRequest = {\n      input,\n      options: wireOptions,\n      frameId,\n    };\n\n    return this.execute<ActResult>({\n      method: \"act\",\n      args: requestBody,\n      serverCache,\n    });\n  }\n\n  async extract<T extends StagehandZodSchema>({\n    instruction,\n    schema: zodSchema,\n    options,\n    frameId,\n  }: ClientExtractParameters): Promise<ExtractResult<T>> {\n    // Convert Zod schema to JSON schema for wire format\n    const jsonSchema = zodSchema ? toJsonSchema(zodSchema) : undefined;\n\n    // Strip non-serializable `page` and SDK-only fields from options before wire serialization\n    let wireOptions: Api.ExtractRequest[\"options\"];\n    let serverCache: boolean | undefined;\n    if (options) {\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      const { page: _, serverCache: enableCache, ...restOptions } = options;\n      serverCache = enableCache;\n      if (Object.keys(restOptions).length > 0) {\n        if (restOptions.model) {\n          restOptions.model = this.prepareModelConfig(restOptions.model);\n        }\n        wireOptions = restOptions as unknown as Api.ExtractRequest[\"options\"];\n      }\n    }\n\n    // Build wire-format request body\n    const requestBody: Api.ExtractRequest = {\n      instruction,\n      schema: jsonSchema,\n      options: wireOptions,\n      frameId,\n    };\n\n    return this.execute<ExtractResult<T>>({\n      method: \"extract\",\n      args: requestBody,\n      serverCache,\n    });\n  }\n\n  async observe({\n    instruction,\n    options,\n    frameId,\n  }: ClientObserveParameters): Promise<ObserveResult> {\n    // Strip non-serializable `page` and SDK-only fields from options before wire serialization\n    let wireOptions: Api.ObserveRequest[\"options\"];\n    let serverCache: boolean | undefined;\n    if (options) {\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      const { page: _, serverCache: enableCache, ...restOptions } = options;\n      serverCache = enableCache;\n      if (Object.keys(restOptions).length > 0) {\n        if (restOptions.model) {\n          restOptions.model = this.prepareModelConfig(restOptions.model);\n        }\n        wireOptions = restOptions as unknown as Api.ObserveRequest[\"options\"];\n      }\n    }\n\n    // Build wire-format request body\n    const requestBody: Api.ObserveRequest = {\n      instruction,\n      options: wireOptions,\n      frameId,\n    };\n\n    return this.execute<ObserveResult>({\n      method: \"observe\",\n      args: requestBody,\n      serverCache,\n    });\n  }\n\n  async goto(\n    url: string,\n    options?: Api.NavigateRequest[\"options\"],\n    frameId?: string,\n  ): Promise<SerializableResponse | null> {\n    const requestBody: Api.NavigateRequest = { url, options, frameId };\n\n    return this.execute<SerializableResponse | null>({\n      method: \"navigate\",\n      args: requestBody,\n    });\n  }\n\n  async agentExecute(\n    agentConfig: AgentConfig,\n    executeOptions: AgentExecuteOptions | string,\n    frameId?: string,\n    shouldCache?: boolean,\n  ): Promise<AgentResult> {\n    // Check if integrations are being used in API mode (not supported)\n    if (agentConfig.integrations && agentConfig.integrations.length > 0) {\n      throw new ExperimentalNotConfiguredError(\"MCP integrations\");\n    }\n\n    // Strip non-serializable `page` from executeOptions before wire serialization\n    let wireExecuteOptions: Api.AgentExecuteRequest[\"executeOptions\"];\n    if (typeof executeOptions === \"string\") {\n      wireExecuteOptions = { instruction: executeOptions };\n    } else if (executeOptions.page) {\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      const { page: _, ...rest } = executeOptions;\n      wireExecuteOptions = rest;\n    } else {\n      wireExecuteOptions = executeOptions;\n    }\n\n    const wireAgentConfig: Api.AgentExecuteRequest[\"agentConfig\"] = {\n      systemPrompt: agentConfig.systemPrompt,\n      mode: agentConfig.mode ?? (agentConfig.cua === true ? \"cua\" : undefined),\n      cua: agentConfig.mode === undefined ? agentConfig.cua : undefined,\n      model: agentConfig.model\n        ? this.prepareModelConfig(agentConfig.model)\n        : undefined,\n      executionModel: agentConfig.executionModel\n        ? this.prepareModelConfig(agentConfig.executionModel)\n        : undefined,\n    };\n\n    // Build wire-format request body\n    const requestBody: Api.AgentExecuteRequest = {\n      agentConfig: wireAgentConfig,\n      executeOptions: wireExecuteOptions,\n      frameId,\n      shouldCache,\n    };\n\n    const result = await this.execute<AgentResult>({\n      method: \"agentExecute\",\n      args: requestBody,\n    });\n\n    const finishedData =\n      this.consumeFinishedEventData<Api.AgentExecuteResult>() ?? null;\n    this.latestAgentCacheEntry =\n      finishedData?.cacheEntry !== undefined\n        ? (finishedData.cacheEntry as AgentCacheTransferPayload)\n        : null;\n    return result;\n  }\n\n  consumeLatestAgentCacheEntry(): AgentCacheTransferPayload | null {\n    const entry = this.latestAgentCacheEntry;\n    this.latestAgentCacheEntry = null;\n    return entry;\n  }\n\n  async end(): Promise<Response> {\n    const url = `/sessions/${this.sessionId}/end`;\n    const response = await this.request(url, {\n      method: \"POST\",\n    });\n    return response;\n  }\n\n  async getReplayMetrics(): Promise<StagehandMetrics> {\n    if (!this.sessionId) {\n      throw new StagehandAPIError(\"sessionId is required to fetch metrics.\");\n    }\n\n    const response = await this.request(`/sessions/${this.sessionId}/replay`, {\n      method: \"GET\",\n    });\n\n    if (response.status !== 200) {\n      const errorText = await response.text();\n      this.logger({\n        category: \"api\",\n        message: `Failed to fetch metrics. Status ${response.status}: ${errorText}`,\n        level: 0,\n      });\n      throw new StagehandHttpError(\n        `Failed to fetch metrics with status ${response.status}: ${errorText}`,\n      );\n    }\n\n    const data = (await response.json()) as\n      | Api.ReplayResponse\n      | { success: false; error?: string };\n\n    if (!data.success) {\n      const errorData = data as { success: false; error?: string };\n      throw new StagehandAPIError(\n        `Failed to fetch metrics: ${errorData.error || \"Unknown error\"}`,\n      );\n    }\n\n    // Parse the API data into StagehandMetrics format\n    const apiData = (data as Api.ReplayResponse).data;\n    const metrics: StagehandMetrics = {\n      actPromptTokens: 0,\n      actCompletionTokens: 0,\n      actReasoningTokens: 0,\n      actCachedInputTokens: 0,\n      actInferenceTimeMs: 0,\n      extractPromptTokens: 0,\n      extractCompletionTokens: 0,\n      extractReasoningTokens: 0,\n      extractCachedInputTokens: 0,\n      extractInferenceTimeMs: 0,\n      observePromptTokens: 0,\n      observeCompletionTokens: 0,\n      observeReasoningTokens: 0,\n      observeCachedInputTokens: 0,\n      observeInferenceTimeMs: 0,\n      agentPromptTokens: 0,\n      agentCompletionTokens: 0,\n      agentReasoningTokens: 0,\n      agentCachedInputTokens: 0,\n      agentInferenceTimeMs: 0,\n      totalPromptTokens: 0,\n      totalCompletionTokens: 0,\n      totalReasoningTokens: 0,\n      totalCachedInputTokens: 0,\n      totalInferenceTimeMs: 0,\n    };\n\n    // Parse pages and their actions\n    const pages = apiData?.pages || [];\n    for (const page of pages) {\n      const actions = page.actions || [];\n      for (const action of actions) {\n        // Get method name and token usage\n        const method = (action.method || \"\").toLowerCase();\n        const tokenUsage = action.tokenUsage;\n\n        if (tokenUsage) {\n          const inputTokens = tokenUsage.inputTokens || 0;\n          const outputTokens = tokenUsage.outputTokens || 0;\n          const reasoningTokens =\n            \"reasoningTokens\" in tokenUsage\n              ? Number(\n                  (tokenUsage as { reasoningTokens?: number })\n                    .reasoningTokens ?? 0,\n                )\n              : 0;\n          const cachedInputTokens =\n            \"cachedInputTokens\" in tokenUsage\n              ? Number(\n                  (tokenUsage as { cachedInputTokens?: number })\n                    .cachedInputTokens ?? 0,\n                )\n              : 0;\n          const timeMs = tokenUsage.timeMs || 0;\n\n          // Map method to metrics fields\n          if (method === \"act\") {\n            metrics.actPromptTokens += inputTokens;\n            metrics.actCompletionTokens += outputTokens;\n            metrics.actReasoningTokens += reasoningTokens;\n            metrics.actCachedInputTokens += cachedInputTokens;\n            metrics.actInferenceTimeMs += timeMs;\n          } else if (method === \"extract\") {\n            metrics.extractPromptTokens += inputTokens;\n            metrics.extractCompletionTokens += outputTokens;\n            metrics.extractReasoningTokens += reasoningTokens;\n            metrics.extractCachedInputTokens += cachedInputTokens;\n            metrics.extractInferenceTimeMs += timeMs;\n          } else if (method === \"observe\") {\n            metrics.observePromptTokens += inputTokens;\n            metrics.observeCompletionTokens += outputTokens;\n            metrics.observeReasoningTokens += reasoningTokens;\n            metrics.observeCachedInputTokens += cachedInputTokens;\n            metrics.observeInferenceTimeMs += timeMs;\n          } else if (method === \"agent\") {\n            metrics.agentPromptTokens += inputTokens;\n            metrics.agentCompletionTokens += outputTokens;\n            metrics.agentReasoningTokens += reasoningTokens;\n            metrics.agentCachedInputTokens += cachedInputTokens;\n            metrics.agentInferenceTimeMs += timeMs;\n          }\n\n          // Always update totals for any method with token usage\n          metrics.totalPromptTokens += inputTokens;\n          metrics.totalCompletionTokens += outputTokens;\n          metrics.totalReasoningTokens += reasoningTokens;\n          metrics.totalCachedInputTokens += cachedInputTokens;\n          metrics.totalInferenceTimeMs += timeMs;\n        }\n      }\n    }\n\n    return metrics;\n  }\n\n  /**\n   * Prepares a model configuration for the API payload by ensuring the `apiKey`\n   * is included. If the model is passed as a string, converts it to an object\n   * with `modelName` and `apiKey`.\n   *\n   * In API mode, we only attempt to load an API key from env vars when the\n   * model provider differs from the one used to init the session.\n   */\n  private prepareModelConfig(\n    model: ModelConfiguration,\n  ): { modelName: string; apiKey: string } & Record<string, unknown> {\n    if (typeof model === \"string\") {\n      // Extract provider from model string (e.g., \"openai/gpt-5-nano\" -> \"openai\")\n      const provider = model.includes(\"/\") ? model.split(\"/\")[0] : undefined;\n      const apiKey =\n        provider && provider !== this.modelProvider\n          ? (loadApiKeyFromEnv(provider, this.logger) ?? this.modelApiKey)\n          : this.modelApiKey;\n      return {\n        modelName: model,\n        apiKey,\n      };\n    }\n\n    if (!model.apiKey) {\n      const provider = model.modelName?.includes(\"/\")\n        ? model.modelName.split(\"/\")[0]\n        : undefined;\n      const apiKey =\n        provider && provider !== this.modelProvider\n          ? (loadApiKeyFromEnv(provider, this.logger) ?? this.modelApiKey)\n          : this.modelApiKey;\n      return {\n        ...model,\n        apiKey,\n      };\n    }\n\n    return model as { modelName: string; apiKey: string } & Record<\n      string,\n      unknown\n    >;\n  }\n\n  private consumeFinishedEventData<T>(): T | null {\n    const data = this.lastFinishedEventData as T | null;\n    this.lastFinishedEventData = null;\n    return data;\n  }\n\n  private async execute<T>({\n    method,\n    args,\n    params,\n    serverCache,\n  }: ExecuteActionParams): Promise<T> {\n    this.lastFinishedEventData = null;\n    const urlParams = new URLSearchParams(params as Record<string, string>);\n    const queryString = urlParams.toString();\n    const url = `/sessions/${this.sessionId}/${method}${queryString ? `?${queryString}` : \"\"}`;\n\n    const response = await this.request(\n      url,\n      {\n        method: \"POST\",\n        body: JSON.stringify(args),\n      },\n      serverCache,\n    );\n\n    // Capture cache status from response header\n    const cacheStatus = response.headers.get(\"browserbase-cache-status\") as\n      | \"HIT\"\n      | \"MISS\"\n      | null;\n\n    if (!response.ok) {\n      const errorBody = await response.text();\n      throw new StagehandHttpError(\n        `HTTP error! status: ${response.status}, body: ${errorBody}`,\n      );\n    }\n\n    if (!response.body) {\n      throw new StagehandResponseBodyError();\n    }\n\n    const reader = response.body.getReader();\n    const decoder = new TextDecoder();\n    let buffer = \"\";\n\n    while (true) {\n      const { value, done } = await reader.read();\n\n      if (done && !buffer) {\n        throw new StagehandServerError(\n          \"Stream ended without completion signal\",\n        );\n      }\n\n      buffer += decoder.decode(value, { stream: true });\n      const lines = buffer.split(\"\\n\\n\");\n      buffer = lines.pop() || \"\";\n\n      for (const line of lines) {\n        if (!line.startsWith(\"data: \")) continue;\n\n        try {\n          const eventData = JSON.parse(line.slice(6));\n\n          if (eventData.type === \"system\") {\n            if (eventData.data.status === \"error\") {\n              const { error: errorMsg } = eventData.data;\n              // Throw plain Error to match local SDK behavior (useApi: false)\n              throw new Error(errorMsg);\n            }\n            if (eventData.data.status === \"finished\") {\n              this.lastFinishedEventData = eventData.data;\n\n              // If caching was bypassed for this request, suppress cache status\n              // so we don't log or surface a MISS that the server emits anyway.\n              const cacheEnabled = this.shouldUseCache(serverCache);\n              return this.attachCacheStatus(\n                eventData.data.result as T,\n                method,\n                cacheEnabled ? cacheStatus : null,\n                cacheEnabled ? eventData : { data: {} },\n              );\n            }\n          } else if (eventData.type === \"log\") {\n            const msg = eventData.data.message;\n            // Skip server-side internal logs that don't apply to API mode\n            if (msg?.message === \"Connecting to local browser\") {\n              continue;\n            }\n            this.logger(eventData.data.message);\n          }\n        } catch (e) {\n          // Let Error instances pass through (server errors thrown above)\n          // Only wrap SyntaxError from JSON.parse as parse errors\n          if (e instanceof Error && !(e instanceof SyntaxError)) {\n            throw e;\n          }\n\n          const errorMessage = e instanceof Error ? e.message : String(e);\n          this.logger({\n            category: \"api\",\n            message: `Failed to parse SSE event: ${errorMessage}`,\n            level: 0,\n          });\n          throw new StagehandResponseParseError(\n            `Failed to parse server response: ${errorMessage}`,\n          );\n        }\n      }\n\n      if (done) {\n        // Process any remaining data in buffer before exiting\n        if (buffer.trim() && buffer.startsWith(\"data: \")) {\n          try {\n            const eventData = JSON.parse(buffer.slice(6));\n            if (\n              eventData.type === \"system\" &&\n              eventData.data.status === \"finished\"\n            ) {\n              return this.attachCacheStatus(\n                eventData.data.result as T,\n                method,\n                cacheStatus,\n                eventData,\n              );\n            }\n          } catch {\n            this.logger({\n              category: \"api\",\n              message: `Incomplete data in final buffer: ${buffer.substring(0, 100)}`,\n              level: 0,\n            });\n          }\n        }\n        throw new StagehandServerError(\n          \"Stream ended without completion signal\",\n        );\n      }\n    }\n  }\n\n  /**\n   * Resolves the final cache status from the response header or SSE event data,\n   * logs it, and attaches it to act/extract results before returning.\n   */\n  private attachCacheStatus<T>(\n    result: T,\n    method: string,\n    cacheStatus: \"HIT\" | \"MISS\" | null,\n    eventData: { data: { cacheHit?: boolean } },\n  ): T {\n    const finalCacheStatus =\n      cacheStatus ||\n      (typeof eventData.data.cacheHit === \"boolean\"\n        ? eventData.data.cacheHit\n          ? \"HIT\"\n          : \"MISS\"\n        : undefined);\n    if (\n      finalCacheStatus &&\n      (method === \"act\" || method === \"extract\" || method === \"observe\")\n    ) {\n      this.logger({\n        category: \"cache\",\n        message: `${method} server cache ${finalCacheStatus.toLowerCase()}`,\n        level: 1,\n      });\n    }\n    if (\n      finalCacheStatus &&\n      result &&\n      typeof result === \"object\" &&\n      (method === \"act\" || method === \"extract\" || method === \"observe\")\n    ) {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      (result as ActResult | ExtractResult<any> | ObserveResult).cacheStatus =\n        finalCacheStatus;\n    }\n    return result;\n  }\n\n  /**\n   * Determine if caching should be enabled for a request.\n   * Method-level setting takes precedence over instance-level setting.\n   */\n  private shouldUseCache(methodServerCache?: boolean): boolean {\n    // If method-level setting is explicitly provided, use it\n    if (methodServerCache !== undefined) {\n      return methodServerCache;\n    }\n    // Otherwise, use instance-level setting\n    return this.serverCache;\n  }\n\n  private async request(\n    path: string,\n    options: RequestInit,\n    serverCache?: boolean,\n  ): Promise<Response> {\n    const defaultHeaders: Record<string, string> = {\n      \"x-bb-api-key\": this.apiKey,\n      ...(this.projectId ? { \"x-bb-project-id\": this.projectId } : {}),\n      \"x-bb-session-id\": this.sessionId,\n      // we want real-time logs, so we stream the response\n      \"x-stream-response\": \"true\",\n      \"x-model-api-key\": this.modelApiKey,\n      \"x-language\": \"typescript\",\n      \"x-sdk-version\": STAGEHAND_VERSION,\n    };\n\n    // Add cache bypass header if caching is disabled\n    if (!this.shouldUseCache(serverCache)) {\n      defaultHeaders[\"browserbase-cache-bypass\"] = \"true\";\n    }\n\n    if (options.method === \"POST\" && options.body) {\n      defaultHeaders[\"Content-Type\"] = \"application/json\";\n    }\n\n    // Use STAGEHAND_API_URL env var if set, otherwise use region-based URL\n    // Ensure /v1 suffix is present for consistency\n    let baseUrl: string;\n    if (process.env.STAGEHAND_API_URL) {\n      const envUrl = process.env.STAGEHAND_API_URL.replace(/\\/+$/, \"\");\n      // Append /v1 if not already present\n      baseUrl = envUrl.endsWith(\"/v1\") ? envUrl : `${envUrl}/v1`;\n    } else {\n      baseUrl = getApiUrlForRegion(this.region);\n    }\n\n    const response = await this.fetchWithCookies(`${baseUrl}${path}`, {\n      ...options,\n      headers: {\n        ...defaultHeaders,\n        ...options.headers,\n      },\n    });\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/cache/ActCache.ts",
    "content": "import { createHash } from \"crypto\";\nimport type { ActHandler } from \"../handlers/actHandler.js\";\nimport type { LLMClient } from \"../llm/LLMClient.js\";\nimport type { Action, ActResult, Logger } from \"../types/public/index.js\";\nimport type { Page } from \"../understudy/page.js\";\nimport { CacheStorage } from \"./CacheStorage.js\";\nimport { safeGetPageUrl, waitForCachedSelector } from \"./utils.js\";\nimport {\n  ActCacheContext,\n  ActCacheDeps,\n  CachedActEntry,\n} from \"../types/private/index.js\";\nimport { StagehandNotInitializedError } from \"../types/public/sdkErrors.js\";\nimport { withTimeout } from \"../timeoutConfig.js\";\n\nexport class ActCache {\n  private readonly storage: CacheStorage;\n  private readonly logger: Logger;\n  private readonly getActHandler: () => ActHandler | null;\n  private readonly getDefaultLlmClient: () => LLMClient;\n  private readonly domSettleTimeoutMs?: number;\n\n  constructor({\n    storage,\n    logger,\n    getActHandler,\n    getDefaultLlmClient,\n    domSettleTimeoutMs,\n  }: ActCacheDeps) {\n    this.storage = storage;\n    this.logger = logger;\n    this.getActHandler = getActHandler;\n    this.getDefaultLlmClient = getDefaultLlmClient;\n    this.domSettleTimeoutMs = domSettleTimeoutMs;\n  }\n\n  get enabled(): boolean {\n    return this.storage.enabled;\n  }\n\n  async prepareContext(\n    instruction: string,\n    page: Page,\n    variables?: Record<string, string>,\n  ): Promise<ActCacheContext | null> {\n    if (!this.enabled) return null;\n    const sanitizedInstruction = instruction.trim();\n    const sanitizedVariables = variables ? { ...variables } : undefined;\n    const variableKeys = sanitizedVariables\n      ? Object.keys(sanitizedVariables).sort()\n      : [];\n    const pageUrl = await safeGetPageUrl(page);\n    const cacheKey = this.buildActCacheKey(\n      sanitizedInstruction,\n      pageUrl,\n      variableKeys,\n    );\n    return {\n      instruction: sanitizedInstruction,\n      cacheKey,\n      pageUrl,\n      variableKeys,\n      variables: sanitizedVariables,\n    };\n  }\n\n  async tryReplay(\n    context: ActCacheContext,\n    page: Page,\n    timeout?: number,\n    llmClientOverride?: LLMClient,\n  ): Promise<ActResult | null> {\n    if (!this.enabled) return null;\n\n    const {\n      value: entry,\n      error,\n      path,\n    } = await this.storage.readJson<CachedActEntry>(`${context.cacheKey}.json`);\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: `failed to read act cache entry: ${path}`,\n        level: 2,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return null;\n    }\n    if (!entry) return null;\n    if (entry.version !== 1) return null;\n    if (!Array.isArray(entry.actions) || entry.actions.length === 0) {\n      return null;\n    }\n\n    const entryVariableKeys = Array.isArray(entry.variableKeys)\n      ? [...entry.variableKeys].sort()\n      : [];\n    const contextVariableKeys = [...context.variableKeys];\n\n    if (!this.doVariableKeysMatch(entryVariableKeys, contextVariableKeys)) {\n      return null;\n    }\n\n    if (\n      contextVariableKeys.length > 0 &&\n      (!context.variables ||\n        !this.hasAllVariableValues(contextVariableKeys, context.variables))\n    ) {\n      this.logger({\n        category: \"cache\",\n        message: \"act cache miss: missing variables for replay\",\n        level: 2,\n        auxiliary: {\n          instruction: { value: context.instruction, type: \"string\" },\n        },\n      });\n      return null;\n    }\n\n    this.logger({\n      category: \"cache\",\n      message: \"act cache hit\",\n      level: 1,\n      auxiliary: {\n        instruction: { value: context.instruction, type: \"string\" },\n        url: {\n          value: entry.url ?? context.pageUrl,\n          type: \"string\",\n        },\n      },\n    });\n\n    return await this.replayCachedActions(\n      context,\n      entry,\n      page,\n      timeout,\n      llmClientOverride,\n    );\n  }\n\n  async store(context: ActCacheContext, result: ActResult): Promise<void> {\n    if (!this.enabled) return;\n\n    const entry: CachedActEntry = {\n      version: 1,\n      instruction: context.instruction,\n      url: context.pageUrl,\n      variableKeys: context.variableKeys,\n      actions: result.actions ?? [],\n      actionDescription: result.actionDescription,\n      message: result.message,\n    };\n\n    const { error, path } = await this.storage.writeJson(\n      `${context.cacheKey}.json`,\n      entry,\n    );\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: \"failed to write act cache entry\",\n        level: 1,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return;\n    }\n\n    this.logger({\n      category: \"cache\",\n      message: \"act cache stored\",\n      level: 2,\n      auxiliary: {\n        instruction: { value: context.instruction, type: \"string\" },\n        url: { value: context.pageUrl, type: \"string\" },\n      },\n    });\n  }\n\n  private buildActCacheKey(\n    instruction: string,\n    url: string,\n    variableKeys: string[],\n  ): string {\n    const payload = JSON.stringify({\n      instruction,\n      url,\n      variableKeys,\n    });\n    return createHash(\"sha256\").update(payload).digest(\"hex\");\n  }\n\n  private async replayCachedActions(\n    context: ActCacheContext,\n    entry: CachedActEntry,\n    page: Page,\n    timeout?: number,\n    llmClientOverride?: LLMClient,\n  ): Promise<ActResult> {\n    const handler = this.getActHandler();\n    if (!handler) {\n      throw new StagehandNotInitializedError(\"act()\");\n    }\n    const effectiveClient = llmClientOverride ?? this.getDefaultLlmClient();\n\n    const execute = async (): Promise<ActResult> => {\n      const actionResults: ActResult[] = [];\n      for (const action of entry.actions) {\n        await waitForCachedSelector({\n          page,\n          selector: action.selector,\n          timeout: this.domSettleTimeoutMs,\n          logger: this.logger,\n          context: \"act\",\n        });\n        const result = await handler.takeDeterministicAction(\n          action,\n          page,\n          this.domSettleTimeoutMs,\n          effectiveClient,\n          undefined,\n          context.variables,\n        );\n        actionResults.push(result);\n        if (!result.success) {\n          break;\n        }\n      }\n\n      if (actionResults.length === 0) {\n        return {\n          success: false,\n          message: \"Failed to perform act: cached entry has no actions\",\n          actionDescription: entry.actionDescription ?? entry.instruction,\n          actions: [],\n        };\n      }\n\n      const success = actionResults.every((r) => r.success);\n      const actions = actionResults.flatMap((r) => r.actions ?? []);\n      const message =\n        actionResults\n          .map((r) => r.message)\n          .filter((m) => m && m.trim().length > 0)\n          .join(\" → \") ||\n        entry.message ||\n        `Replayed ${entry.actions.length} cached action${\n          entry.actions.length === 1 ? \"\" : \"s\"\n        }.`;\n      const actionDescription =\n        entry.actionDescription ||\n        actionResults[actionResults.length - 1]?.actionDescription ||\n        entry.actions[entry.actions.length - 1]?.description ||\n        entry.instruction;\n\n      if (\n        success &&\n        actions.length > 0 &&\n        this.haveActionsChanged(entry.actions, actions)\n      ) {\n        await this.refreshCacheEntry(context, {\n          ...entry,\n          actions,\n          message,\n          actionDescription,\n        });\n      }\n      return {\n        success,\n        message,\n        actionDescription,\n        actions,\n      };\n    };\n\n    return await withTimeout(execute(), timeout, \"act()\");\n  }\n\n  private haveActionsChanged(original: Action[], updated: Action[]): boolean {\n    if (original.length !== updated.length) {\n      return true;\n    }\n\n    for (let i = 0; i < original.length; i += 1) {\n      const orig = original[i];\n      const next = updated[i];\n      if (!next) {\n        return true;\n      }\n\n      if (orig.selector !== next.selector) {\n        return true;\n      }\n\n      if (orig.description !== next.description) {\n        return true;\n      }\n\n      if ((orig.method ?? \"\") !== (next.method ?? \"\")) {\n        return true;\n      }\n\n      const origArgs = orig.arguments ?? [];\n      const nextArgs = next.arguments ?? [];\n      if (origArgs.length !== nextArgs.length) {\n        return true;\n      }\n\n      for (let j = 0; j < origArgs.length; j += 1) {\n        if (origArgs[j] !== nextArgs[j]) {\n          return true;\n        }\n      }\n    }\n\n    return false;\n  }\n\n  private async refreshCacheEntry(\n    context: ActCacheContext,\n    entry: CachedActEntry,\n  ): Promise<void> {\n    const { error, path } = await this.storage.writeJson(\n      `${context.cacheKey}.json`,\n      {\n        ...entry,\n        variableKeys: context.variableKeys,\n      },\n    );\n\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: \"failed to update act cache entry after self-heal\",\n        level: 0,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return;\n    }\n\n    this.logger({\n      category: \"cache\",\n      message: \"act cache entry updated after self-heal\",\n      level: 2,\n      auxiliary: {\n        instruction: { value: context.instruction, type: \"string\" },\n        url: { value: context.pageUrl, type: \"string\" },\n      },\n    });\n  }\n\n  private doVariableKeysMatch(\n    entryKeys: string[],\n    contextKeys: string[],\n  ): boolean {\n    if (entryKeys.length !== contextKeys.length) {\n      return false;\n    }\n\n    for (let i = 0; i < entryKeys.length; i += 1) {\n      if (entryKeys[i] !== contextKeys[i]) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  private hasAllVariableValues(\n    variableKeys: string[],\n    variables: Record<string, string>,\n  ): boolean {\n    for (const key of variableKeys) {\n      if (!(key in variables)) {\n        return false;\n      }\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/cache/AgentCache.ts",
    "content": "import { createHash } from \"crypto\";\nimport type { ActHandler } from \"../handlers/actHandler.js\";\nimport type { LLMClient } from \"../llm/LLMClient.js\";\nimport type {\n  AgentReplayActStep,\n  AgentReplayFillFormStep,\n  AgentReplayGotoStep,\n  AgentReplayKeysStep,\n  AgentReplayNavBackStep,\n  AgentReplayScrollStep,\n  AgentReplayStep,\n  AgentReplayWaitStep,\n  CachedAgentEntry,\n  SanitizedAgentExecuteOptions,\n  ActFn,\n  AgentCacheContext,\n  AgentCacheDeps,\n  AgentCacheTransferPayload,\n} from \"../types/private/index.js\";\nimport type {\n  Action,\n  AgentResult,\n  AgentStreamResult,\n  AgentConfig,\n  AgentExecuteOptionsBase,\n  AvailableModel,\n  Logger,\n} from \"../types/public/index.js\";\nimport type { Page } from \"../understudy/page.js\";\nimport type { V3Context } from \"../understudy/context.js\";\nimport { CacheStorage } from \"./CacheStorage.js\";\nimport {\n  cloneForCache,\n  safeGetPageUrl,\n  waitForCachedSelector,\n} from \"./utils.js\";\n\nconst SENSITIVE_CONFIG_KEYS = new Set([\"apikey\", \"api_key\", \"api-key\"]);\n\nexport class AgentCache {\n  private readonly storage: CacheStorage;\n  private readonly logger: Logger;\n  private readonly getActHandler: () => ActHandler | null;\n  private readonly getContext: () => V3Context | null;\n  private readonly getDefaultLlmClient: () => LLMClient;\n  private readonly getBaseModelName: () => AvailableModel;\n  private readonly getSystemPrompt: () => string | undefined;\n  private readonly domSettleTimeoutMs?: number;\n  private readonly act: ActFn;\n  private readonly bufferLatestEntry: boolean;\n\n  private recording: AgentReplayStep[] | null = null;\n  private latestEntry: AgentCacheTransferPayload | null = null;\n\n  constructor({\n    storage,\n    logger,\n    getActHandler,\n    getContext,\n    getDefaultLlmClient,\n    getBaseModelName,\n    getSystemPrompt,\n    domSettleTimeoutMs,\n    act,\n    bufferLatestEntry,\n  }: AgentCacheDeps) {\n    this.storage = storage;\n    this.logger = logger;\n    this.getActHandler = getActHandler;\n    this.getContext = getContext;\n    this.getDefaultLlmClient = getDefaultLlmClient;\n    this.getBaseModelName = getBaseModelName;\n    this.getSystemPrompt = getSystemPrompt;\n    this.domSettleTimeoutMs = domSettleTimeoutMs;\n    this.act = act;\n    this.bufferLatestEntry = bufferLatestEntry ?? false;\n  }\n\n  get enabled(): boolean {\n    return this.storage.enabled;\n  }\n\n  shouldAttemptCache(instruction: string): boolean {\n    return this.enabled && instruction.trim().length > 0;\n  }\n\n  sanitizeExecuteOptions(\n    options?: AgentExecuteOptionsBase,\n  ): SanitizedAgentExecuteOptions {\n    if (!options) return {};\n    const sanitized: SanitizedAgentExecuteOptions = {};\n    if (typeof options.maxSteps === \"number\") {\n      sanitized.maxSteps = options.maxSteps;\n    }\n    if (\n      \"highlightCursor\" in options &&\n      typeof (options as { highlightCursor?: unknown }).highlightCursor ===\n        \"boolean\"\n    ) {\n      sanitized.highlightCursor = (\n        options as { highlightCursor?: boolean }\n      ).highlightCursor;\n    }\n    return sanitized;\n  }\n\n  buildConfigSignature(agentOptions?: AgentConfig): string {\n    const toolKeys = agentOptions?.tools\n      ? Object.keys(agentOptions.tools).sort()\n      : undefined;\n    const integrationSignatures = agentOptions?.integrations\n      ? agentOptions.integrations.map((integration) =>\n          typeof integration === \"string\" ? integration : \"client\",\n        )\n      : undefined;\n    const serializedModel = this.serializeAgentModelForCache(\n      agentOptions?.model,\n    );\n    const serializedExecutionModel = this.serializeAgentModelForCache(\n      agentOptions?.executionModel,\n    );\n\n    const isCuaMode =\n      agentOptions?.mode !== undefined\n        ? agentOptions.mode === \"cua\"\n        : agentOptions?.cua === true;\n\n    return JSON.stringify({\n      v3Model: this.getBaseModelName(),\n      systemPrompt: this.getSystemPrompt() ?? \"\",\n      agent: {\n        cua: isCuaMode,\n        model: serializedModel ?? null,\n        executionModel: isCuaMode ? null : serializedExecutionModel,\n        systemPrompt: agentOptions?.systemPrompt ?? null,\n        toolKeys,\n        integrations: integrationSignatures,\n      },\n    });\n  }\n\n  async prepareContext(params: {\n    instruction: string;\n    options: SanitizedAgentExecuteOptions;\n    configSignature: string;\n    page: Page;\n    variables?: Record<string, string>;\n  }): Promise<AgentCacheContext | null> {\n    if (!this.shouldAttemptCache(params.instruction)) {\n      return null;\n    }\n    const instruction = params.instruction.trim();\n    const startUrl = await safeGetPageUrl(params.page);\n    const variableKeys = params.variables\n      ? Object.keys(params.variables).sort()\n      : [];\n    const cacheKey = this.buildAgentCacheKey(\n      instruction,\n      startUrl,\n      params.options,\n      params.configSignature,\n      variableKeys,\n    );\n    return {\n      instruction,\n      startUrl,\n      options: params.options,\n      configSignature: params.configSignature,\n      cacheKey,\n      variableKeys,\n      variables: params.variables,\n    };\n  }\n\n  async tryReplay(\n    context: AgentCacheContext,\n    llmClientOverride?: LLMClient,\n  ): Promise<AgentResult | null> {\n    if (!this.enabled) return null;\n\n    const {\n      value: entry,\n      error,\n      path,\n    } = await this.storage.readJson<CachedAgentEntry>(\n      `agent-${context.cacheKey}.json`,\n    );\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: `failed to read agent cache entry: ${path}`,\n        level: 1,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return null;\n    }\n    if (!entry || entry.version !== 1) {\n      return null;\n    }\n\n    this.logger({\n      category: \"cache\",\n      message: \"agent cache hit\",\n      level: 1,\n      auxiliary: {\n        instruction: { value: context.instruction, type: \"string\" },\n        url: { value: context.startUrl, type: \"string\" },\n      },\n    });\n\n    return await this.replayAgentCacheEntry(context, entry, llmClientOverride);\n  }\n\n  /**\n   * Attempts to replay a cached agent execution and returns it as a stream result.\n   *\n   * This method exists because the agent API exposes two execution modes:\n   * - `execute()` - Returns a Promise<AgentResult> directly\n   * - `stream()` - Returns an AgentStreamResult with async iterables for real-time output\n   *\n   * When a cache hit occurs, we need to return the appropriate type for each mode:\n   * - For `execute()`, we use `tryReplay()` which returns AgentResult\n   * - For `stream()`, we use `tryReplayAsStream()` which wraps the result in a\n   *   stream-compatible interface\n   *\n   * This ensures consumers using `stream()` can still iterate over `textStream`\n   * and await `result` even when the response comes from cache, maintaining\n   * API consistency regardless of whether the result was cached or live.\n   */\n  async tryReplayAsStream(\n    context: AgentCacheContext,\n    llmClientOverride?: LLMClient,\n  ): Promise<AgentStreamResult | null> {\n    const result = await this.tryReplay(context, llmClientOverride);\n    if (!result) return null;\n    return this.createCachedStreamResult(result);\n  }\n\n  /**\n   * Creates a mock AgentStreamResult that wraps a cached AgentResult.\n   *\n   * AgentStreamResult (from the AI SDK) is a complex type with multiple async\n   * iterables and promises. When serving from cache, we don't have an actual\n   * LLM stream to consume - we just have the final result. This method creates\n   * a \"fake\" stream\n\n   * This approach lets cached responses be transparent to the consumer -\n   * they can use the same iteration patterns whether the result is live or cached.\n   */\n  private createCachedStreamResult(\n    cachedResult: AgentResult,\n  ): AgentStreamResult {\n    const message = cachedResult.message ?? \"\";\n\n    async function* textStreamGenerator(): AsyncGenerator<string> {\n      yield message;\n    }\n\n    async function* fullStreamGenerator(): AsyncGenerator<{\n      type: string;\n      textDelta?: string;\n    }> {\n      yield { type: \"text-delta\", textDelta: message };\n      yield { type: \"finish\" };\n    }\n\n    const mockStreamResult = {\n      textStream: textStreamGenerator(),\n      fullStream: fullStreamGenerator(),\n      result: Promise.resolve(cachedResult),\n      text: Promise.resolve(message),\n      usage: Promise.resolve({\n        promptTokens: 0,\n        completionTokens: 0,\n        totalTokens: 0,\n      }),\n      finishReason: Promise.resolve(\"stop\" as const),\n      experimental_providerMetadata: Promise.resolve(undefined),\n      response: Promise.resolve({\n        id: \"cached\",\n        timestamp: new Date(),\n        modelId: \"cached\",\n      }),\n      rawResponse: Promise.resolve({ headers: {} }),\n      warnings: Promise.resolve([]),\n      steps: Promise.resolve([]),\n      toolCalls: Promise.resolve([]),\n      toolResults: Promise.resolve([]),\n      [Symbol.asyncIterator]: () => textStreamGenerator(),\n    } as unknown as AgentStreamResult;\n\n    return mockStreamResult;\n  }\n\n  /**\n   * Wraps an AgentStreamResult with caching logic.\n   *\n   * This method handles the complexity of caching for streaming responses:\n   * 1. Begins recording agent replay steps\n   * 2. Wraps the stream's result promise to capture completion\n   * 3. On success: ends recording and stores the cache entry\n   * 4. On error: discards the recording\n   *\n   * This keeps the caching orchestration in AgentCache rather than\n   * spreading it across the V3 class.\n   *\n   * @param context - The cache context for this execution\n   * @param streamResult - The stream result from the agent handler\n   * @param beginRecording - Callback to start recording (from V3)\n   * @param endRecording - Callback to end recording and get steps (from V3)\n   * @param discardRecording - Callback to discard recording on error (from V3)\n   * @returns The wrapped stream result with caching enabled\n   */\n  wrapStreamForCaching(\n    context: AgentCacheContext,\n    streamResult: AgentStreamResult,\n    beginRecording: () => void,\n    endRecording: () => AgentReplayStep[],\n    discardRecording: () => void,\n  ): AgentStreamResult {\n    beginRecording();\n\n    const originalResultPromise = streamResult.result;\n    const wrappedResultPromise = originalResultPromise.then(\n      async (result) => {\n        const agentSteps = endRecording();\n\n        if (result.success && agentSteps.length > 0) {\n          await this.store(context, agentSteps, result);\n        }\n\n        return result;\n      },\n      (error) => {\n        discardRecording();\n        throw error;\n      },\n    );\n\n    streamResult.result = wrappedResultPromise;\n    return streamResult;\n  }\n\n  async store(\n    context: AgentCacheContext,\n    steps: AgentReplayStep[],\n    result: AgentResult,\n  ): Promise<void> {\n    if (!this.enabled) return;\n\n    const entry: CachedAgentEntry = {\n      version: 1,\n      instruction: context.instruction,\n      startUrl: context.startUrl,\n      options: context.options,\n      configSignature: context.configSignature,\n      steps: cloneForCache(steps),\n      result: this.pruneAgentResult(result),\n      timestamp: new Date().toISOString(),\n    };\n\n    const { error, path } = await this.storage.writeJson(\n      `agent-${context.cacheKey}.json`,\n      entry,\n    );\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: \"failed to write agent cache entry\",\n        level: 1,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return;\n    }\n\n    this.logger({\n      category: \"cache\",\n      message: \"agent cache stored\",\n      level: 2,\n      auxiliary: {\n        instruction: { value: context.instruction, type: \"string\" },\n        steps: { value: String(steps.length), type: \"string\" },\n      },\n    });\n\n    if (this.bufferLatestEntry) {\n      this.latestEntry = {\n        cacheKey: context.cacheKey,\n        entry: cloneForCache(entry),\n      };\n    }\n  }\n\n  consumeBufferedEntry(): AgentCacheTransferPayload | null {\n    if (!this.bufferLatestEntry || !this.latestEntry) {\n      return null;\n    }\n\n    const payload = this.latestEntry;\n    this.latestEntry = null;\n    return payload;\n  }\n\n  async storeTransferredEntry(\n    payload: AgentCacheTransferPayload | null,\n  ): Promise<void> {\n    if (!this.enabled || !payload) return;\n\n    const entry = cloneForCache(payload.entry);\n    const { error, path } = await this.storage.writeJson(\n      `agent-${payload.cacheKey}.json`,\n      entry,\n    );\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: \"failed to import remote agent cache entry\",\n        level: 0,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return;\n    }\n\n    this.logger({\n      category: \"cache\",\n      message: \"agent cache imported from server\",\n      level: 2,\n      auxiliary: {\n        instruction: { value: entry.instruction, type: \"string\" },\n        steps: { value: String(entry.steps?.length ?? 0), type: \"string\" },\n      },\n    });\n  }\n\n  /**\n   * Clone the agent result and prune bulky fields (e.g. screenshot base64 blobs)\n   * before persisting it to disk. This keeps cache entries compact without\n   * mutating the live AgentResult returned to callers.\n   */\n  private pruneAgentResult(result: AgentResult): AgentResult {\n    const cloned = cloneForCache(result);\n    if (!Array.isArray(cloned.actions)) {\n      return cloned;\n    }\n\n    for (const action of cloned.actions) {\n      if (action?.type === \"screenshot\") {\n        delete action.base64;\n      }\n    }\n\n    return cloned;\n  }\n\n  beginRecording(): void {\n    this.recording = [];\n  }\n\n  endRecording(): AgentReplayStep[] {\n    if (!this.recording) return [];\n    const steps = cloneForCache(this.recording);\n    this.recording = null;\n    return steps;\n  }\n\n  discardRecording(): void {\n    this.recording = null;\n  }\n\n  isRecording(): boolean {\n    return Array.isArray(this.recording);\n  }\n\n  recordStep(step: AgentReplayStep): void {\n    if (!this.isRecording()) return;\n    try {\n      this.recording!.push(cloneForCache(step));\n    } catch (err) {\n      this.logger({\n        category: \"cache\",\n        message: \"failed to record agent replay step\",\n        level: 2,\n        auxiliary: {\n          error: { value: String(err), type: \"string\" },\n        },\n      });\n    }\n  }\n\n  isReplayActive(): boolean {\n    return this.isRecording();\n  }\n\n  private serializeAgentModelForCache(\n    model?: AgentConfig[\"model\"],\n  ): null | string | { modelName: string; options?: Record<string, unknown> } {\n    if (!model) return null;\n    if (typeof model === \"string\") return model;\n\n    const { modelName, ...modelOptions } = model;\n    const sanitizedOptions =\n      Object.keys(modelOptions).length > 0\n        ? this.sanitizeModelOptionsForCache(\n            modelOptions as Record<string, unknown>,\n          )\n        : undefined;\n    return sanitizedOptions\n      ? { modelName, options: sanitizedOptions }\n      : modelName;\n  }\n\n  private buildAgentCacheKey(\n    instruction: string,\n    startUrl: string,\n    options: SanitizedAgentExecuteOptions,\n    configSignature: string,\n    variableKeys?: string[],\n  ): string {\n    const payload = {\n      instruction,\n      startUrl,\n      options,\n      configSignature,\n      variableKeys: variableKeys ?? [],\n    };\n    return createHash(\"sha256\").update(JSON.stringify(payload)).digest(\"hex\");\n  }\n\n  private sanitizeModelOptionsForCache(\n    value: Record<string, unknown>,\n  ): Record<string, unknown> | undefined {\n    const sanitizedEntries: Record<string, unknown> = {};\n    for (const [key, rawValue] of Object.entries(value)) {\n      if (SENSITIVE_CONFIG_KEYS.has(key.toLowerCase())) {\n        continue;\n      }\n\n      const sanitizedValue = this.sanitizeModelValueForCache(rawValue);\n      if (sanitizedValue !== undefined) {\n        sanitizedEntries[key] = sanitizedValue;\n      }\n    }\n\n    return Object.keys(sanitizedEntries).length > 0\n      ? sanitizedEntries\n      : undefined;\n  }\n\n  private sanitizeModelValueForCache(value: unknown): unknown {\n    if (Array.isArray(value)) {\n      const sanitizedArray = value\n        .map((item) => this.sanitizeModelValueForCache(item))\n        .filter((item) => item !== undefined);\n      return sanitizedArray;\n    }\n\n    if (value && typeof value === \"object\") {\n      return this.sanitizeModelOptionsForCache(\n        value as Record<string, unknown>,\n      );\n    }\n\n    return value;\n  }\n\n  private async replayAgentCacheEntry(\n    context: AgentCacheContext,\n    entry: CachedAgentEntry,\n    llmClientOverride?: LLMClient,\n  ): Promise<AgentResult | null> {\n    const ctx = this.getContext();\n    const handler = this.getActHandler();\n    if (!ctx || !handler) return null;\n    const effectiveClient = llmClientOverride ?? this.getDefaultLlmClient();\n    try {\n      const updatedSteps: AgentReplayStep[] = [];\n      let stepsChanged = false;\n      for (const step of entry.steps ?? []) {\n        const replayedStep =\n          (await this.executeAgentReplayStep(\n            step,\n            ctx,\n            handler,\n            effectiveClient,\n            context.variables,\n          )) ?? step;\n        stepsChanged ||= replayedStep !== step;\n        updatedSteps.push(replayedStep);\n      }\n      const result = cloneForCache(entry.result);\n      result.usage = {\n        input_tokens: 0,\n        output_tokens: 0,\n        reasoning_tokens: 0,\n        cached_input_tokens: 0,\n        inference_time_ms: 0,\n      };\n      result.metadata = {\n        ...(result.metadata ?? {}),\n        cacheHit: true,\n        cacheTimestamp: entry.timestamp,\n      };\n      if (stepsChanged) {\n        await this.refreshAgentCacheEntry(context, entry, updatedSteps);\n      }\n      return result;\n    } catch (err) {\n      this.logger({\n        category: \"cache\",\n        message: \"agent cache replay failed\",\n        level: 1,\n        auxiliary: {\n          error: { value: String(err), type: \"string\" },\n        },\n      });\n      return null;\n    }\n  }\n\n  private async executeAgentReplayStep(\n    step: AgentReplayStep,\n    ctx: V3Context,\n    handler: ActHandler,\n    llmClient: LLMClient,\n    variables?: Record<string, string>,\n  ): Promise<AgentReplayStep> {\n    switch (step.type) {\n      case \"act\":\n        return await this.replayAgentActStep(\n          step as AgentReplayActStep,\n          ctx,\n          handler,\n          llmClient,\n          variables,\n        );\n      case \"fillForm\":\n        return await this.replayAgentFillFormStep(\n          step as AgentReplayFillFormStep,\n          ctx,\n          handler,\n          llmClient,\n          variables,\n        );\n      case \"goto\":\n        await this.replayAgentGotoStep(step as AgentReplayGotoStep, ctx);\n        return step;\n      case \"scroll\":\n        await this.replayAgentScrollStep(step as AgentReplayScrollStep, ctx);\n        return step;\n      case \"wait\":\n        await this.replayAgentWaitStep(step as AgentReplayWaitStep);\n        return step;\n      case \"navback\":\n        await this.replayAgentNavBackStep(step as AgentReplayNavBackStep, ctx);\n        return step;\n      case \"keys\":\n        await this.replayAgentKeysStep(step as AgentReplayKeysStep, ctx);\n        return step;\n      case \"done\":\n      case \"extract\":\n      case \"screenshot\":\n      case \"ariaTree\":\n        return step;\n      default:\n        this.logger({\n          category: \"cache\",\n          message: `agent cache skipping step type: ${step.type}`,\n          level: 2,\n        });\n        return step;\n    }\n  }\n\n  private async replayAgentActStep(\n    step: AgentReplayActStep,\n    ctx: V3Context,\n    handler: ActHandler,\n    llmClient: LLMClient,\n    variables?: Record<string, string>,\n  ): Promise<AgentReplayActStep> {\n    const actions = Array.isArray(step.actions) ? step.actions : [];\n    if (actions.length > 0) {\n      const page = await ctx.awaitActivePage();\n      const updatedActions: Action[] = [];\n      for (const action of actions) {\n        await waitForCachedSelector({\n          page,\n          selector: action.selector,\n          timeout: this.domSettleTimeoutMs,\n          logger: this.logger,\n          context: \"agent act\",\n        });\n        const result = await handler.takeDeterministicAction(\n          action,\n          page,\n          this.domSettleTimeoutMs,\n          llmClient,\n          undefined,\n          variables,\n        );\n        if (result.success && Array.isArray(result.actions)) {\n          updatedActions.push(...cloneForCache(result.actions));\n        } else {\n          updatedActions.push(cloneForCache(action));\n        }\n      }\n      if (this.haveActionsChanged(actions, updatedActions)) {\n        return { ...step, actions: updatedActions };\n      }\n      return step;\n    }\n    await this.act(step.instruction, { timeout: step.timeout, variables });\n    return step;\n  }\n\n  private async replayAgentFillFormStep(\n    step: AgentReplayFillFormStep,\n    ctx: V3Context,\n    handler: ActHandler,\n    llmClient: LLMClient,\n    variables?: Record<string, string>,\n  ): Promise<AgentReplayFillFormStep> {\n    const actions =\n      Array.isArray(step.actions) && step.actions.length > 0\n        ? step.actions\n        : (step.observeResults ?? []);\n    if (!Array.isArray(actions) || actions.length === 0) {\n      return step;\n    }\n    const page = await ctx.awaitActivePage();\n    const updatedActions: Action[] = [];\n    for (const action of actions) {\n      await waitForCachedSelector({\n        page,\n        selector: action.selector,\n        timeout: this.domSettleTimeoutMs,\n        logger: this.logger,\n        context: \"fillForm\",\n      });\n      const result = await handler.takeDeterministicAction(\n        action,\n        page,\n        this.domSettleTimeoutMs,\n        llmClient,\n        undefined, // ensureTimeRemaining is not used in this context\n        variables,\n      );\n      if (result.success && Array.isArray(result.actions)) {\n        updatedActions.push(...cloneForCache(result.actions));\n      } else {\n        updatedActions.push(cloneForCache(action));\n      }\n    }\n    if (this.haveActionsChanged(actions, updatedActions)) {\n      return { ...step, actions: updatedActions };\n    }\n    return step;\n  }\n\n  private async replayAgentGotoStep(\n    step: AgentReplayGotoStep,\n    ctx: V3Context,\n  ): Promise<void> {\n    const page = await ctx.awaitActivePage();\n    await page.goto(step.url, { waitUntil: step.waitUntil ?? \"load\" });\n  }\n\n  private async replayAgentScrollStep(\n    step: AgentReplayScrollStep,\n    ctx: V3Context,\n  ): Promise<void> {\n    const page = await ctx.awaitActivePage();\n    let anchor = step.anchor;\n    if (!anchor) {\n      anchor = await page\n        .mainFrame()\n        .evaluate<{ x: number; y: number }>(() => ({\n          x: Math.max(0, Math.floor(window.innerWidth / 2)),\n          y: Math.max(0, Math.floor(window.innerHeight / 2)),\n        }));\n    }\n    const deltaX = step.deltaX ?? 0;\n    const deltaY = step.deltaY ?? 0;\n    await page.scroll(\n      Math.round(anchor.x ?? 0),\n      Math.round(anchor.y ?? 0),\n      deltaX,\n      deltaY,\n    );\n  }\n\n  private async replayAgentWaitStep(step: AgentReplayWaitStep): Promise<void> {\n    if (!step.timeMs || step.timeMs <= 0) return;\n    await new Promise((resolve) => setTimeout(resolve, step.timeMs));\n  }\n\n  private async replayAgentNavBackStep(\n    step: AgentReplayNavBackStep,\n    ctx: V3Context,\n  ): Promise<void> {\n    const page = await ctx.awaitActivePage();\n    await page.goBack({ waitUntil: step.waitUntil ?? \"domcontentloaded\" });\n  }\n\n  private async replayAgentKeysStep(\n    step: AgentReplayKeysStep,\n    ctx: V3Context,\n  ): Promise<void> {\n    const page = await ctx.awaitActivePage();\n    const { method, text, keys, times } = step.playwrightArguments;\n    const repeatCount = Math.max(1, times ?? 1);\n\n    if (method === \"type\" && text) {\n      for (let i = 0; i < repeatCount; i++) {\n        await page.type(text, { delay: 100 });\n      }\n    } else if (method === \"press\" && keys) {\n      for (let i = 0; i < repeatCount; i++) {\n        await page.keyPress(keys, { delay: 100 });\n      }\n    }\n  }\n\n  private haveActionsChanged(original: Action[], updated: Action[]): boolean {\n    if (original.length !== updated.length) {\n      return true;\n    }\n    for (let i = 0; i < original.length; i += 1) {\n      const orig = original[i];\n      const next = updated[i];\n      if (!orig || !next) {\n        return true;\n      }\n      if (orig.selector !== next.selector) {\n        return true;\n      }\n      if ((orig.description ?? \"\") !== (next.description ?? \"\")) {\n        return true;\n      }\n      if ((orig.method ?? \"\") !== (next.method ?? \"\")) {\n        return true;\n      }\n      const origArgs = Array.isArray(orig.arguments) ? orig.arguments : [];\n      const nextArgs = Array.isArray(next.arguments) ? next.arguments : [];\n      if (origArgs.length !== nextArgs.length) {\n        return true;\n      }\n      for (let j = 0; j < origArgs.length; j += 1) {\n        if (origArgs[j] !== nextArgs[j]) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  private async refreshAgentCacheEntry(\n    context: AgentCacheContext,\n    entry: CachedAgentEntry,\n    updatedSteps: AgentReplayStep[],\n  ): Promise<void> {\n    const updatedEntry: CachedAgentEntry = {\n      ...entry,\n      steps: cloneForCache(updatedSteps),\n      timestamp: new Date().toISOString(),\n    };\n    const { error, path } = await this.storage.writeJson(\n      `agent-${context.cacheKey}.json`,\n      updatedEntry,\n    );\n    if (error && path) {\n      this.logger({\n        category: \"cache\",\n        message: \"failed to update agent cache entry after self-heal\",\n        level: 0,\n        auxiliary: {\n          error: { value: String(error), type: \"string\" },\n        },\n      });\n      return;\n    }\n    this.logger({\n      category: \"cache\",\n      message: \"agent cache entry updated after self-heal\",\n      level: 2,\n      auxiliary: {\n        instruction: { value: context.instruction, type: \"string\" },\n        steps: { value: String(updatedSteps.length), type: \"string\" },\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/cache/CacheStorage.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\nimport type { Logger } from \"../types/public/index.js\";\nimport { ReadJsonResult, WriteJsonResult } from \"../types/private/index.js\";\n\nconst jsonClone = <T>(value: T): T => {\n  const serialized = JSON.stringify(value);\n  if (serialized === undefined) {\n    return value;\n  }\n  return JSON.parse(serialized) as T;\n};\n\nexport class CacheStorage {\n  private constructor(\n    private readonly logger: Logger,\n    private readonly dir?: string,\n    private readonly memoryStore?: Map<string, unknown>,\n  ) {}\n\n  static create(\n    cacheDir: string | undefined,\n    logger: Logger,\n    options?: { label?: string },\n  ): CacheStorage {\n    if (!cacheDir) {\n      return new CacheStorage(logger);\n    }\n\n    const resolved = path.resolve(cacheDir);\n    try {\n      fs.mkdirSync(resolved, { recursive: true });\n      return new CacheStorage(logger, resolved);\n    } catch (err) {\n      const label = options?.label ?? \"cache directory\";\n      logger({\n        category: \"cache\",\n        message: `unable to initialize ${label}: ${resolved}`,\n        level: 1,\n        auxiliary: {\n          error: { value: String(err), type: \"string\" },\n        },\n      });\n      return new CacheStorage(logger);\n    }\n  }\n\n  static createMemory(logger: Logger): CacheStorage {\n    return new CacheStorage(logger, undefined, new Map());\n  }\n\n  get directory(): string | undefined {\n    return this.dir;\n  }\n\n  get enabled(): boolean {\n    return !!this.dir || !!this.memoryStore;\n  }\n\n  private resolvePath(fileName: string): string | null {\n    if (!this.dir) return null;\n    return path.join(this.dir, fileName);\n  }\n\n  async readJson<T>(fileName: string): Promise<ReadJsonResult<T>> {\n    if (this.memoryStore) {\n      if (!this.memoryStore.has(fileName)) {\n        return { value: null };\n      }\n      const existing = this.memoryStore.get(fileName) as T;\n      return { value: jsonClone(existing) };\n    }\n\n    const filePath = this.resolvePath(fileName);\n    if (!filePath) {\n      return { value: null };\n    }\n\n    try {\n      const raw = await fs.promises.readFile(filePath, \"utf8\");\n      return { value: JSON.parse(raw) as T };\n    } catch (err) {\n      const code = (err as NodeJS.ErrnoException)?.code;\n      if (code === \"ENOENT\") {\n        return { value: null };\n      }\n      return { value: null, error: err, path: filePath };\n    }\n  }\n\n  async writeJson(fileName: string, data: unknown): Promise<WriteJsonResult> {\n    if (this.memoryStore) {\n      this.memoryStore.set(fileName, jsonClone(data));\n      return {};\n    }\n\n    const filePath = this.resolvePath(fileName);\n    if (!filePath) {\n      return {};\n    }\n\n    try {\n      await fs.promises.mkdir(path.dirname(filePath), { recursive: true });\n      await fs.promises.writeFile(\n        filePath,\n        JSON.stringify(data, null, 2),\n        \"utf8\",\n      );\n      return {};\n    } catch (err) {\n      return { error: err, path: filePath };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/cache/serverAgentCache.ts",
    "content": "import { AgentCache } from \"./AgentCache.js\";\nimport { CacheStorage } from \"./CacheStorage.js\";\nimport type { V3 } from \"../v3.js\";\nimport type { AgentCacheTransferPayload } from \"../types/private/index.js\";\nimport type { ActHandler } from \"../handlers/actHandler.js\";\nimport type { V3Context } from \"../understudy/context.js\";\nimport type { AvailableModel, V3Options } from \"../types/public/index.js\";\nimport type { ModelConfiguration } from \"../types/public/model.js\";\nimport type { LLMClient } from \"../llm/LLMClient.js\";\n\nexport interface ServerAgentCacheHandle {\n  complete(): AgentCacheTransferPayload | null;\n  discard(): void;\n}\n\n// TODO (refactor-caching): this reflective access is a known temporary escape hatch.\n// Once the caching internals are reworked, replace it with proper V3 helpers so\n// we stop poking private fields from the outside.\nfunction getInternalField<T>(instance: V3, key: string): T {\n  return (instance as unknown as Record<string, unknown>)[key] as T;\n}\n\nfunction setInternalField(instance: V3, key: string, value: unknown): void {\n  (instance as unknown as Record<string, unknown>)[key] = value;\n}\n\nfunction createMemoryAgentCache(stagehand: V3): AgentCache {\n  const resolveLlmClient = getInternalField<\n    (model?: ModelConfiguration) => LLMClient\n  >(stagehand, \"resolveLlmClient\");\n\n  return new AgentCache({\n    storage: CacheStorage.createMemory(stagehand.logger),\n    logger: stagehand.logger,\n    getActHandler: () =>\n      getInternalField<ActHandler | null>(stagehand, \"actHandler\"),\n    getContext: () => getInternalField<V3Context | null>(stagehand, \"ctx\"),\n    getDefaultLlmClient: () => resolveLlmClient.call(stagehand),\n    getBaseModelName: () =>\n      getInternalField<AvailableModel>(stagehand, \"modelName\"),\n    getSystemPrompt: () =>\n      getInternalField<V3Options>(stagehand, \"opts\").systemPrompt,\n    domSettleTimeoutMs: getInternalField<number | undefined>(\n      stagehand,\n      \"domSettleTimeoutMs\",\n    ),\n    act: stagehand.act.bind(stagehand),\n    bufferLatestEntry: true,\n  });\n}\n\nexport function __internalCreateInMemoryAgentCacheHandle(\n  stagehand: V3,\n): ServerAgentCacheHandle {\n  const originalCache = getInternalField<AgentCache>(stagehand, \"agentCache\");\n  const memoryCache = createMemoryAgentCache(stagehand);\n\n  setInternalField(stagehand, \"agentCache\", memoryCache);\n  let restored = false;\n  const restore = () => {\n    if (!restored) {\n      setInternalField(stagehand, \"agentCache\", originalCache);\n      restored = true;\n    }\n  };\n\n  return {\n    complete: () => {\n      const entry = memoryCache.consumeBufferedEntry();\n      restore();\n      return entry;\n    },\n    discard: () => {\n      restore();\n    },\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/cache/utils.ts",
    "content": "import type { Logger } from \"../types/public/index.js\";\nimport { Page } from \"../understudy/page.js\";\n\nconst DEFAULT_WAIT_TIMEOUT_MS = 15000;\n\nexport function cloneForCache<T>(value: T): T {\n  return JSON.parse(JSON.stringify(value)) as T;\n}\n\nexport async function safeGetPageUrl(page: Page): Promise<string> {\n  try {\n    return page.url();\n  } catch {\n    return \"\";\n  }\n}\n\n/**\n * Waits for a cached action's selector to be attached to the DOM before executing.\n * Logs a warning and proceeds if the wait times out (non-blocking).\n */\nexport async function waitForCachedSelector(params: {\n  page: Page;\n  selector: string | undefined;\n  timeout: number | undefined;\n  logger: Logger;\n  context?: string;\n}): Promise<void> {\n  const { page, selector, timeout, logger, context } = params;\n  if (!selector) return;\n\n  try {\n    await page.waitForSelector(selector, {\n      state: \"attached\",\n      timeout: timeout ?? DEFAULT_WAIT_TIMEOUT_MS,\n    });\n  } catch (err) {\n    logger({\n      category: \"cache\",\n      message: `waitForSelector failed for ${context ?? \"cached\"} action selector, proceeding anyway`,\n      level: 2,\n      auxiliary: {\n        selector: { value: selector, type: \"string\" },\n        error: { value: String(err), type: \"string\" },\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/cli.js",
    "content": "#!/usr/bin/env node\n\nimport process from \"node:process\";\nimport { maybeRunShutdownSupervisorFromArgv } from \"./shutdown/supervisor.js\";\n\n// currently the CLI is only used to spawn the shutdown supervisor\n// in the future, we may want to add more CLI commands here\nif (!maybeRunShutdownSupervisorFromArgv(process.argv.slice(2))) {\n  console.error(\n    \"Unsupported stagehand CLI invocation. Expected --supervisor with valid args.\",\n  );\n  process.exit(1);\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/a11yScripts/index.ts",
    "content": "export function getScrollOffsets(): { sx: number; sy: number } {\n  try {\n    const sx =\n      window.scrollX ??\n      window.pageXOffset ??\n      document.documentElement?.scrollLeft ??\n      0;\n    const sy =\n      window.scrollY ??\n      window.pageYOffset ??\n      document.documentElement?.scrollTop ??\n      0;\n    return { sx: Number(sx) || 0, sy: Number(sy) || 0 };\n  } catch {\n    return { sx: 0, sy: 0 };\n  }\n}\n\nexport function getBoundingRectLite(this: Element): {\n  left: number;\n  top: number;\n} {\n  try {\n    const rect = this.getBoundingClientRect();\n    return {\n      left: Number(rect?.left ?? 0) || 0,\n      top: Number(rect?.top ?? 0) || 0,\n    };\n  } catch {\n    return { left: 0, top: 0 };\n  }\n}\n\nexport function resolveDeepActiveElement(): Element | null {\n  try {\n    const deepActive = (doc: Document | ShadowRoot): Element | null => {\n      let el: Element | null = doc.activeElement ?? null;\n      while (el && el.shadowRoot && el.shadowRoot.activeElement) {\n        el = el.shadowRoot.activeElement;\n      }\n      return el ?? null;\n    };\n    return deepActive(document);\n  } catch {\n    return null;\n  }\n}\n\nexport function nodeToAbsoluteXPath(this: Node | null | undefined): string {\n  const compute = (node: Node | null | undefined): string => {\n    try {\n      const sibIndex = (n: Node | null | undefined): number => {\n        if (!n || !n.parentNode) return 1;\n        let i = 1;\n        const targetKey = `${n.nodeType}:${(n.nodeName || \"\").toLowerCase()}`;\n        for (let p = n.previousSibling; p; p = p.previousSibling) {\n          const key = `${p.nodeType}:${(p.nodeName || \"\").toLowerCase()}`;\n          if (key === targetKey) i += 1;\n        }\n        return i;\n      };\n\n      const step = (n: Node | null | undefined): string => {\n        if (!n) return \"\";\n        if (n.nodeType === Node.DOCUMENT_NODE) return \"\";\n        if (n.nodeType === Node.DOCUMENT_FRAGMENT_NODE) return \"//\";\n        if (n.nodeType === Node.TEXT_NODE) return `text()[${sibIndex(n)}]`;\n        if (n.nodeType === Node.COMMENT_NODE)\n          return `comment()[${sibIndex(n)}]`;\n        const tag = (n.nodeName || \"\").toLowerCase();\n        const name = tag.includes(\":\") ? `*[name()='${tag}']` : tag;\n        return `${name}[${sibIndex(n)}]`;\n      };\n\n      const parts: string[] = [];\n      let cur: Node | null | undefined = node;\n      while (cur) {\n        if (cur.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {\n          parts.push(\"//\");\n          cur = (cur as ShadowRoot).host ?? null;\n          continue;\n        }\n        const s = step(cur);\n        if (s) parts.push(s);\n        cur = cur.parentNode;\n      }\n      parts.reverse();\n\n      let out = \"\";\n      for (const part of parts) {\n        if (part === \"//\") {\n          out = out ? (out.endsWith(\"/\") ? `${out}/` : `${out}//`) : \"//\";\n        } else {\n          out = out\n            ? out.endsWith(\"/\")\n              ? `${out}${part}`\n              : `${out}/${part}`\n            : `/${part}`;\n        }\n      }\n      return out || \"/\";\n    } catch {\n      return \"/\";\n    }\n  };\n\n  return compute(this);\n}\n\nexport function documentHasFocusStrict(): boolean {\n  try {\n    return document.hasFocus() === true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/genA11yScripts.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport esbuild from \"esbuild\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\n\nconst here = getCurrentDirPath();\nconst srcDir = path.join(here, \"./a11yScripts\");\nconst outDir = path.join(here, \"./build\");\nconst entry = path.join(srcDir, \"index.ts\");\nconst moduleOut = path.join(outDir, \"a11yScripts.mjs\");\nconst bundleOut = path.join(outDir, \"a11yScripts.bundle.js\");\n\nasync function main(): Promise<void> {\n  fs.mkdirSync(outDir, { recursive: true });\n\n  esbuild.buildSync({\n    entryPoints: [entry],\n    bundle: true,\n    format: \"esm\",\n    platform: \"browser\",\n    target: \"es2020\",\n    minify: true,\n    outfile: moduleOut,\n  });\n\n  esbuild.buildSync({\n    entryPoints: [entry],\n    bundle: true,\n    format: \"iife\",\n    platform: \"browser\",\n    target: \"es2020\",\n    globalName: \"__stagehandA11yScriptsFactory\",\n    minify: true,\n    outfile: bundleOut,\n  });\n\n  const bundleRaw = fs.readFileSync(bundleOut, \"utf8\").trim();\n  const bootstrap = `if (!globalThis.__stagehandA11yScripts) { ${bundleRaw}\\n  globalThis.__stagehandA11yScripts = __stagehandA11yScriptsFactory;\\n}`;\n\n  const compiledModule = (await import(\n    pathToFileURL(moduleOut).href\n  )) as Record<string, unknown>;\n\n  const entries = Object.entries(compiledModule).filter(\n    ([, value]) => typeof value === \"function\",\n  );\n  const sorted = entries.sort(([a], [b]) => a.localeCompare(b));\n\n  const scriptMap: Record<string, string> = Object.fromEntries(\n    sorted.map(([name, fn]) => {\n      const callable = fn as (...args: unknown[]) => unknown;\n      return [name, callable.toString()];\n    }),\n  );\n\n  const banner = `/*\\n * AUTO-GENERATED FILE. DO NOT EDIT.\\n * Update sources in lib/v3/dom/a11yScripts and run genA11yScripts.ts.\\n */`;\n\n  const globalRefs: Record<string, string> = Object.fromEntries(\n    sorted.map(([name]) => [name, `globalThis.__stagehandA11yScripts.${name}`]),\n  );\n\n  const content = `${banner}\nexport const a11yScriptBootstrap = ${JSON.stringify(bootstrap)};\nexport const a11yScriptSources = ${JSON.stringify(scriptMap, null, 2)} as const;\nexport const a11yScriptGlobalRefs = ${JSON.stringify(globalRefs, null, 2)} as const;\nexport type A11yScriptName = keyof typeof a11yScriptSources;\n`;\n\n  fs.writeFileSync(path.join(outDir, \"a11yScripts.generated.ts\"), content);\n\n  await fs.promises.unlink(moduleOut).catch(() => {});\n  await fs.promises.unlink(bundleOut).catch(() => {});\n}\n\nvoid main();\n"
  },
  {
    "path": "packages/core/lib/v3/dom/genDomScripts.ts",
    "content": "/**\n * Build the v3 DOM script into a single JS file and then export its contents\n * as a string constant (`v3ScriptContent`) for CDP injection (document-start).\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport esbuild from \"esbuild\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\n\nconst here = getCurrentDirPath();\nconst outDir = path.join(here, \"./build\");\nfs.mkdirSync(outDir, { recursive: true });\n\nesbuild.buildSync({\n  entryPoints: [path.join(here, \"piercer.entry.ts\")],\n  bundle: true,\n  format: \"iife\",\n  platform: \"browser\",\n  target: \"es2020\",\n  minify: true,\n  legalComments: \"none\",\n  outfile: path.join(outDir, \"v3-index.js\"),\n});\n\nconst script = fs.readFileSync(path.join(outDir, \"v3-index.js\"), \"utf8\");\nconst content = `export const v3ScriptContent = ${JSON.stringify(script)};`;\n\nfs.writeFileSync(path.join(outDir, \"scriptV3Content.ts\"), content);\n\nesbuild.buildSync({\n  entryPoints: [path.join(here, \"rerenderMissingShadows.entry.ts\")],\n  bundle: true,\n  format: \"iife\",\n  platform: \"browser\",\n  target: \"es2020\",\n  minify: true,\n  legalComments: \"none\",\n  outfile: path.join(outDir, \"rerender-index.js\"),\n});\n\nconst rerenderScript = fs.readFileSync(\n  path.join(outDir, \"rerender-index.js\"),\n  \"utf8\",\n);\nconst rerenderContent = `export const reRenderScriptContent = ${JSON.stringify(\n  rerenderScript,\n)};`;\nfs.writeFileSync(\n  path.join(outDir, \"reRenderScriptContent.ts\"),\n  rerenderContent,\n);\n"
  },
  {
    "path": "packages/core/lib/v3/dom/genLocatorScripts.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport esbuild from \"esbuild\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\n\nconst here = getCurrentDirPath();\nconst outDir = path.join(here, \"./build\");\nconst entry = path.join(here, \"./locatorScripts/index.ts\");\nconst moduleOutfile = path.join(outDir, \"locatorScripts.mjs\");\nconst bundleOutfile = path.join(outDir, \"locatorScripts.bundle.js\");\n\nasync function main(): Promise<void> {\n  fs.mkdirSync(outDir, { recursive: true });\n\n  esbuild.buildSync({\n    entryPoints: [entry],\n    bundle: true,\n    format: \"esm\",\n    platform: \"browser\",\n    target: \"es2020\",\n    minify: true,\n    outfile: moduleOutfile,\n  });\n\n  esbuild.buildSync({\n    entryPoints: [entry],\n    bundle: true,\n    format: \"iife\",\n    platform: \"browser\",\n    target: \"es2020\",\n    globalName: \"__stagehandLocatorScriptsFactory\",\n    minify: true,\n    outfile: bundleOutfile,\n  });\n\n  const bundleRaw = fs.readFileSync(bundleOutfile, \"utf8\").trim();\n  const bootstrap = `if (!globalThis.__stagehandLocatorScripts) { ${bundleRaw}\\n  globalThis.__stagehandLocatorScripts = __stagehandLocatorScriptsFactory;\\n}`;\n\n  const compiledModule = (await import(\n    pathToFileURL(moduleOutfile).href\n  )) as Record<string, unknown>;\n\n  const entries = Object.entries(compiledModule).filter(\n    ([, value]) => typeof value === \"function\",\n  );\n  const sorted = entries.sort(([a], [b]) => a.localeCompare(b));\n\n  const scriptMap: Record<string, string> = Object.fromEntries(\n    sorted.map(([name, fn]) => {\n      const callable = fn as (...args: unknown[]) => unknown;\n      return [name, callable.toString()];\n    }),\n  );\n\n  const banner = `/*\\n * AUTO-GENERATED FILE. DO NOT EDIT.\\n * Update sources in lib/v3/dom/locatorScripts and run genLocatorScripts.ts.\\n */`;\n\n  const globalRefs: Record<string, string> = Object.fromEntries(\n    sorted.map(([name]) => [\n      name,\n      `globalThis.__stagehandLocatorScripts.${name}`,\n    ]),\n  );\n\n  const content = `${banner}\\nexport const locatorScriptBootstrap = ${JSON.stringify(bootstrap)};\\nexport const locatorScriptSources = ${JSON.stringify(scriptMap, null, 2)} as const;\\nexport const locatorScriptGlobalRefs = ${JSON.stringify(globalRefs, null, 2)} as const;\\nexport type LocatorScriptName = keyof typeof locatorScriptSources;\\n`;\n\n  fs.writeFileSync(path.join(outDir, \"locatorScripts.generated.ts\"), content);\n\n  await fs.promises.unlink(moduleOutfile).catch(() => {});\n  await fs.promises.unlink(bundleOutfile).catch(() => {});\n}\n\nvoid main();\n"
  },
  {
    "path": "packages/core/lib/v3/dom/genScreenshotScripts.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport esbuild from \"esbuild\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\n\nconst here = getCurrentDirPath();\nconst srcDir = path.join(here, \"./screenshotScripts\");\nconst outDir = path.join(here, \"./build\");\nconst entry = path.join(srcDir, \"index.ts\");\nconst moduleOut = path.join(outDir, \"screenshotScripts.mjs\");\n\nasync function main(): Promise<void> {\n  fs.mkdirSync(outDir, { recursive: true });\n\n  esbuild.buildSync({\n    entryPoints: [entry],\n    bundle: true,\n    format: \"esm\",\n    platform: \"browser\",\n    target: \"es2020\",\n    minify: true,\n    outfile: moduleOut,\n  });\n\n  const compiledModule = (await import(\n    pathToFileURL(moduleOut).href\n  )) as Record<string, unknown>;\n\n  const entries = Object.entries(compiledModule).filter(\n    ([, value]) => typeof value === \"function\",\n  );\n  const sorted = entries.sort(([a], [b]) => a.localeCompare(b));\n\n  const scriptMap: Record<string, string> = Object.fromEntries(\n    sorted.map(([name, fn]) => {\n      const callable = fn as (...args: unknown[]) => unknown;\n      return [name, callable.toString()];\n    }),\n  );\n\n  const banner = `/*\\n * AUTO-GENERATED FILE. DO NOT EDIT.\\n * Update sources in lib/v3/dom/screenshotScripts and run genScreenshotScripts.ts.\\n */`;\n\n  const content = `${banner}\nexport const screenshotScriptSources = ${JSON.stringify(scriptMap, null, 2)} as const;\nexport type ScreenshotScriptName = keyof typeof screenshotScriptSources;\n`;\n\n  fs.writeFileSync(\n    path.join(outDir, \"screenshotScripts.generated.ts\"),\n    content,\n  );\n\n  await fs.promises.unlink(moduleOut).catch(() => {});\n}\n\nvoid main();\n"
  },
  {
    "path": "packages/core/lib/v3/dom/global.d.ts",
    "content": "export interface StagehandV3Backdoor {\n  /** Closed shadow-root accessors */\n  getClosedRoot(host: Element): ShadowRoot | undefined;\n  /** Stats + quick health check */\n  stats(): {\n    installed: true;\n    url: string;\n    isTop: boolean;\n    open: number;\n    closed: number;\n  };\n}\n\ndeclare global {\n  interface Window {\n    __stagehandV3Injected?: boolean;\n    __stagehandV3__?: StagehandV3Backdoor;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/index.ts",
    "content": "export * from \"./piercer.runtime.js\";\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/counts.ts",
    "content": "import { countXPathMatches } from \"./xpathResolver.js\";\n\nexport interface TextMatchSample {\n  tag: string;\n  id: string;\n  class: string;\n  text: string;\n}\n\nexport interface TextMatchResult {\n  count: number;\n  sample: TextMatchSample[];\n  error: null;\n}\n\nexport function countCssMatchesPrimary(selectorRaw: string): number {\n  const selector = String(selectorRaw ?? \"\").trim();\n  if (!selector) return 0;\n\n  const seen = new WeakSet<Node>();\n\n  const visit = (root: Node | null | undefined): number => {\n    if (!root || seen.has(root)) return 0;\n    seen.add(root);\n\n    let total = 0;\n    try {\n      const queryable = root as unknown as ParentNode & {\n        querySelectorAll?: Document[\"querySelectorAll\"];\n      };\n      if (typeof queryable.querySelectorAll === \"function\") {\n        total += queryable.querySelectorAll(selector).length;\n      }\n    } catch {\n      // ignore query errors\n    }\n\n    try {\n      const doc =\n        root instanceof Document\n          ? root\n          : ((root as Element)?.ownerDocument ?? document);\n      const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n      let node: Node | null;\n      while ((node = walker.nextNode())) {\n        if (node instanceof Element && node.shadowRoot) {\n          total += visit(node.shadowRoot);\n        }\n      }\n    } catch {\n      // ignore traversal errors\n    }\n\n    return total;\n  };\n\n  try {\n    return visit(document);\n  } catch {\n    try {\n      return document.querySelectorAll(selector).length;\n    } catch {\n      return 0;\n    }\n  }\n}\n\nexport function countCssMatchesPierce(selectorRaw: string): number {\n  const selector = String(selectorRaw ?? \"\").trim();\n  if (!selector) return 0;\n\n  const backdoor = window.__stagehandV3__;\n  if (!backdoor || typeof backdoor.getClosedRoot !== \"function\") {\n    try {\n      return document.querySelectorAll(selector).length;\n    } catch {\n      return 0;\n    }\n  }\n\n  const seen = new WeakSet<Node>();\n  const queue: Node[] = [];\n\n  const enqueue = (node: Node | null | undefined) => {\n    if (!node || seen.has(node)) return;\n    seen.add(node);\n    queue.push(node);\n  };\n\n  enqueue(document);\n  let total = 0;\n\n  const visitElement = (element: Element) => {\n    const open = element.shadowRoot;\n    if (open) enqueue(open);\n    try {\n      const closed = backdoor.getClosedRoot(element);\n      if (closed) enqueue(closed);\n    } catch {\n      // ignore\n    }\n  };\n\n  while (queue.length) {\n    const root = queue.shift();\n    if (!root) continue;\n\n    try {\n      const queryable = root as unknown as ParentNode & {\n        querySelectorAll?: Document[\"querySelectorAll\"];\n      };\n      if (typeof queryable.querySelectorAll === \"function\") {\n        total += queryable.querySelectorAll(selector).length;\n      }\n    } catch {\n      // ignore query errors\n    }\n\n    try {\n      const doc =\n        root instanceof Document\n          ? root\n          : root instanceof ShadowRoot\n            ? (root.host?.ownerDocument ?? document)\n            : ((root as Element).ownerDocument ?? document);\n      const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n      let node: Node | null;\n      while ((node = walker.nextNode())) {\n        if (node instanceof Element) {\n          visitElement(node);\n        }\n      }\n    } catch {\n      // ignore traversal errors\n    }\n  }\n\n  return total;\n}\n\nexport function countTextMatches(rawNeedle: string): TextMatchResult {\n  const needle = String(rawNeedle ?? \"\");\n  if (!needle) {\n    return { count: 0, sample: [], error: null };\n  }\n\n  const needleLc = needle.toLowerCase();\n  const skipTags = new Set([\n    \"SCRIPT\",\n    \"STYLE\",\n    \"TEMPLATE\",\n    \"NOSCRIPT\",\n    \"HEAD\",\n    \"TITLE\",\n    \"LINK\",\n    \"META\",\n    \"HTML\",\n    \"BODY\",\n  ]);\n\n  const shouldSkip = (node: Element | null | undefined): boolean => {\n    if (!node) return false;\n    const tag = node.tagName?.toUpperCase() ?? \"\";\n    return skipTags.has(tag);\n  };\n\n  const extractText = (element: Element): string => {\n    try {\n      if (shouldSkip(element)) return \"\";\n      const inner = (element as HTMLElement).innerText;\n      if (typeof inner === \"string\" && inner.trim()) return inner.trim();\n    } catch {\n      // ignore\n    }\n    try {\n      const text = element.textContent;\n      if (typeof text === \"string\") return text.trim();\n    } catch {\n      // ignore\n    }\n    return \"\";\n  };\n\n  const matches = (element: Element): boolean => {\n    const text = extractText(element);\n    return !!text && text.toLowerCase().includes(needleLc);\n  };\n\n  const backdoor = window.__stagehandV3__;\n  const getClosedRoot: (host: Element) => ShadowRoot | null =\n    backdoor && typeof backdoor.getClosedRoot === \"function\"\n      ? (host: Element): ShadowRoot | null => {\n          try {\n            return backdoor.getClosedRoot(host) ?? null;\n          } catch {\n            return null;\n          }\n        }\n      : (host: Element): ShadowRoot | null => {\n          void host;\n          return null;\n        };\n\n  const seen = new WeakSet<Node>();\n  const queue: Node[] = [];\n\n  const enqueue = (node: Node | null | undefined) => {\n    if (!node || seen.has(node)) return;\n    seen.add(node);\n    queue.push(node);\n  };\n\n  const walkerFor = (root: Node): TreeWalker | null => {\n    try {\n      const doc =\n        root instanceof Document\n          ? root\n          : ((root as Element)?.ownerDocument ?? document);\n      return doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n    } catch {\n      return null;\n    }\n  };\n\n  const matchesList: Array<{\n    element: Element;\n    tag: string;\n    id: string;\n    className: string;\n    text: string;\n  }> = [];\n\n  enqueue(document);\n\n  while (queue.length) {\n    const root = queue.shift();\n    if (!root) continue;\n\n    if (root instanceof Element && matches(root)) {\n      matchesList.push({\n        element: root,\n        tag: root.tagName ?? \"\",\n        id: root.id ?? \"\",\n        className: (root as HTMLElement).className ?? \"\",\n        text: extractText(root),\n      });\n    }\n\n    const walker = walkerFor(root);\n    if (!walker) continue;\n\n    let node: Node | null;\n    while ((node = walker.nextNode())) {\n      if (!(node instanceof Element)) continue;\n\n      if (matches(node)) {\n        matchesList.push({\n          element: node,\n          tag: node.tagName ?? \"\",\n          id: node.id ?? \"\",\n          className: (node as HTMLElement).className ?? \"\",\n          text: extractText(node),\n        });\n      }\n\n      const open = node.shadowRoot;\n      if (open) enqueue(open);\n\n      const closed = getClosedRoot(node);\n      if (closed) enqueue(closed);\n    }\n  }\n\n  const innermost: typeof matchesList = [];\n  for (const item of matchesList) {\n    const el = item.element;\n    let skip = false;\n    for (const other of matchesList) {\n      if (item === other) continue;\n      try {\n        if (el.contains(other.element)) {\n          skip = true;\n          break;\n        }\n      } catch {\n        // ignore containment errors\n      }\n    }\n    if (!skip) innermost.push(item);\n  }\n\n  const count = innermost.length;\n  const sample = innermost.slice(0, 5).map((item) => ({\n    tag: item.tag,\n    id: item.id,\n    class: item.className,\n    text: item.text,\n  }));\n\n  return { count, sample, error: null };\n}\n\nexport function countXPathMatchesMainWorld(rawXp: string): number {\n  return countXPathMatches(rawXp, { pierceShadow: true });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/index.ts",
    "content": "export * from \"./scripts.js\";\nexport * from \"./selectors.js\";\nexport * from \"./counts.js\";\nexport * from \"./waitForSelector.js\";\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/scripts.ts",
    "content": "/*\n * DOM-side helpers used by Locator Runtime.callFunctionOn invocations.\n *\n * NOTE: These functions run inside the page context. Keep them dependency-free\n * and resilient to exceptions (match the best-effort semantics of the old\n * inline string snippets).\n */\n\nexport interface ClickEventOptions {\n  bubbles?: boolean;\n  cancelable?: boolean;\n  composed?: boolean;\n  detail?: number;\n}\n\nexport function ensureFileInputElement(this: Element): boolean {\n  try {\n    const tag = (this as HTMLElement).tagName?.toLowerCase() ?? \"\";\n    if (tag !== \"input\") return false;\n    const type = String((this as HTMLInputElement).type ?? \"\").toLowerCase();\n    return type === \"file\";\n  } catch {\n    return false;\n  }\n}\n\nexport interface SerializedFilePayload {\n  name: string;\n  mimeType: string;\n  base64: string;\n  lastModified?: number;\n}\n\n/** Attach File objects created from serialized payloads to an <input type=\"file\">. */\nexport function assignFilePayloadsToInputElement(\n  this: Element,\n  payloads: SerializedFilePayload[],\n): boolean {\n  try {\n    const input = this as HTMLInputElement;\n    if (!input || input.tagName?.toLowerCase() !== \"input\") return false;\n    if ((input.type ?? \"\").toLowerCase() !== \"file\") return false;\n\n    const transfer: DataTransfer | null = (() => {\n      try {\n        return new DataTransfer();\n      } catch {\n        return null;\n      }\n    })();\n    if (!transfer) return false;\n\n    const entries = Array.isArray(payloads) ? payloads : [];\n    for (const payload of entries) {\n      if (!payload) continue;\n      const name = payload.name || \"upload.bin\";\n      const mimeType = payload.mimeType || \"application/octet-stream\";\n      const lastModified =\n        typeof payload.lastModified === \"number\"\n          ? payload.lastModified\n          : Date.now();\n\n      const binary = window.atob(payload.base64 ?? \"\");\n      const bytes = new Uint8Array(binary.length);\n      for (let i = 0; i < binary.length; i += 1) {\n        bytes[i] = binary.charCodeAt(i);\n      }\n      const blob = new Blob([bytes], { type: mimeType });\n      const file = new File([blob], name, { type: mimeType, lastModified });\n      transfer.items.add(file);\n    }\n\n    input.files = transfer.files;\n    input.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    input.dispatchEvent(new Event(\"change\", { bubbles: true }));\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nexport function dispatchDomClick(\n  this: Element,\n  options?: ClickEventOptions,\n): void {\n  const opts = options ?? {};\n  try {\n    const event = new MouseEvent(\"click\", {\n      bubbles: !!opts.bubbles,\n      cancelable: !!opts.cancelable,\n      composed: !!opts.composed,\n      detail: typeof opts.detail === \"number\" ? opts.detail : 1,\n      view: this?.ownerDocument?.defaultView ?? window,\n    });\n    this.dispatchEvent(event);\n  } catch {\n    try {\n      // Fallback to native click if MouseEvent construction fails.\n      (this as HTMLElement).click();\n    } catch {\n      /* ignore */\n    }\n  }\n}\n\nexport function scrollElementToPercent(\n  this: Element,\n  percent: number | string,\n): boolean {\n  const normalize = (value: unknown): number => {\n    if (typeof value === \"number\" && Number.isFinite(value)) return value;\n    const str = String(value ?? \"\").trim();\n    if (!str) return 0;\n    const numeric = parseFloat(str.replace(\"%\", \"\"));\n    if (Number.isNaN(numeric) || !Number.isFinite(numeric)) return 0;\n    return numeric;\n  };\n\n  try {\n    const pct = Math.max(0, Math.min(normalize(percent), 100));\n    const element = this as HTMLElement;\n    const tag = element.tagName?.toLowerCase() ?? \"\";\n\n    const scrollWindow = tag === \"html\" || tag === \"body\";\n    if (scrollWindow) {\n      const root =\n        element.ownerDocument?.scrollingElement ||\n        element.ownerDocument?.documentElement ||\n        element.ownerDocument?.body ||\n        document.scrollingElement ||\n        document.documentElement ||\n        document.body;\n      const scrollHeight =\n        root?.scrollHeight ?? document.body.scrollHeight ?? 0;\n      const viewportHeight =\n        element.ownerDocument?.defaultView?.innerHeight ?? window.innerHeight;\n      const maxTop = Math.max(0, scrollHeight - viewportHeight);\n      const top = maxTop * (pct / 100);\n      element.ownerDocument?.defaultView?.scrollTo({\n        top,\n        left:\n          element.ownerDocument?.defaultView?.scrollX ?? window.scrollX ?? 0,\n        behavior: \"smooth\",\n      });\n      return true;\n    }\n\n    const scrollHeight = element.scrollHeight ?? 0;\n    const clientHeight = element.clientHeight ?? 0;\n    const maxTop = Math.max(0, scrollHeight - clientHeight);\n    const top = maxTop * (pct / 100);\n    element.scrollTo({\n      top,\n      left: element.scrollLeft ?? 0,\n      behavior: \"smooth\",\n    });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nconst inputTypesToSetValue = new Set([\n  \"color\",\n  \"date\",\n  \"datetime-local\",\n  \"month\",\n  \"range\",\n  \"time\",\n  \"week\",\n]);\n\nconst inputTypesToTypeInto = new Set([\n  \"\",\n  \"email\",\n  \"number\",\n  \"password\",\n  \"search\",\n  \"tel\",\n  \"text\",\n  \"url\",\n]);\n\nexport type FillElementResult =\n  | { status: \"done\" }\n  | { status: \"needsinput\"; value: string; reason?: string }\n  | { status: \"error\"; reason: string };\n\nexport function prepareElementForTyping(this: Element): boolean {\n  try {\n    const element = this as HTMLElement;\n    if (!element.isConnected) return false;\n\n    const doc = element.ownerDocument || document;\n    const win = doc.defaultView || window;\n\n    try {\n      if (typeof element.focus === \"function\") {\n        element.focus();\n      }\n    } catch {\n      /* ignore */\n    }\n\n    if (\n      element instanceof win.HTMLInputElement ||\n      element instanceof win.HTMLTextAreaElement\n    ) {\n      try {\n        if (typeof element.select === \"function\") {\n          element.select();\n          return true;\n        }\n      } catch {\n        /* ignore */\n      }\n\n      try {\n        const length = (element.value ?? \"\").length;\n        if (typeof element.setSelectionRange === \"function\") {\n          element.setSelectionRange(0, length);\n          return true;\n        }\n      } catch {\n        /* ignore */\n      }\n\n      return true;\n    }\n\n    if (element.isContentEditable) {\n      const selection = doc.getSelection?.();\n      const range = doc.createRange?.();\n      if (selection && range) {\n        try {\n          range.selectNodeContents(element);\n          selection.removeAllRanges();\n          selection.addRange(range);\n        } catch {\n          /* ignore */\n        }\n      }\n      return true;\n    }\n\n    return false;\n  } catch {\n    return false;\n  }\n}\n\nexport function fillElementValue(\n  this: Element,\n  rawValue: string,\n): FillElementResult {\n  const element = this as HTMLElement;\n  if (!element.isConnected) {\n    return { status: \"error\", reason: \"notconnected\" };\n  }\n\n  const doc = element.ownerDocument || document;\n  const win = doc.defaultView || window;\n  let fallbackValue = rawValue ?? \"\";\n\n  try {\n    const dispatchInputAndChange = (eventValue: string): void => {\n      let inputEvent: Event;\n      if (typeof win.InputEvent === \"function\") {\n        try {\n          inputEvent = new win.InputEvent(\"input\", {\n            bubbles: true,\n            composed: true,\n            data: eventValue,\n            inputType: \"insertText\",\n          });\n        } catch {\n          inputEvent = new win.Event(\"input\", {\n            bubbles: true,\n            composed: true,\n          });\n        }\n      } else {\n        inputEvent = new win.Event(\"input\", { bubbles: true, composed: true });\n      }\n\n      element.dispatchEvent(inputEvent);\n\n      const changeEvent = new win.Event(\"change\", { bubbles: true });\n      element.dispatchEvent(changeEvent);\n    };\n\n    if (element instanceof win.HTMLInputElement) {\n      const type = (element.type || \"\").toLowerCase();\n\n      if (!inputTypesToTypeInto.has(type) && !inputTypesToSetValue.has(type)) {\n        return { status: \"error\", reason: `unsupported-input-type:${type}` };\n      }\n\n      let valueForTyping = rawValue;\n\n      if (type === \"number\") {\n        const trimmed = rawValue.trim();\n        if (trimmed !== \"\" && Number.isNaN(Number(trimmed))) {\n          return { status: \"error\", reason: \"invalid-number-value\" };\n        }\n        valueForTyping = trimmed;\n      }\n\n      fallbackValue = valueForTyping;\n\n      if (inputTypesToSetValue.has(type)) {\n        const trimmed = rawValue.trim();\n        fallbackValue = trimmed;\n        prepareElementForTyping.call(element);\n\n        const prototype = win.HTMLInputElement.prototype;\n        const descriptor = Object.getOwnPropertyDescriptor(prototype, \"value\");\n        const nativeSetter = descriptor?.set;\n\n        if (typeof nativeSetter === \"function\") {\n          nativeSetter.call(element, trimmed);\n        } else {\n          element.value = trimmed;\n        }\n\n        const tracker = (\n          element as unknown as {\n            _valueTracker?: { setValue?: (next: string) => void };\n          }\n        )._valueTracker;\n        tracker?.setValue?.(trimmed);\n\n        if (element.value !== trimmed) {\n          return { status: \"error\", reason: \"malformed-value\" };\n        }\n\n        dispatchInputAndChange(trimmed);\n        return { status: \"done\" };\n      }\n\n      prepareElementForTyping.call(element);\n      return { status: \"needsinput\", value: valueForTyping };\n    }\n\n    if (element instanceof win.HTMLTextAreaElement) {\n      prepareElementForTyping.call(element);\n      fallbackValue = rawValue;\n      return { status: \"needsinput\", value: rawValue };\n    }\n\n    if (element instanceof win.HTMLSelectElement) {\n      // Select elements use setInputFiles/selectOption instead.\n      return { status: \"error\", reason: \"unsupported-element\" };\n    }\n\n    if (element.isContentEditable) {\n      prepareElementForTyping.call(element);\n      fallbackValue = rawValue;\n      return { status: \"needsinput\", value: rawValue };\n    }\n\n    return { status: \"error\", reason: \"unsupported-element\" };\n  } catch (error) {\n    let reason = \"exception\";\n    if (error && typeof error === \"object\") {\n      const message = (error as { message?: unknown }).message;\n      if (typeof message === \"string\" && message.trim().length > 0) {\n        reason = `exception:${message}`;\n      }\n    }\n    return { status: \"needsinput\", value: fallbackValue, reason };\n  }\n}\n\nexport function focusElement(this: Element): void {\n  try {\n    if (typeof (this as HTMLElement).focus === \"function\") {\n      (this as HTMLElement).focus();\n    }\n  } catch {\n    /* ignore */\n  }\n}\n\nexport function selectElementOptions(\n  this: Element,\n  rawValues: string | string[],\n): string[] {\n  try {\n    if (!(this instanceof HTMLSelectElement)) return [];\n\n    const desired = Array.isArray(rawValues) ? rawValues : [rawValues];\n    const wanted = new Set(desired.map((v) => String(v ?? \"\").trim()));\n\n    const matches = (option: HTMLOptionElement): boolean => {\n      const label = (option.label || option.textContent || \"\").trim();\n      const value = String(option.value ?? \"\").trim();\n      return wanted.has(label) || wanted.has(value);\n    };\n\n    if (this.multiple) {\n      for (const option of Array.from(this.options)) {\n        option.selected = matches(option);\n      }\n    } else {\n      let chosen = false;\n      for (const option of Array.from(this.options)) {\n        if (!chosen && matches(option)) {\n          option.selected = true;\n          this.value = option.value;\n          chosen = true;\n        } else {\n          option.selected = false;\n        }\n      }\n    }\n\n    const inputEvent = new Event(\"input\", { bubbles: true });\n    const changeEvent = new Event(\"change\", { bubbles: true });\n    this.dispatchEvent(inputEvent);\n    this.dispatchEvent(changeEvent);\n\n    return Array.from(this.selectedOptions).map((opt) => opt.value);\n  } catch {\n    return [];\n  }\n}\n\nexport function isElementVisible(this: Element): boolean {\n  try {\n    const element = this as HTMLElement;\n    if (!element.isConnected) return false;\n\n    const style =\n      element.ownerDocument?.defaultView?.getComputedStyle(element) ??\n      window.getComputedStyle(element);\n    if (!style) return false;\n    if (style.display === \"none\" || style.visibility === \"hidden\") return false;\n    const opacity = parseFloat(style.opacity ?? \"1\");\n    if (!Number.isFinite(opacity) || opacity === 0) return false;\n\n    const rect = element.getBoundingClientRect();\n    if (!rect) return false;\n    if (Math.max(rect.width, rect.height) === 0) return false;\n\n    if (element.getClientRects().length === 0) return false;\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nexport function isElementChecked(this: Element): boolean {\n  try {\n    const element = this as HTMLElement;\n    const tag = (element.tagName || \"\").toLowerCase();\n    if (tag === \"input\") {\n      const type = (element as HTMLInputElement).type?.toLowerCase() ?? \"\";\n      if (type === \"checkbox\" || type === \"radio\") {\n        return !!(element as HTMLInputElement).checked;\n      }\n    }\n    const aria = element.getAttribute?.(\"aria-checked\");\n    if (aria != null) return aria === \"true\";\n    return false;\n  } catch {\n    return false;\n  }\n}\n\nexport function readElementInputValue(this: Element): string {\n  try {\n    const element = this as HTMLElement;\n    const tag = (element.tagName || \"\").toLowerCase();\n    if (tag === \"input\" || tag === \"textarea\") {\n      return String(\n        (element as HTMLInputElement | HTMLTextAreaElement).value ?? \"\",\n      );\n    }\n    if (tag === \"select\") {\n      return String((element as HTMLSelectElement).value ?? \"\");\n    }\n    if (element.isContentEditable) {\n      return String(element.textContent ?? \"\");\n    }\n    return \"\";\n  } catch {\n    return \"\";\n  }\n}\n\nexport function readElementTextContent(this: Element): string {\n  try {\n    return String(this.textContent ?? \"\");\n  } catch {\n    return \"\";\n  }\n}\n\nexport function readElementInnerHTML(this: Element): string {\n  try {\n    return String((this as HTMLElement).innerHTML ?? \"\");\n  } catch {\n    return \"\";\n  }\n}\n\nexport function readElementInnerText(this: Element): string {\n  try {\n    const element = this as HTMLElement;\n    const inner = (element as HTMLElement & { innerText?: unknown }).innerText;\n    if (typeof inner === \"string\" && inner.length > 0) {\n      return inner;\n    }\n    const fallback = element.textContent;\n    return typeof fallback === \"string\" ? fallback : \"\";\n  } catch {\n    return \"\";\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/selectors.ts",
    "content": "import { resolveXPathAtIndex } from \"./xpathResolver.js\";\n\nconst parseTargetIndex = (value: unknown): number => {\n  const num = Number(value ?? 0);\n  if (!Number.isFinite(num) || num < 0) return 0;\n  return Math.floor(num);\n};\n\nconst collectCssMatches = (selector: string, limit: number): Element[] => {\n  if (!selector) return [];\n  const seenRoots = new WeakSet<Node>();\n  const seenElements = new Set<Element>();\n  const results: Element[] = [];\n  const queue: Array<Document | ShadowRoot> = [document];\n\n  const visit = (root: Document | ShadowRoot): void => {\n    if (!root || seenRoots.has(root) || results.length >= limit) return;\n    seenRoots.add(root);\n\n    try {\n      const matches = root.querySelectorAll(selector);\n      for (const element of matches) {\n        if (seenElements.has(element)) continue;\n        seenElements.add(element);\n        results.push(element);\n        if (results.length >= limit) return;\n      }\n    } catch {\n      // ignore querySelectorAll issues\n    }\n\n    try {\n      const ownerDocument =\n        root instanceof Document\n          ? root\n          : (root.host?.ownerDocument ?? document);\n      const walker = ownerDocument.createTreeWalker(\n        root,\n        NodeFilter.SHOW_ELEMENT,\n      );\n      let node: Node | null;\n      while ((node = walker.nextNode())) {\n        if (!(node instanceof Element)) continue;\n        const open = node.shadowRoot;\n        if (open) queue.push(open);\n      }\n    } catch {\n      // ignore traversal issues\n    }\n  };\n\n  while (queue.length && results.length < limit) {\n    const next = queue.shift();\n    if (next) visit(next);\n  }\n\n  return results;\n};\n\nexport function resolveCssSelector(\n  selectorRaw: string,\n  targetIndexRaw?: number,\n): Element | null {\n  const selector = String(selectorRaw ?? \"\").trim();\n  if (!selector) return null;\n\n  const targetIndex = parseTargetIndex(targetIndexRaw);\n  const matches = collectCssMatches(selector, targetIndex + 1);\n  return matches[targetIndex] ?? null;\n}\n\nexport function resolveCssSelectorPierce(\n  selectorRaw: string,\n  targetIndexRaw?: number,\n): Element | null {\n  const selector = String(selectorRaw ?? \"\").trim();\n  if (!selector) return null;\n\n  const targetIndex = parseTargetIndex(targetIndexRaw);\n  const backdoor = window.__stagehandV3__;\n  if (!backdoor || typeof backdoor.getClosedRoot !== \"function\") {\n    const matches = collectCssMatches(selector, targetIndex + 1);\n    return matches[targetIndex] ?? null;\n  }\n\n  const getClosedRoot: (host: Element) => ShadowRoot | null = (\n    host: Element,\n  ) => {\n    try {\n      return backdoor.getClosedRoot(host) ?? null;\n    } catch {\n      return null;\n    }\n  };\n\n  const seenRoots = new WeakSet<Node>();\n  const seenElements = new Set<Element>();\n  const results: Element[] = [];\n  const queue: Array<Document | ShadowRoot> = [document];\n\n  const visit = (root: Document | ShadowRoot): void => {\n    if (!root || seenRoots.has(root) || results.length >= targetIndex + 1)\n      return;\n    seenRoots.add(root);\n\n    try {\n      const matches = root.querySelectorAll(selector);\n      for (const element of matches) {\n        if (seenElements.has(element)) continue;\n        seenElements.add(element);\n        results.push(element);\n        if (results.length >= targetIndex + 1) return;\n      }\n    } catch {\n      // ignore query errors\n    }\n\n    try {\n      const ownerDocument =\n        root instanceof Document\n          ? root\n          : (root.host?.ownerDocument ?? document);\n      const walker = ownerDocument.createTreeWalker(\n        root,\n        NodeFilter.SHOW_ELEMENT,\n      );\n      let node: Node | null;\n      while ((node = walker.nextNode())) {\n        if (!(node instanceof Element)) continue;\n        const open = node.shadowRoot;\n        if (open) queue.push(open);\n        const closed = getClosedRoot(node);\n        if (closed) queue.push(closed);\n      }\n    } catch {\n      // ignore traversal issues\n    }\n  };\n\n  while (queue.length && results.length < targetIndex + 1) {\n    const next = queue.shift();\n    if (next) visit(next);\n  }\n\n  return results[targetIndex] ?? null;\n}\n\nexport function resolveTextSelector(\n  rawNeedle: string,\n  targetIndexRaw?: number,\n): Element | null {\n  const needle = String(rawNeedle ?? \"\");\n  if (!needle) return null;\n  const needleLc = needle.toLowerCase();\n  const targetIndex = parseTargetIndex(targetIndexRaw);\n\n  const skipTags = new Set([\n    \"SCRIPT\",\n    \"STYLE\",\n    \"TEMPLATE\",\n    \"NOSCRIPT\",\n    \"HEAD\",\n    \"TITLE\",\n    \"LINK\",\n    \"META\",\n    \"HTML\",\n    \"BODY\",\n  ]);\n\n  const shouldSkip = (node: Element | null | undefined): boolean => {\n    if (!node) return false;\n    const tag = node.tagName?.toUpperCase() ?? \"\";\n    return skipTags.has(tag);\n  };\n\n  const extractText = (node: Element): string => {\n    try {\n      if (shouldSkip(node)) return \"\";\n      const inner = (node as HTMLElement).innerText;\n      if (typeof inner === \"string\" && inner.trim()) return inner.trim();\n    } catch {\n      // ignore\n    }\n    try {\n      const text = node.textContent;\n      if (typeof text === \"string\") return text.trim();\n    } catch {\n      // ignore\n    }\n    return \"\";\n  };\n\n  const matches = (node: Element): boolean => {\n    const text = extractText(node);\n    return !!text && text.toLowerCase().includes(needleLc);\n  };\n\n  const backdoor = window.__stagehandV3__;\n  const getClosedRoot: (host: Element) => ShadowRoot | null =\n    backdoor && typeof backdoor.getClosedRoot === \"function\"\n      ? (host: Element): ShadowRoot | null => {\n          try {\n            return backdoor.getClosedRoot(host) ?? null;\n          } catch {\n            return null;\n          }\n        }\n      : (host: Element): ShadowRoot | null => {\n          void host;\n          return null;\n        };\n\n  const seen = new WeakSet<Node>();\n  const queue: Node[] = [];\n  const matchesList: Array<{\n    element: Element;\n    tag: string;\n    id: string;\n    className: string;\n    text: string;\n  }> = [];\n\n  const enqueue = (node: Node | null | undefined) => {\n    if (!node || seen.has(node)) return;\n    seen.add(node);\n    queue.push(node);\n  };\n\n  const walkerFor = (root: Node): TreeWalker | null => {\n    try {\n      const doc =\n        root instanceof Document\n          ? root\n          : ((root as Element)?.ownerDocument ?? document);\n      return doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n    } catch {\n      return null;\n    }\n  };\n\n  enqueue(document);\n\n  while (queue.length) {\n    const root = queue.shift();\n    if (!root) continue;\n\n    if (root instanceof Element && matches(root)) {\n      matchesList.push({\n        element: root,\n        tag: root.tagName ?? \"\",\n        id: root.id ?? \"\",\n        className: (root as HTMLElement).className ?? \"\",\n        text: extractText(root),\n      });\n    }\n\n    const walker = walkerFor(root);\n    if (!walker) continue;\n\n    let node: Node | null;\n    while ((node = walker.nextNode())) {\n      if (!(node instanceof Element)) continue;\n\n      if (matches(node)) {\n        matchesList.push({\n          element: node,\n          tag: node.tagName ?? \"\",\n          id: node.id ?? \"\",\n          className: (node as HTMLElement).className ?? \"\",\n          text: extractText(node),\n        });\n      }\n\n      const open = node.shadowRoot;\n      if (open) enqueue(open);\n\n      const closed = getClosedRoot(node);\n      if (closed) enqueue(closed);\n    }\n  }\n\n  const innermost: typeof matchesList = [];\n  for (const item of matchesList) {\n    const el = item.element;\n    let skip = false;\n    for (const other of matchesList) {\n      if (item === other) continue;\n      try {\n        if (el.contains(other.element)) {\n          skip = true;\n          break;\n        }\n      } catch {\n        // ignore containment errors\n      }\n    }\n    if (!skip) {\n      innermost.push(item);\n    }\n  }\n\n  const target = innermost[targetIndex];\n  return target?.element ?? null;\n}\n\nexport function resolveXPathMainWorld(\n  rawXp: string,\n  targetIndexRaw?: number,\n): Element | null {\n  const targetIndex = parseTargetIndex(targetIndexRaw);\n  return resolveXPathAtIndex(rawXp, targetIndex, { pierceShadow: true });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/waitForSelector.ts",
    "content": "/**\n * waitForSelector - Waits for an element matching a selector to reach a specific state.\n * Supports both CSS selectors and XPath expressions.\n * Uses MutationObserver for efficiency and integrates with the V3 piercer for closed shadow roots.\n *\n * NOTE: This function runs inside the page context. Keep it dependency-free\n * and resilient to exceptions.\n */\n\nimport { resolveXPathFirst } from \"./xpathResolver.js\";\n\ntype WaitForSelectorState = \"attached\" | \"detached\" | \"visible\" | \"hidden\";\n\n/**\n * Check if a selector is an XPath expression.\n */\nconst isXPath = (selector: string): boolean => {\n  return selector.startsWith(\"xpath=\") || selector.startsWith(\"/\");\n};\n\n/**\n * Get closed shadow root via the V3 piercer if available.\n */\nconst getClosedRoot = (element: Element): ShadowRoot | null => {\n  try {\n    const backdoor = window.__stagehandV3__;\n    if (backdoor && typeof backdoor.getClosedRoot === \"function\") {\n      return backdoor.getClosedRoot(element) ?? null;\n    }\n  } catch {\n    // ignore\n  }\n  return null;\n};\n\n/**\n * Get shadow root (open or closed via piercer).\n */\nconst getShadowRoot = (element: Element): ShadowRoot | null => {\n  // First try open shadow root\n  if (element.shadowRoot) return element.shadowRoot;\n  // Then try closed shadow root via piercer\n  return getClosedRoot(element);\n};\n\n/**\n * Deep querySelector that pierces shadow DOM (both open and closed via piercer).\n */\nconst deepQuerySelector = (\n  root: Document | ShadowRoot,\n  selector: string,\n  pierceShadow: boolean,\n): Element | null => {\n  // Try regular querySelector first\n  try {\n    const el = root.querySelector(selector);\n    if (el) return el;\n  } catch {\n    // ignore query errors\n  }\n\n  if (!pierceShadow) return null;\n\n  // BFS queue to search all shadow roots (open and closed)\n  const seenRoots = new WeakSet<Node>();\n  const queue: Array<Document | ShadowRoot> = [root];\n\n  while (queue.length > 0) {\n    const currentRoot = queue.shift();\n    if (!currentRoot || seenRoots.has(currentRoot)) continue;\n    seenRoots.add(currentRoot);\n\n    // Try querySelector on this root\n    try {\n      const found = currentRoot.querySelector(selector);\n      if (found) return found;\n    } catch {\n      // ignore query errors\n    }\n\n    // Walk all elements in this root to find shadow hosts\n    try {\n      const ownerDoc =\n        currentRoot instanceof Document\n          ? currentRoot\n          : (currentRoot.host?.ownerDocument ?? document);\n      const walker = ownerDoc.createTreeWalker(\n        currentRoot,\n        NodeFilter.SHOW_ELEMENT,\n      );\n      let node: Node | null;\n      while ((node = walker.nextNode())) {\n        if (!(node instanceof Element)) continue;\n        const shadowRoot = getShadowRoot(node);\n        if (shadowRoot && !seenRoots.has(shadowRoot)) {\n          queue.push(shadowRoot);\n        }\n      }\n    } catch {\n      // ignore traversal errors\n    }\n  }\n\n  return null;\n};\n\n/**\n * Resolve XPath with shadow DOM piercing support.\n */\nconst deepXPathQuery = (\n  xpath: string,\n  pierceShadow: boolean,\n): Element | null => {\n  return resolveXPathFirst(xpath, { pierceShadow });\n};\n\n/**\n * Find element by selector (CSS or XPath) with optional shadow DOM piercing.\n */\nconst findElement = (\n  selector: string,\n  pierceShadow: boolean,\n): Element | null => {\n  if (isXPath(selector)) {\n    return deepXPathQuery(selector, pierceShadow);\n  }\n  return deepQuerySelector(document, selector, pierceShadow);\n};\n\n/**\n * Check if element matches the desired state.\n */\nconst checkState = (\n  el: Element | null,\n  state: WaitForSelectorState,\n): boolean => {\n  if (state === \"detached\") return el === null;\n  if (state === \"attached\") return el !== null;\n  if (el === null) return false;\n\n  if (state === \"hidden\") {\n    try {\n      const style = window.getComputedStyle(el);\n      const rect = el.getBoundingClientRect();\n      return (\n        style.display === \"none\" ||\n        style.visibility === \"hidden\" ||\n        style.opacity === \"0\" ||\n        rect.width === 0 ||\n        rect.height === 0\n      );\n    } catch {\n      return false;\n    }\n  }\n\n  // state === \"visible\"\n  try {\n    const style = window.getComputedStyle(el);\n    const rect = el.getBoundingClientRect();\n    return (\n      style.display !== \"none\" &&\n      style.visibility !== \"hidden\" &&\n      style.opacity !== \"0\" &&\n      rect.width > 0 &&\n      rect.height > 0\n    );\n  } catch {\n    return false;\n  }\n};\n\n/**\n * Set up MutationObservers on all shadow roots to detect changes.\n */\nconst setupShadowObservers = (\n  callback: () => void,\n  observers: MutationObserver[],\n): void => {\n  const seenRoots = new WeakSet<Node>();\n\n  const observeShadowRoots = (node: Element): void => {\n    const shadowRoot = getShadowRoot(node);\n    if (shadowRoot && !seenRoots.has(shadowRoot)) {\n      seenRoots.add(shadowRoot);\n      const shadowObserver = new MutationObserver(callback);\n      shadowObserver.observe(shadowRoot, {\n        childList: true,\n        subtree: true,\n        attributes: true,\n        attributeFilter: [\"style\", \"class\", \"hidden\", \"disabled\"],\n      });\n      observers.push(shadowObserver);\n\n      // Recurse into shadow root children\n      for (const child of Array.from(shadowRoot.children)) {\n        observeShadowRoots(child);\n      }\n    }\n\n    // Recurse into regular children\n    for (const child of Array.from(node.children)) {\n      observeShadowRoots(child);\n    }\n  };\n\n  const root = document.documentElement || document.body;\n  if (root) {\n    observeShadowRoots(root);\n  }\n};\n\n/**\n * Wait for an element matching the selector to reach the specified state.\n * Supports both CSS selectors and XPath expressions (prefix with \"xpath=\" or start with \"/\").\n *\n * @param selectorRaw - CSS selector or XPath expression to wait for\n * @param stateRaw - Element state: 'attached' | 'detached' | 'visible' | 'hidden'\n * @param timeoutRaw - Maximum time to wait in milliseconds\n * @param pierceShadowRaw - Whether to search inside shadow DOM\n * @returns Promise that resolves to true when condition is met, or rejects on timeout\n */\nexport function waitForSelector(\n  selectorRaw: string,\n  stateRaw?: string,\n  timeoutRaw?: number,\n  pierceShadowRaw?: boolean,\n): Promise<boolean> {\n  const selector = String(selectorRaw ?? \"\").trim();\n  const state =\n    (String(stateRaw ?? \"visible\") as WaitForSelectorState) || \"visible\";\n  const timeout =\n    typeof timeoutRaw === \"number\" && timeoutRaw > 0 ? timeoutRaw : 30000;\n  const pierceShadow = pierceShadowRaw !== false;\n\n  return new Promise<boolean>((resolve, reject) => {\n    let timeoutId: ReturnType<typeof setTimeout> | null = null;\n    let domReadyHandler: (() => void) | null = null;\n    let settled = false;\n    const clearTimer = (): void => {\n      if (timeoutId !== null) {\n        clearTimeout(timeoutId);\n        timeoutId = null;\n      }\n    };\n\n    // Check immediately\n    const el = findElement(selector, pierceShadow);\n    if (checkState(el, state)) {\n      settled = true;\n      resolve(true);\n      return;\n    }\n\n    const observers: MutationObserver[] = [];\n\n    const cleanup = (): void => {\n      for (const obs of observers) {\n        obs.disconnect();\n      }\n      if (domReadyHandler) {\n        document.removeEventListener(\"DOMContentLoaded\", domReadyHandler);\n        domReadyHandler = null;\n      }\n    };\n\n    const check = (): void => {\n      if (settled) return;\n      const el = findElement(selector, pierceShadow);\n      if (checkState(el, state)) {\n        settled = true;\n        clearTimer();\n        cleanup();\n        resolve(true);\n      }\n    };\n\n    // Handle case where document.body is not ready yet\n    const observeRoot = document.body || document.documentElement;\n    if (!observeRoot) {\n      domReadyHandler = (): void => {\n        document.removeEventListener(\"DOMContentLoaded\", domReadyHandler!);\n        domReadyHandler = null;\n        check();\n        setupObservers();\n      };\n      document.addEventListener(\"DOMContentLoaded\", domReadyHandler);\n      timeoutId = setTimeout(() => {\n        if (settled) return;\n        settled = true;\n        clearTimer();\n        cleanup();\n        reject(\n          new Error(\n            `waitForSelector: Timeout ${timeout}ms exceeded waiting for \"${selector}\" to be ${state}`,\n          ),\n        );\n      }, timeout);\n      return;\n    }\n\n    const setupObservers = (): void => {\n      const root = document.body || document.documentElement;\n      if (!root) return;\n\n      // Main document observer\n      const mainObserver = new MutationObserver(check);\n      mainObserver.observe(root, {\n        childList: true,\n        subtree: true,\n        attributes: true,\n        attributeFilter: [\"style\", \"class\", \"hidden\", \"disabled\"],\n      });\n      observers.push(mainObserver);\n\n      // Shadow DOM observers (if piercing)\n      if (pierceShadow) {\n        setupShadowObservers(check, observers);\n      }\n    };\n\n    setupObservers();\n\n    // Set up timeout\n    timeoutId = setTimeout(() => {\n      if (settled) return;\n      settled = true;\n      clearTimer();\n      cleanup();\n      reject(\n        new Error(\n          `waitForSelector: Timeout ${timeout}ms exceeded waiting for \"${selector}\" to be ${state}`,\n        ),\n      );\n    }, timeout);\n  });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/xpathParser.ts",
    "content": "export type XPathPredicate =\n  | { type: \"index\"; index: number }\n  | { type: \"attrEquals\"; name: string; value: string; normalize?: boolean }\n  | { type: \"attrExists\"; name: string }\n  | {\n      type: \"attrContains\";\n      name: string;\n      value: string;\n      normalize?: boolean;\n    }\n  | {\n      type: \"attrStartsWith\";\n      name: string;\n      value: string;\n      normalize?: boolean;\n    }\n  | { type: \"textEquals\"; value: string; normalize?: boolean }\n  | { type: \"textContains\"; value: string; normalize?: boolean }\n  | { type: \"and\"; predicates: XPathPredicate[] }\n  | { type: \"or\"; predicates: XPathPredicate[] }\n  | { type: \"not\"; predicate: XPathPredicate };\n\nexport interface XPathStep {\n  axis: \"child\" | \"desc\";\n  tag: string;\n  predicates: XPathPredicate[];\n}\n\n/**\n * Parse an XPath expression into a list of traversal steps.\n *\n * This is a subset parser designed for composed DOM traversal (including\n * shadow roots). It intentionally does not implement the full XPath spec.\n *\n * Supported:\n *  - Child (`/`) and descendant (`//`) axes\n *  - Tag names and wildcard (`*`)\n *  - Positional indices (`[n]`)\n *  - Attribute equality predicates (`[@attr='value']`, `[@attr=\"value\"]`)\n *  - Attribute existence (`[@attr]`)\n *  - Attribute contains/starts-with (`contains(@attr,'v')`, `starts-with(@attr,'v')`)\n *  - Text equality/contains (`[text()='v']`, `[contains(text(),'v')]`, `[.='v']`)\n *  - normalize-space on text/attributes (`[normalize-space(text())='v']`)\n *  - Basic boolean predicates (`and`, `or`, `not(...)`)\n *  - Multiple predicates per step (`[@class='foo'][2]`)\n *  - Optional `xpath=` prefix\n *\n * Not supported:\n *  - Position functions (`[position() > n]`, `[last()]`)\n *  - Axes beyond child/descendant (`ancestor::`, `parent::`, `self::`,\n *    `preceding-sibling::`, `following-sibling::`)\n *  - Union operator (`|`)\n *  - Grouped expressions (`(//div)[n]`)\n *\n * Unsupported predicates are silently ignored — the step still matches\n * by tag name, but the unrecognized predicate has no filtering effect.\n */\nexport function parseXPathSteps(input: string): XPathStep[] {\n  const path = String(input || \"\")\n    .trim()\n    .replace(/^xpath=/i, \"\");\n  if (!path) return [];\n\n  const steps: XPathStep[] = [];\n  let i = 0;\n\n  while (i < path.length) {\n    let axis: \"child\" | \"desc\" = \"child\";\n    if (path.startsWith(\"//\", i)) {\n      axis = \"desc\";\n      i += 2;\n    } else if (path[i] === \"/\") {\n      axis = \"child\";\n      i += 1;\n    }\n\n    const start = i;\n    let bracketDepth = 0;\n    let quote: string | null = null;\n    while (i < path.length) {\n      const ch = path[i];\n      if (quote) {\n        if (ch === quote) quote = null;\n      } else if (ch === \"'\" || ch === '\"') {\n        quote = ch;\n      } else if (ch === \"[\") {\n        bracketDepth++;\n      } else if (ch === \"]\") {\n        bracketDepth--;\n      } else if (ch === \"/\" && bracketDepth === 0) {\n        break;\n      }\n      i += 1;\n    }\n    const rawStep = path.slice(start, i).trim();\n    if (!rawStep) continue;\n\n    const { tag, predicates } = parseStep(rawStep);\n    steps.push({ axis, tag, predicates });\n  }\n\n  return steps;\n}\n\n/**\n * Extract predicate contents from a string like `[@attr='val'][2]`.\n * Handles `]` inside quoted attribute values (e.g. `[@title='a[0]']`).\n */\nfunction extractPredicates(str: string): string[] {\n  const results: string[] = [];\n  let i = 0;\n  while (i < str.length) {\n    if (str[i] !== \"[\") {\n      i++;\n      continue;\n    }\n    i++; // skip opening [\n    const start = i;\n    let quote: string | null = null;\n    while (i < str.length) {\n      const ch = str[i];\n      if (quote) {\n        if (ch === quote) quote = null;\n      } else if (ch === \"'\" || ch === '\"') {\n        quote = ch;\n      } else if (ch === \"]\") {\n        break;\n      }\n      i++;\n    }\n    results.push(str.slice(start, i).trim());\n    i++; // skip closing ]\n  }\n  return results;\n}\n\nfunction parseStep(raw: string): {\n  tag: string;\n  predicates: XPathPredicate[];\n} {\n  const bracketPos = raw.indexOf(\"[\");\n  if (bracketPos === -1) {\n    const tag = raw === \"\" ? \"*\" : raw.toLowerCase();\n    return { tag, predicates: [] };\n  }\n\n  const tagPart = raw.slice(0, bracketPos).trim();\n  const tag = tagPart === \"\" ? \"*\" : tagPart.toLowerCase();\n  const predicateStr = raw.slice(bracketPos);\n\n  const predicates: XPathPredicate[] = [];\n\n  for (const inner of extractPredicates(predicateStr)) {\n    const parsed = parsePredicateExpression(inner);\n    if (parsed) predicates.push(parsed);\n  }\n\n  return { tag, predicates };\n}\n\nfunction parsePredicateExpression(input: string): XPathPredicate | null {\n  const trimmed = input.trim();\n  if (!trimmed) return null;\n\n  const orParts = splitTopLevel(trimmed, \"or\");\n  if (orParts.length > 1) {\n    const preds = orParts\n      .map((part) => parsePredicateExpression(part))\n      .filter(Boolean) as XPathPredicate[];\n    if (preds.length !== orParts.length) return null;\n    return { type: \"or\", predicates: preds };\n  }\n\n  const andParts = splitTopLevel(trimmed, \"and\");\n  if (andParts.length > 1) {\n    const preds = andParts\n      .map((part) => parsePredicateExpression(part))\n      .filter(Boolean) as XPathPredicate[];\n    if (preds.length !== andParts.length) return null;\n    return { type: \"and\", predicates: preds };\n  }\n\n  const notInner = unwrapFunctionCall(trimmed, \"not\");\n  if (notInner != null) {\n    const predicate = parsePredicateExpression(notInner);\n    return predicate ? { type: \"not\", predicate } : null;\n  }\n\n  return parseAtomicPredicate(trimmed);\n}\n\nfunction parseAtomicPredicate(input: string): XPathPredicate | null {\n  const valueMatch = /^(?:'([^']*)'|\"([^\"]*)\")$/;\n  const attrName = \"[a-zA-Z_][\\\\w.-]*\";\n  const quoted = \"(?:'([^']*)'|\\\"([^\\\"]*)\\\")\";\n\n  if (/^\\d+$/.test(input)) {\n    return { type: \"index\", index: Math.max(1, Number(input)) };\n  }\n\n  const normalizeAttrMatch = input.match(\n    new RegExp(\n      `^normalize-space\\\\(\\\\s*@(${attrName})\\\\s*\\\\)\\\\s*=\\\\s*${quoted}$`,\n    ),\n  );\n  if (normalizeAttrMatch) {\n    return {\n      type: \"attrEquals\",\n      name: normalizeAttrMatch[1],\n      value: normalizeAttrMatch[2] ?? normalizeAttrMatch[3] ?? \"\",\n      normalize: true,\n    };\n  }\n\n  const normalizeTextMatch = input.match(\n    new RegExp(\n      `^normalize-space\\\\(\\\\s*(?:text\\\\(\\\\)|\\\\.)\\\\s*\\\\)\\\\s*=\\\\s*${quoted}$`,\n    ),\n  );\n  if (normalizeTextMatch) {\n    return {\n      type: \"textEquals\",\n      value: normalizeTextMatch[1] ?? normalizeTextMatch[2] ?? \"\",\n      normalize: true,\n    };\n  }\n\n  const attrEqualsMatch = input.match(\n    new RegExp(`^@(${attrName})\\\\s*=\\\\s*${quoted}$`),\n  );\n  if (attrEqualsMatch) {\n    return {\n      type: \"attrEquals\",\n      name: attrEqualsMatch[1],\n      value: attrEqualsMatch[2] ?? attrEqualsMatch[3] ?? \"\",\n    };\n  }\n\n  const attrExistsMatch = input.match(new RegExp(`^@(${attrName})$`));\n  if (attrExistsMatch) {\n    return { type: \"attrExists\", name: attrExistsMatch[1] };\n  }\n\n  const attrContainsMatch = input.match(\n    new RegExp(`^contains\\\\(\\\\s*@(${attrName})\\\\s*,\\\\s*${quoted}\\\\s*\\\\)$`),\n  );\n  if (attrContainsMatch) {\n    return {\n      type: \"attrContains\",\n      name: attrContainsMatch[1],\n      value: attrContainsMatch[2] ?? attrContainsMatch[3] ?? \"\",\n    };\n  }\n\n  const attrStartsMatch = input.match(\n    new RegExp(`^starts-with\\\\(\\\\s*@(${attrName})\\\\s*,\\\\s*${quoted}\\\\s*\\\\)$`),\n  );\n  if (attrStartsMatch) {\n    return {\n      type: \"attrStartsWith\",\n      name: attrStartsMatch[1],\n      value: attrStartsMatch[2] ?? attrStartsMatch[3] ?? \"\",\n    };\n  }\n\n  const textEqualsMatch = input.match(\n    new RegExp(`^(?:text\\\\(\\\\)|\\\\.)\\\\s*=\\\\s*${quoted}$`),\n  );\n  if (textEqualsMatch) {\n    return {\n      type: \"textEquals\",\n      value: textEqualsMatch[1] ?? textEqualsMatch[2] ?? \"\",\n    };\n  }\n\n  const textContainsMatch = input.match(\n    new RegExp(`^contains\\\\(\\\\s*(?:text\\\\(\\\\)|\\\\.)\\\\s*,\\\\s*${quoted}\\\\s*\\\\)$`),\n  );\n  if (textContainsMatch) {\n    return {\n      type: \"textContains\",\n      value: textContainsMatch[1] ?? textContainsMatch[2] ?? \"\",\n    };\n  }\n\n  if (valueMatch.test(input)) {\n    return null;\n  }\n\n  return null;\n}\n\nfunction splitTopLevel(input: string, keyword: string): string[] {\n  const parts: string[] = [];\n  let start = 0;\n  let depth = 0;\n  let quote: string | null = null;\n  let i = 0;\n\n  while (i < input.length) {\n    const ch = input[i];\n    if (quote) {\n      if (ch === quote) quote = null;\n      i += 1;\n      continue;\n    }\n\n    if (ch === \"'\" || ch === '\"') {\n      quote = ch;\n      i += 1;\n      continue;\n    }\n\n    if (ch === \"(\") {\n      depth += 1;\n      i += 1;\n      continue;\n    }\n\n    if (ch === \")\") {\n      depth = Math.max(0, depth - 1);\n      i += 1;\n      continue;\n    }\n\n    if (depth === 0 && isKeywordAt(input, i, keyword)) {\n      parts.push(input.slice(start, i).trim());\n      i += keyword.length;\n      start = i;\n      continue;\n    }\n\n    i += 1;\n  }\n\n  parts.push(input.slice(start).trim());\n  return parts.filter((part) => part.length > 0);\n}\n\nfunction isKeywordAt(input: string, index: number, keyword: string): boolean {\n  if (!input.startsWith(keyword, index)) return false;\n  const before = index > 0 ? input[index - 1] : \" \";\n  if (before === \"@\") return false;\n  const after =\n    index + keyword.length < input.length ? input[index + keyword.length] : \" \";\n  return isBoundary(before) && isBoundary(after);\n}\n\nfunction isBoundary(ch: string): boolean {\n  return !/[a-zA-Z0-9_.-]/.test(ch);\n}\n\nfunction unwrapFunctionCall(input: string, name: string): string | null {\n  const prefix = `${name}(`;\n  if (!input.startsWith(prefix) || !input.endsWith(\")\")) return null;\n  const inner = input.slice(prefix.length, -1);\n  return hasBalancedParens(inner) ? inner : null;\n}\n\nfunction hasBalancedParens(input: string): boolean {\n  let depth = 0;\n  let quote: string | null = null;\n  for (let i = 0; i < input.length; i += 1) {\n    const ch = input[i];\n    if (quote) {\n      if (ch === quote) quote = null;\n      continue;\n    }\n    if (ch === \"'\" || ch === '\"') {\n      quote = ch;\n      continue;\n    }\n    if (ch === \"(\") depth += 1;\n    else if (ch === \")\") depth -= 1;\n    if (depth < 0) return false;\n  }\n  return depth === 0;\n}\n\nconst normalizeSpace = (value: string): string =>\n  value.replace(/\\s+/g, \" \").trim();\n\nfunction textValue(element: Element): string {\n  return String(element.textContent ?? \"\");\n}\n\nfunction normalizeMaybe(value: string, normalize?: boolean): string {\n  return normalize ? normalizeSpace(value) : value;\n}\n\nexport function evaluatePredicate(\n  element: Element,\n  predicate: XPathPredicate,\n): boolean {\n  switch (predicate.type) {\n    case \"and\":\n      return predicate.predicates.every((p) => evaluatePredicate(element, p));\n    case \"or\":\n      return predicate.predicates.some((p) => evaluatePredicate(element, p));\n    case \"not\":\n      return !evaluatePredicate(element, predicate.predicate);\n    case \"attrExists\":\n      return element.getAttribute(predicate.name) !== null;\n    case \"attrEquals\": {\n      const attr = element.getAttribute(predicate.name);\n      if (attr === null) return false;\n      return (\n        normalizeMaybe(attr, predicate.normalize) ===\n        normalizeMaybe(predicate.value, predicate.normalize)\n      );\n    }\n    case \"attrContains\": {\n      const attr = element.getAttribute(predicate.name);\n      if (attr === null) return false;\n      return normalizeMaybe(attr, predicate.normalize).includes(\n        normalizeMaybe(predicate.value, predicate.normalize),\n      );\n    }\n    case \"attrStartsWith\": {\n      const attr = element.getAttribute(predicate.name);\n      if (attr === null) return false;\n      return normalizeMaybe(attr, predicate.normalize).startsWith(\n        normalizeMaybe(predicate.value, predicate.normalize),\n      );\n    }\n    case \"textEquals\": {\n      const value = normalizeMaybe(textValue(element), predicate.normalize);\n      return value === normalizeMaybe(predicate.value, predicate.normalize);\n    }\n    case \"textContains\": {\n      const value = normalizeMaybe(textValue(element), predicate.normalize);\n      return value.includes(\n        normalizeMaybe(predicate.value, predicate.normalize),\n      );\n    }\n    case \"index\":\n      return true;\n    default:\n      return true;\n  }\n}\n\nexport function applyPredicates(\n  elements: Element[],\n  predicates: XPathPredicate[],\n): Element[] {\n  let current = elements;\n  for (const predicate of predicates) {\n    if (!current.length) return [];\n\n    if (predicate.type === \"index\") {\n      const idx = predicate.index - 1;\n      current = idx >= 0 && idx < current.length ? [current[idx]!] : [];\n      continue;\n    }\n\n    current = current.filter((el) => evaluatePredicate(el, predicate));\n  }\n  return current;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/locatorScripts/xpathResolver.ts",
    "content": "import {\n  applyPredicates,\n  parseXPathSteps,\n  type XPathStep,\n} from \"./xpathParser.js\";\n\ntype ClosedRootGetter = (host: Element) => ShadowRoot | null;\n\nexport type XPathResolveOptions = {\n  pierceShadow?: boolean;\n};\n\ntype ShadowContext = {\n  getClosedRoot: ClosedRootGetter | null;\n  hasShadow: boolean;\n};\n\nconst normalizeXPath = (selector: string): string => {\n  const raw = String(selector ?? \"\").trim();\n  if (!raw) return \"\";\n  return raw.replace(/^xpath=/i, \"\").trim();\n};\n\nexport function resolveXPathFirst(\n  rawXp: string,\n  options?: XPathResolveOptions,\n): Element | null {\n  return resolveXPathAtIndex(rawXp, 0, options);\n}\n\nexport function resolveXPathAtIndex(\n  rawXp: string,\n  index: number,\n  options?: XPathResolveOptions,\n): Element | null {\n  if (!Number.isFinite(index) || index < 0) return null;\n  const xp = normalizeXPath(rawXp);\n  if (!xp) return null;\n\n  const targetIndex = Math.floor(index);\n  const pierceShadow = options?.pierceShadow !== false;\n  const shadowCtx = pierceShadow ? getShadowContext() : null;\n\n  if (!pierceShadow) {\n    return resolveNativeAtIndexWithError(xp, targetIndex).value;\n  }\n\n  if (!shadowCtx?.hasShadow) {\n    const native = resolveNativeAtIndexWithError(xp, targetIndex);\n    if (!native.error) return native.value;\n    const composed = resolveXPathComposedMatches(xp, shadowCtx?.getClosedRoot);\n    return composed[targetIndex] ?? null;\n  }\n\n  const composed = resolveXPathComposedMatches(xp, shadowCtx.getClosedRoot);\n  return composed[targetIndex] ?? null;\n}\n\nexport function countXPathMatches(\n  rawXp: string,\n  options?: XPathResolveOptions,\n): number {\n  const xp = normalizeXPath(rawXp);\n  if (!xp) return 0;\n\n  const pierceShadow = options?.pierceShadow !== false;\n  const shadowCtx = pierceShadow ? getShadowContext() : null;\n\n  if (!pierceShadow) {\n    return resolveNativeCountWithError(xp).count;\n  }\n\n  if (!shadowCtx?.hasShadow) {\n    const count = resolveNativeCountWithError(xp);\n    if (!count.error) return count.count;\n    return resolveXPathComposedMatches(xp, shadowCtx?.getClosedRoot).length;\n  }\n\n  return resolveXPathComposedMatches(xp, shadowCtx.getClosedRoot).length;\n}\n\nexport function resolveXPathComposedMatches(\n  rawXp: string,\n  getClosedRoot?: ClosedRootGetter | null,\n): Element[] {\n  const xp = normalizeXPath(rawXp);\n  if (!xp) return [];\n\n  const steps = parseXPathSteps(xp);\n  if (!steps.length) return [];\n\n  const closedRoot = getClosedRoot ?? null;\n\n  let current: Array<Document | Element | ShadowRoot | DocumentFragment> = [\n    document,\n  ];\n\n  for (const step of steps) {\n    const next: Element[] = [];\n    const seen = new Set<Element>();\n\n    for (const root of current) {\n      if (!root) continue;\n      const pool =\n        step.axis === \"child\"\n          ? composedChildren(root, closedRoot)\n          : composedDescendants(root, closedRoot);\n      if (!pool.length) continue;\n\n      const tagMatches = pool.filter((candidate) =>\n        matchesTag(candidate, step),\n      );\n      const matches = applyPredicates(tagMatches, step.predicates);\n\n      for (const candidate of matches) {\n        if (!seen.has(candidate)) {\n          seen.add(candidate);\n          next.push(candidate);\n        }\n      }\n    }\n\n    if (!next.length) return [];\n    current = next;\n  }\n\n  return current as Element[];\n}\n\nfunction matchesTag(element: Element, step: XPathStep): boolean {\n  if (step.tag === \"*\") return true;\n  return element.localName === step.tag;\n}\n\nfunction getShadowContext(): ShadowContext {\n  const backdoor = window.__stagehandV3__;\n  const getClosedRoot: ClosedRootGetter | null =\n    backdoor && typeof backdoor.getClosedRoot === \"function\"\n      ? (host: Element): ShadowRoot | null => {\n          try {\n            return backdoor.getClosedRoot(host) ?? null;\n          } catch {\n            return null;\n          }\n        }\n      : null;\n\n  let hasShadow = false;\n  try {\n    if (backdoor && typeof backdoor.stats === \"function\") {\n      const stats = backdoor.stats();\n      hasShadow = (stats?.open ?? 0) > 0 || (stats?.closed ?? 0) > 0;\n    }\n  } catch {\n    // ignore stats errors\n  }\n\n  if (!hasShadow) {\n    try {\n      const walker = document.createTreeWalker(\n        document,\n        NodeFilter.SHOW_ELEMENT,\n      );\n      while (walker.nextNode()) {\n        const el = walker.currentNode as Element;\n        if (el.shadowRoot) {\n          hasShadow = true;\n          break;\n        }\n      }\n    } catch {\n      // ignore scan errors\n    }\n  }\n\n  return { getClosedRoot, hasShadow };\n}\n\nfunction composedChildren(\n  node: Node | null | undefined,\n  getClosedRoot: ClosedRootGetter | null,\n): Element[] {\n  const out: Element[] = [];\n  if (!node) return out;\n\n  if (node instanceof Document) {\n    if (node.documentElement) out.push(node.documentElement);\n    return out;\n  }\n\n  if (node instanceof ShadowRoot || node instanceof DocumentFragment) {\n    out.push(...Array.from(node.children ?? []));\n    return out;\n  }\n\n  if (node instanceof Element) {\n    out.push(...Array.from(node.children ?? []));\n    const open = node.shadowRoot;\n    if (open) out.push(...Array.from(open.children ?? []));\n    if (getClosedRoot) {\n      const closed = getClosedRoot(node);\n      if (closed) out.push(...Array.from(closed.children ?? []));\n    }\n    return out;\n  }\n\n  return out;\n}\n\nfunction composedDescendants(\n  node: Node | null | undefined,\n  getClosedRoot: ClosedRootGetter | null,\n): Element[] {\n  const out: Element[] = [];\n  const seen = new Set<Element>();\n  const stack = [...composedChildren(node, getClosedRoot)].reverse();\n\n  while (stack.length) {\n    const next = stack.pop();\n    if (!next || seen.has(next)) continue;\n    seen.add(next);\n    out.push(next);\n\n    const children = composedChildren(next, getClosedRoot);\n    for (let i = children.length - 1; i >= 0; i -= 1) {\n      stack.push(children[i]!);\n    }\n  }\n\n  return out;\n}\n\nfunction resolveNativeAtIndexWithError(\n  xp: string,\n  index: number,\n): { value: Element | null; error: boolean } {\n  try {\n    const snapshot = document.evaluate(\n      xp,\n      document,\n      null,\n      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,\n      null,\n    );\n    return {\n      value: snapshot.snapshotItem(index) as Element | null,\n      error: false,\n    };\n  } catch {\n    return { value: null, error: true };\n  }\n}\n\nfunction resolveNativeCountWithError(xp: string): {\n  count: number;\n  error: boolean;\n} {\n  try {\n    const snapshot = document.evaluate(\n      xp,\n      document,\n      null,\n      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,\n      null,\n    );\n    return { count: snapshot.snapshotLength, error: false };\n  } catch {\n    return { count: 0, error: true };\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/piercer.entry.ts",
    "content": "import { installV3ShadowPiercer } from \"./piercer.runtime.js\";\n\ninstallV3ShadowPiercer({ debug: true, tagExisting: false });\n"
  },
  {
    "path": "packages/core/lib/v3/dom/piercer.runtime.ts",
    "content": "export interface V3ShadowPatchOptions {\n  debug?: boolean;\n  tagExisting?: boolean;\n}\n\nexport interface StagehandV3Backdoor {\n  /** Closed shadow-root accessors */\n  getClosedRoot(host: Element): ShadowRoot | undefined;\n  /** Stats + quick health check */\n  stats(): {\n    installed: true;\n    url: string;\n    isTop: boolean;\n    open: number;\n    closed: number;\n  };\n}\n\ntype V3InternalState = {\n  hostToRoot: WeakMap<Element, ShadowRoot>;\n  openCount: number;\n  closedCount: number;\n  debug: boolean;\n};\n\ndeclare global {\n  interface Window {\n    __stagehandV3Injected?: boolean;\n    __stagehandV3__?: StagehandV3Backdoor;\n  }\n}\n\nexport function installV3ShadowPiercer(opts: V3ShadowPatchOptions = {}): void {\n  // hardcoded debug (remove later if desired)\n  const DEBUG = true;\n\n  type PatchedFn = Element[\"attachShadow\"] & {\n    __v3Patched?: boolean;\n    __v3State?: V3InternalState;\n  };\n\n  const bindBackdoor = (state: V3InternalState): void => {\n    const { hostToRoot } = state;\n\n    window.__stagehandV3__ = {\n      getClosedRoot: (host: Element) => hostToRoot.get(host),\n      stats: () => ({\n        installed: true,\n        url: location.href,\n        isTop: window.top === window,\n        open: state.openCount,\n        closed: state.closedCount,\n      }),\n    } satisfies StagehandV3Backdoor;\n  };\n\n  // Look at the *current* function on the prototype. If it's already our patched\n  // function, reuse its shared state and rebind the backdoor (no new WeakMap).\n  const currentFn = Element.prototype.attachShadow as PatchedFn;\n  if (currentFn.__v3Patched && currentFn.__v3State) {\n    currentFn.__v3State.debug = DEBUG; // keep debug toggle consistent\n    bindBackdoor(currentFn.__v3State);\n    // idempotent: do not log \"installed\" again\n    return;\n  }\n\n  // First-time install: create shared state and replace the prototype method\n  const state: V3InternalState = {\n    hostToRoot: new WeakMap<Element, ShadowRoot>(),\n    openCount: 0,\n    closedCount: 0,\n    debug: DEBUG,\n  };\n\n  const original = currentFn; // keep a reference to call through\n  const patched: PatchedFn = function (\n    this: Element,\n    init: ShadowRootInit,\n  ): ShadowRoot {\n    const mode = init?.mode ?? \"open\";\n    const root = original.call(this, init);\n    try {\n      state.hostToRoot.set(this, root);\n      if (mode === \"closed\") state.closedCount++;\n      else state.openCount++;\n      if (state.debug) {\n        console.info(\"[v3-piercer] attachShadow\", {\n          tag: (this as Element).tagName?.toLowerCase() ?? \"\",\n          mode,\n          url: location.href,\n        });\n      }\n    } catch {\n      //\n    }\n    return root;\n  } as PatchedFn;\n\n  // Mark the *patched* function with metadata so re-entry sees it\n  patched.__v3Patched = true;\n  patched.__v3State = state;\n\n  Object.defineProperty(Element.prototype, \"attachShadow\", {\n    configurable: true,\n    writable: true,\n    value: patched,\n  });\n\n  // Optionally tag existing open roots (closed cannot be discovered post-hoc)\n  if (opts.tagExisting) {\n    try {\n      const walker = document.createTreeWalker(\n        document,\n        NodeFilter.SHOW_ELEMENT,\n      );\n      while (walker.nextNode()) {\n        const el = walker.currentNode as Element;\n        if (el.shadowRoot) {\n          state.hostToRoot.set(el, el.shadowRoot);\n          state.openCount++;\n        }\n      }\n    } catch {\n      //\n    }\n  }\n\n  window.__stagehandV3Injected = true;\n  bindBackdoor(state);\n\n  if (state.debug) {\n    console.info(\"[v3-piercer] installed\", {\n      url: location.href,\n      isTop: window.top === window,\n      readyState: document.readyState,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/rerenderMissingShadows.entry.ts",
    "content": "import { rerenderMissingShadowHosts } from \"./rerenderMissingShadows.runtime.js\";\n\nrerenderMissingShadowHosts();\n"
  },
  {
    "path": "packages/core/lib/v3/dom/rerenderMissingShadows.runtime.ts",
    "content": "export function rerenderMissingShadowHosts(): void {\n  try {\n    const piercer = window.__stagehandV3__;\n    if (!piercer || typeof piercer.getClosedRoot !== \"function\") return;\n\n    const needsReset: Element[] = [];\n    const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT);\n    while (walker.nextNode()) {\n      const el = walker.currentNode as Element;\n      const tag = el.tagName?.toLowerCase() ?? \"\";\n      if (!tag.includes(\"-\")) continue;\n      if (typeof customElements?.get !== \"function\") continue;\n      if (!customElements.get(tag)) continue;\n      const hasOpen = !!el.shadowRoot;\n      const hasClosed = !!piercer.getClosedRoot(el);\n      if (hasOpen || hasClosed) continue;\n      needsReset.push(el);\n    }\n\n    for (const host of needsReset) {\n      try {\n        const clone = host.cloneNode(true);\n        host.replaceWith(clone);\n      } catch {\n        // ignore individual failures\n      }\n    }\n\n    if (piercer.stats && needsReset.length) {\n      console.info(\"[v3-piercer] rerender\", { count: needsReset.length });\n    }\n  } catch (err) {\n    console.info(\"[v3-piercer] rerender error\", { message: String(err ?? \"\") });\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/dom/screenshotScripts/index.ts",
    "content": "export { resolveMaskRect } from \"./resolveMaskRect.js\";\n"
  },
  {
    "path": "packages/core/lib/v3/dom/screenshotScripts/resolveMaskRect.ts",
    "content": "export type MaskRect = {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  rootToken?: string | null;\n};\n\nexport function resolveMaskRect(\n  this: Element | null,\n  maskToken?: string,\n): MaskRect | null {\n  function safeClosest(el: Element | null, selector: string): Element | null {\n    try {\n      return el && typeof el.closest === \"function\"\n        ? el.closest(selector)\n        : null;\n    } catch {\n      return null;\n    }\n  }\n\n  function safeMatches(el: Element | null, selector: string): boolean {\n    try {\n      return !!el && typeof el.matches === \"function\" && el.matches(selector);\n    } catch {\n      return false;\n    }\n  }\n\n  function findTopLayerRoot(el: Element | null): Element | null {\n    const dialog = safeClosest(el, \"dialog[open]\");\n    if (dialog) return dialog;\n    const popover = safeClosest(el, \"[popover]\");\n    if (popover && safeMatches(popover, \":popover-open\")) return popover;\n    return null;\n  }\n\n  if (!this || typeof this.getBoundingClientRect !== \"function\") return null;\n  const rect = this.getBoundingClientRect();\n  if (!rect) return null;\n  const style = window.getComputedStyle(this);\n  if (!style) return null;\n  if (style.visibility === \"hidden\" || style.display === \"none\") return null;\n  if (rect.width <= 0 || rect.height <= 0) return null;\n\n  const root = findTopLayerRoot(this);\n  if (root) {\n    const rootRect = root.getBoundingClientRect();\n    if (!rootRect) return null;\n    let rootToken: string | null = null;\n    if (maskToken) {\n      try {\n        const existing = root.getAttribute(\"data-stagehand-mask-root\");\n        if (existing && existing.startsWith(maskToken)) {\n          rootToken = existing;\n        } else {\n          rootToken =\n            maskToken + \"_root_\" + Math.random().toString(36).slice(2);\n          root.setAttribute(\"data-stagehand-mask-root\", rootToken);\n        }\n      } catch {\n        rootToken = null;\n      }\n    }\n    return {\n      x:\n        rect.left -\n        rootRect.left -\n        (root.clientLeft || 0) +\n        (root.scrollLeft || 0),\n      y:\n        rect.top - rootRect.top - (root.clientTop || 0) + (root.scrollTop || 0),\n      width: rect.width,\n      height: rect.height,\n      rootToken,\n    };\n  }\n\n  return {\n    x: rect.left + window.scrollX,\n    y: rect.top + window.scrollY,\n    width: rect.width,\n    height: rect.height,\n    rootToken: null,\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/external_clients/aisdk.ts",
    "content": "import {\n  CoreAssistantMessage,\n  ModelMessage,\n  CoreSystemMessage,\n  Tool,\n  CoreUserMessage,\n  generateObject,\n  generateText,\n  ImagePart,\n  TextPart,\n} from \"ai\";\nimport type { LanguageModelV2 } from \"@ai-sdk/provider\";\nimport { CreateChatCompletionOptions, LLMClient } from \"../llm/LLMClient.js\";\nimport { AvailableModel } from \"../types/public/index.js\";\nimport { ChatCompletion } from \"openai/resources\";\n\nexport class AISdkClient extends LLMClient {\n  public type = \"aisdk\" as const;\n  private model: LanguageModelV2;\n\n  constructor({ model }: { model: LanguageModelV2 }) {\n    super(model.modelId as AvailableModel);\n    this.model = model;\n  }\n\n  async createChatCompletion<T = ChatCompletion>({\n    options,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const formattedMessages: ModelMessage[] = options.messages.map(\n      (message) => {\n        if (Array.isArray(message.content)) {\n          if (message.role === \"system\") {\n            const systemMessage: CoreSystemMessage = {\n              role: \"system\",\n              content: message.content\n                .map((c) => (\"text\" in c ? c.text : \"\"))\n                .join(\"\\n\"),\n            };\n            return systemMessage;\n          }\n\n          const contentParts = message.content.map((content) => {\n            if (\"image_url\" in content) {\n              const imageContent: ImagePart = {\n                type: \"image\",\n                image: content.image_url.url,\n              };\n              return imageContent;\n            } else {\n              const textContent: TextPart = {\n                type: \"text\",\n                text: content.text,\n              };\n              return textContent;\n            }\n          });\n\n          if (message.role === \"user\") {\n            const userMessage: CoreUserMessage = {\n              role: \"user\",\n              content: contentParts,\n            };\n            return userMessage;\n          } else {\n            const textOnlyParts = contentParts.map((part) => ({\n              type: \"text\" as const,\n              text: part.type === \"image\" ? \"[Image]\" : part.text,\n            }));\n            const assistantMessage: CoreAssistantMessage = {\n              role: \"assistant\",\n              content: textOnlyParts,\n            };\n            return assistantMessage;\n          }\n        }\n\n        return {\n          role: message.role,\n          content: message.content,\n        };\n      },\n    );\n\n    if (options.response_model) {\n      const response = await generateObject({\n        model: this.model,\n        messages: formattedMessages,\n        schema: options.response_model.schema,\n      });\n\n      return {\n        data: response.object,\n        usage: {\n          prompt_tokens: response.usage.inputTokens ?? 0,\n          completion_tokens: response.usage.outputTokens ?? 0,\n          reasoning_tokens: response.usage.reasoningTokens ?? 0,\n          cached_input_tokens: response.usage.cachedInputTokens ?? 0,\n          total_tokens: response.usage.totalTokens ?? 0,\n        },\n      } as T;\n    }\n\n    const tools: Record<string, Tool> = {};\n\n    for (const rawTool of options.tools) {\n      tools[rawTool.name] = {\n        description: rawTool.description,\n        inputSchema: rawTool.parameters,\n      } as Tool;\n    }\n\n    const response = await generateText({\n      model: this.model,\n      messages: formattedMessages,\n      tools,\n    });\n\n    return {\n      data: response.text,\n      usage: {\n        prompt_tokens: response.usage.inputTokens ?? 0,\n        completion_tokens: response.usage.outputTokens ?? 0,\n        reasoning_tokens: response.usage.reasoningTokens ?? 0,\n        cached_input_tokens: response.usage.cachedInputTokens ?? 0,\n        total_tokens: response.usage.totalTokens ?? 0,\n      },\n    } as T;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/external_clients/customOpenAI.ts",
    "content": "/**\n * Welcome to the Stagehand custom OpenAI client!\n *\n * This is a client for models that are compatible with the OpenAI API, like Ollama, Gemini, etc.\n * You can just pass in an OpenAI instance to the client and it will work.\n */\n\nimport type { AvailableModel } from \"../types/public/model.js\";\nimport { CreateChatCompletionOptions, LLMClient } from \"../llm/LLMClient.js\";\nimport OpenAI from \"openai\";\nimport type {\n  ChatCompletion,\n  ChatCompletionAssistantMessageParam,\n  ChatCompletionContentPartImage,\n  ChatCompletionContentPartText,\n  ChatCompletionCreateParamsNonStreaming,\n  ChatCompletionMessageParam,\n  ChatCompletionSystemMessageParam,\n  ChatCompletionUserMessageParam,\n} from \"openai/resources/chat/completions\";\nimport { toJsonSchema } from \"../zodCompat.js\";\nimport { validateZodSchema } from \"../../utils.js\";\nimport {\n  CreateChatCompletionResponseError,\n  ZodSchemaValidationError,\n} from \"../types/public/sdkErrors.js\";\n\nexport class CustomOpenAIClient extends LLMClient {\n  public type = \"openai\" as const;\n  private client: OpenAI;\n\n  constructor({ modelName, client }: { modelName: string; client: OpenAI }) {\n    super(modelName as AvailableModel);\n    this.client = client;\n    this.modelName = modelName as AvailableModel;\n  }\n\n  async createChatCompletion<T = ChatCompletion>({\n    options,\n    retries = 3,\n    logger,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const { image, requestId, ...optionsWithoutImageAndRequestId } = options;\n\n    // TODO: Implement vision support\n    if (image) {\n      console.warn(\n        \"Image provided. Vision is not currently supported for openai\",\n      );\n    }\n\n    logger({\n      category: \"openai\",\n      message: \"creating chat completion\",\n      level: 1,\n      auxiliary: {\n        options: {\n          value: JSON.stringify({\n            ...optionsWithoutImageAndRequestId,\n            requestId,\n          }),\n          type: \"object\",\n        },\n        modelName: {\n          value: this.modelName,\n          type: \"string\",\n        },\n      },\n    });\n\n    let responseFormat:\n      | ChatCompletionCreateParamsNonStreaming[\"response_format\"]\n      | undefined;\n    if (options.response_model) {\n      responseFormat = {\n        type: \"json_object\",\n      };\n    }\n\n    /* eslint-disable */\n    // Remove unsupported options\n    const { response_model, ...openaiOptions } = {\n      ...optionsWithoutImageAndRequestId,\n      model: this.modelName,\n    };\n\n    logger({\n      category: \"openai\",\n      message: \"creating chat completion\",\n      level: 1,\n      auxiliary: {\n        openaiOptions: {\n          value: JSON.stringify(openaiOptions),\n          type: \"object\",\n        },\n      },\n    });\n\n    const formattedMessages: ChatCompletionMessageParam[] =\n      options.messages.map((message) => {\n        if (Array.isArray(message.content)) {\n          const contentParts = message.content.map((content) => {\n            if (\"image_url\" in content) {\n              const imageContent: ChatCompletionContentPartImage = {\n                image_url: {\n                  url: content.image_url.url,\n                },\n                type: \"image_url\",\n              };\n              return imageContent;\n            } else {\n              const textContent: ChatCompletionContentPartText = {\n                text: content.text,\n                type: \"text\",\n              };\n              return textContent;\n            }\n          });\n\n          if (message.role === \"system\") {\n            const formattedMessage: ChatCompletionSystemMessageParam = {\n              ...message,\n              role: \"system\",\n              content: contentParts.filter(\n                (content): content is ChatCompletionContentPartText =>\n                  content.type === \"text\",\n              ),\n            };\n            return formattedMessage;\n          } else if (message.role === \"user\") {\n            const formattedMessage: ChatCompletionUserMessageParam = {\n              ...message,\n              role: \"user\",\n              content: contentParts,\n            };\n            return formattedMessage;\n          } else {\n            const formattedMessage: ChatCompletionAssistantMessageParam = {\n              ...message,\n              role: \"assistant\",\n              content: contentParts.filter(\n                (content): content is ChatCompletionContentPartText =>\n                  content.type === \"text\",\n              ),\n            };\n            return formattedMessage;\n          }\n        }\n\n        return {\n          ...message,\n          content: message.content,\n        } as ChatCompletionMessageParam;\n      });\n\n    if (options.response_model) {\n      const schemaJson = JSON.stringify(\n        toJsonSchema(options.response_model.schema),\n        null,\n        2,\n      );\n      formattedMessages.push({\n        role: \"user\",\n        content: `Respond with valid JSON matching this schema:\\n${schemaJson}\\n\\nDo not include any other text, formatting or markdown in your output. Do not include \\`\\`\\` or \\`\\`\\`json in your response. Only the JSON object itself.`,\n      });\n    }\n\n    const body: ChatCompletionCreateParamsNonStreaming = {\n      ...openaiOptions,\n      model: this.modelName,\n      messages: formattedMessages,\n      response_format: responseFormat,\n      stream: false,\n      tools: options.tools?.map((tool) => ({\n        function: {\n          name: tool.name,\n          description: tool.description,\n          parameters: tool.parameters,\n        },\n        type: \"function\",\n      })),\n    };\n\n    const response = await this.client.chat.completions.create(body);\n\n    logger({\n      category: \"openai\",\n      message: \"response\",\n      level: 1,\n      auxiliary: {\n        response: {\n          value: JSON.stringify(response),\n          type: \"object\",\n        },\n        requestId: {\n          value: requestId,\n          type: \"string\",\n        },\n      },\n    });\n\n    if (options.response_model) {\n      const extractedData = response.choices[0].message.content;\n      if (!extractedData) {\n        throw new CreateChatCompletionResponseError(\"No content in response\");\n      }\n\n      let parsedData: unknown;\n      try {\n        parsedData = JSON.parse(extractedData);\n        validateZodSchema(options.response_model.schema, parsedData);\n      } catch (e) {\n        const isParseError = e instanceof SyntaxError;\n        logger({\n          category: \"openai\",\n          message: isParseError\n            ? \"Response is not valid JSON\"\n            : \"Response failed Zod schema validation\",\n          level: 0,\n        });\n        if (retries > 0) {\n          return this.createChatCompletion({\n            options,\n            logger,\n            retries: retries - 1,\n          });\n        }\n\n        if (e instanceof ZodSchemaValidationError) {\n          logger({\n            category: \"openai\",\n            message: `Error during chat completion: ${e.message}`,\n            level: 0,\n            auxiliary: {\n              errorDetails: {\n                value: `Message: ${e.message}${e.stack ? \"\\nStack: \" + e.stack : \"\"}`,\n                type: \"string\",\n              },\n              requestId: { value: requestId, type: \"string\" },\n            },\n          });\n          throw new CreateChatCompletionResponseError(e.message);\n        }\n        throw new CreateChatCompletionResponseError(\n          isParseError\n            ? \"Failed to parse model response as JSON\"\n            : e instanceof Error\n              ? e.message\n              : \"Unknown error during response processing\",\n        );\n      }\n\n      return {\n        data: parsedData,\n        usage: {\n          prompt_tokens: response.usage?.prompt_tokens ?? 0,\n          completion_tokens: response.usage?.completion_tokens ?? 0,\n          total_tokens: response.usage?.total_tokens ?? 0,\n        },\n      } as T;\n    }\n\n    return {\n      data: response.choices[0].message.content,\n      usage: {\n        prompt_tokens: response.usage?.prompt_tokens ?? 0,\n        completion_tokens: response.usage?.completion_tokens ?? 0,\n        total_tokens: response.usage?.total_tokens ?? 0,\n      },\n    } as T;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/flowlogger/EventEmitter.ts",
    "content": "import { EventEmitter } from \"node:events\";\n\ntype WildcardEventListener = (...args: unknown[]) => void;\n\nexport class EventEmitterWithWildcardSupport extends EventEmitter {\n  private readonly wildcardListeners = new Set<WildcardEventListener>();\n\n  override on(\n    eventName: string | symbol,\n    listener: (...args: unknown[]) => void,\n  ): this {\n    if (eventName === \"*\") {\n      this.wildcardListeners.add(listener);\n      return this;\n    }\n\n    return super.on(eventName, listener);\n  }\n\n  override off(\n    eventName: string | symbol,\n    listener: (...args: unknown[]) => void,\n  ): this {\n    if (eventName === \"*\") {\n      this.wildcardListeners.delete(listener);\n      return this;\n    }\n\n    return super.off(eventName, listener);\n  }\n\n  override emit(eventName: string | symbol, ...args: unknown[]): boolean {\n    const handled = super.emit(eventName, ...args);\n\n    for (const listener of this.wildcardListeners) {\n      listener(...args);\n    }\n\n    return handled || this.wildcardListeners.size > 0;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/flowlogger/EventSink.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { FlowEvent } from \"./FlowLogger.js\";\nimport type { EventStoreApi, EventStoreQuery } from \"./EventStore.js\";\nimport {\n  prettifyColorStderrLine,\n  prettifyEvent,\n  prettifyIsCdpEvent,\n  prettifySanitizeEvent,\n} from \"./prettify.js\";\n\n// =============================================================================\n// Event Sink Contracts\n// =============================================================================\n\nexport interface EventSink {\n  emit(event: FlowEvent): Promise<void>;\n  query(query: EventStoreQuery): Promise<FlowEvent[]>;\n  destroy(): Promise<void>;\n}\n\n// Checks whether an event matches a query used by queryable sinks. `eventId` matches both the event itself and descendants of that event.\nfunction matchesEventStoreQuery(\n  event: FlowEvent,\n  query: EventStoreQuery,\n): boolean {\n  if (query.sessionId && event.sessionId !== query.sessionId) return false;\n\n  if (query.eventId) {\n    const matchesEvent =\n      event.eventId === query.eventId ||\n      event.eventParentIds.includes(query.eventId);\n    if (!matchesEvent) {\n      return false;\n    }\n  }\n\n  if (query.eventType) {\n    const pattern = new RegExp(\n      `^${query.eventType\n        .replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n        .replace(/\\\\\\*/g, \".*\")}$`,\n    );\n    if (!pattern.test(event.eventType)) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\n// =============================================================================\n// File Sink Helpers\n// =============================================================================\n\n// Returns true when a file sink's stream is still open and writable.\nfunction isWritable(stream: fs.WriteStream | null): stream is fs.WriteStream {\n  return !!(stream && !stream.destroyed && stream.writable);\n}\n\n// Writes a serialized event to a file sink and converts callback-style stream completion into a promise.\nfunction writeToStream(stream: fs.WriteStream, value: string): Promise<void> {\n  return new Promise<void>((resolve, reject) => {\n    try {\n      stream.write(value, (error?: Error | null) => {\n        if (error) {\n          reject(error);\n          return;\n        }\n        resolve();\n      });\n    } catch (error) {\n      reject(error);\n    }\n  });\n}\n\n// =============================================================================\n// Event Sink Implementations\n// =============================================================================\n\nabstract class FileEventSink implements EventSink {\n  private readonly streamPromise: Promise<fs.WriteStream | null>; // Lazily opens the one file stream owned by this sink when the session directory resolves.\n\n  // Creates a best-effort file sink bound to a single session directory.\n  constructor(sessionDirPromise: Promise<string | null>, fileName: string) {\n    this.streamPromise = sessionDirPromise.then((sessionDir) =>\n      sessionDir\n        ? fs.createWriteStream(path.join(sessionDir, fileName), { flags: \"a\" })\n        : null,\n    );\n  }\n\n  protected abstract serialize(event: FlowEvent): Promise<string | null>;\n\n  // Serializes and appends a single event. File sinks are intentionally best-effort and never allowed to affect library execution flow.\n  async emit(event: FlowEvent): Promise<void> {\n    try {\n      const stream = await this.streamPromise;\n      if (!isWritable(stream)) {\n        return;\n      }\n\n      const serialized = await this.serialize(event);\n      if (!serialized) {\n        return;\n      }\n\n      await writeToStream(stream, serialized);\n    } catch {\n      // best effort only\n    }\n  }\n\n  // File sinks are write-only and do not support query reads.\n  async query(): Promise<FlowEvent[]> {\n    return [];\n  }\n\n  // Closes the underlying file stream when the owning store shuts down.\n  async destroy(): Promise<void> {\n    const stream = await this.streamPromise.catch((): null => null);\n    if (!isWritable(stream)) {\n      return;\n    }\n\n    await new Promise<void>((resolve) => {\n      stream.end(resolve);\n    });\n  }\n}\n\nexport class JsonlFileEventSink extends FileEventSink {\n  // Writes full verbatim events to `session_events.jsonl`.\n  constructor(sessionDirPromise: Promise<string | null>) {\n    super(sessionDirPromise, \"session_events.jsonl\");\n  }\n\n  // Serializes the full event for lossless machine-readable storage.\n  protected async serialize(event: FlowEvent): Promise<string> {\n    return `${JSON.stringify(event)}\\n`;\n  }\n}\n\nexport class PrettyLogFileEventSink extends FileEventSink {\n  // Writes human-readable pretty lines to `session_events.log`.\n  constructor(\n    sessionDirPromise: Promise<string | null>,\n    private readonly store: Pick<EventStoreApi, \"query\">, // Queried during prettification so each line can recover recent ancestry tags.\n  ) {\n    super(sessionDirPromise, \"session_events.log\");\n  }\n\n  // Pretty-prints the event using recent in-memory ancestry.\n  protected async serialize(event: FlowEvent): Promise<string | null> {\n    const line = await prettifyEvent(this.store, prettifySanitizeEvent(event));\n    return line ? `${line}\\n` : null;\n  }\n}\n\nexport class PrettyStderrEventSink implements EventSink {\n  // Writes pretty lines to stderr for verbose local debugging. CDP events are intentionally omitted here to keep stderr high-signal.\n  constructor(private readonly store: Pick<EventStoreApi, \"query\">) {} // Queried during prettification so stderr lines can include recent ancestry tags.\n\n  // Best-effort stderr writer used only for interactive debugging output.\n  async emit(event: FlowEvent): Promise<void> {\n    try {\n      if (prettifyIsCdpEvent(event)) {\n        return;\n      }\n\n      const line = await prettifyEvent(\n        this.store,\n        prettifySanitizeEvent(event),\n      );\n      if (!line) {\n        return;\n      }\n\n      await new Promise<void>((resolve, reject) => {\n        try {\n          process.stderr.write(\n            `${prettifyColorStderrLine(line)}\\n`,\n            (error?: Error | null) => {\n              if (error) {\n                reject(error);\n                return;\n              }\n              resolve();\n            },\n          );\n        } catch (error) {\n          reject(error);\n        }\n      });\n    } catch {\n      // best effort only\n    }\n  }\n\n  // Stderr sink is write-only and does not support query reads.\n  async query(): Promise<FlowEvent[]> {\n    return [];\n  }\n\n  // No teardown is required for stderr.\n  async destroy(): Promise<void> {}\n}\n\nexport class InMemoryEventSink implements EventSink {\n  // Retains recent events for query lookups. Tests usually attach this sink explicitly when they need full historical payloads.\n  constructor(protected readonly limit = Infinity) {}\n\n  protected readonly events: FlowEvent[] = []; // Retained history; `emit()` appends to it and trims old entries when `limit` is exceeded.\n\n  // Gives subclasses a hook to transform events before they are retained.\n  protected storeEvent(event: FlowEvent): FlowEvent {\n    return event;\n  }\n\n  // Stores a new event and trims the oldest retained entries once the sink exceeds its configured limit.\n  async emit(event: FlowEvent): Promise<void> {\n    this.events.push(this.storeEvent(event));\n    if (this.events.length > this.limit) {\n      this.events.splice(0, this.events.length - this.limit);\n    }\n  }\n\n  // Returns retained events that match the query, ordered by creation time.\n  async query(query: EventStoreQuery): Promise<FlowEvent[]> {\n    const filtered = this.events.filter((event) =>\n      matchesEventStoreQuery(event, query),\n    );\n    filtered.sort((left, right) => {\n      const createdAtOrder = left.eventCreatedAt.localeCompare(\n        right.eventCreatedAt,\n      );\n      if (createdAtOrder !== 0) {\n        return createdAtOrder;\n      }\n\n      return left.eventId.localeCompare(right.eventId);\n    });\n    return query.limit ? filtered.slice(-query.limit) : filtered;\n  }\n\n  // Clears retained history when the owning store shuts down.\n  async destroy(): Promise<void> {\n    this.events.length = 0;\n  }\n}\n\nexport class ShallowInMemoryEventSink extends InMemoryEventSink {\n  // Retains only ancestry metadata for the default query sink so verbose or long-running sessions do not hold onto large payloads such as screenshots.\n  protected override storeEvent(event: FlowEvent): FlowEvent {\n    return new FlowEvent({\n      eventType: event.eventType,\n      eventId: event.eventId,\n      eventCreatedAt: event.eventCreatedAt,\n      sessionId: event.sessionId,\n      eventParentIds: [...event.eventParentIds],\n      data: {},\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/flowlogger/EventStore.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { V3Options } from \"../types/public/index.js\";\nimport {\n  EventSink,\n  JsonlFileEventSink,\n  PrettyLogFileEventSink,\n  PrettyStderrEventSink,\n  ShallowInMemoryEventSink,\n} from \"./EventSink.js\";\nimport { FlowEvent } from \"./FlowLogger.js\";\n\nconst DEFAULT_IN_MEMORY_EVENT_LIMIT = 500; // Per-session ancestry window retained by the default shallow query sink.\nconst CONFIG_DIR = process.env.BROWSERBASE_CONFIG_DIR || \"\"; // Base directory for session metadata + file-backed flow logs.\nconst FLOW_LOGS_ENABLED = process.env.BROWSERBASE_FLOW_LOGS === \"1\"; // Force-enables the pretty stderr flow sink even when `verbose !== 2`.\nconst SENSITIVE_KEYS =\n  /key|secret|token|api-key|apikey|api_key|password|passwd|pwd|credential|auth/i; // Redacts obvious secrets before session options are written to disk.\n\n// =============================================================================\n// Public Contracts\n// =============================================================================\n\nexport interface EventStoreQuery {\n  sessionId?: string;\n  eventId?: string;\n  eventType?: string;\n  limit?: number;\n}\n\nexport interface EventStoreApi {\n  readonly sessionId: string;\n  emit(event: FlowEvent): Promise<void>;\n  query(query: EventStoreQuery): Promise<FlowEvent[]>;\n  destroy(): Promise<void>;\n}\n\n// =============================================================================\n// Filesystem Helpers\n// =============================================================================\n\n// Redacts secrets before session options are written to `session.json` inside a config-dir-backed session directory.\nfunction sanitizeOptions(options: V3Options): Record<string, unknown> {\n  const sanitize = (value: unknown): unknown => {\n    if (typeof value !== \"object\" || value === null) return value;\n    if (Array.isArray(value)) return value.map(sanitize);\n\n    const result: Record<string, unknown> = {};\n    for (const [key, entry] of Object.entries(value)) {\n      result[key] = SENSITIVE_KEYS.test(key) ? \"******\" : sanitize(entry);\n    }\n    return result;\n  };\n\n  return sanitize({ ...options }) as Record<string, unknown>;\n}\n\n// Resolves the configured Browserbase config directory used by file sinks.\nexport function getConfigDir(): string {\n  return CONFIG_DIR ? path.resolve(CONFIG_DIR) : \"\";\n}\n\n// Creates the per-session directory used by file sinks and writes best-effort metadata such as the sanitized `session.json` file and `latest` symlink.\nasync function createSessionDir(\n  sessionId: string,\n  options?: V3Options,\n): Promise<string | null> {\n  const configDir = getConfigDir();\n  if (!configDir) {\n    return null;\n  }\n\n  const sessionDir = path.join(configDir, \"sessions\", sessionId);\n  await fs.promises.mkdir(sessionDir, { recursive: true });\n\n  if (options) {\n    await fs.promises.writeFile(\n      path.join(sessionDir, \"session.json\"),\n      JSON.stringify(sanitizeOptions(options), null, 2),\n      \"utf-8\",\n    );\n  }\n\n  const latestLink = path.join(configDir, \"sessions\", \"latest\");\n  try {\n    try {\n      await fs.promises.unlink(latestLink);\n    } catch {\n      // ignore missing link\n    }\n    await fs.promises.symlink(sessionId, latestLink, \"dir\");\n  } catch {\n    // symlink best effort only\n  }\n\n  return sessionDir;\n}\n\n// =============================================================================\n// Event Store\n// =============================================================================\n\n// Per-session flow event sink manager.\n// This is not an event bus. V3 forwards already-emitted FlowEvents into it so\n// the store can fan them out to configured sinks, answer `query()` calls from\n// its one query sink, and tear down its sinks when the session closes.\n// We keep this as a separate object instead of wiring sinks directly with\n// `v3.bus.on(\"*\", sink.emit)` because pretty sinks need access to a shared\n// query interface while rendering. Prettified lines often need to look up\n// related parent/child events to recover the readable ancestry tags and labels.\n// Passing sinks into each other to share that state gets messy quickly, so the\n// EventStore contains the circular dependency: all sinks live here, and any\n// sink that needs historical context can call the one `EventStore.query()`\n// entrypoint backed by the main query sink for this session.\nexport class EventStore implements EventStoreApi {\n  private readonly sinks = new Set<EventSink>(); // All sinks attached for this session; constructor registers them here and `destroy()` tears them down.\n  private destroyed = false; // Flipped by `destroy()` so later emits and teardown calls become no-ops.\n  public query: (query: EventStoreQuery) => Promise<FlowEvent[]>; // Always reads from the one query sink chosen at construction time.\n\n  // Creates the per-instance store owned by a single V3 session. This store is intentionally single-session; it ignores events for other session ids.\n  constructor(\n    // Usually matches `browserbaseSessionId` today, but it is the store's own Stagehand session identifier and may diverge in the future.\n    public readonly sessionId: string,\n    options?: V3Options,\n    querySink: EventSink = new ShallowInMemoryEventSink(\n      DEFAULT_IN_MEMORY_EVENT_LIMIT,\n    ),\n  ) {\n    const sessionDirPromise = createSessionDir(sessionId, options);\n\n    this.registerSink(querySink);\n    this.query = async (query) => {\n      if (query.sessionId && query.sessionId !== this.sessionId) {\n        return [];\n      }\n\n      return querySink.query({\n        ...query,\n        sessionId: this.sessionId,\n      });\n    };\n\n    if (getConfigDir()) {\n      this.registerSink(new JsonlFileEventSink(sessionDirPromise));\n      this.registerSink(new PrettyLogFileEventSink(sessionDirPromise, this));\n    }\n\n    if (FLOW_LOGS_ENABLED) {\n      this.registerSink(new PrettyStderrEventSink(this));\n    }\n  }\n\n  // Adds a sink to the direct fanout list used by `emit()`.\n  private registerSink(sink: EventSink): void {\n    this.sinks.add(sink);\n  }\n\n  // Emits an event to all attached sinks when it belongs to this store's single session.\n  emit = async (event: FlowEvent): Promise<void> => {\n    if (!(event instanceof FlowEvent)) {\n      return;\n    }\n\n    if (this.destroyed || event.sessionId !== this.sessionId) {\n      return;\n    }\n\n    await Promise.allSettled([...this.sinks].map((sink) => sink.emit(event)));\n  };\n\n  // Tears down all sinks when the V3 instance is closed.\n  async destroy(): Promise<void> {\n    if (this.destroyed) {\n      return;\n    }\n\n    this.destroyed = true;\n    await Promise.all(\n      [...this.sinks].map((sink) =>\n        sink.destroy().catch(() => {\n          // best effort cleanup\n        }),\n      ),\n    );\n    this.sinks.clear();\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/flowlogger/FlowLogger.ts",
    "content": "import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { v7 as uuidv7 } from \"uuid\";\nimport type { LanguageModelMiddleware } from \"ai\";\nimport { z } from \"zod\";\nimport { EventEmitterWithWildcardSupport } from \"./EventEmitter.js\";\n\n// =============================================================================\n// Flow Event Model\n// =============================================================================\n\nexport const FlowEventDataSchema = z.record(z.string(), z.unknown());\nexport const FlowEventInputSchema = z.object({\n  eventType: z.string(),\n  eventId: z.string().optional(),\n  eventParentIds: z.array(z.string()).optional(),\n  eventCreatedAt: z.string().optional(),\n  sessionId: z.string().optional(),\n  data: FlowEventDataSchema.optional(),\n});\n\nexport type FlowEventData = z.infer<typeof FlowEventDataSchema>;\nexport type FlowEventInput = z.input<typeof FlowEventInputSchema>;\n\n// the same as FlowEventInput, but with all fields required (non-optional)\ntype FlowEventFields = Omit<\n  FlowEventInput,\n  \"eventId\" | \"eventParentIds\" | \"eventCreatedAt\" | \"sessionId\" | \"data\"\n> & {\n  eventId: string;\n  eventParentIds: string[];\n  eventCreatedAt: string;\n  sessionId: string;\n  data: FlowEventData;\n};\n\nexport class FlowEvent implements FlowEventFields {\n  // \"ModuleMethodSomethingEvent\" -> hashToSmallInt(\"Modu) -> 5. eventId = \"...5\"\n  private static deriveEventIdSuffix(eventType: string): string {\n    const prefixMatch = eventType.match(/^[A-Z][a-z0-9]*/);\n    const prefix = prefixMatch?.[0] ?? eventType.slice(0, 4);\n\n    let hash = 0;\n    for (const ch of prefix.slice(0, 4)) {\n      hash = (hash * 31 + ch.charCodeAt(0)) % 10;\n    }\n    return String(hash); // e.g. \"0\" or \"9\"\n  }\n\n  // Builds a sortable UUID-like event id while preserving a stable, human-friendly suffix derived from the event family.\n  static createEventId(eventType: string): string {\n    const rawEventId = uuidv7();\n    return `${rawEventId.slice(0, -1)}${FlowEvent.deriveEventIdSuffix(eventType)}`;\n  }\n\n  // Base required fields for all events:\n  eventType: string;\n  eventId: string;\n  eventParentIds: string[];\n  eventCreatedAt: string;\n  // `sessionId` usually matches `browserbaseSessionId` today, but FlowLogger treats it as a generic Stagehand session identifier because those may diverge in the future.\n  sessionId: string;\n  data: FlowEventData; // event payload (e.g. params, action, result, error, etc.)\n\n  // Normalizes the event shape used everywhere in the flow logger pipeline. This is called at emission time right before an event is attached to the event bus and any sinks.\n  constructor(input: FlowEventInput) {\n    if (!input.sessionId) {\n      throw new Error(\"FlowEvent.sessionId is required.\");\n    }\n    if (input.eventType.endsWith(\"Event\")) {\n      this.eventType = input.eventType;\n    } else {\n      this.eventType = `${input.eventType}Event`;\n    }\n    this.eventId = input.eventId ?? FlowEvent.createEventId(this.eventType);\n    this.eventParentIds = input.eventParentIds ?? [];\n    this.eventCreatedAt = input.eventCreatedAt ?? new Date().toISOString();\n    this.sessionId = input.sessionId;\n    this.data = input.data ?? {};\n  }\n}\n\nexport interface FlowLoggerContext {\n  // Mirrors `FlowEvent.sessionId`; it is currently the Stagehand session id and often matches `browserbaseSessionId`, but callers should not rely on that.\n  sessionId: string;\n  eventBus: EventEmitterWithWildcardSupport; // Shared per-session bus; `emit()` writes to it and V3 forwards wildcard events into the instance-owned EventStore.\n  parentEvents: FlowEvent[]; // Active parent stack for the current async chain; wrappers push/pop this as logged work starts and ends.\n}\n\ntype AsyncOriginalMethod<\n  TArgs extends unknown[] = unknown[],\n  TResult = unknown,\n  TThis = unknown,\n> = (this: TThis, ...args: TArgs) => Promise<TResult>;\n\ntype FlowLoggerLogOptions = FlowEventInput & {\n  context?: FlowLoggerContext;\n};\n\n// AsyncLocalStorage is the authoritative source for the active flow parent stack inside a single async call-chain.\nconst loggerContext = new AsyncLocalStorage<FlowLoggerContext>();\n\n// Converts raw inline image/base64 payload lengths into a compact kb string for LLM prompt summaries.\nfunction dataToKb(data: string): string {\n  return ((data.length * 0.75) / 1024).toFixed(1);\n}\n\n// =============================================================================\n// Flow Logger Internals\n// =============================================================================\n\ntype CdpLogEventType = \"call\" | \"response\" | \"responseError\" | \"message\";\n\ntype CdpLogPayload = {\n  method: string;\n  params?: unknown;\n  result?: unknown;\n  error?: string;\n  targetId?: string | null;\n};\n\nconst CDP_EVENT_NAMES: Record<CdpLogEventType, string> = {\n  call: \"CdpCallEvent\",\n  response: \"CdpResponseEvent\",\n  responseError: \"CdpResponseErrorEvent\",\n  message: \"CdpMessageEvent\",\n};\n\nexport class FlowLogger {\n  // Copies the mutable parts of a context before it is re-entered in a later async callback. This prevents later parent-stack mutations from leaking backward into stored snapshots.\n  private static cloneContext(ctx: FlowLoggerContext): FlowLoggerContext {\n    return {\n      ...ctx,\n      parentEvents: ctx.parentEvents.map((event) => ({\n        ...event,\n        eventParentIds: [...event.eventParentIds],\n      })),\n    };\n  }\n\n  // Chooses the safest context to re-enter when callers already have a stored context\n  // and ALS may or may not already contain one for the same session.\n  // If the current ALS stack extends the stored stack, we keep the richer ALS view.\n  // If the stored stack is deeper, we preserve that instead.\n  // If they diverge, we prefer the current ALS view because it reflects the currently executing call-chain.\n  private static resolveReentryContext(\n    context: FlowLoggerContext,\n  ): FlowLoggerContext {\n    const currentContext = loggerContext.getStore() ?? null;\n    // If ALS is empty or belongs to another session, the caller's stored\n    // snapshot is the only safe context we can re-enter.\n    if (!currentContext || currentContext.sessionId !== context.sessionId) {\n      return FlowLogger.cloneContext(context);\n    }\n\n    const providedParentIds = context.parentEvents.map(\n      (event) => event.eventId,\n    );\n    const currentParentIds = currentContext.parentEvents.map(\n      (event) => event.eventId,\n    );\n    const currentExtendsProvided = providedParentIds.every(\n      (eventId, index) => currentParentIds[index] === eventId,\n    );\n    // ALS already has the provided chain as a prefix, so we keep the richer\n    // currently-executing stack instead of truncating it.\n    if (currentExtendsProvided) {\n      return FlowLogger.cloneContext(currentContext);\n    }\n\n    const providedExtendsCurrent = currentParentIds.every(\n      (eventId, index) => providedParentIds[index] === eventId,\n    );\n    // The stored snapshot is deeper than the current ALS stack, which usually\n    // means we are re-entering from a later async callback and need to restore\n    // the missing parent chain.\n    if (providedExtendsCurrent) {\n      return FlowLogger.cloneContext(context);\n    }\n\n    // If the two chains diverged, prefer the live ALS chain because it reflects\n    // the work currently executing on this async path.\n    return FlowLogger.cloneContext(currentContext);\n  }\n\n  // Materializes and emits a single flow event on the active ALS context.\n  // This is the lowest-level write path used by all higher-level logging helpers\n  // after they have decided which parent chain and session the event belongs to.\n  private static emit(event: FlowEventInput): FlowEvent | null {\n    const ctx = FlowLogger.currentContext;\n\n    const emittedEvent = new FlowEvent({\n      ...event,\n      eventParentIds:\n        event.eventParentIds ??\n        ctx.parentEvents.map((parent) => parent.eventId),\n      sessionId: ctx.sessionId,\n    });\n    ctx.eventBus.emit(emittedEvent.eventType, emittedEvent);\n    return emittedEvent;\n  }\n\n  // Wraps a unit of async work with started/completed/error events while maintaining\n  // the parent stack inside the active context.\n  private static async runWithAutoStatusEventLogging<TResult>(\n    options: FlowLoggerLogOptions,\n    originalMethod: AsyncOriginalMethod<[], TResult>,\n  ): Promise<TResult> {\n    const ctx = FlowLogger.currentContext;\n    const { data, eventParentIds, eventType } = options;\n    let caughtError: unknown = null;\n\n    // if eventParentIds is explicitly [], this is a root event, clear the parent events in context\n    if (eventParentIds && eventParentIds.length === 0) {\n      ctx.parentEvents = [];\n    }\n\n    const startedEvent = FlowLogger.emit({\n      eventType,\n      data,\n      eventParentIds,\n    });\n\n    // Push after emitting so nested work sees this event as its direct parent\n    // for the rest of the wrapped method's lifetime.\n    ctx.parentEvents.push(startedEvent);\n\n    try {\n      return await originalMethod();\n    } catch (error) {\n      caughtError = error;\n      // Error events attach directly under the started event even though the\n      // stack is still live, so the failure edge is explicit in the tree.\n      FlowLogger.emit({\n        eventType: `${eventType}ErrorEvent`,\n        eventParentIds: [...startedEvent.eventParentIds, startedEvent.eventId],\n        data: {\n          error: error instanceof Error ? error.message : String(error),\n          durationMs:\n            Date.now() - new Date(startedEvent.eventCreatedAt).getTime(),\n        },\n      });\n      throw error;\n    } finally {\n      // Pop only the frame owned by this wrapper. If nested code has already\n      // mutated the stack unexpectedly, we skip the completed event rather than\n      // emitting a misleading lifecycle edge.\n      const parentEvent = ctx.parentEvents.pop();\n      if (parentEvent?.eventId === startedEvent.eventId && !caughtError) {\n        FlowLogger.emit({\n          eventType: `${eventType}CompletedEvent`,\n          eventParentIds: [\n            ...startedEvent.eventParentIds,\n            startedEvent.eventId,\n          ],\n          data: {\n            durationMs:\n              Date.now() - new Date(startedEvent.eventCreatedAt).getTime(),\n          },\n        });\n      }\n    }\n  }\n\n  // Emits a CDP event under a caller-supplied context. CDP transport code uses this\n  // instead of `runWithLogging()` because request/response/message events\n  // are separate lifecycle edges with explicit parent ids.\n  private static logCdpEvent(\n    context: FlowLoggerContext,\n    eventType: CdpLogEventType,\n    { method, params, result, error, targetId }: CdpLogPayload,\n    eventParentIds?: string[],\n  ): FlowEvent | null {\n    if (method.endsWith(\".enable\") || method === \"enable\") {\n      return null;\n    }\n\n    if (eventType === \"message\" && FlowLogger.NOISY_CDP_EVENTS.has(method)) {\n      return null;\n    }\n\n    return loggerContext.run(FlowLogger.cloneContext(context), () =>\n      FlowLogger.emit({\n        eventType: CDP_EVENT_NAMES[eventType],\n        eventParentIds,\n        data: {\n          method,\n          params,\n          result,\n          error,\n          targetId,\n        },\n      }),\n    );\n  }\n\n  // Emits an LLM request/response event only when a flow context is active.\n  // LLM logging is best-effort, so callers should not fail if it is invoked outside a tracked async chain.\n  private static emitLlmEvent(event: FlowEventInput): void {\n    const context = FlowLogger.resolveContext();\n    if (!context) {\n      return;\n    }\n\n    loggerContext.run(context, () => {\n      FlowLogger.emit(event);\n    });\n  }\n\n  // Builds the one-line prompt summary used in LLM request events for AI SDK middleware calls.\n  private static buildMiddlewarePromptSummary(params: {\n    prompt?: unknown;\n    tools?: unknown;\n  }): string {\n    const toolCount = Array.isArray(params.tools) ? params.tools.length : 0;\n    const messages = (params.prompt ?? []) as Array<{\n      role?: string;\n      content?: unknown;\n    }>;\n    const lastMsg = messages\n      .filter((message) => message.role !== \"system\")\n      .pop();\n    let rolePrefix = lastMsg?.role ?? \"?\";\n    let promptSummary = `(no text) +{${toolCount} tools}`;\n\n    if (!lastMsg) {\n      return `?: ${promptSummary}`;\n    }\n\n    if (typeof lastMsg.content === \"string\") {\n      promptSummary = `${lastMsg.content} +{${toolCount} tools}`;\n    } else if (Array.isArray(lastMsg.content)) {\n      const toolResult = (\n        lastMsg.content as Array<{\n          type?: string;\n          toolName?: string;\n          output?: { type?: string; value?: unknown };\n        }>\n      ).find((part) => part.type === \"tool-result\");\n\n      if (toolResult) {\n        rolePrefix = `tool result: ${toolResult.toolName}()`;\n        if (toolResult.output?.type === \"json\" && toolResult.output.value) {\n          promptSummary = `${JSON.stringify(toolResult.output.value)} +{${toolCount} tools}`;\n        } else if (Array.isArray(toolResult.output?.value)) {\n          promptSummary = `${\n            extractLlmMessageSummary({\n              content: toolResult.output.value,\n            }) ?? \"(no text)\"\n          } +{${toolCount} tools}`;\n        }\n      } else {\n        promptSummary = `${\n          extractLlmMessageSummary({ content: lastMsg.content }) ?? \"(no text)\"\n        } +{${toolCount} tools}`;\n      }\n    }\n\n    return `${rolePrefix}: ${promptSummary}`;\n  }\n\n  // Builds the one-line output summary used in LLM response events for AI SDK middleware calls.\n  private static buildMiddlewareOutputSummary(result: {\n    text?: string;\n    content?: unknown;\n    toolCalls?: unknown[];\n  }): string {\n    let outputSummary = result.text || \"\";\n    if (!outputSummary && result.content) {\n      if (typeof result.content === \"string\") {\n        outputSummary = result.content;\n      } else if (Array.isArray(result.content)) {\n        outputSummary = (\n          result.content as Array<{\n            type?: string;\n            text?: string;\n            toolName?: string;\n          }>\n        )\n          .map((contentPart) => {\n            if (contentPart.text) {\n              return contentPart.text;\n            }\n\n            if (contentPart.type === \"tool-call\") {\n              return `tool call: ${contentPart.toolName}()`;\n            }\n\n            return `[${contentPart.type}]`;\n          })\n          .join(\" \");\n      }\n    }\n\n    if (!outputSummary && result.toolCalls?.length) {\n      return `[${result.toolCalls.length} tool calls]`;\n    }\n\n    return outputSummary || \"[empty]\";\n  }\n\n  // =============================================================================\n  // Flow Logger Public Lifecycle API\n  // =============================================================================\n\n  // Initialize a new logging context. Call this at the start of a session.\n  static init(\n    sessionId: string,\n    eventBus: EventEmitterWithWildcardSupport,\n  ): FlowLoggerContext {\n    const ctx: FlowLoggerContext = {\n      sessionId,\n      eventBus,\n      parentEvents: [],\n    };\n\n    loggerContext.enterWith(ctx);\n    return ctx;\n  }\n\n  // Clears the parent stack for a session when a V3 instance shuts down.\n  // This does not emit a final event; it just tears down in-memory context.\n  static async close(context?: FlowLoggerContext | null): Promise<void> {\n    const ctx = context ?? loggerContext.getStore() ?? null;\n    if (!ctx) return;\n    ctx.parentEvents = [];\n  }\n\n  // Returns the current ALS-backed flow context and throws when code\n  // executes outside a tracked flow. Use `resolveContext()` for best-effort lookups.\n  static get currentContext(): FlowLoggerContext {\n    const ctx = loggerContext.getStore() ?? null;\n    if (!ctx) {\n      throw new Error(\"FlowLogger context is missing.\");\n    }\n\n    return ctx;\n  }\n\n  // Returns a cloned FlowLogger context for the current async call-chain when one exists,\n  // otherwise falls back to the provided instance-owned context.\n  // This is the non-throwing lookup for callers that can continue without ALS.\n  static resolveContext(\n    fallbackContext?: FlowLoggerContext | null,\n  ): FlowLoggerContext | null {\n    const currentContext = loggerContext.getStore() ?? null;\n    if (currentContext) {\n      return FlowLogger.cloneContext(currentContext);\n    }\n\n    return fallbackContext ? FlowLogger.cloneContext(fallbackContext) : null;\n  }\n\n  // Decorator-style wrapper used on class methods that should emit their own started/completed/error envelope.\n  // It resolves the flow context from either the decorator options or `this.flowLoggerContext`,\n  // then delegates the actual lifecycle handling to `runWithLogging()`.\n  static wrapWithLogging<TMethod extends AsyncOriginalMethod>(\n    options: FlowLoggerLogOptions,\n  ) {\n    return function <\n      TWrappedMethod extends AsyncOriginalMethod<\n        Parameters<TMethod>,\n        Awaited<ReturnType<TMethod>>,\n        ThisParameterType<TMethod>\n      >,\n    >(originalMethod: TWrappedMethod): TWrappedMethod {\n      const wrappedMethod = async function (\n        this: ThisParameterType<TWrappedMethod>,\n        ...args: Parameters<TWrappedMethod>\n      ): Promise<Awaited<ReturnType<TWrappedMethod>>> {\n        let context = options.context;\n        if (!context) {\n          context = (\n            this as { flowLoggerContext?: FlowLoggerContext } | null | undefined\n          )?.flowLoggerContext;\n        }\n\n        return await FlowLogger.runWithLogging(\n          {\n            ...options,\n            context,\n          },\n          (...boundArgs: Parameters<TWrappedMethod>) =>\n            originalMethod.apply(this, boundArgs) as Promise<\n              Awaited<ReturnType<TWrappedMethod>>\n            >,\n          args,\n        );\n      };\n\n      return wrappedMethod as unknown as TWrappedMethod;\n    };\n  }\n\n  // Wraps an async function or zero-arg closure with flow events.\n  // This is the imperative entrypoint used by handlers that cannot use the decorator form.\n  // Standard case: the logged params are the same tuple passed to the wrapped method.\n  static runWithLogging<TMethod extends AsyncOriginalMethod>(\n    options: FlowLoggerLogOptions,\n    originalMethod: TMethod,\n    params: Readonly<Parameters<TMethod>>,\n  ): Promise<Awaited<ReturnType<TMethod>>>;\n  // Special case: log an arbitrary params tuple while executing a zero-arg closure.\n  static runWithLogging<TResult>(\n    options: FlowLoggerLogOptions,\n    originalMethod: AsyncOriginalMethod<[], TResult>,\n    params: ReadonlyArray<unknown>,\n  ): Promise<Awaited<TResult>>;\n  static runWithLogging(\n    options: FlowLoggerLogOptions,\n    originalMethod: AsyncOriginalMethod<unknown[], unknown>,\n    params: ReadonlyArray<unknown>,\n  ): Promise<unknown> {\n    const eventData = {\n      ...(options.data ?? {}),\n      params: [...params],\n    };\n\n    const execute = (): Promise<unknown> =>\n      FlowLogger.runWithAutoStatusEventLogging(\n        {\n          ...options,\n          data: eventData,\n        },\n        () => originalMethod(...params),\n      );\n\n    // No explicit context and no active ALS means there is nothing to attach\n    // this work to, so we leave execution untouched instead of fabricating a\n    // root event.\n    if (!options.context && !(loggerContext.getStore() ?? null)) {\n      return originalMethod(...params);\n    }\n\n    if (options.context) {\n      // Re-enter the caller-owned context so wrapper events land under the same\n      // session tree even when this code executes outside the original ALS\n      // chain.\n      return loggerContext.run(\n        FlowLogger.resolveReentryContext(options.context),\n        execute,\n      );\n    }\n\n    return execute();\n  }\n\n  // Re-enters an existing FlowLogger context without emitting wrapper events.\n  // Use this when work already belongs to a known parent and needs AsyncLocalStorage set manually.\n  static withContext<T>(context: FlowLoggerContext, fn: () => T): T {\n    return loggerContext.run(FlowLogger.resolveReentryContext(context), fn);\n  }\n\n  // ===========================================================================\n  // CDP Events\n  // ===========================================================================\n\n  private static readonly NOISY_CDP_EVENTS = new Set([\n    \"Target.targetInfoChanged\",\n    \"Runtime.executionContextCreated\",\n    \"Runtime.executionContextDestroyed\",\n    \"Runtime.executionContextsCleared\",\n    \"Page.lifecycleEvent\",\n    \"Network.dataReceived\",\n    \"Network.loadingFinished\",\n    \"Network.requestWillBeSentExtraInfo\",\n    \"Network.responseReceivedExtraInfo\",\n    \"Network.requestWillBeSent\",\n    \"Network.responseReceived\",\n  ]);\n\n  // Logs the start of a CDP command. CDP transport calls this before sending a\n  // message over the websocket so the eventual response can attach to it.\n  static logCdpCallEvent(\n    context: FlowLoggerContext,\n    data: {\n      method: string;\n      params?: object;\n      targetId?: string | null;\n    },\n  ): FlowEvent | null {\n    return FlowLogger.logCdpEvent(context, \"call\", data);\n  }\n\n  // Logs the terminal response for a previously emitted CDP call event.\n  static logCdpResponseEvent(\n    context: FlowLoggerContext,\n    parentEvent: Pick<FlowEvent, \"eventId\" | \"eventParentIds\">,\n    data: {\n      method: string;\n      result?: unknown;\n      error?: string;\n      targetId?: string | null;\n    },\n  ): void {\n    FlowLogger.logCdpEvent(\n      context,\n      data.error ? \"responseError\" : \"response\",\n      data,\n      [...parentEvent.eventParentIds, parentEvent.eventId],\n    );\n  }\n\n  // Logs an unsolicited CDP message under the most recent related call event.\n  static logCdpMessageEvent(\n    context: FlowLoggerContext,\n    parentEvent: Pick<FlowEvent, \"eventId\" | \"eventParentIds\">,\n    data: {\n      method: string;\n      params?: unknown;\n      targetId?: string | null;\n    },\n  ): void {\n    FlowLogger.logCdpEvent(context, \"message\", data, [\n      ...parentEvent.eventParentIds,\n      parentEvent.eventId,\n    ]);\n  }\n\n  // ===========================================================================\n  // LLM Events\n  // ===========================================================================\n\n  // Emits a best-effort LLM request event when logging occurs inside an active flow context.\n  static logLlmRequest({\n    requestId,\n    model,\n    prompt,\n  }: {\n    requestId: string;\n    model: string;\n    prompt?: string;\n  }): void {\n    FlowLogger.emitLlmEvent({\n      eventType: \"LlmRequestEvent\",\n      data: {\n        requestId,\n        model,\n        prompt,\n      },\n    });\n  }\n\n  // Emits a best-effort LLM response event when logging occurs inside an active flow context.\n  static logLlmResponse({\n    requestId,\n    model,\n    output,\n    inputTokens,\n    outputTokens,\n  }: {\n    requestId: string;\n    model: string;\n    output?: string;\n    inputTokens?: number;\n    outputTokens?: number;\n  }): void {\n    FlowLogger.emitLlmEvent({\n      eventType: \"LlmResponseEvent\",\n      data: {\n        requestId,\n        model,\n        output,\n        inputTokens,\n        outputTokens,\n      },\n    });\n  }\n\n  // ===========================================================================\n  // LLM Logging Middleware\n  // ===========================================================================\n\n  // Creates AI SDK middleware that wraps a generate call with FlowLogger LLM request/response events\n  // while leaving model execution behavior unchanged.\n  static createLlmLoggingMiddleware(\n    modelId: string,\n  ): Pick<LanguageModelMiddleware, \"wrapGenerate\"> {\n    return {\n      wrapGenerate: async ({ doGenerate, params }) => {\n        const llmRequestId = uuidv7();\n        FlowLogger.logLlmRequest({\n          requestId: llmRequestId,\n          model: modelId,\n          prompt: FlowLogger.buildMiddlewarePromptSummary(params),\n        });\n\n        const result = await doGenerate();\n\n        const res = result as {\n          text?: string;\n          content?: unknown;\n          toolCalls?: unknown[];\n        };\n\n        FlowLogger.logLlmResponse({\n          requestId: llmRequestId,\n          model: modelId,\n          output: FlowLogger.buildMiddlewareOutputSummary(res),\n          inputTokens: result.usage?.inputTokens,\n          outputTokens: result.usage?.outputTokens,\n        });\n\n        return result;\n      },\n    };\n  }\n}\n\n// =============================================================================\n// LLM Event Extraction Helpers\n// =============================================================================\n\ntype ContentPart = {\n  type?: string;\n  text?: string;\n  content?: unknown[];\n  source?: { data?: string };\n  image_url?: { url?: string };\n  inlineData?: { data?: string };\n};\n\ntype LlmMessageContent = {\n  content?: unknown;\n  text?: string;\n  parts?: unknown[];\n};\n\n// Extracts text and image markers from an LLM content array.\n// This is shared by the request-summary helpers below so different provider message\n// shapes render consistently in the flow log.\nfunction extractLlmMessageContent(content: unknown[]): {\n  text?: string;\n  extras: string[];\n} {\n  const result = {\n    text: undefined as string | undefined,\n    extras: [] as string[],\n  };\n\n  for (const part of content) {\n    const p = part as ContentPart;\n    // Text\n    if (!result.text && p.text) {\n      result.text = p.type === \"text\" || !p.type ? p.text : undefined;\n    }\n    // Images - various formats\n    if (p.type === \"image\" || p.type === \"image_url\") {\n      const url = p.image_url?.url;\n      if (url?.startsWith(\"data:\"))\n        result.extras.push(`${dataToKb(url)}kb image`);\n      else if (p.source?.data)\n        result.extras.push(`${dataToKb(p.source.data)}kb image`);\n      else result.extras.push(\"image\");\n    } else if (p.source?.data) {\n      result.extras.push(`${dataToKb(p.source.data)}kb image`);\n    } else if (p.inlineData?.data) {\n      result.extras.push(`${dataToKb(p.inlineData.data)}kb image`);\n    }\n    // Recurse into tool_result content\n    if (p.type === \"tool_result\" && Array.isArray(p.content)) {\n      const nested = extractLlmMessageContent(p.content);\n      if (!result.text && nested.text) {\n        result.text = nested.text;\n      }\n      result.extras.push(...nested.extras);\n    }\n  }\n\n  return result;\n}\n\n// Produces a single compact summary from a provider-specific message payload\n// so request and tool-result logs stay readable.\nfunction extractLlmMessageSummary(\n  input: LlmMessageContent,\n  options?: {\n    trimInstructionPrefix?: boolean;\n    extras?: string[];\n  },\n): string | undefined {\n  const result = {\n    text: undefined as string | undefined,\n    extras: [...(options?.extras ?? [])],\n  };\n\n  if (typeof input.content === \"string\") {\n    result.text = input.content;\n  } else if (typeof input.text === \"string\") {\n    result.text = input.text;\n  } else if (Array.isArray(input.parts)) {\n    const summary = extractLlmMessageContent(input.parts);\n    result.text = summary.text;\n    result.extras.push(...summary.extras);\n  } else if (Array.isArray(input.content)) {\n    const summary = extractLlmMessageContent(input.content);\n    result.text = summary.text;\n    result.extras.push(...summary.extras);\n  }\n\n  if (options?.trimInstructionPrefix && result.text) {\n    result.text = result.text.replace(/^[Ii]nstruction: /, \"\");\n  }\n\n  const text = result.text;\n  if (!text && result.extras.length === 0) return undefined;\n\n  let summary = text || \"\";\n  if (result.extras.length > 0) {\n    const extrasStr = result.extras.map((e) => `+{${e}}`).join(\" \");\n    summary = summary ? `${summary} ${extrasStr}` : extrasStr;\n  }\n  return summary || undefined;\n}\n\n// Formats the last user-facing prompt into the one-line form used by standard LLM request logs,\n// for example: `some text +{5.8kb image} +{schema}`.\nexport function extractLlmPromptSummary(\n  messages: Array<{ role: string; content: unknown }>,\n  options?: { toolCount?: number; hasSchema?: boolean },\n): string | undefined {\n  try {\n    const lastUserMsg = messages.filter((m) => m.role === \"user\").pop();\n    if (!lastUserMsg) return undefined;\n\n    return extractLlmMessageSummary(lastUserMsg, {\n      trimInstructionPrefix: true,\n      extras: [\n        ...(options?.hasSchema ? [\"schema\"] : []),\n        ...(options?.toolCount ? [`${options.toolCount} tools`] : []),\n      ],\n    });\n  } catch {\n    return undefined;\n  }\n}\n\n// Extract a text summary from CUA-style messages. This accepts Anthropic, OpenAI, and Google-style payloads.\nexport function extractLlmCuaPromptSummary(\n  messages: unknown[],\n): string | undefined {\n  try {\n    const lastMsg = messages\n      .filter((m) => {\n        const msg = m as { role?: string; type?: string };\n        return msg.role === \"user\" || msg.type === \"tool_result\";\n      })\n      .pop() as\n      | { content?: unknown; parts?: unknown[]; text?: string }\n      | undefined;\n\n    if (!lastMsg) return undefined;\n\n    return extractLlmMessageSummary(lastMsg);\n  } catch {\n    return undefined;\n  }\n}\n\n// Formats the response side of a CUA exchange into a single short log line.\nexport function extractLlmCuaResponseSummary(output: unknown): string {\n  try {\n    const items: unknown[] =\n      (output as { candidates?: [{ content?: { parts?: unknown[] } }] })\n        ?.candidates?.[0]?.content?.parts ??\n      (Array.isArray(output) ? output : []);\n\n    const summary = items\n      .map((item) => {\n        const i = item as {\n          type?: string;\n          text?: string;\n          name?: string;\n          functionCall?: { name?: string };\n        };\n        if (i.text) return i.text;\n        if (i.functionCall?.name) return i.functionCall.name;\n        if (i.type === \"tool_use\" && i.name) return i.name;\n        return i.type ?? \"[item]\";\n      })\n      .join(\" \");\n\n    return summary;\n  } catch {\n    return \"[error]\";\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/flowlogger/prettify.ts",
    "content": "import { toTitleCase } from \"../../utils.js\";\nimport { FlowEvent } from \"./FlowLogger.js\";\nimport type { EventStoreApi } from \"./EventStore.js\";\n\nconst MAX_LINE_LENGTH = 160; // Maximum width for a prettified log line.\n\n// =============================================================================\n// Pretty Formatting\n// =============================================================================\n\n// All functions in this section intentionally share the `prettify` prefix so the formatting pipeline is easy to scan and reason about in one place.\n\n// Sanitizes individual values before they are included in prettified output. This currently shortens CDP ids but otherwise preserves structure.\nfunction prettifySanitizeValue(value: unknown): unknown {\n  if (typeof value === \"string\") {\n    return truncateCdpIds(value);\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((entry) => prettifySanitizeValue(entry));\n  }\n\n  if (value && typeof value === \"object\") {\n    return Object.fromEntries(\n      Object.entries(value).map(([key, entry]) => [\n        key,\n        prettifySanitizeValue(entry),\n      ]),\n    );\n  }\n\n  return value;\n}\n\n// Produces a prettified-safe copy of the event without mutating the original event that other sinks may still need to serialize verbatim.\nexport function prettifySanitizeEvent(event: FlowEvent): FlowEvent {\n  if (!event.eventType.startsWith(\"Cdp\")) {\n    return event;\n  }\n\n  return {\n    ...event,\n    data: prettifySanitizeValue(event.data) as Record<string, unknown>,\n  };\n}\n\n// Collapses newlines and tabs, then truncates a string to the configured pretty log width while preserving the tail for ids and result summaries.\nfunction prettifyTruncateLine(value: string, maxLen: number): string {\n  const collapsed = value.replace(/[\\r\\n\\t]+/g, \" \");\n  if (collapsed.length <= maxLen) {\n    return collapsed;\n  }\n\n  const endLen = Math.floor(maxLen * 0.3);\n  const startLen = maxLen - endLen - 1;\n  return `${collapsed.slice(0, startLen)}…${collapsed.slice(-endLen)}`;\n}\n\n// Converts any event argument into a compact string representation for pretty logs.\nfunction prettifyFormatValue(value: unknown): string {\n  if (typeof value === \"string\") return `'${value}'`;\n  if (value == null || typeof value !== \"object\") return String(value);\n\n  try {\n    return JSON.stringify(value);\n  } catch {\n    return \"[unserializable]\";\n  }\n}\n\n// Formats one or more call arguments into a comma-separated pretty string.\nfunction prettifyFormatArgs(args?: unknown | unknown[]): string {\n  if (args === undefined) {\n    return \"\";\n  }\n\n  return (Array.isArray(args) ? args : [args])\n    .filter((entry) => entry !== undefined)\n    .map(prettifyFormatValue)\n    .filter((entry) => entry.length > 0)\n    .join(\", \");\n}\n\n// Returns the short id fragment used by pretty tags.\nfunction shortId(id: string | null | undefined): string {\n  return id ? id.slice(-4) : \"-\";\n}\n\n// Shortens 32-character CDP ids so pretty logs stay readable while still leaving enough information to correlate related targets.\nfunction truncateCdpIds(value: string): string {\n  return value.replace(\n    /([iI]d:?\"?)([0-9A-F]{32})(?=\"?[,})\\s]|$)/g,\n    (_, prefix: string, id: string) =>\n      `${prefix}${id.slice(0, 4)}…${id.slice(-4)}`,\n  );\n}\n\nlet nonce = 0;\n\n// Formats timestamps for pretty logs while appending a tiny nonce so lines emitted in the same millisecond remain stable and sortable.\nfunction prettifyFormatTimestamp(date: Date): string {\n  const pad = (value: number, width = 2) => String(value).padStart(width, \"0\");\n  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}${pad(nonce++ % 100)}`;\n}\n\n// Removes noisy quoting artifacts from the final pretty line.\nfunction prettifyRemoveQuotes(value: string): string {\n  return value\n    .replace(/([^\\\\])[\"']/g, \"$1\")\n    .replace(/^[\"']|[\"']$/g, \"\")\n    .trim();\n}\n\n// Strips event lifecycle suffixes so related started/completed/error variants can be grouped under one logical operation name.\nfunction prettifyEventName(eventType: string): string {\n  return eventType\n    .replace(/CompletedEvent$/, \"\")\n    .replace(/ErrorEvent$/, \"\")\n    .replace(/Event$/, \"\");\n}\n\n// Extracts the operation name from a Stagehand/Page/Understudy/Agent event.\nfunction prettifyEventAction(eventType: string): string {\n  return prettifyEventName(eventType)\n    .replace(/^Agent/, \"\")\n    .replace(/^Stagehand/, \"\")\n    .replace(/^Understudy/, \"\")\n    .replace(/^Page/, \"\");\n}\n\n// Formats `Target.method(args)` style entries while gracefully handling events whose action portion is intentionally blank, such as `StagehandEvent`.\nfunction prettifyFormatMethodCall(\n  target: string,\n  method: string,\n  args: unknown,\n): string {\n  const member = method ? `.${method[0].toLowerCase()}${method.slice(1)}` : \"\";\n  return `▷ ${target}${member}(${prettifyFormatEventArgs(args)})`;\n}\n\n// Marks agent lifecycle events for ancestry tags.\nfunction prettifyIsAgentEvent(event: FlowEvent): boolean {\n  return prettifyEventName(event.eventType).startsWith(\"Agent\");\n}\n\n// Marks Stagehand lifecycle events for ancestry tags.\nfunction prettifyIsStagehandEvent(event: FlowEvent): boolean {\n  return prettifyEventName(event.eventType).startsWith(\"Stagehand\");\n}\n\n// Marks page and Understudy actions for the action tag.\nfunction prettifyIsActionEvent(event: FlowEvent): boolean {\n  return /^(Page|Understudy)/.test(prettifyEventName(event.eventType));\n}\n\n// Routes transport-level CDP traffic to the CDP formatter.\nexport function prettifyIsCdpEvent(event: FlowEvent): boolean {\n  return prettifyEventName(event.eventType).startsWith(\"Cdp\");\n}\n\n// Routes LLM request/response events to the LLM formatter.\nfunction prettifyIsLlmEvent(event: FlowEvent): boolean {\n  return prettifyEventName(event.eventType).startsWith(\"Llm\");\n}\n\n// Completed events should inherit tags from the started operation.\nfunction prettifyIsCompletedEvent(event: FlowEvent): boolean {\n  return event.eventType.endsWith(\"CompletedEvent\");\n}\n\n// Error events should inherit tags from the started operation.\nfunction prettifyIsErrorEvent(event: FlowEvent): boolean {\n  return event.eventType.endsWith(\"ErrorEvent\");\n}\n\n// Renders the bracketed pretty tag used in stderr/file pretty logs.\nfunction prettifyFormatTag(\n  label: string | null | undefined,\n  id: string | null | undefined,\n  icon: string,\n): string {\n  return id ? `[${icon} #${shortId(id)}${label ? ` ${label}` : \"\"}]` : \"⤑\";\n}\n\n// Formats duration values stored on completed/error events.\nfunction prettifyFormatDuration(durationMs?: unknown): string | null {\n  return typeof durationMs === \"number\"\n    ? `${(durationMs / 1000).toFixed(2)}s`\n    : null;\n}\n\n// Summarizes a prompt or output payload down to a single displayable string for the LLM pretty formatter.\nfunction prettifySummarizePrompt(value: unknown): string | undefined {\n  if (typeof value === \"string\") {\n    return prettifyTruncateLine(value, MAX_LINE_LENGTH / 2);\n  }\n\n  if (value == null) {\n    return undefined;\n  }\n\n  return prettifyTruncateLine(prettifyFormatValue(value), MAX_LINE_LENGTH / 2);\n}\n\n// Replaces large object references from live runtime objects with placeholders before they are stringified for pretty output.\nfunction prettifyCompactValue(value: unknown): unknown {\n  if (typeof value !== \"object\" || value === null) {\n    return value;\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((entry) => prettifyCompactValue(entry));\n  }\n\n  const result: Record<string, unknown> = {};\n  for (const [key, entry] of Object.entries(value)) {\n    if (\n      key === \"page\" ||\n      key === \"frame\" ||\n      key === \"locator\" ||\n      key === \"conn\" ||\n      key === \"mainSession\" ||\n      key === \"sessions\" ||\n      key === \"registry\" ||\n      key === \"networkManager\" ||\n      key === \"apiClient\"\n    ) {\n      result[key] = `[${toTitleCase(key)}]`;\n      continue;\n    }\n\n    result[key] = prettifyCompactValue(entry);\n  }\n\n  return result;\n}\n\n// Formats event arguments after compacting any live object references.\nfunction prettifyFormatEventArgs(args?: unknown | unknown[]): string {\n  return prettifyFormatArgs(prettifyCompactValue(args) as unknown | unknown[]);\n}\n\n// Finds the nearest event in the current parent chain that satisfies the given predicate. Pretty tags use this to recover agent/stagehand/action/llm ancestry.\nfunction prettifyFindNearestEvent(\n  event: FlowEvent,\n  parentMap: Map<string, FlowEvent>,\n  predicate: (candidate: FlowEvent) => boolean,\n  options?: { includeSelf?: boolean },\n): FlowEvent | null {\n  if (options?.includeSelf !== false && predicate(event)) {\n    return event;\n  }\n\n  for (let index = event.eventParentIds.length - 1; index >= 0; index -= 1) {\n    const parent = parentMap.get(event.eventParentIds[index]);\n    if (parent && predicate(parent)) {\n      return parent;\n    }\n  }\n\n  return null;\n}\n\n// Builds the semantic ancestry tags shown on each pretty log line.\n// 2026-03-16 22:04:15.45540 [🅰 #1083] [🆂 #7bf4 ACT] [🆄 #2125 CLICK] [🅲 #8B8B CDP] ⏴ Network.policyUpdated({})\nfunction prettifyBuildContextTags(\n  event: FlowEvent,\n  parentMap: Map<string, FlowEvent>,\n): string[] {\n  // Completed/error events should inherit tags from their started parent so the completion line points back to the original operation id.\n  const includeSelf =\n    !prettifyIsCompletedEvent(event) && !prettifyIsErrorEvent(event);\n  const agentEvent = prettifyFindNearestEvent(\n    event,\n    parentMap,\n    prettifyIsAgentEvent,\n    { includeSelf },\n  );\n  const stagehandEvent = prettifyFindNearestEvent(\n    event,\n    parentMap,\n    prettifyIsStagehandEvent,\n    { includeSelf },\n  );\n  const actionEvent = prettifyFindNearestEvent(\n    event,\n    parentMap,\n    prettifyIsActionEvent,\n    { includeSelf },\n  );\n  const llmEvent = prettifyFindNearestEvent(\n    event,\n    parentMap,\n    prettifyIsLlmEvent,\n    {\n      includeSelf,\n    },\n  );\n\n  let targetId: string | null = null;\n  if (typeof event.data.targetId === \"string\") {\n    targetId = event.data.targetId;\n  }\n\n  let stagehandLabel = \"\";\n  if (stagehandEvent) {\n    stagehandLabel = prettifyEventAction(\n      stagehandEvent.eventType,\n    ).toUpperCase();\n  }\n\n  let actionLabel = \"\";\n  if (actionEvent) {\n    actionLabel = prettifyEventAction(actionEvent.eventType).toUpperCase();\n  }\n\n  if (prettifyIsAgentEvent(event)) {\n    return [prettifyFormatTag(\"\", agentEvent?.eventId, \"🅰\")];\n  }\n\n  if (prettifyIsStagehandEvent(event)) {\n    return [\n      prettifyFormatTag(\"\", agentEvent?.eventId, \"🅰\"),\n      prettifyFormatTag(\n        prettifyEventAction(\n          stagehandEvent?.eventType ?? event.eventType,\n        ).toUpperCase(),\n        stagehandEvent?.eventId,\n        \"🆂\",\n      ),\n    ];\n  }\n\n  if (prettifyIsActionEvent(event)) {\n    return [\n      prettifyFormatTag(\"\", agentEvent?.eventId, \"🅰\"),\n      prettifyFormatTag(stagehandLabel, stagehandEvent?.eventId, \"🆂\"),\n      prettifyFormatTag(\n        prettifyEventAction(\n          actionEvent?.eventType ?? event.eventType,\n        ).toUpperCase(),\n        actionEvent?.eventId,\n        \"🆄\",\n      ),\n    ];\n  }\n\n  if (prettifyIsCdpEvent(event)) {\n    return [\n      prettifyFormatTag(\"\", agentEvent?.eventId, \"🅰\"),\n      prettifyFormatTag(stagehandLabel, stagehandEvent?.eventId, \"🆂\"),\n      prettifyFormatTag(actionLabel, actionEvent?.eventId, \"🆄\"),\n      prettifyFormatTag(\"CDP\", targetId, \"🅲\"),\n    ];\n  }\n\n  if (prettifyIsLlmEvent(event)) {\n    let requestId: string | null = null;\n    if (typeof event.data.requestId === \"string\") {\n      requestId = event.data.requestId;\n    }\n\n    return [\n      prettifyFormatTag(\"\", agentEvent?.eventId, \"🅰\"),\n      prettifyFormatTag(stagehandLabel, stagehandEvent?.eventId, \"🆂\"),\n      prettifyFormatTag(\"LLM\", requestId ?? llmEvent?.eventId, \"🅻\"),\n    ];\n  }\n\n  return [`[#${shortId(event.eventId)}]`];\n}\n\n// Formats the details section for started/root events.\nfunction prettifyFormatStartedDetails(event: FlowEvent): string {\n  const data = event.data as {\n    params?: unknown[];\n    target?: string;\n  };\n  const name = prettifyEventName(event.eventType);\n  const method = prettifyEventAction(event.eventType);\n\n  if (name.startsWith(\"Stagehand\")) {\n    return prettifyFormatMethodCall(\"Stagehand\", method, data.params);\n  }\n\n  if (name.startsWith(\"Page\")) {\n    return prettifyFormatMethodCall(\"Page\", method, data.params);\n  }\n\n  if (name.startsWith(\"Understudy\")) {\n    const args = [\n      data.target,\n      ...(Array.isArray(data.params) ? data.params : []),\n    ].filter((entry) => entry !== undefined);\n    return prettifyFormatMethodCall(\"Understudy\", method, args);\n  }\n\n  if (name.startsWith(\"Agent\")) {\n    return `▷ Agent.execute(${prettifyFormatEventArgs(data.params)})`;\n  }\n\n  return `${event.eventType}(${prettifyFormatEventArgs(data.params ?? event.data)})`;\n}\n\n// Formats the details section for completed/error events.\nfunction prettifyFormatCompletedDetails(event: FlowEvent): string {\n  const duration = prettifyFormatDuration(event.data.durationMs);\n  const prefix = prettifyIsAgentEvent(event)\n    ? \"Agent.execute() completed\"\n    : `${prettifyEventAction(event.eventType).toUpperCase() || event.eventType} completed`;\n  const message =\n    prettifyIsErrorEvent(event) && typeof event.data.error === \"string\"\n      ? ` ERROR ${event.data.error}`\n      : \"\";\n  return `${prettifyIsErrorEvent(event) ? \"✕\" : \"✓\"} ${prefix}${duration ? ` in ${duration}` : \"\"}${message}`;\n}\n\n// Formats CDP request/response/message details. These are rendered differently from normal Stagehand lifecycle events because they represent transport-level traffic rather than method envelopes.\nfunction prettifyFormatCdpDetails(event: FlowEvent): string {\n  const data = event.data as {\n    method?: string;\n    params?: unknown;\n    result?: unknown;\n    error?: string;\n  };\n  const method = data.method ?? \"unknown\";\n  const icon = event.eventType === \"CdpCallEvent\" ? \"⏵\" : \"⏴\";\n  let payload: unknown;\n  if (event.eventType === \"CdpCallEvent\") {\n    payload = data.params;\n  } else if (data.error) {\n    payload = { error: data.error };\n  } else if (event.eventType === \"CdpMessageEvent\") {\n    payload = data.params;\n  } else {\n    payload = data.result;\n  }\n\n  return `${icon} ${method}(${prettifyFormatEventArgs(payload)})`;\n}\n\n// Formats LLM request/response details for pretty logs.\nfunction prettifyFormatLlmDetails(event: FlowEvent): string {\n  const data = event.data as {\n    model?: string;\n    prompt?: unknown;\n    output?: unknown;\n    inputTokens?: number;\n    outputTokens?: number;\n  };\n  const model = data.model ?? \"llm\";\n\n  if (event.eventType === \"LlmRequestEvent\") {\n    const prompt = prettifySummarizePrompt(data.prompt);\n    return prompt ? `${model} ⏴ ${prompt}` : `${model} ⏴`;\n  }\n\n  const tokenInfo =\n    (data.inputTokens || data.outputTokens) > 0\n      ? ` ꜛ${data.inputTokens ?? 0} ꜜ${data.outputTokens ?? 0}`\n      : \"\";\n  const output = prettifySummarizePrompt(data.output);\n  return output ? `${model} ↳${tokenInfo} ${output}` : `${model} ↳${tokenInfo}`;\n}\n\n// Converts a flow event into a single pretty log line by combining the current event payload with recent shallow ancestry fetched from the store query sink.\nexport async function prettifyEvent(\n  store: Pick<EventStoreApi, \"query\">,\n  event: FlowEvent,\n): Promise<string | null> {\n  const recentEvents = await store.query({ limit: 500 });\n  const parentMap = new Map(\n    recentEvents.map((recentEvent) => [recentEvent.eventId, recentEvent]),\n  );\n  const tags = prettifyBuildContextTags(event, parentMap);\n\n  let details = prettifyFormatStartedDetails(event);\n  if (prettifyIsCdpEvent(event)) {\n    details = prettifyFormatCdpDetails(event);\n  } else if (prettifyIsLlmEvent(event)) {\n    details = prettifyFormatLlmDetails(event);\n  } else if (prettifyIsCompletedEvent(event) || prettifyIsErrorEvent(event)) {\n    details = prettifyFormatCompletedDetails(event);\n  }\n\n  if (!details) {\n    return null;\n  }\n\n  const createdAt = new Date(event.eventCreatedAt);\n  let timestamp = prettifyFormatTimestamp(createdAt);\n  if (Number.isNaN(createdAt.getTime())) {\n    timestamp = prettifyFormatTimestamp(new Date());\n  }\n\n  const line = `${timestamp} ${tags.join(\" \")} ${details}`;\n  const cleaned = prettifyRemoveQuotes(line);\n  const processed = prettifyIsCdpEvent(event)\n    ? truncateCdpIds(cleaned)\n    : cleaned;\n  return prettifyTruncateLine(processed, MAX_LINE_LENGTH);\n}\n\n// Adds subtle terminal color to stderr-only pretty lines without affecting file sinks.\nexport function prettifyColorStderrLine(line: string): string {\n  if (\n    process.env.NO_COLOR !== undefined ||\n    (process.env.FORCE_COLOR ?? \"\") === \"0\" ||\n    (!process.env.FORCE_COLOR &&\n      (!process.stderr.isTTY || process.env.TERM === \"dumb\"))\n  ) {\n    return line;\n  }\n\n  const color = (code: string, value: string) =>\n    `\\u001B[${code}m${value}\\u001B[0m`;\n  return line\n    .replace(/^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{5})/, (_, timestamp) =>\n      color(\"2\", timestamp),\n    )\n    .replace(/\\[([🅰🆂🆄🅻🅲])([^\\]]*)\\]/gu, (_, icon, rest) =>\n      color(\n        icon === \"🅰\"\n          ? \"36\"\n          : icon === \"🆂\"\n            ? \"33\"\n            : icon === \"🆄\"\n              ? \"32\"\n              : icon === \"🅻\"\n                ? \"95\"\n                : \"90\",\n        `[${icon}${rest}]`,\n      ),\n    )\n    .replace(\n      / in (\\d+(?:\\.\\d+)?s)/g,\n      (_, duration) => ` ${color(\"2\", \"in\")} ${color(\"2\", duration)}`,\n    )\n    .replace(/▷/g, color(\"96\", \"▷\"))\n    .replace(/⏴/g, color(\"96\", \"⏴\"))\n    .replace(/↳/g, color(\"95\", \"↳\"))\n    .replace(/ꜛ/g, color(\"33\", \"ꜛ\"))\n    .replace(/ꜜ/g, color(\"95\", \"ꜜ\"))\n    .replace(/…/g, color(\"94\", \"…\"))\n    .replace(/[(){}=]/g, (char) => color(\"94\", char))\n    .replace(\n      /([A-Za-z])(\\.)([A-Za-z])/g,\n      (_, left, dot, right) => `${left}${color(\"94\", dot)}${right}`,\n    )\n    .replace(/ ✓ /g, ` ${color(\"32\", \"✓\")} `)\n    .replace(/ ✕ /g, ` ${color(\"31\", \"✕\")} `);\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/actHandler.ts",
    "content": "// lib/v3/handlers/actHandler.ts\nimport { act as actInference } from \"../../inference.js\";\nimport { buildActPrompt, buildStepTwoPrompt } from \"../../prompt.js\";\nimport { trimTrailingTextNode } from \"../../utils.js\";\nimport { v3Logger } from \"../logger.js\";\nimport { ActHandlerParams } from \"../types/private/handlers.js\";\nimport { ActResult, Action, V3FunctionName } from \"../types/public/methods.js\";\nimport { ActTimeoutError } from \"../types/public/sdkErrors.js\";\nimport {\n  captureHybridSnapshot,\n  diffCombinedTrees,\n} from \"../understudy/a11y/snapshot/index.js\";\nimport { LLMClient } from \"../llm/LLMClient.js\";\nimport { SupportedUnderstudyAction } from \"../types/private/index.js\";\nimport { EncodedId } from \"../types/private/internal.js\";\nimport {\n  AvailableModel,\n  ClientOptions,\n  ModelConfiguration,\n} from \"../types/public/model.js\";\nimport type { Variables } from \"../types/public/agent.js\";\nimport type { Page } from \"../understudy/page.js\";\nimport {\n  performUnderstudyMethod,\n  waitForDomNetworkQuiet,\n} from \"./handlerUtils/actHandlerUtils.js\";\nimport { createTimeoutGuard } from \"./handlerUtils/timeoutGuard.js\";\nimport { resolveVariableValue } from \"../agent/utils/variables.js\";\n\ntype ActInferenceElement = {\n  elementId?: string;\n  description: string;\n  method?: string;\n  arguments?: string[];\n};\n\ntype ActInferenceResponse = Awaited<ReturnType<typeof actInference>>;\n\nexport class ActHandler {\n  private readonly llmClient: LLMClient;\n  private readonly defaultModelName: AvailableModel;\n  private readonly defaultClientOptions: ClientOptions;\n  private readonly resolveLlmClient: (model?: ModelConfiguration) => LLMClient;\n  private readonly systemPrompt: string;\n  private readonly logInferenceToFile: boolean;\n  private readonly selfHeal: boolean;\n  private readonly onMetrics?: (\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ) => void;\n  private readonly defaultDomSettleTimeoutMs?: number;\n\n  constructor(\n    llmClient: LLMClient,\n    defaultModelName: AvailableModel,\n    defaultClientOptions: ClientOptions,\n    resolveLlmClient: (model?: ModelConfiguration) => LLMClient,\n    systemPrompt?: string,\n    logInferenceToFile?: boolean,\n    selfHeal?: boolean,\n    onMetrics?: (\n      functionName: V3FunctionName,\n      promptTokens: number,\n      completionTokens: number,\n      reasoningTokens: number,\n      cachedInputTokens: number,\n      inferenceTimeMs: number,\n    ) => void,\n    defaultDomSettleTimeoutMs?: number,\n  ) {\n    this.llmClient = llmClient;\n    this.defaultModelName = defaultModelName;\n    this.defaultClientOptions = defaultClientOptions;\n    this.resolveLlmClient = resolveLlmClient;\n    this.systemPrompt = systemPrompt ?? \"\";\n    this.logInferenceToFile = logInferenceToFile ?? false;\n    this.selfHeal = !!selfHeal;\n    this.onMetrics = onMetrics;\n    this.defaultDomSettleTimeoutMs = defaultDomSettleTimeoutMs;\n  }\n\n  private recordActMetrics(response: ActInferenceResponse): void {\n    this.onMetrics?.(\n      V3FunctionName.ACT,\n      response.prompt_tokens ?? 0,\n      response.completion_tokens ?? 0,\n      response.reasoning_tokens ?? 0,\n      response.cached_input_tokens ?? 0,\n      response.inference_time_ms ?? 0,\n    );\n  }\n\n  private async getActionFromLLM({\n    instruction,\n    domElements,\n    xpathMap,\n    llmClient,\n    requireMethodAndArguments = true,\n  }: {\n    instruction: string;\n    domElements: string;\n    xpathMap: Record<string, string>;\n    llmClient: LLMClient;\n    requireMethodAndArguments?: boolean;\n  }): Promise<{ action?: Action; response: ActInferenceResponse }> {\n    const response = await actInference({\n      instruction,\n      domElements,\n      llmClient,\n      userProvidedInstructions: this.systemPrompt,\n      logger: v3Logger,\n      logInferenceToFile: this.logInferenceToFile,\n    });\n\n    this.recordActMetrics(response);\n\n    const normalized = normalizeActInferenceElement(\n      response.element as ActInferenceElement | undefined,\n      xpathMap,\n      requireMethodAndArguments,\n    );\n\n    if (!normalized) {\n      return { response };\n    }\n\n    return {\n      action: { ...normalized } as Action,\n      response,\n    };\n  }\n\n  async act(params: ActHandlerParams): Promise<ActResult> {\n    const { instruction, page, variables, timeout, model } = params;\n\n    const llmClient = this.resolveLlmClient(model);\n    const ensureTimeRemaining = createTimeoutGuard(\n      timeout,\n      (ms) => new ActTimeoutError(ms),\n    );\n\n    ensureTimeRemaining();\n    await waitForDomNetworkQuiet(\n      page.mainFrame(),\n      this.defaultDomSettleTimeoutMs,\n    );\n    ensureTimeRemaining();\n    const { combinedTree, combinedXpathMap } = await captureHybridSnapshot(\n      page,\n      { experimental: true },\n    );\n\n    const actInstruction = buildActPrompt(\n      instruction,\n      Object.values(SupportedUnderstudyAction),\n      variables,\n    );\n\n    ensureTimeRemaining();\n    const { action: firstAction, response: actInferenceResponse } =\n      await this.getActionFromLLM({\n        instruction: actInstruction,\n        domElements: combinedTree,\n        xpathMap: combinedXpathMap,\n        llmClient,\n      });\n\n    if (!firstAction) {\n      v3Logger({\n        category: \"action\",\n        message: \"no actionable element returned by LLM\",\n        level: 1,\n      });\n      return {\n        success: false,\n        message: \"Failed to perform act: No action found\",\n        actionDescription: instruction,\n        actions: [],\n      };\n    }\n\n    // First action (self-heal aware path)\n    ensureTimeRemaining();\n    const firstResult = await this.takeDeterministicAction(\n      firstAction,\n      page,\n      this.defaultDomSettleTimeoutMs,\n      llmClient,\n      ensureTimeRemaining,\n      variables,\n    );\n\n    // If not two-step, return the first action result\n    if (actInferenceResponse?.twoStep !== true) {\n      return firstResult;\n    }\n\n    // Take a new focused snapshot and observe again\n    ensureTimeRemaining();\n    const { combinedTree: combinedTree2, combinedXpathMap: combinedXpathMap2 } =\n      await captureHybridSnapshot(page, {\n        experimental: true,\n      });\n\n    let diffedTree = diffCombinedTrees(combinedTree, combinedTree2);\n    if (!diffedTree.trim()) {\n      // Fallback: if no diff detected, use the fresh tree to avoid empty context\n      diffedTree = combinedTree2;\n    }\n\n    const previousAction = `method: ${firstAction.method}, description: ${firstAction.description}, arguments: ${firstAction.arguments}`;\n\n    const stepTwoInstructions = buildStepTwoPrompt(\n      instruction,\n      previousAction,\n      Object.values(SupportedUnderstudyAction).filter(\n        (\n          action,\n        ): action is Exclude<\n          SupportedUnderstudyAction,\n          SupportedUnderstudyAction.SELECT_OPTION_FROM_DROPDOWN\n        > => action !== SupportedUnderstudyAction.SELECT_OPTION_FROM_DROPDOWN,\n      ),\n      variables,\n    );\n\n    ensureTimeRemaining();\n    const { action: secondAction } = await this.getActionFromLLM({\n      instruction: stepTwoInstructions,\n      domElements: diffedTree,\n      xpathMap: combinedXpathMap2,\n      llmClient,\n    });\n\n    if (!secondAction) {\n      // No second action found — return first result as-is\n      return firstResult;\n    }\n\n    ensureTimeRemaining();\n    const secondResult = await this.takeDeterministicAction(\n      secondAction,\n      page,\n      this.defaultDomSettleTimeoutMs,\n      llmClient,\n      ensureTimeRemaining,\n      variables,\n    );\n\n    // Combine results\n    return {\n      success: firstResult.success && secondResult.success,\n      message: secondResult.success\n        ? `${firstResult.message} → ${secondResult.message}`\n        : `${firstResult.message} → ${secondResult.message}`,\n      actionDescription: firstResult.actionDescription,\n      actions: [\n        ...(firstResult.actions || []),\n        ...(secondResult.actions || []),\n      ],\n    };\n  }\n\n  async takeDeterministicAction(\n    action: Action,\n    page: Page,\n    domSettleTimeoutMs?: number,\n    llmClientOverride?: LLMClient,\n    ensureTimeRemaining?: () => void,\n    variables?: Variables,\n  ): Promise<ActResult> {\n    ensureTimeRemaining?.();\n    const settleTimeout = domSettleTimeoutMs ?? this.defaultDomSettleTimeoutMs;\n    const effectiveClient = llmClientOverride ?? this.llmClient;\n    const method = action.method?.trim();\n    if (!method || method === \"not-supported\") {\n      v3Logger({\n        category: \"action\",\n        message: \"action has no supported method\",\n        level: 0,\n        auxiliary: {\n          act: { value: JSON.stringify(action), type: \"object\" },\n        },\n      });\n      return {\n        success: false,\n        message: `Unable to perform action: The method '${method ?? \"\"}' is not supported in Action. Please use a supported Playwright locator method.`,\n        actionDescription:\n          action.description || `Action (${method ?? \"unknown\"})`,\n        actions: [],\n      };\n    }\n\n    const placeholderArgs = Array.isArray(action.arguments)\n      ? [...action.arguments]\n      : [];\n    const resolvedArgs =\n      substituteVariablesInArguments(action.arguments, variables) ?? [];\n\n    try {\n      ensureTimeRemaining?.();\n      await performUnderstudyMethod(\n        page,\n        page.mainFrame(),\n        method,\n        action.selector,\n        resolvedArgs,\n        settleTimeout,\n      );\n      return {\n        success: true,\n        message: `Action [${method}] performed successfully on selector: ${action.selector}`,\n        actionDescription: action.description || `action (${method})`,\n        actions: [\n          {\n            selector: action.selector,\n            description: action.description || `action (${method})`,\n            method,\n            arguments: placeholderArgs,\n          },\n        ],\n      };\n    } catch (err) {\n      if (err instanceof ActTimeoutError) {\n        throw err;\n      }\n      const msg = err instanceof Error ? err.message : String(err);\n\n      // Attempt self-heal: rerun actInference and retry with updated selector\n      if (this.selfHeal) {\n        v3Logger({\n          category: \"action\",\n          message:\n            \"Error performing action. Reprocessing the page and trying again\",\n          level: 1,\n          auxiliary: {\n            error: { value: msg, type: \"string\" },\n            action: {\n              value: JSON.stringify(action),\n              type: \"object\",\n            },\n          },\n        });\n\n        try {\n          // Build an instruction combining method + description, avoiding duplication\n          const actCommand = action.description\n            ? action.description.toLowerCase().startsWith(method.toLowerCase())\n              ? action.description\n              : `${method} ${action.description}`\n            : method;\n\n          // Take a fresh snapshot and ask for a new actionable element\n          ensureTimeRemaining?.();\n          const { combinedTree, combinedXpathMap } =\n            await captureHybridSnapshot(page, {\n              experimental: true,\n            });\n\n          const instruction = buildActPrompt(\n            actCommand,\n            Object.values(SupportedUnderstudyAction),\n            {},\n          );\n\n          ensureTimeRemaining?.();\n          const { action: fallbackAction, response: fallbackResponse } =\n            await this.getActionFromLLM({\n              instruction,\n              domElements: combinedTree,\n              xpathMap: combinedXpathMap,\n              llmClient: effectiveClient,\n              requireMethodAndArguments: false,\n            });\n\n          const fallbackElement = fallbackResponse.element;\n          if (!fallbackElement) {\n            return {\n              success: false,\n              message:\n                \"Failed to self-heal act: No observe results found for action\",\n              actionDescription: actCommand,\n              actions: [],\n            };\n          }\n\n          // Retry with original method/args but new selector from fallback\n          let newSelector = action.selector;\n          if (fallbackAction?.selector) {\n            newSelector = fallbackAction.selector;\n          }\n\n          ensureTimeRemaining?.();\n          await performUnderstudyMethod(\n            page,\n            page.mainFrame(),\n            method,\n            newSelector,\n            resolvedArgs,\n            settleTimeout,\n          );\n\n          return {\n            success: true,\n            message: `Action [${method}] performed successfully on selector: ${newSelector}`,\n            actionDescription: action.description || `action (${method})`,\n            actions: [\n              {\n                selector: newSelector,\n                description: action.description || `action (${method})`,\n                method,\n                arguments: placeholderArgs,\n              },\n            ],\n          };\n        } catch (retryErr) {\n          if (retryErr instanceof ActTimeoutError) {\n            throw retryErr;\n          }\n          const retryMsg =\n            retryErr instanceof Error ? retryErr.message : String(retryErr);\n          return {\n            success: false,\n            message: `Failed to perform act after self-heal: ${retryMsg}`,\n            actionDescription: action.description || `action (${method})`,\n            actions: [],\n          };\n        }\n      }\n\n      return {\n        success: false,\n        message: `Failed to perform act: ${msg}`,\n        actionDescription: action.description || `action (${method})`,\n        actions: [],\n      };\n    }\n  }\n}\n\nfunction normalizeActInferenceElement(\n  element: ActInferenceElement | undefined,\n  xpathMap: Record<string, string>,\n  requireMethodAndArguments = true,\n): Action | undefined {\n  if (!element) {\n    return undefined;\n  }\n  const { elementId, description, method, arguments: args } = element;\n  const hasArgs = Array.isArray(args);\n\n  if (\n    requireMethodAndArguments &&\n    (!method || method === \"not-supported\" || !hasArgs)\n  ) {\n    return undefined;\n  }\n\n  if (typeof elementId !== \"string\" || !elementId.includes(\"-\")) {\n    return undefined;\n  }\n\n  const xp = xpathMap[elementId as EncodedId];\n  const trimmed = trimTrailingTextNode(xp);\n  if (!trimmed) {\n    return undefined;\n  }\n\n  // For dragAndDrop, convert element ID in arguments to xpath (target element)\n  let resolvedArgs = hasArgs ? args : undefined;\n  if (method === \"dragAndDrop\" && hasArgs && args.length > 0) {\n    const targetArg = args[0];\n    // Check if argument looks like an element ID (e.g., \"1-67\")\n    if (typeof targetArg === \"string\" && /^\\d+-\\d+$/.test(targetArg)) {\n      const argXpath = xpathMap[targetArg as EncodedId];\n      const trimmedArgXpath = trimTrailingTextNode(argXpath);\n      if (trimmedArgXpath) {\n        resolvedArgs = [`xpath=${trimmedArgXpath}`, ...args.slice(1)];\n      } else {\n        // Target element lookup failed, filter out this action\n        v3Logger({\n          category: \"action\",\n          message: \"dragAndDrop target element lookup failed\",\n          level: 1,\n          auxiliary: {\n            targetElementId: { value: targetArg, type: \"string\" },\n            sourceElementId: { value: elementId, type: \"string\" },\n          },\n        });\n        return undefined;\n      }\n    } else {\n      v3Logger({\n        category: \"action\",\n        message: \"dragAndDrop target element invalid ID format\",\n        level: 0,\n        auxiliary: {\n          targetElementId: { value: String(targetArg), type: \"string\" },\n          sourceElementId: { value: elementId, type: \"string\" },\n        },\n      });\n      return undefined;\n    }\n  }\n\n  return {\n    description,\n    method,\n    arguments: resolvedArgs,\n    selector: `xpath=${trimmed}`,\n  } as Action;\n}\n\nfunction substituteVariablesInArguments(\n  args: string[] | undefined,\n  variables?: Variables,\n): string[] | undefined {\n  if (!variables || !Array.isArray(args)) {\n    return args;\n  }\n\n  return args.map((arg: string) => {\n    let out = arg;\n    for (const [key, v] of Object.entries(variables)) {\n      const token = `%${key}%`;\n      out = out.split(token).join(resolveVariableValue(v));\n    }\n    return out;\n  });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/extractHandler.ts",
    "content": "// lib/v3/handlers/extractHandler.ts\nimport { extract as runExtract } from \"../../inference.js\";\nimport {\n  getZFactory,\n  getZodType,\n  injectUrls,\n  transformSchema,\n} from \"../../utils.js\";\nimport { v3Logger } from \"../logger.js\";\nimport { V3FunctionName } from \"../types/public/methods.js\";\nimport { captureHybridSnapshot } from \"../understudy/a11y/snapshot/index.js\";\nimport type { ZodTypeAny } from \"zod\";\nimport { LLMClient } from \"../llm/LLMClient.js\";\nimport { ExtractHandlerParams } from \"../types/private/handlers.js\";\nimport { EncodedId, ZodPathSegments } from \"../types/private/internal.js\";\nimport {\n  defaultExtractSchema,\n  pageTextSchema,\n} from \"../types/public/methods.js\";\nimport {\n  AvailableModel,\n  ClientOptions,\n  ModelConfiguration,\n} from \"../types/public/model.js\";\nimport {\n  StagehandInvalidArgumentError,\n  ExtractTimeoutError,\n} from \"../types/public/sdkErrors.js\";\nimport { createTimeoutGuard } from \"./handlerUtils/timeoutGuard.js\";\nimport type {\n  InferStagehandSchema,\n  StagehandZodObject,\n  StagehandZodSchema,\n} from \"../zodCompat.js\";\n\n/**\n * Scans the provided Zod schema for any `z.string().url()` fields and\n * replaces them with `z.number()`.\n *\n * @param schema - The Zod object schema to transform.\n * @returns A tuple containing:\n *   1. The transformed schema (or the original schema if no changes were needed).\n *   2. An array of {@link ZodPathSegments} objects representing all the replaced URL fields,\n *      with each path segment showing where in the schema the replacement occurred.\n */\nexport function transformUrlStringsToNumericIds<T extends StagehandZodSchema>(\n  schema: T,\n): [StagehandZodSchema, ZodPathSegments[]] {\n  const [finalSchema, urlPaths] = transformSchema(schema, []);\n  return [finalSchema, urlPaths];\n}\n\ninterface ExtractionResponseBase {\n  metadata: { completed: boolean };\n  prompt_tokens: number;\n  completion_tokens: number;\n  reasoning_tokens: number;\n  cached_input_tokens?: number;\n  inference_time_ms: number;\n}\n\ntype ExtractionResponse<T extends StagehandZodObject> = ExtractionResponseBase &\n  InferStagehandSchema<T>;\n\nexport class ExtractHandler {\n  private readonly llmClient: LLMClient;\n  private readonly defaultModelName: AvailableModel;\n  private readonly defaultClientOptions: ClientOptions;\n  private readonly resolveLlmClient: (model?: ModelConfiguration) => LLMClient;\n  private readonly systemPrompt: string;\n  private readonly logInferenceToFile: boolean;\n  private readonly experimental: boolean;\n  private readonly onMetrics?: (\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ) => void;\n\n  constructor(\n    llmClient: LLMClient,\n    defaultModelName: AvailableModel,\n    defaultClientOptions: ClientOptions,\n    resolveLlmClient: (model?: ModelConfiguration) => LLMClient,\n    systemPrompt?: string,\n    logInferenceToFile?: boolean,\n    experimental?: boolean,\n    onMetrics?: (\n      functionName: V3FunctionName,\n      promptTokens: number,\n      completionTokens: number,\n      reasoningTokens: number,\n      cachedInputTokens: number,\n      inferenceTimeMs: number,\n    ) => void,\n  ) {\n    this.llmClient = llmClient;\n    this.defaultModelName = defaultModelName;\n    this.defaultClientOptions = defaultClientOptions;\n    this.resolveLlmClient = resolveLlmClient;\n    this.systemPrompt = systemPrompt ?? \"\";\n    this.logInferenceToFile = logInferenceToFile ?? false;\n    this.experimental = experimental ?? false;\n    this.onMetrics = onMetrics;\n  }\n\n  async extract<T extends StagehandZodSchema>(\n    params: ExtractHandlerParams<T>,\n  ): Promise<InferStagehandSchema<T> | { pageText: string }> {\n    const { instruction, schema, page, selector, timeout, model } = params;\n\n    const llmClient = this.resolveLlmClient(model);\n\n    const ensureTimeRemaining = createTimeoutGuard(\n      timeout,\n      (ms) => new ExtractTimeoutError(ms),\n    );\n\n    // No-args → page text (parity with v2)\n    const noArgs = !instruction && !schema;\n    if (noArgs) {\n      const focusSelector = selector?.replace(/^xpath=/i, \"\") ?? \"\";\n      ensureTimeRemaining();\n      const snap = await captureHybridSnapshot(page, {\n        experimental: this.experimental,\n        focusSelector: focusSelector || undefined,\n      });\n      ensureTimeRemaining();\n\n      const result = { pageText: snap.combinedTree };\n      // Validate via the same schema used in v2\n      return pageTextSchema.parse(result);\n    }\n\n    if (!instruction && schema) {\n      throw new StagehandInvalidArgumentError(\n        \"extract() requires an instruction when a schema is provided.\",\n      );\n    }\n\n    const focusSelector = selector?.replace(/^xpath=/, \"\") ?? \"\";\n\n    // Build the hybrid snapshot (includes combinedTree; combinedUrlMap optional)\n    ensureTimeRemaining();\n    const { combinedTree, combinedUrlMap } = await captureHybridSnapshot(page, {\n      experimental: this.experimental,\n      focusSelector: focusSelector,\n    });\n\n    v3Logger({\n      category: \"extraction\",\n      message: \"Starting extraction using a11y snapshot\",\n      level: 1,\n      auxiliary: instruction\n        ? { instruction: { value: instruction, type: \"string\" } }\n        : undefined,\n    });\n\n    // Normalize schema: if instruction provided without schema, use defaultExtractSchema\n    const baseSchema: StagehandZodSchema = (schema ??\n      defaultExtractSchema) as StagehandZodSchema;\n    // Ensure we pass an object schema into inference; wrap non-object schemas\n    const isObjectSchema = getZodType(baseSchema) === \"object\";\n    const WRAP_KEY = \"value\" as const;\n    const factory = getZFactory(baseSchema);\n    const objectSchema: StagehandZodObject = isObjectSchema\n      ? (baseSchema as StagehandZodObject)\n      : (factory.object({\n          [WRAP_KEY]: baseSchema as ZodTypeAny,\n        }) as StagehandZodObject);\n\n    const [transformedSchema, urlFieldPaths] =\n      transformUrlStringsToNumericIds(objectSchema);\n\n    ensureTimeRemaining();\n    const extractionResponse: ExtractionResponse<StagehandZodObject> =\n      await runExtract<StagehandZodObject>({\n        instruction,\n        domElements: combinedTree,\n        schema: transformedSchema as StagehandZodObject,\n        llmClient,\n        userProvidedInstructions: this.systemPrompt,\n        logger: v3Logger,\n        logInferenceToFile: this.logInferenceToFile,\n      });\n\n    const {\n      metadata: { completed },\n      prompt_tokens,\n      completion_tokens,\n      reasoning_tokens = 0,\n      cached_input_tokens = 0,\n      inference_time_ms,\n      ...rest\n    } = extractionResponse;\n    let output = rest as InferStagehandSchema<StagehandZodObject>;\n\n    // Update EXTRACT metrics from the LLM calls\n    this.onMetrics?.(\n      V3FunctionName.EXTRACT,\n      prompt_tokens,\n      completion_tokens,\n      reasoning_tokens,\n      cached_input_tokens,\n      inference_time_ms,\n    );\n\n    // Re-inject URLs for any url() fields we temporarily converted to number()\n    const idToUrl: Record<EncodedId, string> = (combinedUrlMap ?? {}) as Record<\n      EncodedId,\n      string\n    >;\n    for (const { segments } of urlFieldPaths) {\n      injectUrls(\n        output as Record<string, unknown>,\n        segments,\n        idToUrl as unknown as Record<string, string>,\n      );\n    }\n    // If we wrapped a non-object schema, unwrap the value\n    if (!isObjectSchema && output && typeof output === \"object\") {\n      output = (output as Record<string, unknown>)[WRAP_KEY];\n    }\n\n    const resultPreviewLength = 200;\n    const resultString = JSON.stringify(output) ?? \"undefined\";\n    const resultPreview =\n      resultString.length > resultPreviewLength\n        ? resultString.slice(0, resultPreviewLength) + \"...\"\n        : resultString;\n\n    v3Logger({\n      category: \"extraction\",\n      message: completed\n        ? \"Extraction completed successfully\"\n        : \"Extraction incomplete after processing all data\",\n      level: 1,\n      auxiliary: {\n        prompt_tokens: { value: String(prompt_tokens), type: \"string\" },\n        completion_tokens: { value: String(completion_tokens), type: \"string\" },\n        inference_time_ms: {\n          value: String(inference_time_ms),\n          type: \"string\",\n        },\n        result: { value: resultPreview, type: \"string\" },\n      },\n    });\n\n    return output as InferStagehandSchema<T>;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts",
    "content": "// lib/v3/handlers/handlerUtils/actHandlerUtils.ts\nimport { Protocol } from \"devtools-protocol\";\nimport { Frame } from \"../../understudy/frame.js\";\nimport { Locator } from \"../../understudy/locator.js\";\nimport { MouseButton } from \"../../types/public/locator.js\";\nimport { resolveLocatorWithHops } from \"../../understudy/deepLocator.js\";\nimport type { Page } from \"../../understudy/page.js\";\nimport { v3Logger } from \"../../logger.js\";\nimport { FlowLogger } from \"../../flowlogger/FlowLogger.js\";\nimport { toTitleCase } from \"../../../utils.js\";\nimport {\n  StagehandClickError,\n  UnderstudyCommandException,\n} from \"../../types/public/sdkErrors.js\";\n\nexport interface UnderstudyMethodHandlerContext {\n  method: string;\n  locator: Locator;\n  xpath: string;\n  args: ReadonlyArray<string>;\n  frame: Frame;\n  page: Page;\n  initialUrl: string;\n  domSettleTimeoutMs?: number;\n}\n\n// Normalize cases where the XPath is the root \"/\" to point to the HTML element.\nfunction normalizeRootXPath(input: string): string {\n  const s = String(input ?? \"\").trim();\n  if (s === \"/\") return \"/html\";\n  if (/^xpath=\\/$/i.test(s)) return \"xpath=/html\";\n  return s;\n}\n\nexport async function performUnderstudyMethod(\n  page: Page,\n  frame: Frame,\n  method: string,\n  rawXPath: string,\n  args: ReadonlyArray<unknown>,\n  domSettleTimeoutMs?: number,\n): Promise<void> {\n  const selectorRaw = normalizeRootXPath(rawXPath);\n\n  try {\n    await FlowLogger.runWithLogging(\n      {\n        eventType: `Understudy${toTitleCase(method)}`, // e.g. \"UnderstudyClick\"\n        data: {\n          target: selectorRaw,\n        },\n      },\n      async () => {\n        // Unified resolver: supports '>>' hops and XPath across iframes.\n        const locator: Locator = await resolveLocatorWithHops(\n          page,\n          frame,\n          selectorRaw,\n        );\n        const initialUrl = await getFrameUrl(frame);\n\n        v3Logger({\n          category: \"action\",\n          message: \"performing understudy method\",\n          level: 2,\n          auxiliary: {\n            xpath: { value: selectorRaw, type: \"string\" },\n            method: { value: method, type: \"string\" },\n            url: { value: initialUrl, type: \"string\" },\n          },\n        });\n\n        const ctx: UnderstudyMethodHandlerContext = {\n          method,\n          locator,\n          xpath: selectorRaw,\n          args: args.map((a) => (a == null ? \"\" : String(a))),\n          frame,\n          page,\n          initialUrl,\n          domSettleTimeoutMs,\n        };\n        const handler = METHOD_HANDLER_MAP[method] ?? null;\n\n        if (handler) {\n          await handler(ctx);\n          return;\n        }\n\n        v3Logger({\n          category: \"action\",\n          message: \"chosen method is invalid\",\n          level: 1,\n          auxiliary: { method: { value: method, type: \"string\" } },\n        });\n        throw new UnderstudyCommandException(`Method ${method} not supported`);\n      },\n      args,\n    );\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    const stack = e instanceof Error ? e.stack : undefined;\n    v3Logger({\n      category: \"action\",\n      message: \"error performing method\",\n      level: 1,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        trace: { value: stack ?? \"\", type: \"string\" },\n        method: { value: method, type: \"string\" },\n        xpath: { value: selectorRaw, type: \"string\" },\n        args: { value: JSON.stringify(args), type: \"object\" },\n      },\n    });\n    if (e instanceof UnderstudyCommandException) {\n      throw e;\n    }\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\n/* ===================== Handlers & Map ===================== */\n\nconst METHOD_HANDLER_MAP: Record<\n  string,\n  (ctx: UnderstudyMethodHandlerContext) => Promise<void>\n> = {\n  scrollIntoView,\n  scrollByPixelOffset,\n  scrollTo: scrollElementToPercentage,\n  scroll: scrollElementToPercentage,\n  \"mouse.wheel\": wheelScroll,\n  fill: fillOrType,\n  type: typeText,\n  press: pressKey,\n  click: clickElement,\n  doubleClick,\n  dragAndDrop,\n  nextChunk: scrollToNextChunk,\n  prevChunk: scrollToPreviousChunk,\n  selectOptionFromDropdown: selectOption,\n  selectOption: selectOption,\n  hover: hover,\n};\n\nexport async function selectOption(ctx: UnderstudyMethodHandlerContext) {\n  const { locator, xpath, args } = ctx;\n  try {\n    const text = args[0]?.toString() || \"\";\n    await locator.selectOption(text);\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    const stack = e instanceof Error ? e.stack : undefined;\n    v3Logger({\n      category: \"action\",\n      message: \"error selecting option\",\n      level: 0,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        trace: { value: stack ?? \"\", type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function scrollIntoView(\n  ctx: UnderstudyMethodHandlerContext,\n): Promise<void> {\n  const { locator, xpath } = ctx;\n  v3Logger({\n    category: \"action\",\n    message: \"scrolling element into view\",\n    level: 2,\n    auxiliary: { xpath: { value: xpath, type: \"string\" } },\n  });\n  const { objectId } = await locator.resolveNode();\n  const ownerSession = locator.getFrame().session;\n  await ownerSession.send(\"DOM.scrollIntoViewIfNeeded\", { objectId });\n  await ownerSession\n    .send(\"Runtime.releaseObject\", { objectId })\n    .catch(() => {});\n}\n\nasync function scrollElementToPercentage(\n  ctx: UnderstudyMethodHandlerContext,\n): Promise<void> {\n  const { locator, xpath, args } = ctx;\n  v3Logger({\n    category: \"action\",\n    message: \"scrolling element vertically to specified percentage\",\n    level: 2,\n    auxiliary: {\n      xpath: { value: xpath, type: \"string\" },\n      coordinate: { value: JSON.stringify(args), type: \"string\" },\n    },\n  });\n\n  const [yArg = \"0%\"] = args;\n  await locator.scrollTo(yArg);\n}\n\n/** Scroll the page by pixel offset, starting from the element's center. */\nasync function scrollByPixelOffset(\n  ctx: UnderstudyMethodHandlerContext,\n): Promise<void> {\n  const { locator, page, args } = ctx;\n  const dx = Number(args[0] ?? 0);\n  const dy = Number(args[1] ?? 0);\n\n  try {\n    const { x, y } = await locator.centroid();\n    await page.scroll(x, y, dx, dy);\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function wheelScroll(ctx: UnderstudyMethodHandlerContext): Promise<void> {\n  const { frame, args } = ctx;\n  const deltaY = Number(args[0] ?? 200);\n  v3Logger({\n    category: \"action\",\n    message: \"dispatching mouse wheel\",\n    level: 2,\n    auxiliary: { deltaY: { value: String(deltaY), type: \"string\" } },\n  });\n  await frame.session.send<never>(\"Input.dispatchMouseEvent\", {\n    type: \"mouseWheel\",\n    x: 0,\n    y: 0,\n    deltaY,\n    deltaX: 0,\n  } as Protocol.Input.DispatchMouseEventRequest);\n}\n\nasync function fillOrType(ctx: UnderstudyMethodHandlerContext): Promise<void> {\n  const { locator, xpath, args } = ctx;\n  try {\n    await locator.fill(\"\"); // clear\n    await locator.fill(args[0] ?? \"\");\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    v3Logger({\n      category: \"action\",\n      message: \"error filling element\",\n      level: 1,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function typeText(ctx: UnderstudyMethodHandlerContext): Promise<void> {\n  const { locator, xpath, args } = ctx;\n  try {\n    await locator.type(args[0] ?? \"\");\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    v3Logger({\n      category: \"action\",\n      message: \"error typing into element\",\n      level: 1,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function pressKey(ctx: UnderstudyMethodHandlerContext): Promise<void> {\n  const { args, xpath, page } = ctx;\n  const key = args[0] ?? \"\";\n  try {\n    v3Logger({\n      category: \"action\",\n      message: \"pressing key\",\n      level: 1,\n      auxiliary: {\n        key: { value: key, type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    await page.keyPress(key);\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    v3Logger({\n      category: \"action\",\n      message: \"error pressing key\",\n      level: 1,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        key: { value: key, type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function clickElement(\n  ctx: UnderstudyMethodHandlerContext,\n): Promise<void> {\n  const { locator, xpath, args } = ctx;\n  try {\n    await locator.click({ button: (args[0] as MouseButton) || undefined });\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    v3Logger({\n      category: \"action\",\n      message: \"error performing click\",\n      level: 0,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new StagehandClickError(ctx.xpath, msg);\n  }\n}\n\nasync function doubleClick(ctx: UnderstudyMethodHandlerContext): Promise<void> {\n  const { locator, xpath } = ctx;\n  try {\n    await locator.click({ clickCount: 2 });\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    v3Logger({\n      category: \"action\",\n      message: \"error performing doubleClick\",\n      level: 0,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function dragAndDrop(ctx: UnderstudyMethodHandlerContext): Promise<void> {\n  const { page, frame, locator, args, xpath } = ctx;\n  const toXPath = String(args[0] ?? \"\").trim();\n  if (!toXPath)\n    throw new UnderstudyCommandException(\n      \"dragAndDrop requires a target XPath arg\",\n    );\n\n  const targetLocator = await resolveLocatorWithHops(page, frame, toXPath);\n\n  try {\n    // 1) Centers in local (owning-frame) viewport\n    const { x: fromLocalX, y: fromLocalY } = await locator.centroid();\n    const { x: toLocalX, y: toLocalY } = await targetLocator.centroid();\n\n    // 2) Convert to main-viewport absolute coordinates\n    const fromAbs = await locator\n      .getFrame()\n      .evaluate<{ x: number; y: number }, { x: number; y: number }>(\n        ({ x, y }: { x: number; y: number }) => {\n          let X = x;\n          let Y = y;\n          let w: Window = window;\n          while (w !== w.top) {\n            const fe = w.frameElement as HTMLElement | null;\n            if (!fe) break;\n            const r = fe.getBoundingClientRect();\n            X += r.left;\n            Y += r.top;\n            w = w.parent as Window;\n          }\n          return { x: Math.round(X), y: Math.round(Y) };\n        },\n        { x: fromLocalX, y: fromLocalY },\n      );\n\n    const toAbs = await targetLocator\n      .getFrame()\n      .evaluate<{ x: number; y: number }, { x: number; y: number }>(\n        ({ x, y }: { x: number; y: number }) => {\n          let X = x;\n          let Y = y;\n          let w: Window = window;\n          while (w !== w.top) {\n            const fe = w.frameElement as HTMLElement | null;\n            if (!fe) break;\n            const r = fe.getBoundingClientRect();\n            X += r.left;\n            Y += r.top;\n            w = w.parent as Window;\n          }\n          return { x: Math.round(X), y: Math.round(Y) };\n        },\n        { x: toLocalX, y: toLocalY },\n      );\n\n    // 3) Perform drag in main session\n    await page.dragAndDrop(fromAbs.x, fromAbs.y, toAbs.x, toAbs.y, {\n      steps: 10,\n      delay: 5,\n    });\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    v3Logger({\n      category: \"action\",\n      message: \"error performing dragAndDrop\",\n      level: 0,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        from: { value: xpath, type: \"string\" },\n        to: { value: toXPath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\nasync function scrollToNextChunk(\n  ctx: UnderstudyMethodHandlerContext,\n): Promise<void> {\n  await scrollByElementHeight(ctx, /*dir=*/ 1);\n}\n\nasync function scrollToPreviousChunk(\n  ctx: UnderstudyMethodHandlerContext,\n): Promise<void> {\n  await scrollByElementHeight(ctx, /*dir=*/ -1);\n}\n\nasync function scrollByElementHeight(\n  ctx: UnderstudyMethodHandlerContext,\n  direction: 1 | -1,\n): Promise<void> {\n  const { locator, xpath } = ctx;\n  v3Logger({\n    category: \"action\",\n    message:\n      direction > 0 ? \"scrolling to next chunk\" : \"scrolling to previous chunk\",\n    level: 2,\n    auxiliary: { xpath: { value: xpath, type: \"string\" } },\n  });\n\n  const { objectId } = await locator.resolveNode();\n  try {\n    const ownerSession = locator.getFrame().session;\n    await ownerSession.send<Protocol.Runtime.CallFunctionOnResponse>(\n      \"Runtime.callFunctionOn\",\n      {\n        objectId,\n        functionDeclaration: `\n          function(dir) {\n            const waitForScrollEnd = (el) => new Promise((resolve) => {\n              let last = el.scrollTop ?? 0;\n              const check = () => {\n                const cur = el.scrollTop ?? 0;\n                if (cur === last) return resolve();\n                last = cur;\n                requestAnimationFrame(check);\n              };\n              requestAnimationFrame(check);\n            });\n\n            const tag = this.tagName?.toLowerCase();\n            if (tag === \"html\" || tag === \"body\") {\n              const h = window.visualViewport?.height ?? window.innerHeight;\n              window.scrollBy({ top: h * dir, left: 0, behavior: \"smooth\" });\n              const root = document.scrollingElement ?? document.documentElement;\n              return waitForScrollEnd(root);\n            }\n            const h = this.getBoundingClientRect().height;\n            this.scrollBy({ top: h * dir, left: 0, behavior: \"smooth\" });\n            return waitForScrollEnd(this);\n          }\n        `,\n        arguments: [{ value: direction }],\n        awaitPromise: true,\n        returnByValue: true,\n      },\n    );\n  } finally {\n    const ownerSession = locator.getFrame().session;\n    await ownerSession\n      .send(\"Runtime.releaseObject\", { objectId })\n      .catch(() => {});\n  }\n}\n\nexport async function hover(ctx: UnderstudyMethodHandlerContext) {\n  const { locator, xpath } = ctx;\n  try {\n    await locator.hover();\n  } catch (e) {\n    const msg = e instanceof Error ? e.message : String(e);\n    const stack = e instanceof Error ? e.stack : undefined;\n    v3Logger({\n      category: \"action\",\n      message: \"error attempting to hover\",\n      level: 0,\n      auxiliary: {\n        error: { value: msg, type: \"string\" },\n        trace: { value: stack ?? \"\", type: \"string\" },\n        xpath: { value: xpath, type: \"string\" },\n      },\n    });\n    throw new UnderstudyCommandException(msg, e);\n  }\n}\n\n/* ===================== Helpers ===================== */\n\nasync function getFrameUrl(frame: Frame): Promise<string> {\n  // Evaluate from within the frame's isolated world\n  const url = await frame.evaluate<string>(\"location.href\");\n  return url;\n}\n\n/**\n * More robust DOM settle using Network + Page events to detect network quiet.\n * Closely modeled after the provided snippet, adapted to our Frame/session + logger.\n */\nexport async function waitForDomNetworkQuiet(\n  frame: Frame,\n  timeoutMs?: number,\n): Promise<void> {\n  const overallTimeout =\n    typeof timeoutMs === \"number\" && Number.isFinite(timeoutMs)\n      ? Math.max(0, timeoutMs)\n      : 5_000;\n  const client = frame.session;\n  const settleStart = Date.now();\n\n  // Ensure a document exists; if not, wait for DOMContentLoaded on this frame.\n  let hasDoc: boolean;\n  try {\n    const rs = await frame.evaluate<string>(\"document.readyState\");\n    hasDoc = rs === \"interactive\" || rs === \"complete\";\n  } catch {\n    hasDoc = false;\n  }\n  if (!hasDoc && overallTimeout > 0) {\n    await frame\n      .waitForLoadState(\"domcontentloaded\", overallTimeout)\n      .catch(() => {});\n  }\n\n  const elapsed = Date.now() - settleStart;\n  const remainingBudget = Math.max(0, overallTimeout - elapsed);\n  if (remainingBudget === 0) {\n    return;\n  }\n\n  await client.send(\"Network.enable\").catch(() => {});\n  await client.send(\"Page.enable\").catch(() => {});\n  // Best-effort; some sessions may not support Target.setAutoAttach here.\n  await client\n    .send(\"Target.setAutoAttach\", {\n      autoAttach: true,\n      waitForDebuggerOnStart: false,\n      flatten: true,\n      filter: [\n        { type: \"worker\", exclude: true },\n        { type: \"shared_worker\", exclude: true },\n      ],\n    })\n    .catch(() => {});\n\n  return new Promise<void>((resolve) => {\n    const inflight = new Set<string>();\n    const meta = new Map<string, { url: string; start: number }>();\n    const docByFrame = new Map<string, string>();\n\n    let quietTimer: NodeJS.Timeout | null = null;\n    let stalledRequestSweepTimer: NodeJS.Timeout | null = null;\n\n    const clearQuiet = () => {\n      if (quietTimer) {\n        clearTimeout(quietTimer);\n        quietTimer = null;\n      }\n    };\n\n    const maybeQuiet = () => {\n      if (inflight.size === 0 && !quietTimer)\n        quietTimer = setTimeout(() => resolveDone(), 500);\n    };\n\n    const finishReq = (id: string) => {\n      if (!inflight.delete(id)) return;\n      meta.delete(id);\n      for (const [fid, rid] of docByFrame)\n        if (rid === id) docByFrame.delete(fid);\n      clearQuiet();\n      maybeQuiet();\n    };\n\n    const onRequest = (p: Protocol.Network.RequestWillBeSentEvent) => {\n      // Ignore long-lived streams\n      // ResourceType includes: Document, XHR, Fetch, WebSocket, EventSource, etc.\n      if (p.type === \"WebSocket\" || p.type === \"EventSource\") return;\n\n      inflight.add(p.requestId);\n      meta.set(p.requestId, { url: p.request.url, start: Date.now() });\n\n      if (p.type === \"Document\" && p.frameId)\n        docByFrame.set(p.frameId, p.requestId);\n\n      clearQuiet();\n    };\n\n    const onFinish = (p: { requestId: string }) => finishReq(p.requestId);\n    const onCached = (p: { requestId: string }) => finishReq(p.requestId);\n    const onDataUrl = (p: Protocol.Network.ResponseReceivedEvent) => {\n      if (p.response.url?.startsWith(\"data:\")) finishReq(p.requestId);\n    };\n\n    const onFrameStop = (f: Protocol.Page.FrameStoppedLoadingEvent) => {\n      const id = docByFrame.get(f.frameId);\n      if (id) finishReq(id);\n    };\n\n    client.on(\"Network.requestWillBeSent\", onRequest);\n    client.on(\"Network.loadingFinished\", onFinish);\n    client.on(\"Network.loadingFailed\", onFinish);\n    client.on(\"Network.requestServedFromCache\", onCached);\n    client.on(\"Network.responseReceived\", onDataUrl);\n    client.on(\"Page.frameStoppedLoading\", onFrameStop);\n\n    stalledRequestSweepTimer = setInterval(() => {\n      const now = Date.now();\n      for (const [id, m] of meta) {\n        if (now - m.start > 2_000) {\n          inflight.delete(id);\n          meta.delete(id);\n          v3Logger({\n            category: \"dom\",\n            message: \"⏳ forcing completion of stalled iframe document\",\n            level: 1,\n            auxiliary: {\n              url: { value: (m.url ?? \"\").slice(0, 120), type: \"string\" },\n            },\n          });\n        }\n      }\n      maybeQuiet();\n    }, 500);\n\n    maybeQuiet();\n\n    const guard = setTimeout(() => {\n      if (inflight.size) {\n        v3Logger({\n          category: \"dom\",\n          message:\n            \"⚠️ DOM-settle timeout reached – network requests still pending\",\n          level: 1,\n          auxiliary: {\n            count: { value: String(inflight.size), type: \"integer\" },\n          },\n        });\n      }\n      resolveDone();\n    }, remainingBudget);\n\n    const resolveDone = () => {\n      client.off(\"Network.requestWillBeSent\", onRequest);\n      client.off(\"Network.loadingFinished\", onFinish);\n      client.off(\"Network.loadingFailed\", onFinish);\n      client.off(\"Network.requestServedFromCache\", onCached);\n      client.off(\"Network.responseReceived\", onDataUrl);\n      client.off(\"Page.frameStoppedLoading\", onFrameStop);\n      if (quietTimer) clearTimeout(quietTimer);\n      if (stalledRequestSweepTimer) clearInterval(stalledRequestSweepTimer);\n      clearTimeout(guard);\n      resolve();\n    };\n  });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/handlerUtils/timeoutGuard.ts",
    "content": "import { TimeoutError } from \"../../types/public/sdkErrors.js\";\n\nexport type TimeoutGuard = () => void;\n\nexport function createTimeoutGuard(\n  timeoutMs?: number,\n  errorFactory?: (timeoutMs: number) => Error,\n): TimeoutGuard {\n  if (!timeoutMs || timeoutMs <= 0) {\n    return () => {};\n  }\n\n  const startTime = Date.now();\n  return () => {\n    if (Date.now() - startTime >= timeoutMs) {\n      const err =\n        errorFactory?.(timeoutMs) ?? new TimeoutError(\"operation\", timeoutMs);\n      throw err;\n    }\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/observeHandler.ts",
    "content": "// lib/v3/handlers/observeHandler.ts\nimport { observe as runObserve } from \"../../inference.js\";\nimport { trimTrailingTextNode } from \"../../utils.js\";\nimport { v3Logger } from \"../logger.js\";\nimport { V3FunctionName } from \"../types/public/methods.js\";\nimport { captureHybridSnapshot } from \"../understudy/a11y/snapshot/index.js\";\nimport { LLMClient } from \"../llm/LLMClient.js\";\nimport {\n  ObserveHandlerParams,\n  SupportedUnderstudyAction,\n} from \"../types/private/handlers.js\";\nimport { EncodedId } from \"../types/private/internal.js\";\nimport { Action } from \"../types/public/methods.js\";\nimport {\n  AvailableModel,\n  ClientOptions,\n  ModelConfiguration,\n} from \"../types/public/model.js\";\nimport { ObserveTimeoutError } from \"../types/public/sdkErrors.js\";\nimport { createTimeoutGuard } from \"./handlerUtils/timeoutGuard.js\";\n\nexport class ObserveHandler {\n  private readonly llmClient: LLMClient;\n  private readonly defaultModelName: AvailableModel;\n  private readonly defaultClientOptions: ClientOptions;\n  private readonly resolveLlmClient: (model?: ModelConfiguration) => LLMClient;\n  private readonly systemPrompt: string;\n  private readonly logInferenceToFile: boolean;\n  private readonly experimental: boolean;\n  private readonly onMetrics?: (\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ) => void;\n\n  constructor(\n    llmClient: LLMClient,\n    defaultModelName: AvailableModel,\n    defaultClientOptions: ClientOptions,\n    resolveLlmClient: (model?: ModelConfiguration) => LLMClient,\n    systemPrompt?: string,\n    logInferenceToFile?: boolean,\n    experimental?: boolean,\n    onMetrics?: (\n      functionName: V3FunctionName,\n      promptTokens: number,\n      completionTokens: number,\n      reasoningTokens: number,\n      cachedInputTokens: number,\n      inferenceTimeMs: number,\n    ) => void,\n  ) {\n    this.llmClient = llmClient;\n    this.defaultModelName = defaultModelName;\n    this.defaultClientOptions = defaultClientOptions;\n    this.resolveLlmClient = resolveLlmClient;\n    this.systemPrompt = systemPrompt ?? \"\";\n    this.logInferenceToFile = logInferenceToFile ?? false;\n    this.experimental = experimental ?? false;\n    this.onMetrics = onMetrics;\n  }\n\n  async observe(params: ObserveHandlerParams): Promise<Action[]> {\n    const { instruction, page, timeout, selector, model } = params;\n\n    const llmClient = this.resolveLlmClient(model);\n\n    const ensureTimeRemaining = createTimeoutGuard(\n      timeout,\n      (ms) => new ObserveTimeoutError(ms),\n    );\n\n    const effectiveInstruction =\n      instruction ??\n      \"Find elements that can be used for any future actions in the page. These may be navigation links, related pages, section/subsection links, buttons, or other interactive elements. Be comprehensive: if there are multiple elements that may be relevant for future actions, return all of them.\";\n\n    v3Logger({\n      category: \"observation\",\n      message: \"starting observation\",\n      level: 1,\n      auxiliary: {\n        instruction: {\n          value: effectiveInstruction,\n          type: \"string\",\n        },\n      },\n    });\n\n    // Build the hybrid snapshot (a11y-centric text tree + lookup maps)\n    const focusSelector = selector?.replace(/^xpath=/i, \"\") ?? \"\";\n    ensureTimeRemaining();\n    const snapshot = await captureHybridSnapshot(page, {\n      experimental: this.experimental,\n      focusSelector: focusSelector || undefined,\n    });\n\n    const combinedTree = snapshot.combinedTree;\n    const combinedXpathMap = snapshot.combinedXpathMap ?? {};\n\n    v3Logger({\n      category: \"observation\",\n      message: \"Got accessibility tree data\",\n      level: 1,\n    });\n\n    // Call the LLM to propose actionable elements\n    ensureTimeRemaining();\n    const observationResponse = await runObserve({\n      instruction: effectiveInstruction,\n      domElements: combinedTree,\n      llmClient,\n      userProvidedInstructions: this.systemPrompt,\n      logger: v3Logger,\n      logInferenceToFile: this.logInferenceToFile,\n      supportedActions: Object.values(SupportedUnderstudyAction),\n    });\n\n    const {\n      prompt_tokens = 0,\n      completion_tokens = 0,\n      reasoning_tokens = 0,\n      cached_input_tokens = 0,\n      inference_time_ms = 0,\n    } = observationResponse;\n\n    // Update OBSERVE metrics from the LLM observation call\n    this.onMetrics?.(\n      V3FunctionName.OBSERVE,\n      prompt_tokens,\n      completion_tokens,\n      reasoning_tokens,\n      cached_input_tokens,\n      inference_time_ms,\n    );\n\n    // Map elementIds -> selectors via combinedXpathMap\n    const elementsWithSelectors = (\n      await Promise.all(\n        observationResponse.elements.map(async (element) => {\n          const { elementId, ...rest } = element; // rest may or may not have method/arguments\n          if (typeof elementId === \"string\" && elementId.includes(\"-\")) {\n            const lookUpIndex = elementId as EncodedId;\n            const xpath = combinedXpathMap[lookUpIndex];\n            const trimmedXpath = trimTrailingTextNode(xpath);\n            if (!trimmedXpath) return undefined;\n\n            // For dragAndDrop, convert element ID in arguments to xpath (target element)\n            let resolvedArgs = rest.arguments;\n            if (\n              rest.method === \"dragAndDrop\" &&\n              Array.isArray(rest.arguments) &&\n              rest.arguments.length > 0\n            ) {\n              const targetArg = rest.arguments[0];\n              // Check if argument looks like an element ID (e.g., \"1-67\")\n              if (\n                typeof targetArg === \"string\" &&\n                /^\\d+-\\d+$/.test(targetArg)\n              ) {\n                const argXpath = combinedXpathMap[targetArg as EncodedId];\n                const trimmedArgXpath = trimTrailingTextNode(argXpath);\n                if (trimmedArgXpath) {\n                  resolvedArgs = [\n                    `xpath=${trimmedArgXpath}`,\n                    ...rest.arguments.slice(1),\n                  ];\n                } else {\n                  // Target element lookup failed, filter out this action\n                  v3Logger({\n                    category: \"observation\",\n                    message: \"dragAndDrop target element lookup failed\",\n                    level: 0,\n                    auxiliary: {\n                      targetElementId: { value: targetArg, type: \"string\" },\n                      sourceElementId: { value: elementId, type: \"string\" },\n                    },\n                  });\n                  return undefined;\n                }\n              } else {\n                v3Logger({\n                  category: \"observation\",\n                  message: \"dragAndDrop target element invalid ID format\",\n                  level: 0,\n                  auxiliary: {\n                    targetElementId: { value: targetArg, type: \"string\" },\n                    sourceElementId: { value: elementId, type: \"string\" },\n                  },\n                });\n                return undefined;\n              }\n            }\n\n            return {\n              ...rest,\n              arguments: resolvedArgs,\n              selector: `xpath=${trimmedXpath}`,\n            } as {\n              description: string;\n              method?: string;\n              arguments?: string[];\n              selector: string;\n            };\n          }\n          // shadow-root fallback:\n          return {\n            description: \"an element inside a shadow DOM\",\n            method: \"not-supported\",\n            arguments: [],\n            selector: \"not-supported\",\n          };\n        }),\n      )\n    ).filter(<T>(e: T | undefined): e is T => e !== undefined);\n\n    v3Logger({\n      category: \"observation\",\n      message: \"found elements\",\n      level: 1,\n      auxiliary: {\n        elements: {\n          value: JSON.stringify(elementsWithSelectors),\n          type: \"object\",\n        },\n      },\n    });\n\n    return elementsWithSelectors;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/v3AgentHandler.ts",
    "content": "import { createAgentTools } from \"../agent/tools/index.js\";\nimport { buildAgentSystemPrompt } from \"../agent/prompts/agentSystemPrompt.js\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { V3 } from \"../v3.js\";\nimport {\n  ModelMessage,\n  ToolSet,\n  wrapLanguageModel,\n  stepCountIs,\n  LanguageModel,\n  type LanguageModelUsage,\n  type StepResult,\n  type GenerateTextOnStepFinishCallback,\n  type StreamTextOnStepFinishCallback,\n  type PrepareStepFunction,\n} from \"ai\";\nimport { StagehandZodObject } from \"../zodCompat.js\";\nimport { processMessages } from \"../agent/utils/messageProcessing.js\";\nimport { LLMClient } from \"../llm/LLMClient.js\";\nimport { FlowLogger } from \"../flowlogger/FlowLogger.js\";\nimport {\n  AgentExecuteOptions,\n  AgentStreamExecuteOptions,\n  AgentExecuteOptionsBase,\n  AgentResult,\n  AgentContext,\n  AgentState,\n  AgentStreamResult,\n  AgentStreamCallbacks,\n  AgentToolMode,\n  AgentModelConfig,\n  Variables,\n} from \"../types/public/agent.js\";\nimport { V3FunctionName } from \"../types/public/methods.js\";\nimport { mapToolResultToActions } from \"../agent/utils/actionMapping.js\";\nimport {\n  MissingLLMConfigurationError,\n  MissingEnvironmentVariableError,\n  StreamingCallbacksInNonStreamingModeError,\n  AgentAbortError,\n} from \"../types/public/sdkErrors.js\";\nimport { handleDoneToolCall } from \"../agent/utils/handleDoneToolCall.js\";\nimport {\n  CaptchaSolver,\n  CAPTCHA_SOLVED_MSG,\n  CAPTCHA_ERRORED_MSG,\n} from \"../agent/utils/captchaSolver.js\";\n\nfunction getErrorMessage(error: unknown): string {\n  return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Prepends a system message with cache control to the messages array.\n * The cache control providerOptions are used by Anthropic and ignored by other providers.\n */\nfunction prependSystemMessage(\n  systemPrompt: string,\n  messages: ModelMessage[],\n): ModelMessage[] {\n  return [\n    {\n      role: \"system\",\n      content: systemPrompt,\n      providerOptions: {\n        anthropic: {\n          cacheControl: { type: \"ephemeral\" },\n        },\n      },\n    },\n    ...messages,\n  ];\n}\n\nexport class V3AgentHandler {\n  private v3: V3;\n  private logger: (message: LogLine) => void;\n  private llmClient: LLMClient;\n  private executionModel?: string | AgentModelConfig;\n  private systemInstructions?: string;\n  private mcpTools?: ToolSet;\n  private mode: AgentToolMode;\n  private captchaAutoSolveEnabled: boolean;\n\n  constructor(\n    v3: V3,\n    logger: (message: LogLine) => void,\n    llmClient: LLMClient,\n    executionModel?: string | AgentModelConfig,\n    systemInstructions?: string,\n    mcpTools?: ToolSet,\n    mode?: AgentToolMode,\n    captchaAutoSolveEnabled?: boolean,\n  ) {\n    this.v3 = v3;\n    this.logger = logger;\n    this.llmClient = llmClient;\n    this.executionModel = executionModel;\n    this.systemInstructions = systemInstructions;\n    this.mcpTools = mcpTools;\n    this.mode = mode ?? \"dom\";\n    this.captchaAutoSolveEnabled = captchaAutoSolveEnabled ?? false;\n  }\n\n  private async prepareAgent(\n    instructionOrOptions: string | AgentExecuteOptionsBase,\n  ): Promise<AgentContext> {\n    try {\n      const options =\n        typeof instructionOrOptions === \"string\"\n          ? { instruction: instructionOrOptions }\n          : instructionOrOptions;\n\n      const maxSteps = options.maxSteps || 20;\n\n      // Get the initial page URL first (needed for the system prompt)\n      const initialPageUrl = (await this.v3.context.awaitActivePage()).url();\n\n      // Build the system prompt with mode-aware tool guidance\n      const systemPrompt = buildAgentSystemPrompt({\n        url: initialPageUrl,\n        executionInstruction: options.instruction,\n        mode: this.mode,\n        systemInstructions: this.systemInstructions,\n        captchasAutoSolve: this.v3.isCaptchaAutoSolveEnabled,\n        excludeTools: options.excludeTools,\n        variables: options.variables,\n        useSearch: options.useSearch,\n      });\n\n      if (options.useSearch) {\n        const bbApiKey = this.v3.browserbaseApiKey;\n        if (!bbApiKey) {\n          throw new MissingEnvironmentVariableError(\n            \"BROWSERBASE_API_KEY\",\n            \"agent search (useSearch: true)\",\n          );\n        }\n      }\n\n      const tools = this.createTools(\n        options.excludeTools,\n        options.variables,\n        options.toolTimeout,\n        options.useSearch,\n      );\n      const allTools: ToolSet = { ...tools, ...this.mcpTools };\n\n      // Use provided messages for continuation, or start fresh with the instruction\n      const messages: ModelMessage[] = options.messages?.length\n        ? [...options.messages, { role: \"user\", content: options.instruction }]\n        : [{ role: \"user\", content: options.instruction }];\n\n      if (!this.llmClient?.getLanguageModel) {\n        throw new MissingLLMConfigurationError();\n      }\n      const baseModel = this.llmClient.getLanguageModel();\n      //to do - we likely do not need middleware anymore\n      const wrappedModel = wrapLanguageModel({\n        model: baseModel,\n        middleware: {\n          ...FlowLogger.createLlmLoggingMiddleware(baseModel.modelId),\n        },\n      });\n\n      if (\n        this.mode === \"hybrid\" &&\n        !baseModel.modelId.includes(\"gemini-3-flash\") &&\n        !baseModel.modelId.includes(\"claude\")\n      ) {\n        this.logger({\n          category: \"agent\",\n          message: `Warning: \"${baseModel.modelId}\" may not perform well in hybrid mode. See recommended models: https://docs.stagehand.dev/v3/basics/agent#hybrid-mode`,\n          level: 0,\n        });\n      }\n\n      return {\n        options,\n        maxSteps,\n        systemPrompt,\n        allTools,\n        messages,\n        wrappedModel,\n        initialPageUrl,\n      };\n    } catch (error) {\n      this.logger({\n        category: \"agent\",\n        message: `failed to prepare agent: ${error}`,\n        level: 0,\n      });\n      throw error;\n    }\n  }\n  private createPrepareStep(\n    userCallback?: PrepareStepFunction<ToolSet>,\n    captchaSolver?: CaptchaSolver,\n  ): PrepareStepFunction<ToolSet> {\n    return async (options) => {\n      processMessages(options.messages);\n      if (captchaSolver) {\n        if (captchaSolver.isSolving()) {\n          this.logger({\n            category: \"agent\",\n            message:\n              \"Captcha detected — waiting for Browserbase to solve it before continuing\",\n            level: 1,\n          });\n        }\n        await captchaSolver.waitIfSolving();\n        const { solved, errored } = captchaSolver.consumeSolveResult();\n        if (solved) {\n          options.messages.push({\n            role: \"user\",\n            content: CAPTCHA_SOLVED_MSG,\n          });\n          this.logger({\n            category: \"agent\",\n            message:\n              \"Captcha solved — injected notification into agent message stream\",\n            level: 1,\n          });\n        }\n        if (errored) {\n          options.messages.push({\n            role: \"user\",\n            content: CAPTCHA_ERRORED_MSG,\n          });\n          this.logger({\n            category: \"agent\",\n            message:\n              \"Captcha solver failed — injected error notification into agent message stream\",\n            level: 1,\n          });\n        }\n      }\n      if (userCallback) {\n        return userCallback(options);\n      }\n      return options;\n    };\n  }\n\n  private createStepHandler(\n    state: AgentState,\n    userCallback?:\n      | GenerateTextOnStepFinishCallback<ToolSet>\n      | StreamTextOnStepFinishCallback<ToolSet>,\n  ) {\n    return async (event: StepResult<ToolSet>) => {\n      this.logger({\n        category: \"agent\",\n        message: `Step finished: ${event.finishReason}`,\n        level: 2,\n      });\n\n      if (event.toolCalls && event.toolCalls.length > 0) {\n        for (let i = 0; i < event.toolCalls.length; i++) {\n          const toolCall = event.toolCalls[i];\n          const args = toolCall.input;\n          const toolResult = event.toolResults?.[i];\n\n          if (event.text && event.text.length > 0) {\n            state.collectedReasoning.push(event.text);\n            this.logger({\n              category: \"agent\",\n              message: `reasoning: ${event.text}`,\n              level: 1,\n            });\n          }\n\n          if (toolCall.toolName === \"done\") {\n            state.completed = true;\n            if (args?.taskComplete) {\n              const doneReasoning = args.reasoning;\n              const allReasoning = state.collectedReasoning.join(\" \");\n              state.finalMessage = doneReasoning\n                ? `${allReasoning} ${doneReasoning}`.trim()\n                : allReasoning || \"Task completed successfully\";\n            }\n          }\n          const mappedActions = mapToolResultToActions({\n            toolCallName: toolCall.toolName,\n            toolResult,\n            args,\n            reasoning: event.text || undefined,\n          });\n\n          for (const action of mappedActions) {\n            action.pageUrl = state.currentPageUrl;\n            action.timestamp = Date.now();\n            state.actions.push(action);\n          }\n        }\n        state.currentPageUrl = (await this.v3.context.awaitActivePage()).url();\n      }\n\n      if (userCallback) {\n        await userCallback(event);\n      }\n    };\n  }\n\n  public async execute(\n    instructionOrOptions: string | AgentExecuteOptions,\n  ): Promise<AgentResult> {\n    const startTime = Date.now();\n    const options =\n      typeof instructionOrOptions === \"object\" ? instructionOrOptions : null;\n    const signal = options?.signal;\n\n    // Highlight cursor defaults to true for hybrid mode, can be overridden\n    const shouldHighlightCursor =\n      options?.highlightCursor ?? this.mode === \"hybrid\";\n\n    const state: AgentState = {\n      collectedReasoning: [],\n      actions: [],\n      finalMessage: \"\",\n      completed: false,\n      currentPageUrl: \"\",\n    };\n\n    let messages: ModelMessage[] = [];\n    let captchaSolver: CaptchaSolver | undefined;\n\n    try {\n      const {\n        options: preparedOptions,\n        maxSteps,\n        systemPrompt,\n        allTools,\n        messages: preparedMessages,\n        wrappedModel,\n        initialPageUrl,\n      } = await this.prepareAgent(instructionOrOptions);\n\n      // Enable cursor overlay for hybrid mode (coordinate-based interactions)\n      if (shouldHighlightCursor && this.mode === \"hybrid\") {\n        const page = await this.v3.context.awaitActivePage();\n        await page.enableCursorOverlay().catch(() => {});\n      }\n\n      // Set up captcha solver for Browserbase environments\n      if (this.captchaAutoSolveEnabled) {\n        captchaSolver = new CaptchaSolver();\n        captchaSolver.init(() => this.v3.context.awaitActivePage());\n      }\n\n      messages = preparedMessages;\n      state.currentPageUrl = initialPageUrl;\n\n      const callbacks = (instructionOrOptions as AgentExecuteOptions).callbacks;\n\n      if (callbacks) {\n        const streamingOnlyCallbacks = [\n          \"onChunk\",\n          \"onFinish\",\n          \"onError\",\n          \"onAbort\",\n        ];\n        const invalidCallbacks = streamingOnlyCallbacks.filter(\n          (name) => callbacks[name as keyof typeof callbacks] != null,\n        );\n        if (invalidCallbacks.length > 0) {\n          throw new StreamingCallbacksInNonStreamingModeError(invalidCallbacks);\n        }\n      }\n\n      const result = await this.llmClient.generateText({\n        model: wrappedModel,\n        messages: prependSystemMessage(systemPrompt, messages),\n        tools: allTools,\n        stopWhen: (result) => this.handleStop(result, maxSteps),\n        temperature: 1,\n        toolChoice: \"auto\",\n\n        prepareStep: this.createPrepareStep(\n          callbacks?.prepareStep,\n          captchaSolver,\n        ),\n        onStepFinish: this.createStepHandler(state, callbacks?.onStepFinish),\n        abortSignal: preparedOptions.signal,\n        providerOptions: {\n          google: { mediaResolution: \"MEDIA_RESOLUTION_HIGH\" },\n          openai: { store: false },\n        },\n      });\n\n      const allMessages = [...messages, ...(result.response?.messages || [])];\n      const doneResult = await this.ensureDone(\n        state,\n        wrappedModel,\n        allMessages,\n        preparedOptions.instruction,\n        preparedOptions.output,\n        this.logger,\n      );\n\n      return this.consolidateMetricsAndResult(\n        startTime,\n        state,\n        doneResult.messages,\n        result,\n        maxSteps,\n        doneResult.output,\n      );\n    } catch (error) {\n      // Re-throw validation errors that should propagate to the caller\n      if (\n        error instanceof StreamingCallbacksInNonStreamingModeError ||\n        error instanceof MissingEnvironmentVariableError\n      ) {\n        throw error;\n      }\n\n      // Re-throw abort errors wrapped in AgentAbortError for consistent error typing\n      if (signal?.aborted) {\n        const reason = signal.reason ? String(signal.reason) : \"aborted\";\n        throw new AgentAbortError(reason);\n      }\n\n      const errorMessage = getErrorMessage(error);\n      this.logger({\n        category: \"agent\",\n        message: `Error executing agent task: ${errorMessage}`,\n        level: 0,\n      });\n\n      // For non-abort errors, return a failure result instead of throwing\n      return {\n        success: false,\n        actions: state.actions,\n        message: `Failed to execute task: ${errorMessage}`,\n        completed: false,\n        messages,\n      };\n    } finally {\n      captchaSolver?.dispose();\n    }\n  }\n\n  public async stream(\n    instructionOrOptions: string | AgentStreamExecuteOptions,\n  ): Promise<AgentStreamResult> {\n    const streamOptions =\n      typeof instructionOrOptions === \"object\" ? instructionOrOptions : null;\n\n    // Highlight cursor defaults to true for hybrid mode, can be overridden\n    const shouldHighlightCursor =\n      streamOptions?.highlightCursor ?? this.mode === \"hybrid\";\n\n    const {\n      options,\n      maxSteps,\n      systemPrompt,\n      allTools,\n      messages,\n      wrappedModel,\n      initialPageUrl,\n    } = await this.prepareAgent(instructionOrOptions);\n\n    // Enable cursor overlay for hybrid mode (coordinate-based interactions)\n    if (shouldHighlightCursor && this.mode === \"hybrid\") {\n      const page = await this.v3.context.awaitActivePage();\n      await page.enableCursorOverlay().catch(() => {});\n    }\n\n    // Set up captcha solver for Browserbase environments\n    let captchaSolver: CaptchaSolver | undefined;\n    if (this.captchaAutoSolveEnabled) {\n      captchaSolver = new CaptchaSolver();\n      captchaSolver.init(() => this.v3.context.awaitActivePage());\n    }\n\n    const callbacks = (instructionOrOptions as AgentStreamExecuteOptions)\n      .callbacks as AgentStreamCallbacks | undefined;\n\n    const state: AgentState = {\n      collectedReasoning: [],\n      actions: [],\n      finalMessage: \"\",\n      completed: false,\n      currentPageUrl: initialPageUrl,\n    };\n    const startTime = Date.now();\n\n    let resolveResult: (value: AgentResult | PromiseLike<AgentResult>) => void;\n    let rejectResult: (reason: unknown) => void;\n    const resultPromise = new Promise<AgentResult>((resolve, reject) => {\n      resolveResult = resolve;\n      rejectResult = reject;\n    });\n\n    const handleError = (error: unknown) => {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      this.logger({\n        category: \"agent\",\n        message: `Error during streaming: ${errorMessage}`,\n        level: 0,\n      });\n      rejectResult(error);\n    };\n\n    let streamResult: ReturnType<typeof this.llmClient.streamText>;\n    try {\n      streamResult = this.llmClient.streamText({\n        model: wrappedModel,\n        messages: prependSystemMessage(systemPrompt, messages),\n        tools: allTools,\n        stopWhen: (result) => this.handleStop(result, maxSteps),\n        temperature: 1,\n        toolChoice: \"auto\",\n        prepareStep: this.createPrepareStep(\n          callbacks?.prepareStep,\n          captchaSolver,\n        ),\n        onStepFinish: this.createStepHandler(state, callbacks?.onStepFinish),\n        onError: (event) => {\n          captchaSolver?.dispose();\n          if (callbacks?.onError) {\n            callbacks.onError(event);\n          }\n          handleError(event.error);\n        },\n        onChunk: callbacks?.onChunk,\n        onFinish: (event) => {\n          captchaSolver?.dispose();\n          if (callbacks?.onFinish) {\n            callbacks.onFinish(event);\n          }\n\n          const allMessages = [\n            ...messages,\n            ...(event.response?.messages || []),\n          ];\n          this.ensureDone(\n            state,\n            wrappedModel,\n            allMessages,\n            options.instruction,\n            options.output,\n            this.logger,\n          ).then((doneResult) => {\n            const result = this.consolidateMetricsAndResult(\n              startTime,\n              state,\n              doneResult.messages,\n              event,\n              maxSteps,\n              doneResult.output,\n            );\n            resolveResult(result);\n          });\n        },\n        onAbort: (event) => {\n          captchaSolver?.dispose();\n          if (callbacks?.onAbort) {\n            callbacks.onAbort(event);\n          }\n          // Reject the result promise with AgentAbortError when stream is aborted\n          const reason = options.signal?.reason\n            ? String(options.signal.reason)\n            : \"Stream was aborted\";\n          rejectResult(new AgentAbortError(reason));\n        },\n        abortSignal: options.signal,\n        providerOptions: {\n          google: { mediaResolution: \"MEDIA_RESOLUTION_HIGH\" },\n          openai: { store: false },\n        },\n      });\n    } catch (error) {\n      captchaSolver?.dispose();\n      throw error;\n    }\n\n    const agentStreamResult = streamResult as AgentStreamResult;\n    agentStreamResult.result = resultPromise;\n    return agentStreamResult;\n  }\n\n  private consolidateMetricsAndResult(\n    startTime: number,\n    state: AgentState,\n    inputMessages: ModelMessage[],\n    result: {\n      text?: string;\n      totalUsage?: LanguageModelUsage;\n      response?: { messages?: ModelMessage[] };\n      steps?: StepResult<ToolSet>[];\n    },\n    maxSteps?: number,\n    output?: Record<string, unknown>,\n  ): AgentResult {\n    if (!state.finalMessage) {\n      const allReasoning = state.collectedReasoning.join(\" \").trim();\n\n      if (!state.completed && maxSteps && result.steps?.length >= maxSteps) {\n        this.logger({\n          category: \"agent\",\n          message: `Agent stopped: reached maximum steps (${maxSteps})`,\n          level: 1,\n        });\n        state.finalMessage = `Agent stopped: reached maximum steps (${maxSteps})`;\n      } else {\n        state.finalMessage = allReasoning || result.text || \"\";\n      }\n    }\n\n    const endTime = Date.now();\n    const inferenceTimeMs = endTime - startTime;\n    if (result.totalUsage) {\n      this.v3.updateMetrics(\n        V3FunctionName.AGENT,\n        result.totalUsage.inputTokens || 0,\n        result.totalUsage.outputTokens || 0,\n        result.totalUsage.reasoningTokens || 0,\n        result.totalUsage.cachedInputTokens || 0,\n        inferenceTimeMs,\n      );\n    }\n\n    return {\n      success: state.completed,\n      message: state.finalMessage || \"Task execution completed\",\n      actions: state.actions,\n      completed: state.completed,\n      output,\n      usage: result.totalUsage\n        ? {\n            input_tokens: result.totalUsage.inputTokens || 0,\n            output_tokens: result.totalUsage.outputTokens || 0,\n            reasoning_tokens: result.totalUsage.reasoningTokens || 0,\n            cached_input_tokens: result.totalUsage.cachedInputTokens || 0,\n            inference_time_ms: inferenceTimeMs,\n          }\n        : undefined,\n      messages: inputMessages,\n    };\n  }\n\n  private createTools(\n    excludeTools?: string[],\n    variables?: Variables,\n    toolTimeout?: number,\n    useSearch?: boolean,\n  ) {\n    const provider = this.llmClient?.getLanguageModel?.()?.provider;\n    return createAgentTools(this.v3, {\n      executionModel: this.executionModel,\n      logger: this.logger,\n      mode: this.mode,\n      provider,\n      excludeTools,\n      variables,\n      toolTimeout,\n      useSearch,\n      browserbaseApiKey: useSearch ? this.v3.browserbaseApiKey : undefined,\n    });\n  }\n\n  private handleStop(\n    result: Parameters<ReturnType<typeof stepCountIs>>[0],\n    maxSteps: number,\n  ): boolean | PromiseLike<boolean> {\n    const lastStep = result.steps[result.steps.length - 1];\n    if (lastStep?.toolCalls?.some((tc) => tc.toolName === \"done\")) {\n      return true;\n    }\n    return stepCountIs(maxSteps)(result);\n  }\n\n  /**\n   * Ensures the done tool is called at the end of agent execution.\n   * Returns the messages and any extracted output from the done call.\n   */\n  private async ensureDone(\n    state: AgentState,\n    model: LanguageModel,\n    messages: ModelMessage[],\n    instruction: string,\n    outputSchema?: StagehandZodObject,\n    logger?: (message: LogLine) => void,\n  ): Promise<{ messages: ModelMessage[]; output?: Record<string, unknown> }> {\n    if (state.completed) return { messages };\n\n    const doneResult = await handleDoneToolCall({\n      model,\n      inputMessages: messages,\n      instruction,\n      outputSchema,\n      logger,\n    });\n\n    state.completed = doneResult.taskComplete;\n    state.finalMessage = doneResult.reasoning;\n\n    const doneAction = mapToolResultToActions({\n      toolCallName: \"done\",\n      toolResult: {\n        success: true,\n        reasoning: doneResult.reasoning,\n        taskComplete: doneResult.taskComplete,\n      },\n      args: {\n        reasoning: doneResult.reasoning,\n        taskComplete: doneResult.taskComplete,\n      },\n      reasoning: doneResult.reasoning,\n    });\n\n    for (const action of doneAction) {\n      action.pageUrl = state.currentPageUrl;\n      action.timestamp = Date.now();\n      state.actions.push(action);\n    }\n\n    return {\n      messages: [...messages, ...doneResult.messages],\n      output: doneResult.output,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/handlers/v3CuaAgentHandler.ts",
    "content": "import { computeActiveElementXpath } from \"../understudy/a11y/snapshot/index.js\";\nimport { V3 } from \"../v3.js\";\nimport { ToolSet } from \"ai\";\nimport { AgentClient } from \"../agent/AgentClient.js\";\nimport { AgentProvider } from \"../agent/AgentProvider.js\";\nimport { GoogleCUAClient } from \"../agent/GoogleCUAClient.js\";\nimport { OpenAICUAClient } from \"../agent/OpenAICUAClient.js\";\nimport { mapKeyToPlaywright } from \"../agent/utils/cuaKeyMapping.js\";\nimport { ensureXPath } from \"../agent/utils/xpath.js\";\nimport {\n  ActionExecutionResult,\n  AgentAction,\n  AgentExecuteOptions,\n  AgentHandlerOptions,\n  AgentResult,\n  SafetyConfirmationHandler,\n} from \"../types/public/agent.js\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { type Action, V3FunctionName } from \"../types/public/methods.js\";\nimport { FlowLogger } from \"../flowlogger/FlowLogger.js\";\nimport { toTitleCase } from \"../../utils.js\";\nimport { StagehandClosedError } from \"../types/public/sdkErrors.js\";\nimport {\n  CaptchaSolver,\n  CAPTCHA_SOLVED_MSG,\n  CAPTCHA_ERRORED_MSG,\n} from \"../agent/utils/captchaSolver.js\";\n\nexport class V3CuaAgentHandler {\n  private v3: V3;\n  private agent: AgentClient;\n  private provider: AgentProvider;\n  private logger: (message: LogLine) => void;\n  private agentClient: AgentClient;\n  private options: AgentHandlerOptions;\n  private highlightCursor: boolean;\n  private captchaSolver: CaptchaSolver | null = null;\n  private captchaClickGuardRemaining = 0;\n  private currentInstruction = \"\";\n\n  constructor(\n    v3: V3,\n    logger: (message: LogLine) => void,\n    options: AgentHandlerOptions,\n    tools?: ToolSet,\n  ) {\n    this.v3 = v3;\n    this.logger = logger;\n    this.options = options;\n\n    this.provider = new AgentProvider(logger);\n    const client = this.provider.getClient(\n      options.modelName,\n      options.clientOptions || {},\n      options.userProvidedInstructions,\n      tools,\n    );\n    this.agentClient = client;\n    this.setupAgentClient();\n    this.agent = client;\n  }\n\n  /**\n   * Ensures the V3 context is still available (not closed).\n   * Throws StagehandClosedError if stagehand.close() was called.\n   */\n  private ensureNotClosed(): void {\n    if (!this.v3.context) {\n      throw new StagehandClosedError();\n    }\n  }\n\n  private setupAgentClient(): void {\n    // Provide screenshots to the agent client\n    this.agentClient.setScreenshotProvider(async () => {\n      this.ensureNotClosed();\n      const page = await this.v3.context.awaitActivePage();\n      const screenshotBuffer = await page.screenshot({ fullPage: false });\n      return screenshotBuffer.toString(\"base64\"); // base64 png\n    });\n\n    // Provide action executor\n    this.agentClient.setActionHandler(async (action) => {\n      this.ensureNotClosed();\n\n      // Wait for captcha solver to finish before executing action\n      if (this.captchaSolver) {\n        if (this.captchaSolver.isSolving()) {\n          this.logger({\n            category: \"agent\",\n            message:\n              \"Captcha detected — waiting for Browserbase to solve it before continuing\",\n            level: 1,\n          });\n        }\n        await this.captchaSolver.waitIfSolving();\n        this.handleCaptchaSolveResult(this.captchaSolver.consumeSolveResult());\n      }\n\n      action.pageUrl = (await this.v3.context.awaitActivePage()).url();\n      if (await this.shouldSkipSolvedCaptchaInteraction(action)) {\n        this.captchaClickGuardRemaining = Math.max(\n          0,\n          this.captchaClickGuardRemaining - 1,\n        );\n        this.agentClient.addContextNote(\n          `The captcha has already been solved automatically. Do not click the captcha checkbox, widget, or challenge again. Continue with the original task outside the captcha area. Original task: ${this.currentInstruction}`,\n        );\n        this.logger({\n          category: \"agent\",\n          message:\n            \"Skipped click on solved captcha widget — injected follow-up guidance\",\n          level: 1,\n        });\n        return;\n      }\n\n      const defaultDelay = 500;\n      const waitBetween =\n        (this.options.clientOptions?.waitBetweenActions as number) ||\n        defaultDelay;\n      try {\n        // Try to inject cursor before each action if enabled\n        if (this.highlightCursor) {\n          try {\n            await this.injectCursor();\n          } catch {\n            // Ignore cursor injection failures\n          }\n        }\n        await new Promise((r) => setTimeout(r, 300));\n        // Skip logging for screenshot actions - they're no-ops, the actual\n        // Page.screenshot in captureAndSendScreenshot() is logged separately\n        const shouldLog = action.type !== \"screenshot\";\n        if (shouldLog) {\n          await FlowLogger.runWithLogging(\n            {\n              eventType: `V3Cua${toTitleCase(action.type)}`, // e.g. \"V3CuaClick\"\n              data: {\n                target: this.computePointerTarget(action),\n              },\n            },\n            async (loggedAction: typeof action) =>\n              await this.executeAction(loggedAction),\n            [action],\n          );\n        } else {\n          await this.executeAction(action);\n        }\n\n        action.timestamp = Date.now();\n\n        await new Promise((r) => setTimeout(r, waitBetween));\n        try {\n          await this.captureAndSendScreenshot();\n        } catch (e) {\n          this.logger({\n            category: \"agent\",\n            message: `Warning: Failed to take screenshot after action: ${String(\n              (e as Error)?.message ?? e,\n            )}`,\n            level: 1,\n          });\n        }\n      } catch (error) {\n        const msg = (error as Error)?.message ?? String(error);\n        this.logger({\n          category: \"agent\",\n          message: `Error executing action ${action.type}: ${msg}`,\n          level: 0,\n        });\n        throw error;\n      }\n    });\n\n    void this.updateClientViewport();\n    void this.updateClientUrl();\n  }\n\n  setSafetyConfirmationHandler(handler?: SafetyConfirmationHandler): void {\n    if (\n      this.agentClient instanceof GoogleCUAClient ||\n      this.agentClient instanceof OpenAICUAClient\n    ) {\n      this.agentClient.setSafetyConfirmationHandler(handler);\n    }\n  }\n\n  async execute(\n    optionsOrInstruction: AgentExecuteOptions | string,\n  ): Promise<AgentResult> {\n    const options =\n      typeof optionsOrInstruction === \"string\"\n        ? { instruction: optionsOrInstruction }\n        : optionsOrInstruction;\n\n    this.setSafetyConfirmationHandler(options.callbacks?.onSafetyConfirmation);\n\n    this.highlightCursor = options.highlightCursor !== false;\n    this.currentInstruction = options.instruction;\n\n    // Redirect if blank\n    const page = await this.v3.context.awaitActivePage();\n    const currentUrl = page.url();\n    if (!currentUrl || currentUrl === \"about:blank\") {\n      this.logger({\n        category: \"agent\",\n        message: `Page URL is empty. Navigating to https://www.google.com ...`,\n        level: 1,\n      });\n      await page.goto(\"https://www.google.com\", { waitUntil: \"load\" });\n    }\n\n    // Set up captcha solver for Browserbase environments\n    if (this.v3.isCaptchaAutoSolveEnabled) {\n      this.captchaSolver = new CaptchaSolver();\n      this.captchaSolver.init(() => this.v3.context.awaitActivePage());\n\n      // Block the CUA agent loop before each step while a captcha is being solved\n      this.agentClient.setPreStepHook(async () => {\n        if (this.captchaSolver?.isSolving()) {\n          this.logger({\n            category: \"agent\",\n            message:\n              \"Captcha detected — waiting for Browserbase to solve it before continuing\",\n            level: 1,\n          });\n        }\n        await this.captchaSolver?.waitIfSolving();\n        this.handleCaptchaSolveResult(this.captchaSolver?.consumeSolveResult());\n      });\n    }\n\n    if (this.highlightCursor) {\n      try {\n        await this.injectCursor();\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        this.logger({\n          category: \"agent\",\n          message: `Warning: Failed to inject cursor: ${errorMessage}. Continuing with execution.`,\n          level: 1,\n        });\n        // Continue execution even if cursor injection fails\n      }\n    }\n\n    const start = Date.now();\n    let result: AgentResult;\n    try {\n      result = await this.agent.execute({ options, logger: this.logger });\n    } finally {\n      this.captchaSolver?.dispose();\n      this.captchaSolver = null;\n    }\n    const inferenceTimeMs = Date.now() - start;\n    if (result.usage) {\n      this.v3.updateMetrics(\n        V3FunctionName.AGENT,\n        result.usage.input_tokens,\n        result.usage.output_tokens,\n        result.usage.reasoning_tokens ?? 0,\n        result.usage.cached_input_tokens ?? 0,\n        inferenceTimeMs,\n      );\n    }\n    return result;\n  }\n\n  private async executeAction(\n    action: AgentAction,\n  ): Promise<ActionExecutionResult> {\n    const page = await this.v3.context.awaitActivePage();\n    const recording = this.v3.isAgentReplayActive();\n    switch (action.type) {\n      case \"click\": {\n        const { x, y, button = \"left\", clickCount } = action;\n        if (recording) {\n          const xpath = await page.click(x as number, y as number, {\n            button: (button as \"left\" | \"right\" | \"middle\") ?? \"left\",\n            clickCount: (clickCount as number) ?? 1,\n            returnXpath: true,\n          });\n          const normalized = ensureXPath(xpath);\n          if (normalized) {\n            const stagehandAction: Action = {\n              selector: normalized,\n              description: this.describePointerAction(\"click\", x, y),\n              method: \"click\",\n              arguments: [],\n            };\n            this.recordCuaActStep(\n              action,\n              [stagehandAction],\n              stagehandAction.description,\n            );\n          }\n        } else {\n          await page.click(x as number, y as number, {\n            button: (button as \"left\" | \"right\" | \"middle\") ?? \"left\",\n            clickCount: (clickCount as number) ?? 1,\n          });\n        }\n        return { success: true };\n      }\n      case \"double_click\":\n      case \"doubleClick\": {\n        const { x, y } = action;\n        if (recording) {\n          const xpath = await page.click(x as number, y as number, {\n            button: \"left\",\n            clickCount: 2,\n            returnXpath: true,\n          });\n          const normalized = ensureXPath(xpath);\n          if (normalized) {\n            const stagehandAction: Action = {\n              selector: normalized,\n              description: this.describePointerAction(\"double click\", x, y),\n              method: \"doubleClick\",\n              arguments: [],\n            };\n            this.recordCuaActStep(\n              action,\n              [stagehandAction],\n              stagehandAction.description,\n            );\n          }\n        } else {\n          await page.click(x as number, y as number, {\n            button: \"left\",\n            clickCount: 2,\n          });\n        }\n        return { success: true };\n      }\n      case \"tripleClick\": {\n        const { x, y } = action;\n        if (recording) {\n          const xpath = await page.click(x as number, y as number, {\n            button: \"left\",\n            clickCount: 3,\n            returnXpath: true,\n          });\n          const normalized = ensureXPath(xpath);\n          if (normalized) {\n            const stagehandAction: Action = {\n              selector: normalized,\n              description: this.describePointerAction(\"triple click\", x, y),\n              method: \"tripleClick\",\n              arguments: [],\n            };\n            this.recordCuaActStep(\n              action,\n              [stagehandAction],\n              stagehandAction.description,\n            );\n          }\n        } else {\n          await page.click(x as number, y as number, {\n            clickCount: 3,\n          });\n        }\n        return { success: true };\n      }\n      case \"type\": {\n        const { text } = action;\n        await page.type(String(text ?? \"\"));\n        if (recording) {\n          const xpath = await computeActiveElementXpath(page);\n          const normalized = ensureXPath(xpath);\n          if (normalized) {\n            const stagehandAction: Action = {\n              selector: normalized,\n              description: this.describeTypeAction(String(text ?? \"\")),\n              method: \"type\",\n              arguments: [String(text ?? \"\")],\n            };\n            this.recordCuaActStep(\n              action,\n              [stagehandAction],\n              stagehandAction.description,\n            );\n          }\n        }\n        return { success: true };\n      }\n      case \"keypress\": {\n        const { keys } = action;\n        const keyList = Array.isArray(keys) ? keys : [keys];\n        const stagehandActions: Action[] = [];\n        for (const rawKey of keyList) {\n          const mapped = mapKeyToPlaywright(String(rawKey ?? \"\"));\n          await page.keyPress(mapped);\n          if (recording) {\n            stagehandActions.push({\n              selector: \"xpath=/html\",\n              description: `press ${mapped}`,\n              method: \"press\",\n              arguments: [mapped],\n            });\n          }\n        }\n        if (recording && stagehandActions.length > 0) {\n          this.recordCuaActStep(\n            action,\n            stagehandActions,\n            stagehandActions\n              .map((a) => a.description)\n              .filter(Boolean)\n              .join(\", \") || \"keypress\",\n          );\n        }\n        return { success: true };\n      }\n      case \"scroll\": {\n        const { x, y, scroll_x = 0, scroll_y = 0 } = action;\n        await page.scroll(\n          (x as number) ?? 0,\n          (y as number) ?? 0,\n          (scroll_x as number) ?? 0,\n          (scroll_y as number) ?? 0,\n        );\n        this.v3.recordAgentReplayStep({\n          type: \"scroll\",\n          deltaX: Number(scroll_x ?? 0),\n          deltaY: Number(scroll_y ?? 0),\n          anchor:\n            typeof x === \"number\" && typeof y === \"number\"\n              ? { x: Math.round(x), y: Math.round(y) }\n              : undefined,\n        });\n        return { success: true };\n      }\n      case \"drag\": {\n        const { path } = action;\n        if (Array.isArray(path) && path.length >= 2) {\n          const start = path[0];\n          const end = path[path.length - 1];\n          if (recording) {\n            const xps = await page.dragAndDrop(start.x, start.y, end.x, end.y, {\n              steps: Math.min(20, Math.max(5, path.length)),\n              delay: 10,\n              returnXpath: true,\n            });\n            const [fromXpath, toXpath] = (xps as [string, string]) || [\"\", \"\"];\n            const from = ensureXPath(fromXpath);\n            const to = ensureXPath(toXpath);\n            if (from && to) {\n              const stagehandAction: Action = {\n                selector: from,\n                description: this.describeDragAction(),\n                method: \"dragAndDrop\",\n                arguments: [to],\n              };\n              this.recordCuaActStep(\n                action,\n                [stagehandAction],\n                stagehandAction.description,\n              );\n            }\n          } else {\n            await page.dragAndDrop(start.x, start.y, end.x, end.y, {\n              steps: Math.min(20, Math.max(5, path.length)),\n              delay: 10,\n            });\n          }\n        }\n        return { success: true };\n      }\n      case \"move\": {\n        const { x, y } = action;\n        if (typeof x === \"number\" && typeof y === \"number\") {\n          if (recording) {\n            const xpath = await page.hover(x, y, { returnXpath: true });\n            const normalized = ensureXPath(xpath);\n            if (normalized) {\n              const stagehandAction: Action = {\n                selector: normalized,\n                description: this.describePointerAction(\"hover\", x, y),\n                method: \"hover\",\n                arguments: [],\n              };\n              this.recordCuaActStep(\n                action,\n                [stagehandAction],\n                stagehandAction.description,\n              );\n            }\n          } else {\n            await page.hover(x, y);\n          }\n        }\n        return { success: true };\n      }\n      case \"wait\": {\n        const time = action?.timeMs ?? 1000;\n        await new Promise((r) => setTimeout(r, time));\n        if (time > 0 && recording) {\n          this.v3.recordAgentReplayStep({ type: \"wait\", timeMs: Number(time) });\n        }\n        return { success: true };\n      }\n      case \"screenshot\": {\n        // No-op - screenshot is captured by captureAndSendScreenshot() after all actions\n        return { success: true };\n      }\n      case \"goto\": {\n        const { url } = action;\n        await page.goto(String(url ?? \"\"), { waitUntil: \"load\" });\n        if (recording) {\n          this.v3.recordAgentReplayStep({\n            type: \"goto\",\n            url: String(url ?? \"\"),\n          });\n        }\n        return { success: true };\n      }\n      case \"back\": {\n        await page.goBack();\n        if (recording) {\n          this.v3.recordAgentReplayStep({\n            type: \"back\",\n          });\n        }\n        return { success: true };\n      }\n      case \"forward\": {\n        await page.goForward();\n        if (recording) {\n          this.v3.recordAgentReplayStep({\n            type: \"forward\",\n          });\n        }\n        return { success: true };\n      }\n      case \"open_web_browser\": {\n        // Browser is already open, this is a no-op\n        return { success: true };\n      }\n      case \"custom_tool\": {\n        // Custom tools are handled by the agent client directly\n        return { success: true };\n      }\n      default:\n        this.logger({\n          category: \"agent\",\n          message: `Unknown action type: ${String(action.type)}`,\n          level: 1,\n        });\n        return {\n          success: false,\n          error: `Unknown action ${String(action.type)}`,\n        };\n    }\n  }\n\n  // helper to make pointer target human-readable for logging\n  private computePointerTarget(action: AgentAction): string | undefined {\n    return typeof action.x === \"number\" && typeof action.y === \"number\"\n      ? `(${action.x}, ${action.y})`\n      : typeof action.selector === \"string\"\n        ? action.selector\n        : typeof action.input === \"string\"\n          ? action.input\n          : typeof action.description === \"string\"\n            ? action.description\n            : undefined;\n  }\n\n  private describePointerAction(kind: string, x: unknown, y: unknown): string {\n    const nx = Number(x);\n    const ny = Number(y);\n    if (Number.isFinite(nx) && Number.isFinite(ny)) {\n      return `${kind} at (${Math.round(nx)}, ${Math.round(ny)})`;\n    }\n    return kind;\n  }\n\n  private describeTypeAction(text: string): string {\n    const snippet = text.length > 30 ? `${text.slice(0, 27)}...` : text;\n    return `type \"${snippet}\"`;\n  }\n\n  private describeDragAction(): string {\n    return \"drag and drop\";\n  }\n\n  private buildInstructionFallback(\n    agentAction: AgentAction,\n    fallback: string,\n  ): string {\n    const raw =\n      (typeof agentAction.action === \"string\" && agentAction.action.trim()) ||\n      (typeof agentAction.reasoning === \"string\" &&\n        agentAction.reasoning.trim());\n    return raw && raw.length > 0 ? raw : fallback;\n  }\n\n  private recordCuaActStep(\n    agentAction: AgentAction,\n    stagehandActions: Action[],\n    fallback: string,\n  ): void {\n    if (!stagehandActions.length) return;\n    const instruction = this.buildInstructionFallback(agentAction, fallback);\n    const description = stagehandActions[0]?.description || instruction;\n    const actions = stagehandActions.map((act) => ({\n      ...act,\n      description: act.description || description,\n    }));\n    this.v3.recordAgentReplayStep({\n      type: \"act\",\n      instruction,\n      actions,\n      actionDescription: description,\n      message:\n        typeof agentAction.reasoning === \"string\" &&\n        agentAction.reasoning.trim().length > 0\n          ? agentAction.reasoning.trim()\n          : undefined,\n    });\n  }\n\n  private async updateClientViewport(): Promise<void> {\n    try {\n      // For Google CUA, use configured viewport for coordinate normalization\n      // advancedStealth uses fixed 1288x711, otherwise use configured viewport\n      if (this.agentClient instanceof GoogleCUAClient) {\n        const dims = this.v3.isAdvancedStealth\n          ? { width: 1288, height: 711 }\n          : this.v3.configuredViewport;\n        this.agentClient.setViewport(dims.width, dims.height);\n      } else {\n        // For other clients, use actual window dimensions\n        const page = await this.v3.context.awaitActivePage();\n        const { w, h } = await page.mainFrame().evaluate<{\n          w: number;\n          h: number;\n        }>(\"({ w: window.innerWidth, h: window.innerHeight })\");\n        if (w && h) this.agentClient.setViewport(w, h);\n      }\n    } catch {\n      //\n    }\n  }\n\n  private async updateClientUrl(): Promise<void> {\n    try {\n      const page = await this.v3.context.awaitActivePage();\n      const url = page.url();\n      this.agentClient.setCurrentUrl(url);\n    } catch {\n      //\n    }\n  }\n\n  async captureAndSendScreenshot(): Promise<unknown> {\n    this.logger({\n      category: \"agent\",\n      message: \"Capturing screenshot\",\n      level: 1,\n    });\n    try {\n      const page = await this.v3.context.awaitActivePage();\n      const screenshotBuffer = await page.screenshot({ fullPage: false });\n\n      const currentUrl = page.url();\n      return await this.agentClient.captureScreenshot({\n        base64Image: screenshotBuffer.toString(\"base64\"),\n        currentUrl,\n      });\n    } catch (e) {\n      this.logger({\n        category: \"agent\",\n        message: `Error capturing screenshot: ${String((e as Error)?.message ?? e)}`,\n        level: 0,\n      });\n      return null;\n    }\n  }\n\n  private handleCaptchaSolveResult(result?: {\n    solved: boolean;\n    errored: boolean;\n  }): void {\n    if (!result) return;\n\n    if (result.solved) {\n      this.captchaClickGuardRemaining = 3;\n      this.agentClient.addContextNote(CAPTCHA_SOLVED_MSG);\n      this.logger({\n        category: \"agent\",\n        message: \"Captcha solved — continuing with task\",\n        level: 1,\n      });\n    }\n\n    if (result.errored) {\n      this.captchaClickGuardRemaining = 0;\n      this.agentClient.addContextNote(CAPTCHA_ERRORED_MSG);\n      this.logger({\n        category: \"agent\",\n        message: \"Captcha solver failed or errored\",\n        level: 1,\n      });\n    }\n  }\n\n  private async shouldSkipSolvedCaptchaInteraction(\n    action: AgentAction,\n  ): Promise<boolean> {\n    if (this.captchaClickGuardRemaining <= 0) {\n      return false;\n    }\n\n    if (action.type !== \"click\") {\n      return false;\n    }\n\n    const x = action.x;\n    const y = action.y;\n    if (typeof x !== \"number\" || typeof y !== \"number\") {\n      return false;\n    }\n\n    try {\n      const page = await this.v3.context.awaitActivePage();\n      const boxes = await page.evaluate<\n        Array<{ left: number; top: number; right: number; bottom: number }>\n      >(() => {\n        const selectors = [\n          'iframe[title*=\"reCAPTCHA\"]',\n          'iframe[src*=\"recaptcha\"]',\n          'iframe[src*=\"hcaptcha\"]',\n          'iframe[src*=\"turnstile\"]',\n          \".g-recaptcha\",\n          \"[data-sitekey]\",\n          '[class*=\"captcha\"]',\n          '[id*=\"captcha\"]',\n        ];\n\n        const seen = new Set<Element>();\n        const bounds: Array<{\n          left: number;\n          top: number;\n          right: number;\n          bottom: number;\n        }> = [];\n\n        for (const selector of selectors) {\n          for (const element of document.querySelectorAll(selector)) {\n            if (seen.has(element)) continue;\n            seen.add(element);\n            const rect = element.getBoundingClientRect();\n            if (rect.width <= 0 || rect.height <= 0) continue;\n            bounds.push({\n              left: rect.left,\n              top: rect.top,\n              right: rect.right,\n              bottom: rect.bottom,\n            });\n          }\n        }\n\n        return bounds;\n      });\n\n      return boxes.some(\n        (box) =>\n          x >= box.left && x <= box.right && y >= box.top && y <= box.bottom,\n      );\n    } catch {\n      return false;\n    }\n  }\n\n  private async injectCursor(): Promise<void> {\n    try {\n      const page = await this.v3.context.awaitActivePage();\n      await page.enableCursorOverlay();\n    } catch {\n      // Best-effort only\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/index.ts",
    "content": "import * as PublicApi from \"./types/public/index.js\";\nimport { V3 } from \"./v3.js\";\nimport { AnnotatedScreenshotText, LLMClient } from \"./llm/LLMClient.js\";\nimport {\n  AgentProvider,\n  modelToAgentProviderMap,\n} from \"./agent/AgentProvider.js\";\nimport {\n  validateZodSchema,\n  isRunningInBun,\n  toGeminiSchema,\n  getZodType,\n  transformSchema,\n  injectUrls,\n  providerEnvVarMap,\n  loadApiKeyFromEnv,\n  trimTrailingTextNode,\n  jsonSchemaToZod,\n} from \"../utils.js\";\nimport { isZod4Schema, isZod3Schema, toJsonSchema } from \"./zodCompat.js\";\nimport { connectToMCPServer } from \"./mcp/connection.js\";\nimport { V3Evaluator } from \"../v3Evaluator.js\";\nimport { tool } from \"ai\";\nimport { getAISDKLanguageModel } from \"./llm/LLMProvider.js\";\nimport { __internalCreateInMemoryAgentCacheHandle } from \"./cache/serverAgentCache.js\";\nimport { maybeRunShutdownSupervisorFromArgv } from \"./shutdown/supervisor.js\";\n\nexport { V3 } from \"./v3.js\";\nexport { V3 as Stagehand } from \"./v3.js\";\n\nexport * from \"./types/public/index.js\";\nexport { AnnotatedScreenshotText, LLMClient } from \"./llm/LLMClient.js\";\n\nexport {\n  AgentProvider,\n  modelToAgentProviderMap,\n} from \"./agent/AgentProvider.js\";\nexport type {\n  AgentTools,\n  AgentToolTypesMap,\n  AgentUITools,\n  AgentToolCall,\n  AgentToolResult,\n} from \"./agent/tools/index.js\";\n\nexport {\n  validateZodSchema,\n  isRunningInBun,\n  toGeminiSchema,\n  getZodType,\n  transformSchema,\n  injectUrls,\n  providerEnvVarMap,\n  loadApiKeyFromEnv,\n  trimTrailingTextNode,\n  jsonSchemaToZod,\n} from \"../utils.js\";\nexport { isZod4Schema, isZod3Schema, toJsonSchema } from \"./zodCompat.js\";\n\nexport { connectToMCPServer } from \"./mcp/connection.js\";\nexport { V3Evaluator } from \"../v3Evaluator.js\";\nexport { tool } from \"ai\";\nexport { getAISDKLanguageModel } from \"./llm/LLMProvider.js\";\nexport { __internalCreateInMemoryAgentCacheHandle } from \"./cache/serverAgentCache.js\";\nexport { maybeRunShutdownSupervisorFromArgv as __internalMaybeRunShutdownSupervisorFromArgv } from \"./shutdown/supervisor.js\";\nexport type { ServerAgentCacheHandle } from \"./cache/serverAgentCache.js\";\n\nexport type {\n  ChatMessage,\n  ChatMessageContent,\n  ChatMessageImageContent,\n  ChatMessageTextContent,\n  ChatCompletionOptions,\n  LLMResponse,\n  CreateChatCompletionOptions,\n  LLMUsage,\n  LLMParsedResponse,\n} from \"./llm/LLMClient.js\";\n\nexport type {\n  StagehandZodSchema,\n  StagehandZodObject,\n  InferStagehandSchema,\n  JsonSchemaDocument,\n} from \"./zodCompat.js\";\n\nexport type { JsonSchema, JsonSchemaProperty } from \"../utils.js\";\n\nconst StagehandDefault = {\n  ...PublicApi,\n  V3,\n  Stagehand: V3,\n  AnnotatedScreenshotText,\n  LLMClient,\n  AgentProvider,\n  modelToAgentProviderMap,\n  validateZodSchema,\n  isRunningInBun,\n  toGeminiSchema,\n  getZodType,\n  transformSchema,\n  injectUrls,\n  providerEnvVarMap,\n  loadApiKeyFromEnv,\n  trimTrailingTextNode,\n  jsonSchemaToZod,\n  isZod4Schema,\n  isZod3Schema,\n  toJsonSchema,\n  connectToMCPServer,\n  V3Evaluator,\n  tool,\n  getAISDKLanguageModel,\n  __internalCreateInMemoryAgentCacheHandle,\n  __internalMaybeRunShutdownSupervisorFromArgv:\n    maybeRunShutdownSupervisorFromArgv,\n};\n\nexport default StagehandDefault;\n"
  },
  {
    "path": "packages/core/lib/v3/launch/browserbase.ts",
    "content": "import Browserbase from \"@browserbasehq/sdk\";\nimport {\n  BrowserbaseSessionNotFoundError,\n  StagehandInitError,\n} from \"../types/public/sdkErrors.js\";\nimport type { BrowserbaseSessionCreateParams } from \"../types/public/api.js\";\nimport { getEnvTimeoutMs, withTimeout } from \"../timeoutConfig.js\";\n\nexport async function createBrowserbaseSession(\n  apiKey: string,\n  projectId?: string,\n  params?: BrowserbaseSessionCreateParams,\n  resumeSessionId?: string,\n): Promise<{ ws: string; sessionId: string; bb: Browserbase }> {\n  const bb = new Browserbase({ apiKey });\n  const sessionCreateTimeoutMs = getEnvTimeoutMs(\n    \"BROWSERBASE_SESSION_CREATE_MAX_MS\",\n  );\n\n  // Resume an existing session if provided\n  if (resumeSessionId) {\n    const existing = (await withTimeout(\n      bb.sessions.retrieve(resumeSessionId),\n      sessionCreateTimeoutMs,\n      \"Browserbase session retrieve\",\n    )) as unknown as {\n      id: string;\n      connectUrl?: string;\n      status?: string;\n    };\n    if (!existing?.id) {\n      throw new BrowserbaseSessionNotFoundError();\n    }\n\n    const ws = existing.connectUrl;\n    if (!ws) {\n      throw new StagehandInitError(\n        `Browserbase session resume missing connectUrl for ${resumeSessionId}`,\n      );\n    }\n    return { ws, sessionId: resumeSessionId, bb };\n  }\n\n  // Create a new session with optional overrides and a default viewport\n  const {\n    projectId: overrideProjectId,\n    browserSettings,\n    userMetadata,\n    ...rest\n  } = params ?? {};\n\n  // satisfies check ensures our BrowserbaseSessionCreateParamsSchema stays in sync with SDK\n  const resolvedProjectId = overrideProjectId ?? projectId;\n  const createPayload = {\n    ...(resolvedProjectId ? { projectId: resolvedProjectId } : {}),\n    ...rest,\n    browserSettings: {\n      ...(browserSettings ?? {}),\n      viewport: browserSettings?.viewport ?? { width: 1288, height: 711 },\n    },\n    userMetadata: {\n      ...(userMetadata ?? {}),\n      stagehand: \"true\",\n    },\n  } satisfies Browserbase.Sessions.SessionCreateParams;\n\n  const created = (await withTimeout(\n    bb.sessions.create(createPayload),\n    sessionCreateTimeoutMs,\n    \"Browserbase session create\",\n  )) as unknown as { id: string; connectUrl: string };\n\n  if (!created?.connectUrl || !created?.id) {\n    throw new StagehandInitError(\n      \"Browserbase session creation returned an unexpected shape.\",\n    );\n  }\n\n  return { ws: created.connectUrl, sessionId: created.id, bb };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/launch/local.ts",
    "content": "import { launch, LaunchedChrome } from \"chrome-launcher\";\nimport WebSocket from \"ws\";\nimport { ConnectionTimeoutError } from \"../types/public/sdkErrors.js\";\n\ninterface LaunchLocalOptions {\n  chromePath?: string;\n  chromeFlags?: string[];\n  headless?: boolean;\n  userDataDir?: string;\n  port?: number;\n  connectTimeoutMs?: number;\n  handleSIGINT?: boolean;\n}\n\nexport async function launchLocalChrome(\n  opts: LaunchLocalOptions,\n): Promise<{ ws: string; chrome: LaunchedChrome }> {\n  const connectTimeoutMs = opts.connectTimeoutMs ?? 15_000;\n  const deadlineMs = Date.now() + connectTimeoutMs;\n  const connectionPollInterval = 250;\n  const maxConnectionRetries = Math.max(\n    1,\n    Math.ceil(connectTimeoutMs / connectionPollInterval),\n  );\n  const headless = opts.headless ?? false;\n  const chromeFlags = [\n    headless ? \"--headless=new\" : undefined,\n    \"--remote-allow-origins=*\",\n    \"--no-first-run\",\n    \"--no-default-browser-check\",\n    \"--disable-dev-shm-usage\",\n    \"--site-per-process\",\n    ...(opts.chromeFlags ?? []),\n  ].filter((f): f is string => typeof f === \"string\");\n\n  const chrome = await launch({\n    chromePath: opts.chromePath,\n    chromeFlags,\n    port: opts.port,\n    userDataDir: opts.userDataDir,\n    handleSIGINT: opts.handleSIGINT,\n    connectionPollInterval,\n    maxConnectionRetries,\n  });\n\n  const ws = await waitForWebSocketDebuggerUrl(chrome.port, deadlineMs);\n  await waitForWebSocketReady(ws, deadlineMs);\n\n  return { ws, chrome };\n}\n\nasync function waitForWebSocketDebuggerUrl(\n  port: number,\n  deadlineMs: number,\n): Promise<string> {\n  let lastErrMsg = \"\";\n\n  while (Date.now() < deadlineMs) {\n    try {\n      const resp = await fetch(`http://127.0.0.1:${port}/json/version`);\n      if (resp.ok) {\n        const json = (await resp.json()) as unknown;\n        const url = (json as { webSocketDebuggerUrl?: string })\n          .webSocketDebuggerUrl;\n        if (typeof url === \"string\") return url;\n      } else {\n        lastErrMsg = `${resp.status} ${resp.statusText}`;\n      }\n    } catch (err) {\n      lastErrMsg = err instanceof Error ? err.message : String(err);\n    }\n    await new Promise((r) => setTimeout(r, 250));\n  }\n\n  throw new ConnectionTimeoutError(\n    `Timed out waiting for /json/version on port ${port} ${\n      lastErrMsg ? ` (last error: ${lastErrMsg})` : \"\"\n    }`,\n  );\n}\n\nasync function waitForWebSocketReady(\n  wsUrl: string,\n  deadlineMs: number,\n): Promise<void> {\n  let lastErrMsg = \"\";\n  while (Date.now() < deadlineMs) {\n    const remainingMs = Math.max(200, deadlineMs - Date.now());\n    try {\n      await probeWebSocket(wsUrl, Math.min(2_000, remainingMs));\n      return;\n    } catch (error) {\n      lastErrMsg = error instanceof Error ? error.message : String(error);\n      await new Promise((r) => setTimeout(r, 100));\n    }\n  }\n  throw new ConnectionTimeoutError(\n    `Timed out waiting for CDP websocket to accept connections at ${wsUrl}${\n      lastErrMsg ? ` (last error: ${lastErrMsg})` : \"\"\n    }`,\n  );\n}\n\nfunction probeWebSocket(wsUrl: string, timeoutMs: number): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const ws = new WebSocket(wsUrl);\n    let settled = false;\n    const finish = (error?: unknown) => {\n      if (settled) return;\n      settled = true;\n      clearTimeout(timer);\n      try {\n        ws.terminate();\n      } catch {\n        // best-effort cleanup\n      }\n      if (error) {\n        reject(error);\n        return;\n      }\n      resolve();\n    };\n    const timer = setTimeout(() => {\n      finish(new Error(`websocket probe timeout after ${timeoutMs}ms`));\n    }, timeoutMs);\n\n    ws.once(\"open\", () => finish());\n    ws.once(\"error\", (error) => finish(error));\n  });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/AnthropicClient.ts",
    "content": "import Anthropic, { ClientOptions } from \"@anthropic-ai/sdk\";\nimport {\n  ImageBlockParam,\n  MessageParam,\n  TextBlockParam,\n  Tool,\n} from \"@anthropic-ai/sdk/resources\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport {\n  AnthropicJsonSchemaObject,\n  AvailableModel,\n} from \"../types/public/model.js\";\nimport {\n  CreateChatCompletionOptions,\n  LLMClient,\n  LLMResponse,\n} from \"./LLMClient.js\";\nimport { CreateChatCompletionResponseError } from \"../types/public/sdkErrors.js\";\nimport { toJsonSchema } from \"../zodCompat.js\";\n\nexport class AnthropicClient extends LLMClient {\n  public type = \"anthropic\" as const;\n  private client: Anthropic;\n  declare public clientOptions: ClientOptions;\n\n  constructor({\n    modelName,\n    clientOptions,\n    userProvidedInstructions,\n  }: {\n    logger: (message: LogLine) => void;\n    modelName: AvailableModel;\n    clientOptions?: ClientOptions;\n    userProvidedInstructions?: string;\n  }) {\n    super(modelName);\n    this.client = new Anthropic(clientOptions);\n    this.modelName = modelName;\n    this.clientOptions = clientOptions;\n    this.userProvidedInstructions = userProvidedInstructions;\n  }\n\n  async createChatCompletion<T = LLMResponse>({\n    options,\n    retries,\n    logger,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const optionsWithoutImage = { ...options };\n    delete optionsWithoutImage.image;\n\n    logger({\n      category: \"anthropic\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        options: {\n          value: JSON.stringify(optionsWithoutImage),\n          type: \"object\",\n        },\n      },\n    });\n\n    const systemMessage = options.messages.find((msg) => {\n      if (msg.role === \"system\") {\n        if (typeof msg.content === \"string\") {\n          return true;\n        } else if (Array.isArray(msg.content)) {\n          return msg.content.every((content) => content.type !== \"image_url\");\n        }\n      }\n      return false;\n    });\n\n    const userMessages = options.messages.filter(\n      (msg) => msg.role !== \"system\",\n    );\n\n    const formattedMessages: MessageParam[] = userMessages.map((msg) => {\n      if (typeof msg.content === \"string\") {\n        return {\n          role: msg.role as \"user\" | \"assistant\", // ensure its not checking for system types\n          content: msg.content,\n        };\n      } else {\n        return {\n          role: msg.role as \"user\" | \"assistant\",\n          content: msg.content.map((content) => {\n            if (\"image_url\" in content) {\n              const formattedContent: ImageBlockParam = {\n                type: \"image\",\n                source: {\n                  type: \"base64\",\n                  media_type: \"image/jpeg\",\n                  data: content.image_url.url,\n                },\n              };\n\n              return formattedContent;\n            } else {\n              return { type: \"text\", text: content.text };\n            }\n          }),\n        };\n      }\n    });\n\n    if (options.image) {\n      const screenshotMessage: MessageParam = {\n        role: \"user\",\n        content: [\n          {\n            type: \"image\",\n            source: {\n              type: \"base64\",\n              media_type: \"image/jpeg\",\n              data: options.image.buffer.toString(\"base64\"),\n            },\n          },\n        ],\n      };\n      if (\n        options.image.description &&\n        Array.isArray(screenshotMessage.content)\n      ) {\n        screenshotMessage.content.push({\n          type: \"text\",\n          text: options.image.description,\n        });\n      }\n\n      formattedMessages.push(screenshotMessage);\n    }\n\n    let anthropicTools: Tool[] = options.tools?.map((tool) => {\n      return {\n        name: tool.name,\n        description: tool.description,\n        input_schema: {\n          type: \"object\",\n          properties: tool.parameters.properties,\n          required: tool.parameters.required,\n        },\n      };\n    });\n\n    let toolDefinition: Tool | undefined;\n    if (options.response_model) {\n      const jsonSchema = toJsonSchema(options.response_model.schema);\n      const { properties: schemaProperties, required: schemaRequired } =\n        extractSchemaProperties(jsonSchema);\n\n      toolDefinition = {\n        name: \"print_extracted_data\",\n        description: \"Prints the extracted data based on the provided schema.\",\n        input_schema: {\n          type: \"object\",\n          properties: schemaProperties,\n          required: schemaRequired,\n        },\n      };\n    }\n\n    if (toolDefinition) {\n      anthropicTools = anthropicTools ?? [];\n      anthropicTools.push(toolDefinition);\n    }\n\n    const response = await this.client.messages.create({\n      model: this.modelName,\n      max_tokens: options.maxOutputTokens || 8192,\n      messages: formattedMessages,\n      tools: anthropicTools,\n      system: systemMessage\n        ? (systemMessage.content as string | TextBlockParam[]) // we can cast because we already filtered out image content\n        : undefined,\n      temperature: options.temperature,\n    });\n\n    logger({\n      category: \"anthropic\",\n      message: \"response\",\n      level: 2,\n      auxiliary: {\n        response: {\n          value: JSON.stringify(response),\n          type: \"object\",\n        },\n        requestId: {\n          value: options.requestId,\n          type: \"string\",\n        },\n      },\n    });\n\n    // We'll compute usage data from the response\n    const usageData = {\n      prompt_tokens: response.usage.input_tokens,\n      completion_tokens: response.usage.output_tokens,\n      total_tokens: response.usage.input_tokens + response.usage.output_tokens,\n    };\n\n    const transformedResponse: LLMResponse = {\n      id: response.id,\n      object: \"chat.completion\",\n      created: Date.now(),\n      model: response.model,\n      choices: [\n        {\n          index: 0,\n          message: {\n            role: \"assistant\",\n            content:\n              response.content.find((c) => c.type === \"text\")?.text || null,\n            tool_calls: response.content\n              .filter((c) => c.type === \"tool_use\")\n              .map((toolUse) => ({\n                id: toolUse.id,\n                type: \"function\",\n                function: {\n                  name: toolUse.name,\n                  arguments: JSON.stringify(toolUse.input),\n                },\n              })),\n          },\n          finish_reason: response.stop_reason,\n        },\n      ],\n      usage: usageData,\n    };\n\n    logger({\n      category: \"anthropic\",\n      message: \"transformed response\",\n      level: 2,\n      auxiliary: {\n        transformedResponse: {\n          value: JSON.stringify(transformedResponse),\n          type: \"object\",\n        },\n        requestId: {\n          value: options.requestId,\n          type: \"string\",\n        },\n      },\n    });\n\n    if (options.response_model) {\n      const toolUse = response.content.find((c) => c.type === \"tool_use\");\n      if (toolUse && \"input\" in toolUse) {\n        const result = toolUse.input;\n\n        const finalParsedResponse = {\n          data: result,\n          usage: usageData,\n        } as unknown as T;\n\n        return finalParsedResponse;\n      } else {\n        if (!retries || retries < 5) {\n          return this.createChatCompletion({\n            options,\n            logger,\n            retries: (retries ?? 0) + 1,\n          });\n        }\n        logger({\n          category: \"anthropic\",\n          message: \"error creating chat completion\",\n          level: 0,\n          auxiliary: {\n            requestId: {\n              value: options.requestId,\n              type: \"string\",\n            },\n          },\n        });\n        throw new CreateChatCompletionResponseError(\n          \"No tool use with input in response\",\n        );\n      }\n    }\n\n    // if the function was called with a response model, it would have returned earlier\n    // so we can safely cast here to T, which defaults to AnthropicTransformedResponse\n    return transformedResponse as T;\n  }\n}\n\nconst extractSchemaProperties = (jsonSchema: AnthropicJsonSchemaObject) => {\n  const schemaRoot = jsonSchema.definitions?.MySchema || jsonSchema;\n\n  return {\n    properties: schemaRoot.properties,\n    required: schemaRoot.required,\n  };\n};\n"
  },
  {
    "path": "packages/core/lib/v3/llm/CerebrasClient.ts",
    "content": "import OpenAI from \"openai\";\nimport type { ClientOptions } from \"openai\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { AvailableModel } from \"../types/public/model.js\";\nimport {\n  ChatMessage,\n  CreateChatCompletionOptions,\n  LLMClient,\n  LLMResponse,\n} from \"./LLMClient.js\";\nimport { CreateChatCompletionResponseError } from \"../types/public/sdkErrors.js\";\nimport { toJsonSchema } from \"../zodCompat.js\";\n\nexport class CerebrasClient extends LLMClient {\n  public type = \"cerebras\" as const;\n  private client: OpenAI;\n  declare public clientOptions: ClientOptions;\n  public hasVision = false;\n\n  constructor({\n    modelName,\n    clientOptions,\n    userProvidedInstructions,\n  }: {\n    logger: (message: LogLine) => void;\n    modelName: AvailableModel;\n    clientOptions?: ClientOptions;\n    userProvidedInstructions?: string;\n  }) {\n    super(modelName, userProvidedInstructions);\n\n    // Create OpenAI client with the base URL set to Cerebras API\n    this.client = new OpenAI({\n      baseURL: \"https://api.cerebras.ai/v1\",\n      apiKey: clientOptions?.apiKey || process.env.CEREBRAS_API_KEY,\n      ...clientOptions,\n    });\n\n    this.modelName = modelName;\n    this.clientOptions = clientOptions;\n  }\n\n  async createChatCompletion<T = LLMResponse>({\n    options,\n    retries,\n    logger,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const optionsWithoutImage = { ...options };\n    delete optionsWithoutImage.image;\n\n    logger({\n      category: \"cerebras\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        options: {\n          value: JSON.stringify(optionsWithoutImage),\n          type: \"object\",\n        },\n      },\n    });\n\n    // Format messages for Cerebras API (using OpenAI format)\n    const formattedMessages = options.messages.map((msg: ChatMessage) => {\n      const baseMessage = {\n        content:\n          typeof msg.content === \"string\"\n            ? msg.content\n            : Array.isArray(msg.content) &&\n                msg.content.length > 0 &&\n                \"text\" in msg.content[0]\n              ? msg.content[0].text\n              : \"\",\n      };\n\n      // Cerebras only supports system, user, and assistant roles\n      if (msg.role === \"system\") {\n        return { ...baseMessage, role: \"system\" as const };\n      } else if (msg.role === \"assistant\") {\n        return { ...baseMessage, role: \"assistant\" as const };\n      } else {\n        // Default to user for any other role\n        return { ...baseMessage, role: \"user\" as const };\n      }\n    });\n\n    // Format tools if provided\n    let tools = options.tools?.map((tool) => ({\n      type: \"function\" as const,\n      function: {\n        name: tool.name,\n        description: tool.description,\n        parameters: {\n          type: \"object\",\n          properties: tool.parameters.properties,\n          required: tool.parameters.required,\n        },\n      },\n    }));\n\n    // Add response model as a tool if provided\n    if (options.response_model) {\n      const jsonSchema = toJsonSchema(options.response_model.schema) as {\n        properties?: Record<string, unknown>;\n        required?: string[];\n      };\n      const schemaProperties = jsonSchema.properties || {};\n      const schemaRequired = jsonSchema.required || [];\n\n      const responseTool = {\n        type: \"function\" as const,\n        function: {\n          name: \"print_extracted_data\",\n          description:\n            \"Prints the extracted data based on the provided schema.\",\n          parameters: {\n            type: \"object\",\n            properties: schemaProperties,\n            required: schemaRequired,\n          },\n        },\n      };\n\n      tools = tools ? [...tools, responseTool] : [responseTool];\n    }\n\n    try {\n      // Use OpenAI client with Cerebras API\n      const apiResponse = await this.client.chat.completions.create({\n        model: this.modelName.split(\"cerebras-\")[1],\n        messages: [\n          ...formattedMessages,\n          // Add explicit instruction to return JSON if we have a response model\n          ...(options.response_model\n            ? [\n                {\n                  role: \"system\" as const,\n                  content: `IMPORTANT: Your response must be valid JSON that matches this schema: ${JSON.stringify(\n                    options.response_model.schema,\n                  )}`,\n                },\n              ]\n            : []),\n        ],\n        temperature: options.temperature || 0.7,\n        max_tokens: options.maxOutputTokens,\n        tools: tools,\n        tool_choice: options.tool_choice || \"auto\",\n      });\n\n      // Format the response to match the expected LLMResponse format\n      const response: LLMResponse = {\n        id: apiResponse.id,\n        object: \"chat.completion\",\n        created: Date.now(),\n        model: this.modelName.split(\"cerebras-\")[1],\n        choices: [\n          {\n            index: 0,\n            message: {\n              role: \"assistant\",\n              content: apiResponse.choices[0]?.message?.content || null,\n              tool_calls: apiResponse.choices[0]?.message?.tool_calls || [],\n            },\n            finish_reason: apiResponse.choices[0]?.finish_reason || \"stop\",\n          },\n        ],\n        usage: {\n          prompt_tokens: apiResponse.usage?.prompt_tokens || 0,\n          completion_tokens: apiResponse.usage?.completion_tokens || 0,\n          total_tokens: apiResponse.usage?.total_tokens || 0,\n        },\n      };\n\n      logger({\n        category: \"cerebras\",\n        message: \"response\",\n        level: 2,\n        auxiliary: {\n          response: {\n            value: JSON.stringify(response),\n            type: \"object\",\n          },\n          requestId: {\n            value: options.requestId,\n            type: \"string\",\n          },\n        },\n      });\n\n      // If we have no response model, just return the entire LLMResponse\n      if (!options.response_model) {\n        return response as T;\n      }\n\n      // If we have a response model, parse JSON from tool calls or content\n      const toolCall = response.choices[0]?.message?.tool_calls?.[0];\n      if (toolCall?.function?.arguments) {\n        try {\n          const result = JSON.parse(toolCall.function.arguments);\n          const finalResponse = {\n            data: result,\n            usage: response.usage,\n          };\n          return finalResponse as T;\n        } catch (e) {\n          logger({\n            category: \"cerebras\",\n            message: \"failed to parse tool call arguments as JSON, retrying\",\n            level: 0,\n            auxiliary: {\n              error: {\n                value: e.message,\n                type: \"string\",\n              },\n            },\n          });\n        }\n      }\n\n      // If we have content but no tool calls, try to parse the content as JSON\n      const content = response.choices[0]?.message?.content;\n      if (content) {\n        try {\n          const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n          if (jsonMatch) {\n            const result = JSON.parse(jsonMatch[0]);\n            const finalResponse = {\n              data: result,\n              usage: response.usage,\n            };\n            return finalResponse as T;\n          }\n        } catch (e) {\n          logger({\n            category: \"cerebras\",\n            message: \"failed to parse content as JSON\",\n            level: 0,\n            auxiliary: {\n              error: {\n                value: e.message,\n                type: \"string\",\n              },\n            },\n          });\n        }\n      }\n\n      // If we still haven't found valid JSON and have retries left, try again\n      if (!retries || retries < 5) {\n        return this.createChatCompletion({\n          options,\n          logger,\n          retries: (retries ?? 0) + 1,\n        });\n      }\n\n      throw new CreateChatCompletionResponseError(\"Invalid response schema\");\n    } catch (error) {\n      logger({\n        category: \"cerebras\",\n        message: \"error creating chat completion\",\n        level: 0,\n        auxiliary: {\n          error: {\n            value: error.message,\n            type: \"string\",\n          },\n          requestId: {\n            value: options.requestId,\n            type: \"string\",\n          },\n        },\n      });\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/GoogleClient.ts",
    "content": "import {\n  GoogleGenAI,\n  HarmCategory,\n  HarmBlockThreshold,\n  Content,\n  Part,\n  Tool,\n  FunctionCall,\n  Schema,\n  Type,\n} from \"@google/genai\";\n\nimport { LogLine } from \"../types/public/logs.js\";\nimport { AvailableModel, ClientOptions } from \"../types/public/model.js\";\nimport {\n  validateZodSchema,\n  toGeminiSchema,\n  loadApiKeyFromEnv,\n} from \"../../utils.js\";\nimport {\n  ChatCompletionOptions,\n  ChatMessage,\n  CreateChatCompletionOptions,\n  LLMClient,\n  LLMResponse,\n  AnnotatedScreenshotText,\n} from \"./LLMClient.js\";\nimport {\n  CreateChatCompletionResponseError,\n  StagehandError,\n} from \"../types/public/sdkErrors.js\";\n\n// Mapping from generic roles to Gemini roles\nconst roleMap: { [key in ChatMessage[\"role\"]]: string } = {\n  user: \"user\",\n  assistant: \"model\",\n  system: \"user\", // Gemini API prefers system instructions either via system_instruction or at the start of 'user' content\n};\n\n// Basic safety settings - adjust as needed\nconst safetySettings = [\n  {\n    category: HarmCategory.HARM_CATEGORY_HARASSMENT,\n    threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,\n  },\n  {\n    category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,\n    threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,\n  },\n  {\n    category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,\n    threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,\n  },\n  {\n    category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,\n    threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,\n  },\n];\n\nexport class GoogleClient extends LLMClient {\n  public type = \"google\" as const;\n  private client: GoogleGenAI;\n  declare public clientOptions: ClientOptions;\n  declare public hasVision: boolean;\n  private logger: (message: LogLine) => void;\n\n  constructor({\n    logger, // Added logger based on other clients\n    modelName,\n    clientOptions,\n  }: {\n    logger: (message: LogLine) => void; // Added logger type\n    modelName: AvailableModel;\n    clientOptions?: ClientOptions; // Expecting { apiKey: string } here\n  }) {\n    super(modelName);\n    if (!clientOptions?.apiKey) {\n      // Try to get the API key from the environment variable GOOGLE_API_KEY\n      clientOptions.apiKey = loadApiKeyFromEnv(\"google_legacy\", logger);\n    }\n    this.clientOptions = clientOptions;\n    this.client = new GoogleGenAI({ apiKey: clientOptions.apiKey });\n    this.modelName = modelName;\n    this.logger = logger;\n    // Determine vision capability based on model name (adjust as needed)\n    this.hasVision =\n      modelName.includes(\"vision\") || modelName.includes(\"gemini-1.5\"); // Example logic\n  }\n\n  // Helper to convert project's ChatMessage[] to Gemini's Content[]\n  private formatMessages(\n    messages: ChatMessage[],\n    image?: ChatCompletionOptions[\"image\"],\n  ): Content[] {\n    const contents: Content[] = [];\n    let systemInstruction: string | null = null;\n\n    messages.forEach((msg, index) => {\n      const role = roleMap[msg.role];\n      if (!role) {\n        this.logger({\n          category: \"google\",\n          message: `WARNING: Unsupported role: ${msg.role}`,\n          level: 1,\n        });\n        return; // Skip unsupported roles\n      }\n\n      // Handle system messages - prepend to the first user message or use system_instruction if available\n      if (msg.role === \"system\") {\n        if (typeof msg.content === \"string\") {\n          systemInstruction =\n            (systemInstruction ? systemInstruction + \"\\n\\n\" : \"\") + msg.content;\n        }\n        return; // Don't add system messages directly to contents yet\n      }\n\n      const parts: Part[] = [];\n\n      if (Array.isArray(msg.content)) {\n        msg.content.forEach((partContent) => {\n          if (partContent.type === \"text\") {\n            parts.push({ text: partContent.text });\n          } else if (partContent.type === \"image_url\") {\n            if (\"image_url\" in partContent && partContent.image_url?.url) {\n              // Assuming base64 data URI format: data:[<mediatype>];base64,<data>\n              const base64Data = partContent.image_url.url.split(\",\")[1];\n              const mimeTypeMatch = partContent.image_url.url.match(\n                /^data:(image\\/\\w+);base64,/,\n              );\n              if (base64Data && mimeTypeMatch) {\n                parts.push({\n                  inlineData: { mimeType: mimeTypeMatch[1], data: base64Data },\n                });\n              } else {\n                this.logger({\n                  category: \"google\",\n                  message: \"WARNING: Could not parse image data URI format\",\n                  level: 1,\n                });\n              }\n            }\n          }\n        });\n      } else if (typeof msg.content === \"string\") {\n        parts.push({ text: msg.content });\n      }\n\n      // Add image from options if this is the last message and it's a user message\n      if (image && index === messages.length - 1 && msg.role === \"user\") {\n        const imageDesc = image.description || AnnotatedScreenshotText;\n        parts.push({ text: imageDesc }); // Add description first\n        parts.push({\n          inlineData: {\n            mimeType: \"image/jpeg\", // Assuming JPEG, adjust if needed\n            data: image.buffer.toString(\"base64\"),\n          },\n        });\n      }\n\n      // Apply system instruction to the first non-system message if needed\n      if (systemInstruction && contents.length === 0 && role === \"user\") {\n        const firstPartText = parts.find((p) => \"text\" in p);\n        if (firstPartText && \"text\" in firstPartText) {\n          firstPartText.text = `${systemInstruction}\\n\\n${firstPartText.text}`;\n        } else {\n          parts.unshift({ text: systemInstruction });\n        }\n        systemInstruction = null; // Clear after applying\n      }\n\n      if (parts.length > 0) {\n        contents.push({ role, parts });\n      }\n    });\n\n    // If system instruction wasn't applied (e.g., no user messages followed it), add it as a final user message\n    if (systemInstruction) {\n      contents.unshift({ role: \"user\", parts: [{ text: systemInstruction }] });\n    }\n\n    return contents;\n  }\n\n  // Helper to convert LLMTool[] to Gemini's Tool[]\n  private formatTools(\n    tools?: ChatCompletionOptions[\"tools\"],\n  ): Tool[] | undefined {\n    if (!tools || tools.length === 0) {\n      return undefined;\n    }\n\n    return [\n      {\n        functionDeclarations: tools.map((tool) => {\n          let parameters: Schema | undefined = undefined;\n          if (tool.parameters) {\n            parameters = {\n              type: Type.OBJECT,\n              properties: tool.parameters.properties as {\n                [key: string]: Schema;\n              },\n              required: tool.parameters.required as string[] | undefined,\n            };\n          }\n          return {\n            name: tool.name,\n            description: tool.description,\n            parameters: parameters,\n          };\n        }),\n      },\n    ];\n  }\n\n  async createChatCompletion<T = LLMResponse>({\n    // Ensure LLMResponse is compatible\n    options,\n    logger,\n    retries = 3,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const {\n      image,\n      requestId,\n      response_model,\n      tools,\n      temperature,\n      top_p,\n      maxOutputTokens,\n    } = options;\n\n    const formattedMessages = this.formatMessages(options.messages, image);\n    const formattedTools = this.formatTools(tools);\n\n    const generationConfig = {\n      maxOutputTokens: maxOutputTokens,\n      temperature: temperature,\n      topP: top_p,\n      responseMimeType: response_model ? \"application/json\" : undefined,\n      responseSchema: response_model\n        ? toGeminiSchema(response_model.schema)\n        : undefined,\n    };\n\n    logger({\n      category: \"google\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        modelName: { value: this.modelName, type: \"string\" },\n        requestId: { value: requestId, type: \"string\" },\n        requestPayloadSummary: {\n          value: `Model: ${this.modelName}, Messages: ${formattedMessages.length}, Config Keys: ${Object.keys(generationConfig).join(\", \")}, Tools: ${formattedTools ? formattedTools.length : 0}, Safety Categories: ${safetySettings.map((s) => s.category).join(\", \")}`,\n          type: \"string\",\n        },\n      },\n    });\n\n    // Construct the full request object\n    const requestPayload = {\n      model: this.modelName,\n      contents: formattedMessages,\n      config: {\n        ...generationConfig,\n        safetySettings: safetySettings,\n        tools: formattedTools,\n      },\n    };\n\n    // Log the full payload safely\n    try {\n      logger({\n        category: \"google\",\n        message: \"Full request payload\",\n        level: 2,\n        auxiliary: {\n          requestId: { value: requestId, type: \"string\" },\n          fullPayload: {\n            value: JSON.stringify(requestPayload),\n            type: \"object\",\n          },\n        },\n      });\n    } catch (e) {\n      logger({\n        category: \"google\",\n        message: \"Failed to stringify full request payload for logging\",\n        level: 0,\n        auxiliary: {\n          requestId: { value: requestId, type: \"string\" },\n          error: { value: e.message, type: \"string\" },\n        },\n      });\n    }\n\n    try {\n      const result = await this.client.models.generateContent(requestPayload); // Pass the constructed payload\n\n      logger({\n        category: \"google\",\n        message: \"received response\",\n        level: 2,\n        auxiliary: {\n          requestId: { value: requestId, type: \"string\" },\n          response: {\n            value: JSON.stringify(result),\n            type: \"object\",\n          },\n        },\n      });\n\n      const finishReason = result.candidates?.[0]?.finishReason || \"unknown\";\n      const toolCalls = result.functionCalls?.map(\n        (fc: FunctionCall, index: number) => ({\n          id: `tool_call_${requestId}_${index}`,\n          type: \"function\" as const,\n          function: {\n            name: fc.name,\n            arguments: JSON.stringify(fc.args),\n          },\n        }),\n      );\n\n      let content: string | null = null;\n      try {\n        content = result.text;\n      } catch (e) {\n        logger({\n          category: \"google\",\n          message: `Could not extract text content: ${e.message}`,\n          level: 1,\n          auxiliary: { requestId: { value: requestId, type: \"string\" } },\n        });\n        content = null;\n      }\n\n      // Construct LLMResponse shape\n      const llmResponse: LLMResponse = {\n        id: result.candidates?.[0]?.index?.toString() || requestId,\n        object: \"chat.completion\",\n        created: Math.floor(Date.now() / 1000),\n        model: this.modelName,\n        choices: [\n          {\n            index: 0,\n            message: {\n              role: \"assistant\",\n              content: content,\n              tool_calls: toolCalls,\n            },\n            finish_reason: finishReason,\n          },\n        ],\n        usage: {\n          prompt_tokens: result.usageMetadata?.promptTokenCount || 0,\n          completion_tokens: result.usageMetadata?.candidatesTokenCount || 0,\n          total_tokens: result.usageMetadata?.totalTokenCount || 0,\n        },\n      };\n\n      // Validate schema if response_model was provided\n      if (response_model) {\n        let parsedData;\n        try {\n          // Need to handle potential markdown fences if the model didn't follow instructions perfectly\n          const potentialJson =\n            content?.trim().replace(/^```json\\n?|\\n?```$/g, \"\") || \"{}\";\n          parsedData = JSON.parse(potentialJson);\n        } catch (e) {\n          logger({\n            category: \"google\",\n            message: `Failed to parse JSON response: ${e.message}`,\n            level: 0,\n            auxiliary: {\n              content: { value: content || \"null\", type: \"string\" },\n            },\n          });\n          if (retries > 0) {\n            return this.createChatCompletion({\n              options,\n              logger,\n              retries: retries - 1,\n            });\n          }\n          throw new CreateChatCompletionResponseError(\n            `Failed to parse JSON response: ${e.message}`,\n          );\n        }\n\n        try {\n          validateZodSchema(response_model.schema, parsedData);\n        } catch (err) {\n          logger({\n            category: \"google\",\n            message: \"Response failed Zod schema validation\",\n            level: 0,\n          });\n          if (retries > 0) {\n            return this.createChatCompletion({\n              options,\n              logger,\n              retries: retries - 1,\n            });\n          }\n          throw err;\n        }\n\n        // If schema validation passes, structure the response for extraction use case\n        const extractionResult = {\n          data: parsedData,\n          usage: llmResponse.usage,\n        };\n\n        return extractionResult as T;\n      }\n\n      return llmResponse as T;\n    } catch (error) {\n      logger({\n        category: \"google\",\n        message: `Error during Google AI chat completion: ${error.message}`,\n        level: 0,\n        auxiliary: {\n          errorDetails: {\n            value: `Message: ${error.message}${error.stack ? \"\\nStack: \" + error.stack : \"\"}`,\n            type: \"string\",\n          },\n          requestId: { value: requestId, type: \"string\" },\n        },\n      });\n\n      // Basic retry logic\n      if (retries > 0) {\n        logger({\n          category: \"google\",\n          message: `Retrying... (${retries} attempts left)`,\n          level: 1,\n        });\n        await new Promise((resolve) =>\n          setTimeout(resolve, 1000 * (4 - retries)),\n        ); // Simple backoff\n        return this.createChatCompletion({\n          options,\n          logger,\n          retries: retries - 1,\n        });\n      }\n\n      // Re-throw specific Stagehand errors or a generic one\n      if (error instanceof StagehandError) {\n        throw error;\n      }\n      throw new StagehandError(\n        `Google AI API request failed: ${error.message}`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/GroqClient.ts",
    "content": "import type { ClientOptions } from \"openai\";\nimport OpenAI from \"openai\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { AvailableModel } from \"../types/public/model.js\";\nimport {\n  ChatMessage,\n  CreateChatCompletionOptions,\n  LLMClient,\n  LLMResponse,\n} from \"./LLMClient.js\";\nimport { CreateChatCompletionResponseError } from \"../types/public/sdkErrors.js\";\nimport { toJsonSchema } from \"../zodCompat.js\";\n\nexport class GroqClient extends LLMClient {\n  public type = \"groq\" as const;\n  private client: OpenAI;\n  declare public clientOptions: ClientOptions;\n  public hasVision = false;\n\n  constructor({\n    modelName,\n    clientOptions,\n    userProvidedInstructions,\n  }: {\n    logger: (message: LogLine) => void;\n    modelName: AvailableModel;\n    clientOptions?: ClientOptions;\n    userProvidedInstructions?: string;\n  }) {\n    super(modelName, userProvidedInstructions);\n\n    // Create OpenAI client with the base URL set to Groq API\n    this.client = new OpenAI({\n      baseURL: \"https://api.groq.com/openai/v1\",\n      apiKey: clientOptions?.apiKey || process.env.GROQ_API_KEY,\n      ...clientOptions,\n    });\n\n    this.modelName = modelName;\n    this.clientOptions = clientOptions;\n  }\n\n  async createChatCompletion<T = LLMResponse>({\n    options,\n    retries,\n    logger,\n  }: CreateChatCompletionOptions): Promise<T> {\n    const optionsWithoutImage = { ...options };\n    delete optionsWithoutImage.image;\n\n    logger({\n      category: \"groq\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        options: {\n          value: JSON.stringify(optionsWithoutImage),\n          type: \"object\",\n        },\n      },\n    });\n\n    // Format messages for Groq API (using OpenAI format)\n    const formattedMessages = options.messages.map((msg: ChatMessage) => {\n      const baseMessage = {\n        content:\n          typeof msg.content === \"string\"\n            ? msg.content\n            : Array.isArray(msg.content) &&\n                msg.content.length > 0 &&\n                \"text\" in msg.content[0]\n              ? msg.content[0].text\n              : \"\",\n      };\n\n      // Groq supports system, user, and assistant roles\n      if (msg.role === \"system\") {\n        return { ...baseMessage, role: \"system\" as const };\n      } else if (msg.role === \"assistant\") {\n        return { ...baseMessage, role: \"assistant\" as const };\n      } else {\n        // Default to user for any other role\n        return { ...baseMessage, role: \"user\" as const };\n      }\n    });\n\n    // Format tools if provided\n    let tools = options.tools?.map((tool) => ({\n      type: \"function\" as const,\n      function: {\n        name: tool.name,\n        description: tool.description,\n        parameters: {\n          type: \"object\",\n          properties: tool.parameters.properties,\n          required: tool.parameters.required,\n        },\n      },\n    }));\n\n    // Add response model as a tool if provided\n    if (options.response_model) {\n      const jsonSchema = toJsonSchema(options.response_model.schema) as {\n        properties?: Record<string, unknown>;\n        required?: string[];\n      };\n      const schemaProperties = jsonSchema.properties || {};\n      const schemaRequired = jsonSchema.required || [];\n\n      const responseTool = {\n        type: \"function\" as const,\n        function: {\n          name: \"print_extracted_data\",\n          description:\n            \"Prints the extracted data based on the provided schema.\",\n          parameters: {\n            type: \"object\",\n            properties: schemaProperties,\n            required: schemaRequired,\n          },\n        },\n      };\n\n      tools = tools ? [...tools, responseTool] : [responseTool];\n    }\n\n    try {\n      // Use OpenAI client with Groq API\n      const apiResponse = await this.client.chat.completions.create({\n        model: this.modelName.split(\"groq-\")[1],\n        messages: [\n          ...formattedMessages,\n          // Add explicit instruction to return JSON if we have a response model\n          ...(options.response_model\n            ? [\n                {\n                  role: \"system\" as const,\n                  content: `IMPORTANT: Your response must be valid JSON that matches this schema: ${JSON.stringify(\n                    options.response_model.schema,\n                  )}`,\n                },\n              ]\n            : []),\n        ],\n        temperature: options.temperature || 0.7,\n        max_tokens: options.maxOutputTokens,\n        tools: tools,\n        tool_choice: options.tool_choice || \"auto\",\n      });\n\n      // Format the response to match the expected LLMResponse format\n      const response: LLMResponse = {\n        id: apiResponse.id,\n        object: \"chat.completion\",\n        created: Date.now(),\n        model: this.modelName.split(\"groq-\")[1],\n        choices: [\n          {\n            index: 0,\n            message: {\n              role: \"assistant\",\n              content: apiResponse.choices[0]?.message?.content || null,\n              tool_calls: apiResponse.choices[0]?.message?.tool_calls || [],\n            },\n            finish_reason: apiResponse.choices[0]?.finish_reason || \"stop\",\n          },\n        ],\n        usage: {\n          prompt_tokens: apiResponse.usage?.prompt_tokens || 0,\n          completion_tokens: apiResponse.usage?.completion_tokens || 0,\n          total_tokens: apiResponse.usage?.total_tokens || 0,\n        },\n      };\n\n      logger({\n        category: \"groq\",\n        message: \"response\",\n        level: 2,\n        auxiliary: {\n          response: {\n            value: JSON.stringify(response),\n            type: \"object\",\n          },\n          requestId: {\n            value: options.requestId,\n            type: \"string\",\n          },\n        },\n      });\n\n      // If there's no response model, return the entire response object\n      if (!options.response_model) {\n        return response as T;\n      }\n\n      // Otherwise, try parsing the JSON from the tool call or content\n      const toolCall = response.choices[0]?.message?.tool_calls?.[0];\n      if (toolCall?.function?.arguments) {\n        try {\n          const result = JSON.parse(toolCall.function.arguments);\n          const finalResponse = {\n            data: result,\n            usage: response.usage,\n          };\n          return finalResponse as T;\n        } catch (e) {\n          logger({\n            category: \"groq\",\n            message: \"failed to parse tool call arguments as JSON, retrying\",\n            level: 0,\n            auxiliary: {\n              error: {\n                value: e.message,\n                type: \"string\",\n              },\n            },\n          });\n        }\n      }\n\n      // If we have content but no tool calls, try to parse the content as JSON\n      const content = response.choices[0]?.message?.content;\n      if (content) {\n        try {\n          // Try to extract JSON from the content\n          const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n          if (jsonMatch) {\n            const result = JSON.parse(jsonMatch[0]);\n            const finalResponse = {\n              data: result,\n              usage: response.usage,\n            };\n            return finalResponse as T;\n          }\n        } catch (e) {\n          logger({\n            category: \"groq\",\n            message: \"failed to parse content as JSON\",\n            level: 0,\n            auxiliary: {\n              error: {\n                value: e.message,\n                type: \"string\",\n              },\n            },\n          });\n        }\n      }\n\n      // If we still haven't found valid JSON and have retries left, try again\n      if (!retries || retries < 5) {\n        return this.createChatCompletion({\n          options,\n          logger,\n          retries: (retries ?? 0) + 1,\n        });\n      }\n\n      throw new CreateChatCompletionResponseError(\"Invalid response schema\");\n    } catch (error) {\n      logger({\n        category: \"groq\",\n        message: \"error creating chat completion\",\n        level: 0,\n        auxiliary: {\n          error: {\n            value: error.message,\n            type: \"string\",\n          },\n          requestId: {\n            value: options.requestId,\n            type: \"string\",\n          },\n        },\n      });\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/LLMClient.ts",
    "content": "import { LLMTool } from \"../types/public/model.js\";\nimport {\n  embed,\n  embedMany,\n  experimental_generateImage,\n  experimental_generateSpeech,\n  experimental_transcribe,\n  generateObject,\n  generateText,\n  streamObject,\n  streamText,\n} from \"ai\";\nimport type { LanguageModelV2 } from \"@ai-sdk/provider\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { AvailableModel, ClientOptions } from \"../types/public/model.js\";\nimport type { StagehandZodSchema } from \"../zodCompat.js\";\n\nexport interface ChatMessage {\n  role: \"system\" | \"user\" | \"assistant\";\n  content: ChatMessageContent;\n}\n\nexport type ChatMessageContent =\n  | string\n  | (ChatMessageImageContent | ChatMessageTextContent)[];\n\nexport interface ChatMessageImageContent {\n  type: string;\n  image_url?: { url: string };\n  text?: string;\n  source?: {\n    type: string;\n    media_type: string;\n    data: string;\n  };\n}\n\nexport interface ChatMessageTextContent {\n  type: string;\n  text: string;\n}\n\nexport const AnnotatedScreenshotText =\n  \"This is a screenshot of the current page state with the elements annotated on it. Each element id is annotated with a number to the top left of it. Duplicate annotations at the same location are under each other vertically.\";\n\nexport interface ChatCompletionOptions {\n  messages: ChatMessage[];\n  temperature?: number;\n  top_p?: number;\n  frequency_penalty?: number;\n  presence_penalty?: number;\n  image?: {\n    buffer: Buffer;\n    description?: string;\n  };\n  response_model?: {\n    name: string;\n    schema: StagehandZodSchema;\n  };\n  tools?: LLMTool[];\n  tool_choice?: \"auto\" | \"none\" | \"required\";\n  maxOutputTokens?: number;\n  requestId?: string;\n}\n\nexport type LLMResponse = {\n  id: string;\n  object: string;\n  created: number;\n  model: string;\n  choices: {\n    index: number;\n    message: {\n      role: string;\n      content: string | null;\n      tool_calls: {\n        id: string;\n        type: string;\n        function: {\n          name: string;\n          arguments: string;\n        };\n      }[];\n    };\n    finish_reason: string;\n  }[];\n  usage: {\n    prompt_tokens: number;\n    completion_tokens: number;\n    total_tokens: number;\n  };\n};\n\nexport interface CreateChatCompletionOptions {\n  options: ChatCompletionOptions;\n  logger: (message: LogLine) => void;\n  retries?: number;\n}\n\n/** Simple usage shape if your LLM returns usage tokens. */\nexport interface LLMUsage {\n  prompt_tokens: number;\n  completion_tokens: number;\n  total_tokens: number;\n  reasoning_tokens?: number;\n  cached_input_tokens?: number;\n}\n\n/**\n * For calls that use a schema: the LLMClient may return { data: T; usage?: LLMUsage }\n */\nexport interface LLMParsedResponse<T> {\n  data: T;\n  usage?: LLMUsage;\n}\n\nexport abstract class LLMClient {\n  public type: \"openai\" | \"anthropic\" | \"cerebras\" | \"groq\" | (string & {});\n  public modelName: AvailableModel | (string & {});\n  public hasVision: boolean;\n  public clientOptions: ClientOptions;\n  public userProvidedInstructions?: string;\n\n  constructor(modelName: AvailableModel, userProvidedInstructions?: string) {\n    this.modelName = modelName;\n    this.userProvidedInstructions = userProvidedInstructions;\n  }\n\n  // Overload 1: When response_model is provided, returns LLMParsedResponse<T>\n  abstract createChatCompletion<T>(\n    options: CreateChatCompletionOptions & {\n      options: {\n        response_model: { name: string; schema: StagehandZodSchema };\n      };\n    },\n  ): Promise<LLMParsedResponse<T>>;\n\n  // Overload 2: When response_model is not provided, returns T (defaults to LLMResponse)\n  abstract createChatCompletion<T = LLMResponse>(\n    options: CreateChatCompletionOptions,\n  ): Promise<T>;\n\n  public generateObject = generateObject;\n  public generateText = generateText;\n  public streamText = streamText;\n  public streamObject = streamObject;\n  public generateImage = experimental_generateImage;\n  public embed = embed;\n  public embedMany = embedMany;\n  public transcribe = experimental_transcribe;\n  public generateSpeech = experimental_generateSpeech;\n\n  getLanguageModel?(): LanguageModelV2;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/LLMProvider.ts",
    "content": "import {\n  ExperimentalNotConfiguredError,\n  UnsupportedAISDKModelProviderError,\n  UnsupportedModelError,\n  UnsupportedModelProviderError,\n} from \"../types/public/sdkErrors.js\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport {\n  AvailableModel,\n  ClientOptions,\n  ModelProvider,\n} from \"../types/public/model.js\";\nimport { AISdkClient } from \"./aisdk.js\";\nimport { AnthropicClient } from \"./AnthropicClient.js\";\nimport { CerebrasClient } from \"./CerebrasClient.js\";\nimport { GoogleClient } from \"./GoogleClient.js\";\nimport { GroqClient } from \"./GroqClient.js\";\nimport { LLMClient } from \"./LLMClient.js\";\nimport { OpenAIClient } from \"./OpenAIClient.js\";\nimport { openai, createOpenAI } from \"@ai-sdk/openai\";\nimport { bedrock, createAmazonBedrock } from \"@ai-sdk/amazon-bedrock\";\nimport { vertex, createVertex } from \"@ai-sdk/google-vertex\";\nimport { anthropic, createAnthropic } from \"@ai-sdk/anthropic\";\nimport { google, createGoogleGenerativeAI } from \"@ai-sdk/google\";\nimport { xai, createXai } from \"@ai-sdk/xai\";\nimport { azure, createAzure } from \"@ai-sdk/azure\";\nimport { groq, createGroq } from \"@ai-sdk/groq\";\nimport { cerebras, createCerebras } from \"@ai-sdk/cerebras\";\nimport { togetherai, createTogetherAI } from \"@ai-sdk/togetherai\";\nimport { mistral, createMistral } from \"@ai-sdk/mistral\";\nimport { deepseek, createDeepSeek } from \"@ai-sdk/deepseek\";\nimport { perplexity, createPerplexity } from \"@ai-sdk/perplexity\";\nimport { ollama, createOllama } from \"ollama-ai-provider-v2\";\nimport { gateway, createGateway } from \"ai\";\nimport { AISDKProvider, AISDKCustomProvider } from \"../types/public/model.js\";\n\nconst AISDKProviders: Record<string, AISDKProvider> = {\n  openai,\n  bedrock,\n  anthropic,\n  google,\n  xai,\n  azure,\n  groq,\n  cerebras,\n  togetherai,\n  mistral,\n  deepseek,\n  perplexity,\n  ollama,\n  vertex,\n  gateway,\n};\nconst AISDKProvidersWithAPIKey: Record<string, AISDKCustomProvider> = {\n  openai: createOpenAI,\n  bedrock: createAmazonBedrock,\n  anthropic: createAnthropic,\n  google: createGoogleGenerativeAI,\n  vertex: createVertex,\n  xai: createXai,\n  azure: createAzure,\n  groq: createGroq,\n  cerebras: createCerebras,\n  togetherai: createTogetherAI,\n  mistral: createMistral,\n  deepseek: createDeepSeek,\n  perplexity: createPerplexity,\n  ollama: createOllama,\n  gateway: createGateway,\n};\n\nconst modelToProviderMap: { [key in AvailableModel]: ModelProvider } = {\n  \"gpt-4.1\": \"openai\",\n  \"gpt-4.1-mini\": \"openai\",\n  \"gpt-4.1-nano\": \"openai\",\n  \"o4-mini\": \"openai\",\n  //prettier-ignore\n  \"o3\": \"openai\",\n  \"o3-mini\": \"openai\",\n  //prettier-ignore\n  \"o1\": \"openai\",\n  \"o1-mini\": \"openai\",\n  \"gpt-4o\": \"openai\",\n  \"gpt-4o-mini\": \"openai\",\n  \"gpt-4o-2024-08-06\": \"openai\",\n  \"gpt-4.5-preview\": \"openai\",\n  \"o1-preview\": \"openai\",\n  \"cerebras-llama-3.3-70b\": \"cerebras\",\n  \"cerebras-llama-3.1-8b\": \"cerebras\",\n  \"groq-llama-3.3-70b-versatile\": \"groq\",\n  \"groq-llama-3.3-70b-specdec\": \"groq\",\n  \"moonshotai/kimi-k2-instruct\": \"groq\",\n  \"gemini-1.5-flash\": \"google\",\n  \"gemini-1.5-pro\": \"google\",\n  \"gemini-1.5-flash-8b\": \"google\",\n  \"gemini-2.0-flash-lite\": \"google\",\n  \"gemini-2.0-flash\": \"google\",\n  \"gemini-2.5-flash-preview-04-17\": \"google\",\n  \"gemini-2.5-pro-preview-03-25\": \"google\",\n};\n\nexport function getAISDKLanguageModel(\n  subProvider: string,\n  subModelName: string,\n  clientOptions?: ClientOptions,\n) {\n  const hasValidOptions =\n    clientOptions &&\n    Object.values(clientOptions).some((v) => v !== undefined && v !== null);\n\n  if (hasValidOptions) {\n    const creator = AISDKProvidersWithAPIKey[subProvider];\n    if (!creator) {\n      throw new UnsupportedAISDKModelProviderError(\n        subProvider,\n        Object.keys(AISDKProvidersWithAPIKey),\n      );\n    }\n    const provider = creator(clientOptions);\n    // Get the specific model from the provider\n    return provider(subModelName);\n  } else {\n    const provider = AISDKProviders[subProvider];\n    if (!provider) {\n      throw new UnsupportedAISDKModelProviderError(\n        subProvider,\n        Object.keys(AISDKProviders),\n      );\n    }\n    return provider(subModelName);\n  }\n}\n\nexport class LLMProvider {\n  private logger: (message: LogLine) => void;\n\n  constructor(logger: (message: LogLine) => void) {\n    this.logger = logger;\n  }\n\n  getClient(\n    modelName: AvailableModel,\n    clientOptions?: ClientOptions,\n    options?: { experimental?: boolean; disableAPI?: boolean },\n  ): LLMClient {\n    if (modelName.includes(\"/\")) {\n      const firstSlashIndex = modelName.indexOf(\"/\");\n      const subProvider = modelName.substring(0, firstSlashIndex);\n      const subModelName = modelName.substring(firstSlashIndex + 1);\n      if (\n        subProvider === \"vertex\" &&\n        !options?.disableAPI &&\n        !options?.experimental\n      ) {\n        throw new ExperimentalNotConfiguredError(\"Vertex provider\");\n      }\n\n      const languageModel = getAISDKLanguageModel(\n        subProvider,\n        subModelName,\n        clientOptions,\n      );\n\n      return new AISdkClient({\n        model: languageModel,\n        logger: this.logger,\n      });\n    }\n\n    // Model name doesn't include \"/\" - this format is deprecated\n    const provider = modelToProviderMap[modelName];\n    if (!provider) {\n      throw new UnsupportedModelError(Object.keys(modelToProviderMap));\n    }\n\n    this.logger({\n      category: \"llm\",\n      message: `Deprecation warning: Model format \"${modelName}\" is deprecated. Please use the provider/model format (e.g., \"openai/gpt-5\" or \"anthropic/claude-sonnet-4\").`,\n      level: 0,\n    });\n\n    const availableModel = modelName as AvailableModel;\n    switch (provider) {\n      case \"openai\":\n        return new OpenAIClient({\n          logger: this.logger,\n          modelName: availableModel,\n          clientOptions,\n        });\n      case \"anthropic\":\n        return new AnthropicClient({\n          logger: this.logger,\n          modelName: availableModel,\n          clientOptions,\n        });\n      case \"cerebras\":\n        return new CerebrasClient({\n          logger: this.logger,\n          modelName: availableModel,\n          clientOptions,\n        });\n      case \"groq\":\n        return new GroqClient({\n          logger: this.logger,\n          modelName: availableModel,\n          clientOptions,\n        });\n      case \"google\":\n        return new GoogleClient({\n          logger: this.logger,\n          modelName: availableModel,\n          clientOptions,\n        });\n      default:\n        // This default case handles unknown providers that exist in modelToProviderMap\n        // but aren't implemented in the switch. This is an internal consistency issue.\n        throw new UnsupportedModelProviderError([\n          ...new Set(Object.values(modelToProviderMap)),\n        ]);\n    }\n  }\n\n  static getModelProvider(modelName: AvailableModel): ModelProvider {\n    if (modelName.includes(\"/\")) {\n      const firstSlashIndex = modelName.indexOf(\"/\");\n      const subProvider = modelName.substring(0, firstSlashIndex);\n      if (AISDKProviders[subProvider]) {\n        return \"aisdk\";\n      }\n    }\n    const provider = modelToProviderMap[modelName];\n    return provider;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/OpenAIClient.ts",
    "content": "import OpenAI, { ClientOptions } from \"openai\";\nimport {\n  ChatCompletionAssistantMessageParam,\n  ChatCompletionContentPartImage,\n  ChatCompletionContentPartText,\n  ChatCompletionCreateParamsNonStreaming,\n  ChatCompletionMessageParam,\n  ChatCompletionSystemMessageParam,\n  ChatCompletionUserMessageParam,\n} from \"openai/resources/chat\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { AvailableModel } from \"../types/public/model.js\";\nimport { validateZodSchema } from \"../../utils.js\";\nimport {\n  ChatCompletionOptions,\n  ChatMessage,\n  CreateChatCompletionOptions,\n  LLMClient,\n  LLMResponse,\n} from \"./LLMClient.js\";\nimport {\n  CreateChatCompletionResponseError,\n  StagehandError,\n  ZodSchemaValidationError,\n} from \"../types/public/sdkErrors.js\";\nimport { toJsonSchema } from \"../zodCompat.js\";\n\nexport class OpenAIClient extends LLMClient {\n  public type = \"openai\" as const;\n  private client: OpenAI;\n  declare public clientOptions: ClientOptions;\n\n  constructor({\n    modelName,\n    clientOptions,\n  }: {\n    logger: (message: LogLine) => void;\n    modelName: AvailableModel;\n    clientOptions?: ClientOptions;\n  }) {\n    super(modelName);\n    this.clientOptions = clientOptions;\n    this.client = new OpenAI(clientOptions);\n    this.modelName = modelName;\n  }\n\n  async createChatCompletion<T = LLMResponse>({\n    options: optionsInitial,\n    logger,\n    retries = 3,\n  }: CreateChatCompletionOptions): Promise<T> {\n    let options: Partial<ChatCompletionOptions> = optionsInitial;\n\n    // O1 models do not support most of the options. So we override them.\n    // For schema and tools, we add them as user messages.\n    let isToolsOverridedForO1 = false;\n    if (this.modelName.startsWith(\"o1\") || this.modelName.startsWith(\"o3\")) {\n      /* eslint-disable */\n      // Remove unsupported options\n      let {\n        tool_choice,\n        top_p,\n        frequency_penalty,\n        presence_penalty,\n        temperature,\n      } = options;\n      ({\n        tool_choice,\n        top_p,\n        frequency_penalty,\n        presence_penalty,\n        temperature,\n        ...options\n      } = options);\n      /* eslint-enable */\n      // Remove unsupported options\n      options.messages = options.messages.map((message) => ({\n        ...message,\n        role: \"user\",\n      }));\n      if (options.tools && options.response_model) {\n        throw new StagehandError(\n          \"Cannot use both tool and response_model for o1 models\",\n        );\n      }\n\n      if (options.tools) {\n        // Remove unsupported options\n        const { tools, ...rest } = options;\n        options = rest;\n        isToolsOverridedForO1 = true;\n        options.messages.push({\n          role: \"user\",\n          content: `You have the following tools available to you:\\n${JSON.stringify(\n            tools,\n          )}\n\n          Respond with the following zod schema format to use a method: {\n            \"name\": \"<tool_name>\",\n            \"arguments\": <tool_args>\n          }\n          \n          Do not include any other text or formattings like \\`\\`\\` in your response. Just the JSON object.`,\n        });\n      }\n    }\n    if (\n      options.temperature &&\n      (this.modelName.startsWith(\"o1\") || this.modelName.startsWith(\"o3\"))\n    ) {\n      throw new StagehandError(\"Temperature is not supported for o1 models\");\n    }\n\n    const { requestId, ...optionsWithoutImageAndRequestId } = options;\n\n    logger({\n      category: \"openai\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        options: {\n          value: JSON.stringify({\n            ...optionsWithoutImageAndRequestId,\n            requestId,\n          }),\n          type: \"object\",\n        },\n        modelName: {\n          value: this.modelName,\n          type: \"string\",\n        },\n      },\n    });\n\n    if (options.image) {\n      const screenshotMessage: ChatMessage = {\n        role: \"user\",\n        content: [\n          {\n            type: \"image_url\",\n            image_url: {\n              url: `data:image/jpeg;base64,${options.image.buffer.toString(\"base64\")}`,\n            },\n          },\n          ...(options.image.description\n            ? [{ type: \"text\", text: options.image.description }]\n            : []),\n        ],\n      };\n\n      options.messages.push(screenshotMessage);\n    }\n\n    let responseFormat:\n      | ChatCompletionCreateParamsNonStreaming[\"response_format\"]\n      | undefined;\n    if (options.response_model) {\n      // For O1 models, we need to add the schema as a user message.\n      if (this.modelName.startsWith(\"o1\") || this.modelName.startsWith(\"o3\")) {\n        try {\n          const parsedSchema = JSON.stringify(\n            toJsonSchema(options.response_model.schema),\n          );\n          options.messages.push({\n            role: \"user\",\n            content: `Respond in this zod schema format:\\n${parsedSchema}\\n\n\n          Do not include any other text, formatting or markdown in your output. Do not include \\`\\`\\` or \\`\\`\\`json in your response. Only the JSON object itself.`,\n          });\n        } catch (error) {\n          logger({\n            category: \"openai\",\n            message: \"Failed to parse response model schema\",\n            level: 0,\n          });\n\n          if (retries > 0) {\n            // as-casting to account for o1 models not supporting all options\n            return this.createChatCompletion({\n              options: options as ChatCompletionOptions,\n              logger,\n              retries: retries - 1,\n            });\n          }\n\n          throw error;\n        }\n      } else {\n        responseFormat = {\n          type: \"json_schema\",\n          json_schema: {\n            name: options.response_model.name,\n            schema: toJsonSchema(options.response_model.schema),\n          },\n        };\n      }\n    }\n\n    /* eslint-disable */\n    // Remove unsupported options\n    const { response_model, ...openAiOptions } = {\n      ...optionsWithoutImageAndRequestId,\n      model: this.modelName,\n    };\n    /* eslint-enable */\n\n    logger({\n      category: \"openai\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        openAiOptions: {\n          value: JSON.stringify(openAiOptions),\n          type: \"object\",\n        },\n      },\n    });\n\n    const formattedMessages: ChatCompletionMessageParam[] =\n      options.messages.map((message) => {\n        if (Array.isArray(message.content)) {\n          const contentParts = message.content.map((content) => {\n            if (\"image_url\" in content) {\n              const imageContent: ChatCompletionContentPartImage = {\n                image_url: {\n                  url: content.image_url.url,\n                },\n                type: \"image_url\",\n              };\n              return imageContent;\n            } else {\n              const textContent: ChatCompletionContentPartText = {\n                text: content.text,\n                type: \"text\",\n              };\n              return textContent;\n            }\n          });\n\n          if (message.role === \"system\") {\n            const formattedMessage: ChatCompletionSystemMessageParam = {\n              ...message,\n              role: \"system\",\n              content: contentParts.filter(\n                (content): content is ChatCompletionContentPartText =>\n                  content.type === \"text\",\n              ),\n            };\n            return formattedMessage;\n          } else if (message.role === \"user\") {\n            const formattedMessage: ChatCompletionUserMessageParam = {\n              ...message,\n              role: \"user\",\n              content: contentParts,\n            };\n            return formattedMessage;\n          } else {\n            const formattedMessage: ChatCompletionAssistantMessageParam = {\n              ...message,\n              role: \"assistant\",\n              content: contentParts.filter(\n                (content): content is ChatCompletionContentPartText =>\n                  content.type === \"text\",\n              ),\n            };\n            return formattedMessage;\n          }\n        }\n\n        const formattedMessage: ChatCompletionUserMessageParam = {\n          role: \"user\",\n          content: message.content,\n        };\n\n        return formattedMessage;\n      });\n\n    const body: ChatCompletionCreateParamsNonStreaming = {\n      ...openAiOptions,\n      model: this.modelName,\n      messages: formattedMessages,\n      response_format: responseFormat,\n      stream: false,\n      tools: options.tools?.map((tool) => ({\n        function: {\n          name: tool.name,\n          description: tool.description,\n          parameters: tool.parameters,\n        },\n        type: \"function\",\n      })),\n    };\n\n    const response = await this.client.chat.completions.create(body);\n\n    // For O1 models, we need to parse the tool call response manually and add it to the response.\n    if (isToolsOverridedForO1) {\n      try {\n        const parsedContent = JSON.parse(response.choices[0].message.content);\n\n        response.choices[0].message.tool_calls = [\n          {\n            function: {\n              name: parsedContent[\"name\"],\n              arguments: JSON.stringify(parsedContent[\"arguments\"]),\n            },\n            type: \"function\",\n            id: \"-1\",\n          },\n        ];\n        response.choices[0].message.content = null;\n      } catch (error) {\n        logger({\n          category: \"openai\",\n          message: \"Failed to parse tool call response\",\n          level: 0,\n          auxiliary: {\n            error: {\n              value: error.message,\n              type: \"string\",\n            },\n            content: {\n              value: response.choices[0].message.content,\n              type: \"string\",\n            },\n          },\n        });\n\n        if (retries > 0) {\n          // as-casting to account for o1 models not supporting all options\n          return this.createChatCompletion({\n            options: options as ChatCompletionOptions,\n            logger,\n            retries: retries - 1,\n          });\n        }\n\n        throw error;\n      }\n    }\n\n    logger({\n      category: \"openai\",\n      message: \"response\",\n      level: 2,\n      auxiliary: {\n        response: {\n          value: JSON.stringify(response),\n          type: \"object\",\n        },\n        requestId: {\n          value: requestId,\n          type: \"string\",\n        },\n      },\n    });\n\n    if (options.response_model) {\n      const extractedData = response.choices[0].message.content;\n      const parsedData = JSON.parse(extractedData);\n\n      try {\n        validateZodSchema(options.response_model.schema, parsedData);\n      } catch (e) {\n        logger({\n          category: \"openai\",\n          message: \"Response failed Zod schema validation\",\n          level: 0,\n        });\n        if (retries > 0) {\n          // as-casting to account for o1 models not supporting all options\n          return this.createChatCompletion({\n            options: options as ChatCompletionOptions,\n            logger,\n            retries: retries - 1,\n          });\n        }\n\n        if (e instanceof ZodSchemaValidationError) {\n          logger({\n            category: \"openai\",\n            message: `Error during OpenAI chat completion: ${e.message}`,\n            level: 0,\n            auxiliary: {\n              errorDetails: {\n                value: `Message: ${e.message}${e.stack ? \"\\nStack: \" + e.stack : \"\"}`,\n                type: \"string\",\n              },\n              requestId: { value: requestId, type: \"string\" },\n            },\n          });\n          throw new CreateChatCompletionResponseError(e.message);\n        }\n        throw e;\n      }\n\n      return {\n        data: parsedData,\n        usage: response.usage,\n      } as T;\n    }\n\n    // if the function was called with a response model, it would have returned earlier\n    // so we can safely cast here to T, which defaults to ChatCompletion\n    return response as T;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/llm/aisdk.ts",
    "content": "import {\n  CoreAssistantMessage,\n  ModelMessage,\n  CoreSystemMessage,\n  CoreUserMessage,\n  generateObject,\n  generateText,\n  ImagePart,\n  NoObjectGeneratedError,\n  TextPart,\n  ToolSet,\n  Tool,\n} from \"ai\";\nimport type { LanguageModelV2 } from \"@ai-sdk/provider\";\nimport { ChatCompletion } from \"openai/resources\";\nimport { v7 as uuidv7 } from \"uuid\";\nimport { LogLine } from \"../types/public/logs.js\";\nimport { AvailableModel } from \"../types/public/model.js\";\nimport { CreateChatCompletionOptions, LLMClient } from \"./LLMClient.js\";\nimport {\n  FlowLogger,\n  extractLlmPromptSummary,\n} from \"../flowlogger/FlowLogger.js\";\nimport { toJsonSchema } from \"../zodCompat.js\";\n\nexport class AISdkClient extends LLMClient {\n  public type = \"aisdk\" as const;\n  private model: LanguageModelV2;\n  private logger?: (message: LogLine) => void;\n\n  constructor({\n    model,\n    logger,\n  }: {\n    model: LanguageModelV2;\n    logger?: (message: LogLine) => void;\n  }) {\n    super(model.modelId as AvailableModel);\n    this.model = model;\n    this.logger = logger;\n  }\n\n  public getLanguageModel(): LanguageModelV2 {\n    return this.model;\n  }\n\n  async createChatCompletion<T = ChatCompletion>({\n    options,\n  }: CreateChatCompletionOptions): Promise<T> {\n    this.logger?.({\n      category: \"aisdk\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        options: {\n          value: JSON.stringify({\n            ...options,\n            image: undefined,\n            messages: options.messages.map((msg) => ({\n              ...msg,\n              content: Array.isArray(msg.content)\n                ? msg.content.map((c) =>\n                    \"image_url\" in c\n                      ? { ...c, image_url: { url: \"[IMAGE_REDACTED]\" } }\n                      : c,\n                  )\n                : msg.content,\n            })),\n          }),\n          type: \"object\",\n        },\n        modelName: {\n          value: this.model.modelId,\n          type: \"string\",\n        },\n      },\n    });\n\n    const formattedMessages: ModelMessage[] = options.messages.map(\n      (message) => {\n        if (Array.isArray(message.content)) {\n          if (message.role === \"system\") {\n            const systemMessage: CoreSystemMessage = {\n              role: \"system\",\n              content: message.content\n                .map((c) => (\"text\" in c ? c.text : \"\"))\n                .join(\"\\n\"),\n            };\n            return systemMessage;\n          }\n\n          const contentParts = message.content.map((content) => {\n            if (\"image_url\" in content) {\n              const imageContent: ImagePart = {\n                type: \"image\",\n                image: content.image_url.url,\n              };\n              return imageContent;\n            } else {\n              const textContent: TextPart = {\n                type: \"text\",\n                text: content.text,\n              };\n              return textContent;\n            }\n          });\n\n          if (message.role === \"user\") {\n            const userMessage: CoreUserMessage = {\n              role: \"user\",\n              content: contentParts,\n            };\n            return userMessage;\n          } else {\n            const textOnlyParts = contentParts.map((part) => ({\n              type: \"text\" as const,\n              text: part.type === \"image\" ? \"[Image]\" : part.text,\n            }));\n            const assistantMessage: CoreAssistantMessage = {\n              role: \"assistant\",\n              content: textOnlyParts,\n            };\n            return assistantMessage;\n          }\n        }\n\n        return {\n          role: message.role,\n          content: message.content,\n        };\n      },\n    );\n\n    let objectResponse: Awaited<ReturnType<typeof generateObject>>;\n    const isGPT5 = this.model.modelId.includes(\"gpt-5\");\n    const isCodex = this.model.modelId.includes(\"codex\");\n    const usesLowReasoningEffort =\n      (this.model.modelId.includes(\"gpt-5.1\") ||\n        this.model.modelId.includes(\"gpt-5.2\")) &&\n      !isCodex;\n    // Kimi models only support temperature=1\n    const isKimi = this.model.modelId.includes(\"kimi\");\n    const temperature = isKimi ? 1 : options.temperature;\n\n    // Models that lack native structured-output support need a prompt-based\n    // JSON fallback instead of response_format: { type: \"json_schema\" }.\n    const PROMPT_JSON_FALLBACK_PATTERNS = [\"deepseek\", \"kimi\", \"glm\"];\n    const needsPromptJsonFallback = PROMPT_JSON_FALLBACK_PATTERNS.some((p) =>\n      this.model.modelId.includes(p),\n    );\n\n    if (options.response_model) {\n      // Log LLM request for generateObject (extract)\n      const llmRequestId = uuidv7();\n      const promptSummary = extractLlmPromptSummary(options.messages, {\n        hasSchema: true,\n      });\n      FlowLogger.logLlmRequest({\n        requestId: llmRequestId,\n        model: this.model.modelId,\n        prompt: promptSummary,\n      });\n\n      // For models that don't support native structured outputs, add a prompt instruction\n      if (needsPromptJsonFallback) {\n        const parsedSchema = JSON.stringify(\n          toJsonSchema(options.response_model.schema),\n        );\n\n        formattedMessages.push({\n          role: \"user\",\n          content: `Respond in this zod schema format:\\n${parsedSchema}\\n\nYou must respond in JSON format. respond WITH JSON. Do not include any other text, formatting or markdown in your output. Do not include \\`\\`\\` or \\`\\`\\`json in your response. Only the JSON object itself.`,\n        });\n      }\n\n      try {\n        objectResponse = await generateObject({\n          model: this.model,\n          messages: formattedMessages,\n          schema: options.response_model.schema,\n          temperature,\n          providerOptions: isGPT5\n            ? {\n                openai: {\n                  textVerbosity: isCodex ? \"medium\" : \"low\", // codex models only support 'medium'\n                  reasoningEffort: isCodex\n                    ? \"medium\"\n                    : usesLowReasoningEffort\n                      ? \"low\"\n                      : \"minimal\",\n                },\n              }\n            : undefined,\n        });\n      } catch (err) {\n        // Log error response to maintain request/response pairing\n        FlowLogger.logLlmResponse({\n          requestId: llmRequestId,\n          model: this.model.modelId,\n          output: `[error: ${err instanceof Error ? err.message : \"unknown\"}]`,\n        });\n\n        if (NoObjectGeneratedError.isInstance(err)) {\n          this.logger?.({\n            category: \"AISDK error\",\n            message: err.message,\n            level: 0,\n            auxiliary: {\n              cause: {\n                value: JSON.stringify(err.cause ?? {}),\n                type: \"object\",\n              },\n              text: {\n                value: err.text ?? \"\",\n                type: \"string\",\n              },\n              response: {\n                value: JSON.stringify(err.response ?? {}),\n                type: \"object\",\n              },\n              usage: {\n                value: JSON.stringify(err.usage ?? {}),\n                type: \"object\",\n              },\n              finishReason: {\n                value: err.finishReason ?? \"unknown\",\n                type: \"string\",\n              },\n              requestId: {\n                value: options.requestId,\n                type: \"string\",\n              },\n            },\n          });\n\n          throw err;\n        }\n        throw err;\n      }\n\n      const result = {\n        data: objectResponse.object,\n        usage: {\n          prompt_tokens: objectResponse.usage.inputTokens ?? 0,\n          completion_tokens: objectResponse.usage.outputTokens ?? 0,\n          reasoning_tokens: objectResponse.usage.reasoningTokens ?? 0,\n          cached_input_tokens: objectResponse.usage.cachedInputTokens ?? 0,\n          total_tokens: objectResponse.usage.totalTokens ?? 0,\n        },\n      } as T;\n\n      // Log LLM response for generateObject\n      FlowLogger.logLlmResponse({\n        requestId: llmRequestId,\n        model: this.model.modelId,\n        output: JSON.stringify(objectResponse.object),\n        inputTokens: objectResponse.usage.inputTokens,\n        outputTokens: objectResponse.usage.outputTokens,\n      });\n\n      this.logger?.({\n        category: \"aisdk\",\n        message: \"response\",\n        level: 1,\n        auxiliary: {\n          response: {\n            value: JSON.stringify({\n              object: objectResponse.object,\n              usage: objectResponse.usage,\n              finishReason: objectResponse.finishReason,\n              // Omit request and response properties that might contain images\n            }),\n            type: \"object\",\n          },\n          requestId: {\n            value: options.requestId,\n            type: \"string\",\n          },\n        },\n      });\n\n      return result;\n    }\n\n    const tools: ToolSet = {};\n    if (options.tools && options.tools.length > 0) {\n      for (const tool of options.tools) {\n        tools[tool.name] = {\n          description: tool.description,\n          inputSchema: tool.parameters,\n        } as Tool;\n      }\n    }\n\n    // Log LLM request for generateText (act/observe)\n    const llmRequestId = uuidv7();\n    const toolCount = Object.keys(tools).length;\n    const promptSummary = extractLlmPromptSummary(options.messages, {\n      toolCount,\n    });\n    FlowLogger.logLlmRequest({\n      requestId: llmRequestId,\n      model: this.model.modelId,\n      prompt: promptSummary,\n    });\n\n    let textResponse: Awaited<ReturnType<typeof generateText>>;\n    try {\n      textResponse = await generateText({\n        model: this.model,\n        messages: formattedMessages,\n        tools: Object.keys(tools).length > 0 ? tools : undefined,\n        toolChoice:\n          Object.keys(tools).length > 0\n            ? options.tool_choice === \"required\"\n              ? \"required\"\n              : options.tool_choice === \"none\"\n                ? \"none\"\n                : \"auto\"\n            : undefined,\n        temperature,\n      });\n    } catch (err) {\n      // Log error response to maintain request/response pairing\n      FlowLogger.logLlmResponse({\n        requestId: llmRequestId,\n        model: this.model.modelId,\n        output: `[error: ${err instanceof Error ? err.message : \"unknown\"}]`,\n      });\n      throw err;\n    }\n\n    // Transform AI SDK response to match LLMResponse format expected by operator handler\n    const transformedToolCalls = (textResponse.toolCalls || []).map(\n      (toolCall) => ({\n        id:\n          toolCall.toolCallId ||\n          `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n        type: \"function\",\n        function: {\n          name: toolCall.toolName,\n          arguments: JSON.stringify(toolCall.input),\n        },\n      }),\n    );\n\n    const result = {\n      id: `chatcmpl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n      object: \"chat.completion\",\n      created: Math.floor(Date.now() / 1000),\n      model: this.model.modelId,\n      choices: [\n        {\n          index: 0,\n          message: {\n            role: \"assistant\",\n            content: textResponse.text || null,\n            tool_calls: transformedToolCalls,\n          },\n          finish_reason: textResponse.finishReason || \"stop\",\n        },\n      ],\n      usage: {\n        prompt_tokens: textResponse.usage.inputTokens ?? 0,\n        completion_tokens: textResponse.usage.outputTokens ?? 0,\n        reasoning_tokens: textResponse.usage.reasoningTokens ?? 0,\n        cached_input_tokens: textResponse.usage.cachedInputTokens ?? 0,\n        total_tokens: textResponse.usage.totalTokens ?? 0,\n      },\n    } as T;\n\n    // Log LLM response for generateText\n    FlowLogger.logLlmResponse({\n      requestId: llmRequestId,\n      model: this.model.modelId,\n      output:\n        textResponse.text ||\n        (transformedToolCalls.length > 0\n          ? `[${transformedToolCalls.length} tool calls]`\n          : \"\"),\n      inputTokens: textResponse.usage.inputTokens,\n      outputTokens: textResponse.usage.outputTokens,\n    });\n\n    this.logger?.({\n      category: \"aisdk\",\n      message: \"response\",\n      level: 2,\n      auxiliary: {\n        response: {\n          value: JSON.stringify({\n            text: textResponse.text,\n            usage: textResponse.usage,\n            finishReason: textResponse.finishReason,\n            // Omit request and response properties that might contain images\n          }),\n          type: \"object\",\n        },\n        requestId: {\n          value: options.requestId,\n          type: \"string\",\n        },\n      },\n    });\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/logger.ts",
    "content": "import type { LogLine } from \"./types/public/logs.js\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Stagehand V3 Logging\n *\n * Design goals:\n * - Support concurrent V3 instances with independent logger configuration\n * - Each V3 instance has its own StagehandLogger (handles usePino, verbose, externalLogger)\n * - Provide AsyncLocalStorage-based routing for backward compatibility with handler code\n * - Prevent cross-talk between concurrent instances\n *\n * How it works:\n * - Each V3 instance creates a StagehandLogger in its constructor (per-instance config)\n * - bindInstanceLogger()/unbindInstanceLogger(): registers logger callback per instance ID\n * - withInstanceLogContext(): establishes AsyncLocalStorage context for an async operation\n * - v3Logger(): routes logs using AsyncLocalStorage with console fallback\n *\n * ⚠️ CONTEXT LOSS SCENARIOS:\n * 1. setTimeout/setInterval callbacks lose context (runs outside AsyncLocalStorage scope)\n * 2. Event emitters (EventEmitter.on) lose context (callback invoked outside scope)\n * 3. Fire-and-forget promises (void promise) lose context if they don't complete synchronously\n * 4. Third-party library callbacks may lose context depending on implementation\n *\n * WORKAROUND for context loss:\n * - Use explicit logger parameter instead of v3Logger()\n * - Wrap callback in withInstanceLogContext() manually\n * - Or let logs fall back to console (acceptable for edge cases)\n */\n\n// Per-instance routing using AsyncLocalStorage\nconst logContext = new AsyncLocalStorage<string>();\nconst instanceLoggers = new Map<string, (line: LogLine) => void>();\n\nexport function bindInstanceLogger(\n  instanceId: string,\n  logger: (line: LogLine) => void,\n): void {\n  instanceLoggers.set(instanceId, logger);\n}\n\nexport function unbindInstanceLogger(instanceId: string): void {\n  instanceLoggers.delete(instanceId);\n}\n\nexport function withInstanceLogContext<T>(instanceId: string, fn: () => T): T {\n  return logContext.run(instanceId, fn);\n}\n\n/**\n * Routes logs to the appropriate instance logger based on AsyncLocalStorage context.\n * Falls back to console output if no instance context is available.\n */\nexport function v3Logger(line: LogLine): void {\n  const id = logContext.getStore();\n  if (id) {\n    const fn = instanceLoggers.get(id);\n    if (fn) {\n      const enriched: LogLine = {\n        ...line,\n        auxiliary: {\n          ...(line.auxiliary || {}),\n        },\n      };\n      try {\n        fn(enriched);\n        return;\n      } catch {\n        // fallback to console below\n      }\n    }\n  }\n\n  // Fallback: log to console when no instance context\n  const ts = line.timestamp ?? new Date().toISOString();\n  const lvl = line.level ?? 1;\n  const levelStr = lvl === 0 ? \"ERROR\" : lvl === 2 ? \"DEBUG\" : \"INFO\";\n  let output = `[${ts}] ${levelStr}: ${line.message}`;\n\n  if (line.auxiliary) {\n    for (const [key, { value, type }] of Object.entries(line.auxiliary)) {\n      let formattedValue = value;\n      if (type === \"object\") {\n        try {\n          formattedValue = JSON.stringify(JSON.parse(value), null, 2)\n            .split(\"\\n\")\n            .map((line, i) => (i === 0 ? line : `    ${line}`))\n            .join(\"\\n\");\n        } catch {\n          formattedValue = value;\n        }\n      }\n      output += `\\n    ${key}: ${formattedValue}`;\n    }\n  }\n\n  if (lvl === 0) {\n    console.error(output);\n  } else if (lvl === 2) {\n    (console.debug ?? console.log)(output);\n  } else {\n    console.log(output);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/mcp/connection.ts",
    "content": "import {\n  Client,\n  ClientOptions,\n} from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n  StreamableHTTPClientTransport,\n  type StreamableHTTPClientTransportOptions,\n} from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { MCPConnectionError } from \"../types/public/sdkErrors.js\";\n\nexport interface ConnectToMCPServerOptions {\n  serverUrl: string | URL;\n  clientOptions?: ClientOptions;\n  requestOptions?: StreamableHTTPClientTransportOptions;\n}\n\nexport interface StdioServerConfig {\n  command: string;\n  args?: string[];\n  env?: Record<string, string>;\n}\n\nexport const connectToMCPServer = async (\n  serverConfig: string | URL | StdioServerConfig | ConnectToMCPServerOptions,\n): Promise<Client> => {\n  try {\n    let transport;\n    let clientOptions: ClientOptions | undefined;\n    let requestOptions: StreamableHTTPClientTransportOptions | undefined;\n\n    // Check if it's a stdio config (has 'command' property)\n    if (typeof serverConfig === \"object\" && \"command\" in serverConfig) {\n      transport = new StdioClientTransport(serverConfig);\n    } else {\n      // Handle URL-based connection\n      let serverUrl: string | URL;\n\n      if (typeof serverConfig === \"string\" || serverConfig instanceof URL) {\n        serverUrl = serverConfig;\n      } else {\n        serverUrl = (serverConfig as ConnectToMCPServerOptions).serverUrl;\n        clientOptions = (serverConfig as ConnectToMCPServerOptions)\n          .clientOptions;\n        requestOptions = (serverConfig as ConnectToMCPServerOptions)\n          .requestOptions;\n      }\n\n      transport = new StreamableHTTPClientTransport(\n        new URL(serverUrl),\n        requestOptions,\n      );\n    }\n\n    const client = new Client({\n      name: \"Stagehand\",\n      version: \"1.0.0\",\n      ...clientOptions,\n    });\n\n    await client.connect(transport);\n\n    try {\n      await client.ping();\n    } catch (pingError) {\n      await client.close();\n      throw new MCPConnectionError(serverConfig.toString(), pingError);\n    }\n\n    return client;\n  } catch (error) {\n    // Handle any errors during transport/client creation or connection\n    if (error instanceof MCPConnectionError) {\n      throw error; // Re-throw our custom error\n    }\n    throw new MCPConnectionError(serverConfig.toString(), error);\n  }\n};\n"
  },
  {
    "path": "packages/core/lib/v3/mcp/utils.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { ToolSet } from \"ai\";\nimport { JsonSchema, jsonSchemaToZod } from \"../../utils.js\";\nimport { connectToMCPServer } from \"./connection.js\";\n\nexport const resolveTools = async (\n  clients: (Client | string)[],\n  userTools: ToolSet,\n): Promise<ToolSet> => {\n  const tools: ToolSet = { ...userTools };\n\n  for (const client of clients) {\n    let clientInstance: Client;\n    if (typeof client === \"string\") {\n      clientInstance = await connectToMCPServer(client);\n    } else {\n      clientInstance = client;\n    }\n\n    let nextCursor: string | undefined = undefined;\n\n    do {\n      const clientTools = await clientInstance.listTools({\n        cursor: nextCursor,\n      });\n\n      for (const tool of clientTools.tools) {\n        tools[tool.name] = {\n          description: tool.description,\n          inputSchema: jsonSchemaToZod(tool.inputSchema as JsonSchema),\n          execute: async (input) => {\n            const result = await clientInstance.callTool({\n              name: tool.name,\n              arguments: input,\n            });\n            return result;\n          },\n        };\n      }\n      nextCursor = clientTools.nextCursor;\n    } while (nextCursor);\n  }\n\n  return tools;\n};\n"
  },
  {
    "path": "packages/core/lib/v3/runtimePaths.ts",
    "content": "/**\n * Keep this file in sync with:\n * - /packages/core/lib/v3/runtimePaths.ts\n * - /packages/server-v3/scripts/runtimePaths.ts\n * - /packages/server-v4/scripts/runtimePaths.ts\n * - /packages/evals/runtimePaths.ts\n * - /packages/docs/scripts/runtimePaths.js\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createRequire } from \"node:module\";\n\nconst PACKAGE_SEGMENT = \"/packages/core/\";\nconst EVAL_FRAMES = new Set([\"[eval]\", \"[eval]-wrapper\"]);\nconst INTERNAL_FRAME_NAMES = new Set([\n  \"readCallsites\",\n  \"readCallsitePath\",\n  \"resolveCallerFilePath\",\n  \"getCurrentFilePath\",\n  \"getCurrentDirPath\",\n  \"getRepoRootDir\",\n  \"getPackageRootDir\",\n  \"createRequireFromCaller\",\n  \"isMainModule\",\n]);\n\nconst normalizePath = (value: string): string => {\n  const input = value.startsWith(\"file://\") ? fileURLToPath(value) : value;\n  return path.resolve(input).replaceAll(\"\\\\\", \"/\");\n};\n\nconst readCallsites = (): NodeJS.CallSite[] => {\n  const previousPrepare = Error.prepareStackTrace;\n  try {\n    Error.prepareStackTrace = (_, stack) => stack;\n    return (\n      (new Error().stack as unknown as NodeJS.CallSite[] | undefined) ?? []\n    );\n  } finally {\n    Error.prepareStackTrace = previousPrepare;\n  }\n};\n\ntype CallSiteWithScriptName = NodeJS.CallSite & {\n  getScriptNameOrSourceURL?: () => string | null;\n};\n\nconst readCallsitePath = (callsite: NodeJS.CallSite): string | null => {\n  const callsiteWithScript = callsite as CallSiteWithScriptName;\n  const rawPath =\n    callsite.getFileName() ?? callsiteWithScript.getScriptNameOrSourceURL?.();\n  if (!rawPath) return null;\n  if (rawPath.startsWith(\"node:\")) return null;\n  if (EVAL_FRAMES.has(rawPath)) return null;\n  return normalizePath(rawPath);\n};\n\nconst isInternalCallsite = (callsite: NodeJS.CallSite): boolean => {\n  const functionName = callsite.getFunctionName();\n  if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) return true;\n\n  const methodName = callsite.getMethodName();\n  if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) return true;\n\n  const callsiteString = callsite.toString();\n  for (const frameName of INTERNAL_FRAME_NAMES) {\n    if (callsiteString.includes(`${frameName} (`)) return true;\n    if (callsiteString.includes(`.${frameName} (`)) return true;\n  }\n  return false;\n};\n\nconst resolveCallerFilePath = (): string => {\n  const packageCandidates: string[] = [];\n  const fallbackCandidates: string[] = [];\n\n  for (const callsite of readCallsites()) {\n    const filePath = readCallsitePath(callsite);\n    if (!filePath) continue;\n    if (isInternalCallsite(callsite)) continue;\n    if (filePath.includes(PACKAGE_SEGMENT)) {\n      packageCandidates.push(filePath);\n      continue;\n    }\n    fallbackCandidates.push(filePath);\n  }\n\n  const packageCandidate = packageCandidates[0];\n  if (packageCandidate) return packageCandidate;\n\n  const fallbackCandidate = fallbackCandidates[0];\n  if (fallbackCandidate) return fallbackCandidate;\n\n  throw new Error(\"Unable to resolve caller file path.\");\n};\n\nexport const getCurrentFilePath = (): string => resolveCallerFilePath();\n\nexport const getCurrentDirPath = (): string =>\n  path.dirname(getCurrentFilePath());\n\nexport const getRepoRootDir = (): string => {\n  const currentFilePath = getCurrentFilePath();\n  const index = currentFilePath.lastIndexOf(PACKAGE_SEGMENT);\n  if (index === -1) {\n    throw new Error(\n      `Unable to determine repo root from ${currentFilePath} (missing ${PACKAGE_SEGMENT}).`,\n    );\n  }\n  return currentFilePath.slice(0, index);\n};\n\nexport const getPackageRootDir = (): string =>\n  `${getRepoRootDir()}${PACKAGE_SEGMENT.slice(0, -1)}`;\n\nexport const createRequireFromCaller = () =>\n  createRequire(getCurrentFilePath());\n\nexport const isMainModule = (): boolean => {\n  const entryScript = process.argv.at(1);\n  if (!entryScript) return false;\n  return normalizePath(entryScript) === getCurrentFilePath();\n};\n"
  },
  {
    "path": "packages/core/lib/v3/shutdown/cleanupLocal.ts",
    "content": "import fs from \"node:fs\";\n\n/**\n * Shared cleanup logic for locally launched Chrome.\n *\n * Used by both `V3.close()` (normal shutdown) and the supervisor process\n * (crash cleanup). The caller provides a `killChrome` callback since the\n * kill mechanism differs: chrome-launcher's `chrome.kill()` in-process\n * vs raw `process.kill(pid)` from the supervisor.\n */\nexport async function cleanupLocalBrowser(opts: {\n  killChrome?: () => Promise<void> | void;\n  userDataDir?: string;\n  createdTempProfile?: boolean;\n  preserveUserDataDir?: boolean;\n}): Promise<void> {\n  if (opts.killChrome) {\n    try {\n      await opts.killChrome();\n    } catch {\n      // best-effort\n    }\n  }\n  if (\n    opts.createdTempProfile &&\n    !opts.preserveUserDataDir &&\n    opts.userDataDir\n  ) {\n    try {\n      fs.rmSync(opts.userDataDir, { recursive: true, force: true });\n    } catch {\n      // ignore cleanup errors\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/shutdown/supervisor.ts",
    "content": "/**\n * Shutdown supervisor process.\n *\n * This process watches a stdin lifeline. When the parent dies, stdin closes\n * and the supervisor performs best-effort cleanup:\n * - LOCAL: kill Chrome + remove temp profile\n * - STAGEHAND_API: request session release\n */\n\nimport Browserbase from \"@browserbasehq/sdk\";\nimport type { ShutdownSupervisorConfig } from \"../types/private/shutdown.js\";\nimport { cleanupLocalBrowser } from \"./cleanupLocal.js\";\n\nconst SIGKILL_POLL_MS = 250;\nconst SIGKILL_TIMEOUT_MS = 7_000;\nconst PID_POLL_INTERVAL_MS = 500;\n\n// `cleanupPromise` guarantees we execute cleanup at most once.\nlet config: ShutdownSupervisorConfig | null = null;\nlet cleanupPromise: Promise<void> | null = null;\nlet started = false;\nlet localPidKnownGone = false;\n\nconst exit = (code = 0): void => {\n  try {\n    process.exit(code);\n  } catch {\n    // ignore\n  }\n};\n\n// Best-effort two-phase kill: SIGTERM first, then SIGKILL after timeout.\n// Treat only ESRCH as \"already gone\"; other errors should not imply dead.\nconst politeKill = async (pid: number): Promise<void> => {\n  const isAlive = (): boolean => {\n    try {\n      process.kill(pid, 0);\n      return true;\n    } catch (error) {\n      const err = error as NodeJS.ErrnoException;\n      // ESRCH = \"No such process\" (PID is already gone).\n      return err.code !== \"ESRCH\";\n    }\n  };\n\n  if (!isAlive()) return;\n  try {\n    process.kill(pid, \"SIGTERM\");\n  } catch (error) {\n    const err = error as NodeJS.ErrnoException;\n    // ESRCH = process already exited; no further action needed.\n    if (err.code === \"ESRCH\") return;\n  }\n\n  const deadline = Date.now() + SIGKILL_TIMEOUT_MS;\n  while (Date.now() < deadline) {\n    await new Promise((resolve) => setTimeout(resolve, SIGKILL_POLL_MS));\n    if (!isAlive()) return;\n  }\n  try {\n    process.kill(pid, \"SIGKILL\");\n  } catch {\n    // best-effort\n  }\n};\n\nlet pidPollTimer: NodeJS.Timeout | null = null;\n\n// Local-only fallback: if Chrome dies while parent still lives, run cleanup and exit.\nconst startPidPolling = (pid: number): void => {\n  if (pidPollTimer) return;\n  pidPollTimer = setInterval(() => {\n    try {\n      process.kill(pid, 0);\n      return;\n    } catch (error) {\n      const err = error as NodeJS.ErrnoException;\n      // Only ESRCH means the process is definitely gone.\n      if (err.code !== \"ESRCH\") return;\n    }\n\n    localPidKnownGone = true;\n    if (pidPollTimer) {\n      clearInterval(pidPollTimer);\n      pidPollTimer = null;\n    }\n    void runCleanup(\"Browser process exited\").finally(() => exit(0));\n  }, PID_POLL_INTERVAL_MS);\n};\n\nconst cleanupLocal = async (\n  cfg: Extract<ShutdownSupervisorConfig, { kind: \"LOCAL\" }>,\n  reason: string,\n) => {\n  const deletingUserDataDir = Boolean(\n    cfg.createdTempProfile && !cfg.preserveUserDataDir && cfg.userDataDir,\n  );\n  await cleanupLocalBrowser({\n    // If polling already observed ESRCH, avoid a follow-up PID kill.\n    // The PID could be reused by a different process before cleanup runs.\n    killChrome:\n      cfg.pid && !localPidKnownGone\n        ? () => {\n            console.error(\n              `[shutdown-supervisor] Shutting down Chrome pid=${cfg.pid} ` +\n                `(reason=${reason}, deletingUserDataDir=${deletingUserDataDir})`,\n            );\n            return politeKill(cfg.pid);\n          }\n        : undefined,\n    userDataDir: cfg.userDataDir,\n    createdTempProfile: cfg.createdTempProfile,\n    preserveUserDataDir: cfg.preserveUserDataDir,\n  });\n};\n\nconst cleanupBrowserbase = async (\n  cfg: Extract<ShutdownSupervisorConfig, { kind: \"STAGEHAND_API\" }>,\n  reason: string,\n) => {\n  if (!cfg.apiKey || !cfg.sessionId) return;\n  try {\n    console.error(\n      `[shutdown-supervisor] Ending Browserbase session ${cfg.sessionId} ` +\n        `(reason=${reason})`,\n    );\n    const bb = new Browserbase({ apiKey: cfg.apiKey });\n    await bb.sessions.update(cfg.sessionId, {\n      status: \"REQUEST_RELEASE\",\n      ...(cfg.projectId ? { projectId: cfg.projectId } : {}),\n    } as Browserbase.Sessions.SessionUpdateParams);\n  } catch {\n    // best-effort cleanup\n  }\n};\n\n// Idempotent cleanup entrypoint used by all supervisor shutdown paths.\nconst runCleanup = (reason: string): Promise<void> => {\n  if (!cleanupPromise) {\n    cleanupPromise = (async () => {\n      const cfg = config;\n      if (!cfg) return;\n      if (cfg.kind === \"LOCAL\") {\n        await cleanupLocal(cfg, reason);\n        return;\n      }\n      if (cfg.kind === \"STAGEHAND_API\") {\n        await cleanupBrowserbase(cfg, reason);\n      }\n    })();\n  }\n  return cleanupPromise;\n};\n\nconst applyConfig = (nextConfig: ShutdownSupervisorConfig): void => {\n  config = nextConfig;\n  localPidKnownGone = false;\n  if (config.kind === \"LOCAL\" && config.pid) {\n    startPidPolling(config.pid);\n  }\n};\n\nconst onLifelineClosed = (reason: string) => {\n  void runCleanup(reason).finally(() => exit(0));\n};\n\nconst parseConfigFromArgv = (\n  argv: readonly string[] = process.argv.slice(2),\n): ShutdownSupervisorConfig | null => {\n  const prefix = \"--supervisor-config=\";\n  const raw = argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);\n  if (!argv.includes(\"--supervisor\") || !raw) return null;\n  try {\n    return JSON.parse(raw) as ShutdownSupervisorConfig;\n  } catch {\n    return null;\n  }\n};\n\nexport const runShutdownSupervisor = (\n  initialConfig: ShutdownSupervisorConfig,\n): void => {\n  if (started) return;\n  started = true;\n  applyConfig(initialConfig);\n\n  // Stdin is the lifeline; losing it means parent is gone.\n  try {\n    process.stdin.resume();\n    process.stdin.on(\"end\", () =>\n      onLifelineClosed(\"Stagehand process completed\"),\n    );\n    process.stdin.on(\"close\", () =>\n      onLifelineClosed(\"Stagehand process completed\"),\n    );\n    process.stdin.on(\"error\", () =>\n      onLifelineClosed(\"Stagehand process crashed or was killed\"),\n    );\n  } catch {\n    // ignore\n  }\n};\n\nexport const maybeRunShutdownSupervisorFromArgv = (\n  argv: readonly string[] = process.argv.slice(2),\n): boolean => {\n  const parsed = parseConfigFromArgv(argv);\n  if (!parsed) return false;\n  runShutdownSupervisor(parsed);\n  return true;\n};\n"
  },
  {
    "path": "packages/core/lib/v3/shutdown/supervisorClient.ts",
    "content": "/**\n * Parent-side helper for spawning the shutdown supervisor process.\n *\n * The supervisor runs out-of-process and watches a lifeline pipe. If the parent\n * dies, the supervisor performs best-effort cleanup (Chrome kill or Browserbase\n * session release) when keepAlive is false.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport type {\n  ShutdownSupervisorConfig,\n  ShutdownSupervisorHandle,\n} from \"../types/private/shutdown.js\";\nimport {\n  ShutdownSupervisorResolveError,\n  ShutdownSupervisorSpawnError,\n} from \"../types/private/shutdownErrors.js\";\nimport { getCurrentFilePath } from \"../runtimePaths.js\";\n\nconst moduleFilename = getCurrentFilePath();\nconst moduleDir = path.dirname(moduleFilename);\nconst nodeRequire = createRequire(moduleFilename);\n\nconst isSeaRuntime = (): boolean => {\n  try {\n    const sea = nodeRequire(\"node:sea\") as { isSea?: () => boolean };\n    return Boolean(sea.isSea?.());\n  } catch {\n    return false;\n  }\n};\n\n// SEA: re-exec current binary with supervisor args.\n// Non-SEA: execute Stagehand CLI entrypoint with supervisor args.\nconst resolveCliPath = (): string => `${moduleDir}/../cli.js`;\n\nconst resolveSupervisorCommand = (\n  config: ShutdownSupervisorConfig,\n): {\n  command: string;\n  args: string[];\n} | null => {\n  const baseArgs = [\"--supervisor\", serializeConfigArg(config)];\n\n  if (isSeaRuntime()) {\n    return { command: process.execPath, args: baseArgs };\n  }\n\n  const cliPath = resolveCliPath();\n  if (!fs.existsSync(cliPath)) return null;\n  const needsTsxLoader =\n    fs.existsSync(`${moduleDir}/supervisor.ts`) &&\n    !fs.existsSync(`${moduleDir}/supervisor.js`);\n  return {\n    command: process.execPath,\n    args: needsTsxLoader\n      ? [\"--import\", \"tsx\", cliPath, ...baseArgs]\n      : [cliPath, ...baseArgs],\n  };\n};\n\n// Single JSON arg keeps supervisor bootstrap parsing tiny and versionable.\nconst serializeConfigArg = (config: ShutdownSupervisorConfig): string =>\n  `--supervisor-config=${JSON.stringify({\n    ...config,\n    parentPid: process.pid,\n  })}`;\n\n/**\n * Start a supervisor process for crash cleanup. Returns a handle that can\n * stop the supervisor during a normal shutdown.\n */\nexport function startShutdownSupervisor(\n  config: ShutdownSupervisorConfig,\n  opts?: { onError?: (error: Error, context: string) => void },\n): ShutdownSupervisorHandle | null {\n  const resolved = resolveSupervisorCommand(config);\n  if (!resolved) {\n    opts?.onError?.(\n      new ShutdownSupervisorResolveError(\n        \"Shutdown supervisor entry missing (expected Stagehand CLI entrypoint).\",\n      ),\n      \"resolve\",\n    );\n    return null;\n  }\n\n  const child = spawn(resolved.command, resolved.args, {\n    // stdin is the parent lifeline.\n    // Preserve supervisor stderr so crash-cleanup debug lines are visible.\n    stdio: [\"pipe\", \"ignore\", \"inherit\"],\n    detached: true,\n  });\n  child.on(\"error\", (error) => {\n    opts?.onError?.(\n      new ShutdownSupervisorSpawnError(\n        `Shutdown supervisor failed to start: ${error.message}`,\n      ),\n      \"spawn\",\n    );\n  });\n\n  try {\n    child.unref();\n    const stdin = child.stdin as unknown as { unref?: () => void } | null;\n    stdin?.unref?.();\n  } catch {\n    // best-effort: avoid keeping the event loop alive\n  }\n\n  const stop = () => {\n    // Normal close path: terminate supervisor directly.\n    try {\n      child.kill(\"SIGTERM\");\n    } catch {\n      // ignore\n    }\n  };\n\n  return { stop };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/timeoutConfig.ts",
    "content": "import { TimeoutError } from \"./types/public/sdkErrors.js\";\n\nexport function getEnvTimeoutMs(name: string): number | undefined {\n  const raw = process.env[name];\n  if (!raw) return undefined;\n  const normalized = raw.trim().replace(/ms$/i, \"\");\n  const value = Number(normalized);\n  if (!Number.isFinite(value) || value <= 0) return undefined;\n  return value;\n}\n\nexport async function withTimeout<T>(\n  promise: Promise<T>,\n  timeoutMs: number | null | undefined,\n  operation: string,\n): Promise<T> {\n  if (\n    typeof timeoutMs !== \"number\" ||\n    !Number.isFinite(timeoutMs) ||\n    timeoutMs <= 0\n  ) {\n    return await promise;\n  }\n\n  let timeoutId: NodeJS.Timeout | undefined;\n  const timeoutPromise = new Promise<never>((_, reject) => {\n    timeoutId = setTimeout(() => {\n      reject(new TimeoutError(operation, timeoutMs));\n    }, timeoutMs);\n  });\n  try {\n    return await Promise.race([promise, timeoutPromise]);\n  } finally {\n    if (timeoutId) clearTimeout(timeoutId);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/agent.ts",
    "content": "export interface ActionMappingOptions {\n  toolCallName: string;\n  toolResult: unknown;\n  args: Record<string, unknown>;\n  reasoning?: string;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/api.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\n\nexport interface SerializableResponse {\n  requestId: string;\n  frameId?: string;\n  loaderId?: string;\n  response: Protocol.Network.Response;\n  fromServiceWorkerFlag?: boolean;\n  finishedSettled?: boolean;\n  extraInfoHeaders?: Protocol.Network.Headers | null;\n  extraInfoHeadersText?: string;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/cache.ts",
    "content": "import type {\n  ActOptions,\n  ActResult,\n  AvailableModel,\n  Logger,\n  AgentResult,\n  Action,\n  LoadState,\n} from \"../public/index.js\";\nimport { CacheStorage } from \"../../cache/CacheStorage.js\";\nimport type { ActHandler } from \"../../handlers/actHandler.js\";\nimport type { V3Context } from \"../../understudy/context.js\";\nimport type { LLMClient } from \"../../llm/LLMClient.js\";\n\nexport type ActFn = (\n  instruction: string,\n  options?: ActOptions,\n) => Promise<ActResult>;\n\nexport type AgentCacheContext = {\n  instruction: string;\n  startUrl: string;\n  options: SanitizedAgentExecuteOptions;\n  configSignature: string;\n  cacheKey: string;\n  variableKeys: string[] /** Variable keys used in this execution (for cache key) */;\n  /** Variable values to substitute during replay */\n  variables?: Record<string, string>;\n};\n\nexport type AgentCacheTransferPayload = {\n  cacheKey: string;\n  entry: CachedAgentEntry;\n};\n\nexport type AgentCacheDeps = {\n  storage: CacheStorage;\n  logger: Logger;\n  getActHandler: () => ActHandler | null;\n  getContext: () => V3Context | null;\n  getDefaultLlmClient: () => LLMClient;\n  getBaseModelName: () => AvailableModel;\n  getSystemPrompt: () => string | undefined;\n  domSettleTimeoutMs?: number;\n  act: ActFn;\n  bufferLatestEntry?: boolean;\n};\n\nexport type ActCacheContext = {\n  instruction: string;\n  cacheKey: string;\n  pageUrl: string;\n  variableKeys: string[];\n  variables?: Record<string, string>;\n};\n\nexport type ActCacheDeps = {\n  storage: CacheStorage;\n  logger: Logger;\n  getActHandler: () => ActHandler | null;\n  getDefaultLlmClient: () => LLMClient;\n  domSettleTimeoutMs?: number;\n};\n\nexport type ReadJsonResult<T> = {\n  value: T | null;\n  path?: string;\n  error?: unknown;\n};\n\nexport type WriteJsonResult = {\n  path?: string;\n  error?: unknown;\n};\n\nexport interface CachedActEntry {\n  version: 1;\n  instruction: string;\n  url: string;\n  variableKeys: string[];\n  actions: Action[];\n  actionDescription?: string;\n  message?: string;\n}\n\nexport type AgentReplayStep =\n  | AgentReplayActStep\n  | AgentReplayFillFormStep\n  | AgentReplayGotoStep\n  | AgentReplayScrollStep\n  | AgentReplayWaitStep\n  | AgentReplayNavBackStep\n  | AgentReplayKeysStep\n  | { type: string; [key: string]: unknown };\n\nexport interface AgentReplayActStep {\n  type: \"act\";\n  instruction: string;\n  actions?: Action[];\n  actionDescription?: string;\n  message?: string;\n  timeout?: number;\n}\n\nexport interface AgentReplayFillFormStep {\n  type: \"fillForm\";\n  fields?: Array<{ action: string }>;\n  observeResults?: Action[];\n  actions?: Action[];\n}\n\nexport interface AgentReplayGotoStep {\n  type: \"goto\";\n  url: string;\n  waitUntil?: LoadState;\n}\n\nexport interface AgentReplayScrollStep {\n  type: \"scroll\";\n  deltaX?: number;\n  deltaY?: number;\n  anchor?: { x: number; y: number };\n}\n\nexport interface AgentReplayWaitStep {\n  type: \"wait\";\n  timeMs: number;\n}\n\nexport interface AgentReplayNavBackStep {\n  type: \"navback\";\n  waitUntil?: LoadState;\n}\n\nexport interface AgentReplayKeysStep {\n  type: \"keys\";\n  instruction?: string;\n  playwrightArguments: {\n    method: \"type\" | \"press\";\n    text?: string;\n    keys?: string;\n    times?: number;\n  };\n}\n\nexport interface SanitizedAgentExecuteOptions {\n  maxSteps?: number;\n  highlightCursor?: boolean;\n}\n\nexport interface CachedAgentEntry {\n  version: 1;\n  instruction: string;\n  startUrl: string;\n  options: SanitizedAgentExecuteOptions;\n  configSignature: string;\n  steps: AgentReplayStep[];\n  result: AgentResult;\n  timestamp: string;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/evaluator.ts",
    "content": "export type EvaluateOptions = {\n  /** The question to ask about the task state */\n  question: string;\n  /** The answer to the question */\n  answer?: string;\n  /** Whether to take a screenshot of the task state, or array of screenshots to evaluate */\n  screenshot?: boolean | Buffer[];\n  /** Custom system prompt for the evaluator */\n  systemPrompt?: string;\n  /** Delay in milliseconds before taking the screenshot @default 250 */\n  screenshotDelayMs?: number;\n  /** The agent's reasoning/thought process for completing the task */\n  agentReasoning?: string;\n};\n\nexport type BatchAskOptions = {\n  /** Array of questions with optional answers */\n  questions: Array<{\n    question: string;\n    answer?: string;\n  }>;\n  /** Whether to take a screenshot of the task state */\n  screenshot?: boolean;\n  /** Custom system prompt for the evaluator */\n  systemPrompt?: string;\n  /** Delay in milliseconds before taking the screenshot @default 1000 */\n  screenshotDelayMs?: number;\n};\n\n/**\n * Result of an evaluation\n */\nexport interface EvaluationResult {\n  /**\n   * The evaluation result ('YES', 'NO', or 'INVALID' if parsing failed or value was unexpected)\n   */\n  evaluation: \"YES\" | \"NO\" | \"INVALID\";\n  /**\n   * The reasoning behind the evaluation\n   */\n  reasoning: string;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/handlers.ts",
    "content": "import { Page } from \"../../understudy/page.js\";\nimport { ModelConfiguration } from \"../public/model.js\";\nimport type { StagehandZodSchema } from \"../../zodCompat.js\";\nimport type { Variables } from \"../public/agent.js\";\n\nexport interface ActHandlerParams {\n  instruction: string;\n  model?: ModelConfiguration;\n  variables?: Variables;\n  timeout?: number;\n  page: Page;\n}\n\nexport interface ExtractHandlerParams<T extends StagehandZodSchema> {\n  instruction?: string;\n  schema?: T;\n  model?: ModelConfiguration;\n  timeout?: number;\n  selector?: string;\n  page: Page;\n}\n\nexport interface ObserveHandlerParams {\n  instruction?: string;\n  model?: ModelConfiguration;\n  timeout?: number;\n  selector?: string;\n  page: Page;\n}\n\n// We can use this enum to list the actions supported in performUnderstudyMethod\nexport enum SupportedUnderstudyAction {\n  CLICK = \"click\",\n  FILL = \"fill\",\n  TYPE = \"type\",\n  PRESS = \"press\",\n  SCROLL = \"scrollTo\",\n  NEXT_CHUNK = \"nextChunk\",\n  PREV_CHUNK = \"prevChunk\",\n  SELECT_OPTION_FROM_DROPDOWN = \"selectOptionFromDropdown\",\n  HOVER = \"hover\",\n  DOUBLE_CLICK = \"doubleClick\",\n  DRAG_AND_DROP = \"dragAndDrop\",\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/index.ts",
    "content": "export * from \"./api.js\";\nexport * from \"./handlers.js\";\nexport * from \"./internal.js\";\nexport * from \"./evaluator.js\";\nexport * from \"./cache.js\";\nexport * from \"./agent.js\";\nexport * from \"./snapshot.js\";\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/internal.ts",
    "content": "import Browserbase from \"@browserbasehq/sdk\";\nimport { LaunchedChrome } from \"chrome-launcher\";\n\nexport type InitState =\n  | { kind: \"UNINITIALIZED\" }\n  | {\n      kind: \"LOCAL\";\n      chrome: LaunchedChrome;\n      ws: string;\n      userDataDir?: string;\n      createdTempProfile?: boolean;\n      preserveUserDataDir?: boolean;\n    }\n  | { kind: \"BROWSERBASE\"; bb: Browserbase; sessionId: string; ws: string };\n\nexport type EncodedId = `${number}-${number}`;\n\n/**\n * Represents a path through a Zod schema from the root object down to a\n * particular field. The `segments` array describes the chain of keys/indices.\n *\n * - **String** segments indicate object property names.\n * - **Number** segments indicate array indices.\n *\n * For example, `[\"users\", 0, \"homepage\"]` might describe reaching\n * the `homepage` field in `schema.users[0].homepage`.\n */\nexport interface ZodPathSegments {\n  /**\n   * The ordered list of keys/indices leading from the schema root\n   * to the targeted field.\n   */\n  segments: Array<string | number>;\n}\n\nexport type InitScriptSource<Arg> =\n  | string\n  | { path?: string; content?: string }\n  | ((arg: Arg) => unknown);\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/locator.ts",
    "content": "import { Buffer } from \"buffer\";\n\nexport interface NormalizedFilePayload {\n  name: string;\n  mimeType: string;\n  buffer: Buffer;\n  lastModified: number;\n  /** Absolute path to the source file when provided by the caller. */\n  absolutePath?: string;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/network.ts",
    "content": "import { Protocol } from \"devtools-protocol\";\n\n/** Metadata tracked for each network request currently in-flight. */\nexport type NetworkRequestInfo = {\n  sessionId: string;\n  requestId: string;\n  requestKey: string;\n  frameId?: string;\n  loaderId?: string;\n  url?: string;\n  timestamp: number;\n  resourceType?: Protocol.Network.ResourceType;\n  documentRequest: boolean;\n};\n\n/** Callback hooks consumers can implement to observe network transitions. */\nexport interface NetworkObserver {\n  onRequestStarted(info: NetworkRequestInfo): void;\n  onRequestFinished(info: NetworkRequestInfo): void;\n  onRequestFailed(info: NetworkRequestInfo): void;\n}\n\n/** Options for the idle waiter helper. */\nexport type WaitForIdleOptions = {\n  startTime?: number;\n  timeoutMs: number;\n  idleTimeMs?: number;\n  filter?: (info: NetworkRequestInfo) => boolean;\n  totalBudgetMs?: number;\n};\n\nexport const DEFAULT_IDLE_WAIT = 500;\nexport const IGNORED_RESOURCE_TYPES = new Set<\n  Protocol.Network.ResourceType | undefined\n>([\"EventSource\", \"WebSocket\"]);\n\n/** The handle returned by the network manager idle helper. */\nexport type WaitForIdleHandle = {\n  promise: Promise<void>;\n  dispose: () => void;\n};\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/shutdown.ts",
    "content": "/**\n * Internal-only types for the shutdown supervisor process.\n */\n\nexport type ShutdownSupervisorConfig =\n  | {\n      kind: \"LOCAL\";\n      pid: number;\n      userDataDir?: string;\n      createdTempProfile?: boolean;\n      preserveUserDataDir?: boolean;\n    }\n  | {\n      kind: \"STAGEHAND_API\";\n      sessionId: string;\n      apiKey: string;\n      projectId?: string;\n    };\n\nexport interface ShutdownSupervisorHandle {\n  /** Best-effort signal to stop the supervisor process. */\n  stop: () => void;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/shutdownErrors.ts",
    "content": "/**\n * Internal-only errors for the shutdown supervisor.\n */\n\nexport class ShutdownSupervisorError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"ShutdownSupervisorError\";\n  }\n}\n\nexport class ShutdownSupervisorResolveError extends ShutdownSupervisorError {\n  constructor(message: string) {\n    super(message);\n    this.name = \"ShutdownSupervisorResolveError\";\n  }\n}\n\nexport class ShutdownSupervisorSpawnError extends ShutdownSupervisorError {\n  constructor(message: string) {\n    super(message);\n    this.name = \"ShutdownSupervisorSpawnError\";\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/private/snapshot.ts",
    "content": "/**\n * Options that control how hybrid snapshots and targeted scopes are captured.\n */\nexport type SnapshotOptions = {\n  /**\n   * Filter the snapshot to a specific element/subtree using a selector that can cross iframes.\n   * Supports XPath (prefixed with `xpath=` or starting with `/`) and CSS with iframe hops via `>>`.\n   */\n  focusSelector?: string;\n  /**\n   * Pierce shadow DOM when calling DOM.getDocument. Defaults to true to retain the\n   * existing behaviour.\n   */\n  pierceShadow?: boolean;\n  /**\n   * Toggle whether iframe subtrees are included in the merged snapshot. Defaults to true.\n   */\n  includeIframes?: boolean;\n  /**\n   * Optional feature flag that surfaces experimental traversal tweaks in the Accessibility layer.\n   */\n  experimental?: boolean;\n};\n\n/**\n * Hybrid snapshot payload consumed by act/extract/observe handlers.\n */\nexport type HybridSnapshot = {\n  /** Merged outline across every frame. */\n  combinedTree: string;\n  /** EncodedId (frameOrdinal-backendNodeId) -> absolute XPath. */\n  combinedXpathMap: Record<string, string>;\n  /** EncodedId -> URL extracted from AX properties. */\n  combinedUrlMap: Record<string, string>;\n  /** Per-frame payloads expose the original relative data for debugging. */\n  perFrame?: PerFrameSnapshot[];\n};\n\nexport type PerFrameSnapshot = {\n  frameId: string;\n  outline: string;\n  xpathMap: Record<string, string>;\n  urlMap: Record<string, string>;\n};\n\n/**\n * Compact encoding of DOM data for an entire session. Shared between capture\n * and focus helpers so DOM traversal can be unit tested in isolation.\n */\nexport type SessionDomIndex = {\n  rootBackend: number;\n  absByBe: Map<number, string>;\n  tagByBe: Map<number, string>;\n  scrollByBe: Map<number, boolean>;\n  docRootOf: Map<number, number>;\n  contentDocRootByIframe: Map<number, number>;\n};\n\nexport type FrameDomMaps = {\n  tagNameMap: Record<string, string>;\n  xpathMap: Record<string, string>;\n  scrollableMap: Record<string, boolean>;\n  urlMap: Record<string, string>;\n};\n\nexport type ResolvedLocation = {\n  frameId: string;\n  backendNodeId: number;\n  absoluteXPath: string;\n};\n\nexport type ResolvedFocusFrame = {\n  targetFrameId: string;\n  tailXPath: string;\n  absPrefix: string;\n};\n\nexport type ResolvedCssFocus = {\n  targetFrameId: string;\n  tailSelector: string;\n  absPrefix: string;\n};\n\nexport type Axis = \"child\" | \"desc\";\n\nexport type Step = {\n  axis: Axis;\n  raw: string;\n  name: string;\n};\n\nexport type A11yNode = {\n  role: string;\n  name?: string;\n  description?: string;\n  value?: string | number | boolean;\n  nodeId: string;\n  backendDOMNodeId?: number;\n  parentId?: string;\n  childIds?: string[];\n  children?: A11yNode[];\n  encodedId?: string;\n};\n\nexport type A11yOptions = {\n  focusSelector?: string;\n  experimental: boolean;\n  tagNameMap: Record<string, string>;\n  scrollableMap: Record<string, boolean>;\n  encode: (backendNodeId: number) => string;\n};\n\nexport type AccessibilityTreeResult = {\n  outline: string;\n  urlMap: Record<string, string>;\n  scopeApplied: boolean;\n};\n\nexport type FrameParentIndex = Map<string, string | null>;\n\n/**\n * Shared frame metadata that every snapshot step needs.\n * - `rootId`: stable identifier for the main frame so we can detect root prefixes.\n * - `parentByFrame`: lookup table for iframe parentage (used by focus scoping and prefixing).\n * - `frames`: DFS-ordered frame ids so merging walks parents before children.\n */\nexport type FrameContext = {\n  rootId: string;\n  parentByFrame: FrameParentIndex;\n  frames: string[];\n};\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/agent.ts",
    "content": "import type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n  ToolSet,\n  ModelMessage,\n  wrapLanguageModel,\n  StreamTextResult,\n  StepResult,\n  PrepareStepFunction,\n  GenerateTextOnStepFinishCallback,\n  StreamTextOnStepFinishCallback,\n  StreamTextOnErrorCallback,\n  StreamTextOnChunkCallback,\n  StreamTextOnFinishCallback,\n} from \"ai\";\nimport { LogLine } from \"./logs.js\";\nimport { ClientOptions } from \"./model.js\";\nimport { StagehandZodObject } from \"../../zodCompat.js\";\n\n// Re-export ModelMessage for consumers who want to use it for conversation continuation\nexport type { ModelMessage } from \"ai\";\n\n// Re-export Tool type for consumers who want to define custom tools\nexport type { Tool } from \"ai\";\nimport { Page as PlaywrightPage } from \"playwright-core\";\nimport { Page as PuppeteerPage } from \"puppeteer-core\";\nimport { Page as PatchrightPage } from \"patchright-core\";\nimport { Page } from \"../../understudy/page.js\";\n\n// =============================================================================\n// Variable Types\n// =============================================================================\n\n/**\n * A variable value can be a simple primitive or a rich object with an optional description.\n * This unified type is shared across `act`, `agent.execute`, and other methods.\n *\n * @example Simple (backward-compatible):\n * ```typescript\n * variables: { username: \"john@example.com\" }\n * ```\n *\n * @example Rich with description (useful for agents):\n * ```typescript\n * variables: {\n *   username: { value: \"john@example.com\", description: \"The login email\" }\n * }\n * ```\n */\nexport type VariableValue =\n  | string\n  | number\n  | boolean\n  | { value: string | number | boolean; description?: string };\n\n/**\n * A collection of named variables for use in act, agent, and other methods.\n */\nexport type Variables = Record<string, VariableValue>;\n\nexport interface AgentContext {\n  options: AgentExecuteOptionsBase;\n  maxSteps: number;\n  systemPrompt: string;\n  allTools: ToolSet;\n  messages: ModelMessage[];\n  wrappedModel: ReturnType<typeof wrapLanguageModel>;\n  initialPageUrl: string;\n}\n\nexport interface AgentState {\n  collectedReasoning: string[];\n  actions: AgentAction[];\n  finalMessage: string;\n  completed: boolean;\n  currentPageUrl: string;\n}\n\nexport interface AgentAction {\n  type: string;\n  reasoning?: string;\n  taskCompleted?: boolean;\n  action?: string;\n  // Tool-specific fields\n  timeMs?: number; // wait tool\n  pageText?: string; // ariaTree tool\n  pageUrl?: string; // ariaTree tool\n  instruction?: string; // various tools\n  [key: string]: unknown;\n}\n\nexport interface AgentResult {\n  success: boolean;\n  message: string;\n  actions: AgentAction[];\n  completed: boolean;\n  metadata?: Record<string, unknown>;\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n    reasoning_tokens?: number;\n    cached_input_tokens?: number;\n    inference_time_ms: number;\n  };\n  /**\n   * The conversation messages from this execution.\n   * Pass these to a subsequent execute() call via the `messages` option to continue the conversation.\n   * @experimental\n   */\n  messages?: ModelMessage[];\n  /**\n   * Custom output data extracted based on the `output` schema provided in execute options.\n   * Only populated if an `output` schema was provided.\n   * @experimental\n   */\n  output?: Record<string, unknown>;\n}\n\nexport type AgentStreamResult = StreamTextResult<ToolSet, never> & {\n  result: Promise<AgentResult>;\n};\n\n/**\n * Base callbacks shared between execute (non-streaming) and streaming modes.\n */\nexport interface AgentCallbacks {\n  /**\n   * Optional function called before each step to modify settings.\n   * You can change the model, tool choices, active tools, system prompt,\n   * and input messages for each step.\n   */\n  prepareStep?: PrepareStepFunction<ToolSet>;\n  /**\n   * Callback called when each step (LLM call) is finished.\n   * This is called for intermediate steps as well as the final step.\n   */\n  onStepFinish?:\n    | GenerateTextOnStepFinishCallback<ToolSet>\n    | StreamTextOnStepFinishCallback<ToolSet>;\n}\n\n/**\n * Error message type for streaming-only callbacks used in non-streaming mode.\n * This provides a clear error message when users try to use streaming callbacks without stream: true.\n */\ntype StreamingCallbackNotAvailable =\n  \"This callback requires 'stream: true' in AgentConfig. Set stream: true to use streaming callbacks like onChunk, onFinish, onError, and onAbort.\";\n\n/**\n * Error message for safety confirmation callback misuse.\n * Safety confirmations are only available for non-streaming CUA agent executions.\n */\ntype SafetyConfirmationCallbackNotAvailable =\n  \"Safety confirmation callbacks are only available via non-streaming AgentExecuteOptions.callbacks when using mode: 'cua'.\";\n\n/**\n * Callbacks specific to the non-streaming execute method.\n */\nexport interface AgentExecuteCallbacks extends AgentCallbacks {\n  /**\n   * Callback called when each step (LLM call) is finished.\n   */\n  onStepFinish?: GenerateTextOnStepFinishCallback<ToolSet>;\n  /**\n   * Callback for handling safety confirmation requests from CUA providers.\n   * Only available when running an agent configured with mode: \"cua\".\n   */\n  onSafetyConfirmation?: SafetyConfirmationHandler;\n\n  /**\n   * NOT AVAILABLE in non-streaming mode.\n   * This callback requires `stream: true` in AgentConfig.\n   *\n   * @example\n   * ```typescript\n   * // Enable streaming to use onChunk:\n   * const agent = stagehand.agent({ stream: true });\n   * await agent.execute({\n   *   instruction: \"...\",\n   *   callbacks: { onChunk: async (chunk) => console.log(chunk) }\n   * });\n   * ```\n   */\n  onChunk?: StreamingCallbackNotAvailable;\n\n  /**\n   * NOT AVAILABLE in non-streaming mode.\n   * This callback requires `stream: true` in AgentConfig.\n   *\n   * @example\n   * ```typescript\n   * // Enable streaming to use onFinish:\n   * const agent = stagehand.agent({ stream: true });\n   * await agent.execute({\n   *   instruction: \"...\",\n   *   callbacks: { onFinish: (event) => console.log(\"Done!\", event) }\n   * });\n   * ```\n   */\n  onFinish?: StreamingCallbackNotAvailable;\n\n  /**\n   * NOT AVAILABLE in non-streaming mode.\n   * This callback requires `stream: true` in AgentConfig.\n   *\n   * @example\n   * ```typescript\n   * // Enable streaming to use onError:\n   * const agent = stagehand.agent({ stream: true });\n   * await agent.execute({\n   *   instruction: \"...\",\n   *   callbacks: { onError: ({ error }) => console.error(error) }\n   * });\n   * ```\n   */\n  onError?: StreamingCallbackNotAvailable;\n\n  /**\n   * NOT AVAILABLE in non-streaming mode.\n   * This callback requires `stream: true` in AgentConfig.\n   *\n   * @example\n   * ```typescript\n   * // Enable streaming to use onAbort:\n   * const agent = stagehand.agent({ stream: true });\n   * await agent.execute({\n   *   instruction: \"...\",\n   *   callbacks: { onAbort: (event) => console.log(\"Aborted\", event.steps) }\n   * });\n   * ```\n   */\n  onAbort?: StreamingCallbackNotAvailable;\n}\n\n/**\n * Callbacks specific to the streaming mode.\n */\nexport interface AgentStreamCallbacks extends AgentCallbacks {\n  /**\n   * Callback called when each step (LLM call) is finished during streaming.\n   */\n  onStepFinish?: StreamTextOnStepFinishCallback<ToolSet>;\n  /**\n   * Callback called when an error occurs during streaming.\n   * Use this to log errors or handle error states.\n   */\n  onError?: StreamTextOnErrorCallback;\n  /**\n   * Callback called for each chunk of the stream.\n   * Stream processing will pause until the callback promise resolves.\n   */\n  onChunk?: StreamTextOnChunkCallback<ToolSet>;\n  /**\n   * Callback called when the stream finishes.\n   */\n  onFinish?: StreamTextOnFinishCallback<ToolSet>;\n  /**\n   * Callback called when the stream is aborted.\n   */\n  onAbort?: (event: {\n    steps: Array<StepResult<ToolSet>>;\n  }) => PromiseLike<void> | void;\n  /**\n   * NOT AVAILABLE in streaming mode.\n   * Safety confirmations currently require non-streaming execute() on CUA agents.\n   */\n  onSafetyConfirmation?: SafetyConfirmationCallbackNotAvailable;\n}\n\n/**\n * Base options for agent execution (without callbacks).\n */\nexport interface AgentExecuteOptionsBase {\n  instruction: string;\n  maxSteps?: number;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  highlightCursor?: boolean;\n  /**\n   * Previous conversation messages to continue from.\n   * Pass the `messages` from a previous AgentResult to continue that conversation.\n   * @experimental\n   */\n  messages?: ModelMessage[];\n  /**\n   * An AbortSignal that can be used to cancel the agent execution.\n   * When aborted, the agent will stop and return a partial result.\n   * @experimental\n   *\n   * @example\n   * ```typescript\n   * const controller = new AbortController();\n   * setTimeout(() => controller.abort(), 30000); // 30 second timeout\n   *\n   * const result = await agent.execute({\n   *   instruction: \"...\",\n   *   signal: controller.signal\n   * });\n   * ```\n   */\n  signal?: AbortSignal;\n  /**\n   * Tools to exclude from this execution.\n   * Pass an array of tool names to prevent the agent from using those tools.\n   *\n   * **Note:** Not supported in CUA mode (`mode: \"cua\"`).\n   *\n   * **Available tools by mode:**\n   *\n   * **DOM mode (default):**\n   * - `act` - Perform semantic actions (click, type, etc.)\n   * - `fillForm` - Fill form fields using DOM selectors\n   * - `ariaTree` - Get accessibility tree of the page\n   * - `extract` - Extract structured data from page\n   * - `goto` - Navigate to a URL\n   * - `scroll` - Scroll using semantic directions (up/down/left/right)\n   * - `keys` - Press keyboard keys\n   * - `navback` - Navigate back in history\n   * - `screenshot` - Take a screenshot\n   * - `think` - Agent reasoning/planning step\n   * - `wait` - Wait for time or condition\n   * - `done` - Mark task as complete\n   * - `search` - Web search (requires useSearch: true and BROWSERBASE_API_KEY)\n   *\n   * **Hybrid mode:**\n   * - `click` - Click at specific coordinates\n   * - `type` - Type text at coordinates\n   * - `dragAndDrop` - Drag from one point to another\n   * - `clickAndHold` - Click and hold at coordinates\n   * - `fillFormVision` - Fill forms using vision/coordinates\n   * - `act` - Perform semantic actions\n   * - `ariaTree` - Get accessibility tree\n   * - `extract` - Extract data from page\n   * - `goto` - Navigate to URL\n   * - `scroll` - Scroll using coordinates\n   * - `keys` - Press keyboard keys\n   * - `navback` - Navigate back\n   * - `screenshot` - Take screenshot\n   * - `think` - Agent reasoning step\n   * - `wait` - Wait for time/condition\n   * - `done` - Mark task complete\n   * - `search` - Web search (requires useSearch: true and BROWSERBASE_API_KEY)\n   *\n   * @experimental\n   * @example\n   * ```typescript\n   * // Exclude screenshot and extract tools\n   * const result = await agent.execute({\n   *   instruction: \"Click the submit button\",\n   *   excludeTools: [\"screenshot\", \"extract\"]\n   * });\n   * ```\n   */\n  excludeTools?: string[];\n  /**\n   * A Zod schema defining custom output data to return when the task completes.\n   * The agent will populate this data in the final done tool call.\n   *\n   * @experimental\n   * @example\n   * ```typescript\n   * const result = await agent.execute({\n   *   instruction: \"Find the cheapest flight from NYC to LA\",\n   *   output: z.object({\n   *     price: z.string().describe(\"The price of the flight\"),\n   *     airline: z.string().describe(\"The airline name\"),\n   *     departureTime: z.string().describe(\"Departure time\"),\n   *   }),\n   * });\n   *\n   * console.log(result.output); // { price: \"$199\", airline: \"Delta\", departureTime: \"8:00 AM\" }\n   * ```\n   */\n  output?: StagehandZodObject;\n  /**\n   * Variables that the agent can use when filling forms or typing text.\n   * The agent will see variable names and descriptions in the system prompt,\n   * and can use them via `%variableName%` syntax in act/type/fillForm tool calls.\n   *\n   * Accepts both simple values and rich objects with descriptions (same type as `act`).\n   *\n   * **Note:** Not supported in CUA mode (`mode: \"cua\"`). Requires `experimental: true`.\n   *\n   * @experimental\n   * @example\n   * ```typescript\n   * // Simple values\n   * variables: { username: \"john@example.com\", password: \"secret123\" }\n   *\n   * // Rich values with descriptions (helps the agent understand context)\n   * variables: {\n   *   username: { value: \"john@example.com\", description: \"The login email\" },\n   *   password: { value: \"secret123\", description: \"The login password\" },\n   * }\n   * ```\n   */\n  variables?: Variables;\n  /**\n   * Timeout in milliseconds for each agent tool call.\n   * If a tool call exceeds this duration, it will be aborted and\n   * reported back to the LLM as a timeout error so it can retry or adjust.\n   * For tools that call v3 methods (act, extract, fillForm, ariaTree), the\n   * timeout is also forwarded to the underlying v3 call for true cancellation.\n   * @default 45000 (45 seconds)\n   */\n  toolTimeout?: number;\n  /**\n   * Enable the web search tool powered by Browserbase Search API.\n   * Requires a valid Browserbase API key (BROWSERBASE_API_KEY).\n   * When set to true, the agent gains access to a `search` tool for web searches.\n   *\n   * @example\n   * ```typescript\n   * const result = await agent.execute({\n   *   instruction: \"Find the latest news about AI\",\n   *   useSearch: true,\n   * });\n   * ```\n   */\n  useSearch?: boolean;\n}\n\n/**\n * Options for non-streaming agent execution.\n * Only accepts AgentExecuteCallbacks (no streaming-specific callbacks like onChunk, onFinish).\n */\nexport interface AgentExecuteOptions extends AgentExecuteOptionsBase {\n  /**\n   * Callbacks for non-streaming agent execution.\n   * For streaming callbacks (onChunk, onFinish, onError, onAbort), use stream: true in AgentConfig.\n   */\n  callbacks?: AgentExecuteCallbacks;\n}\n\n/**\n * Options for streaming agent execution.\n * Accepts AgentStreamCallbacks including onChunk, onFinish, onError, and onAbort.\n */\nexport interface AgentStreamExecuteOptions extends AgentExecuteOptionsBase {\n  /**\n   * Callbacks for streaming agent execution.\n   * Includes streaming-specific callbacks: onChunk, onFinish, onError, onAbort.\n   */\n  callbacks?: AgentStreamCallbacks;\n}\nexport type AgentType =\n  | \"openai\"\n  | \"anthropic\"\n  | \"google\"\n  | \"microsoft\"\n  | \"bedrock\";\n\nexport const AVAILABLE_CUA_MODELS = [\n  \"openai/computer-use-preview\",\n  \"openai/computer-use-preview-2025-03-11\",\n  \"anthropic/claude-opus-4-5-20251101\",\n  \"anthropic/claude-opus-4-6\",\n  \"anthropic/claude-sonnet-4-6\",\n  \"anthropic/claude-haiku-4-5-20251001\",\n  \"anthropic/claude-sonnet-4-20250514\",\n  \"anthropic/claude-sonnet-4-5-20250929\",\n  \"google/gemini-2.5-computer-use-preview-10-2025\",\n  \"google/gemini-3-flash-preview\",\n  \"google/gemini-3-pro-preview\",\n  \"microsoft/fara-7b\",\n] as const;\nexport type AvailableCuaModel = (typeof AVAILABLE_CUA_MODELS)[number];\n\nexport interface AgentExecutionOptions<\n  TOptions extends AgentExecuteOptions = AgentExecuteOptions,\n> {\n  options: TOptions;\n  logger: (message: LogLine) => void;\n  retries?: number;\n}\n\nexport interface AgentHandlerOptions {\n  modelName: string;\n  clientOptions?: ClientOptions;\n  userProvidedInstructions?: string;\n  experimental?: boolean;\n}\n\nexport interface ActionExecutionResult {\n  success: boolean;\n  error?: string;\n  data?: unknown;\n}\n\n/**\n * Represents a safety check that requires user confirmation before proceeding.\n * These are issued by CUA providers (OpenAI, Google) when the agent attempts\n * potentially risky actions.\n */\nexport interface SafetyCheck {\n  /** Unique identifier for this safety check */\n  id: string;\n  /** Code identifying the type of safety concern */\n  code: string;\n  /** Human-readable description of the safety concern */\n  message: string;\n}\n\n/**\n * Response from the user for a safety confirmation request.\n */\nexport interface SafetyConfirmationResponse {\n  /** Whether the user acknowledged/approved the safety checks */\n  acknowledged: boolean;\n}\n\n/**\n * Callback for handling safety confirmation requests.\n * Called when the CUA provider issues safety checks that require user confirmation.\n * The callback should return a promise that resolves when the user has made a decision.\n *\n * @param safetyChecks - Array of safety checks requiring confirmation\n * @returns Promise resolving to the user's response\n *\n * @example\n * ```typescript\n * const agent = stagehand.agent({\n *   mode: \"cua\",\n * });\n * await agent.execute({\n *   instruction: \"...\",\n *   callbacks: {\n *     onSafetyConfirmation: async (checks) => {\n *       console.log(\"Safety checks:\", checks);\n *       const userApproved = await showConfirmationDialog(checks);\n *       return { acknowledged: userApproved };\n *     },\n *   },\n * });\n * ```\n */\nexport type SafetyConfirmationHandler = (\n  safetyChecks: SafetyCheck[],\n) => Promise<SafetyConfirmationResponse>;\n\n// Anthropic types:\n\nexport interface ToolUseItem extends ResponseItem {\n  type: \"tool_use\";\n  id: string; // This is the correct property name from Anthropic's API\n  name: string; // Name of the tool being used\n  input: Record<string, unknown>;\n}\n\nexport interface AnthropicMessage {\n  role: string;\n  content: string | Array<AnthropicContentBlock>;\n}\n\nexport interface AnthropicContentBlock {\n  type: string;\n  [key: string]: unknown;\n}\n\nexport interface AnthropicTextBlock extends AnthropicContentBlock {\n  type: \"text\";\n  text: string;\n}\n\nexport interface AnthropicToolResult {\n  type: \"tool_result\";\n  tool_use_id: string;\n  content: string | Array<AnthropicContentBlock>;\n}\n\n// OpenAI types:\n\nexport interface ResponseItem {\n  type: string;\n  id: string;\n  [key: string]: unknown;\n}\n\nexport interface ComputerCallItem extends ResponseItem {\n  type: \"computer_call\";\n  call_id: string;\n  action: {\n    type: string;\n    [key: string]: unknown;\n  };\n  pending_safety_checks?: Array<{\n    id: string;\n    code: string;\n    message: string;\n  }>;\n}\n\nexport interface FunctionCallItem extends ResponseItem {\n  type: \"function_call\";\n  call_id: string;\n  name: string;\n  arguments: string;\n}\n\nexport type ResponseInputItem =\n  | { role: string; content: string }\n  | {\n      type: \"computer_call_output\";\n      call_id: string;\n      output:\n        | {\n            type: \"input_image\";\n            image_url: string;\n            current_url?: string;\n            error?: string;\n            [key: string]: unknown;\n          }\n        | string;\n      acknowledged_safety_checks?: Array<{\n        id: string;\n        code: string;\n        message: string;\n      }>;\n    }\n  | {\n      type: \"function_call_output\";\n      call_id: string;\n      output: string;\n    };\n\nexport interface AgentInstance {\n  execute: (\n    instructionOrOptions: string | AgentExecuteOptions,\n  ) => Promise<AgentResult>;\n}\n\nexport type AgentProviderType = AgentType;\n\nexport type AgentModelConfig<TModelName extends string = string> = {\n  modelName: TModelName;\n} & Record<string, unknown>;\n\n/**\n * Agent tool mode determines which set of tools are available to the agent.\n * - 'dom': Uses DOM-based tools (act, fillForm) - better for structured page interactions\n * - 'hybrid': Uses coordinate-based tools (click, type, dragAndDrop, etc.) - better for visual/screenshot-based interactions\n * - 'cua': Uses Computer Use Agent (CUA) providers like Anthropic Claude or Google Gemini for screenshot-based automation\n */\nexport type AgentToolMode = \"dom\" | \"hybrid\" | \"cua\";\n\nexport type AgentConfig = {\n  /**\n   * Custom system prompt to provide to the agent. Overrides the default system prompt.\n   */\n  systemPrompt?: string;\n  /**\n   * MCP integrations - Array of Client objects\n   */\n  integrations?: (Client | string)[];\n  /**\n   * Tools passed to the agent client\n   */\n  tools?: ToolSet;\n  /**\n   * @deprecated Use `mode: \"cua\"` instead. This option will be removed in a future version.\n   * Enables Computer Use Agent (CUA) mode.\n   */\n  cua?: boolean;\n  /**\n   * The model to use for agent functionality\n   */\n  model?: string | AgentModelConfig<string>;\n  /**\n   * The model to use for tool execution (observe/act calls within agent tools).\n   * If not specified, inherits from the main model configuration.\n   * Format: \"provider/model\" (e.g., \"openai/gpt-4o-mini\", \"google/gemini-2.0-flash-exp\")\n   */\n  executionModel?: string | AgentModelConfig<string>;\n  /**\n   * Enable streaming mode for the agent.\n   * When true, execute() returns AgentStreamResult with textStream for incremental output.\n   * When false (default), execute() returns AgentResult after completion.\n   */\n  stream?: boolean;\n  /**\n   * Tool mode for the agent. Determines which set of tools are available.\n   * - 'dom' (default): Uses DOM-based tools (act, fillForm) for structured interactions\n   * - 'hybrid': Uses coordinate-based tools (click, type, dragAndDrop, clickAndHold, fillFormVision)\n   *             for visual/screenshot-based interactions\n   * - 'cua': Uses Computer Use Agent (CUA) providers for screenshot-based automation\n   */\n  mode?: AgentToolMode;\n};\n\n/**\n * Agent instance returned when stream: true is set in AgentConfig.\n * execute() returns a streaming result that can be consumed incrementally.\n * Accepts AgentStreamExecuteOptions with streaming-specific callbacks.\n */\nexport interface StreamingAgentInstance {\n  execute: (\n    instructionOrOptions: string | AgentStreamExecuteOptions,\n  ) => Promise<AgentStreamResult>;\n}\n\n/**\n * Agent instance returned when stream is false or not set in AgentConfig.\n * execute() returns a result after the agent completes.\n * Accepts AgentExecuteOptions with non-streaming callbacks only.\n */\nexport interface NonStreamingAgentInstance {\n  execute: (\n    instructionOrOptions: string | AgentExecuteOptions,\n  ) => Promise<AgentResult>;\n}\n\n// =============================================================================\n// Vision Action Tool Result Types\n// =============================================================================\n\n/**\n * Content item type for toModelOutput return values.\n * Used in tool definitions to return text and/or media to the model.\n */\nexport type ModelOutputContentItem =\n  | { type: \"text\"; text: string }\n  | { type: \"media\"; mediaType: string; data: string };\n\nexport interface ClickToolResult {\n  success: boolean;\n  describe?: string;\n  coordinates?: number[];\n  error?: string;\n  screenshotBase64?: string;\n}\n\nexport interface TypeToolResult {\n  success: boolean;\n  describe?: string;\n  text?: string;\n  error?: string;\n  screenshotBase64?: string;\n}\n\nexport interface DragAndDropToolResult {\n  success: boolean;\n  describe?: string;\n  error?: string;\n  screenshotBase64?: string;\n}\n\nexport interface FillFormField {\n  action: string;\n  value: string;\n  coordinates: { x: number; y: number };\n}\n\nexport interface FillFormVisionToolResult {\n  success: boolean;\n  playwrightArguments?: FillFormField[];\n  error?: string;\n  screenshotBase64?: string;\n}\n\nexport interface ScrollToolResult {\n  success: boolean;\n  message: string;\n  scrolledPixels: number;\n  error?: string;\n}\n\nexport interface ScrollVisionToolResult extends ScrollToolResult {\n  screenshotBase64?: string;\n}\n\nexport interface WaitToolResult {\n  success: boolean;\n  waited: number;\n  screenshotBase64?: string;\n  error?: string;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/api.ts",
    "content": "/**\n * Centralized Zod schemas for Stagehand Server API\n *\n * Naming conventions:\n * - `*RequestSchema` - Request body schemas (zod4), `*Request` is the inferred type\n * - `*ResultSchema` - Inner response data (unwrapped), `*Result` is the inferred type\n * - `*ResponseSchema` - Full response with success wrapper: { success: true, data: *Result }, `*Response` is the inferred type\n *\n * All TypeScript types are inferred from the Zod4 *Schemas using z.infer<>\n */\nimport { z } from \"zod/v4\";\nimport type Browserbase from \"@browserbasehq/sdk\";\n\n// =============================================================================\n// Shared Components\n// =============================================================================\n\n/** Browser launch options for local browsers */\nexport const LocalBrowserLaunchOptionsSchema = z\n  .object({\n    args: z.array(z.string()).optional(),\n    executablePath: z.string().optional(),\n    port: z.number().optional(),\n    userDataDir: z.string().optional(),\n    preserveUserDataDir: z.boolean().optional(),\n    headless: z.boolean().optional(),\n    devtools: z.boolean().optional(),\n    chromiumSandbox: z.boolean().optional(),\n    ignoreDefaultArgs: z.union([z.boolean(), z.array(z.string())]).optional(),\n    proxy: z\n      .object({\n        server: z.string(),\n        bypass: z.string().optional(),\n        username: z.string().optional(),\n        password: z.string().optional(),\n      })\n      .optional(),\n    locale: z.string().optional(),\n    viewport: z.object({ width: z.number(), height: z.number() }).optional(),\n    deviceScaleFactor: z.number().optional(),\n    hasTouch: z.boolean().optional(),\n    ignoreHTTPSErrors: z.boolean().optional(),\n    cdpUrl: z.string().optional(),\n    cdpHeaders: z.record(z.string(), z.string()).optional(),\n    connectTimeoutMs: z.number().optional(),\n    downloadsPath: z.string().optional(),\n    acceptDownloads: z.boolean().optional(),\n  })\n  .strict()\n  .meta({ id: \"LocalBrowserLaunchOptions\" });\n\n/** Detailed model configuration object */\nexport const ModelConfigObjectSchema = z\n  .object({\n    provider: z\n      .enum([\"openai\", \"anthropic\", \"google\", \"microsoft\", \"bedrock\"])\n      .optional()\n      .meta({\n        description:\n          \"AI provider for the model (or provide a baseURL endpoint instead)\",\n        example: \"openai\",\n      }),\n    modelName: z.string().meta({\n      description:\n        \"Model name string with provider prefix (e.g., 'openai/gpt-5-nano')\",\n      example: \"openai/gpt-5-nano\",\n    }),\n    apiKey: z.string().optional().meta({\n      description: \"API key for the model provider\",\n      example: \"sk-some-openai-api-key\",\n    }),\n    baseURL: z.string().url().optional().meta({\n      description: \"Base URL for the model provider\",\n      example: \"https://api.openai.com/v1\",\n    }),\n  })\n  .meta({ id: \"ModelConfigObject\" });\n\n/** Model configuration */\nexport const ModelConfigSchema = ModelConfigObjectSchema.meta({\n  id: \"ModelConfig\",\n});\n\n/** Action object returned by observe and used by act */\nexport const ActionSchema = z\n  .object({\n    selector: z.string().meta({\n      description: \"CSS selector or XPath for the element\",\n      example: \"[data-testid='submit-button']\",\n    }),\n    description: z.string().meta({\n      description: \"Human-readable description of the action\",\n      example: \"Click the submit button\",\n    }),\n    backendNodeId: z.number().optional().meta({\n      description: \"Backend node ID for the element\",\n    }),\n    method: z.string().optional().meta({\n      description: \"The method to execute (click, fill, etc.)\",\n      example: \"click\",\n    }),\n    arguments: z\n      .array(z.string())\n      .optional()\n      .meta({\n        description: \"Arguments to pass to the method\",\n        example: [\"Hello World\"],\n      }),\n  })\n  .meta({\n    id: \"Action\",\n    description: \"Action object returned by observe and used by act\",\n  });\n\n/** Session ID path parameter */\nexport const SessionIdParamsSchema = z\n  .object({\n    id: z.string().meta({\n      description: \"Unique session identifier\",\n      example: \"c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\",\n    }),\n  })\n  .strict()\n  .meta({ id: \"SessionIdParams\" });\n\n/** Browser configuration for session start */\nexport const BrowserConfigSchema = z\n  .object({\n    type: z.enum([\"local\", \"browserbase\"]).optional().meta({\n      description: \"Browser type to use\",\n      example: \"local\",\n    }),\n    cdpUrl: z.string().optional().meta({\n      description:\n        \"Chrome DevTools Protocol URL for connecting to existing browser\",\n      example: \"ws://localhost:9222\",\n    }),\n    launchOptions: LocalBrowserLaunchOptionsSchema.optional(),\n  })\n  .meta({ id: \"BrowserConfig\" });\n\n// =============================================================================\n// Request Headers (operational only - auth headers are in security schemes)\n// =============================================================================\n\n/** Operational headers for all session requests (auth handled via security schemes) */\nexport const SessionHeadersSchema = z\n  .object({\n    \"x-stream-response\": z.enum([\"true\", \"false\"]).optional().meta({\n      description: \"Whether to stream the response via SSE\",\n      example: \"true\",\n    }),\n  })\n  .meta({ id: \"SessionHeaders\" });\n\n// =============================================================================\n// Response Wrapper Helper\n// =============================================================================\n\n/** Wraps a result schema in standard success response format */\nconst wrapResponse = <T extends z.ZodTypeAny>(resultSchema: T, name: string) =>\n  z\n    .object({\n      success: z.boolean().meta({\n        description: \"Indicates whether the request was successful\",\n      }),\n      data: resultSchema,\n    })\n    .meta({ id: name });\n\n/** Standard error response */\nexport const ErrorResponseSchema = z\n  .object({\n    success: z.literal(false),\n    error: z.string(),\n    code: z.string().optional(),\n  })\n  .strict()\n  .meta({ id: \"ErrorResponse\" });\n\n// =============================================================================\n// Browserbase Session Create Params  (zod+hints duplicated version of Browserbase.Sessions.SessionCreateParams)\n// =============================================================================\n\n/** Browserbase viewport configuration */\nexport const BrowserbaseViewportSchema = z\n  .object({\n    width: z.number().optional(),\n    height: z.number().optional(),\n  })\n  .meta({ id: \"BrowserbaseViewport\" });\n\n/** Browserbase fingerprint screen configuration */\nexport const BrowserbaseFingerprintScreenSchema = z\n  .object({\n    maxHeight: z.number().optional(),\n    maxWidth: z.number().optional(),\n    minHeight: z.number().optional(),\n    minWidth: z.number().optional(),\n  })\n  .meta({ id: \"BrowserbaseFingerprintScreen\" });\n\n/** Browserbase fingerprint configuration for stealth mode */\nexport const BrowserbaseFingerprintSchema = z\n  .object({\n    browsers: z\n      .array(z.enum([\"chrome\", \"edge\", \"firefox\", \"safari\"]))\n      .optional(),\n    devices: z.array(z.enum([\"desktop\", \"mobile\"])).optional(),\n    httpVersion: z.enum([\"1\", \"2\"]).optional(),\n    locales: z.array(z.string()).optional(),\n    operatingSystems: z\n      .array(z.enum([\"android\", \"ios\", \"linux\", \"macos\", \"windows\"]))\n      .optional(),\n    screen: BrowserbaseFingerprintScreenSchema.optional(),\n  })\n  .meta({ id: \"BrowserbaseFingerprint\" });\n\n/** Browserbase context configuration for session persistence */\nexport const BrowserbaseContextSchema = z\n  .object({\n    id: z.string(),\n    persist: z.boolean().optional(),\n  })\n  .meta({ id: \"BrowserbaseContext\" });\n\n/** Browserbase browser settings for session creation */\nexport const BrowserbaseBrowserSettingsSchema = z\n  .object({\n    advancedStealth: z.boolean().optional(),\n    blockAds: z.boolean().optional(),\n    context: BrowserbaseContextSchema.optional(),\n    extensionId: z.string().optional(),\n    fingerprint: BrowserbaseFingerprintSchema.optional(),\n    logSession: z.boolean().optional(),\n    recordSession: z.boolean().optional(),\n    solveCaptchas: z.boolean().optional(),\n    viewport: BrowserbaseViewportSchema.optional(),\n  })\n  .meta({ id: \"BrowserbaseBrowserSettings\" });\n\n/** Browserbase managed proxy geolocation configuration */\nexport const BrowserbaseProxyGeolocationSchema = z\n  .object({\n    country: z.string(),\n    city: z.string().optional(),\n    state: z.string().optional(),\n  })\n  .meta({ id: \"BrowserbaseProxyGeolocation\" });\n\n/** Browserbase managed proxy configuration */\nexport const BrowserbaseProxyConfigSchema = z\n  .object({\n    type: z.literal(\"browserbase\"),\n    domainPattern: z.string().optional(),\n    geolocation: BrowserbaseProxyGeolocationSchema.optional(),\n  })\n  .meta({ id: \"BrowserbaseProxyConfig\" });\n\n/** External proxy configuration */\nexport const ExternalProxyConfigSchema = z\n  .object({\n    type: z.literal(\"external\"),\n    server: z.string(),\n    domainPattern: z.string().optional(),\n    username: z.string().optional(),\n    password: z.string().optional(),\n  })\n  .meta({ id: \"ExternalProxyConfig\" });\n\n/** Union of proxy configuration types */\nexport const ProxyConfigSchema = z\n  .discriminatedUnion(\"type\", [\n    BrowserbaseProxyConfigSchema,\n    ExternalProxyConfigSchema,\n  ])\n  .meta({ id: \"ProxyConfig\" });\n\n/** Browserbase region identifier for multi-region support */\nexport const BrowserbaseRegionSchema = z\n  .enum([\"us-west-2\", \"us-east-1\", \"eu-central-1\", \"ap-southeast-1\"])\n  .meta({ id: \"BrowserbaseRegion\" });\n\n/** Browserbase session creation parameters */\nexport const BrowserbaseSessionCreateParamsSchema = z\n  .object({\n    projectId: z.string().optional(),\n    browserSettings: BrowserbaseBrowserSettingsSchema.optional(),\n    extensionId: z.string().optional(),\n    keepAlive: z.boolean().optional(),\n    proxies: z.union([z.boolean(), z.array(ProxyConfigSchema)]).optional(),\n    region: BrowserbaseRegionSchema.optional(),\n    timeout: z.number().optional(),\n    userMetadata: z.record(z.string(), z.unknown()).optional(),\n  })\n  .meta({ id: \"BrowserbaseSessionCreateParams\" });\n\n// =============================================================================\n// Session Start\n// =============================================================================\n\nexport const SessionStartRequestSchema = z\n  .object({\n    modelName: z.string().meta({\n      description: \"Model name to use for AI operations\",\n      example: \"openai/gpt-4o\",\n    }),\n    domSettleTimeoutMs: z.number().optional().meta({\n      description: \"Timeout in ms to wait for DOM to settle\",\n      example: 5000,\n    }),\n    verbose: z\n      .union([z.literal(0), z.literal(1), z.literal(2)])\n      .optional()\n      .meta({\n        description: \"Logging verbosity level (0=quiet, 1=normal, 2=debug)\",\n        example: 1,\n        override: ({ jsonSchema }: { jsonSchema: Record<string, unknown> }) => {\n          delete jsonSchema.anyOf;\n          delete jsonSchema.allOf;\n          delete jsonSchema.oneOf;\n          jsonSchema.type = \"number\";\n          jsonSchema.enum = [0, 1, 2];\n        },\n      }),\n    systemPrompt: z.string().optional().meta({\n      description: \"Custom system prompt for AI operations\",\n    }),\n    browserbaseSessionCreateParams:\n      BrowserbaseSessionCreateParamsSchema.optional(),\n    browser: BrowserConfigSchema.optional(),\n    selfHeal: z.boolean().optional().meta({\n      description: \"Enable self-healing for failed actions\",\n      example: true,\n    }),\n    browserbaseSessionID: z.string().optional().meta({\n      description: \"Existing Browserbase session ID to resume\",\n    }),\n    // experimental is a V3 field but doesn't need to go over the wire - included because wire type imports options type\n    experimental: z.boolean().optional(),\n    // V2 compatibility fields - only included because the server imports this type and supports V2\n    // should never be used in v3 clients or v3-only server implementations\n    waitForCaptchaSolves: z.boolean().optional().meta({\n      description: \"Wait for captcha solves (deprecated, v2 only)\",\n    }),\n    actTimeoutMs: z.number().optional().meta({\n      description: \"Timeout in ms for act operations (deprecated, v2 only)\",\n    }),\n  })\n  .meta({ id: \"SessionStartRequest\" });\n\nexport const SessionStartResultSchema = z\n  .object({\n    sessionId: z.string().meta({\n      description: \"Unique Browserbase session identifier\",\n      example: \"c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\",\n    }),\n    cdpUrl: z.string().nullish().meta({\n      description:\n        \"CDP WebSocket URL for connecting to the Browserbase cloud browser (present when available)\",\n      example: \"wss://connect.browserbase.com/?signingKey=abc123\",\n    }),\n    available: z.boolean(),\n  })\n  .meta({ id: \"SessionStartResult\" });\n\nexport const SessionStartResponseSchema = wrapResponse(\n  SessionStartResultSchema,\n  \"SessionStartResponse\",\n);\n\n// =============================================================================\n// Session End\n// =============================================================================\n\n/** Session end request - no request body. */\nexport const SessionEndRequestSchema = z\n  .object({})\n  .strict()\n  .optional()\n  .meta({ id: \"SessionEndRequest\" });\n\nexport const SessionEndResultSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"SessionEndResult\" });\n\n/** Session end response - just success flag, no data wrapper */\nexport const SessionEndResponseSchema = z\n  .object({\n    success: z.boolean().meta({\n      description: \"Indicates whether the request was successful\",\n    }),\n  })\n  .strict()\n  .meta({ id: \"SessionEndResponse\" });\n\n// =============================================================================\n// Act\n// =============================================================================\n\nexport const ActOptionsSchema = z\n  .object({\n    model: z.union([ModelConfigSchema, z.string()]).optional().meta({\n      description:\n        \"Model configuration object or model name string (e.g., 'openai/gpt-5-nano')\",\n    }),\n    variables: z\n      .record(z.string(), z.string())\n      .optional()\n      .meta({\n        description: \"Variables to substitute in the action instruction\",\n        example: { username: \"john_doe\" },\n      }),\n    timeout: z.number().optional().meta({\n      description: \"Timeout in ms for the action\",\n      example: 30000,\n    }),\n  })\n  .optional()\n  .meta({ id: \"ActOptions\" });\n\nexport const ActRequestSchema = z\n  .object({\n    input: z.string().or(ActionSchema).meta({\n      description: \"Natural language instruction or Action object\",\n      example: \"Click the login button\",\n    }),\n    options: ActOptionsSchema,\n    frameId: z.string().nullish().meta({\n      description: \"Target frame ID for the action\",\n    }),\n    streamResponse: z.boolean().optional().meta({\n      description: \"Whether to stream the response via SSE\",\n      example: true,\n    }),\n  })\n  .meta({ id: \"ActRequest\" });\n\n/** Inner act result data */\nexport const ActResultDataSchema = z\n  .object({\n    success: z.boolean().meta({\n      description: \"Whether the action completed successfully\",\n      example: true,\n    }),\n    message: z.string().meta({\n      description: \"Human-readable result message\",\n      example: \"Successfully clicked the login button\",\n    }),\n    actionDescription: z.string().meta({\n      description: \"Description of the action that was performed\",\n      example: \"Clicked button with text 'Login'\",\n    }),\n    actions: z.array(ActionSchema).meta({\n      description: \"List of actions that were executed\",\n    }),\n  })\n  .meta({ id: \"ActResultData\" });\n\nexport const ActResultSchema = z\n  .object({\n    result: ActResultDataSchema,\n    actionId: z.string().optional().meta({\n      description: \"Action ID for tracking\",\n    }),\n  })\n  .meta({ id: \"ActResult\" });\n\nexport const ActResponseSchema = wrapResponse(ActResultSchema, \"ActResponse\");\n\n// =============================================================================\n// Extract\n// =============================================================================\n\nexport const ExtractOptionsSchema = z\n  .object({\n    model: z.union([ModelConfigSchema, z.string()]).optional().meta({\n      description:\n        \"Model configuration object or model name string (e.g., 'openai/gpt-5-nano')\",\n    }),\n    timeout: z.number().optional().meta({\n      description: \"Timeout in ms for the extraction\",\n      example: 30000,\n    }),\n    selector: z.string().optional().meta({\n      description: \"CSS selector to scope extraction to a specific element\",\n      example: \"#main-content\",\n    }),\n  })\n  .optional()\n  .meta({ id: \"ExtractOptions\" });\n\nexport const ExtractRequestSchema = z\n  .object({\n    instruction: z.string().optional().meta({\n      description: \"Natural language instruction for what to extract\",\n      example: \"Extract all product names and prices from the page\",\n    }),\n    schema: z.record(z.string(), z.unknown()).optional().meta({\n      description: \"JSON Schema defining the structure of data to extract\",\n    }),\n    options: ExtractOptionsSchema,\n    frameId: z.string().nullish().meta({\n      description: \"Target frame ID for the extraction\",\n    }),\n    streamResponse: z.boolean().optional().meta({\n      description: \"Whether to stream the response via SSE\",\n      example: true,\n    }),\n  })\n  .meta({ id: \"ExtractRequest\" });\n\nexport const ExtractResultSchema = z\n  .object({\n    result: z.unknown().meta({\n      description: \"Extracted data matching the requested schema\",\n      override: ({ jsonSchema }: { jsonSchema: Record<string, unknown> }) => {\n        jsonSchema[\"x-stainless-any\"] = true;\n      },\n    }),\n    actionId: z.string().optional().meta({\n      description: \"Action ID for tracking\",\n    }),\n  })\n  .meta({ id: \"ExtractResult\" });\n\nexport const ExtractResponseSchema = wrapResponse(\n  ExtractResultSchema,\n  \"ExtractResponse\",\n);\n\n// =============================================================================\n// Observe\n// =============================================================================\n\nexport const ObserveOptionsSchema = z\n  .object({\n    model: z.union([ModelConfigSchema, z.string()]).optional().meta({\n      description:\n        \"Model configuration object or model name string (e.g., 'openai/gpt-5-nano')\",\n    }),\n    timeout: z.number().optional().meta({\n      description: \"Timeout in ms for the observation\",\n      example: 30000,\n    }),\n    selector: z.string().optional().meta({\n      description: \"CSS selector to scope observation to a specific element\",\n      example: \"nav\",\n    }),\n  })\n  .optional()\n  .meta({ id: \"ObserveOptions\" });\n\nexport const ObserveRequestSchema = z\n  .object({\n    instruction: z.string().optional().meta({\n      description: \"Natural language instruction for what actions to find\",\n      example: \"Find all clickable navigation links\",\n    }),\n    options: ObserveOptionsSchema,\n    frameId: z.string().nullish().meta({\n      description: \"Target frame ID for the observation\",\n    }),\n    streamResponse: z.boolean().optional().meta({\n      description: \"Whether to stream the response via SSE\",\n      example: true,\n    }),\n  })\n  .meta({ id: \"ObserveRequest\" });\n\nexport const ObserveResultSchema = z\n  .object({\n    result: z.array(ActionSchema),\n    actionId: z.string().optional().meta({\n      description: \"Action ID for tracking\",\n    }),\n  })\n  .meta({ id: \"ObserveResult\" });\n\nexport const ObserveResponseSchema = wrapResponse(\n  ObserveResultSchema,\n  \"ObserveResponse\",\n);\n\n// =============================================================================\n// Agent Execute\n// =============================================================================\n\nexport const AgentConfigSchema = z\n  .object({\n    provider: z // cloud accepts provider: at the top level for legacy reasons, in the future we should remove it\n      .enum([\"openai\", \"anthropic\", \"google\", \"microsoft\", \"bedrock\"])\n      .optional()\n      .meta({\n        description:\n          \"AI provider for the agent (legacy, use model: openai/gpt-5-nano instead)\",\n        example: \"openai\",\n      }),\n    model: z.union([ModelConfigSchema, z.string()]).optional().meta({\n      description:\n        \"Model configuration object or model name string (e.g., 'openai/gpt-5-nano')\",\n    }),\n    systemPrompt: z.string().optional().meta({\n      description: \"Custom system prompt for the agent\",\n    }),\n    cua: z.boolean().optional().meta({\n      description:\n        \"Deprecated. Use mode: 'cua' instead. If both are provided, mode takes precedence.\",\n      example: true,\n    }),\n    mode: z.enum([\"dom\", \"hybrid\", \"cua\"]).optional().meta({\n      description:\n        \"Tool mode for the agent (dom, hybrid, cua). If set, overrides cua.\",\n      example: \"cua\",\n    }),\n    executionModel: z.union([ModelConfigSchema, z.string()]).optional().meta({\n      description:\n        \"Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool execution (observe/act calls within agent tools). If not specified, inherits from the main model configuration.\",\n    }),\n  })\n  .meta({ id: \"AgentConfig\" });\n\n/** Action taken by the agent during execution */\nexport const AgentActionSchema = z\n  .object({\n    type: z.string().meta({\n      description: \"Type of action taken\",\n      example: \"click\",\n    }),\n    reasoning: z.string().optional().meta({\n      description: \"Agent's reasoning for taking this action\",\n    }),\n    taskCompleted: z.boolean().optional(),\n    action: z.string().optional(),\n    timeMs: z.number().optional().meta({\n      description: \"Time taken for this action in ms\",\n    }),\n    pageText: z.string().optional(),\n    pageUrl: z.string().optional(),\n    instruction: z.string().optional(),\n  })\n  .passthrough()\n  .meta({ id: \"AgentAction\" });\n\n/** Token usage statistics for agent execution */\nexport const AgentUsageSchema = z\n  .object({\n    input_tokens: z.number().meta({ example: 1500 }),\n    output_tokens: z.number().meta({ example: 250 }),\n    reasoning_tokens: z.number().optional(),\n    cached_input_tokens: z.number().optional(),\n    inference_time_ms: z.number().meta({ example: 2500 }),\n  })\n  .meta({ id: \"AgentUsage\" });\n\n/** Result data from agent execution */\nexport const AgentResultDataSchema = z\n  .object({\n    success: z.boolean().meta({\n      description: \"Whether the agent completed successfully\",\n      example: true,\n    }),\n    message: z.string().meta({\n      description: \"Summary of what the agent accomplished\",\n      example: \"Successfully logged in and navigated to dashboard\",\n    }),\n    actions: z.array(AgentActionSchema),\n    completed: z.boolean().meta({\n      description: \"Whether the agent finished its task\",\n      example: true,\n    }),\n    metadata: z.record(z.string(), z.unknown()).optional(),\n    usage: AgentUsageSchema.optional(),\n  })\n  .meta({ id: \"AgentResultData\" });\n\nexport const AgentCacheEntrySchema = z\n  .object({\n    cacheKey: z.string().meta({\n      description:\n        \"Opaque cache identifier computed from instruction, URL, options, and config\",\n    }),\n    entry: z.unknown().meta({\n      description: \"Serialized cache entry that can be written to disk\",\n    }),\n  })\n  .meta({ id: \"AgentCacheEntry\" });\n\nexport const AgentExecuteOptionsSchema = z\n  .object({\n    instruction: z.string().meta({\n      description: \"Natural language instruction for the agent\",\n      example:\n        \"Log in with username 'demo' and password 'test123', then navigate to settings\",\n    }),\n    maxSteps: z.number().optional().meta({\n      description: \"Maximum number of steps the agent can take\",\n      example: 20,\n    }),\n    highlightCursor: z.boolean().optional().meta({\n      description: \"Whether to visually highlight the cursor during execution\",\n      example: true,\n    }),\n    useSearch: z.boolean().optional().meta({\n      description:\n        \"Whether to enable the web search tool powered by Browserbase Search API\",\n      example: true,\n    }),\n    toolTimeout: z.number().optional().meta({\n      description: \"Timeout in milliseconds for each agent tool call\",\n      example: 30000,\n    }),\n  })\n  .meta({ id: \"AgentExecuteOptions\" });\n\nexport const AgentExecuteRequestSchema = z\n  .object({\n    agentConfig: AgentConfigSchema,\n    executeOptions: AgentExecuteOptionsSchema,\n    frameId: z.string().nullish().meta({\n      description: \"Target frame ID for the agent\",\n    }),\n    streamResponse: z.boolean().optional().meta({\n      description: \"Whether to stream the response via SSE\",\n      example: true,\n    }),\n    shouldCache: z.boolean().optional().meta({\n      description:\n        \"If true, the server captures a cache entry and returns it to the client\",\n    }),\n  })\n  .meta({ id: \"AgentExecuteRequest\" });\n\nexport const AgentExecuteResultSchema = z\n  .object({\n    result: AgentResultDataSchema,\n    cacheEntry: AgentCacheEntrySchema.optional(),\n  })\n  .meta({ id: \"AgentExecuteResult\" });\n\nexport const AgentExecuteResponseSchema = wrapResponse(\n  AgentExecuteResultSchema,\n  \"AgentExecuteResponse\",\n);\n\n// =============================================================================\n// Navigate\n// =============================================================================\n\nexport const NavigateOptionsSchema = z\n  .object({\n    referer: z.string().optional().meta({\n      description: \"Referer header to send with the request\",\n    }),\n    timeout: z.number().optional().meta({\n      description: \"Timeout in ms for the navigation\",\n      example: 30000,\n    }),\n    waitUntil: z\n      .enum([\"load\", \"domcontentloaded\", \"networkidle\"])\n      .optional()\n      .meta({\n        description: \"When to consider navigation complete\",\n        example: \"networkidle\",\n      }),\n  })\n  .optional()\n  .meta({ id: \"NavigateOptions\" });\n\nexport const NavigateRequestSchema = z\n  .object({\n    url: z.string().meta({\n      description: \"URL to navigate to\",\n      example: \"https://example.com\",\n    }),\n    options: NavigateOptionsSchema,\n    frameId: z.string().nullish().meta({\n      description: \"Target frame ID for the navigation\",\n    }),\n    streamResponse: z.boolean().optional().meta({\n      description: \"Whether to stream the response via SSE\",\n      example: true,\n    }),\n  })\n  .meta({ id: \"NavigateRequest\" });\n\nexport const NavigateResultSchema = z\n  .object({\n    // SerializableResponse from types/private/api.ts - no Zod schema available\n    // as it wraps complex devtools-protocol types (Protocol.Network.Response)\n    result: z\n      .unknown()\n      .nullable()\n      .meta({\n        description: \"Navigation response (Playwright Response object or null)\",\n        override: ({ jsonSchema }: { jsonSchema: Record<string, unknown> }) => {\n          jsonSchema[\"x-stainless-any\"] = true;\n        },\n      }),\n    actionId: z.string().optional().meta({\n      description: \"Action ID for tracking\",\n    }),\n  })\n  .meta({ id: \"NavigateResult\" });\n\nexport const NavigateResponseSchema = wrapResponse(\n  NavigateResultSchema,\n  \"NavigateResponse\",\n);\n\n// =============================================================================\n// Replay Metrics\n// =============================================================================\n\n/** Token usage for a single action */\nexport const TokenUsageSchema = z\n  .object({\n    inputTokens: z.number().optional(),\n    outputTokens: z.number().optional(),\n    timeMs: z.number().optional(),\n    cost: z.number().optional(),\n  })\n  .meta({ id: \"TokenUsage\" });\n\n/** Action entry in replay metrics */\nexport const ReplayActionSchema = z\n  .object({\n    method: z.string(),\n    parameters: z.record(z.string(), z.unknown()),\n    result: z.record(z.string(), z.unknown()),\n    timestamp: z.number(),\n    endTime: z.number().optional(),\n    tokenUsage: TokenUsageSchema.optional(),\n  })\n  .meta({ id: \"ReplayAction\" });\n\n/** Page entry in replay metrics */\nexport const ReplayPageSchema = z\n  .object({\n    url: z.string(),\n    timestamp: z.number(),\n    duration: z.number(),\n    actions: z.array(ReplayActionSchema),\n  })\n  .meta({ id: \"ReplayPage\" });\n\n/** Inner result data for replay */\nexport const ReplayResultSchema = z\n  .object({\n    pages: z.array(ReplayPageSchema),\n    clientLanguage: z.string().optional(),\n  })\n  .meta({ id: \"ReplayResult\" });\n\nexport const ReplayResponseSchema = wrapResponse(\n  ReplayResultSchema,\n  \"ReplayResponse\",\n);\n\n// =============================================================================\n// SSE Stream Events\n// =============================================================================\n// These schemas define the Server-Sent Events format for streaming responses.\n// Streaming is enabled by setting the `x-stream-response: true` header.\n\n/** Status values for SSE stream events */\nexport const StreamEventStatusSchema = z\n  .enum([\"starting\", \"connected\", \"running\", \"finished\", \"error\"])\n  .meta({\n    id: \"StreamEventStatus\",\n    description: \"Current status of the streaming operation\",\n  });\n\n/** Type discriminator for SSE stream events */\nexport const StreamEventTypeSchema = z.enum([\"system\", \"log\"]).meta({\n  id: \"StreamEventType\",\n  description: \"Type of stream event - system events or log messages\",\n});\n\n/** Data payload for system stream events */\nexport const StreamEventSystemDataSchema = z\n  .object({\n    status: StreamEventStatusSchema,\n    result: z\n      .unknown()\n      .optional()\n      .meta({\n        description: \"Operation result (present when status is 'finished')\",\n        override: ({ jsonSchema }: { jsonSchema: Record<string, unknown> }) => {\n          jsonSchema[\"x-stainless-any\"] = true;\n        },\n      }),\n    error: z.string().optional().meta({\n      description: \"Error message (present when status is 'error')\",\n    }),\n  })\n  .meta({ id: \"StreamEventSystemData\" });\n\n/** Data payload for log stream events */\nexport const StreamEventLogDataSchema = z\n  .object({\n    status: z.literal(\"running\"),\n    message: z.string().meta({\n      description: \"Log message from the operation\",\n    }),\n  })\n  .meta({ id: \"StreamEventLogData\" });\n\n/**\n * SSE stream event sent during streaming responses.\n *\n * IMPORTANT: Key ordering matters for Stainless SDK generation.\n * The `data` field MUST be serialized first, with `status` as the first key within it.\n * This allows Stainless to use `data_starts_with: '{\"data\":{\"status\":\"finished\"'` for event handling.\n *\n * Expected serialization order: {\"data\":{\"status\":...},\"type\":...,\"id\":...}\n */\nexport const StreamEventSchema = z\n  .object({\n    data: z.union([StreamEventSystemDataSchema, StreamEventLogDataSchema]),\n    type: StreamEventTypeSchema,\n    id: z.string().uuid().meta({\n      description: \"Unique identifier for this event\",\n      example: \"c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\",\n    }),\n  })\n  .meta({\n    id: \"StreamEvent\",\n    description:\n      \"Server-Sent Event emitted during streaming responses. Events are sent as `data: <JSON>\\\\n\\\\n`. Key order: data (with status first), type, id.\",\n  });\n\n// =============================================================================\n// OpenAPI Components\n// =============================================================================\n// These objects are exported for use in gen-openapi.ts to configure the spec.\n\n/** OpenAPI security schemes for authentication */\nexport const openApiSecuritySchemes = {\n  BrowserbaseApiKey: {\n    type: \"apiKey\",\n    in: \"header\",\n    name: \"x-bb-api-key\",\n    description: \"Browserbase API key for authentication\",\n  },\n  BrowserbaseProjectId: {\n    type: \"apiKey\",\n    in: \"header\",\n    name: \"x-bb-project-id\",\n    description: \"Browserbase project ID\",\n  },\n  ModelApiKey: {\n    type: \"apiKey\",\n    in: \"header\",\n    name: \"x-model-api-key\",\n    description: \"API key for the AI model provider (OpenAI, Anthropic, etc.)\",\n  },\n} as const;\n\n/** OpenAPI links for session operations (used in SessionStart response) */\nexport const openApiLinks = {\n  SessionAct: {\n    operationId: \"SessionAct\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"Perform an action on the session\",\n  },\n  SessionExtract: {\n    operationId: \"SessionExtract\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"Extract data from the session\",\n  },\n  SessionObserve: {\n    operationId: \"SessionObserve\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"Observe available actions on the session\",\n  },\n  SessionNavigate: {\n    operationId: \"SessionNavigate\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"Navigate to a URL in the session\",\n  },\n  SessionAgentExecute: {\n    operationId: \"SessionAgentExecute\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"Execute an agent on the session\",\n  },\n  SessionReplay: {\n    operationId: \"SessionReplay\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"Replay session metrics\",\n  },\n  SessionEnd: {\n    operationId: \"SessionEnd\",\n    parameters: { id: \"$response.body#/data/sessionId\" },\n    description: \"End the session and release resources\",\n  },\n} as const;\n\n/** OpenAPI operation metadata for each endpoint */\nexport const Operations = {\n  SessionStart: {\n    operationId: \"SessionStart\",\n    summary: \"Start a new browser session\",\n    description:\n      \"Creates a new browser session with the specified configuration. Returns a session ID used for all subsequent operations.\",\n  },\n  SessionEnd: {\n    operationId: \"SessionEnd\",\n    summary: \"End a browser session\",\n    description:\n      \"Terminates the browser session and releases all associated resources.\",\n  },\n  SessionAct: {\n    operationId: \"SessionAct\",\n    summary: \"Perform an action\",\n    description:\n      \"Executes a browser action using natural language instructions or a predefined Action object.\",\n  },\n  SessionExtract: {\n    operationId: \"SessionExtract\",\n    summary: \"Extract data from the page\",\n    description:\n      \"Extracts structured data from the current page using AI-powered analysis.\",\n  },\n  SessionObserve: {\n    operationId: \"SessionObserve\",\n    summary: \"Observe available actions\",\n    description:\n      \"Identifies and returns available actions on the current page that match the given instruction.\",\n  },\n  SessionNavigate: {\n    operationId: \"SessionNavigate\",\n    summary: \"Navigate to a URL\",\n    description: \"Navigates the browser to the specified URL.\",\n  },\n  SessionAgentExecute: {\n    operationId: \"SessionAgentExecute\",\n    summary: \"Execute an AI agent\",\n    description:\n      \"Runs an autonomous AI agent that can perform complex multi-step browser tasks.\",\n  },\n  SessionReplay: {\n    operationId: \"SessionReplay\",\n    summary: \"Replay session metrics\",\n    description: \"Retrieves replay metrics for a session.\",\n  },\n} as const;\n\n// =============================================================================\n// Type Exports (inferred from schemas)\n// =============================================================================\n\n// Shared types\nexport type Action = z.infer<typeof ActionSchema>;\nexport type ModelConfig = z.infer<typeof ModelConfigSchema>;\nexport type BrowserConfig = z.infer<typeof BrowserConfigSchema>;\nexport type SessionIdParams = z.infer<typeof SessionIdParamsSchema>;\n\n// Header types\nexport type SessionHeaders = z.infer<typeof SessionHeadersSchema>;\n\n// Browserbase types\nexport type BrowserbaseViewport = z.infer<typeof BrowserbaseViewportSchema>;\nexport type BrowserbaseFingerprintScreen = z.infer<\n  typeof BrowserbaseFingerprintScreenSchema\n>;\nexport type BrowserbaseFingerprint = z.infer<\n  typeof BrowserbaseFingerprintSchema\n>;\nexport type BrowserbaseContext = z.infer<typeof BrowserbaseContextSchema>;\nexport type BrowserbaseBrowserSettings = z.infer<\n  typeof BrowserbaseBrowserSettingsSchema\n>;\nexport type BrowserbaseProxyGeolocation = z.infer<\n  typeof BrowserbaseProxyGeolocationSchema\n>;\nexport type BrowserbaseProxyConfig = z.infer<\n  typeof BrowserbaseProxyConfigSchema\n>;\nexport type ExternalProxyConfig = z.infer<typeof ExternalProxyConfigSchema>;\nexport type BrowserbaseRegion = z.infer<typeof BrowserbaseRegionSchema>;\nexport type BrowserbaseSessionCreateParams = z.infer<\n  typeof BrowserbaseSessionCreateParamsSchema\n>;\n\n// Type check: ensure our schema-derived type is assignable to the SDK type\n// This will cause a compile error if our schema drifts from the SDK\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype _BrowserbaseSessionCreateParamsCheck =\n  BrowserbaseSessionCreateParams extends Browserbase.Sessions.SessionCreateParams\n    ? true\n    : never;\n\n// /sessions/start\nexport type SessionStartRequest = z.infer<typeof SessionStartRequestSchema>;\nexport type SessionStartResult = z.infer<typeof SessionStartResultSchema>;\nexport type SessionStartResponse = z.infer<typeof SessionStartResponseSchema>;\n\n// /sessions/{id}/end\nexport type SessionEndResult = z.infer<typeof SessionEndResultSchema>;\nexport type SessionEndResponse = z.infer<typeof SessionEndResponseSchema>;\n\n// /sessions/{id}/act\nexport type ActRequest = z.infer<typeof ActRequestSchema>;\nexport type ActResultData = z.infer<typeof ActResultDataSchema>;\nexport type ActResult = z.infer<typeof ActResultSchema>;\nexport type ActResponse = z.infer<typeof ActResponseSchema>;\n\n// /sessions/{id}/extract\nexport type ExtractRequest = z.infer<typeof ExtractRequestSchema>;\nexport type ExtractResult = z.infer<typeof ExtractResultSchema>;\nexport type ExtractResponse = z.infer<typeof ExtractResponseSchema>;\n\n// /sessions/{id}/observe\nexport type ObserveRequest = z.infer<typeof ObserveRequestSchema>;\nexport type ObserveResult = z.infer<typeof ObserveResultSchema>;\nexport type ObserveResponse = z.infer<typeof ObserveResponseSchema>;\n\n// /sessions/{id}/agentExecute\nexport type AgentAction = z.infer<typeof AgentActionSchema>;\nexport type AgentUsage = z.infer<typeof AgentUsageSchema>;\nexport type AgentResultData = z.infer<typeof AgentResultDataSchema>;\nexport type AgentExecuteRequest = z.infer<typeof AgentExecuteRequestSchema>;\nexport type AgentExecuteResult = z.infer<typeof AgentExecuteResultSchema>;\nexport type AgentExecuteResponse = z.infer<typeof AgentExecuteResponseSchema>;\n\n// /sessions/{id}/navigate\nexport type NavigateRequest = z.infer<typeof NavigateRequestSchema>;\nexport type NavigateResult = z.infer<typeof NavigateResultSchema>;\nexport type NavigateResponse = z.infer<typeof NavigateResponseSchema>;\n\n// /sessions/{id}/replay\nexport type TokenUsage = z.infer<typeof TokenUsageSchema>;\nexport type ReplayAction = z.infer<typeof ReplayActionSchema>;\nexport type ReplayPage = z.infer<typeof ReplayPageSchema>;\nexport type ReplayResult = z.infer<typeof ReplayResultSchema>;\nexport type ReplayResponse = z.infer<typeof ReplayResponseSchema>;\n\n// SSE Stream Events\nexport type StreamEventStatus = z.infer<typeof StreamEventStatusSchema>;\nexport type StreamEventType = z.infer<typeof StreamEventTypeSchema>;\nexport type StreamEventSystemData = z.infer<typeof StreamEventSystemDataSchema>;\nexport type StreamEventLogData = z.infer<typeof StreamEventLogDataSchema>;\nexport type StreamEvent = z.infer<typeof StreamEventSchema>;\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/apiErrors.ts",
    "content": "export class StagehandAPIError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = this.constructor.name;\n  }\n}\n\nexport class StagehandAPIUnauthorizedError extends StagehandAPIError {\n  constructor(message?: string) {\n    super(message || \"Unauthorized request\");\n  }\n}\n\nexport class StagehandHttpError extends StagehandAPIError {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\nexport class StagehandServerError extends StagehandAPIError {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\nexport class StagehandResponseBodyError extends StagehandAPIError {\n  constructor() {\n    super(\"Response body is null\");\n  }\n}\n\nexport class StagehandResponseParseError extends StagehandAPIError {\n  constructor(message: string) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/context.ts",
    "content": "/** A cookie as returned by the browser. */\nexport interface Cookie {\n  name: string;\n  value: string;\n  domain: string;\n  path: string;\n  /** Unix time in seconds. -1 means session cookie. */\n  expires: number;\n  httpOnly: boolean;\n  secure: boolean;\n  sameSite: \"Strict\" | \"Lax\" | \"None\";\n}\n\n/** Parameters for setting a cookie. Provide `url` OR `domain`+`path`, not both. */\nexport interface CookieParam {\n  name: string;\n  value: string;\n  /** Convenience: if provided, domain/path/secure are derived from this URL. */\n  url?: string;\n  domain?: string;\n  path?: string;\n  /** Unix timestamp in seconds. -1 or omitted = session cookie. */\n  expires?: number;\n  httpOnly?: boolean;\n  secure?: boolean;\n  sameSite?: \"Strict\" | \"Lax\" | \"None\";\n}\n\n/** Filter options for clearing cookies selectively. */\nexport interface ClearCookieOptions {\n  name?: string | RegExp;\n  domain?: string | RegExp;\n  path?: string | RegExp;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/index.ts",
    "content": "export * from \"./agent.js\";\n// Export api.ts under namespace to avoid conflicts with methods.ts types\nexport * as Api from \"./api.js\";\n// Also export BrowserbaseRegion directly for convenience\nexport type { BrowserbaseRegion } from \"./api.js\";\nexport * from \"./apiErrors.js\";\nexport * from \"./logs.js\";\nexport * from \"./methods.js\";\nexport * from \"./metrics.js\";\nexport * from \"./model.js\";\nexport * from \"./options.js\";\nexport * from \"./page.js\";\nexport * from \"./sdkErrors.js\";\nexport * from \"./context.js\";\nexport { AISdkClient } from \"../../external_clients/aisdk.js\";\nexport { CustomOpenAIClient } from \"../../external_clients/customOpenAI.js\";\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/locator.ts",
    "content": "import { Buffer } from \"buffer\";\n\nexport type MouseButton = \"left\" | \"right\" | \"middle\";\n\nexport interface SetInputFilePayload {\n  name: string;\n  mimeType?: string;\n  buffer: ArrayBuffer | Uint8Array | Buffer | string;\n  lastModified?: number;\n}\n\nexport type SetInputFilesArgument =\n  | string\n  | string[]\n  | SetInputFilePayload\n  | SetInputFilePayload[];\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/logs.ts",
    "content": "export type LogLevel = 0 | 1 | 2;\n\n/**\n * Mapping between numeric log levels and their names\n *\n * 0 - error/warn - Critical issues or important warnings\n * 1 - info - Standard information messages\n * 2 - debug - Detailed information for debugging\n */\nexport const LOG_LEVEL_NAMES: Record<LogLevel, string> = {\n  0: \"error\",\n  1: \"info\",\n  2: \"debug\",\n};\n\nexport type LogLine = {\n  id?: string;\n  category?: string;\n  message: string;\n  level?: LogLevel;\n  timestamp?: string;\n  auxiliary?: {\n    [key: string]: {\n      value: string;\n      type: \"object\" | \"string\" | \"html\" | \"integer\" | \"float\" | \"boolean\";\n    };\n  };\n};\n\nexport type Logger = (logLine: LogLine) => void;\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/methods.ts",
    "content": "import { Page as PatchrightPage } from \"patchright-core\";\nimport { Page as PlaywrightPage } from \"playwright-core\";\nimport { Page as PuppeteerPage } from \"puppeteer-core\";\nimport { z } from \"zod\";\nimport type {\n  InferStagehandSchema,\n  StagehandZodSchema,\n} from \"../../zodCompat.js\";\nimport { Page } from \"../../understudy/page.js\";\nimport { ModelConfiguration } from \"../public/model.js\";\nimport type { Variables } from \"./agent.js\";\n\nexport interface ActOptions {\n  model?: ModelConfiguration;\n  variables?: Variables;\n  timeout?: number;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  /**\n   * Override the instance-level serverCache setting for this request.\n   * When true, enables server-side caching.\n   * When false, disables server-side caching.\n   */\n  serverCache?: boolean;\n}\n\nexport interface ActResult {\n  success: boolean;\n  message: string;\n  actionDescription: string;\n  actions: Action[];\n  cacheStatus?: \"HIT\" | \"MISS\";\n}\n\nexport type ExtractResult<T extends StagehandZodSchema> =\n  InferStagehandSchema<T> & {\n    cacheStatus?: \"HIT\" | \"MISS\";\n  };\n\nexport interface Action {\n  selector: string;\n  description: string;\n  method?: string;\n  arguments?: string[];\n}\n\nexport interface HistoryEntry {\n  method: \"act\" | \"extract\" | \"observe\" | \"navigate\" | \"agent\";\n  parameters: unknown;\n  result: unknown;\n  timestamp: string;\n}\n\nexport interface ExtractOptions {\n  model?: ModelConfiguration;\n  timeout?: number;\n  selector?: string;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  /**\n   * Override the instance-level serverCache setting for this request.\n   * When true, enables server-side caching.\n   * When false, disables server-side caching.\n   */\n  serverCache?: boolean;\n}\n\nexport const defaultExtractSchema = z.object({\n  extraction: z.string(),\n});\n\nexport const pageTextSchema = z.object({\n  pageText: z.string(),\n});\n\nexport interface ObserveOptions {\n  model?: ModelConfiguration;\n  timeout?: number;\n  selector?: string;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  /**\n   * Override the instance-level serverCache setting for this request.\n   * When true, enables server-side caching.\n   * When false, disables server-side caching.\n   */\n  serverCache?: boolean;\n}\n\n/**\n * Observe returns an array of candidate actions. The optional `cacheStatus`\n * property is attached when the server responds with a\n * `browserbase-cache-status` header so callers can tell whether the result\n * was served from the server-side cache.\n */\nexport type ObserveResult = Action[] & { cacheStatus?: \"HIT\" | \"MISS\" };\n\nexport enum V3FunctionName {\n  ACT = \"ACT\",\n  EXTRACT = \"EXTRACT\",\n  OBSERVE = \"OBSERVE\",\n  AGENT = \"AGENT\",\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/metrics.ts",
    "content": "export interface StagehandMetrics {\n  actPromptTokens: number;\n  actCompletionTokens: number;\n  actReasoningTokens: number;\n  actCachedInputTokens: number;\n  actInferenceTimeMs: number;\n  extractPromptTokens: number;\n  extractCompletionTokens: number;\n  extractReasoningTokens: number;\n  extractCachedInputTokens: number;\n  extractInferenceTimeMs: number;\n  observePromptTokens: number;\n  observeCompletionTokens: number;\n  observeReasoningTokens: number;\n  observeCachedInputTokens: number;\n  observeInferenceTimeMs: number;\n  agentPromptTokens: number;\n  agentCompletionTokens: number;\n  agentReasoningTokens: number;\n  agentCachedInputTokens: number;\n  agentInferenceTimeMs: number;\n  totalPromptTokens: number;\n  totalCompletionTokens: number;\n  totalReasoningTokens: number;\n  totalCachedInputTokens: number;\n  totalInferenceTimeMs: number;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/model.ts",
    "content": "import type { ClientOptions as AnthropicClientOptionsBase } from \"@anthropic-ai/sdk\";\nimport type { GoogleVertexProviderSettings as GoogleVertexProviderSettingsBase } from \"@ai-sdk/google-vertex\";\nimport type { LanguageModelV2 } from \"@ai-sdk/provider\";\nimport type { ClientOptions as OpenAIClientOptionsBase } from \"openai\";\nimport type { AgentProviderType } from \"./agent.js\";\n\nexport type OpenAIClientOptions = Pick<\n  OpenAIClientOptionsBase,\n  \"baseURL\" | \"apiKey\"\n>;\n\nexport type AnthropicClientOptions = Pick<\n  AnthropicClientOptionsBase,\n  \"baseURL\" | \"apiKey\"\n>;\n\nexport interface GoogleServiceAccountCredentials {\n  type?: string;\n  project_id?: string;\n  private_key_id?: string;\n  private_key?: string;\n  client_email?: string;\n  client_id?: string;\n  auth_uri?: string;\n  token_uri?: string;\n  auth_provider_x509_cert_url?: string;\n  client_x509_cert_url?: string;\n  universe_domain?: string;\n}\n\nexport type GoogleVertexProviderSettings = Pick<\n  GoogleVertexProviderSettingsBase,\n  \"project\" | \"location\" | \"headers\"\n> & {\n  googleAuthOptions?: {\n    credentials?: GoogleServiceAccountCredentials;\n  };\n};\n\nexport type AnthropicJsonSchemaObject = {\n  definitions?: {\n    MySchema?: {\n      properties?: Record<string, unknown>;\n      required?: string[];\n    };\n  };\n  properties?: Record<string, unknown>;\n  required?: string[];\n} & Record<string, unknown>;\n\nexport interface LLMTool {\n  type: \"function\";\n  name: string;\n  description: string;\n  parameters: Record<string, unknown>;\n}\n\nexport type AISDKProvider = (modelName: string) => LanguageModelV2;\n// Represents a function that takes options (like apiKey) and returns an AISDKProvider\nexport type AISDKCustomProvider = (options: ClientOptions) => AISDKProvider;\n\nexport type AvailableModel =\n  | \"gpt-4.1\"\n  | \"gpt-4.1-mini\"\n  | \"gpt-4.1-nano\"\n  | \"o4-mini\"\n  | \"o3\"\n  | \"o3-mini\"\n  | \"o1\"\n  | \"o1-mini\"\n  | \"gpt-4o\"\n  | \"gpt-4o-mini\"\n  | \"gpt-4o-2024-08-06\"\n  | \"gpt-4.5-preview\"\n  | \"o1-preview\"\n  | \"cerebras-llama-3.3-70b\"\n  | \"cerebras-llama-3.1-8b\"\n  | \"groq-llama-3.3-70b-versatile\"\n  | \"groq-llama-3.3-70b-specdec\"\n  | \"gemini-1.5-flash\"\n  | \"gemini-1.5-pro\"\n  | \"gemini-1.5-flash-8b\"\n  | \"gemini-2.0-flash-lite\"\n  | \"gemini-2.0-flash\"\n  | \"gemini-2.5-flash-preview-04-17\"\n  | \"gemini-2.5-pro-preview-03-25\"\n  | string;\n\nexport type ModelProvider =\n  | \"openai\"\n  | \"anthropic\"\n  | \"cerebras\"\n  | \"groq\"\n  | \"google\"\n  | \"aisdk\";\n\nexport type ClientOptions = (\n  | OpenAIClientOptions\n  | AnthropicClientOptions\n  | GoogleVertexProviderSettings\n) & {\n  apiKey?: string;\n  provider?: AgentProviderType;\n  baseURL?: string;\n  /** OpenAI organization ID */\n  organization?: string;\n  /** Delay between agent actions in ms */\n  waitBetweenActions?: number;\n  /** Anthropic thinking budget for extended thinking */\n  thinkingBudget?: number;\n  /** Environment type for CUA agents (browser, mac, windows, ubuntu) */\n  environment?: string;\n  /** Max images for Microsoft FARA agent */\n  maxImages?: number;\n  /** Temperature for model inference */\n  temperature?: number;\n  /** Custom headers sent with every request to the provider */\n  headers?: Record<string, string>;\n};\n\nexport type ModelConfiguration =\n  | AvailableModel\n  | (ClientOptions & { modelName: AvailableModel });\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/options.ts",
    "content": "import { z } from \"zod\";\nimport { LLMClient } from \"../../llm/LLMClient.js\";\nimport { ModelConfiguration } from \"./model.js\";\nimport { LogLine } from \"./logs.js\";\nimport {\n  type BrowserbaseSessionCreateParams,\n  LocalBrowserLaunchOptionsSchema,\n} from \"./api.js\";\n\nexport type V3Env = \"LOCAL\" | \"BROWSERBASE\";\n\n// Re-export for backwards compatibility (camelCase alias)\nexport const localBrowserLaunchOptionsSchema = LocalBrowserLaunchOptionsSchema;\n\nexport type LocalBrowserLaunchOptions = z.infer<\n  typeof LocalBrowserLaunchOptionsSchema\n>;\n\n/** Constructor options for V3 */\nexport interface V3Options {\n  env: V3Env;\n  /**\n   * Optional external session identifier to use for flow logging/event storage.\n   * When omitted, Stagehand falls back to its internal instance id.\n   * This currently ends up 1:1 with the Browserbase session id when one exists,\n   * but callers should not rely on that remaining a permanent invariant.\n   */\n  sessionId?: string;\n  // Browserbase (required when env = \"BROWSERBASE\")\n  apiKey?: string;\n  projectId?: string;\n  /**\n   * Optional: fine-tune Browserbase session creation or resume an existing session.\n   */\n  browserbaseSessionCreateParams?: BrowserbaseSessionCreateParams;\n  browserbaseSessionID?: string;\n  /**\n   * Controls browser keepalive behavior. When set, it overrides any value in\n   * browserbaseSessionCreateParams.keepAlive.\n   */\n  keepAlive?: boolean;\n\n  // Local Chromium (optional)\n  localBrowserLaunchOptions?: LocalBrowserLaunchOptions;\n\n  model?: ModelConfiguration;\n  llmClient?: LLMClient; // allow user to pass their own\n  systemPrompt?: string;\n  logInferenceToFile?: boolean;\n  experimental?: boolean;\n  verbose?: 0 | 1 | 2;\n  selfHeal?: boolean;\n  // V2 compatibility fields - only included because the server imports this type and supports V2\n  waitForCaptchaSolves?: boolean;\n  actTimeoutMs?: number;\n  /** Disable pino logging backend (useful for tests or minimal environments). */\n  disablePino?: boolean;\n  /** Optional external logger hook for integrating with host apps. */\n  logger?: (line: LogLine) => void;\n  /** Directory used to persist cached actions for act(). */\n  cacheDir?: string;\n  domSettleTimeout?: number;\n  disableAPI?: boolean;\n  /**\n   * When true, enables server-side caching for API requests.\n   * When false, disables server-side caching.\n   * Defaults to true (caching enabled).\n   * Can be overridden per-method in act(), extract(), and observe() options.\n   */\n  serverCache?: boolean;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/page.ts",
    "content": "import { Page } from \"../../understudy/page.js\";\nimport { Page as PlaywrightPage } from \"playwright-core\";\nimport { Page as PatchrightPage } from \"patchright-core\";\nimport { Page as PuppeteerPage } from \"puppeteer-core\";\n\nexport type { PlaywrightPage, PatchrightPage, PuppeteerPage, Page };\nexport type AnyPage = PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n\nexport { ConsoleMessage } from \"../../understudy/consoleMessage.js\";\nexport type { ConsoleListener } from \"../../understudy/consoleMessage.js\";\n\nexport type LoadState = \"load\" | \"domcontentloaded\" | \"networkidle\";\nexport { Response } from \"../../understudy/response.js\";\n\nexport type SnapshotResult = {\n  formattedTree: string;\n  xpathMap: Record<string, string>;\n  urlMap: Record<string, string>;\n};\n\nexport type PageSnapshotOptions = {\n  includeIframes?: boolean;\n};\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/screenshotTypes.ts",
    "content": "import type { Locator } from \"../../understudy/locator.js\";\n\nexport type ScreenshotAnimationsOption = \"disabled\" | \"allow\";\nexport type ScreenshotCaretOption = \"hide\" | \"initial\";\nexport type ScreenshotScaleOption = \"css\" | \"device\";\n\nexport interface ScreenshotClip {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport interface ScreenshotOptions {\n  animations?: ScreenshotAnimationsOption;\n  caret?: ScreenshotCaretOption;\n  clip?: ScreenshotClip;\n  fullPage?: boolean;\n  mask?: Locator[];\n  maskColor?: string;\n  omitBackground?: boolean;\n  path?: string;\n  quality?: number;\n  scale?: ScreenshotScaleOption;\n  style?: string;\n  timeout?: number;\n  type?: \"png\" | \"jpeg\";\n}\n"
  },
  {
    "path": "packages/core/lib/v3/types/public/sdkErrors.ts",
    "content": "import { ZodError } from \"zod\";\n// Avoid .js extension so bundlers resolve TS source\nimport { STAGEHAND_VERSION } from \"../../../version.js\";\n\nexport class StagehandError extends Error {\n  public readonly cause?: unknown;\n\n  constructor(message: string, cause?: unknown) {\n    super(message);\n    this.name = this.constructor.name;\n    if (cause !== undefined) {\n      this.cause = cause;\n    }\n  }\n}\n\nexport class StagehandDefaultError extends StagehandError {\n  constructor(error?: unknown) {\n    if (error instanceof Error || error instanceof StagehandError) {\n      super(\n        `\\nHey! We're sorry you ran into an error. \\nStagehand version: ${STAGEHAND_VERSION} \\nIf you need help, please open a Github issue or reach out to us on Discord: https://stagehand.dev/discord\\n\\nFull error:\\n${error.message}`,\n      );\n    }\n  }\n}\n\nexport class StagehandEnvironmentError extends StagehandError {\n  constructor(\n    currentEnvironment: string,\n    requiredEnvironment: string,\n    feature: string,\n  ) {\n    super(\n      `You seem to be setting the current environment to ${currentEnvironment}.` +\n        `Ensure the environment is set to ${requiredEnvironment} if you want to use ${feature}.`,\n    );\n  }\n}\n\nexport class MissingEnvironmentVariableError extends StagehandError {\n  constructor(missingEnvironmentVariable: string, feature: string) {\n    super(\n      `${missingEnvironmentVariable} is required to use ${feature}.` +\n        `Please set ${missingEnvironmentVariable} in your environment.`,\n    );\n  }\n}\n\nexport class UnsupportedModelError extends StagehandError {\n  constructor(supportedModels: string[], feature?: string) {\n    const message = feature\n      ? `${feature} requires a valid model.`\n      : `Unsupported model.`;\n\n    const guidance =\n      `\\n\\nPlease use the provider/model format (e.g., \"openai/gpt-4o\", \"anthropic/claude-sonnet-4-5\", \"google/gemini-3-flash-preview\").` +\n      `\\n\\nFor a complete list of supported models and providers, see: https://docs.stagehand.dev/v3/configuration/models#configuration-setup`;\n\n    super(`${message}${guidance}`);\n  }\n}\n\nexport class UnsupportedModelProviderError extends StagehandError {\n  constructor(supportedProviders: string[], feature?: string) {\n    super(\n      feature\n        ? `${feature} requires one of the following model providers: ${supportedProviders}`\n        : `please use one of the supported model providers: ${supportedProviders}`,\n    );\n  }\n}\n\nexport class UnsupportedAISDKModelProviderError extends StagehandError {\n  constructor(provider: string, supportedProviders: string[]) {\n    super(\n      `${provider} is not currently supported for aiSDK. please use one of the supported model providers: ${supportedProviders}`,\n    );\n  }\n}\n\nexport class InvalidAISDKModelFormatError extends StagehandError {\n  constructor(modelName: string) {\n    super(\n      `${modelName} does not follow correct format for specifying aiSDK models. Please define your model as 'provider/model-name'. For example: \\`model: 'openai/gpt-4o-mini'\\``,\n    );\n  }\n}\n\nexport class StagehandNotInitializedError extends StagehandError {\n  constructor(prop: string) {\n    super(\n      `You seem to be calling \\`${prop}\\` on a page in an uninitialized \\`Stagehand\\` object. ` +\n        `Ensure you are running \\`await stagehand.init()\\` on the Stagehand object before ` +\n        `referencing the \\`page\\` object.`,\n    );\n  }\n}\n\nexport class BrowserbaseSessionNotFoundError extends StagehandError {\n  constructor() {\n    super(\"No Browserbase session ID found\");\n  }\n}\n\nexport class CaptchaTimeoutError extends StagehandError {\n  constructor() {\n    super(\"Captcha timeout\");\n  }\n}\n\nexport class MissingLLMConfigurationError extends StagehandError {\n  constructor() {\n    super(\n      \"No LLM API key or LLM Client configured. An LLM API key or a custom LLM Client \" +\n        \"is required to use act, extract, or observe.\",\n    );\n  }\n}\n\nexport class HandlerNotInitializedError extends StagehandError {\n  constructor(handlerType: string) {\n    super(`${handlerType} handler not initialized`);\n  }\n}\n\nexport class StagehandInvalidArgumentError extends StagehandError {\n  constructor(message: string) {\n    super(`InvalidArgumentError: ${message}`);\n  }\n}\n\nexport class CookieValidationError extends StagehandError {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\nexport class CookieSetError extends StagehandError {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\nexport class StagehandElementNotFoundError extends StagehandError {\n  constructor(xpaths: string[]) {\n    super(`Could not find an element for the given xPath(s): ${xpaths}`);\n  }\n}\n\nexport class AgentScreenshotProviderError extends StagehandError {\n  constructor(message: string) {\n    super(`ScreenshotProviderError: ${message}`);\n  }\n}\n\nexport class StagehandMissingArgumentError extends StagehandError {\n  constructor(message: string) {\n    super(`MissingArgumentError: ${message}`);\n  }\n}\n\nexport class CreateChatCompletionResponseError extends StagehandError {\n  constructor(message: string) {\n    super(`CreateChatCompletionResponseError: ${message}`);\n  }\n}\n\nexport class StagehandEvalError extends StagehandError {\n  constructor(message: string) {\n    super(`StagehandEvalError: ${message}`);\n  }\n}\n\nexport class StagehandDomProcessError extends StagehandError {\n  constructor(message: string) {\n    super(`Error Processing Dom: ${message}`);\n  }\n}\n\nexport class StagehandLocatorError extends StagehandError {\n  constructor(action: string, selector: string, message: string) {\n    super(\n      `Error ${action} Element with selector: ${selector} Reason: ${message}`,\n    );\n  }\n}\n\nexport class StagehandClickError extends StagehandError {\n  constructor(message: string, selector: string) {\n    super(\n      `Error Clicking Element with selector: ${selector} Reason: ${message}`,\n    );\n  }\n}\n\nexport class LLMResponseError extends StagehandError {\n  constructor(primitive: string, message: string) {\n    super(`${primitive} LLM response error: ${message}`);\n  }\n}\n\nexport class StagehandIframeError extends StagehandError {\n  constructor(frameUrl: string, message: string) {\n    super(\n      `Unable to resolve frameId for iframe with URL: ${frameUrl} Full error: ${message}`,\n    );\n  }\n}\n\nexport class ContentFrameNotFoundError extends StagehandError {\n  constructor(selector: string) {\n    super(`Unable to obtain a content frame for selector: ${selector}`);\n  }\n}\n\nexport class XPathResolutionError extends StagehandError {\n  constructor(xpath: string) {\n    super(`XPath \"${xpath}\" does not resolve in the current page or frames`);\n  }\n}\n\nexport class ExperimentalApiConflictError extends StagehandError {\n  constructor() {\n    super(\n      \"`experimental` mode cannot be used together with the Stagehand API. \" +\n        \"To use experimental features, set experimental: true and disableAPI: true in the stagehand constructor. \" +\n        \"To use the Stagehand API, set experimental: false and disableAPI: false (or omit it) in the stagehand constructor.\",\n    );\n  }\n}\n\nexport class ExperimentalNotConfiguredError extends StagehandError {\n  constructor(featureName: string) {\n    super(`Feature \"${featureName}\" is an experimental feature, and cannot be configured when disableAPI: false.\n    Please set experimental: true and disableAPI: true in the stagehand constructor to use this feature.\n    If you wish to use the Stagehand API, please ensure ${featureName} is not defined in your function call,\n    and set experimental: false, disableAPI: false (or omit it) in the Stagehand constructor.`);\n  }\n}\n\nexport class CuaModelRequiredError extends StagehandError {\n  constructor(availableModels: readonly string[]) {\n    super(\n      `To use the computer use agent (CUA), please provide a CUA model in the agent constructor or stagehand config. ` +\n        `Try one of our supported CUA models: ${availableModels.join(\", \")}`,\n    );\n  }\n}\n\nexport class ZodSchemaValidationError extends Error {\n  constructor(\n    public readonly received: unknown,\n    public readonly issues: ReturnType<ZodError[\"format\"]>,\n  ) {\n    super(`Zod schema validation failed\n\n— Received —\n${JSON.stringify(received, null, 2)}\n\n— Issues —\n${JSON.stringify(issues, null, 2)}`);\n    this.name = \"ZodSchemaValidationError\";\n  }\n}\n\nexport class StagehandInitError extends StagehandError {\n  constructor(message: string) {\n    super(message);\n  }\n}\n\nexport class MCPConnectionError extends StagehandError {\n  public readonly serverUrl: string;\n  public readonly originalError: unknown;\n\n  constructor(serverUrl: string, originalError: unknown) {\n    const errorMessage =\n      originalError instanceof Error\n        ? originalError.message\n        : String(originalError);\n\n    super(\n      `Failed to connect to MCP server at \"${serverUrl}\". ${errorMessage}. ` +\n        `Please verify the server URL is correct and the server is running.`,\n    );\n\n    this.serverUrl = serverUrl;\n    this.originalError = originalError;\n  }\n}\n\nexport class StagehandShadowRootMissingError extends StagehandError {\n  constructor(detail?: string) {\n    super(\n      `No shadow root present on the resolved host` +\n        (detail ? `: ${detail}` : \"\"),\n    );\n  }\n}\n\nexport class StagehandShadowSegmentEmptyError extends StagehandError {\n  constructor() {\n    super(`Empty selector segment after shadow-DOM hop (\"//\")`);\n  }\n}\n\nexport class StagehandShadowSegmentNotFoundError extends StagehandError {\n  constructor(segment: string, hint?: string) {\n    super(\n      `Shadow segment '${segment}' matched no element inside shadow root` +\n        (hint ? ` ${hint}` : \"\"),\n    );\n  }\n}\n\nexport class ElementNotVisibleError extends StagehandError {\n  constructor(selector: string) {\n    super(`Element not visible (no box model): ${selector}`);\n  }\n}\n\nexport class ResponseBodyError extends StagehandError {\n  constructor(message: string) {\n    super(`Failed to retrieve response body: ${message}`);\n  }\n}\n\nexport class ResponseParseError extends StagehandError {\n  constructor(message: string) {\n    super(`Failed to parse response: ${message}`);\n  }\n}\n\nexport class TimeoutError extends StagehandError {\n  constructor(operation: string, timeoutMs: number) {\n    super(`${operation} timed out after ${timeoutMs}ms`);\n  }\n}\n\nexport class ActTimeoutError extends TimeoutError {\n  constructor(timeoutMs: number) {\n    super(\"act()\", timeoutMs);\n    this.name = \"ActTimeoutError\";\n  }\n}\n\nexport class ExtractTimeoutError extends TimeoutError {\n  constructor(timeoutMs: number) {\n    super(\"extract()\", timeoutMs);\n    this.name = \"ExtractTimeoutError\";\n  }\n}\n\nexport class ObserveTimeoutError extends TimeoutError {\n  constructor(timeoutMs: number) {\n    super(\"observe()\", timeoutMs);\n    this.name = \"ObserveTimeoutError\";\n  }\n}\n\nexport class PageNotFoundError extends StagehandError {\n  constructor(identifier: string) {\n    super(`No Page found for ${identifier}`);\n  }\n}\n\nexport class ConnectionTimeoutError extends StagehandError {\n  constructor(message: string) {\n    super(`Connection timeout: ${message}`);\n  }\n}\n\nexport class StreamingCallbacksInNonStreamingModeError extends StagehandError {\n  public readonly invalidCallbacks: string[];\n\n  constructor(invalidCallbacks: string[]) {\n    super(\n      `Streaming-only callback(s) \"${invalidCallbacks.join('\", \"')}\" cannot be used in non-streaming mode. ` +\n        `Set 'stream: true' in AgentConfig to use these callbacks.`,\n    );\n    this.invalidCallbacks = invalidCallbacks;\n  }\n}\n\nexport class AgentAbortError extends StagehandError {\n  public readonly reason: string;\n\n  constructor(reason?: string) {\n    const message = reason\n      ? `Agent execution was aborted: ${reason}`\n      : \"Agent execution was aborted\";\n    super(message);\n    this.reason = reason || \"aborted\";\n  }\n}\n\nexport class StagehandClosedError extends StagehandError {\n  constructor() {\n    super(\"Stagehand session was closed\");\n  }\n}\n\nexport class CdpConnectionClosedError extends StagehandError {\n  constructor(reason: string) {\n    super(`CDP connection closed: ${reason}`);\n  }\n}\n\nexport class StagehandSetExtraHTTPHeadersError extends StagehandError {\n  public readonly failures: string[];\n\n  constructor(failures: string[]) {\n    super(\n      `setExtraHTTPHeaders failed for ${failures.length} session(s): ${failures.join(\", \")}`,\n    );\n    this.failures = failures;\n  }\n}\n\nexport class StagehandSnapshotError extends StagehandError {\n  constructor(cause?: unknown) {\n    const suffix =\n      cause instanceof Error\n        ? `: ${cause.message}`\n        : cause\n          ? `: ${String(cause)}`\n          : \"\";\n    super(`error taking snapshot${suffix}`, cause);\n  }\n}\n\nexport class UnderstudyCommandException extends StagehandError {\n  constructor(message: string, cause?: unknown) {\n    super(message, cause);\n    this.name = \"UnderstudyCommandException\";\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/a11yTree.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"../../cdp.js\";\nimport type {\n  A11yNode,\n  A11yOptions,\n  AccessibilityTreeResult,\n} from \"../../../types/private/snapshot.js\";\nimport {\n  resolveObjectIdForCss,\n  resolveObjectIdForXPath,\n} from \"./focusSelectors.js\";\nimport { formatTreeLine, normaliseSpaces } from \"./treeFormatUtils.js\";\n\n/**\n * Fetch and prune the accessibility tree for a frame, optionally scoping the\n * output to a selector root for faster targeted snapshots.\n */\nexport async function a11yForFrame(\n  session: CDPSessionLike,\n  frameId: string | undefined,\n  opts: A11yOptions,\n): Promise<AccessibilityTreeResult> {\n  await session.send(\"Accessibility.enable\").catch(() => {});\n  await session.send(\"Runtime.enable\").catch(() => {});\n  await session.send(\"DOM.enable\").catch(() => {});\n\n  let nodes: Protocol.Accessibility.AXNode[] = [];\n  try {\n    const params = frameId ? ({ frameId } as Record<string, unknown>) : {};\n    ({ nodes } = await session.send<{\n      nodes: Protocol.Accessibility.AXNode[];\n    }>(\"Accessibility.getFullAXTree\", params));\n  } catch (e) {\n    const msg = String((e as Error)?.message ?? e ?? \"\");\n    const isFrameScopeError =\n      msg.includes(\"Frame with the given\") ||\n      msg.includes(\"does not belong to the target\") ||\n      msg.includes(\"is not found\");\n    if (!isFrameScopeError || !frameId) throw e;\n    ({ nodes } = await session.send<{\n      nodes: Protocol.Accessibility.AXNode[];\n    }>(\"Accessibility.getFullAXTree\"));\n  }\n\n  const urlMap: Record<string, string> = {};\n  for (const n of nodes) {\n    const be = n.backendDOMNodeId;\n    if (typeof be !== \"number\") continue;\n    const url = extractUrlFromAXNode(n);\n    if (!url) continue;\n    const enc = opts.encode(be);\n    urlMap[enc] = url;\n  }\n\n  let scopeApplied = false;\n  const nodesForOutline = await (async () => {\n    const sel = opts.focusSelector?.trim();\n    if (!sel) return nodes;\n    try {\n      const looksLikeXPath = /^xpath=/i.test(sel) || sel.startsWith(\"/\");\n      const objectId = looksLikeXPath\n        ? await resolveObjectIdForXPath(session, sel, frameId)\n        : await resolveObjectIdForCss(session, sel, frameId);\n      if (!objectId) return nodes;\n      const desc = await session.send<{ node?: { backendNodeId?: number } }>(\n        \"DOM.describeNode\",\n        { objectId },\n      );\n      const be = desc.node?.backendNodeId;\n      if (typeof be !== \"number\") return nodes;\n      const target = nodes.find((n) => n.backendDOMNodeId === be);\n      if (!target) return nodes;\n      scopeApplied = true;\n      const keep = new Set<string>([target.nodeId]);\n      const queue: Protocol.Accessibility.AXNode[] = [target];\n      while (queue.length) {\n        const cur = queue.shift()!;\n        for (const id of cur.childIds ?? []) {\n          if (keep.has(id)) continue;\n          keep.add(id);\n          const child = nodes.find((n) => n.nodeId === id);\n          if (child) queue.push(child);\n        }\n      }\n      return nodes\n        .filter((n) => keep.has(n.nodeId))\n        .map((n) =>\n          n.nodeId === target.nodeId ? { ...n, parentId: undefined } : n,\n        );\n    } catch {\n      return nodes;\n    }\n  })();\n\n  const decorated = decorateRoles(nodesForOutline, opts);\n  const { tree } = await buildHierarchicalTree(decorated, opts);\n\n  const simplified = tree.map((n) => formatTreeLine(n)).join(\"\\n\");\n  return { outline: simplified.trimEnd(), urlMap, scopeApplied };\n}\n\nexport function decorateRoles(\n  nodes: Protocol.Accessibility.AXNode[],\n  opts: A11yOptions,\n): A11yNode[] {\n  const asRole = (n: Protocol.Accessibility.AXNode) =>\n    String(n.role?.value ?? \"\");\n\n  return nodes.map((n) => {\n    let encodedId: string | undefined;\n    if (typeof n.backendDOMNodeId === \"number\") {\n      try {\n        encodedId = opts.encode(n.backendDOMNodeId);\n      } catch {\n        //\n      }\n    }\n\n    let role = asRole(n);\n\n    const domIsScrollable = encodedId\n      ? opts.scrollableMap[encodedId] === true\n      : false;\n    const tag = encodedId ? opts.tagNameMap[encodedId] : undefined;\n    const isHtmlElement = tag === \"html\";\n    if ((domIsScrollable || isHtmlElement) && tag !== \"#document\") {\n      const tagLabel = tag && tag.startsWith(\"#\") ? tag.slice(1) : tag;\n      role = tagLabel\n        ? `scrollable, ${tagLabel}`\n        : `scrollable${role ? `, ${role}` : \"\"}`;\n    }\n\n    return {\n      role,\n      name: n.name?.value,\n      description: n.description?.value,\n      value: n.value?.value,\n      nodeId: n.nodeId,\n      backendDOMNodeId: n.backendDOMNodeId,\n      parentId: n.parentId,\n      childIds: n.childIds,\n      encodedId,\n    };\n  });\n}\n\nexport async function buildHierarchicalTree(\n  nodes: A11yNode[],\n  opts: A11yOptions,\n): Promise<{ tree: A11yNode[] }> {\n  const nodeMap = new Map<string, A11yNode>();\n\n  for (const n of nodes) {\n    const keep =\n      !!(n.name && n.name.trim()) ||\n      !!(n.childIds && n.childIds.length) ||\n      !isStructural(n.role);\n    if (!keep) continue;\n    nodeMap.set(n.nodeId, { ...n });\n  }\n\n  for (const n of nodes) {\n    if (!n.parentId) continue;\n    const parent = nodeMap.get(n.parentId);\n    const cur = nodeMap.get(n.nodeId);\n    if (parent && cur) (parent.children ??= []).push(cur);\n  }\n\n  const roots = nodes\n    .filter((n) => !n.parentId && nodeMap.has(n.nodeId))\n    .map((n) => nodeMap.get(n.nodeId)!) as A11yNode[];\n\n  const cleaned = (await Promise.all(roots.map(pruneStructuralSafe))).filter(\n    Boolean,\n  ) as A11yNode[];\n\n  return { tree: cleaned };\n\n  async function pruneStructuralSafe(node: A11yNode): Promise<A11yNode | null> {\n    if (+node.nodeId < 0) return null;\n\n    const children = node.children ?? [];\n    if (!children.length) {\n      return isStructural(node.role) ? null : node;\n    }\n\n    const cleanedKids = (\n      await Promise.all(children.map(pruneStructuralSafe))\n    ).filter(Boolean) as A11yNode[];\n\n    const prunedStatic = removeRedundantStaticTextChildren(node, cleanedKids);\n\n    if (isStructural(node.role)) {\n      if (prunedStatic.length === 1) return prunedStatic[0]!;\n      if (prunedStatic.length === 0) return null;\n    }\n\n    let newRole = node.role;\n    if ((newRole === \"generic\" || newRole === \"none\") && node.encodedId) {\n      const tagName = opts.tagNameMap[node.encodedId];\n      if (tagName) newRole = tagName;\n    }\n\n    if (newRole === \"combobox\" && node.encodedId) {\n      const tagName = opts.tagNameMap[node.encodedId];\n      if (tagName === \"select\") newRole = \"select\";\n    }\n\n    return { ...node, role: newRole, children: prunedStatic };\n  }\n}\n\nexport function isStructural(role: string): boolean {\n  const r = role?.toLowerCase();\n  return r === \"generic\" || r === \"none\" || r === \"inlinetextbox\";\n}\n\nexport function extractUrlFromAXNode(\n  ax: Protocol.Accessibility.AXNode,\n): string | undefined {\n  const props = ax.properties ?? [];\n  const urlProp = props.find((p) => p.name === \"url\");\n  const value = urlProp?.value?.value;\n  return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nexport function removeRedundantStaticTextChildren(\n  parent: A11yNode,\n  children: A11yNode[],\n): A11yNode[] {\n  if (!parent.name) return children;\n  const parentNorm = normaliseSpaces(parent.name).trim();\n  let combined = \"\";\n  for (const c of children) {\n    if (c.role === \"StaticText\" && c.name) {\n      combined += normaliseSpaces(c.name).trim();\n    }\n  }\n  if (combined === parentNorm) {\n    return children.filter((c) => c.role !== \"StaticText\");\n  }\n  return children;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/activeElement.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { Page } from \"../../page.js\";\nimport { executionContexts } from \"../../executionContextRegistry.js\";\nimport { buildA11yInvocation } from \"../../a11yInvocation.js\";\nimport { a11yScriptSources } from \"../../../dom/build/a11yScripts.generated.js\";\nimport {\n  absoluteXPathForBackendNode,\n  normalizeXPath,\n  prefixXPath,\n} from \"./xpathUtils.js\";\n\n/**\n * Compute the absolute XPath for the currently focused element.\n * - Detects which frame has focus via document.hasFocus().\n * - Finds the deepest activeElement (dives into shadow DOM).\n * - Builds an absolute, cross-frame XPath by prefixing iframe hosts.\n */\nexport async function computeActiveElementXpath(\n  page: Page,\n): Promise<string | null> {\n  const tree = page.getFullFrameTree();\n  const parentByFrame = new Map<string, string | null>();\n  (function index(n: Protocol.Page.FrameTree, parent: string | null) {\n    parentByFrame.set(n.frame.id, parent);\n    for (const c of n.childFrames ?? []) index(c, n.frame.id);\n  })(tree, null);\n\n  const frames = page.listAllFrameIds();\n  let focusedFrameId: string | null = null;\n  for (const fid of frames) {\n    const sess = page.getSessionForFrame(fid);\n    try {\n      await sess.send(\"Runtime.enable\").catch(() => {});\n      const ctxId = await executionContexts\n        .waitForMainWorld(sess, fid, 1000)\n        .catch(() => {});\n      const hasFocusExpr = buildA11yInvocation(\"documentHasFocusStrict\", []);\n      const evalParams = ctxId\n        ? {\n            contextId: ctxId,\n            expression: hasFocusExpr,\n            returnByValue: true,\n          }\n        : { expression: hasFocusExpr, returnByValue: true };\n      const { result } = await sess.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        evalParams,\n      );\n      if (result?.value === true) {\n        focusedFrameId = fid;\n        break;\n      }\n    } catch {\n      //\n    }\n  }\n  if (!focusedFrameId) focusedFrameId = page.mainFrameId();\n  const focusedSession = page.getSessionForFrame(focusedFrameId);\n\n  let objectId: string | undefined;\n  try {\n    await focusedSession.send(\"Runtime.enable\").catch(() => {});\n    const ctxId = await executionContexts\n      .waitForMainWorld(focusedSession, focusedFrameId, 1000)\n      .catch(() => {});\n    const activeExpr = buildA11yInvocation(\"resolveDeepActiveElement\", []);\n    const evalParams = ctxId\n      ? {\n          contextId: ctxId,\n          expression: activeExpr,\n          returnByValue: false,\n        }\n      : { expression: activeExpr, returnByValue: false };\n    const { result } =\n      await focusedSession.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        evalParams,\n      );\n    objectId = result?.objectId as string | undefined;\n  } catch {\n    objectId = undefined;\n  }\n  if (!objectId) return null;\n\n  const leafXPath = await (async () => {\n    try {\n      const { result } = await focusedSession.send<{\n        result: { value?: string };\n      }>(\"Runtime.callFunctionOn\", {\n        objectId,\n        functionDeclaration: a11yScriptSources.nodeToAbsoluteXPath,\n        returnByValue: true,\n      });\n      try {\n        await focusedSession.send(\"Runtime.releaseObject\", { objectId });\n      } catch {\n        //\n      }\n      const xp = result?.value || \"\";\n      return typeof xp === \"string\" && xp ? xp : null;\n    } catch {\n      try {\n        await focusedSession.send(\"Runtime.releaseObject\", { objectId });\n      } catch {\n        //\n      }\n      return null;\n    }\n  })();\n\n  if (!leafXPath) return null;\n\n  let prefix = \"\";\n  let cur: string | null | undefined = focusedFrameId;\n  while (cur) {\n    const parent = parentByFrame.get(cur) ?? null;\n    if (!parent) break;\n    const parentSess = page.getSessionForFrame(parent);\n    try {\n      const { backendNodeId } = await parentSess.send<{\n        backendNodeId?: number;\n      }>(\"DOM.getFrameOwner\", { frameId: cur });\n      if (typeof backendNodeId === \"number\") {\n        const xp = await absoluteXPathForBackendNode(parentSess, backendNodeId);\n        if (xp) prefix = prefix ? prefixXPath(prefix, xp) : normalizeXPath(xp);\n      }\n    } catch {\n      //\n    }\n    cur = parent;\n  }\n\n  return prefix ? prefixXPath(prefix, leafXPath) : normalizeXPath(leafXPath);\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/capture.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"../../cdp.js\";\nimport { Page } from \"../../page.js\";\nimport { v3Logger } from \"../../../logger.js\";\nimport type {\n  FrameContext,\n  FrameDomMaps,\n  FrameParentIndex,\n  HybridSnapshot,\n  SnapshotOptions,\n  SessionDomIndex,\n} from \"../../../types/private/index.js\";\nimport { a11yForFrame } from \"./a11yTree.js\";\nimport {\n  resolveCssFocusFrameAndTail,\n  resolveFocusFrameAndTail,\n} from \"./focusSelectors.js\";\nimport {\n  buildSessionDomIndex,\n  domMapsForSession,\n  relativizeXPath,\n} from \"./domTree.js\";\nimport { injectSubtrees } from \"./treeFormatUtils.js\";\nimport { ownerSession, parentSession } from \"./sessions.js\";\nimport { normalizeXPath, prefixXPath } from \"./xpathUtils.js\";\n\n/**\n * Capture a hybrid DOM + Accessibility snapshot for the provided page.\n *\n * Flow overview:\n * 1. (Optional) Scope directly to a requested selector. We walk iframe hops to\n *    find the owning frame, build just that frame’s DOM + AX tree, and bail out\n *    early when the subtree satisfies the caller.\n * 2. Build DOM indexes for every unique CDP session. DOM.getDocument is called\n *    once per session and hydrated so per-frame slices can share the result.\n * 3. Slice each frame’s DOM data from its session index and fetch its AX tree.\n *    This yields relative XPath/tag/url maps for the document rooted at that frame.\n * 4. Walk the frame tree to compute absolute iframe prefixes. Every child frame\n *    needs the XPath of the iframe element that hosts it so we can prefix maps.\n * 5. Merge all per-frame results into global combined maps and stitch the text\n *    outline. The final payload mirrors the legacy shape but is built in layers.\n *\n * Each numbered block below references the step above for easier debugging.\n */\nexport async function captureHybridSnapshot(\n  page: Page,\n  options?: SnapshotOptions,\n): Promise<HybridSnapshot> {\n  const pierce = options?.pierceShadow ?? true;\n  const includeIframes = options?.includeIframes !== false;\n\n  const context = buildFrameContext(page);\n\n  const scopedSnapshot = await tryScopedSnapshot(\n    page,\n    options,\n    context,\n    pierce,\n  );\n  if (scopedSnapshot) return scopedSnapshot;\n\n  const framesInScope = includeIframes ? [...context.frames] : [context.rootId];\n  if (!framesInScope.includes(context.rootId)) {\n    framesInScope.unshift(context.rootId);\n  }\n\n  const sessionToIndex = await buildSessionIndexes(page, framesInScope, pierce);\n  const { perFrameMaps, perFrameOutlines } = await collectPerFrameMaps(\n    page,\n    context,\n    sessionToIndex,\n    options,\n    pierce,\n    framesInScope,\n  );\n  const { absPrefix, iframeHostEncByChild } = await computeFramePrefixes(\n    page,\n    context,\n    perFrameMaps,\n    framesInScope,\n  );\n\n  return mergeFramesIntoSnapshot(\n    context,\n    perFrameMaps,\n    perFrameOutlines,\n    absPrefix,\n    iframeHostEncByChild,\n    framesInScope,\n  );\n}\n\n/**\n * Snapshot the current frame tree so downstream helpers have consistent topology\n * without re-querying CDP. The map is intentionally shallow (frameId → parentId)\n * so it is serializable/testable without holding on to CDP handles.\n */\nexport function buildFrameContext(page: Page): FrameContext {\n  const rootId = page.mainFrameId();\n  const frameTree = page.asProtocolFrameTree(rootId);\n  const parentByFrame: FrameParentIndex = new Map();\n  (function index(n: Protocol.Page.FrameTree, parent: string | null) {\n    parentByFrame.set(n.frame.id, parent);\n    for (const c of n.childFrames ?? []) index(c, n.frame.id);\n  })(frameTree, null);\n  const frames = page.listAllFrameIds();\n  return { rootId, parentByFrame, frames };\n}\n\n/**\n * Step 1 – scoped snapshot fast-path. If a selector is provided we try to:\n *  1) Resolve the selector (XPath or CSS) across iframes.\n *  2) Build DOM + AX data only for the owning frame.\n *  3) Bail out early when the selector's subtree satisfies the request.\n *\n * Returns `null` when scoping fails (e.g., selector miss) so the caller can\n * fall back to the full multi-frame snapshot.\n */\nexport async function tryScopedSnapshot(\n  page: Page,\n  options: SnapshotOptions | undefined,\n  context: FrameContext,\n  pierce: boolean,\n): Promise<HybridSnapshot | null> {\n  const requestedFocus = options?.focusSelector?.trim();\n  if (!requestedFocus) return null;\n\n  const logScopeFallback = () => {\n    v3Logger({\n      message: `Unable to narrow scope with selector. Falling back to using full DOM`,\n      level: 1,\n      auxiliary: {\n        arguments: {\n          value: `selector: ${options?.focusSelector?.trim()}`,\n          type: \"string\",\n        },\n      },\n    });\n  };\n\n  try {\n    let targetFrameId: string;\n    let tailSelector: string | undefined;\n    let absPrefix: string | undefined;\n\n    const looksLikeXPath =\n      /^xpath=/i.test(requestedFocus) || requestedFocus.startsWith(\"/\");\n    if (looksLikeXPath) {\n      const focus = normalizeXPath(requestedFocus);\n      const hit = await resolveFocusFrameAndTail(\n        page,\n        focus,\n        context.parentByFrame,\n        context.rootId,\n      );\n      targetFrameId = hit.targetFrameId;\n      tailSelector = hit.tailXPath || undefined;\n      absPrefix = hit.absPrefix;\n    } else {\n      const cssHit = await resolveCssFocusFrameAndTail(\n        page,\n        requestedFocus,\n        context.parentByFrame,\n        context.rootId,\n      );\n      targetFrameId = cssHit.targetFrameId;\n      tailSelector = cssHit.tailSelector || undefined;\n      absPrefix = cssHit.absPrefix;\n    }\n\n    const owningSess = ownerSession(page, targetFrameId);\n    const parentId = context.parentByFrame.get(targetFrameId);\n    const sameSessionAsParent =\n      !!parentId &&\n      ownerSession(page, parentId) === ownerSession(page, targetFrameId);\n    const { tagNameMap, xpathMap, scrollableMap } = await domMapsForSession(\n      owningSess,\n      targetFrameId,\n      pierce,\n      (fid, be) => `${page.getOrdinal(fid)}-${be}`,\n      sameSessionAsParent,\n    );\n\n    const { outline, urlMap, scopeApplied } = await a11yForFrame(\n      owningSess,\n      targetFrameId,\n      {\n        focusSelector: tailSelector || undefined,\n        tagNameMap,\n        experimental: options?.experimental ?? false,\n        scrollableMap,\n        encode: (backendNodeId) =>\n          `${page.getOrdinal(targetFrameId)}-${backendNodeId}`,\n      },\n    );\n\n    const scopedXpathMap: Record<string, string> = {};\n    const abs = absPrefix ?? \"\";\n    const isRoot = !abs || abs === \"/\";\n    if (isRoot) {\n      Object.assign(scopedXpathMap, xpathMap);\n    } else {\n      // Prefix relative XPaths so the scoped result matches the global encoding.\n      for (const [encId, xp] of Object.entries(xpathMap)) {\n        scopedXpathMap[encId] = prefixXPath(abs, xp);\n      }\n    }\n\n    const scopedUrlMap: Record<string, string> = { ...urlMap };\n\n    const snapshot: HybridSnapshot = {\n      combinedTree: outline,\n      combinedXpathMap: scopedXpathMap,\n      combinedUrlMap: scopedUrlMap,\n      perFrame: [\n        {\n          frameId: targetFrameId,\n          outline,\n          xpathMap,\n          urlMap,\n        },\n      ],\n    };\n\n    if (scopeApplied) {\n      return snapshot;\n    }\n\n    logScopeFallback();\n  } catch {\n    logScopeFallback();\n  }\n  return null;\n}\n\n/**\n * Step 2 – call DOM.getDocument once per unique CDP session and hydrate the\n * result so per-frame slices can share the structure. We key by session id\n * because same process iframes live inside the same session.\n */\nexport async function buildSessionIndexes(\n  page: Page,\n  frames: string[],\n  pierce: boolean,\n): Promise<Map<string, SessionDomIndex>> {\n  const sessionToIndex = new Map<string, SessionDomIndex>();\n  const sessionById = new Map<string, CDPSessionLike>();\n  for (const frameId of frames) {\n    const sess = ownerSession(page, frameId);\n    const sid = sess.id ?? \"root\";\n    if (!sessionById.has(sid)) sessionById.set(sid, sess);\n  }\n  for (const [sid, sess] of sessionById.entries()) {\n    const idx = await buildSessionDomIndex(sess, pierce);\n    sessionToIndex.set(sid, idx);\n  }\n  return sessionToIndex;\n}\n\n/**\n * Step 3 – derive per-frame DOM maps and accessibility outlines.\n * Each frame:\n *  - slices the shared session index down to its document root\n *  - builds frame-aware encoded ids (ordinal-backendNodeId)\n *  - collects tag/xpath/scrollability maps for DOM-based lookups\n *  - fetches its AX tree to produce outlines and URL maps\n */\nexport async function collectPerFrameMaps(\n  page: Page,\n  context: FrameContext,\n  sessionToIndex: Map<string, SessionDomIndex>,\n  options: SnapshotOptions | undefined,\n  pierce: boolean,\n  frameIds: string[],\n): Promise<{\n  perFrameMaps: Map<string, FrameDomMaps>;\n  perFrameOutlines: Array<{ frameId: string; outline: string }>;\n}> {\n  const perFrameMaps = new Map<string, FrameDomMaps>();\n  const perFrameOutlines: Array<{ frameId: string; outline: string }> = [];\n\n  for (const frameId of frameIds) {\n    const sess = ownerSession(page, frameId);\n    const sid = sess.id ?? \"root\";\n    let idx = sessionToIndex.get(sid);\n    if (!idx) {\n      idx = await buildSessionDomIndex(sess, pierce);\n      sessionToIndex.set(sid, idx);\n    }\n\n    const parentId = context.parentByFrame.get(frameId);\n    const sameSessionAsParent =\n      !!parentId && ownerSession(page, parentId) === sess;\n    let docRootBe = idx.rootBackend;\n    if (sameSessionAsParent) {\n      try {\n        const { backendNodeId } = await sess.send<{ backendNodeId?: number }>(\n          \"DOM.getFrameOwner\",\n          { frameId },\n        );\n        if (typeof backendNodeId === \"number\") {\n          const cdBe = idx.contentDocRootByIframe.get(backendNodeId);\n          if (typeof cdBe === \"number\") docRootBe = cdBe;\n        }\n      } catch {\n        //\n      }\n    }\n\n    const tagNameMap: Record<string, string> = {};\n    const xpathMap: Record<string, string> = {};\n    const scrollableMap: Record<string, boolean> = {};\n    const enc = (be: number) => `${page.getOrdinal(frameId)}-${be}`;\n    const baseAbs = idx.absByBe.get(docRootBe) ?? \"/\";\n\n    for (const [be, nodeAbs] of idx.absByBe.entries()) {\n      const nodeDocRoot = idx.docRootOf.get(be);\n      if (nodeDocRoot !== docRootBe) continue;\n\n      // Translate absolute XPaths into document-relative ones for this frame.\n      const rel = relativizeXPath(baseAbs, nodeAbs);\n      const key = enc(be);\n      xpathMap[key] = rel;\n      const tag = idx.tagByBe.get(be);\n      if (tag) tagNameMap[key] = tag;\n      if (idx.scrollByBe.get(be)) scrollableMap[key] = true;\n    }\n\n    const { outline, urlMap } = await a11yForFrame(sess, frameId, {\n      experimental: options?.experimental ?? false,\n      tagNameMap,\n      scrollableMap,\n      encode: (backendNodeId) => `${page.getOrdinal(frameId)}-${backendNodeId}`,\n    });\n\n    perFrameOutlines.push({ frameId, outline });\n    perFrameMaps.set(frameId, { tagNameMap, xpathMap, scrollableMap, urlMap });\n  }\n\n  return { perFrameMaps, perFrameOutlines };\n}\n\n/**\n * Step 4 – walk the frame tree (parent-first) to compute absolute prefixes for\n * every frame. The prefix is the absolute XPath of the iframe element hosting\n * the frame, so we can later convert relative XPaths into cross-frame ones.\n */\nexport async function computeFramePrefixes(\n  page: Page,\n  context: FrameContext,\n  perFrameMaps: Map<string, FrameDomMaps>,\n  frameIds: string[],\n): Promise<{\n  absPrefix: Map<string, string>;\n  iframeHostEncByChild: Map<string, string>;\n}> {\n  const absPrefix = new Map<string, string>();\n  const iframeHostEncByChild = new Map<string, string>();\n  absPrefix.set(context.rootId, \"\");\n  const included = new Set(frameIds);\n\n  const queue: string[] = [];\n  if (included.has(context.rootId)) {\n    queue.push(context.rootId);\n  }\n\n  while (queue.length) {\n    const parent = queue.shift()!;\n    const parentAbs = absPrefix.get(parent)!;\n\n    for (const child of context.frames) {\n      if (!included.has(child)) continue;\n      if (context.parentByFrame.get(child) !== parent) continue;\n      queue.push(child);\n\n      const parentSess = parentSession(page, context.parentByFrame, child);\n\n      const ownerBackendNodeId = await (async () => {\n        try {\n          const { backendNodeId } = await parentSess.send<{\n            backendNodeId?: number;\n          }>(\"DOM.getFrameOwner\", { frameId: child });\n          return backendNodeId;\n        } catch {\n          return undefined;\n        }\n      })();\n\n      if (!ownerBackendNodeId) {\n        // OOPIFs resolved via a different session inherit the parent prefix.\n        absPrefix.set(child, parentAbs);\n        continue;\n      }\n\n      const parentDom = perFrameMaps.get(parent);\n      const iframeEnc = `${page.getOrdinal(parent)}-${ownerBackendNodeId}`;\n      const iframeXPath = parentDom?.xpathMap[iframeEnc];\n\n      const childAbs = iframeXPath\n        ? prefixXPath(parentAbs || \"/\", iframeXPath)\n        : parentAbs;\n\n      absPrefix.set(child, childAbs);\n      iframeHostEncByChild.set(child, iframeEnc);\n    }\n  }\n\n  return { absPrefix, iframeHostEncByChild };\n}\n\n/**\n * Step 5 – merge per-frame maps into the combined snapshot payload. We prefix\n * each frame's relative XPaths with the absolute path collected in step 4,\n * merge URL maps, and stitch text outlines by nesting child trees under the\n * encoded id of their parent iframe host.\n */\nexport function mergeFramesIntoSnapshot(\n  context: FrameContext,\n  perFrameMaps: Map<string, FrameDomMaps>,\n  perFrameOutlines: Array<{ frameId: string; outline: string }>,\n  absPrefix: Map<string, string>,\n  iframeHostEncByChild: Map<string, string>,\n  frameIds: string[],\n): HybridSnapshot {\n  const combinedXpathMap: Record<string, string> = {};\n  const combinedUrlMap: Record<string, string> = {};\n\n  for (const frameId of frameIds) {\n    const maps = perFrameMaps.get(frameId);\n    if (!maps) continue;\n\n    const abs = absPrefix.get(frameId) ?? \"\";\n    const isRoot = abs === \"\" || abs === \"/\";\n\n    if (isRoot) {\n      Object.assign(combinedXpathMap, maps.xpathMap);\n      Object.assign(combinedUrlMap, maps.urlMap);\n      continue;\n    }\n\n    for (const [encId, xp] of Object.entries(maps.xpathMap)) {\n      combinedXpathMap[encId] = prefixXPath(abs, xp);\n    }\n    Object.assign(combinedUrlMap, maps.urlMap);\n  }\n\n  const idToTree = new Map<string, string>();\n  for (const { frameId, outline } of perFrameOutlines) {\n    const parentEnc = iframeHostEncByChild.get(frameId);\n    // The key is the parent iframe's encoded id so injectSubtrees can nest lines.\n    if (parentEnc) idToTree.set(parentEnc, outline);\n  }\n\n  const rootOutline =\n    perFrameOutlines.find((o) => o.frameId === context.rootId)?.outline ??\n    perFrameOutlines[0]?.outline ??\n    \"\";\n  const combinedTree = injectSubtrees(rootOutline, idToTree);\n\n  return {\n    combinedTree,\n    combinedXpathMap,\n    combinedUrlMap,\n    perFrame: perFrameOutlines.map(({ frameId, outline }) => {\n      const maps = perFrameMaps.get(frameId);\n      return {\n        frameId,\n        outline,\n        xpathMap: maps?.xpathMap ?? {},\n        urlMap: maps?.urlMap ?? {},\n      };\n    }),\n  };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/coordinateResolver.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"../../cdp.js\";\nimport { Page } from \"../../page.js\";\nimport { executionContexts } from \"../../executionContextRegistry.js\";\nimport { a11yScriptSources } from \"../../../dom/build/a11yScripts.generated.js\";\nimport { buildA11yInvocation } from \"../../a11yInvocation.js\";\nimport type { ResolvedLocation } from \"../../../types/private/snapshot.js\";\nimport { listChildrenOf } from \"./focusSelectors.js\";\nimport { buildAbsoluteXPathFromChain } from \"./xpathUtils.js\";\n\n/**\n * Resolve deepest node for a page coordinate and compute its absolute XPath across frames.\n * More efficient than building a full hybrid snapshot when only a single node’s XPath is needed.\n */\nexport async function resolveXpathForLocation(\n  page: Page,\n  x: number,\n  y: number,\n): Promise<ResolvedLocation | null> {\n  const tree = page.getFullFrameTree();\n  const parentByFrame = new Map<string, string | null>();\n  (function index(n: Protocol.Page.FrameTree, parent: string | null) {\n    parentByFrame.set(n.frame.id, parent);\n    for (const c of n.childFrames ?? []) index(c, n.frame.id);\n  })(tree, null);\n\n  const iframeChain: Array<{\n    parentSession: CDPSessionLike;\n    iframeBackendNodeId: number;\n  }> = [];\n\n  let curFrameId = page.mainFrameId();\n  let curSession = page.getSessionForFrame(curFrameId);\n  let curX = x;\n  let curY = y;\n\n  for (let depth = 0; depth < 8; depth++) {\n    try {\n      await curSession.send(\"DOM.enable\").catch(() => {});\n\n      let sx = 0;\n      let sy = 0;\n      try {\n        await curSession.send(\"Runtime.enable\").catch(() => {});\n        const ctxId = await executionContexts\n          .waitForMainWorld(curSession, curFrameId)\n          .catch(() => {});\n        const scrollExpr = buildA11yInvocation(\"getScrollOffsets\", []);\n        const evalParams = ctxId\n          ? {\n              contextId: ctxId,\n              expression: scrollExpr,\n              returnByValue: true,\n            }\n          : { expression: scrollExpr, returnByValue: true };\n        const { result } = await curSession.send<{\n          result: { value?: { sx?: number; sy?: number } };\n        }>(\"Runtime.evaluate\", evalParams);\n        sx = Number(result?.value?.sx ?? 0);\n        sy = Number(result?.value?.sy ?? 0);\n      } catch {\n        //\n      }\n      const xi = Math.max(0, Math.floor(curX + sx));\n      const yi = Math.max(0, Math.floor(curY + sy));\n\n      let res: { backendNodeId?: number; frameId?: string } | undefined;\n      try {\n        res = await curSession.send<{\n          backendNodeId?: number;\n          frameId?: string;\n        }>(\"DOM.getNodeForLocation\", {\n          x: xi,\n          y: yi,\n          includeUserAgentShadowDOM: false,\n          ignorePointerEventsNone: false,\n        });\n      } catch {\n        return null;\n      }\n\n      const be = res?.backendNodeId;\n      const reportedFrameId = res?.frameId;\n      if (\n        typeof be === \"number\" &&\n        reportedFrameId &&\n        reportedFrameId !== curFrameId\n      ) {\n        const abs = await buildAbsoluteXPathFromChain(\n          iframeChain,\n          curSession,\n          be,\n        );\n        return abs\n          ? { frameId: reportedFrameId, backendNodeId: be, absoluteXPath: abs }\n          : null;\n      }\n\n      if (typeof be !== \"number\") return null;\n\n      let matchedChild: string | undefined;\n      for (const fid of listChildrenOf(parentByFrame, curFrameId)) {\n        try {\n          const { backendNodeId } = await curSession.send<{\n            backendNodeId?: number;\n          }>(\"DOM.getFrameOwner\", { frameId: fid });\n          if (backendNodeId === be) {\n            matchedChild = fid;\n            break;\n          }\n        } catch {\n          continue;\n        }\n      }\n\n      if (!matchedChild) {\n        const abs = await buildAbsoluteXPathFromChain(\n          iframeChain,\n          curSession,\n          be,\n        );\n        return abs\n          ? { frameId: curFrameId, backendNodeId: be, absoluteXPath: abs }\n          : null;\n      }\n\n      iframeChain.push({\n        parentSession: curSession,\n        iframeBackendNodeId: be,\n      });\n\n      let left = 0;\n      let top = 0;\n      try {\n        const { object } = await curSession.send<{\n          object: { objectId?: string };\n        }>(\"DOM.resolveNode\", { backendNodeId: be });\n        const objectId = object?.objectId;\n        if (objectId) {\n          const { result } = await curSession.send<{\n            result: { value?: { left: number; top: number } };\n          }>(\"Runtime.callFunctionOn\", {\n            objectId,\n            functionDeclaration: a11yScriptSources.getBoundingRectLite,\n            returnByValue: true,\n          });\n          left = Number(result?.value?.left ?? 0);\n          top = Number(result?.value?.top ?? 0);\n          await curSession\n            .send(\"Runtime.releaseObject\", { objectId })\n            .catch(() => {});\n        }\n      } catch {\n        //\n      }\n      curX = Math.max(0, curX - left);\n      curY = Math.max(0, curY - top);\n      curFrameId = matchedChild;\n      curSession = page.getSessionForFrame(curFrameId);\n    } catch {\n      return null;\n    }\n  }\n  return null;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/domTree.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"../../cdp.js\";\nimport { StagehandDomProcessError } from \"../../../types/public/sdkErrors.js\";\nimport type { SessionDomIndex } from \"../../../types/private/snapshot.js\";\nimport {\n  buildChildXPathSegments,\n  joinXPath,\n  normalizeXPath,\n} from \"./xpathUtils.js\";\n\n// starting from infinite depth (-1), exponentially shrink down to 1\nconst DOM_DEPTH_ATTEMPTS = [-1, 256, 128, 64, 32, 16, 8, 4, 2, 1];\nconst DESCRIBE_DEPTH_ATTEMPTS = [-1, 64, 32, 16, 8, 4, 2, 1];\n\n/** Identify CDP failures caused by deep DOM trees blowing the CBOR encoder stack. */\nfunction isCborStackError(message: string): boolean {\n  return message.includes(\"CBOR: stack limit exceeded\");\n}\n\n/**\n * Determine if CDP truncated a node's children when streaming the DOM tree.\n * childNodeCount stays accurate even when `children` are omitted; we use this to\n * decide whether DOM.describeNode must be re-run for that node.\n */\nexport function shouldExpandNode(node: Protocol.DOM.Node): boolean {\n  const declaredChildren = node.childNodeCount ?? 0;\n  const realizedChildren = node.children?.length ?? 0;\n  return declaredChildren > realizedChildren;\n}\n\n/** Merge an expanded DescribeNode payload back into the original shallow node. */\nexport function mergeDomNodes(\n  target: Protocol.DOM.Node,\n  source: Protocol.DOM.Node,\n): void {\n  target.childNodeCount = source.childNodeCount ?? target.childNodeCount;\n  target.children = source.children ?? target.children;\n  target.shadowRoots = source.shadowRoots ?? target.shadowRoots;\n  target.contentDocument = source.contentDocument ?? target.contentDocument;\n}\n\n/** Helper that returns every nested collection we recurse through uniformly. */\nexport function collectDomTraversalTargets(\n  node: Protocol.DOM.Node,\n): Protocol.DOM.Node[] {\n  const targets: Protocol.DOM.Node[] = [];\n  if (node.children) targets.push(...node.children);\n  if (node.shadowRoots) targets.push(...node.shadowRoots);\n  if (node.contentDocument) targets.push(node.contentDocument);\n  return targets;\n}\n\n/**\n * Rehydrate a truncated DOM tree by repeatedly calling DOM.describeNode with\n * decreasing depths. Any non-CBOR failure is surfaced as a StagehandDomProcessError.\n */\nexport async function hydrateDomTree(\n  session: CDPSessionLike,\n  root: Protocol.DOM.Node,\n  pierce: boolean,\n): Promise<void> {\n  const stack: Protocol.DOM.Node[] = [root];\n  const expandedNodeIds = new Set<number>();\n  const expandedBackendIds = new Set<number>();\n\n  while (stack.length) {\n    const node = stack.pop()!;\n    const nodeId =\n      typeof node.nodeId === \"number\" && node.nodeId > 0\n        ? node.nodeId\n        : undefined;\n    const backendId =\n      typeof node.backendNodeId === \"number\" && node.backendNodeId > 0\n        ? node.backendNodeId\n        : undefined;\n\n    const seenByNode = nodeId ? expandedNodeIds.has(nodeId) : false;\n    const seenByBackend =\n      !nodeId && backendId ? expandedBackendIds.has(backendId) : false;\n    if (seenByNode || seenByBackend) continue;\n    if (nodeId) expandedNodeIds.add(nodeId);\n    else if (backendId) expandedBackendIds.add(backendId);\n\n    const needsExpansion = shouldExpandNode(node);\n    if (needsExpansion && (nodeId || backendId)) {\n      const describeParamsBase = nodeId\n        ? { nodeId }\n        : { backendNodeId: backendId! };\n      let expanded = false;\n      for (const depth of DESCRIBE_DEPTH_ATTEMPTS) {\n        try {\n          const described =\n            await session.send<Protocol.DOM.DescribeNodeResponse>(\n              \"DOM.describeNode\",\n              {\n                ...describeParamsBase,\n                depth,\n                pierce,\n              },\n            );\n          mergeDomNodes(node, described.node);\n          if (!nodeId && described.node.nodeId && described.node.nodeId > 0) {\n            node.nodeId = described.node.nodeId;\n            expandedNodeIds.add(described.node.nodeId);\n          }\n          expanded = true;\n          break;\n        } catch (err) {\n          const message = err instanceof Error ? err.message : String(err);\n          if (isCborStackError(message)) {\n            continue;\n          }\n          const identifier = nodeId ?? backendId ?? \"unknown\";\n          throw new StagehandDomProcessError(\n            `Failed to expand DOM node ${identifier}: ${String(err)}`,\n          );\n        }\n      }\n      if (!expanded) {\n        const identifier = nodeId ?? backendId ?? \"unknown\";\n        throw new StagehandDomProcessError(\n          `Unable to expand DOM node ${identifier} after describeNode depth retries`,\n        );\n      }\n    }\n\n    for (const child of collectDomTraversalTargets(node)) {\n      stack.push(child);\n    }\n  }\n}\n\n/**\n * Attempt DOM.getDocument with progressively shallower depths until CBOR stops\n * complaining. When a shallower snapshot is returned we hydrate the missing\n * branches so downstream DOM traversals see the full tree shape.\n */\nexport async function getDomTreeWithFallback(\n  session: CDPSessionLike,\n  pierce: boolean,\n): Promise<Protocol.DOM.Node> {\n  let lastCborMessage = \"\";\n\n  for (const depth of DOM_DEPTH_ATTEMPTS) {\n    try {\n      const { root } = await session.send<{ root: Protocol.DOM.Node }>(\n        \"DOM.getDocument\",\n        { depth, pierce },\n      );\n\n      if (depth !== -1) {\n        await hydrateDomTree(session, root, pierce);\n      }\n\n      return root;\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      if (isCborStackError(message)) {\n        lastCborMessage = message;\n        continue;\n      }\n      throw err;\n    }\n  }\n\n  throw new StagehandDomProcessError(\n    lastCborMessage\n      ? `CDP DOM.getDocument failed after adaptive depth retries: ${lastCborMessage}`\n      : \"CDP DOM.getDocument failed after adaptive depth retries.\",\n  );\n}\n\n/**\n * Build tag name and XPath maps for a single frame session.\n * EncodedId is produced by a frame-aware encoder provided by the caller.\n */\nexport async function domMapsForSession(\n  session: CDPSessionLike,\n  frameId: string,\n  pierce: boolean,\n  encode: (fid: string, backendNodeId: number) => string,\n  attemptOwnerLookup = true,\n): Promise<{\n  tagNameMap: Record<string, string>;\n  xpathMap: Record<string, string>;\n  scrollableMap: Record<string, boolean>;\n}> {\n  await session.send(\"DOM.enable\").catch(() => {});\n  const root = await getDomTreeWithFallback(session, pierce);\n\n  let startNode: Protocol.DOM.Node = root;\n  if (attemptOwnerLookup) {\n    try {\n      const owner = await session.send<{ backendNodeId?: number }>(\n        \"DOM.getFrameOwner\",\n        { frameId },\n      );\n      const ownerBackendId = owner.backendNodeId;\n      if (typeof ownerBackendId === \"number\") {\n        const ownerEl = findNodeByBackendId(root, ownerBackendId);\n        if (ownerEl?.contentDocument) {\n          startNode = ownerEl.contentDocument;\n        }\n      }\n    } catch {\n      // OOPIF or race → keep startNode = root\n    }\n  }\n\n  const tagNameMap: Record<string, string> = {};\n  const xpathMap: Record<string, string> = {};\n  const scrollableMap: Record<string, boolean> = {};\n\n  type StackEntry = { node: Protocol.DOM.Node; xpath: string };\n  const stack: StackEntry[] = [{ node: startNode, xpath: \"\" }];\n\n  while (stack.length) {\n    const { node, xpath } = stack.pop()!;\n\n    if (node.backendNodeId) {\n      const encId = encode(frameId, node.backendNodeId);\n      tagNameMap[encId] = String(node.nodeName).toLowerCase();\n      xpathMap[encId] = xpath || \"/\";\n      const isScrollable = node?.isScrollable === true;\n      if (isScrollable) scrollableMap[encId] = true;\n    }\n\n    const kids = node.children ?? [];\n    if (kids.length) {\n      const segs = buildChildXPathSegments(kids);\n      for (let i = kids.length - 1; i >= 0; i--) {\n        const child = kids[i]!;\n        const step = segs[i]!;\n        stack.push({\n          node: child,\n          xpath: joinXPath(xpath, step),\n        });\n      }\n    }\n\n    for (const sr of node.shadowRoots ?? []) {\n      stack.push({\n        node: sr,\n        xpath: joinXPath(xpath, \"//\"),\n      });\n    }\n  }\n\n  return { tagNameMap, xpathMap, scrollableMap };\n}\n\n/**\n * Build an index of absolute XPath/tag metadata for an entire CDP session.\n * Once the index is cached, per-frame slices are derived without extra DOM\n * calls, which keeps snapshot capture linear in the number of frames.\n */\nexport async function buildSessionDomIndex(\n  session: CDPSessionLike,\n  pierce: boolean,\n): Promise<SessionDomIndex> {\n  await session.send(\"DOM.enable\").catch(() => {});\n  const root = await getDomTreeWithFallback(session, pierce);\n\n  const absByBe = new Map<number, string>();\n  const tagByBe = new Map<number, string>();\n  const scrollByBe = new Map<number, boolean>();\n  const docRootOf = new Map<number, number>();\n  const contentDocRootByIframe = new Map<number, number>();\n\n  type Entry = { node: Protocol.DOM.Node; xp: string; docRootBe: number };\n  const rootBe = root.backendNodeId!;\n  const stack: Entry[] = [{ node: root, xp: \"/\", docRootBe: rootBe }];\n\n  while (stack.length) {\n    const { node, xp, docRootBe } = stack.pop()!;\n    if (node.backendNodeId) {\n      absByBe.set(node.backendNodeId, xp || \"/\");\n      tagByBe.set(node.backendNodeId, String(node.nodeName).toLowerCase());\n      if (node?.isScrollable === true) scrollByBe.set(node.backendNodeId, true);\n      docRootOf.set(node.backendNodeId, docRootBe);\n    }\n\n    const kids = node.children ?? [];\n    if (kids.length) {\n      const segs = buildChildXPathSegments(kids);\n      for (let i = kids.length - 1; i >= 0; i--) {\n        const child = kids[i]!;\n        const step = segs[i]!;\n        stack.push({ node: child, xp: joinXPath(xp, step), docRootBe });\n      }\n    }\n\n    for (const sr of node.shadowRoots ?? []) {\n      stack.push({ node: sr, xp: joinXPath(xp, \"//\"), docRootBe });\n    }\n\n    const cd = node.contentDocument as Protocol.DOM.Node | undefined;\n    if (cd && typeof cd.backendNodeId === \"number\") {\n      contentDocRootByIframe.set(node.backendNodeId!, cd.backendNodeId);\n      stack.push({ node: cd, xp, docRootBe: cd.backendNodeId });\n    }\n  }\n\n  return {\n    rootBackend: rootBe,\n    absByBe,\n    tagByBe,\n    scrollByBe,\n    docRootOf,\n    contentDocRootByIframe,\n  };\n}\n\n/**\n * Relativize an absolute XPath against a document root's absolute path.\n * When the node lives outside the document we return the absolute path as-is.\n */\nexport function relativizeXPath(baseAbs: string, nodeAbs: string): string {\n  const base = normalizeXPath(baseAbs);\n  const abs = normalizeXPath(nodeAbs);\n  if (abs === base) return \"/\";\n  if (abs.startsWith(base)) {\n    const tail = abs.slice(base.length);\n    if (!tail) return \"/\";\n    return tail.startsWith(\"/\") || tail.startsWith(\"//\") ? tail : `/${tail}`;\n  }\n  if (base === \"/\") return abs;\n  return abs;\n}\n\n/** Find a node by backendNodeId inside a DOM.getDocument tree. */\nexport function findNodeByBackendId(\n  root: Protocol.DOM.Node,\n  backendNodeId: number,\n): Protocol.DOM.Node | undefined {\n  const stack: Protocol.DOM.Node[] = [root];\n  while (stack.length) {\n    const n = stack.pop()!;\n    if (n.backendNodeId === backendNodeId) return n;\n    if (n.children) for (const c of n.children) stack.push(c);\n    if (n.shadowRoots) for (const s of n.shadowRoots) stack.push(s);\n  }\n  return undefined;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/focusSelectors.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"../../cdp.js\";\nimport { Page } from \"../../page.js\";\nimport { executionContexts } from \"../../executionContextRegistry.js\";\nimport { buildLocatorInvocation } from \"../../locatorInvocation.js\";\nimport { StagehandIframeError } from \"../../../types/public/sdkErrors.js\";\nimport type {\n  Axis,\n  FrameParentIndex,\n  ResolvedCssFocus,\n  ResolvedFocusFrame,\n  Step,\n} from \"../../../types/private/snapshot.js\";\nimport { prefixXPath } from \"./xpathUtils.js\";\n\n/**\n * Parse a cross-frame XPath into discrete steps. Each step tracks whether it\n * represents a descendant hop (“//”) or a single-child hop (“/”).\n */\nexport function parseXPathToSteps(path: string): Step[] {\n  const s = path.trim();\n  let i = 0;\n  const steps: Step[] = [];\n  while (i < s.length) {\n    let axis: Axis = \"child\";\n    if (s.startsWith(\"//\", i)) {\n      axis = \"desc\";\n      i += 2;\n    } else if (s[i] === \"/\") {\n      axis = \"child\";\n      i += 1;\n    }\n\n    const start = i;\n    while (i < s.length && s[i] !== \"/\") i++;\n    const raw = s.slice(start, i).trim();\n    if (!raw) continue;\n    const name = raw.replace(/\\[\\d+\\]\\s*$/u, \"\").toLowerCase();\n    steps.push({ axis, raw, name });\n  }\n  return steps;\n}\n\n/** Rebuild an XPath string from parsed steps. */\nexport function buildXPathFromSteps(steps: ReadonlyArray<Step>): string {\n  let out = \"\";\n  for (const st of steps) {\n    out += st.axis === \"desc\" ? \"//\" : \"/\";\n    out += st.raw;\n  }\n  return out || \"/\";\n}\n\nexport const IFRAME_STEP_RE = /^i?frame(?:\\[\\d+])?$/i;\n\n/**\n * Given a cross-frame XPath, walk iframe steps to resolve:\n * - the target frameId (last iframe hop)\n * - the tail XPath (within the target frame)\n * - the absolute XPath prefix up to the iframe element hosting that frame\n */\nexport async function resolveFocusFrameAndTail(\n  page: Page,\n  absoluteXPath: string,\n  parentByFrame: FrameParentIndex,\n  rootId: string,\n): Promise<ResolvedFocusFrame> {\n  const steps = parseXPathToSteps(absoluteXPath);\n  let ctxFrameId = rootId;\n  let buf: Step[] = [];\n  let absPrefix = \"\";\n\n  const flushIntoChild = async (): Promise<void> => {\n    if (!buf.length) return;\n    const selectorForIframe = buildXPathFromSteps(buf);\n    const parentSess = page.getSessionForFrame(ctxFrameId);\n    const objectId = await resolveObjectIdForXPath(\n      parentSess,\n      selectorForIframe,\n      ctxFrameId,\n    );\n    if (!objectId)\n      throw new StagehandIframeError(\n        selectorForIframe,\n        \"Failed to resolve iframe element by XPath\",\n      );\n\n    try {\n      await parentSess.send(\"DOM.enable\").catch(() => {});\n      const desc = await parentSess.send<Protocol.DOM.DescribeNodeResponse>(\n        \"DOM.describeNode\",\n        { objectId },\n      );\n      const iframeBackendNodeId = desc.node.backendNodeId;\n\n      let childFrameId: string | undefined;\n      for (const fid of listChildrenOf(parentByFrame, ctxFrameId)) {\n        try {\n          const { backendNodeId } = await parentSess.send<{\n            backendNodeId: number;\n          }>(\"DOM.getFrameOwner\", { frameId: fid });\n          if (backendNodeId === iframeBackendNodeId) {\n            childFrameId = fid;\n            break;\n          }\n        } catch {\n          continue;\n        }\n      }\n      if (!childFrameId)\n        throw new StagehandIframeError(\n          selectorForIframe,\n          \"Could not map iframe to child frameId\",\n        );\n\n      absPrefix = prefixXPath(absPrefix || \"/\", selectorForIframe);\n      ctxFrameId = childFrameId;\n    } finally {\n      await parentSess\n        .send(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n\n    buf = [];\n  };\n\n  for (const st of steps) {\n    buf.push(st);\n    if (IFRAME_STEP_RE.test(st.name)) {\n      await flushIntoChild();\n    }\n  }\n\n  const tailXPath = buildXPathFromSteps(buf);\n  return { targetFrameId: ctxFrameId, tailXPath, absPrefix };\n}\n\n/** Resolve focus frame and tail CSS selector using '>>' to hop iframes. */\nexport async function resolveCssFocusFrameAndTail(\n  page: Page,\n  rawSelector: string,\n  parentByFrame: FrameParentIndex,\n  rootId: string,\n): Promise<ResolvedCssFocus> {\n  const parts = rawSelector\n    .split(\">>\")\n    .map((s) => s.trim())\n    .filter(Boolean);\n  let ctxFrameId = rootId;\n  const absPrefix = \"\";\n\n  for (let i = 0; i < Math.max(0, parts.length - 1); i++) {\n    const parentSess = page.getSessionForFrame(ctxFrameId);\n    const objectId = await resolveObjectIdForCss(\n      parentSess,\n      parts[i]!,\n      ctxFrameId,\n    );\n    if (!objectId)\n      throw new StagehandIframeError(\n        parts[i]!,\n        \"Failed to resolve iframe via CSS hop\",\n      );\n    try {\n      await parentSess.send(\"DOM.enable\").catch(() => {});\n      const desc = await parentSess.send<Protocol.DOM.DescribeNodeResponse>(\n        \"DOM.describeNode\",\n        { objectId },\n      );\n      const iframeBackendNodeId = desc.node.backendNodeId;\n      let childFrameId: string | undefined;\n      for (const fid of listChildrenOf(parentByFrame, ctxFrameId)) {\n        try {\n          const { backendNodeId } = await parentSess.send<{\n            backendNodeId: number;\n          }>(\"DOM.getFrameOwner\", { frameId: fid });\n          if (backendNodeId === iframeBackendNodeId) {\n            childFrameId = fid;\n            break;\n          }\n        } catch {\n          continue;\n        }\n      }\n      if (!childFrameId)\n        throw new StagehandIframeError(\n          parts[i]!,\n          \"Could not map CSS iframe hop to child frameId\",\n        );\n      ctxFrameId = childFrameId;\n    } finally {\n      await parentSess\n        .send(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  const tailSelector = parts[parts.length - 1] ?? \"*\";\n  return { targetFrameId: ctxFrameId, tailSelector, absPrefix };\n}\n\n/** Resolve an XPath to a Runtime remoteObjectId in the given CDP session. */\nexport async function resolveObjectIdForXPath(\n  session: CDPSessionLike,\n  xpath: string,\n  frameId?: string,\n): Promise<string | null> {\n  let contextId: number | undefined;\n  try {\n    if (frameId) {\n      contextId = await executionContexts\n        .waitForMainWorld(session, frameId, 800)\n        .catch(\n          () => executionContexts.getMainWorld(session, frameId) ?? undefined,\n        );\n    }\n  } catch {\n    contextId = undefined;\n  }\n  const expr = buildLocatorInvocation(\"resolveXPathMainWorld\", [\n    JSON.stringify(xpath),\n    \"0\",\n  ]);\n  const { result, exceptionDetails } = await session.send<{\n    result: { objectId?: string | undefined };\n    exceptionDetails?: Protocol.Runtime.ExceptionDetails;\n  }>(\"Runtime.evaluate\", {\n    expression: expr,\n    returnByValue: false,\n    contextId,\n    awaitPromise: true,\n  });\n  if (exceptionDetails) return null;\n  return result?.objectId ?? null;\n}\n\n/** Resolve a CSS selector (supports '>>' within the same frame only) to a Runtime objectId. */\nexport async function resolveObjectIdForCss(\n  session: CDPSessionLike,\n  selector: string,\n  frameId?: string,\n): Promise<string | null> {\n  let contextId: number | undefined;\n  try {\n    if (frameId) {\n      contextId = await executionContexts\n        .waitForMainWorld(session, frameId, 800)\n        .catch(\n          () => executionContexts.getMainWorld(session, frameId) ?? undefined,\n        );\n    }\n  } catch {\n    contextId = undefined;\n  }\n  const primaryExpr = buildLocatorInvocation(\"resolveCssSelector\", [\n    JSON.stringify(selector),\n    \"0\",\n  ]);\n  const fallbackExpr = buildLocatorInvocation(\"resolveCssSelectorPierce\", [\n    JSON.stringify(selector),\n    \"0\",\n  ]);\n\n  const evaluate = async (expression: string): Promise<string | null> => {\n    const { result, exceptionDetails } = await session.send<{\n      result: { objectId?: string | undefined };\n      exceptionDetails?: Protocol.Runtime.ExceptionDetails;\n    }>(\"Runtime.evaluate\", {\n      expression,\n      returnByValue: false,\n      contextId,\n      awaitPromise: true,\n    });\n    if (exceptionDetails) return null;\n    return result?.objectId ?? null;\n  };\n\n  const primary = await evaluate(primaryExpr);\n  if (primary) return primary;\n  return evaluate(fallbackExpr);\n}\n\nexport function listChildrenOf(\n  parentByFrame: FrameParentIndex,\n  parentId: string,\n): string[] {\n  const out: string[] = [];\n  for (const [fid, p] of parentByFrame.entries()) {\n    if (p === parentId) out.push(fid);\n  }\n  return out;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/index.ts",
    "content": "export { captureHybridSnapshot } from \"./capture.js\";\nexport { computeActiveElementXpath } from \"./activeElement.js\";\nexport { diffCombinedTrees } from \"./treeFormatUtils.js\";\nexport { resolveXpathForLocation } from \"./coordinateResolver.js\";\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/sessions.ts",
    "content": "import type { CDPSessionLike } from \"../../cdp.js\";\nimport { Page } from \"../../page.js\";\nimport type { FrameParentIndex } from \"../../../types/private/snapshot.js\";\n\n/**\n * Session helpers ensure DOM lookups are always executed against the session\n * that actually owns a frame. Keeping this logic centralized prevents subtle\n * bugs when OOPIF adoption changes session ownership mid-capture.\n */\n\n/** Return the owning session for a frame as registered on the Page. */\nexport function ownerSession(page: Page, frameId: string): CDPSessionLike {\n  return page.getSessionForFrame(frameId);\n}\n\n/**\n * DOM.getFrameOwner must be called against the parent frame's session.\n * This helper hides the lookup (including main-frame fallback) so callers\n * always reach for the correct connection.\n */\nexport function parentSession(\n  page: Page,\n  parentByFrame: FrameParentIndex,\n  frameId: string,\n): CDPSessionLike {\n  const parentId = parentByFrame.get(frameId) ?? null;\n  if (!parentId) {\n    return page.getSessionForFrame(frameId);\n  }\n  return page.getSessionForFrame(parentId);\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/treeFormatUtils.ts",
    "content": "import type { A11yNode } from \"../../../types/private/snapshot.js\";\n\n/**\n * Render a formatted outline (with encoded ids) for the accessibility tree.\n * Keeps indentation logic shared between modules so unit tests can cover these\n * pure formatting helpers without a full snapshot pipeline.\n */\nexport function formatTreeLine(node: A11yNode, level = 0): string {\n  const indent = \"  \".repeat(level);\n  const labelId = node.encodedId ?? node.nodeId;\n  const label = `[${labelId}] ${node.role}${node.name ? `: ${cleanText(node.name)}` : \"\"}`;\n  const kids =\n    node.children?.map((c) => formatTreeLine(c, level + 1)).join(\"\\n\") ?? \"\";\n  return kids ? `${indent}${label}\\n${kids}` : `${indent}${label}`;\n}\n\n/**\n * Inject each child frame outline under the parent's iframe node line.\n * Keys in `idToTree` are the parent's iframe encoded ids.\n */\nexport function injectSubtrees(\n  rootOutline: string,\n  idToTree: Map<string, string>,\n): string {\n  type Frame = { lines: string[]; i: number };\n  const out: string[] = [];\n  const visited = new Set<string>();\n  const stack: Frame[] = [{ lines: rootOutline.split(\"\\n\"), i: 0 }];\n\n  while (stack.length) {\n    const top = stack[stack.length - 1];\n    if (top.i >= top.lines.length) {\n      stack.pop();\n      continue;\n    }\n\n    const raw = top.lines[top.i++];\n    out.push(raw);\n\n    const indent = raw.match(/^(\\s*)/)?.[1] ?? \"\";\n    const content = raw.slice(indent.length);\n\n    const m = content.match(/^\\[([^\\]]+)]/);\n    if (!m) continue;\n\n    const encId = m[1]!;\n    const childOutline = idToTree.get(encId);\n    if (!childOutline || visited.has(encId)) continue;\n\n    visited.add(encId);\n\n    const fullyInjectedChild = injectSubtrees(childOutline, idToTree);\n    out.push(indentBlock(fullyInjectedChild.trimEnd(), indent + \"  \"));\n  }\n\n  return out.join(\"\\n\");\n}\n\nexport function indentBlock(block: string, indent: string): string {\n  if (!block) return \"\";\n  return block\n    .split(\"\\n\")\n    .map((line) => (line.length ? indent + line : indent + line))\n    .join(\"\\n\");\n}\n\n/**\n * Return the lines that appear in `nextTree` but not in `prevTree`.\n * Comparison is done line-by-line, ignoring leading whitespace in both trees.\n * The returned block is re-indented so the minimal indent becomes column 0.\n */\nexport function diffCombinedTrees(prevTree: string, nextTree: string): string {\n  const prevSet = new Set(\n    (prevTree || \"\")\n      .split(\"\\n\")\n      .map((l) => l.trim())\n      .filter((l) => l.length > 0),\n  );\n\n  const nextLines = (nextTree || \"\").split(\"\\n\");\n  const added: string[] = [];\n  for (const line of nextLines) {\n    const core = line.trim();\n    if (!core) continue;\n    if (!prevSet.has(core)) added.push(line);\n  }\n\n  if (added.length === 0) return \"\";\n\n  let minIndent = Infinity;\n  for (const l of added) {\n    if (!l.trim()) continue;\n    const m = l.match(/^\\s*/);\n    const indentLen = m ? m[0]!.length : 0;\n    if (indentLen < minIndent) minIndent = indentLen;\n  }\n  if (!isFinite(minIndent)) minIndent = 0;\n\n  const out = added.map((l) =>\n    l.length >= minIndent ? l.slice(minIndent) : l,\n  );\n  return out.join(\"\\n\");\n}\n\n/**\n * Remove whitespace noise and invisible code points before rendering names.\n */\nexport function cleanText(input: string): string {\n  const PUA_START = 0xe000;\n  const PUA_END = 0xf8ff;\n  const NBSP = new Set<number>([0x00a0, 0x202f, 0x2007, 0xfeff]);\n\n  let out = \"\";\n  let prevSpace = false;\n  for (let i = 0; i < input.length; i++) {\n    const code = input.charCodeAt(i);\n    if (code >= PUA_START && code <= PUA_END) continue;\n    if (NBSP.has(code)) {\n      if (!prevSpace) {\n        out += \" \";\n        prevSpace = true;\n      }\n      continue;\n    }\n    out += input[i];\n    prevSpace = input[i] === \" \";\n  }\n  return out.trim();\n}\n\n/**\n * Collapse all whitespace runs in a string to a single space without trimming.\n * Exported for pruning routines that need the same normalization.\n */\nexport function normaliseSpaces(s: string): string {\n  let out = \"\";\n  let inWs = false;\n  for (let i = 0; i < s.length; i++) {\n    const ch = s[i]!;\n    const isWs = /\\s/.test(ch);\n    if (isWs) {\n      if (!inWs) {\n        out += \" \";\n        inWs = true;\n      }\n    } else {\n      out += ch;\n      inWs = false;\n    }\n  }\n  return out;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11y/snapshot/xpathUtils.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"../../cdp.js\";\nimport { a11yScriptSources } from \"../../../dom/build/a11yScripts.generated.js\";\n\n/**\n * Build the absolute XPath for a node by walking through every iframe host\n * we've traversed so far followed by the leaf backend node.\n */\nexport async function buildAbsoluteXPathFromChain(\n  chain: Array<{\n    parentSession: CDPSessionLike;\n    iframeBackendNodeId: number;\n  }>,\n  leafSession: CDPSessionLike,\n  leafBackendNodeId: number,\n): Promise<string | null> {\n  let prefix = \"\";\n  for (const step of chain) {\n    const xp = await absoluteXPathForBackendNode(\n      step.parentSession,\n      step.iframeBackendNodeId,\n    );\n    if (!xp) continue;\n    prefix = prefix ? prefixXPath(prefix, xp) : normalizeXPath(xp);\n  }\n  const leaf = await absoluteXPathForBackendNode(\n    leafSession,\n    leafBackendNodeId,\n  );\n  if (!leaf) return prefix || \"/\";\n  return prefix ? prefixXPath(prefix, leaf) : normalizeXPath(leaf);\n}\n\n/**\n * Resolve a backend node to an absolute XPath within the provided session.\n * The CDP Runtime is used so we can invoke a small helper that walks the DOM.\n */\nexport async function absoluteXPathForBackendNode(\n  session: CDPSessionLike,\n  backendNodeId: number,\n): Promise<string | null> {\n  try {\n    const { object } = await session.send<{ object: { objectId?: string } }>(\n      \"DOM.resolveNode\",\n      { backendNodeId },\n    );\n    const objectId = object?.objectId;\n    if (!objectId) return null;\n\n    const { result } = await session.send<{ result: { value?: string } }>(\n      \"Runtime.callFunctionOn\",\n      {\n        objectId,\n        functionDeclaration: a11yScriptSources.nodeToAbsoluteXPath,\n        returnByValue: true,\n      },\n    );\n    await session.send(\"Runtime.releaseObject\", { objectId }).catch(() => {});\n    return typeof result?.value === \"string\" && result.value\n      ? result.value\n      : null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Prefix `child` XPath with an absolute iframe path `parentAbs`.\n * Handles root slashes and shadow hops (“//”) cleanly.\n */\nexport function prefixXPath(parentAbs: string, child: string): string {\n  const p = parentAbs === \"/\" ? \"\" : parentAbs.replace(/\\/$/, \"\");\n  if (!child || child === \"/\") return p || \"/\";\n  if (child.startsWith(\"//\"))\n    return p ? `${p}//${child.slice(2)}` : `//${child.slice(2)}`;\n  const c = child.replace(/^\\//, \"\");\n  return p ? `${p}/${c}` : `/${c}`;\n}\n\n/** Normalize an XPath: strip `xpath=`, ensure leading '/', remove trailing '/'. */\nexport function normalizeXPath(x?: string): string {\n  if (!x) return \"\";\n  let s = x.trim().replace(/^xpath=/i, \"\");\n  if (!s.startsWith(\"/\")) s = \"/\" + s;\n  if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n  return s;\n}\n\n/** Build per-sibling XPath steps for DOM traversal. */\nexport function buildChildXPathSegments(kids: Protocol.DOM.Node[]): string[] {\n  const segs: string[] = [];\n  const ctr: Record<string, number> = {};\n  for (const child of kids) {\n    const tag = String(child.nodeName).toLowerCase();\n    const key = `${child.nodeType}:${tag}`;\n    const idx = (ctr[key] = (ctr[key] ?? 0) + 1);\n    if (child.nodeType === 3) {\n      segs.push(`text()[${idx}]`);\n    } else if (child.nodeType === 8) {\n      segs.push(`comment()[${idx}]`);\n    } else {\n      segs.push(\n        tag.includes(\":\") ? `*[name()='${tag}'][${idx}]` : `${tag}[${idx}]`,\n      );\n    }\n  }\n  return segs;\n}\n\n/** Join two XPath fragments while preserving special shadow-root hops. */\nexport function joinXPath(base: string, step: string): string {\n  if (step === \"//\") {\n    if (!base || base === \"/\") return \"//\";\n    return base.endsWith(\"/\") ? `${base}/` : `${base}//`;\n  }\n  if (!base || base === \"/\") return step ? `/${step}` : \"/\";\n  if (base.endsWith(\"//\")) return `${base}${step}`;\n  if (!step) return base;\n  return `${base}/${step}`;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/a11yInvocation.ts",
    "content": "import {\n  a11yScriptBootstrap,\n  a11yScriptGlobalRefs,\n  type A11yScriptName,\n} from \"../dom/build/a11yScripts.generated.js\";\n\n/**\n * Wrap a generated a11y script in a self-invoking expression that first ensures\n * the bootstrap has run, then calls the requested helper via its global ref.\n * This mirrors the locator resolver’s injection path so any CDP Runtime.evaluate\n * can reuse the shared bundle without inlining JS strings.\n */\nexport function buildA11yInvocation(\n  name: A11yScriptName,\n  args: string[],\n): string {\n  const invocation = `${a11yScriptGlobalRefs[name]}(${args.join(\", \")})`;\n  return `(() => { ${a11yScriptBootstrap}; return ${invocation}; })()`;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/cdp.ts",
    "content": "// lib/v3/understudy/cdp.ts\nimport WebSocket from \"ws\";\nimport type { Protocol } from \"devtools-protocol\";\nimport { STAGEHAND_VERSION } from \"../../version.js\";\nimport {\n  FlowLogger,\n  type FlowEvent,\n  type FlowLoggerContext,\n} from \"../flowlogger/FlowLogger.js\";\nimport {\n  CdpConnectionClosedError,\n  PageNotFoundError,\n} from \"../types/public/sdkErrors.js\";\n\n/**\n * CDP transport & session multiplexer\n *\n * Owns the browser WebSocket and multiplexes flattened Target sessions.\n * Tracks inflight CDP calls, routes responses to the right session, and forwards events.\n *\n * This does not interpret Page/DOM/Runtime semantics — callers own that logic.\n */\nexport interface CDPSessionLike {\n  send<R = unknown>(method: string, params?: object): Promise<R>;\n  on<P = unknown>(event: string, handler: (params: P) => void): void;\n  off<P = unknown>(event: string, handler: (params: P) => void): void;\n  close(): Promise<void>;\n  readonly id: string | null;\n}\n\ntype Inflight = {\n  resolve: (v: unknown) => void;\n  reject: (e: Error) => void;\n  sessionId?: string | null;\n  method: string;\n  params?: object;\n  stack?: string;\n  ts: number;\n  flowLoggerContext?: FlowLoggerContext | null; // Snapshot of the flow context captured when the request was sent; response handling re-enters this if ALS is gone.\n  cdpCallEvent?: Pick<FlowEvent, \"eventId\" | \"eventParentIds\"> | null; // The emitted CdpCallEvent identity; later response/error events attach under this exact parent.\n};\n\ntype EventHandler = (params: unknown) => void;\ntype SessionDispatchWaiter = {\n  sessionId: string;\n  method: string;\n  match?: (params?: object) => boolean;\n  resolve: () => void;\n  reject: (error: Error) => void;\n};\n\ntype RawMessage =\n  | {\n      id: number;\n      result?: unknown;\n      error?: { code: number; message: string; data?: unknown };\n      sessionId?: string;\n    }\n  | { method: string; params?: unknown; sessionId?: string };\n\nexport class CdpConnection implements CDPSessionLike {\n  private ws: WebSocket;\n  private nextId = 1;\n  private inflight = new Map<number, Inflight>(); // Outstanding request records; `_sendViaSession()` inserts and `onMessage()` removes/resolves them.\n  private latestCdpCallEvent = new Map<\n    // Most recent CDP call per session/root; `_sendViaSession()` refreshes it and later unsolicited messages reuse it as their parent anchor.\n    string | null,\n    {\n      flowLoggerContext: FlowLoggerContext; // Flow context captured when the latest call on this session/root was emitted.\n      cdpCallEvent: Pick<FlowEvent, \"eventId\" | \"eventParentIds\">; // Identity of that latest call event; unsolicited messages reuse it as their parent.\n    }\n  >();\n  private eventHandlers = new Map<string, Set<EventHandler>>();\n  private sessions = new Map<string, CdpSession>();\n  /** Maps sessionId -> targetId (1:1 mapping) */\n  private sessionToTarget = new Map<string, string>();\n  private sessionDispatchWaiters = new Set<SessionDispatchWaiter>();\n  public readonly id: string | null = null; // root\n  private transportCloseHandlers = new Set<(why: string) => void>();\n\n  public flowLoggerContext?: FlowLoggerContext; // Instance-owned fallback flow context; V3 sets this once and later sends/callbacks re-enter it when ALS is absent.\n\n  public onTransportClosed(handler: (why: string) => void): void {\n    this.transportCloseHandlers.add(handler);\n  }\n  public offTransportClosed(handler: (why: string) => void): void {\n    this.transportCloseHandlers.delete(handler);\n  }\n\n  private emitTransportClosed(why: string) {\n    for (const h of this.transportCloseHandlers) {\n      try {\n        h(why);\n      } catch {\n        //\n      }\n    }\n  }\n\n  private constructor(ws: WebSocket) {\n    this.ws = ws;\n    this.ws.on(\"close\", (code, reason) => {\n      // Reason is a Buffer in ws; stringify defensively\n      const why = `socket-close code=${code} reason=${String(reason || \"\")}`;\n      this.rejectAllInflight(why);\n      this.emitTransportClosed(why);\n    });\n\n    this.ws.on(\"error\", (err) => {\n      const why = `socket-error ${err?.message ?? String(err)}`;\n      this.rejectAllInflight(why);\n      this.emitTransportClosed(why);\n    });\n    this.ws.on(\"message\", (data) => this.onMessage(data.toString()));\n  }\n\n  static async connect(\n    wsUrl: string,\n    options?: { headers?: Record<string, string> },\n  ): Promise<CdpConnection> {\n    // Include User-Agent header for server-side observability and version tracking\n    // Merge user-provided headers, letting them override defaults\n    const headers = {\n      \"User-Agent\": `Stagehand/${STAGEHAND_VERSION}`,\n      ...options?.headers,\n    };\n    const ws = new WebSocket(wsUrl, { headers });\n    await new Promise<void>((resolve, reject) => {\n      ws.once(\"open\", () => resolve());\n      ws.once(\"error\", (e) => reject(e));\n    });\n    return new CdpConnection(ws);\n  }\n\n  async enableAutoAttach(): Promise<void> {\n    await this.send(\"Target.setAutoAttach\", {\n      autoAttach: true,\n      flatten: true,\n      waitForDebuggerOnStart: true,\n    });\n    await this.send(\"Target.setDiscoverTargets\", { discover: true });\n  }\n\n  async send<R = unknown>(method: string, params?: object): Promise<R> {\n    const id = this.nextId++;\n    const payload = { id, method, params };\n    const stack = new Error().stack?.split(\"\\n\").slice(1, 4).join(\"\\n\");\n    const flowLoggerContext = FlowLogger.resolveContext(this.flowLoggerContext);\n    const cdpCallEvent = flowLoggerContext\n      ? FlowLogger.logCdpCallEvent(flowLoggerContext, {\n          method,\n          params,\n          targetId: null,\n        })\n      : null;\n    if (flowLoggerContext && cdpCallEvent) {\n      this.latestCdpCallEvent.set(null, {\n        flowLoggerContext,\n        cdpCallEvent,\n      });\n    }\n    const p = new Promise<R>((resolve, reject) => {\n      this.inflight.set(id, {\n        resolve,\n        reject,\n        sessionId: null,\n        method,\n        params,\n        stack,\n        ts: Date.now(),\n        flowLoggerContext,\n        cdpCallEvent,\n      });\n    });\n    // Prevent unhandledRejection if a session detaches before the caller awaits.\n    void p.catch(() => {});\n    this.ws.send(JSON.stringify(payload));\n    return p;\n  }\n\n  on<P = unknown>(event: string, handler: (params: P) => void): void {\n    const set = this.eventHandlers.get(event) ?? new Set<EventHandler>();\n    set.add(handler as EventHandler);\n    this.eventHandlers.set(event, set);\n  }\n\n  off<P = unknown>(event: string, handler: (params: P) => void): void {\n    const set = this.eventHandlers.get(event);\n    if (set) set.delete(handler as EventHandler);\n  }\n\n  async close(): Promise<void> {\n    if (this.ws.readyState === WebSocket.CLOSED) return;\n    await new Promise<void>((resolve) => {\n      this.ws.once(\"close\", () => resolve());\n      this.ws.close();\n    });\n  }\n\n  private rejectAllInflight(why: string): void {\n    for (const [id, entry] of this.inflight.entries()) {\n      entry.reject(new CdpConnectionClosedError(why));\n      this.inflight.delete(id);\n    }\n    this.latestCdpCallEvent.clear();\n    for (const waiter of Array.from(this.sessionDispatchWaiters)) {\n      waiter.reject(new CdpConnectionClosedError(why));\n    }\n  }\n\n  getSession(sessionId: string): CdpSession | undefined {\n    return this.sessions.get(sessionId);\n  }\n\n  waitForSessionDispatch(\n    sessionId: string,\n    method: string,\n    match?: (params?: object) => boolean,\n  ): Promise<void> {\n    return new Promise<void>((resolve, reject) => {\n      const waiter: SessionDispatchWaiter = {\n        sessionId,\n        method,\n        match,\n        resolve: () => {\n          this.sessionDispatchWaiters.delete(waiter);\n          resolve();\n        },\n        reject: (error: Error) => {\n          this.sessionDispatchWaiters.delete(waiter);\n          reject(error);\n        },\n      };\n      this.sessionDispatchWaiters.add(waiter);\n    });\n  }\n\n  async attachToTarget(targetId: string): Promise<CdpSession> {\n    const { sessionId } = (await this.send<{ sessionId: string }>(\n      \"Target.attachToTarget\",\n      { targetId, flatten: true },\n    )) as { sessionId: string };\n\n    let session = this.sessions.get(sessionId);\n    if (!session) {\n      session = new CdpSession(this, sessionId);\n      this.sessions.set(sessionId, session);\n    }\n    this.sessionToTarget.set(sessionId, targetId);\n    return session;\n  }\n\n  async getTargets(): Promise<Protocol.Target.TargetInfo[]> {\n    const res = await this.send<{\n      targetInfos: Protocol.Target.TargetInfo[];\n    }>(\"Target.getTargets\");\n    return res.targetInfos;\n  }\n\n  private onMessage(json: string): void {\n    const msg = JSON.parse(json) as RawMessage;\n\n    if (\"id\" in msg) {\n      const rec = this.inflight.get(msg.id);\n      if (!rec) return;\n\n      this.inflight.delete(msg.id);\n\n      if (\"error\" in msg && msg.error) {\n        // Response/error events only make sense if the original send captured\n        // both a flow context to re-enter and the emitted CdpCallEvent to hang\n        // the terminal edge under.\n        if (rec.flowLoggerContext && rec.cdpCallEvent) {\n          let targetId: string | null;\n          if (rec.sessionId) {\n            const mappedTargetId = this.sessionToTarget.get(rec.sessionId);\n            if (mappedTargetId) {\n              targetId = mappedTargetId;\n            } else {\n              targetId = rec.sessionId;\n            }\n          } else {\n            targetId = null;\n          }\n          FlowLogger.logCdpResponseEvent(\n            rec.flowLoggerContext,\n            rec.cdpCallEvent,\n            {\n              method: rec.method,\n              error: `${msg.error.code} ${msg.error.message}`,\n              targetId,\n            },\n          );\n        }\n        rec.reject(new Error(`${msg.error.code} ${msg.error.message}`));\n      } else {\n        // Successful responses reuse the same cached call context so the\n        // response lands under the exact CdpCallEvent emitted at send time.\n        if (rec.flowLoggerContext && rec.cdpCallEvent) {\n          let targetId: string | null;\n          if (rec.sessionId) {\n            const mappedTargetId = this.sessionToTarget.get(rec.sessionId);\n            if (mappedTargetId) {\n              targetId = mappedTargetId;\n            } else {\n              targetId = rec.sessionId;\n            }\n          } else {\n            targetId = null;\n          }\n          FlowLogger.logCdpResponseEvent(\n            rec.flowLoggerContext,\n            rec.cdpCallEvent,\n            {\n              method: rec.method,\n              result: (msg as { result?: unknown }).result,\n              targetId,\n            },\n          );\n        }\n        rec.resolve((msg as { result?: unknown }).result);\n      }\n      return;\n    }\n\n    if (\"method\" in msg) {\n      if (msg.method === \"Target.attachedToTarget\") {\n        const p = (msg as { params: Protocol.Target.AttachedToTargetEvent })\n          .params;\n        if (!this.sessions.has(p.sessionId)) {\n          this.sessions.set(p.sessionId, new CdpSession(this, p.sessionId));\n        }\n        this.sessionToTarget.set(p.sessionId, p.targetInfo.targetId);\n      } else if (msg.method === \"Target.detachedFromTarget\") {\n        const p = (msg as { params: Protocol.Target.DetachedFromTargetEvent })\n          .params;\n        for (const [id, entry] of this.inflight.entries()) {\n          if (entry.sessionId === p.sessionId) {\n            entry.reject(\n              new PageNotFoundError(\n                `target closed before CDP response (sessionId=${p.sessionId}, targetId=${p.targetId})`,\n              ),\n            );\n            this.inflight.delete(id);\n          }\n        }\n        for (const waiter of Array.from(this.sessionDispatchWaiters)) {\n          if (waiter.sessionId === p.sessionId) {\n            waiter.reject(\n              new PageNotFoundError(\n                `target closed before CDP send (sessionId=${p.sessionId}, targetId=${p.targetId})`,\n              ),\n            );\n          }\n        }\n        this.sessions.delete(p.sessionId);\n        this.sessionToTarget.delete(p.sessionId);\n        this.latestCdpCallEvent.delete(p.sessionId);\n      } else if (msg.method === \"Target.targetDestroyed\") {\n        const p = (msg as { params: { targetId: string } }).params;\n        // Remove any session mapping for this target\n        for (const [sessionId, targetId] of this.sessionToTarget.entries()) {\n          if (targetId === p.targetId) {\n            this.sessionToTarget.delete(sessionId);\n            this.latestCdpCallEvent.delete(sessionId);\n            break;\n          }\n        }\n      }\n\n      const { method, params, sessionId } = msg;\n      const latestCdpCallEvent =\n        this.latestCdpCallEvent.get(sessionId ?? null) ??\n        (sessionId ? this.latestCdpCallEvent.get(null) : null);\n      let targetId: string | null;\n      if (sessionId) {\n        const mappedTargetId = this.sessionToTarget.get(sessionId);\n        if (mappedTargetId) {\n          targetId = mappedTargetId;\n        } else {\n          targetId = sessionId;\n        }\n      } else {\n        targetId = null;\n      }\n\n      // Unsolicited protocol messages are attached under the most recent call on\n      // that session/root when one is known, so later callbacks still show up\n      // in the same flow subtree.\n      if (latestCdpCallEvent) {\n        FlowLogger.logCdpMessageEvent(\n          latestCdpCallEvent.flowLoggerContext,\n          latestCdpCallEvent.cdpCallEvent,\n          {\n            method,\n            params,\n            targetId,\n          },\n        );\n      }\n\n      const dispatch = () => {\n        if (sessionId) {\n          const session = this.sessions.get(sessionId);\n          session?.dispatch(method, params);\n\n          // Forward target lifecycle events to root listeners as well.\n          // Some browsers emit these via a parent session rather than the root\n          // connection; fan-out keeps target tracking consistent.\n          if (method.startsWith(\"Target.\")) {\n            const handlers = this.eventHandlers.get(method);\n            if (handlers) for (const h of handlers) h(params);\n          }\n          return;\n        }\n\n        const handlers = this.eventHandlers.get(method);\n        if (handlers) for (const h of handlers) h(params);\n      };\n\n      if (latestCdpCallEvent) {\n        FlowLogger.withContext(latestCdpCallEvent.flowLoggerContext, dispatch);\n      } else {\n        dispatch();\n      }\n    }\n  }\n\n  _sendViaSession<R = unknown>(\n    sessionId: string,\n    method: string,\n    params?: object,\n  ): Promise<R> {\n    const id = this.nextId++;\n    const payload = { id, method, params, sessionId };\n    const stack = new Error().stack?.split(\"\\n\").slice(1, 4).join(\"\\n\");\n    const flowLoggerContext = FlowLogger.resolveContext(this.flowLoggerContext);\n    let targetId: string | null;\n    const mappedTargetId = this.sessionToTarget.get(sessionId);\n    if (mappedTargetId) {\n      targetId = mappedTargetId;\n    } else {\n      targetId = null;\n    }\n    const cdpCallEvent = flowLoggerContext\n      ? FlowLogger.logCdpCallEvent(flowLoggerContext, {\n          method,\n          params,\n          targetId,\n        })\n      : null;\n    if (flowLoggerContext && cdpCallEvent) {\n      this.latestCdpCallEvent.set(sessionId, {\n        flowLoggerContext,\n        cdpCallEvent,\n      });\n    }\n\n    const p = new Promise<R>((resolve, reject) => {\n      this.inflight.set(id, {\n        resolve,\n        reject,\n        sessionId,\n        method,\n        params,\n        stack,\n        ts: Date.now(),\n        flowLoggerContext,\n        cdpCallEvent,\n      });\n    });\n    // Prevent unhandledRejection if a session detaches before the caller awaits.\n    void p.catch(() => {});\n    for (const waiter of Array.from(this.sessionDispatchWaiters)) {\n      if (waiter.sessionId !== sessionId) continue;\n      if (waiter.method !== method) continue;\n      if (waiter.match && !waiter.match(params)) continue;\n      waiter.resolve();\n      break;\n    }\n    this.ws.send(JSON.stringify(payload));\n    return p;\n  }\n\n  _onSessionEvent(\n    sessionId: string,\n    event: string,\n    handler: EventHandler,\n  ): void {\n    const key = `${sessionId}:${event}`;\n    const set = this.eventHandlers.get(key) ?? new Set<EventHandler>();\n    set.add(handler);\n    this.eventHandlers.set(key, set);\n  }\n\n  _offSessionEvent(\n    sessionId: string,\n    event: string,\n    handler: EventHandler,\n  ): void {\n    const key = `${sessionId}:${event}`;\n    const set = this.eventHandlers.get(key);\n    if (set) set.delete(handler);\n  }\n\n  _dispatchToSession(sessionId: string, event: string, params: unknown): void {\n    const key = `${sessionId}:${event}`;\n    const handlers = this.eventHandlers.get(key);\n    if (handlers) for (const h of handlers) h(params);\n  }\n}\n\nexport class CdpSession implements CDPSessionLike {\n  constructor(\n    private readonly root: CdpConnection,\n    public readonly id: string,\n  ) {}\n\n  send<R = unknown>(method: string, params?: object): Promise<R> {\n    return this.root._sendViaSession<R>(this.id, method, params);\n  }\n\n  on<P = unknown>(event: string, handler: (params: P) => void): void {\n    this.root._onSessionEvent(this.id, event, handler as EventHandler);\n  }\n\n  off<P = unknown>(event: string, handler: (params: P) => void): void {\n    this.root._offSessionEvent(this.id, event, handler as EventHandler);\n  }\n\n  async close(): Promise<void> {\n    await this.root.send<void>(\"Target.detachFromTarget\", {\n      sessionId: this.id,\n    });\n  }\n\n  dispatch(event: string, params: unknown): void {\n    this.root._dispatchToSession(this.id, event, params);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/consoleMessage.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { Page } from \"./page.js\";\n\ntype RemoteObject = Protocol.Runtime.RemoteObject;\n\nexport type ConsoleListener = (message: ConsoleMessage) => void;\n\nfunction formatRemoteObject(obj: RemoteObject | undefined): string {\n  if (!obj) return \"\";\n\n  if (\"value\" in obj) {\n    const value = obj.value;\n    if (value === undefined) return \"\";\n    if (typeof value === \"string\") return value;\n    try {\n      return JSON.stringify(value);\n    } catch {\n      return String(value);\n    }\n  }\n\n  if (obj.unserializableValue) return obj.unserializableValue;\n  if (obj.description) return obj.description;\n\n  return obj.type ?? \"\";\n}\n\nexport class ConsoleMessage {\n  constructor(\n    private readonly event: Protocol.Runtime.ConsoleAPICalledEvent,\n    private readonly pageRef?: Page,\n  ) {}\n\n  type(): Protocol.Runtime.ConsoleAPICalledEvent[\"type\"] {\n    return this.event.type;\n  }\n\n  text(): string {\n    const args = this.args();\n    if (!args.length) return \"\";\n    return args\n      .map((arg) => formatRemoteObject(arg))\n      .filter((chunk) => chunk.length > 0)\n      .join(\" \");\n  }\n\n  args(): RemoteObject[] {\n    return this.event.args ? [...this.event.args] : [];\n  }\n\n  location(): { url?: string; lineNumber?: number; columnNumber?: number } {\n    const frame = this.event.stackTrace?.callFrames?.[0];\n    return {\n      url: frame?.url,\n      lineNumber: frame?.lineNumber,\n      columnNumber: frame?.columnNumber,\n    };\n  }\n\n  page(): Page | undefined {\n    return this.pageRef;\n  }\n\n  timestamp(): number | undefined {\n    return this.event.timestamp;\n  }\n\n  raw(): Protocol.Runtime.ConsoleAPICalledEvent {\n    return this.event;\n  }\n\n  toString(): string {\n    return this.text();\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/context.ts",
    "content": "// lib/v3/understudy/context.ts\nimport type { Protocol } from \"devtools-protocol\";\nimport { v3Logger } from \"../logger.js\";\nimport { CdpConnection, CDPSessionLike } from \"./cdp.js\";\nimport { Page } from \"./page.js\";\nimport { installV3PiercerIntoSession } from \"./piercer.js\";\nimport { v3ScriptContent } from \"../dom/build/scriptV3Content.js\";\nimport { executionContexts } from \"./executionContextRegistry.js\";\nimport type { StagehandAPIClient } from \"../api.js\";\nimport { LocalBrowserLaunchOptions } from \"../types/public/index.js\";\nimport { InitScriptSource } from \"../types/private/index.js\";\nimport { normalizeInitScriptSource } from \"./initScripts.js\";\nimport {\n  TimeoutError,\n  CookieSetError,\n  PageNotFoundError,\n  StagehandSetExtraHTTPHeadersError,\n} from \"../types/public/sdkErrors.js\";\nimport { getEnvTimeoutMs, withTimeout } from \"../timeoutConfig.js\";\nimport {\n  filterCookies,\n  normalizeCookieParams,\n  cookieMatchesFilter,\n  toCdpCookieParam,\n} from \"./cookies.js\";\nimport {\n  Cookie,\n  ClearCookieOptions,\n  CookieParam,\n} from \"../types/public/context.js\";\n\ntype TargetId = string;\ntype SessionId = string;\n\ntype TargetType = \"page\" | \"iframe\" | string;\n\n/**\n * Returns true when the target's URL points to a document with a real,\n * pierceable HTML DOM.  We allowlist the small set of schemes that carry\n * web content rather than trying to blacklist every internal browser scheme\n * (chrome://, chrome-extension://, devtools://, brave://, edge://, …).\n */\nfunction hasInjectableDOM(url: string | undefined): boolean {\n  if (!url || url === \"\") return true;\n  if (\n    url === \"about:blank\" ||\n    url === \"about:srcdoc\" ||\n    url.startsWith(\"about:blank#\")\n  )\n    return true;\n  if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) return true;\n  if (\n    url.startsWith(\"data:\") ||\n    url.startsWith(\"blob:\") ||\n    url.startsWith(\"file://\") ||\n    url.startsWith(\"filesystem:\")\n  )\n    return true;\n  return false;\n}\n\nfunction isNonWebTarget(info: Protocol.Target.TargetInfo): boolean {\n  return (\n    (info.type !== \"page\" && info.type !== \"iframe\") ||\n    !hasInjectableDOM(info.url)\n  );\n}\n\nfunction isTopLevelPage(info: Protocol.Target.TargetInfo): boolean {\n  const ti = info as unknown as { subtype?: string };\n  return info.type === \"page\" && ti.subtype !== \"iframe\";\n}\n\nconst DEFAULT_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS = 5000;\nconst CI_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS = 30000;\nconst FIRST_TOP_LEVEL_PAGE_TIMEOUT_ENV =\n  \"STAGEHAND_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS\";\nconst WAIT_FOR_FIRST_TOP_LEVEL_PAGE_OPERATION =\n  \"waitForFirstTopLevelPage (no top-level Page)\";\n\nfunction getFirstTopLevelPageTimeoutMs(): number {\n  return (\n    getEnvTimeoutMs(FIRST_TOP_LEVEL_PAGE_TIMEOUT_ENV) ??\n    (process.env.CI\n      ? CI_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS\n      : DEFAULT_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS)\n  );\n}\n\n/**\n * V3Context\n *\n * Owns the root CDP connection and wires Target/Page events into Page.\n * Maintains one Page per top-level target, adopts OOPIF child sessions into the owner Page,\n * and tracks target→page and (root) frame→target mappings for lookups.\n *\n * IMPORTANT: FrameId → session ownership is managed inside Page (via its FrameRegistry).\n * Context never “guesses” owners; it simply forwards events (with the emitting session)\n * so Page can record the correct owner at event time.\n */\nexport class V3Context {\n  private constructor(\n    readonly conn: CdpConnection,\n    private readonly env: \"LOCAL\" | \"BROWSERBASE\" = \"LOCAL\",\n    private readonly apiClient: StagehandAPIClient | null = null,\n    private readonly localBrowserLaunchOptions: LocalBrowserLaunchOptions | null = null,\n  ) {}\n\n  private readonly _piercerInstalled = new Set<string>();\n  // Timestamp for most recent popup/open signal\n  private _lastPopupSignalAt = 0;\n  private readonly _targetSessionListeners = new Set<SessionId>();\n\n  private readonly _sessionInit = new Set<SessionId>();\n  private pagesByTarget = new Map<TargetId, Page>();\n  private mainFrameToTarget = new Map<string, TargetId>();\n  private sessionOwnerPage = new Map<SessionId, Page>();\n  private frameOwnerPage = new Map<string, Page>();\n  private pendingOopifByMainFrame = new Map<string, SessionId>();\n  private createdAtByTarget = new Map<TargetId, number>();\n  private typeByTarget = new Map<TargetId, TargetType>();\n  private _pageOrder: TargetId[] = [];\n  private pendingCreatedTargetUrl = new Map<TargetId, string>();\n  private readonly initScripts: string[] = [];\n  private extraHttpHeaders: Record<string, string> | null = null;\n\n  private installTargetSessionListeners(session: CDPSessionLike): void {\n    const sessionId = session.id;\n    if (!sessionId) return;\n    if (this._targetSessionListeners.has(sessionId)) return;\n    this._targetSessionListeners.add(sessionId);\n\n    session.on<Protocol.Target.AttachedToTargetEvent>(\n      \"Target.attachedToTarget\",\n      (evt) => {\n        void this.onAttachedToTarget(evt.targetInfo, evt.sessionId);\n      },\n    );\n    session.on<Protocol.Target.DetachedFromTargetEvent>(\n      \"Target.detachedFromTarget\",\n      (evt) => {\n        this.onDetachedFromTarget(evt.sessionId, evt.targetId ?? null);\n      },\n    );\n    session.on<Protocol.Target.TargetDestroyedEvent>(\n      \"Target.targetDestroyed\",\n      (evt) => {\n        this.cleanupByTarget(evt.targetId);\n      },\n    );\n  }\n\n  /**\n   * Create a Context for a given CDP websocket URL and bootstrap target wiring.\n   */\n  static async create(\n    wsUrl: string,\n    opts?: {\n      env?: \"LOCAL\" | \"BROWSERBASE\";\n      apiClient?: StagehandAPIClient | null;\n      localBrowserLaunchOptions?: LocalBrowserLaunchOptions | null;\n      cdpHeaders?: Record<string, string>;\n    },\n  ): Promise<V3Context> {\n    const connectTask = async () => {\n      const conn = await CdpConnection.connect(wsUrl, {\n        headers: opts?.cdpHeaders,\n      });\n      const ctx = new V3Context(\n        conn,\n        opts?.env ?? \"LOCAL\",\n        opts?.apiClient ?? null,\n        opts?.localBrowserLaunchOptions ?? null,\n      );\n      await ctx.bootstrap();\n      await ctx.ensureFirstTopLevelPage(getFirstTopLevelPageTimeoutMs());\n      return ctx;\n    };\n\n    const cdpTimeoutMs =\n      opts?.env === \"BROWSERBASE\"\n        ? getEnvTimeoutMs(\"BROWSERBASE_CDP_CONNECT_MAX_MS\")\n        : undefined;\n\n    if (cdpTimeoutMs) {\n      let timedOut = false;\n      const connectPromise = connectTask();\n      const guarded = withTimeout(\n        connectPromise,\n        cdpTimeoutMs,\n        \"Browserbase CDP connect\",\n      ).catch((err) => {\n        timedOut = true;\n        throw err;\n      });\n      connectPromise\n        .then((ctx) => {\n          if (timedOut) void ctx.close();\n        })\n        .catch(() => {});\n      return await guarded;\n    }\n\n    return await connectTask();\n  }\n\n  private hasTopLevelPage(): boolean {\n    for (const [targetId, targetType] of this.typeByTarget) {\n      if (targetType === \"page\" && this.pagesByTarget.has(targetId)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private async ensureFirstTopLevelPage(timeoutMs: number): Promise<void> {\n    if (this.hasTopLevelPage()) return;\n\n    try {\n      await this.waitForFirstTopLevelPage(timeoutMs);\n      return;\n    } catch (err) {\n      if (!(err instanceof TimeoutError)) {\n        throw err;\n      }\n      v3Logger({\n        category: \"ctx\",\n        message:\n          \"No open browser pages found after connect; creating an initial about:blank page\",\n        level: 1,\n      });\n    }\n\n    await this.newPage(\"about:blank\");\n  }\n\n  /**\n   * Wait until at least one top-level Page has been created and registered.\n   * We poll internal maps that bootstrap/onAttachedToTarget populate.\n   */\n  private async waitForFirstTopLevelPage(timeoutMs: number): Promise<void> {\n    const deadline = Date.now() + timeoutMs;\n    while (Date.now() < deadline) {\n      // A top-level Page is present if typeByTarget has an entry \"page\"\n      // and pagesByTarget has the corresponding Page object.\n      for (const [tid, ttype] of this.typeByTarget) {\n        if (ttype === \"page\") {\n          const p = this.pagesByTarget.get(tid);\n          if (p) return;\n        }\n      }\n      await new Promise((r) => setTimeout(r, 25));\n    }\n    throw new TimeoutError(WAIT_FOR_FIRST_TOP_LEVEL_PAGE_OPERATION, timeoutMs);\n  }\n\n  private async waitForInitialTopLevelTargets(\n    targetIds: TargetId[],\n    timeoutMs = 3000,\n  ): Promise<void> {\n    if (!targetIds.length) return;\n    const pending = new Set(targetIds);\n    const deadline = Date.now() + timeoutMs;\n    while (pending.size && Date.now() < deadline) {\n      for (const tid of Array.from(pending)) {\n        if (this.pagesByTarget.has(tid)) {\n          pending.delete(tid);\n        }\n      }\n      if (!pending.size) return;\n      await new Promise((r) => setTimeout(r, 25));\n    }\n    if (pending.size) {\n      v3Logger({\n        category: \"ctx\",\n        message: \"Timed out waiting for existing top-level targets to attach\",\n        level: 2,\n        auxiliary: {\n          remainingTargets: {\n            value: JSON.stringify(Array.from(pending)),\n            type: \"object\",\n          },\n        },\n      });\n    }\n  }\n\n  private async ensurePiercer(session: CDPSessionLike): Promise<boolean> {\n    const id = session.id ?? \"\";\n    if (this._piercerInstalled.has(id)) return true;\n\n    const installed = await installV3PiercerIntoSession(session);\n    if (installed) {\n      this._piercerInstalled.add(id);\n    }\n    return installed;\n  }\n\n  /** Mark a page target as the most-recent one (active). */\n  private _pushActive(tid: TargetId): void {\n    // remove prior entry if any\n    const i = this._pageOrder.indexOf(tid);\n    if (i !== -1) this._pageOrder.splice(i, 1);\n    this._pageOrder.push(tid);\n  }\n\n  /** Remove a page target from the recency list (used on close). */\n  private _removeFromOrder(tid: TargetId): void {\n    const i = this._pageOrder.indexOf(tid);\n    if (i !== -1) this._pageOrder.splice(i, 1);\n  }\n\n  /** Return the current active Page (most-recent page that still exists). */\n  public activePage(): Page | undefined {\n    // prune any stale ids from the tail\n    for (let i = this._pageOrder.length - 1; i >= 0; i--) {\n      const tid = this._pageOrder[i]!;\n      const p = this.pagesByTarget.get(tid);\n      if (p) return p;\n      // stale — remove and continue\n      this._pageOrder.splice(i, 1);\n    }\n    // fallback: pick the newest by createdAt if order is empty\n    let newestTid: TargetId | undefined;\n    let newestTs = -1;\n    for (const [tid] of this.pagesByTarget) {\n      const ts = this.createdAtByTarget.get(tid) ?? 0;\n      if (ts > newestTs) {\n        newestTs = ts;\n        newestTid = tid;\n      }\n    }\n    return newestTid ? this.pagesByTarget.get(newestTid) : undefined;\n  }\n\n  /** Explicitly mark a known Page as the most-recent active page (and focus it). */\n  public setActivePage(page: Page): void {\n    let targetId = page.targetId();\n    if (this.pagesByTarget.get(targetId) !== page) {\n      const lookup = this.findTargetIdByPage(page);\n      if (!lookup) {\n        v3Logger({\n          category: \"ctx\",\n          message: \"setActivePage called with unknown Page\",\n          level: 2,\n          auxiliary: {\n            targetId: { value: String(targetId), type: \"string\" },\n          },\n        });\n        return;\n      }\n      targetId = lookup;\n    }\n\n    this._pushActive(targetId);\n\n    // Bring the tab to the foreground in headful Chrome (best effort).\n    void this.conn.send(\"Target.activateTarget\", { targetId }).catch(() => {});\n  }\n\n  public async addInitScript<Arg>(\n    script: InitScriptSource<Arg>,\n    arg?: Arg,\n  ): Promise<void> {\n    const source = await normalizeInitScriptSource(script, arg);\n    if (this.initScripts.includes(source)) return;\n    this.initScripts.push(source);\n    const pages = this.pages();\n    await Promise.all(pages.map((page) => page.registerInitScript(source)));\n  }\n\n  public async setExtraHTTPHeaders(\n    headers: Record<string, string>,\n  ): Promise<void> {\n    const nextHeaders = { ...headers };\n    this.extraHttpHeaders = nextHeaders;\n\n    const sessions: CDPSessionLike[] = [];\n    for (const sessionId of this._sessionInit) {\n      const session = this.conn.getSession(sessionId);\n      if (session) sessions.push(session);\n    }\n\n    if (!sessions.length) return;\n\n    const results = await Promise.allSettled(\n      sessions.map(async (session) => {\n        await session.send(\"Network.enable\");\n        await session.send(\"Network.setExtraHTTPHeaders\", {\n          headers: nextHeaders,\n        });\n      }),\n    );\n\n    const failures = results\n      .map((result, index) => ({ result, session: sessions[index] }))\n      .filter(\n        (\n          entry,\n        ): entry is {\n          result: PromiseRejectedResult;\n          session: CDPSessionLike;\n        } => entry.result.status === \"rejected\",\n      )\n      .map((entry) => {\n        const reason = entry.result.reason as Error;\n        const sid = entry.session.id ?? \"unknown\";\n        const message = reason?.message ?? String(reason);\n        return `session=${sid} error=${message}`;\n      });\n\n    if (failures.length) {\n      throw new StagehandSetExtraHTTPHeadersError(failures);\n    }\n  }\n\n  /**\n   * Return top-level `Page`s (oldest → newest). OOPIF targets are not included.\n   */\n  pages(): Page[] {\n    const rows: Array<{ tid: TargetId; page: Page; created: number }> = [];\n    for (const [tid, page] of this.pagesByTarget) {\n      if (this.typeByTarget.get(tid) === \"page\") {\n        rows.push({ tid, page, created: this.createdAtByTarget.get(tid) ?? 0 });\n      }\n    }\n    rows.sort((a, b) => a.created - b.created);\n    return rows.map((r) => r.page);\n  }\n\n  private async applyInitScriptsToPage(\n    page: Page,\n    opts?: { seedOnly?: boolean },\n  ): Promise<void> {\n    if (opts?.seedOnly) {\n      for (const source of this.initScripts) {\n        page.seedInitScript(source);\n      }\n      return;\n    }\n    for (const source of this.initScripts) {\n      await page.registerInitScript(source);\n    }\n  }\n\n  /**\n   * Resolve an owning `Page` by the **top-level main frame id**.\n   * Note: child (OOPIF) roots are intentionally not present in this mapping.\n   */\n  resolvePageByMainFrameId(frameId: string): Page | undefined {\n    const targetId = this.mainFrameToTarget.get(frameId);\n    return targetId ? this.pagesByTarget.get(targetId) : undefined;\n  }\n\n  /**\n   * Serialize the full frame tree for a given top-level main frame id.\n   */\n  async getFullFrameTreeByMainFrameId(\n    rootMainFrameId: string,\n  ): Promise<Protocol.Page.FrameTree> {\n    const owner = this.resolvePageByMainFrameId(rootMainFrameId);\n    if (!owner) throw new PageNotFoundError(`mainFrameId=${rootMainFrameId}`);\n    return owner.asProtocolFrameTree(rootMainFrameId);\n  }\n\n  /**\n   * Create a new top-level page (tab) with the given URL and return its Page object.\n   * Waits until the target is attached and registered.\n   */\n  public async newPage(url = \"about:blank\"): Promise<Page> {\n    const targetUrl = String(url ?? \"about:blank\");\n    const { targetId } = await this.conn.send<{ targetId: string }>(\n      \"Target.createTarget\",\n      // Create at about:blank so init scripts can install before first real navigation.\n      { url: \"about:blank\" },\n    );\n    this.pendingCreatedTargetUrl.set(targetId, \"about:blank\");\n    // Best-effort bring-to-front\n    await this.conn.send(\"Target.activateTarget\", { targetId }).catch(() => {});\n\n    const deadline = Date.now() + 5000;\n    while (Date.now() < deadline) {\n      const page = this.pagesByTarget.get(targetId);\n      if (page) {\n        // we created at about:blank; navigate only after attach so init scripts run\n        // on the first real document. Fire-and-forget so newPage() resolves on attach.\n        if (targetUrl !== \"about:blank\") {\n          // Seed requested URL into the page cache before navigation events arrive.\n          page.seedCurrentUrl(targetUrl);\n          void page\n            .sendCDP(\"Page.navigate\", { url: targetUrl })\n            .catch(() => {});\n        }\n        return page;\n      }\n      await new Promise((r) => setTimeout(r, 25));\n    }\n    throw new TimeoutError(`newPage: target not attached (${targetId})`, 5000);\n  }\n\n  /**\n   * Close CDP and clear all mappings. Best-effort cleanup.\n   */\n  async close(): Promise<void> {\n    await this.conn.close();\n    this.pagesByTarget.clear();\n    this.mainFrameToTarget.clear();\n    this.sessionOwnerPage.clear();\n    this.frameOwnerPage.clear();\n    this.pendingOopifByMainFrame.clear();\n    this.createdAtByTarget.clear();\n    this.typeByTarget.clear();\n    this.pendingCreatedTargetUrl.clear();\n  }\n\n  /**\n   * Bootstrap target lifecycle:\n   * - Attach to existing targets.\n   * - Handle auto-attach events.\n   * - Clean up on detach/destroy.\n   */\n  private async bootstrap(): Promise<void> {\n    // Live attach via auto-attach (normal path)\n    this.conn.on<Protocol.Target.AttachedToTargetEvent>(\n      \"Target.attachedToTarget\",\n      async (evt) => {\n        await this.onAttachedToTarget(evt.targetInfo, evt.sessionId);\n      },\n    );\n\n    // Live detach (clean up session from owner page & frame graph)\n    this.conn.on<Protocol.Target.DetachedFromTargetEvent>(\n      \"Target.detachedFromTarget\",\n      (evt) => {\n        this.onDetachedFromTarget(evt.sessionId, evt.targetId ?? null);\n      },\n    );\n\n    // Destroyed targets (fallback cleanup by targetId)\n    this.conn.on<Protocol.Target.TargetDestroyedEvent>(\n      \"Target.targetDestroyed\",\n      (evt) => {\n        this.cleanupByTarget(evt.targetId);\n      },\n    );\n\n    this.conn.on<Protocol.Target.TargetCreatedEvent>(\n      \"Target.targetCreated\",\n      async (evt) => {\n        const info = evt.targetInfo;\n        // Note popups to help activePage settle\n        const ti = info;\n        if (info.type === \"page\" && (ti?.openerId || ti?.openerFrameId)) {\n          this._notePopupSignal();\n        }\n      },\n    );\n\n    // Only enable auto-attach after listeners are ready so replayed targets are captured.\n    await this.conn.enableAutoAttach();\n\n    const targets = await this.conn.getTargets();\n    for (const t of targets) {\n      if (t.attached) continue; // auto-attach already handled this target\n      try {\n        await this.conn.attachToTarget(t.targetId);\n      } catch {\n        // ignore attach race\n      }\n    }\n\n    const topLevelTargetIds = targets\n      .filter((t) => isTopLevelPage(t))\n      .map((t) => t.targetId);\n    await this.waitForInitialTopLevelTargets(topLevelTargetIds);\n  }\n\n  /**\n   * Handle a newly attached target (top-level or potential OOPIF):\n   * - Enable Page domain and lifecycle events.\n   * - If top-level → create Page, wire listeners, resume.\n   * - Else → probe child root frame id via `Page.getFrameTree` and adopt immediately\n   *   if the parent is known; otherwise stage until parent `frameAttached`.\n   * - Resume the target only after listeners are wired.\n   */\n  private async onAttachedToTarget(\n    info: Protocol.Target.TargetInfo,\n    sessionId: SessionId,\n  ): Promise<void> {\n    // Skip non-web targets (workers, chrome extensions, background pages, etc.).\n    // They still need to be resumed so we don't leave them paused by\n    // waitForDebuggerOnStart, but injecting the piercer into these targets\n    // can throw or corrupt their internal state (e.g. Chrome's PDF viewer).\n    if (isNonWebTarget(info)) {\n      const session = this.conn.getSession(sessionId);\n      if (session) {\n        await session.send(\"Runtime.runIfWaitingForDebugger\").catch(() => {});\n      }\n      return;\n    }\n\n    const session = this.conn.getSession(sessionId);\n    if (!session) return;\n\n    // Init guard\n    if (this._sessionInit.has(sessionId)) return;\n    this._sessionInit.add(sessionId);\n\n    this.installTargetSessionListeners(session);\n\n    // Register for Runtime events before enabling it so we don't miss initial contexts.\n    executionContexts.attachSession(session);\n\n    // Ensure we only resume once even if multiple code paths hit finally.\n    let resumed = false;\n    const resume = async (): Promise<void> => {\n      if (resumed) return;\n      resumed = true;\n      // waitForDebuggerOnStart pauses new targets; resume once we've done\n      // any \"must happen before first document\" work.\n      await session.send(\"Runtime.runIfWaitingForDebugger\").catch(() => {});\n    };\n\n    // Attach lifecycle (per target session):\n    // 1) while paused, enable domains + child auto-attach and register init scripts;\n    // 2) resume target execution;\n    // 3) build/adopt Page ownership and frame bridges.\n    // Some CDP backends defer *.enable() responses until after resume, so we\n    // cannot await those responses before resuming. Instead we:\n    // - wait for transport-level dispatch of required pre-resume commands;\n    // - then dispatch resume;\n    // - then await responses.\n    const queuePreResume = (\n      method: string,\n      params?: object,\n      match?: (sentParams?: object) => boolean,\n    ) => {\n      const dispatched = this.conn\n        .waitForSessionDispatch(sessionId, method, match)\n        .then(() => true)\n        .catch(() => false);\n      const response = session\n        .send(method, params)\n        .then(() => true)\n        .catch(() => false);\n      return { dispatched, response };\n    };\n    const initScriptOps: Array<{\n      dispatched: Promise<boolean>;\n      response: Promise<boolean>;\n    }> = [];\n    // Pre-resume ordering matters:\n    // - enable domains;\n    // - enable child auto-attach with waitForDebuggerOnStart;\n    // - register init scripts.\n    // Commands are sent in-order on the same session before resume.\n    const corePreResumeOps = [\n      queuePreResume(\"Page.enable\"),\n      queuePreResume(\"Runtime.enable\"),\n      queuePreResume(\"Target.setAutoAttach\", {\n        autoAttach: true,\n        waitForDebuggerOnStart: true,\n        flatten: true,\n      }),\n    ];\n    const headerPreResumeOps: Array<{\n      dispatched: Promise<boolean>;\n      response: Promise<boolean>;\n    }> = [];\n    if (this.extraHttpHeaders) {\n      const headers = { ...this.extraHttpHeaders };\n      headerPreResumeOps.push(queuePreResume(\"Network.enable\"));\n      headerPreResumeOps.push(\n        queuePreResume(\"Network.setExtraHTTPHeaders\", { headers }),\n      );\n    }\n    // Send init scripts only after auto-attach has been queued.\n    if (this.initScripts.length) {\n      for (const source of this.initScripts) {\n        initScriptOps.push(\n          queuePreResume(\n            \"Page.addScriptToEvaluateOnNewDocument\",\n            {\n              source,\n              runImmediately: true,\n            },\n            (sentParams) =>\n              (sentParams as { source?: string } | undefined)?.source ===\n              source,\n          ),\n        );\n      }\n    }\n    const piercerPreloadOp = queuePreResume(\n      \"Page.addScriptToEvaluateOnNewDocument\",\n      {\n        source: v3ScriptContent,\n        runImmediately: true,\n      },\n      (sentParams) =>\n        (sentParams as { source?: string } | undefined)?.source ===\n        v3ScriptContent,\n    );\n    const preResumeDispatched = (\n      await Promise.all([\n        ...corePreResumeOps.map((op) => op.dispatched),\n        ...headerPreResumeOps.map((op) => op.dispatched),\n        ...initScriptOps.map((op) => op.dispatched),\n        piercerPreloadOp.dispatched,\n      ])\n    ).every(Boolean);\n    // Dispatch resume only after pre-resume setup has actually been sent.\n    const resumeOp = queuePreResume(\"Runtime.runIfWaitingForDebugger\");\n    const [resumedDispatched, resumedOk] = await Promise.all([\n      resumeOp.dispatched,\n      resumeOp.response,\n    ]);\n    const [\n      coreResults,\n      headerResults,\n      initScriptResults,\n      piercerPreRegistered,\n    ] = await Promise.all([\n      Promise.all(corePreResumeOps.map((op) => op.response)),\n      Promise.all(headerPreResumeOps.map((op) => op.response)),\n      Promise.all(initScriptOps.map((op) => op.response)),\n      piercerPreloadOp.response,\n    ]);\n    // Header propagation is independent of init-script determinism but still\n    // part of pre-resume attach setup; awaited above for ordering/lifecycle.\n    void headerResults;\n    if (!preResumeDispatched || !resumedDispatched || !resumedOk) {\n      // Short-lived child targets can detach before resume is acknowledged.\n      // Keep this noisy only for top-level pages where missing attach is fatal.\n      if (isTopLevelPage(info)) {\n        v3Logger({\n          category: \"ctx\",\n          message: \"Failed target pre-resume setup ordering\",\n          level: 2,\n          auxiliary: {\n            targetId: { value: String(info.targetId), type: \"string\" },\n            targetType: { value: String(info.type), type: \"string\" },\n            preResumeDispatched: {\n              value: String(preResumeDispatched),\n              type: \"string\",\n            },\n            resumedDispatched: {\n              value: String(resumedDispatched),\n              type: \"string\",\n            },\n            resumedOk: { value: String(resumedOk), type: \"string\" },\n          },\n        });\n      }\n      return;\n    }\n    resumed = true;\n    const scriptsInstalled =\n      coreResults.every(Boolean) && initScriptResults.every(Boolean);\n\n    try {\n      // Best-effort lifecycle events; do not block top-level page registration\n      // on this optional signal stream.\n      void session\n        .send(\"Page.setLifecycleEventsEnabled\", { enabled: true })\n        .catch(() => {});\n\n      // Top-level handling\n      if (isTopLevelPage(info)) {\n        let page: Page | null = null;\n        let createError: unknown;\n        // Deterministic contract: never drop a newly attached top-level target\n        // because an arbitrary local timeout fired. We wait for Page.create and\n        // let it finish regardless of CDP call latency.\n        try {\n          page = await Page.create(\n            this.conn,\n            session,\n            info.targetId,\n            this.apiClient,\n            this.localBrowserLaunchOptions,\n            this.env === \"BROWSERBASE\",\n          );\n        } catch (error) {\n          createError = error;\n        }\n        if (!page) {\n          v3Logger({\n            category: \"ctx\",\n            message: \"Failed to create top-level Page\",\n            level: 2,\n            auxiliary: {\n              targetId: { value: String(info.targetId), type: \"string\" },\n              targetType: { value: String(info.type), type: \"string\" },\n              targetUrl: { value: String(info.url ?? \"\"), type: \"string\" },\n              error: {\n                value: String(\n                  createError instanceof Error\n                    ? createError.message\n                    : createError,\n                ),\n                type: \"string\",\n              },\n            },\n          });\n          return;\n        }\n        this.wireSessionToOwnerPage(sessionId, page);\n        this.pagesByTarget.set(info.targetId, page);\n        this.mainFrameToTarget.set(page.mainFrameId(), info.targetId);\n        this.sessionOwnerPage.set(sessionId, page);\n        this.frameOwnerPage.set(page.mainFrameId(), page);\n        this.typeByTarget.set(info.targetId, \"page\");\n        if (!this.createdAtByTarget.has(info.targetId)) {\n          this.createdAtByTarget.set(info.targetId, Date.now());\n        }\n        const pendingSeedUrl = this.pendingCreatedTargetUrl.get(info.targetId);\n        this.pendingCreatedTargetUrl.delete(info.targetId);\n        page.seedCurrentUrl(pendingSeedUrl ?? info.url ?? \"\");\n        this._pushActive(info.targetId);\n        this.installFrameEventBridges(sessionId, page);\n        if (piercerPreRegistered) {\n          this._piercerInstalled.add(sessionId);\n        }\n        // If we already installed scripts at the session level, only seed the\n        // Page's registry to avoid double-installing DOMContentLoaded handlers.\n        await this.applyInitScriptsToPage(page, {\n          seedOnly: scriptsInstalled,\n        });\n        if (!piercerPreRegistered) {\n          void this.ensurePiercer(session).catch(() => {});\n        }\n\n        return;\n      }\n\n      const piercerReady = await this.ensurePiercer(session).catch(() => false);\n      if (!piercerReady) return;\n\n      // Child (iframe / OOPIF)\n      try {\n        const { frameTree } =\n          await session.send<Protocol.Page.GetFrameTreeResponse>(\n            \"Page.getFrameTree\",\n          );\n        const childMainId = frameTree.frame.id;\n\n        // Try to find owner Page now (it may already have the node in its tree)\n        let owner = this.frameOwnerPage.get(childMainId);\n        if (!owner) {\n          for (const p of this.pagesByTarget.values()) {\n            const tree = p.asProtocolFrameTree(p.mainFrameId());\n            const has = (function find(n: Protocol.Page.FrameTree): boolean {\n              if (n.frame.id === childMainId) return true;\n              for (const c of n.childFrames ?? []) if (find(c)) return true;\n              return false;\n            })(tree);\n            if (has) {\n              owner = p;\n              break;\n            }\n          }\n        }\n\n        if (owner) {\n          owner.adoptOopifSession(session, childMainId);\n          this.sessionOwnerPage.set(sessionId, owner);\n          this.installFrameEventBridges(sessionId, owner);\n          // Prime the execution-context registry so later lookups succeed even if\n          // the frame navigates before we issue a command.\n          void executionContexts\n            .waitForMainWorld(session, childMainId)\n            .catch(() => {});\n        } else {\n          this.pendingOopifByMainFrame.set(childMainId, sessionId);\n        }\n      } catch {\n        // page.getFrameTree failed. Most likely was an ad iframe\n        // that opened & closed before we could attach. ignore\n      }\n    } finally {\n      await resume();\n    }\n  }\n\n  /**\n   * Detach handler:\n   * - Remove child session ownership and prune its subtree.\n   * - If a top-level target, cleanup its `Page` and mappings.\n   * - Drop any staged child for this session.\n   */\n  private onDetachedFromTarget(\n    sessionId: SessionId,\n    targetId: string | null,\n  ): void {\n    const owner = this.sessionOwnerPage.get(sessionId);\n    if (owner) {\n      owner.detachOopifSession(sessionId);\n      this.sessionOwnerPage.delete(sessionId);\n    }\n\n    if (targetId && this.pagesByTarget.has(targetId)) {\n      this.cleanupByTarget(targetId);\n    }\n\n    for (const [fid, sid] of Array.from(\n      this.pendingOopifByMainFrame.entries(),\n    )) {\n      if (sid === sessionId) this.pendingOopifByMainFrame.delete(fid);\n    }\n\n    this._targetSessionListeners.delete(sessionId);\n    this._sessionInit.delete(sessionId);\n    this._piercerInstalled.delete(sessionId);\n  }\n\n  /**\n   * Cleanup a top-level Page by target id, removing its root and staged children.\n   */\n  private cleanupByTarget(targetId: TargetId): void {\n    const page = this.pagesByTarget.get(targetId);\n    if (!page) return;\n\n    const mainId = page.mainFrameId();\n    this.mainFrameToTarget.delete(mainId);\n    this.frameOwnerPage.delete(mainId);\n\n    for (const [sid, p] of Array.from(this.sessionOwnerPage.entries())) {\n      if (p === page) this.sessionOwnerPage.delete(sid);\n    }\n\n    for (const [fid] of Array.from(this.pendingOopifByMainFrame.entries())) {\n      const owner = this.frameOwnerPage.get(fid);\n      if (!owner || owner === page) this.pendingOopifByMainFrame.delete(fid);\n    }\n\n    this._removeFromOrder(targetId);\n    this.pagesByTarget.delete(targetId);\n    this.createdAtByTarget.delete(targetId);\n    this.typeByTarget.delete(targetId);\n    this.pendingCreatedTargetUrl.delete(targetId);\n  }\n\n  /**\n   * Wire Page-domain frame events for a session into the owning Page & mappings.\n   * We forward the *emitting session* with every event so Page can stamp ownership precisely.\n   */\n  private installFrameEventBridges(sessionId: SessionId, owner: Page): void {\n    const session = this.conn.getSession(sessionId);\n    if (!session) return;\n\n    session.on<Protocol.Page.FrameAttachedEvent>(\n      \"Page.frameAttached\",\n      (evt) => {\n        const { frameId, parentFrameId } = evt;\n\n        owner.onFrameAttached(frameId, parentFrameId ?? null, session);\n\n        // If we were waiting for this id (OOPIF child), adopt now.\n        const pendingChildSessionId = this.pendingOopifByMainFrame.get(frameId);\n        if (pendingChildSessionId) {\n          const child = this.conn.getSession(pendingChildSessionId);\n          if (child) {\n            owner.adoptOopifSession(child, frameId);\n            this.sessionOwnerPage.set(child.id, owner);\n            // Wire bridges for the child so its Page events keep flowing.\n            this.installFrameEventBridges(pendingChildSessionId, owner);\n          }\n          this.pendingOopifByMainFrame.delete(frameId);\n        }\n\n        // Track Page ownership for quick reverse lookups (debug helpers).\n        this.frameOwnerPage.set(frameId, owner);\n\n        // Root handoff: keep mainFrameToTarget aligned for the page\n        if (!parentFrameId) {\n          const newRoot = owner.mainFrameId();\n          const topTargetId = this.findTargetIdByPage(owner);\n          if (topTargetId) {\n            this.mainFrameToTarget.set(newRoot, topTargetId);\n          }\n          this.frameOwnerPage.set(newRoot, owner);\n        }\n      },\n    );\n\n    session.on<Protocol.Page.FrameDetachedEvent>(\n      \"Page.frameDetached\",\n      (evt) => {\n        owner.onFrameDetached(evt.frameId, evt.reason ?? \"remove\");\n        if (evt.reason !== \"swap\") {\n          this.frameOwnerPage.delete(evt.frameId);\n        }\n      },\n    );\n\n    session.on<Protocol.Page.FrameNavigatedEvent>(\n      \"Page.frameNavigated\",\n      (evt) => {\n        owner.onFrameNavigated(evt.frame, session);\n      },\n    );\n\n    session.on<Protocol.Page.NavigatedWithinDocumentEvent>(\n      \"Page.navigatedWithinDocument\",\n      (evt) => {\n        owner.onNavigatedWithinDocument(evt.frameId, evt.url, session);\n      },\n    );\n\n    // Observe window.open to anticipate default page changes\n    session.on<Protocol.Page.WindowOpenEvent>(\"Page.windowOpen\", () => {\n      this._notePopupSignal();\n    });\n  }\n\n  /**\n   * Register that a session belongs to a Page (used by event routing).\n   */\n  private wireSessionToOwnerPage(sessionId: SessionId, owner: Page): void {\n    this.sessionOwnerPage.set(sessionId, owner);\n  }\n\n  /**\n   * Utility: reverse-lookup the top-level target id that owns a given Page.\n   */\n  private findTargetIdByPage(page: Page): TargetId | undefined {\n    for (const [tid, p] of this.pagesByTarget) {\n      if (p === page) return tid;\n    }\n    return undefined;\n  }\n\n  private _notePopupSignal(): void {\n    this._lastPopupSignalAt = Date.now();\n  }\n\n  /**\n   * Await the current active page, waiting briefly if a popup/open was just triggered.\n   * Normal path returns immediately; popup path waits up to timeoutMs for the new page.\n   */\n  async awaitActivePage(timeoutMs?: number): Promise<Page> {\n    const defaultTimeout = this.env === \"BROWSERBASE\" ? 4000 : 2000;\n    timeoutMs = timeoutMs ?? defaultTimeout;\n    // If a popup was just triggered, Chrome (especially on Browserbase)\n    // may briefly pause new targets at document start (\"waiting for debugger\").\n    const recentWindowMs = this.env === \"BROWSERBASE\" ? 1000 : 300;\n    const now = Date.now();\n    const hasRecentPopup = now - this._lastPopupSignalAt <= recentWindowMs;\n\n    const immediate = this.activePage();\n    if (!hasRecentPopup && immediate) return immediate;\n\n    const deadline = now + timeoutMs;\n    while (Date.now() < deadline) {\n      // Prefer most-recent by createdAt\n      let newestTid: TargetId | undefined;\n      let newestTs = -1;\n      for (const [tid] of this.pagesByTarget) {\n        const ts = this.createdAtByTarget.get(tid) ?? 0;\n        if (ts > newestTs) {\n          newestTs = ts;\n          newestTid = tid;\n        }\n      }\n      if (newestTid) {\n        const p = this.pagesByTarget.get(newestTid);\n        if (p && newestTs >= this._lastPopupSignalAt) return p;\n      }\n      await new Promise((r) => setTimeout(r, 25));\n    }\n    if (immediate) return immediate;\n    throw new PageNotFoundError(\"awaitActivePage: no page available\");\n  }\n\n  /**\n   * Get all browser cookies, optionally filtered by URL(s).\n   *\n   * When `urls` is omitted or empty every cookie in the browser context is\n   * returned. When one or more URLs are supplied only cookies whose\n   * domain/path/secure attributes match are included.\n   */\n  async cookies(urls?: string | string[]): Promise<Cookie[]> {\n    const urlList = !urls ? [] : typeof urls === \"string\" ? [urls] : urls;\n\n    const { cookies } = await this.conn.send<{\n      cookies: Protocol.Network.Cookie[];\n    }>(\"Storage.getCookies\");\n\n    const mapped: Cookie[] = cookies.map((c) => ({\n      name: c.name,\n      value: c.value,\n      domain: c.domain,\n      path: c.path,\n      expires: c.expires,\n      httpOnly: c.httpOnly,\n      secure: c.secure,\n      sameSite: (c.sameSite as Cookie[\"sameSite\"]) ?? \"Lax\",\n    }));\n\n    return filterCookies(mapped, urlList);\n  }\n\n  /**\n   * Add one or more cookies to the browser context.\n   *\n   * Each cookie must specify either a `url` (from which domain/path/secure are\n   * derived) or an explicit `domain` + `path` pair.\n   *\n   * We surface CDP errors if the browser rejects a cookie.\n   */\n  async addCookies(cookies: CookieParam[]): Promise<void> {\n    const normalized = normalizeCookieParams(cookies);\n    if (!normalized.length) return;\n\n    const cdpCookies = normalized.map(toCdpCookieParam);\n\n    try {\n      await this.conn.send(\"Storage.setCookies\", { cookies: cdpCookies });\n    } catch (err) {\n      const detail = err instanceof Error ? err.message : String(err);\n      const names = normalized.map((c) => `\"${c.name}\"`).join(\", \");\n      throw new CookieSetError(\n        `Failed to set cookies [${names}] — ` +\n          `the browser rejected the batch. Check that the domain, path, and secure/sameSite values are valid.` +\n          (detail ? ` (CDP error: ${detail})` : \"\"),\n      );\n    }\n  }\n\n  /**\n   * Clear cookies from the browser context.\n   *\n   * - Called with no arguments: clears **all** cookies atomically via\n   *   `Storage.clearCookies`.\n   * - Called with filter options: fetches all cookies, clears everything,\n   *   then re-adds only the cookies that do NOT match the filter via\n   *   `Storage.setCookies`. This is necessary on the browser endpoint because\n   *   the Storage domain does not support targeted deletes.\n   */\n  async clearCookies(options?: ClearCookieOptions): Promise<void> {\n    const hasFilter =\n      options?.name !== undefined ||\n      options?.domain !== undefined ||\n      options?.path !== undefined;\n\n    if (!hasFilter) {\n      // Atomic single-call wipe — no race condition, no O(N) roundtrips.\n      await this.conn.send(\"Storage.clearCookies\");\n      return;\n    }\n\n    const current = await this.cookies();\n    const toKeep = current.filter((c) => !cookieMatchesFilter(c, options!));\n\n    if (toKeep.length === current.length) return;\n\n    // Storage domain doesn't support targeted deletes on the browser endpoint.\n    // Clear everything, then re-add only the cookies we're keeping.\n    await this.conn.send(\"Storage.clearCookies\");\n    if (toKeep.length) {\n      try {\n        await this.conn.send(\"Storage.setCookies\", {\n          cookies: toKeep.map(toCdpCookieParam),\n        });\n      } catch (err) {\n        const detail = err instanceof Error ? err.message : String(err);\n        const names = toKeep.map((c) => `\"${c.name}\"`).join(\", \");\n        throw new CookieSetError(\n          `clearCookies: cookies were cleared but failed to re-add the ${toKeep.length} ` +\n            `non-matching cookie(s) [${names}]. The browser cookie jar is now empty. ` +\n            (detail ? `(CDP error: ${detail})` : \"\"),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/cookies.ts",
    "content": "import {\n  Cookie,\n  CookieParam,\n  ClearCookieOptions,\n} from \"../types/public/context.js\";\nimport { CookieValidationError } from \"../types/public/sdkErrors.js\";\n\n/**\n * helpers for browser cookie management.\n *\n * Mirrors Playwright's cookie API surface, adapted for direct CDP usage\n * against a single default browser context.\n */\n\n/**\n * Filter cookies by URL matching (domain, path, secure).\n * If `urls` is empty every cookie passes.\n */\nexport function filterCookies(cookies: Cookie[], urls: string[]): Cookie[] {\n  if (!urls.length) return cookies;\n  const parsed = urls.map((u) => {\n    try {\n      return new URL(u);\n    } catch {\n      throw new CookieValidationError(\n        `Invalid URL passed to cookies(): \"${u}\"`,\n      );\n    }\n  });\n  return cookies.filter((c) => {\n    for (const url of parsed) {\n      let domain = c.domain;\n      if (!domain.startsWith(\".\")) domain = \".\" + domain;\n      if (!(\".\" + url.hostname).endsWith(domain)) continue;\n      // Path must match on a \"/\" boundary: cookie path \"/foo\" should match\n      // \"/foo\" and \"/foo/bar\" but NOT \"/foobar\".\n      const p = url.pathname;\n      if (\n        !p.startsWith(c.path) ||\n        (c.path.length < p.length &&\n          !c.path.endsWith(\"/\") &&\n          p[c.path.length] !== \"/\")\n      )\n        continue;\n      const isLoopback =\n        url.hostname === \"localhost\" ||\n        url.hostname === \"127.0.0.1\" ||\n        url.hostname === \"[::1]\";\n      if (url.protocol !== \"https:\" && !isLoopback && c.secure) continue;\n      return true;\n    }\n    return false;\n  });\n}\n\n/**\n * Validate and normalise `CookieParam` values before sending to CDP.\n *\n * - Ensures every cookie has either `url` or `domain`+`path`.\n * - When `url` is provided, derives `domain`, `path`, and `secure` from it.\n * - Validates that `sameSite: \"None\"` is paired with `secure: true`\n *   (browsers silently reject this — we throw early with a clear message).\n */\nexport function normalizeCookieParams(cookies: CookieParam[]): CookieParam[] {\n  return cookies.map((c) => {\n    if (!c.url && !(c.domain && c.path)) {\n      throw new CookieValidationError(\n        `Cookie \"${c.name}\" must have a url or a domain/path pair`,\n      );\n    }\n    if (c.url && c.domain) {\n      throw new CookieValidationError(\n        `Cookie \"${c.name}\" should have either url or domain, not both`,\n      );\n    }\n    if (c.url && c.path) {\n      throw new CookieValidationError(\n        `Cookie \"${c.name}\" should have either url or path, not both`,\n      );\n    }\n    if (c.expires !== undefined && c.expires < 0 && c.expires !== -1) {\n      throw new CookieValidationError(\n        `Cookie \"${c.name}\" has an invalid expires value; use -1 for session cookies or a positive unix timestamp`,\n      );\n    }\n\n    const copy = { ...c };\n    if (copy.url) {\n      if (copy.url === \"about:blank\") {\n        throw new CookieValidationError(\n          `Blank page cannot have cookie \"${c.name}\"`,\n        );\n      }\n      if (copy.url.startsWith(\"data:\")) {\n        throw new CookieValidationError(\n          `Data URL page cannot have cookie \"${c.name}\"`,\n        );\n      }\n      let url: URL;\n      try {\n        url = new URL(copy.url);\n      } catch {\n        throw new CookieValidationError(\n          `Cookie \"${c.name}\" has an invalid url: \"${copy.url}\"`,\n        );\n      }\n      copy.domain = url.hostname;\n      copy.path = url.pathname.substring(0, url.pathname.lastIndexOf(\"/\") + 1);\n      copy.secure = url.protocol === \"https:\";\n      delete copy.url;\n    }\n\n    // Browsers silently reject SameSite=None cookies that aren't Secure.\n    // Catch this early with a clear error instead of a silent CDP failure.\n    // Use !copy.secure to catch both explicit false AND undefined (omitted),\n    // since CDP defaults secure to false when omitted.\n    if (copy.sameSite === \"None\" && !copy.secure) {\n      throw new CookieValidationError(\n        `Cookie \"${c.name}\" has sameSite: \"None\" without secure: true. ` +\n          `Browsers require secure: true when sameSite is \"None\".`,\n      );\n    }\n\n    return copy;\n  });\n}\n\n/**\n * Map a Cookie or CookieParam to the shape CDP's Storage.setCookies expects.\n * Session cookies (expires === -1) omit the expires field so CDP treats them\n * as session-scoped.\n */\nexport function toCdpCookieParam(\n  c: Cookie | CookieParam,\n): Record<string, unknown> {\n  return {\n    name: c.name,\n    value: c.value,\n    domain: c.domain,\n    path: c.path,\n    expires: c.expires === -1 ? undefined : c.expires,\n    httpOnly: c.httpOnly,\n    secure: c.secure,\n    sameSite: c.sameSite,\n  };\n}\n\n/**\n * Returns true if a cookie matches all supplied filter criteria.\n * Undefined filters are treated as \"match anything\".\n */\nexport function cookieMatchesFilter(\n  cookie: Cookie,\n  options: ClearCookieOptions,\n): boolean {\n  const check = (\n    prop: \"name\" | \"domain\" | \"path\",\n    value: string | RegExp | undefined,\n  ): boolean => {\n    if (value === undefined) return true;\n    if (value instanceof RegExp) {\n      value.lastIndex = 0;\n      return value.test(cookie[prop]);\n    }\n    return cookie[prop] === value;\n  };\n  return (\n    check(\"name\", options.name) &&\n    check(\"domain\", options.domain) &&\n    check(\"path\", options.path)\n  );\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/deepLocator.ts",
    "content": "import { Locator } from \"./locator.js\";\nimport type { Frame } from \"./frame.js\";\nimport type { Page } from \"./page.js\";\nimport { FrameLocator, frameLocatorFromFrame } from \"./frameLocator.js\";\nimport { StagehandInvalidArgumentError } from \"../types/public/sdkErrors.js\";\nimport { IFRAME_STEP_RE } from \"./a11y/snapshot/focusSelectors.js\";\n\ntype Axis = \"child\" | \"desc\";\ntype Step = { axis: Axis; raw: string; name: string };\n\nexport type ResolvedLocatorTarget = {\n  frame: Frame;\n  selector: string;\n};\n\n/** Parse XPath into steps preserving '/' vs '//' and the raw token (with [n]) */\nfunction parseXPath(path: string): Step[] {\n  const s = path.trim();\n  let i = 0;\n  const steps: Step[] = [];\n  while (i < s.length) {\n    let axis: Axis = \"child\";\n    if (s.startsWith(\"//\", i)) {\n      axis = \"desc\";\n      i += 2;\n    } else if (s[i] === \"/\") {\n      axis = \"child\";\n      i += 1;\n    }\n\n    const start = i;\n    while (i < s.length && s[i] !== \"/\") i++;\n    const raw = s.slice(start, i).trim();\n    if (!raw) continue;\n\n    const name = raw.replace(/\\[\\d+\\]\\s*$/u, \"\").toLowerCase();\n    steps.push({ axis, raw, name });\n  }\n  return steps;\n}\n\nfunction buildXPathFromSteps(steps: ReadonlyArray<Step>): string {\n  let out = \"\";\n  for (const st of steps) {\n    out += st.axis === \"desc\" ? \"//\" : \"/\";\n    out += st.raw; // keep predicates intact\n  }\n  return out || \"/\";\n}\n\n/** Build a Locator scoped to the correct frame for a deep XPath crossing iframes. */\nexport async function deepLocatorThroughIframes(\n  page: Page,\n  root: Frame,\n  xpathOrSelector: string,\n): Promise<Locator> {\n  const target = await resolveDeepXPathTarget(page, root, xpathOrSelector);\n  return new Locator(target.frame, target.selector);\n}\n\n/**\n * Unified resolver that supports '>>' hop notation, deep XPath across iframes,\n * and plain single-frame selectors. Keeps hop logic in one shared place.\n */\nexport async function resolveLocatorTarget(\n  page: Page,\n  root: Frame,\n  selectorRaw: string,\n): Promise<ResolvedLocatorTarget> {\n  const sel = selectorRaw.trim();\n  const parts = sel\n    .split(\">>\")\n    .map((s) => s.trim())\n    .filter(Boolean);\n\n  if (parts.length > 1) {\n    // Build a FrameLocator chain for all but the last segment\n    let fl = frameLocatorFromFrame(page, root, parts[0]!);\n    for (let i = 1; i < parts.length - 1; i++) {\n      fl = fl.frameLocator(parts[i]!);\n    }\n    const targetFrame = await fl.resolveFrame();\n    return { frame: targetFrame, selector: parts[parts.length - 1]! };\n  }\n\n  // No hops — delegate to XPath-aware deep resolver when needed\n  const isXPath = sel.startsWith(\"xpath=\") || sel.startsWith(\"/\");\n  if (isXPath) {\n    return resolveDeepXPathTarget(page, root, sel);\n  }\n  return { frame: root, selector: sel };\n}\n\nexport async function resolveLocatorWithHops(\n  page: Page,\n  root: Frame,\n  selectorRaw: string,\n): Promise<Locator> {\n  const target = await resolveLocatorTarget(page, root, selectorRaw);\n  return new Locator(target.frame, target.selector);\n}\n\n/**\n * DeepLocatorDelegate: a lightweight wrapper that looks like a Locator and\n * resolves to the correct frame/element on each call using hop/deep-XPath logic.\n *\n * Returned by `page.deepLocator()` for ergonomic, await-free chaining:\n *   page.deepLocator('iframe#ifrA >> #btn').click()\n */\nexport class DeepLocatorDelegate {\n  constructor(\n    private readonly page: Page,\n    private readonly root: Frame,\n    private readonly selector: string,\n    private readonly nthIndex: number = 0,\n  ) {}\n\n  private async real(): Promise<Locator> {\n    const base = await resolveLocatorWithHops(\n      this.page,\n      this.root,\n      this.selector,\n    );\n    return base.nth(this.nthIndex);\n  }\n\n  // Locator API delegates\n  async click(options?: {\n    button?: \"left\" | \"right\" | \"middle\";\n    clickCount?: number;\n  }) {\n    return (await this.real()).click(options);\n  }\n  async count() {\n    return (await this.real()).count();\n  }\n  async hover() {\n    return (await this.real()).hover();\n  }\n  async fill(value: string) {\n    return (await this.real()).fill(value);\n  }\n  async type(text: string, options?: { delay?: number }) {\n    return (await this.real()).type(text, options);\n  }\n  async selectOption(values: string | string[]) {\n    return (await this.real()).selectOption(values);\n  }\n  async scrollTo(percent: number | string) {\n    return (await this.real()).scrollTo(percent);\n  }\n  async isVisible() {\n    return (await this.real()).isVisible();\n  }\n  async isChecked() {\n    return (await this.real()).isChecked();\n  }\n  async inputValue() {\n    return (await this.real()).inputValue();\n  }\n  async textContent() {\n    return (await this.real()).textContent();\n  }\n  async innerHtml() {\n    return (await this.real()).innerHtml();\n  }\n  async innerText() {\n    return (await this.real()).innerText();\n  }\n  async centroid() {\n    return (await this.real()).centroid();\n  }\n  async backendNodeId() {\n    return (await this.real()).backendNodeId();\n  }\n  async highlight(options?: {\n    durationMs?: number;\n    borderColor?: { r: number; g: number; b: number; a?: number };\n    contentColor?: { r: number; g: number; b: number; a?: number };\n  }) {\n    return (await this.real()).highlight(options);\n  }\n  async sendClickEvent(options?: {\n    bubbles?: boolean;\n    cancelable?: boolean;\n    composed?: boolean;\n    detail?: number;\n  }) {\n    return (await this.real()).sendClickEvent(options);\n  }\n  async setInputFiles(\n    files:\n      | string\n      | string[]\n      | {\n          name: string;\n          mimeType: string;\n          buffer: ArrayBuffer | Uint8Array | Buffer | string;\n        }\n      | Array<{\n          name: string;\n          mimeType: string;\n          buffer: ArrayBuffer | Uint8Array | Buffer | string;\n        }>,\n  ) {\n    return (await this.real()).setInputFiles(files);\n  }\n  first() {\n    return this.nth(0);\n  }\n  nth(index: number): DeepLocatorDelegate {\n    const value = Number(index);\n    if (!Number.isFinite(value) || value < 0) {\n      throw new StagehandInvalidArgumentError(\n        \"deepLocator().nth() expects a non-negative index\",\n      );\n    }\n\n    const nextIndex = Math.floor(value);\n    if (nextIndex === this.nthIndex) return this;\n\n    return new DeepLocatorDelegate(\n      this.page,\n      this.root,\n      this.selector,\n      nextIndex,\n    );\n  }\n}\n\n/** Factory to create a deep locator delegate from a Page + root frame. */\nexport function deepLocatorFromPage(\n  page: Page,\n  root: Frame,\n  selector: string,\n): DeepLocatorDelegate {\n  return new DeepLocatorDelegate(page, root, selector);\n}\n\nasync function resolveDeepXPathTarget(\n  page: Page,\n  root: Frame,\n  xpathOrSelector: string,\n): Promise<ResolvedLocatorTarget> {\n  let path = xpathOrSelector.trim();\n  if (path.startsWith(\"xpath=\")) path = path.slice(\"xpath=\".length).trim();\n  if (!path.startsWith(\"/\")) path = \"/\" + path;\n\n  const steps = parseXPath(path);\n  let fl: FrameLocator | undefined;\n  let buf: Step[] = [];\n\n  const flushIntoFrameLocator = () => {\n    if (!buf.length) return;\n    const selectorForIframe = \"xpath=\" + buildXPathFromSteps(buf);\n    fl = fl\n      ? fl.frameLocator(selectorForIframe)\n      : frameLocatorFromFrame(page, root, selectorForIframe);\n    buf = [];\n  };\n\n  for (const st of steps) {\n    buf.push(st);\n    if (IFRAME_STEP_RE.test(st.name)) flushIntoFrameLocator();\n  }\n\n  const finalSelector = \"xpath=\" + buildXPathFromSteps(buf);\n  const targetFrame = fl ? await fl.resolveFrame() : root;\n  return { frame: targetFrame, selector: finalSelector };\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/executionContextRegistry.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"./cdp.js\";\n\ntype FrameId = Protocol.Page.FrameId;\ntype ExecId = Protocol.Runtime.ExecutionContextId;\n\nexport class ExecutionContextRegistry {\n  private readonly byFrame = new WeakMap<\n    CDPSessionLike,\n    Map<FrameId, ExecId>\n  >();\n  private readonly byExec = new WeakMap<CDPSessionLike, Map<ExecId, FrameId>>();\n\n  /** Wire listeners for this session. Call BEFORE Runtime.enable. */\n  attachSession(session: CDPSessionLike): void {\n    const onCreated = (\n      evt: Protocol.Runtime.ExecutionContextCreatedEvent,\n    ): void => {\n      const aux = (evt.context.auxData ?? {}) as {\n        frameId?: string;\n        isDefault?: boolean;\n      };\n      if (aux.isDefault === true && typeof aux.frameId === \"string\") {\n        this.register(session, aux.frameId as FrameId, evt.context.id);\n      }\n    };\n    const onDestroyed = (\n      evt: Protocol.Runtime.ExecutionContextDestroyedEvent,\n    ): void => {\n      const rev = this.byExec.get(session);\n      const fwd = this.byFrame.get(session);\n      if (!rev || !fwd) return;\n      const frameId = rev.get(evt.executionContextId);\n      if (!frameId) return;\n      rev.delete(evt.executionContextId);\n      if (fwd.get(frameId) === evt.executionContextId) fwd.delete(frameId);\n    };\n    const onCleared = (): void => {\n      this.byFrame.delete(session);\n      this.byExec.delete(session);\n    };\n\n    session.on(\"Runtime.executionContextCreated\", onCreated);\n    session.on(\"Runtime.executionContextDestroyed\", onDestroyed);\n    session.on(\"Runtime.executionContextsCleared\", onCleared);\n  }\n\n  getMainWorld(session: CDPSessionLike, frameId: FrameId): ExecId | null {\n    return this.byFrame.get(session)?.get(frameId) ?? null;\n  }\n\n  async waitForMainWorld(\n    session: CDPSessionLike,\n    frameId: FrameId,\n    timeoutMs: number = 800,\n  ): Promise<ExecId> {\n    const cached = this.getMainWorld(session, frameId);\n    if (cached) return cached;\n\n    await session.send(\"Runtime.enable\").catch(() => {});\n    const after = this.getMainWorld(session, frameId);\n    if (after) return after;\n\n    return await new Promise<ExecId>((resolve, reject) => {\n      let done = false;\n      const onCreated = (\n        evt: Protocol.Runtime.ExecutionContextCreatedEvent,\n      ): void => {\n        const aux = (evt.context.auxData ?? {}) as {\n          frameId?: string;\n          isDefault?: boolean;\n        };\n        if (aux.isDefault === true && aux.frameId === frameId) {\n          this.register(session, frameId, evt.context.id);\n          if (!done) {\n            done = true;\n            clearTimeout(timer);\n            session.off(\"Runtime.executionContextCreated\", onCreated);\n            resolve(evt.context.id);\n          }\n        }\n      };\n      const timer = setTimeout(() => {\n        if (!done) {\n          done = true;\n          session.off(\"Runtime.executionContextCreated\", onCreated);\n          reject(new Error(`main world not ready for frame ${frameId}`));\n        }\n      }, timeoutMs);\n      session.on(\"Runtime.executionContextCreated\", onCreated);\n    });\n  }\n\n  private register(\n    session: CDPSessionLike,\n    frameId: FrameId,\n    ctxId: ExecId,\n  ): void {\n    let fwd = this.byFrame.get(session);\n    if (!fwd) {\n      fwd = new Map<FrameId, ExecId>();\n      this.byFrame.set(session, fwd);\n    }\n    let rev = this.byExec.get(session);\n    if (!rev) {\n      rev = new Map<ExecId, FrameId>();\n      this.byExec.set(session, rev);\n    }\n    fwd.set(frameId, ctxId);\n    rev.set(ctxId, frameId);\n  }\n}\n\nexport const executionContexts = new ExecutionContextRegistry();\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/fileUploadUtils.ts",
    "content": "import { promises as fs, type Stats } from \"fs\";\nimport path from \"path\";\nimport { Buffer } from \"buffer\";\nimport { StagehandInvalidArgumentError } from \"../types/public/sdkErrors.js\";\nimport {\n  SetInputFilesArgument,\n  SetInputFilePayload,\n} from \"../types/public/locator.js\";\nimport { NormalizedFilePayload } from \"../types/private/locator.js\";\n\nconst DEFAULT_MIME_TYPE = \"application/octet-stream\";\n\n/**\n * Normalize user-provided setInputFiles arguments into in-memory payloads.\n * - Resolves string paths relative to the provided base directory.\n * - Validates that each path exists and is a regular file.\n * - Converts all buffers into Node Buffers for downstream processing.\n */\nexport async function normalizeInputFiles(\n  files: SetInputFilesArgument,\n  opts: { baseDir?: string } = {},\n): Promise<NormalizedFilePayload[]> {\n  if (files === null || files === undefined) return [];\n\n  const flattened = Array.isArray(files)\n    ? (files as Array<string | SetInputFilePayload>)\n    : [files];\n  if (!flattened.length) return [];\n\n  const baseDir = opts.baseDir ?? process.cwd();\n  const normalized: NormalizedFilePayload[] = [];\n\n  for (const entry of flattened) {\n    if (typeof entry === \"string\") {\n      const absolutePath = path.isAbsolute(entry)\n        ? entry\n        : path.resolve(baseDir, entry);\n      const stat = await statFile(absolutePath);\n      if (!stat.isFile()) {\n        throw new StagehandInvalidArgumentError(\n          `setInputFiles(): expected a file but received directory or special entry at ${absolutePath}`,\n        );\n      }\n      const buffer = await fs.readFile(absolutePath);\n      normalized.push({\n        name: path.basename(absolutePath) || \"upload.bin\",\n        mimeType: DEFAULT_MIME_TYPE,\n        buffer,\n        lastModified: stat.mtimeMs || Date.now(),\n        absolutePath,\n      });\n      continue;\n    }\n\n    if (entry && typeof entry === \"object\" && \"buffer\" in entry) {\n      const payload = entry as SetInputFilePayload;\n      const buffer = toBuffer(payload.buffer);\n      normalized.push({\n        name: payload.name || \"upload.bin\",\n        mimeType: payload.mimeType || DEFAULT_MIME_TYPE,\n        buffer,\n        lastModified:\n          typeof payload.lastModified === \"number\"\n            ? payload.lastModified\n            : Date.now(),\n      });\n      continue;\n    }\n\n    throw new StagehandInvalidArgumentError(\n      \"setInputFiles(): expected file path(s) or payload object(s)\",\n    );\n  }\n\n  return normalized;\n}\n\nasync function statFile(absolutePath: string): Promise<Stats> {\n  try {\n    return await fs.stat(absolutePath);\n  } catch (error) {\n    const code = (error as NodeJS.ErrnoException)?.code;\n    if (code === \"ENOENT\") {\n      throw new StagehandInvalidArgumentError(\n        `setInputFiles(): file not found at ${absolutePath}`,\n      );\n    }\n    throw error;\n  }\n}\n\nexport function toBuffer(\n  data: ArrayBuffer | Uint8Array | Buffer | string,\n): Buffer {\n  if (Buffer.isBuffer(data)) return data;\n  if (data instanceof Uint8Array) return Buffer.from(data);\n  if (typeof data === \"string\") return Buffer.from(data);\n  if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));\n  throw new StagehandInvalidArgumentError(\n    \"Unsupported file payload buffer type\",\n  );\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/frame.ts",
    "content": "// lib/v3/understudy/frame.ts\nimport { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport { Locator } from \"./locator.js\";\nimport { StagehandEvalError } from \"../types/public/sdkErrors.js\";\nimport { executionContexts } from \"./executionContextRegistry.js\";\n\ninterface FrameManager {\n  session: CDPSessionLike;\n  frameId: string;\n  pageId: string;\n}\n\n/**\n * Frame\n *\n * A thin, session-bound handle to a specific DOM frame (by frameId).\n * All CDP calls in this class go through `this.session`, which MUST be the\n * owning session for `this.frameId`. Page is responsible for constructing\n * Frames with the correct session.\n */\nexport class Frame implements FrameManager {\n  /** Owning CDP session id (useful for logs); null for root connection (should not happen for targets) */\n  public readonly sessionId: string | null;\n\n  constructor(\n    public session: CDPSessionLike,\n    public frameId: string,\n    public pageId: string,\n    private readonly remoteBrowser: boolean,\n  ) {\n    this.sessionId = this.session.id ?? null;\n  }\n\n  /** True when the controlled browser runs on a different machine. */\n  public isBrowserRemote(): boolean {\n    return this.remoteBrowser;\n  }\n\n  /** DOM.getNodeForLocation → DOM.describeNode */\n  async getNodeAtLocation(x: number, y: number): Promise<Protocol.DOM.Node> {\n    await this.session.send(\"DOM.enable\");\n    const { backendNodeId } = await this.session.send<{\n      backendNodeId: Protocol.DOM.BackendNodeId;\n    }>(\"DOM.getNodeForLocation\", {\n      x,\n      y,\n      includeUserAgentShadowDOM: true,\n      ignorePointerEventsNone: false,\n    });\n\n    const { node } = await this.session.send<{\n      node: Protocol.DOM.Node;\n    }>(\"DOM.describeNode\", { backendNodeId });\n\n    return node;\n  }\n\n  /** CSS selector → DOM.querySelector → DOM.getBoxModel */\n  async getLocationForSelector(\n    selector: string,\n  ): Promise<{ x: number; y: number; width: number; height: number }> {\n    await this.session.send(\"DOM.enable\");\n\n    const { root } = await this.session.send<{ root: Protocol.DOM.Node }>(\n      \"DOM.getDocument\",\n    );\n\n    const { nodeId } = await this.session.send<{ nodeId: Protocol.DOM.NodeId }>(\n      \"DOM.querySelector\",\n      { nodeId: root.nodeId, selector },\n    );\n\n    const { model } = await this.session.send<{ model: Protocol.DOM.BoxModel }>(\n      \"DOM.getBoxModel\",\n      { nodeId },\n    );\n\n    const x = model.content[0];\n    const y = model.content[1];\n    const width = model.width;\n    const height = model.height;\n    return { x, y, width, height };\n  }\n\n  /** Accessibility.getFullAXTree (+ recurse into child frames if requested) */\n  async getAccessibilityTree(\n    withFrames = false,\n  ): Promise<Protocol.Accessibility.AXNode[]> {\n    await this.session.send(\"Accessibility.enable\");\n    let nodes: Protocol.Accessibility.AXNode[];\n    try {\n      ({ nodes } = await this.session.send<{\n        nodes: Protocol.Accessibility.AXNode[];\n      }>(\"Accessibility.getFullAXTree\", { frameId: this.frameId }));\n    } catch (e) {\n      const msg = String((e as Error)?.message ?? e ?? \"\");\n      const isFrameScopeError =\n        msg.includes(\"Frame with the given\") ||\n        msg.includes(\"does not belong to the target\") ||\n        msg.includes(\"is not found\");\n      if (!isFrameScopeError) throw e;\n      // Retry unscoped: on OOPIF sessions, returns the child doc's AX tree.\n      ({ nodes } = await this.session.send<{\n        nodes: Protocol.Accessibility.AXNode[];\n      }>(\"Accessibility.getFullAXTree\"));\n    }\n\n    if (!withFrames) return nodes;\n\n    const children = await this.childFrames();\n    for (const child of children) {\n      const childNodes = await child.getAccessibilityTree(false);\n      nodes.push(...childNodes);\n    }\n    return nodes;\n  }\n\n  /**\n   * Evaluate a function or expression in this frame's main world.\n   * - If a string is provided, treated as a JS expression.\n   * - If a function is provided, it is stringified and invoked with the optional argument.\n   */\n  async evaluate<R = unknown, Arg = unknown>(\n    pageFunctionOrExpression: string | ((arg: Arg) => R | Promise<R>),\n    arg?: Arg,\n  ): Promise<R> {\n    await this.session.send(\"Runtime.enable\").catch(() => {});\n    const contextId = await this.getMainWorldExecutionContextId();\n\n    const isString = typeof pageFunctionOrExpression === \"string\";\n    let expression: string;\n\n    if (isString) {\n      expression = String(pageFunctionOrExpression);\n    } else {\n      const fnSrc = pageFunctionOrExpression.toString();\n      const argJson = JSON.stringify(arg);\n      expression = `(() => {\n        const __fn = ${fnSrc};\n        const __arg = ${argJson};\n        try {\n          const __res = __fn(__arg);\n          return Promise.resolve(__res).then(v => {\n            try { return JSON.parse(JSON.stringify(v)); } catch { return v; }\n          });\n        } catch (e) { throw e; }\n      })()`;\n    }\n\n    let res: Protocol.Runtime.EvaluateResponse;\n    try {\n      res = await this.session.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression,\n          contextId,\n          awaitPromise: true,\n          returnByValue: true,\n        },\n      );\n    } catch (error) {\n      // Execution contexts can be recreated between context lookup and\n      // Runtime.evaluate during popup/navigate churn. Retry once with a fresh id.\n      const msg = error instanceof Error ? error.message : String(error);\n      if (!msg.includes(\"Cannot find context with specified id\")) throw error;\n      const freshContextId = await this.getMainWorldExecutionContextId();\n      res = await this.session.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression,\n          contextId: freshContextId,\n          awaitPromise: true,\n          returnByValue: true,\n        },\n      );\n    }\n    if (res.exceptionDetails) {\n      throw new StagehandEvalError(\n        res.exceptionDetails.text ?? \"Evaluation failed\",\n      );\n    }\n    return res.result.value as R;\n  }\n\n  /** Page.captureScreenshot (frame-scoped session) */\n  async screenshot(options?: {\n    fullPage?: boolean;\n    clip?: { x: number; y: number; width: number; height: number };\n    type?: \"png\" | \"jpeg\";\n    quality?: number;\n    scale?: number;\n  }): Promise<Buffer> {\n    await this.session.send(\"Page.enable\");\n    const format = options?.type ?? \"png\";\n    const params: Protocol.Page.CaptureScreenshotRequest & { scale?: number } =\n      {\n        format,\n        fromSurface: true,\n        captureBeyondViewport: options?.fullPage,\n      };\n\n    const clampScale = (value: number): number =>\n      Math.min(2, Math.max(0.1, value));\n\n    const normalizedScale =\n      typeof options?.scale === \"number\"\n        ? clampScale(options.scale)\n        : undefined;\n\n    if (options?.clip) {\n      const clip = {\n        x: options.clip.x,\n        y: options.clip.y,\n        width: options.clip.width,\n        height: options.clip.height,\n        scale: normalizedScale ?? 1,\n      };\n      params.clip = clip;\n    } else if (normalizedScale !== undefined && normalizedScale !== 1) {\n      params.scale = normalizedScale;\n    }\n\n    if (format === \"jpeg\" && typeof options?.quality === \"number\") {\n      const q = Math.round(options.quality);\n      params.quality = Math.min(100, Math.max(0, q));\n    }\n\n    const { data } =\n      await this.session.send<Protocol.Page.CaptureScreenshotResponse>(\n        \"Page.captureScreenshot\",\n        params,\n      );\n    return Buffer.from(data, \"base64\");\n  }\n\n  /** Child frames via Page.getFrameTree */\n  async childFrames(): Promise<Frame[]> {\n    const { frameTree } = await this.session.send<{\n      frameTree: Protocol.Page.FrameTree;\n    }>(\"Page.getFrameTree\");\n    const frames: Frame[] = [];\n\n    const collect = (tree: Protocol.Page.FrameTree) => {\n      if (tree.frame.parentId === this.frameId) {\n        frames.push(\n          new Frame(\n            this.session,\n            tree.frame.id,\n            this.pageId,\n            this.remoteBrowser,\n          ),\n        );\n      }\n      tree.childFrames?.forEach(collect);\n    };\n\n    collect(frameTree);\n    return frames;\n  }\n\n  /** Wait for a lifecycle state (load/domcontentloaded/networkidle) */\n  async waitForLoadState(\n    state: \"load\" | \"domcontentloaded\" | \"networkidle\" = \"load\",\n    timeoutMs: number = 15_000,\n  ): Promise<void> {\n    await this.session.send(\"Page.enable\");\n    const targetState = state.toLowerCase();\n    const timeout = Math.max(0, timeoutMs);\n    await new Promise<void>((resolve, reject) => {\n      let done = false;\n      let timer: ReturnType<typeof setTimeout> | null = null;\n      const finish = () => {\n        if (done) return;\n        done = true;\n        this.session.off(\"Page.lifecycleEvent\", handler);\n        if (timer) {\n          clearTimeout(timer);\n          timer = null;\n        }\n        resolve();\n      };\n      const handler = (evt: Protocol.Page.LifecycleEventEvent) => {\n        const sameFrame = evt.frameId === this.frameId;\n        // need to normalize here because CDP lifecycle names look like 'DOMContentLoaded'\n        // but we accept 'domcontentloaded'\n        const lifecycleName = String(evt.name ?? \"\").toLowerCase();\n        if (sameFrame && lifecycleName === targetState) {\n          finish();\n        }\n      };\n      this.session.on(\"Page.lifecycleEvent\", handler);\n\n      timer = setTimeout(() => {\n        if (done) return;\n        done = true;\n        this.session.off(\"Page.lifecycleEvent\", handler);\n        reject(\n          new Error(\n            `waitForLoadState(${state}) timed out after ${timeout}ms for frame ${this.frameId}`,\n          ),\n        );\n      }, timeout);\n    });\n  }\n\n  /** Simple placeholder for your own locator abstraction */\n  locator(\n    selector: string,\n    options?: { deep?: boolean; depth?: number },\n  ): Locator {\n    return new Locator(this, selector, options);\n  }\n\n  /** Resolve the main-world execution context id for this frame. */\n  private async getMainWorldExecutionContextId(): Promise<number> {\n    return executionContexts.waitForMainWorld(this.session, this.frameId, 1000);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/frameLocator.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { Locator } from \"./locator.js\";\nimport type { Page } from \"./page.js\";\nimport { Frame } from \"./frame.js\";\nimport { executionContexts } from \"./executionContextRegistry.js\";\nimport {\n  ContentFrameNotFoundError,\n  StagehandInvalidArgumentError,\n} from \"../types/public/sdkErrors.js\";\n\n/**\n * FrameLocator: resolves iframe elements to their child Frames and allows\n * creating locators scoped to that frame. Supports chaining.\n */\nexport class FrameLocator {\n  private readonly parent?: FrameLocator;\n  private readonly selector: string;\n  private readonly page: Page;\n  private readonly root?: Frame;\n\n  constructor(\n    page: Page,\n    selector: string,\n    parent?: FrameLocator,\n    root?: Frame,\n  ) {\n    this.page = page;\n    this.selector = selector;\n    this.parent = parent;\n    this.root = root;\n  }\n\n  /** Create a nested FrameLocator under this one. */\n  frameLocator(selector: string): FrameLocator {\n    return new FrameLocator(this.page, selector, this);\n  }\n\n  /** Resolve to the concrete Frame for this FrameLocator chain. */\n  async resolveFrame(): Promise<Frame> {\n    const parentFrame: Frame = this.parent\n      ? await this.parent.resolveFrame()\n      : (this.root ?? this.page.mainFrame());\n\n    // Resolve the iframe element inside the parent frame\n    const tmp = parentFrame.locator(this.selector);\n    const parentSession = parentFrame.session;\n    const { objectId } = await tmp.resolveNode();\n\n    try {\n      await parentSession.send(\"DOM.enable\").catch(() => {});\n      const desc = await parentSession.send<Protocol.DOM.DescribeNodeResponse>(\n        \"DOM.describeNode\",\n        { objectId },\n      );\n      const iframeBackendNodeId = desc.node.backendNodeId;\n\n      // Find direct child frames under the parent by consulting the Page's registry\n      const childIds = await listDirectChildFrameIdsFromRegistry(\n        this.page,\n        parentFrame.frameId,\n        1000,\n      );\n\n      for (const fid of childIds) {\n        try {\n          const owner = await parentSession.send<{\n            backendNodeId: Protocol.DOM.BackendNodeId;\n            nodeId?: Protocol.DOM.NodeId;\n          }>(\"DOM.getFrameOwner\", { frameId: fid as Protocol.Page.FrameId });\n          if (owner.backendNodeId === iframeBackendNodeId) {\n            // Ensure child frame is ready (handles OOPIF adoption or same-process)\n            await ensureChildFrameReady(this.page, parentFrame, fid, 1200);\n            return this.page.frameForId(fid);\n          }\n        } catch {\n          // ignore and try next\n        }\n      }\n      throw new ContentFrameNotFoundError(this.selector);\n    } finally {\n      await parentSession\n        .send(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /** Return a Locator scoped to this frame. Methods delegate to the frame lazily. */\n  locator(selector: string): LocatorDelegate {\n    return new LocatorDelegate(this, selector);\n  }\n}\n\n/** A small delegating wrapper that resolves the frame lazily per call. */\nclass LocatorDelegate {\n  constructor(\n    private readonly fl: FrameLocator,\n    private readonly sel: string,\n    private readonly nthIndex: number = -1,\n  ) {}\n\n  private async real(): Promise<Locator> {\n    const frame = await this.fl.resolveFrame();\n    const locator = frame.locator(this.sel);\n    if (this.nthIndex < 0) return locator;\n    return locator.nth(this.nthIndex);\n  }\n\n  // Locator API delegates\n  async click(options?: {\n    button?: \"left\" | \"right\" | \"middle\";\n    clickCount?: number;\n  }) {\n    return (await this.real()).click(options);\n  }\n  async hover() {\n    return (await this.real()).hover();\n  }\n  async fill(value: string) {\n    return (await this.real()).fill(value);\n  }\n  async type(text: string, options?: { delay?: number }) {\n    return (await this.real()).type(text, options);\n  }\n  async selectOption(values: string | string[]) {\n    return (await this.real()).selectOption(values);\n  }\n  async scrollTo(percent: number | string) {\n    return (await this.real()).scrollTo(percent);\n  }\n  async isVisible() {\n    return (await this.real()).isVisible();\n  }\n  async isChecked() {\n    return (await this.real()).isChecked();\n  }\n  async inputValue() {\n    return (await this.real()).inputValue();\n  }\n  async textContent() {\n    return (await this.real()).textContent();\n  }\n  async innerHtml() {\n    return (await this.real()).innerHtml();\n  }\n  async innerText() {\n    return (await this.real()).innerText();\n  }\n  async count() {\n    return (await this.real()).count();\n  }\n  first(): LocatorDelegate {\n    return this.nth(0);\n  }\n  nth(index: number): LocatorDelegate {\n    const value = Number(index);\n    if (!Number.isFinite(value) || value < 0) {\n      throw new StagehandInvalidArgumentError(\n        \"locator().nth() expects a non-negative index\",\n      );\n    }\n\n    const nextIndex = Math.floor(value);\n    if (nextIndex === this.nthIndex) return this;\n\n    return new LocatorDelegate(this.fl, this.sel, nextIndex);\n  }\n}\n\n/** Factory to start a FrameLocator chain from an arbitrary root Frame. */\nexport function frameLocatorFromFrame(\n  page: Page,\n  root: Frame,\n  selector: string,\n): FrameLocator {\n  return new FrameLocator(page, selector, undefined, root);\n}\n\nasync function listDirectChildFrameIdsFromRegistry(\n  page: Page,\n  parentFrameId: string,\n  timeoutMs: number,\n): Promise<string[]> {\n  const deadline = Date.now() + timeoutMs;\n  while (true) {\n    try {\n      const tree = page.getFullFrameTree();\n      const node = findFrameNode(tree, parentFrameId);\n      const ids = node?.childFrames?.map((c) => c.frame.id as string) ?? [];\n      if (ids.length > 0 || Date.now() >= deadline) return ids;\n    } catch {\n      // ignore\n    }\n    await new Promise((r) => setTimeout(r, 50));\n  }\n}\n\nfunction findFrameNode(\n  tree: Protocol.Page.FrameTree,\n  targetId: string,\n): Protocol.Page.FrameTree | undefined {\n  if (tree.frame.id === targetId) return tree;\n  for (const c of tree.childFrames ?? []) {\n    const hit = findFrameNode(c, targetId);\n    if (hit) return hit;\n  }\n  return undefined;\n}\n\n/**\n * Ensure we can evaluate in the child frame with minimal delay.\n * - If the child is same-process: parent session owns it and main world appears quickly.\n * - If OOPIF and adoption not finished: wait briefly for ownership change, then main world.\n */\nasync function ensureChildFrameReady(\n  page: Page,\n  parentFrame: Frame,\n  childFrameId: string,\n  budgetMs: number,\n): Promise<void> {\n  const parentSession = parentFrame.session;\n  const deadline = Date.now() + Math.max(0, budgetMs);\n\n  // If already owned by a different session (OOPIF adopted), wait briefly there.\n  const owner = page.getSessionForFrame(childFrameId);\n  if (owner && owner !== parentSession) {\n    try {\n      await executionContexts.waitForMainWorld(owner, childFrameId, 600);\n    } catch {\n      // best effort\n    }\n    return;\n  }\n\n  const hasMainWorldOnParent = (): boolean => {\n    try {\n      return (\n        executionContexts.getMainWorld(parentSession, childFrameId) !== null\n      );\n    } catch {\n      return false;\n    }\n  };\n\n  if (hasMainWorldOnParent()) return;\n\n  await parentSession\n    .send(\"Page.setLifecycleEventsEnabled\", { enabled: true })\n    .catch(() => {});\n  await parentSession.send(\"Runtime.enable\").catch(() => {});\n\n  await new Promise<void>((resolve) => {\n    let done = false;\n    const finish = () => {\n      if (done) return;\n      done = true;\n      parentSession.off(\"Page.lifecycleEvent\", onLifecycle);\n      resolve();\n    };\n    const onLifecycle = (evt: Protocol.Page.LifecycleEventEvent) => {\n      if (\n        evt.frameId !== childFrameId ||\n        (evt.name !== \"DOMContentLoaded\" &&\n          evt.name !== \"load\" &&\n          evt.name !== \"networkIdle\" &&\n          evt.name !== \"networkidle\")\n      ) {\n        return;\n      }\n      if (hasMainWorldOnParent()) return finish();\n      try {\n        const nowOwner = page.getSessionForFrame(childFrameId);\n        if (nowOwner && nowOwner !== parentSession) {\n          const left = Math.max(150, deadline - Date.now());\n          executionContexts\n            .waitForMainWorld(nowOwner, childFrameId, left)\n            .finally(finish);\n        }\n      } catch {\n        // ignore\n      }\n    };\n    parentSession.on(\"Page.lifecycleEvent\", onLifecycle);\n\n    const tick = () => {\n      if (done) return;\n      if (hasMainWorldOnParent()) return finish();\n      try {\n        const nowOwner = page.getSessionForFrame(childFrameId);\n        if (nowOwner && nowOwner !== parentSession) {\n          const left = Math.max(150, deadline - Date.now());\n          executionContexts\n            .waitForMainWorld(nowOwner, childFrameId, left)\n            .finally(finish);\n          return;\n        }\n      } catch {\n        // ignore\n      }\n      if (Date.now() >= deadline) return finish();\n      setTimeout(tick, 50);\n    };\n    tick();\n  });\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/frameRegistry.ts",
    "content": "// lib/v3/understudy/frameRegistry.ts\nimport type { Protocol } from \"devtools-protocol\";\n\n/**\n * FrameRegistry\n *\n * Purpose:\n * A single, authoritative source of truth for **both**:\n *   1) Frame topology (parent/children, current main/root id, last-seen CDP `Frame`)\n *   2) Frame → Session ownership (which CDP session owns a given frameId)\n *   3) Optional iframe-owner metadata (backendNodeId of the <iframe> element in the parent doc)\n *\n *\n * Model:\n *  - This class is **CDP-agnostic**; it stores **sessionId strings** (not session objects).\n *  - Context bridges (wiring Target/Page events) must call the mutators below (onAttached,\n *    onNavigated, onDetached, adoptChildSession, seedFromFrameTree, setOwnerBackendNodeId).\n *  - Consumers ask read APIs (getOwnerSessionId, getParent, asProtocolFrameTree, listAll, …)\n *    and never probe ownership at run time.\n */\n\ntype FrameId = string;\ntype SessionId = string;\n\ntype FrameInfo = {\n  /** Parent frame id, or null for root */\n  parentId: FrameId | null;\n  /** Children frame ids (direct) */\n  children: Set<FrameId>;\n  /** Last-seen CDP Frame metadata for this id (may be a shell if never seen) */\n  lastSeen?: Protocol.Page.Frame;\n\n  /** Owning session id (CDP child session for OOPIF, top-level session for same-process) */\n  ownerSessionId?: SessionId;\n\n  /**\n   * The backendNodeId of the <iframe> element **in the parent document** that hosts this frame.\n   * Useful for building absolute XPath prefixes or DOM scoping in the parent session.\n   */\n  ownerBackendNodeId?: number;\n};\n\n/** Minimal “shell” CDP frame used when we haven’t yet seen a real Frame from events. */\nfunction shellFrame(id: FrameId): Protocol.Page.Frame {\n  return {\n    id,\n    loaderId: \"\",\n    url: \"\",\n    domainAndRegistry: \"\",\n    securityOrigin: \"\",\n    mimeType: \"text/html\",\n    secureContextType: \"InsecureScheme\",\n    crossOriginIsolatedContextType: \"NotIsolated\",\n    gatedAPIFeatures: [],\n  } as Protocol.Page.Frame;\n}\n\nexport class FrameRegistry {\n  /** Owner target id (top-level target); informational only */\n  private readonly ownerTargetId: string;\n\n  /** Current main/root frame id (changes on root swaps) */\n  private rootFrameId: FrameId;\n\n  /** frameId → FrameInfo */\n  private frames = new Map<FrameId, FrameInfo>();\n\n  /** sessionId → Set<frameId> (inverse map for diagnostics/fast membership checks) */\n  private framesBySession = new Map<SessionId, Set<FrameId>>();\n\n  constructor(ownerTargetId: string, mainFrameId: FrameId) {\n    this.ownerTargetId = ownerTargetId;\n    this.rootFrameId = mainFrameId;\n    this.ensureNode(mainFrameId);\n  }\n\n  // ---------------------- Mutators (called by Context/Page bridges) ----------------------\n\n  /**\n   * Record that a frame attached. If `parentId` is null and `frameId` differs from the current\n   * root, this is a root swap and we rename the root id.\n   *\n   * IMPORTANT: The emitter's `sessionId` is the **owner** for the new/attached frame.\n   */\n  onFrameAttached(\n    frameId: FrameId,\n    parentId: FrameId | null,\n    sessionId: SessionId,\n  ): void {\n    // Root swap (parentId === null for main frames).\n    if (!parentId && frameId !== this.rootFrameId) {\n      this.renameNodeId(this.rootFrameId, frameId);\n      this.rootFrameId = frameId;\n      // ownership moves to this session as well\n      this.setOwnerSessionIdInternal(frameId, sessionId);\n      return;\n    }\n\n    // Normal attach\n    this.ensureNode(frameId);\n    if (parentId) this.ensureNode(parentId);\n\n    const info = this.frames.get(frameId)!;\n    info.parentId = parentId ?? null;\n\n    if (parentId) {\n      this.frames.get(parentId)!.children.add(frameId);\n    }\n\n    // Ownership: the session that emitted frameAttached owns this frame.\n    this.setOwnerSessionIdInternal(frameId, sessionId);\n  }\n\n  /**\n   * Record a navigation with the full CDP `Frame`. Also updates ownership based on the emitting\n   * session id. Handles root swap if the navigated frame is the new main (no parentId).\n   */\n  onFrameNavigated(frame: Protocol.Page.Frame, sessionId: SessionId): void {\n    this.ensureNode(frame.id);\n    const info = this.frames.get(frame.id)!;\n    info.lastSeen = frame;\n\n    // Ownership follows the session that reported the navigation\n    this.setOwnerSessionIdInternal(frame.id, sessionId);\n\n    // If this frame has no parent, it might be the (new) main/root\n    if (!(\"parentId\" in frame) || !frame.parentId) {\n      if (frame.id !== this.rootFrameId) {\n        // carry ordinal semantics by renaming the root id\n        this.renameNodeId(this.rootFrameId, frame.id);\n        this.rootFrameId = frame.id;\n      }\n    }\n  }\n\n  onNavigatedWithinDocument(\n    frameId: FrameId,\n    url: string,\n    sessionId: SessionId,\n  ): void {\n    this.ensureNode(frameId);\n    const info = this.frames.get(frameId)!;\n    const lastSeen = info.lastSeen ?? shellFrame(frameId);\n    info.lastSeen = { ...lastSeen, url };\n    this.setOwnerSessionIdInternal(frameId, sessionId);\n  }\n\n  /**\n   * Record that a frame detached. If `reason !== \"swap\"`, remove the subtree from the graph,\n   * and clean the inverse maps. For “swap” we keep the node to preserve continuity.\n   */\n  onFrameDetached(\n    frameId: FrameId,\n    reason: \"remove\" | \"swap\" | string = \"remove\",\n  ): void {\n    if (reason === \"swap\") return;\n\n    // Collect subtree starting from frameId.\n    const toRemove: FrameId[] = [];\n    const collect = (fid: FrameId) => {\n      toRemove.push(fid);\n      const kids = this.frames.get(fid)?.children ?? new Set<FrameId>();\n      for (const k of kids) collect(k);\n    };\n    collect(frameId);\n\n    // Remove nodes, fix parents and inverse maps\n    for (const fid of toRemove) {\n      const info = this.frames.get(fid);\n      if (!info) continue;\n\n      // unlink from parent\n      if (info.parentId) {\n        const p = this.frames.get(info.parentId);\n        p?.children.delete(fid);\n      }\n\n      // unlink inverse session map\n      if (info.ownerSessionId) {\n        const bag = this.framesBySession.get(info.ownerSessionId);\n        bag?.delete(fid);\n        if (bag && bag.size === 0)\n          this.framesBySession.delete(info.ownerSessionId);\n      }\n\n      this.frames.delete(fid);\n    }\n\n    // Guard root if we removed it; assign a placeholder root if needed\n    if (!this.frames.has(this.rootFrameId)) {\n      // Choose an arbitrary remaining node as root\n      const iter = this.frames.keys().next();\n      if (!iter.done) this.rootFrameId = iter.value;\n    }\n  }\n\n  /**\n   * An adopted OOPIF child session was created whose **main** frame id equals the parent iframe’s frameId.\n   * We mark the entire child subtree as owned by `childSessionId`.\n   * (Topology edges remain aligned by the parent session’s `frameAttached` events.)\n   */\n  adoptChildSession(\n    childSessionId: SessionId,\n    childMainFrameId: FrameId,\n  ): void {\n    // The child session will emit its own navigations/attachments; as a seed,\n    // mark the root frame as owned by the child session.\n    this.setOwnerSessionIdInternal(childMainFrameId, childSessionId);\n  }\n\n  /**\n   * Seed topology and ownership from an existing `Page.getFrameTree` snapshot, typically right after\n   * a session is attached. This is a best-effort: we record frames and set the provided `sessionId`\n   * as owner for the subtree **if** an owner isn't already set.\n   */\n  seedFromFrameTree(\n    sessionId: SessionId,\n    frameTree: Protocol.Page.FrameTree,\n  ): void {\n    const walk = (tree: Protocol.Page.FrameTree, parent: FrameId | null) => {\n      this.ensureNode(tree.frame.id);\n      // topology\n      this.frames.get(tree.frame.id)!.parentId = parent;\n      if (parent) this.frames.get(parent)!.children.add(tree.frame.id);\n      // last-seen frame\n      this.frames.get(tree.frame.id)!.lastSeen = tree.frame;\n      // ownership (only if unknown)\n      if (!this.frames.get(tree.frame.id)!.ownerSessionId) {\n        this.setOwnerSessionIdInternal(tree.frame.id, sessionId);\n      }\n      for (const c of tree.childFrames ?? []) walk(c, tree.frame.id);\n    };\n    walk(frameTree, null);\n  }\n\n  /**\n   * Set the backendNodeId of the `<iframe>` element for a child frame **as seen from its parent**.\n   * This is useful for building absolute XPath prefixes later (from the parent document).\n   */\n  setOwnerBackendNodeId(childFrameId: FrameId, backendNodeId: number): void {\n    this.ensureNode(childFrameId);\n    this.frames.get(childFrameId)!.ownerBackendNodeId = backendNodeId;\n  }\n\n  // ---------------------- Readers (consumed by Page/snapshot/locators) ----------------------\n\n  mainFrameId(): FrameId {\n    return this.rootFrameId;\n  }\n\n  /**\n   * Return the owner session id for this frame. If unknown, returns `undefined`.\n   */\n  getOwnerSessionId(frameId: FrameId): SessionId | undefined {\n    return this.frames.get(frameId)?.ownerSessionId;\n  }\n\n  /**\n   * Return the owner backendNodeId (iframe element) if recorded.\n   * This is in the **parent** document; pair it with `getParent`.\n   */\n  getOwnerBackendNodeId(frameId: FrameId): number | undefined {\n    return this.frames.get(frameId)?.ownerBackendNodeId;\n  }\n\n  /**\n   * Return the parent frame id, or null for root/unknown.\n   */\n  getParent(frameId: FrameId): FrameId | null {\n    return this.frames.get(frameId)?.parentId ?? null;\n  }\n\n  /**\n   * List frame ids in root-first DFS order (same shape as CDP’s FrameTree traversal).\n   */\n  listAllFrames(): FrameId[] {\n    const out: FrameId[] = [];\n    const dfs = (fid: FrameId) => {\n      out.push(fid);\n      const kids = this.frames.get(fid)?.children ?? new Set<FrameId>();\n      for (const k of kids) dfs(k);\n    };\n    if (this.frames.has(this.rootFrameId)) dfs(this.rootFrameId);\n    return out;\n  }\n\n  /**\n   * Serialize to `Protocol.Page.FrameTree` starting at the given root id (typically mainFrameId()).\n   */\n  asProtocolFrameTree(rootId: FrameId): Protocol.Page.FrameTree {\n    const build = (fid: FrameId): Protocol.Page.FrameTree => {\n      const info = this.frames.get(fid);\n      const frame = info?.lastSeen ?? shellFrame(fid);\n\n      const kids = info?.children ?? new Set<FrameId>();\n      const childFrames = kids.size\n        ? [...kids].map((k) => build(k))\n        : undefined;\n\n      return childFrames ? { frame, childFrames } : { frame };\n    };\n\n    return build(rootId);\n  }\n\n  /**\n   * For diagnostics: return the current owner sessions for a frame id (0..n),\n   * usually 0 or 1, but helpful to see potential inconsistencies during wiring.\n   */\n  sessionsForFrame(frameId: FrameId): SessionId[] {\n    const info = this.frames.get(frameId);\n    return info?.ownerSessionId ? [info.ownerSessionId] : [];\n  }\n\n  /**\n   * For diagnostics: return current frame set per session.\n   */\n  framesForSession(sessionId: SessionId): FrameId[] {\n    return [...(this.framesBySession.get(sessionId) ?? new Set())];\n  }\n\n  // ---------------------- Internal helpers ----------------------\n\n  private ensureNode(fid: FrameId): void {\n    if (this.frames.has(fid)) return;\n    this.frames.set(fid, {\n      parentId: null,\n      children: new Set<FrameId>(),\n      lastSeen: shellFrame(fid),\n      ownerSessionId: undefined,\n      ownerBackendNodeId: undefined,\n    });\n  }\n\n  private renameNodeId(oldId: FrameId, newId: FrameId): void {\n    if (oldId === newId) return;\n    this.ensureNode(oldId);\n\n    const info = this.frames.get(oldId)!;\n\n    // Move info under new id\n    this.frames.delete(oldId);\n    this.frames.set(newId, { ...info });\n\n    // Fix parent’s children set\n    if (info.parentId) {\n      const p = this.frames.get(info.parentId);\n      if (p) {\n        p.children.delete(oldId);\n        p.children.add(newId);\n      }\n    }\n\n    // Fix children’s parent pointers\n    for (const c of info.children) {\n      const ci = this.frames.get(c);\n      if (ci) ci.parentId = newId;\n    }\n\n    // Fix inverse map (session -> frames)\n    if (info.ownerSessionId) {\n      const bag = this.framesBySession.get(info.ownerSessionId);\n      if (bag) {\n        bag.delete(oldId);\n        bag.add(newId);\n      }\n    }\n\n    // If root moved, keep the root id updated is handled by caller\n  }\n\n  private setOwnerSessionIdInternal(\n    frameId: FrameId,\n    sessionId: SessionId,\n  ): void {\n    this.ensureNode(frameId);\n    const info = this.frames.get(frameId)!;\n\n    // If the owner is unchanged, do nothing\n    if (info.ownerSessionId === sessionId) return;\n\n    // Remove from previous owner bag\n    if (info.ownerSessionId) {\n      const prev = this.framesBySession.get(info.ownerSessionId);\n      prev?.delete(frameId);\n      if (prev && prev.size === 0)\n        this.framesBySession.delete(info.ownerSessionId);\n    }\n\n    // Set new owner and update bag\n    info.ownerSessionId = sessionId;\n    const bag = this.framesBySession.get(sessionId) ?? new Set<FrameId>();\n    bag.add(frameId);\n    this.framesBySession.set(sessionId, bag);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/initScripts.ts",
    "content": "import { promises as fs } from \"fs\";\nimport { InitScriptSource } from \"../types/private/index.js\";\nimport { StagehandInvalidArgumentError } from \"../types/public/sdkErrors.js\";\n\nconst DEFAULT_CALLER = \"context.addInitScript\";\n\nfunction appendSourceURL(source: string, filePath: string): string {\n  const sanitized = filePath.replace(/\\n/g, \"\");\n  return `${source}\\n//# sourceURL=${sanitized}`;\n}\n\nexport async function normalizeInitScriptSource<Arg>(\n  script: InitScriptSource<Arg>,\n  arg?: Arg,\n  caller: string = DEFAULT_CALLER,\n): Promise<string> {\n  if (typeof script === \"function\") {\n    const argString = Object.is(arg, undefined)\n      ? \"undefined\"\n      : JSON.stringify(arg);\n    return `(${script.toString()})(${argString})`;\n  }\n\n  if (!Object.is(arg, undefined)) {\n    throw new StagehandInvalidArgumentError(\n      `${caller}: 'arg' is only supported when passing a function.`,\n    );\n  }\n\n  if (typeof script === \"string\") {\n    return script;\n  }\n\n  if (!script || typeof script !== \"object\") {\n    throw new StagehandInvalidArgumentError(\n      `${caller}: provide a string, function, or an object with path/content.`,\n    );\n  }\n\n  if (typeof script.content === \"string\") {\n    return script.content;\n  }\n\n  if (typeof script.path === \"string\" && script.path.trim()) {\n    const raw = await fs.readFile(script.path, \"utf8\");\n    return appendSourceURL(raw, script.path);\n  }\n\n  throw new StagehandInvalidArgumentError(\n    `${caller}: provide a string, function, or an object with path/content.`,\n  );\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/lifecycleWatcher.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { LoadState } from \"../types/public/page.js\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport type { NetworkManager } from \"./networkManager.js\";\nimport type { Page } from \"./page.js\";\nimport { TimeoutError } from \"../types/public/sdkErrors.js\";\nimport {\n  DEFAULT_IDLE_WAIT,\n  IGNORED_RESOURCE_TYPES,\n  type NetworkRequestInfo,\n  WaitForIdleHandle,\n} from \"../types/private/network.js\";\n\n/**\n * Coordinates page lifecycle waits (load/domcontentloaded/networkidle) while\n * following main-frame swaps and navigation aborts. Each navigation spawns a\n * one-off watcher that listens for relevant CDP events and resolves or rejects\n * depending on the requested `waitUntil` state.\n */\n\n/**\n * Small utility that mirrors Playwright's lifecycle watcher semantics. Bridges\n * main-frame lifecycle events with the NetworkManager's idle signal so callers\n * can await `load`, `domcontentloaded`, or `networkidle` with a single promise.\n */\nexport class LifecycleWatcher {\n  private readonly page: Page;\n  private readonly mainSession: CDPSessionLike;\n  private readonly networkManager: NetworkManager;\n  private readonly waitUntil: LoadState;\n  private readonly timeoutMs: number;\n  private readonly startTime: number;\n  private readonly navigationCommandId: number;\n  private currentLoaderId: string | undefined;\n  private idleStartTime: number;\n\n  private cleanupCallbacks: Array<() => void> = [];\n  private idleHandle: WaitForIdleHandle | null = null;\n\n  private abortReject: ((error: Error) => void) | null = null;\n  private abortPromise: Promise<never>;\n  private abortError: Error | null = null;\n  private disposed = false;\n\n  private expectedLoaderId: string | undefined;\n  private initialLoaderId: string | undefined;\n  private pendingFollowupNavigation = false;\n\n  /**\n   * Create a watcher; callers should subsequently invoke {@link wait}.\n   */\n  constructor(params: {\n    page: Page;\n    mainSession: CDPSessionLike;\n    networkManager: NetworkManager;\n    waitUntil: LoadState;\n    timeoutMs: number;\n    navigationCommandId: number;\n  }) {\n    this.page = params.page;\n    this.mainSession = params.mainSession;\n    this.networkManager = params.networkManager;\n    this.waitUntil = params.waitUntil;\n    this.timeoutMs = params.timeoutMs;\n    this.startTime = Date.now();\n    this.navigationCommandId = params.navigationCommandId;\n    this.idleStartTime = this.startTime;\n\n    this.abortPromise = new Promise<never>((_, reject) => {\n      this.abortReject = reject;\n    });\n\n    this.installSessionListeners();\n  }\n\n  /** Hint the watcher with the loader id returned by Page.navigate. */\n  public setExpectedLoaderId(loaderId: string | undefined): void {\n    if (!loaderId) return;\n    this.expectedLoaderId = loaderId;\n    this.initialLoaderId = loaderId;\n    this.currentLoaderId = loaderId;\n    this.idleStartTime = Date.now();\n  }\n\n  /** Wait for the requested lifecycle state or throw on timeout/abort. */\n  public async wait(): Promise<void> {\n    const deadline = Date.now() + this.timeoutMs;\n\n    try {\n      if (this.waitUntil === \"domcontentloaded\") {\n        await this.awaitWithAbort(\n          this.page.waitForMainLoadState(\n            \"domcontentloaded\",\n            this.timeRemaining(deadline),\n          ),\n        );\n        return;\n      }\n\n      while (true) {\n        await this.awaitWithAbort(\n          this.page.waitForMainLoadState(\"load\", this.timeRemaining(deadline)),\n        );\n\n        if (this.waitUntil !== \"networkidle\") break;\n\n        try {\n          await this.awaitWithAbort(this.waitForNetworkIdle(deadline));\n          break;\n        } catch (error) {\n          if (this.shouldRestartAfterFollowup(error)) {\n            continue;\n          }\n          throw error;\n        }\n      }\n    } finally {\n      this.dispose();\n    }\n\n    if (this.abortError) throw this.abortError;\n  }\n\n  /** Cancel any outstanding network-idle waits and remove event listeners. */\n  public dispose(): void {\n    if (this.disposed) return;\n    this.disposed = true;\n\n    if (this.idleHandle) {\n      void this.idleHandle.promise.catch(() => {});\n      this.idleHandle.dispose();\n      this.idleHandle = null;\n    }\n\n    for (const fn of this.cleanupCallbacks) {\n      try {\n        fn();\n      } catch {\n        // ignore listener cleanup errors\n      }\n    }\n    this.cleanupCallbacks = [];\n    this.abortReject = null;\n  }\n\n  /** Subscribe to main-frame events to detect abort conditions. */\n  private installSessionListeners(): void {\n    const onFrameNavigated = (evt: Protocol.Page.FrameNavigatedEvent) => {\n      if (!evt?.frame?.id) return;\n\n      const mainFrameId = this.page.mainFrameId();\n      if (evt.frame.id !== mainFrameId) return;\n\n      const loaderId = evt.frame.loaderId;\n      if (!loaderId) return;\n\n      if (!this.initialLoaderId) {\n        this.initialLoaderId = loaderId;\n        this.currentLoaderId = loaderId;\n        this.idleStartTime = Date.now();\n      }\n\n      if (!this.expectedLoaderId) {\n        this.expectedLoaderId = loaderId;\n        this.currentLoaderId = loaderId;\n        this.idleStartTime = Date.now();\n        return;\n      }\n\n      if (loaderId !== this.expectedLoaderId) {\n        if (!this.page.isCurrentNavigationCommand(this.navigationCommandId)) {\n          this.triggerAbort(\n            new Error(\"Navigation was superseded by a new request\"),\n          );\n          return;\n        }\n\n        this.adoptNewMainLoader(loaderId);\n      }\n    };\n\n    const onFrameDetached = (evt: Protocol.Page.FrameDetachedEvent) => {\n      if (!evt?.frameId) return;\n      const mainFrameId = this.page.mainFrameId();\n      if (evt.frameId !== mainFrameId) return;\n      if (evt.reason === \"swap\") return;\n      this.triggerAbort(new Error(\"Main frame was detached\"));\n    };\n\n    this.mainSession.on(\"Page.frameNavigated\", onFrameNavigated);\n    this.cleanupCallbacks.push(() => {\n      this.mainSession.off(\"Page.frameNavigated\", onFrameNavigated);\n    });\n\n    this.mainSession.on(\"Page.frameDetached\", onFrameDetached);\n    this.cleanupCallbacks.push(() => {\n      this.mainSession.off(\"Page.frameDetached\", onFrameDetached);\n    });\n  }\n\n  /** Compute remaining time until the shared deadline elapses. */\n  private timeRemaining(deadline: number): number {\n    const remaining = deadline - Date.now();\n    if (remaining <= 0) {\n      throw new TimeoutError(\"Lifecycle wait\", this.timeoutMs);\n    }\n    return remaining;\n  }\n\n  /** Await an operation but abort early if navigation replacement fires. */\n  private async awaitWithAbort<T>(operation: Promise<T>): Promise<T> {\n    try {\n      return await Promise.race([operation, this.abortPromise]);\n    } catch (error) {\n      if (this.abortError) throw this.abortError;\n      throw error;\n    }\n  }\n\n  /** Mark the watcher as aborted and reject any pending waiters. */\n  private triggerAbort(error: Error): void {\n    if (this.abortError) return;\n    this.abortError = error;\n    if (this.abortReject) {\n      this.abortReject(error);\n      this.abortReject = null;\n    }\n  }\n  private waitForNetworkIdle(deadline: number): Promise<void> {\n    this.pendingFollowupNavigation = false;\n    const remaining = this.timeRemaining(deadline);\n    const idleWindow = Math.min(DEFAULT_IDLE_WAIT, remaining);\n    this.idleHandle = this.networkManager.waitForIdle({\n      startTime: this.idleStartTime,\n      timeoutMs: remaining,\n      totalBudgetMs: this.timeoutMs,\n      idleTimeMs: idleWindow,\n      filter: this.buildIdleFilter(),\n    });\n\n    return this.idleHandle.promise.catch((error) => {\n      if (this.abortError) throw this.abortError;\n      throw error;\n    });\n  }\n\n  private shouldRestartAfterFollowup(error: unknown): boolean {\n    if (!this.pendingFollowupNavigation) return false;\n    if (!(error instanceof Error)) return false;\n    if (error.message !== \"waitForIdle disposed\") return false;\n    this.pendingFollowupNavigation = false;\n    return true;\n  }\n\n  private adoptNewMainLoader(loaderId: string): void {\n    this.expectedLoaderId = loaderId;\n    this.currentLoaderId = loaderId;\n    this.idleStartTime = Date.now();\n    if (this.waitUntil !== \"networkidle\") return;\n\n    this.pendingFollowupNavigation = true;\n\n    if (this.idleHandle) {\n      const handle = this.idleHandle;\n      this.idleHandle = null;\n      void handle.promise.catch(() => {});\n      handle.dispose();\n    }\n  }\n\n  private buildIdleFilter(): (info: NetworkRequestInfo) => boolean {\n    const loaderId = this.currentLoaderId;\n    const mainFrameId = this.page.mainFrameId();\n\n    return (info: NetworkRequestInfo) => {\n      if (IGNORED_RESOURCE_TYPES.has(info.resourceType)) return false;\n\n      if (loaderId && info.loaderId) {\n        return info.loaderId === loaderId;\n      }\n\n      if (!info.loaderId && info.frameId) {\n        return info.frameId === mainFrameId;\n      }\n\n      return true;\n    };\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/locator.ts",
    "content": "// lib/v3/understudy/locator.ts\nimport { Protocol } from \"devtools-protocol\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport {\n  locatorScriptBootstrap,\n  locatorScriptGlobalRefs,\n  locatorScriptSources,\n} from \"../dom/build/locatorScripts.generated.js\";\nimport type { Frame } from \"./frame.js\";\nimport {\n  FrameSelectorResolver,\n  type SelectorQuery,\n} from \"./selectorResolver.js\";\nimport {\n  StagehandElementNotFoundError,\n  StagehandInvalidArgumentError,\n  StagehandLocatorError,\n  ElementNotVisibleError,\n} from \"../types/public/sdkErrors.js\";\nimport { normalizeInputFiles } from \"./fileUploadUtils.js\";\nimport { SetInputFilesArgument, MouseButton } from \"../types/public/locator.js\";\nimport { NormalizedFilePayload } from \"../types/private/locator.js\";\n\nconst MAX_REMOTE_UPLOAD_BYTES = 50 * 1024 * 1024; // 50MB guard copied from Playwright\n\n/**\n * Locator\n *\n * Purpose:\n * A small, CDP-based element interaction helper scoped to a specific `Frame`.\n * It resolves a CSS/XPath selector inside the frame’s **isolated world**, and then\n * performs low-level actions (click, type, select) using DOM/Runtime/Input\n * protocol domains with minimal abstraction.\n *\n * Key change:\n * - Prefer **objectId**-based CDP calls (scroll, geometry) to avoid brittle\n *   frontend nodeId mappings. nodeId is resolved on a best-effort basis and\n *   returned for compatibility, but actions do not depend on it.\n *\n * Notes:\n * - Resolution is lazy: every action resolves the selector again.\n * - Uses `Page.createIsolatedWorld` so evaluation is isolated from page scripts.\n * - Releases remote objects (`Runtime.releaseObject`) where appropriate.\n */\nexport class Locator {\n  private readonly selectorResolver: FrameSelectorResolver;\n\n  private readonly selectorQuery: SelectorQuery;\n\n  // -1 means \"no explicit nth()\"; default locator resolves to first match for actions.\n  private readonly nthIndex: number;\n\n  constructor(\n    private readonly frame: Frame,\n    private readonly selector: string,\n    private readonly options?: { deep?: boolean; depth?: number },\n    nthIndex: number = -1,\n  ) {\n    this.selectorResolver = new FrameSelectorResolver(this.frame);\n    this.selectorQuery = FrameSelectorResolver.parseSelector(selector);\n    const normalized = Number.isFinite(nthIndex) ? Math.floor(nthIndex) : -1;\n    this.nthIndex = normalized < 0 ? -1 : normalized;\n  }\n\n  /** Return the owning Frame for this locator (typed accessor, no private access). */\n  public getFrame(): Frame {\n    return this.frame;\n  }\n\n  /**\n   * Set files on an <input type=\"file\"> element.\n   *\n   * Mirrors Playwright's Locator.setInputFiles basics:\n   * - Accepts file path(s) or payload object(s) { name, mimeType, buffer }.\n   * - Uses CDP DOM.setFileInputFiles under the hood.\n   * - Best‑effort dispatches change/input via CDP (Chrome does by default).\n   * - Passing an empty array clears the selection.\n   */\n  public async setInputFiles(files: SetInputFilesArgument): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n\n    const tempFiles: string[] = [];\n\n    try {\n      // Validate element is an <input type=\"file\">\n      try {\n        const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n          \"Runtime.callFunctionOn\",\n          {\n            objectId,\n            functionDeclaration: locatorScriptSources.ensureFileInputElement,\n            returnByValue: true,\n          },\n        );\n        const ok = Boolean(res.result.value);\n        if (!ok)\n          throw new StagehandInvalidArgumentError(\n            'Target is not an <input type=\"file\"> element',\n          );\n      } catch (e) {\n        throw new StagehandInvalidArgumentError(\n          e instanceof Error\n            ? e.message\n            : \"Unable to verify file input element\",\n        );\n      }\n\n      const normalized = await normalizeInputFiles(files);\n\n      if (!normalized.length) {\n        await session.send<never>(\"DOM.setFileInputFiles\", {\n          objectId,\n          files: [],\n        });\n        return;\n      }\n\n      if (this.frame.isBrowserRemote()) {\n        await this.assignFilesViaPayloadInjection(objectId, normalized);\n        return;\n      }\n\n      const filePaths: string[] = [];\n      for (const payload of normalized) {\n        if (payload.absolutePath) {\n          filePaths.push(payload.absolutePath);\n          continue;\n        }\n        const ext = path.extname(payload.name);\n        const tmp = path.join(\n          os.tmpdir(),\n          `stagehand-upload-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`,\n        );\n        await fs.promises.writeFile(tmp, payload.buffer);\n        tempFiles.push(tmp);\n        filePaths.push(tmp);\n      }\n\n      await session.send<never>(\"DOM.setFileInputFiles\", {\n        objectId,\n        files: filePaths,\n      });\n    } finally {\n      // Cleanup: release element and remove any temporary files we created\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n      for (const p of tempFiles) {\n        try {\n          await fs.promises.unlink(p);\n        } catch {\n          // ignore\n        }\n      }\n    }\n  }\n\n  /**\n   * Remote browser fallback: build File objects inside the page and attach them via JS.\n   *\n   * When Stagehand is driving a browser that cannot see the local filesystem (Browserbase,\n   * remote CDP, etc.), CDP's DOM.setFileInputFiles would fail because Chrome can't reach\n   * our temp files. Instead we base64-encode the payloads, send them into the page, and\n   * let a DOM helper create File objects + dispatch change/input events.\n   */\n  private async assignFilesViaPayloadInjection(\n    objectId: Protocol.Runtime.RemoteObjectId,\n    files: NormalizedFilePayload[],\n  ): Promise<void> {\n    const session = this.frame.session;\n\n    for (const payload of files) {\n      if (payload.buffer.length > MAX_REMOTE_UPLOAD_BYTES) {\n        throw new StagehandInvalidArgumentError(\n          `setInputFiles(): file \"${payload.name}\" is larger than the 50MB limit for remote uploads`,\n        );\n      }\n    }\n\n    const serialized = files.map((payload) => ({\n      name: payload.name,\n      mimeType: payload.mimeType,\n      lastModified: payload.lastModified,\n      base64: payload.buffer.toString(\"base64\"),\n    }));\n\n    const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n      \"Runtime.callFunctionOn\",\n      {\n        objectId,\n        functionDeclaration:\n          locatorScriptSources.assignFilePayloadsToInputElement,\n        arguments: [\n          {\n            value: serialized,\n          },\n        ],\n        returnByValue: true,\n      },\n    );\n\n    const ok = Boolean(res.result?.value);\n    if (!ok) {\n      throw new StagehandInvalidArgumentError(\n        \"Unable to assign file payloads to remote input element\",\n      );\n    }\n  }\n\n  /**\n   * Return the DOM backendNodeId for this locator's target element.\n   * Useful for identity comparisons without needing element handles.\n   */\n  async backendNodeId(): Promise<Protocol.DOM.BackendNodeId> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      await session.send(\"DOM.enable\").catch(() => {});\n      const { node } = await session.send<{ node: Protocol.DOM.Node }>(\n        \"DOM.describeNode\",\n        { objectId },\n      );\n      return node.backendNodeId as Protocol.DOM.BackendNodeId;\n    } finally {\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /** Return how many nodes the current selector resolves to. */\n  public async count(): Promise<number> {\n    const session = this.frame.session;\n    await session.send(\"Runtime.enable\");\n    await session.send(\"DOM.enable\");\n    return this.selectorResolver.count(this.selectorQuery);\n  }\n\n  /**\n   * Return the center of the element's bounding box in the owning frame's viewport\n   * (CSS pixels), rounded to integers. Scrolls into view best-effort.\n   */\n  public async centroid(): Promise<{ x: number; y: number }> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      await session\n        .send(\"DOM.scrollIntoViewIfNeeded\", { objectId })\n        .catch(() => {});\n      const box = await session.send<Protocol.DOM.GetBoxModelResponse>(\n        \"DOM.getBoxModel\",\n        { objectId },\n      );\n      if (!box.model) throw new ElementNotVisibleError(this.selector);\n      const { cx, cy } = this.centerFromBoxContent(box.model.content);\n      return { x: Math.round(cx), y: Math.round(cy) };\n    } finally {\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /**\n   * Highlight the element's bounding box using the CDP Overlay domain.\n   * - Scrolls element into view best-effort.\n   * - Shows a semi-transparent overlay briefly, then hides it.\n   */\n  public async highlight(options?: {\n    durationMs?: number;\n    borderColor?: { r: number; g: number; b: number; a?: number };\n    contentColor?: { r: number; g: number; b: number; a?: number };\n  }): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    const duration = Math.max(0, options?.durationMs ?? 800);\n\n    const borderColor = options?.borderColor ?? { r: 255, g: 0, b: 0, a: 0.9 };\n    const contentColor =\n      options?.contentColor ?? ({ r: 255, g: 200, b: 0, a: 0.2 } as const);\n\n    try {\n      await session.send(\"Overlay.enable\").catch(() => {});\n      await session\n        .send(\"DOM.scrollIntoViewIfNeeded\", { objectId })\n        .catch(() => {});\n\n      // Prefer backendNodeId to keep highlight stable even if objectId is released.\n      await session.send(\"DOM.enable\").catch(() => {});\n      let backendNodeId: Protocol.DOM.BackendNodeId | undefined;\n      try {\n        const { node } = await session.send<{ node: Protocol.DOM.Node }>(\n          \"DOM.describeNode\",\n          { objectId },\n        );\n        backendNodeId = node.backendNodeId as Protocol.DOM.BackendNodeId;\n      } catch {\n        backendNodeId = undefined;\n      }\n\n      const highlightConfig: Protocol.Overlay.HighlightConfig = {\n        showInfo: false,\n        showStyles: false,\n        showRulers: false,\n        showExtensionLines: false,\n        borderColor,\n        contentColor,\n      } as Protocol.Overlay.HighlightConfig;\n\n      const highlightOnce = async () => {\n        await session.send<never>(\"Overlay.highlightNode\", {\n          ...(backendNodeId ? { backendNodeId } : { objectId }),\n          highlightConfig,\n        });\n      };\n\n      // Initial draw\n      await highlightOnce();\n\n      // Keep alive until duration elapses to resist overlay clears on mouse move/repaints\n      if (duration > 0) {\n        const start = Date.now();\n        const tick = Math.min(300, Math.max(100, Math.floor(duration / 50)));\n        while (Date.now() - start < duration) {\n          await new Promise((r) => setTimeout(r, tick));\n          try {\n            await highlightOnce();\n          } catch {\n            // ignore transient errors\n          }\n        }\n        await session.send<never>(\"Overlay.hideHighlight\").catch(() => {});\n      }\n    } finally {\n      // Releasing objectId should not affect highlight when using backendNodeId.\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /**\n   * Move the mouse cursor to the element's visual center without clicking.\n   * - Scrolls into view best-effort, resolves geometry, then dispatches a mouse move.\n   */\n  async hover(): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      await session\n        .send(\"DOM.scrollIntoViewIfNeeded\", { objectId })\n        .catch(() => {});\n\n      const box = await session.send<Protocol.DOM.GetBoxModelResponse>(\n        \"DOM.getBoxModel\",\n        { objectId },\n      );\n      if (!box.model) throw new ElementNotVisibleError(this.selector);\n      const { cx, cy } = this.centerFromBoxContent(box.model.content);\n\n      await session.send<never>(\"Input.dispatchMouseEvent\", {\n        type: \"mouseMoved\",\n        x: cx,\n        y: cy,\n        button: \"none\",\n      } as Protocol.Input.DispatchMouseEventRequest);\n    } finally {\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /**\n   * Click the element at its visual center.\n   * Steps:\n   *  1) Resolve selector to { objectId } in the frame world.\n   *  2) Scroll into view via `DOM.scrollIntoViewIfNeeded({ objectId })`.\n   *  3) Read geometry via `DOM.getBoxModel({ objectId })` → compute a center point.\n   *  4) Synthesize mouse press + release via `Input.dispatchMouseEvent`.\n   */\n  async click(options?: {\n    button?: MouseButton;\n    clickCount?: number;\n  }): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n\n    const button = options?.button ?? \"left\";\n    const clickCount = options?.clickCount ?? 1;\n\n    try {\n      // Scroll into view using objectId (avoids frontend nodeId dependence)\n      await session.send(\"DOM.scrollIntoViewIfNeeded\", { objectId });\n\n      // Get geometry using objectId\n      const box = await session.send<Protocol.DOM.GetBoxModelResponse>(\n        \"DOM.getBoxModel\",\n        { objectId },\n      );\n      if (!box.model) throw new ElementNotVisibleError(this.selector);\n      const { cx, cy } = this.centerFromBoxContent(box.model.content);\n\n      // Dispatch click events in a pipelined burst to reduce inter-click delay\n      // from network/CPU jitter between round trips.\n      const dispatches: Array<Promise<unknown>> = [];\n      dispatches.push(\n        session.send<never>(\"Input.dispatchMouseEvent\", {\n          type: \"mouseMoved\",\n          x: cx,\n          y: cy,\n          button: \"none\",\n        } as Protocol.Input.DispatchMouseEventRequest),\n      );\n\n      for (let i = 1; i <= clickCount; i++) {\n        dispatches.push(\n          session.send<never>(\"Input.dispatchMouseEvent\", {\n            type: \"mousePressed\",\n            x: cx,\n            y: cy,\n            button,\n            clickCount: i,\n          } as Protocol.Input.DispatchMouseEventRequest),\n        );\n        dispatches.push(\n          session.send<never>(\"Input.dispatchMouseEvent\", {\n            type: \"mouseReleased\",\n            x: cx,\n            y: cy,\n            button,\n            clickCount: i,\n          } as Protocol.Input.DispatchMouseEventRequest),\n        );\n      }\n      await Promise.all(dispatches);\n    } finally {\n      // release the element handle\n      try {\n        await session.send<never>(\"Runtime.releaseObject\", { objectId });\n      } catch {\n        // If the context navigated or was destroyed (e.g., link opens new tab),\n        // releaseObject may fail with -32000. Ignore as best-effort cleanup.\n      }\n    }\n  }\n\n  /**\n   * Dispatch a DOM 'click' MouseEvent on the element itself.\n   * - Does not synthesize real pointer input; directly dispatches an event.\n   * - Useful for elements that rely on click handlers without needing hit-testing.\n   */\n  async sendClickEvent(options?: {\n    bubbles?: boolean;\n    cancelable?: boolean;\n    composed?: boolean;\n    detail?: number;\n  }): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    const bubbles = options?.bubbles ?? true;\n    const cancelable = options?.cancelable ?? true;\n    const composed = options?.composed ?? true;\n    const detail = options?.detail ?? 1;\n    try {\n      await session\n        .send(\"DOM.scrollIntoViewIfNeeded\", { objectId })\n        .catch(() => {});\n      await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.dispatchDomClick,\n          arguments: [\n            {\n              value: { bubbles, cancelable, composed, detail },\n            },\n          ],\n          returnByValue: true,\n        },\n      );\n    } finally {\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /**\n   * Scroll the element vertically to a given percentage (0–100).\n   * - If the element is <html> or <body>, scrolls the window/document.\n   * - Otherwise, scrolls the element itself via element.scrollTo.\n   */\n  async scrollTo(percent: number | string): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.scrollElementToPercent,\n          arguments: [{ value: percent as unknown as number }],\n          returnByValue: true,\n        },\n      );\n    } finally {\n      await session\n        .send<never>(\"Runtime.releaseObject\", { objectId })\n        .catch(() => {});\n    }\n  }\n\n  /**\n   * Fill an input/textarea/contenteditable element.\n   * Mirrors Playwright semantics: the DOM helper either applies the native\n   * value setter (for special input types) or asks us to type text via the CDP\n   * Input domain after focusing/selecting.\n   */\n  async fill(value: string): Promise<void> {\n    const session = this.frame.session;\n    // Use the bundled locator globals; the raw fill snippet depends on helper symbols.\n    const fillDeclaration = `function(value) { ${locatorScriptBootstrap}; return ${locatorScriptGlobalRefs.fillElementValue}.call(this, value); }`;\n    const { objectId } = await this.resolveNode();\n\n    let releaseNeeded = true;\n\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: fillDeclaration,\n          arguments: [{ value }],\n          returnByValue: true,\n        },\n      );\n      if (res.exceptionDetails) {\n        // prefer exception.description over text (eg \"Uncaught\")\n        const message =\n          res.exceptionDetails.exception?.description ??\n          res.exceptionDetails.text ??\n          \"Unknown exception during locator().fill()\";\n        throw new StagehandLocatorError(\"Filling\", this.selector, message);\n      }\n\n      const result = res.result.value as\n        | { status?: string; reason?: string; value?: string }\n        | null\n        | undefined;\n      const status =\n        typeof result === \"object\" && result ? result.status : undefined;\n\n      if (status === \"done\") {\n        return;\n      }\n\n      if (status === \"needsinput\") {\n        // Release the current handle before synthesizing keyboard input to avoid leaking it.\n        await session\n          .send<never>(\"Runtime.releaseObject\", { objectId })\n          .catch(() => {});\n        releaseNeeded = false;\n\n        const valueToType =\n          typeof result?.value === \"string\" ? result.value : value;\n\n        let prepared = false;\n        try {\n          const { objectId: prepObjectId } = await this.resolveNode();\n          try {\n            const prepRes =\n              await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n                \"Runtime.callFunctionOn\",\n                {\n                  objectId: prepObjectId,\n                  functionDeclaration:\n                    locatorScriptSources.prepareElementForTyping,\n                  returnByValue: true,\n                },\n              );\n            prepared = Boolean(prepRes.result.value);\n          } finally {\n            await session\n              .send<never>(\"Runtime.releaseObject\", { objectId: prepObjectId })\n              .catch(() => {});\n          }\n        } catch {\n          // Ignore preparation failures; we'll fall back to typing best-effort.\n        }\n\n        if (!prepared && valueToType.length > 0) {\n          await this.type(valueToType);\n          return;\n        }\n\n        if (valueToType.length === 0) {\n          // Simulate deleting the currently selected text to clear the field.\n          await session.send<never>(\"Input.dispatchKeyEvent\", {\n            type: \"keyDown\",\n            key: \"Backspace\",\n            code: \"Backspace\",\n            windowsVirtualKeyCode: 8,\n            nativeVirtualKeyCode: 8,\n          } as Protocol.Input.DispatchKeyEventRequest);\n          await session.send<never>(\"Input.dispatchKeyEvent\", {\n            type: \"keyUp\",\n            key: \"Backspace\",\n            code: \"Backspace\",\n            windowsVirtualKeyCode: 8,\n            nativeVirtualKeyCode: 8,\n          } as Protocol.Input.DispatchKeyEventRequest);\n        } else {\n          await session.send<never>(\"Input.insertText\", { text: valueToType });\n        }\n\n        return;\n      }\n\n      if (status === \"error\") {\n        const reason =\n          typeof result?.reason === \"string\" && result.reason.length > 0\n            ? result.reason\n            : \"Failed to fill element\";\n        throw new StagehandInvalidArgumentError(\n          `Failed to fill element (${reason})`,\n        );\n      }\n\n      // Backward compatibility: if no status is returned (older bundle), fall back to setter logic.\n      if (!status) {\n        await this.type(value);\n      }\n    } finally {\n      if (releaseNeeded) {\n        await session\n          .send<never>(\"Runtime.releaseObject\", { objectId })\n          .catch(() => {});\n      }\n    }\n  }\n\n  /**\n   * Type text into the element (focuses first).\n   * - Focus via element.focus() in page JS (no DOM.focus(nodeId)).\n   * - If no delay, uses `Input.insertText` for efficiency.\n   * - With delay, synthesizes `keyDown`/`keyUp` per character.\n   */\n  async type(text: string, options?: { delay?: number }): Promise<void> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n\n    try {\n      // Focus using JS (avoids DOM.focus(nodeId))\n      await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.focusElement,\n          returnByValue: true,\n        },\n      );\n\n      if (!options?.delay) {\n        await session.send<never>(\"Input.insertText\", { text });\n        return;\n      }\n\n      for (const ch of text) {\n        await session.send<never>(\"Input.dispatchKeyEvent\", {\n          type: \"keyDown\",\n          text: ch,\n          key: ch,\n        } as Protocol.Input.DispatchKeyEventRequest);\n\n        await session.send<never>(\"Input.dispatchKeyEvent\", {\n          type: \"keyUp\",\n          text: ch,\n          key: ch,\n        } as Protocol.Input.DispatchKeyEventRequest);\n\n        await new Promise((r) => setTimeout(r, options.delay));\n      }\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Select one or more options on a `<select>` element.\n   * Returns the values actually selected after the operation.\n   */\n  async selectOption(values: string | string[]): Promise<string[]> {\n    const session = this.frame.session;\n    const desired = Array.isArray(values) ? values : [values];\n    const { objectId } = await this.resolveNode();\n\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.selectElementOptions,\n          arguments: [{ value: desired }],\n          returnByValue: true,\n        },\n      );\n\n      return (res.result.value as string[]) ?? [];\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return true if the element is attached and visible (rough heuristic).\n   */\n  async isVisible(): Promise<boolean> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.isElementVisible,\n          returnByValue: true,\n        },\n      );\n      return Boolean(res.result.value);\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return true if the element is an input[type=checkbox|radio] and is checked.\n   * Also considers aria-checked for ARIA widgets.\n   */\n  async isChecked(): Promise<boolean> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.isElementChecked,\n          returnByValue: true,\n        },\n      );\n      return Boolean(res.result.value);\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return the element's input value (for input/textarea/select/contenteditable).\n   */\n  async inputValue(): Promise<string> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.readElementInputValue,\n          returnByValue: true,\n        },\n      );\n      return String(res.result.value ?? \"\");\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return the element's textContent (raw, not innerText).\n   */\n  async textContent(): Promise<string> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.readElementTextContent,\n          returnByValue: true,\n        },\n      );\n      return String(res.result.value ?? \"\");\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return the element's innerHTML string.\n   */\n  async innerHtml(): Promise<string> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.readElementInnerHTML,\n          returnByValue: true,\n        },\n      );\n      return String(res.result.value ?? \"\");\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return the element's innerText (layout-aware, visible text).\n   */\n  async innerText(): Promise<string> {\n    const session = this.frame.session;\n    const { objectId } = await this.resolveNode();\n    try {\n      const res = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n        \"Runtime.callFunctionOn\",\n        {\n          objectId,\n          functionDeclaration: locatorScriptSources.readElementInnerText,\n          returnByValue: true,\n        },\n      );\n      return String(res.result.value ?? \"\");\n    } finally {\n      await session.send<never>(\"Runtime.releaseObject\", { objectId });\n    }\n  }\n\n  /**\n   * Return a locator narrowed to the first match.\n   */\n  first(): Locator {\n    return this.nth(0);\n  }\n\n  /** Return a locator narrowed to the element at the given zero-based index. */\n  nth(index: number): Locator {\n    const value = Number(index);\n    if (!Number.isFinite(value) || value < 0) {\n      throw new StagehandInvalidArgumentError(\n        \"locator().nth() expects a non-negative index\",\n      );\n    }\n\n    const nextIndex = Math.floor(value);\n    if (nextIndex === this.nthIndex) {\n      return this;\n    }\n\n    return new Locator(this.frame, this.selector, this.options, nextIndex);\n  }\n\n  // ---------- helpers ----------\n\n  /**\n   * Resolve `this.selector` within the frame to `{ objectId, nodeId? }`:\n   * Delegates to a shared selector resolver so all selector logic stays in sync.\n   */\n  public async resolveNode(): Promise<{\n    nodeId: Protocol.DOM.NodeId | null;\n    objectId: Protocol.Runtime.RemoteObjectId;\n  }> {\n    const session = this.frame.session;\n\n    await session.send(\"Runtime.enable\");\n    await session.send(\"DOM.enable\");\n\n    const index = this.nthIndex < 0 ? 0 : this.nthIndex;\n    const resolved = await this.selectorResolver.resolveAtIndex(\n      this.selectorQuery,\n      index,\n    );\n    if (!resolved) {\n      throw new StagehandElementNotFoundError([this.selector]);\n    }\n\n    return resolved;\n  }\n\n  /**\n   * Resolve all matching nodes for this locator.\n   * If the locator is narrowed via nth(), only that index is returned.\n   */\n  public async resolveNodesForMask(): Promise<\n    Array<{\n      nodeId: Protocol.DOM.NodeId | null;\n      objectId: Protocol.Runtime.RemoteObjectId;\n    }>\n  > {\n    const session = this.frame.session;\n\n    await session.send(\"Runtime.enable\");\n    await session.send(\"DOM.enable\");\n\n    if (this.nthIndex >= 0) {\n      const resolved = await this.selectorResolver.resolveAtIndex(\n        this.selectorQuery,\n        this.nthIndex,\n      );\n      if (!resolved) {\n        throw new StagehandElementNotFoundError([this.selector]);\n      }\n      return [resolved];\n    }\n\n    const resolved = await this.selectorResolver.resolveAll(this.selectorQuery);\n    if (!resolved.length) {\n      throw new StagehandElementNotFoundError([this.selector]);\n    }\n    return resolved;\n  }\n\n  /** Compute a center point from a BoxModel content quad */\n  private centerFromBoxContent(content: number[]): { cx: number; cy: number } {\n    // content is [x1,y1, x2,y2, x3,y3, x4,y4]\n    if (!content || content.length < 8) {\n      throw new StagehandInvalidArgumentError(\"Invalid box model content quad\");\n    }\n    const xs = [content[0], content[2], content[4], content[6]];\n    const ys = [content[1], content[3], content[5], content[7]];\n    const cx = (xs[0] + xs[1] + xs[2] + xs[3]) / 4;\n    const cy = (ys[0] + ys[1] + ys[2] + ys[3]) / 4;\n    return { cx, cy };\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/locatorInvocation.ts",
    "content": "import {\n  locatorScriptBootstrap,\n  locatorScriptGlobalRefs,\n  type LocatorScriptName,\n} from \"../dom/build/locatorScripts.generated.js\";\n\n/**\n * Build an expression that injects the locator bundle (if needed) and invokes a\n * specific helper via its stable global reference. This keeps Runtime.evaluate\n * payloads tiny while guaranteeing our selector utilities are present in any\n * execution context.\n */\nexport function buildLocatorInvocation(\n  name: LocatorScriptName,\n  args: string[],\n): string {\n  const invocation = `${locatorScriptGlobalRefs[name]}(${args.join(\", \")})`;\n  return `(() => { ${locatorScriptBootstrap}; return ${invocation}; })()`;\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/navigationResponseTracker.ts",
    "content": "/**\n * NavigationResponseTracker\n * -------------------------\n *\n * Tracks DevTools Protocol network events for a single navigation command so\n * Stagehand can surface a Playwright-like response object from `Page.goto` and\n * related APIs. The tracker listens for `Network.responseReceived` events that\n * correspond to the targeted document navigation, handles loader-id churn that\n * arises from redirects or preloading, and enriches the resulting\n * `Response` with extra header information. It also observes\n * `Network.loadingFinished` / `Network.loadingFailed` to fulfil the\n * `response.finished()` contract exposed to consumers.\n */\n\nimport type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport type { Page } from \"./page.js\";\nimport { Response } from \"./response.js\";\n\n/**\n * Watches CDP events on a given session and resolves with the navigation's\n * primary document response once identified.\n */\nexport class NavigationResponseTracker {\n  private readonly page: Page;\n  private readonly session: CDPSessionLike;\n  private readonly navigationCommandId: number;\n\n  private expectedLoaderId: string | undefined;\n  private selectedRequestId: string | null = null;\n  private selectedResponse: Response | null = null;\n  private acceptNextWithoutLoader = false;\n\n  private responseResolved = false;\n  private resolveResponse!: (value: Response | null) => void;\n  private responsePromise: Promise<Response | null>;\n\n  private readonly pendingResponsesByLoader = new Map<\n    string,\n    Protocol.Network.ResponseReceivedEvent\n  >();\n  private readonly pendingExtraInfo = new Map<\n    string,\n    Protocol.Network.ResponseReceivedExtraInfoEvent\n  >();\n\n  private readonly listeners: Array<{\n    event: string;\n    handler: (event: unknown) => void;\n  }> = [];\n\n  /**\n   * Create a tracker bound to a specific navigation command. The tracker begins\n   * listening for network events immediately so it should be constructed before\n   * the navigation request is dispatched.\n   */\n  constructor(params: {\n    page: Page;\n    session: CDPSessionLike;\n    navigationCommandId: number;\n  }) {\n    this.page = params.page;\n    this.session = params.session;\n    this.navigationCommandId = params.navigationCommandId;\n\n    this.responsePromise = new Promise<Response | null>((resolve) => {\n      this.resolveResponse = (value) => {\n        if (this.responseResolved) return;\n        this.responseResolved = true;\n        resolve(value);\n      };\n    });\n\n    this.installListeners();\n  }\n\n  /** Stop listening for CDP events and release any pending bookkeeping. */\n  public dispose(): void {\n    for (const { event, handler } of this.listeners) {\n      this.session.off(event, handler as never);\n    }\n    this.listeners.length = 0;\n    this.pendingResponsesByLoader.clear();\n    this.pendingExtraInfo.clear();\n  }\n\n  /**\n   * Hint the tracker with the loader id returned by `Page.navigate`. Chrome only\n   * emits this once the browser begins navigating, so we store early responses\n   * and match them once the loader id is known.\n   */\n  public setExpectedLoaderId(loaderId: string | undefined): void {\n    if (!loaderId) return;\n    this.expectedLoaderId = loaderId;\n    const pending = this.pendingResponsesByLoader.get(loaderId);\n    if (pending) {\n      this.pendingResponsesByLoader.delete(loaderId);\n      this.selectResponse(pending);\n    }\n  }\n\n  /**\n   * Some navigation APIs (reload/history traversal) do not provide a loader id\n   * up front. This flag instructs the tracker to accept the next qualifying\n   * document response even if no loader id has been announced yet.\n   */\n  public expectNavigationWithoutKnownLoader(): void {\n    this.acceptNextWithoutLoader = true;\n  }\n\n  /**\n   * Returns a promise that resolves with the matched response (or `null` when\n   * no document response was observed).\n   */\n  public async navigationCompleted(): Promise<Response | null> {\n    if (!this.responseResolved) {\n      queueMicrotask(() => {\n        if (!this.responseResolved) this.resolveResponse(null);\n      });\n    }\n    return this.responsePromise;\n  }\n\n  /** Expose the raw response promise (mainly for tests). */\n  public async response(): Promise<Response | null> {\n    return this.responsePromise;\n  }\n\n  /** Register all CDP listeners relevant to navigation tracking. */\n  private installListeners(): void {\n    this.addListener(\"Network.responseReceived\", (event) => {\n      this.onResponseReceived(event as Protocol.Network.ResponseReceivedEvent);\n    });\n    this.addListener(\"Network.responseReceivedExtraInfo\", (event) => {\n      this.onResponseReceivedExtraInfo(\n        event as Protocol.Network.ResponseReceivedExtraInfoEvent,\n      );\n    });\n    this.addListener(\"Network.loadingFinished\", (event) => {\n      this.onLoadingFinished(event as Protocol.Network.LoadingFinishedEvent);\n    });\n    this.addListener(\"Network.loadingFailed\", (event) => {\n      this.onLoadingFailed(event as Protocol.Network.LoadingFailedEvent);\n    });\n  }\n\n  /** Attach a CDP listener and track it for later disposal. */\n  private addListener(event: string, handler: (event: unknown) => void): void {\n    this.session.on(event, handler as never);\n    this.listeners.push({ event, handler });\n  }\n\n  /** Handle the initial response payload for document navigations. */\n  private onResponseReceived(\n    event: Protocol.Network.ResponseReceivedEvent,\n  ): void {\n    if (!this.page.isCurrentNavigationCommand(this.navigationCommandId)) return;\n    if (!event || !event.response) return;\n    if (event.type !== \"Document\") return;\n    if (event.frameId !== this.page.mainFrameId()) return;\n\n    const loaderId = event.loaderId ?? \"\";\n    if (this.acceptNextWithoutLoader) {\n      this.acceptNextWithoutLoader = false;\n      this.selectResponse(event);\n      return;\n    }\n\n    if (this.expectedLoaderId) {\n      if (loaderId && loaderId !== this.expectedLoaderId) {\n        this.pendingResponsesByLoader.set(loaderId, event);\n        return;\n      }\n      this.selectResponse(event);\n      return;\n    }\n\n    if (loaderId) {\n      this.pendingResponsesByLoader.set(loaderId, event);\n      return;\n    }\n\n    this.selectResponse(event);\n  }\n\n  /** Merge auxiliary header information once Chrome exposes it. */\n  private onResponseReceivedExtraInfo(\n    event: Protocol.Network.ResponseReceivedExtraInfoEvent,\n  ): void {\n    if (!event || !event.requestId) return;\n    if (this.selectedRequestId && event.requestId === this.selectedRequestId) {\n      this.selectedResponse?.applyExtraInfo(event);\n      return;\n    }\n    this.pendingExtraInfo.set(event.requestId, event);\n  }\n\n  /** Resolve the response's finished promise when the request completes. */\n  private onLoadingFinished(\n    event: Protocol.Network.LoadingFinishedEvent,\n  ): void {\n    if (!event || !event.requestId) return;\n    if (event.requestId !== this.selectedRequestId) return;\n    this.selectedResponse?.markFinished(null);\n  }\n\n  /** Resolve the response's finished promise with an error on failure. */\n  private onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {\n    // Ignore malformed events or ones without a request id\n    if (!event || !event.requestId) return;\n    // Only the tracked document request should toggle the response state\n    if (event.requestId !== this.selectedRequestId) return;\n    // Surface Chrome's failure text through response.finished()\n    const errorText = event.errorText || \"Navigation request failed\";\n    this.selectedResponse?.markFinished(new Error(errorText));\n  }\n\n  /**\n   * Create the `Response` wrapper for the chosen document response and\n   * resolve awaiting consumers. Subsequent events flesh out the header/body\n   * helpers and mark the request as finished.\n   */\n  private selectResponse(event: Protocol.Network.ResponseReceivedEvent): void {\n    if (event.loaderId) {\n      this.pendingResponsesByLoader.delete(event.loaderId);\n    }\n\n    if (this.responseResolved) return;\n    if (this.selectedResponse) return;\n\n    const protocol = event.response?.protocol?.toLowerCase() ?? \"\";\n    const url = event.response?.url ?? \"\";\n    const isDataUrl = protocol === \"data\" || url.startsWith(\"data:\");\n    const isAboutUrl = protocol === \"about\" || url.startsWith(\"about:\");\n\n    if (isDataUrl || isAboutUrl) {\n      this.pendingExtraInfo.delete(event.requestId);\n      this.selectedRequestId = null;\n      this.selectedResponse = null;\n      this.resolveResponse(null);\n      return;\n    }\n\n    const response = new Response({\n      page: this.page,\n      session: this.session,\n      requestId: event.requestId,\n      frameId: event.frameId,\n      loaderId: event.loaderId,\n      response: event.response,\n      fromServiceWorker: Boolean(event.response?.fromServiceWorker),\n    });\n\n    this.selectedRequestId = event.requestId;\n    this.selectedResponse = response;\n\n    const extraInfo = this.pendingExtraInfo.get(event.requestId);\n    if (extraInfo) {\n      response.applyExtraInfo(extraInfo);\n      this.pendingExtraInfo.delete(event.requestId);\n    }\n\n    this.resolveResponse(response);\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/networkManager.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport {\n  DEFAULT_IDLE_WAIT,\n  IGNORED_RESOURCE_TYPES,\n  NetworkObserver,\n  NetworkRequestInfo,\n  WaitForIdleHandle,\n  WaitForIdleOptions,\n} from \"../types/private/network.js\";\n\n/**\n * Cross-session network tracker.\n *\n * Centralises network bookkeeping for a Page: every CDP session (top-level and OOPIF)\n * funnels `Network.*` events through here so higher-level waiters can reason about\n * in-flight requests across the entire frame tree. The manager exposes a simple\n * observer interface plus a \"wait until idle\" helper that resolves once no filtered\n * requests remain for a quiet window.\n */\n\n/**\n * Aggregates network information for all CDP sessions owned by a Page.\n */\nexport class NetworkManager {\n  private readonly sessions = new Map<\n    string,\n    {\n      session: CDPSessionLike;\n      detach: () => void;\n    }\n  >();\n\n  private readonly observers = new Set<NetworkObserver>();\n\n  private readonly requests = new Map<string, NetworkRequestInfo>();\n\n  private readonly documentRequestsByFrame = new Map<string, string>();\n\n  /**\n   * Begin tracking network traffic for a CDP session (top-level or OOPIF).\n   * Safe to call multiple times; duplicate registrations are ignored.\n   */\n  public trackSession(session: CDPSessionLike): void {\n    const sid = this.sessionKey(session);\n    if (this.sessions.has(sid)) return;\n\n    const onRequest = (evt: Protocol.Network.RequestWillBeSentEvent) => {\n      if (!evt || !evt.requestId) return;\n\n      const info: NetworkRequestInfo = {\n        sessionId: sid,\n        requestId: evt.requestId,\n        requestKey: this.requestKey(sid, evt.requestId),\n        frameId: evt.frameId ?? undefined,\n        loaderId: evt.loaderId ?? undefined,\n        url: evt.request?.url,\n        timestamp: Date.now(),\n        resourceType: evt.type,\n        documentRequest: evt.type === \"Document\",\n      };\n\n      this.requests.set(info.requestKey, info);\n      if (info.documentRequest && info.frameId) {\n        this.documentRequestsByFrame.set(info.frameId, info.requestKey);\n      }\n\n      this.emitStart(info);\n    };\n\n    const finish = (reqId: string) => {\n      const key = this.requestKey(sid, reqId);\n      const stored = this.requests.get(key);\n      if (stored?.frameId) {\n        this.documentRequestsByFrame.delete(stored.frameId);\n      }\n      const info: NetworkRequestInfo = stored ?? {\n        sessionId: sid,\n        requestId: reqId,\n        requestKey: key,\n        timestamp: Date.now(),\n        documentRequest: false,\n      };\n      this.requests.delete(key);\n      this.emitFinish(info);\n    };\n\n    const fail = (reqId: string) => {\n      const key = this.requestKey(sid, reqId);\n      const stored = this.requests.get(key);\n      if (stored?.frameId) {\n        this.documentRequestsByFrame.delete(stored.frameId);\n      }\n      const info: NetworkRequestInfo = stored ?? {\n        sessionId: sid,\n        requestId: reqId,\n        requestKey: key,\n        timestamp: Date.now(),\n        documentRequest: false,\n      };\n      this.requests.delete(key);\n      this.emitFailure(info);\n    };\n\n    const onFinished = (evt: { requestId: string }) => {\n      if (!evt?.requestId) return;\n      finish(evt.requestId);\n    };\n\n    const onFailed = (evt: Protocol.Network.LoadingFailedEvent) => {\n      if (!evt?.requestId) return;\n      fail(evt.requestId);\n    };\n\n    const onResponse = (evt: Protocol.Network.ResponseReceivedEvent) => {\n      if (!evt?.requestId) return;\n      const url = evt.response?.url ?? \"\";\n      if (url.startsWith(\"data:\")) finish(evt.requestId);\n    };\n\n    const onFrameStopped = (evt: Protocol.Page.FrameStoppedLoadingEvent) => {\n      if (!evt?.frameId) return;\n      const key = this.documentRequestsByFrame.get(evt.frameId);\n      if (!key) return;\n      const stored = this.requests.get(key);\n      if (!stored) {\n        this.documentRequestsByFrame.delete(evt.frameId);\n        return;\n      }\n      this.requests.delete(key);\n      this.documentRequestsByFrame.delete(evt.frameId);\n      this.emitFinish({ ...stored, timestamp: Date.now() });\n    };\n\n    session.on(\"Network.requestWillBeSent\", onRequest);\n    session.on(\"Network.loadingFinished\", onFinished);\n    session.on(\"Network.loadingFailed\", onFailed);\n    session.on(\"Network.requestServedFromCache\", onFinished);\n    session.on(\"Network.responseReceived\", onResponse);\n    session.on(\"Page.frameStoppedLoading\", onFrameStopped);\n\n    void session.send(\"Network.enable\").catch(() => {});\n    void session.send(\"Page.enable\").catch(() => {});\n\n    this.sessions.set(sid, {\n      session,\n      detach: () => {\n        session.off(\"Network.requestWillBeSent\", onRequest);\n        session.off(\"Network.loadingFinished\", onFinished);\n        session.off(\"Network.loadingFailed\", onFailed);\n        session.off(\"Network.requestServedFromCache\", onFinished);\n        session.off(\"Network.responseReceived\", onResponse);\n        session.off(\"Page.frameStoppedLoading\", onFrameStopped);\n      },\n    });\n  }\n\n  /**\n   * Stop tracking a session and discard any inflight bookkeeping owned by it.\n   */\n  public untrackSession(rawSessionId: string | undefined): void {\n    const sid = rawSessionId ?? \"__main__\";\n    const entry = this.sessions.get(sid);\n    if (!entry) return;\n    entry.detach();\n    this.sessions.delete(sid);\n\n    for (const key of [...this.requests.keys()]) {\n      if (key.startsWith(`${sid}:`)) this.requests.delete(key);\n    }\n\n    for (const [frameId, key] of [...this.documentRequestsByFrame.entries()]) {\n      if (key.startsWith(`${sid}:`)) {\n        this.documentRequestsByFrame.delete(frameId);\n      }\n    }\n  }\n\n  /**\n   * Register a passive observer for request lifecycle notifications.\n   * Returns a disposer that removes the observer.\n   */\n  public addObserver(observer: NetworkObserver): () => void {\n    this.observers.add(observer);\n    return () => {\n      this.observers.delete(observer);\n    };\n  }\n\n  /**\n   * Resolve once no (filtered) requests are in flight for the given quiet window.\n   * The waiter automatically unregisters itself on completion or timeout.\n   */\n  public waitForIdle(options: WaitForIdleOptions): WaitForIdleHandle {\n    const startTime = options.startTime ?? Date.now();\n    const idleTimeMs = options.idleTimeMs ?? DEFAULT_IDLE_WAIT;\n    const timeoutMs = options.timeoutMs;\n    const remainingBudgetMs = Number.isFinite(timeoutMs)\n      ? timeoutMs\n      : undefined;\n    const originalBudgetMs = Number.isFinite(options.totalBudgetMs ?? NaN)\n      ? (options.totalBudgetMs as number)\n      : remainingBudgetMs;\n\n    const filter =\n      options.filter ??\n      ((info: NetworkRequestInfo) => {\n        return !IGNORED_RESOURCE_TYPES.has(info.resourceType);\n      });\n\n    const tracked = new Set<string>();\n    let idleTimer: ReturnType<typeof setTimeout> | null = null;\n    let timeoutTimer: ReturnType<typeof setTimeout> | null = null;\n    let settled = false;\n\n    let resolveFn: (() => void) | null = null;\n    let rejectFn: ((error: Error) => void) | null = null;\n\n    const cleanup = (error?: Error) => {\n      if (settled) return;\n      settled = true;\n      if (idleTimer) clearTimeout(idleTimer);\n      if (timeoutTimer) clearTimeout(timeoutTimer);\n      removeObserver();\n      tracked.clear();\n      if (error) {\n        rejectFn?.(error);\n      } else {\n        resolveFn?.();\n      }\n    };\n\n    const maybeIdle = () => {\n      if (settled) return;\n      if (tracked.size === 0) {\n        if (!idleTimer) {\n          idleTimer = setTimeout(() => {\n            cleanup();\n          }, idleTimeMs);\n        }\n      } else if (idleTimer) {\n        clearTimeout(idleTimer);\n        idleTimer = null;\n      }\n    };\n\n    const observer: NetworkObserver = {\n      onRequestStarted: (info) => {\n        if (settled) return;\n        if (info.timestamp < startTime) return;\n        if (!filter(info)) return;\n        tracked.add(info.requestKey);\n        if (idleTimer) {\n          clearTimeout(idleTimer);\n          idleTimer = null;\n        }\n      },\n      onRequestFinished: (info) => {\n        if (settled) return;\n        if (!tracked.delete(info.requestKey)) return;\n        maybeIdle();\n      },\n      onRequestFailed: (info) => {\n        if (settled) return;\n        if (!tracked.delete(info.requestKey)) return;\n        maybeIdle();\n      },\n    };\n\n    const removeObserver = this.addObserver(observer);\n\n    const promise = new Promise<void>((resolve, reject) => {\n      resolveFn = resolve;\n      rejectFn = reject;\n    });\n\n    // Trigger initial idle check so that we still respect the quiet window\n    maybeIdle();\n\n    if (Number.isFinite(timeoutMs)) {\n      timeoutTimer = setTimeout(\n        () => {\n          const elapsed = Date.now() - startTime;\n          const message =\n            originalBudgetMs !== undefined\n              ? `networkidle timed out after ${originalBudgetMs}ms`\n              : `networkidle timed out after ${elapsed}ms`;\n          cleanup(new Error(message));\n        },\n        Math.max(0, timeoutMs),\n      );\n    }\n\n    return {\n      promise,\n      dispose: () => cleanup(new Error(\"waitForIdle disposed\")),\n    };\n  }\n\n  /**\n   * Tear down all session listeners and clear observers/bookkeeping.\n   */\n  public dispose(): void {\n    for (const { detach } of this.sessions.values()) {\n      detach();\n    }\n    this.sessions.clear();\n    this.observers.clear();\n    this.requests.clear();\n    this.documentRequestsByFrame.clear();\n  }\n\n  /** Fan-out helper when a tracked request starts. */\n  private emitStart(info: NetworkRequestInfo): void {\n    for (const obs of this.observers) {\n      obs.onRequestStarted(info);\n    }\n  }\n\n  /** Fan-out helper when a tracked request completes successfully. */\n  private emitFinish(info: NetworkRequestInfo): void {\n    for (const obs of this.observers) {\n      obs.onRequestFinished(info);\n    }\n  }\n\n  /** Fan-out helper when a tracked request fails mid-flight. */\n  private emitFailure(info: NetworkRequestInfo): void {\n    for (const obs of this.observers) {\n      obs.onRequestFailed(info);\n    }\n  }\n\n  /** Compute a stable key for a session (falls back to synthetic root id). */\n  private sessionKey(session: CDPSessionLike): string {\n    return session.id ?? \"__main__\";\n  }\n\n  /** Compose the unique key for tracking a request under a session. */\n  private requestKey(sessionId: string, requestId: string): string {\n    return `${sessionId}:${requestId}`;\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/page.ts",
    "content": "import { Protocol } from \"devtools-protocol\";\nimport { promises as fs } from \"fs\";\nimport { v3Logger } from \"../logger.js\";\nimport { FlowLogger } from \"../flowlogger/FlowLogger.js\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport { CdpConnection } from \"./cdp.js\";\nimport { Frame } from \"./frame.js\";\nimport { FrameLocator } from \"./frameLocator.js\";\nimport { deepLocatorFromPage, resolveLocatorTarget } from \"./deepLocator.js\";\nimport {\n  captureHybridSnapshot,\n  resolveXpathForLocation,\n} from \"./a11y/snapshot/index.js\";\nimport { FrameRegistry } from \"./frameRegistry.js\";\nimport { executionContexts } from \"./executionContextRegistry.js\";\nimport {\n  LoadState,\n  SnapshotResult,\n  PageSnapshotOptions,\n} from \"../types/public/page.js\";\nimport { NetworkManager } from \"./networkManager.js\";\nimport { LifecycleWatcher } from \"./lifecycleWatcher.js\";\nimport { NavigationResponseTracker } from \"./navigationResponseTracker.js\";\nimport { Response, isSerializableResponse } from \"./response.js\";\nimport { ConsoleMessage, ConsoleListener } from \"./consoleMessage.js\";\nimport type { StagehandAPIClient } from \"../api.js\";\nimport {\n  LocalBrowserLaunchOptions,\n  StagehandSetExtraHTTPHeadersError,\n  StagehandSnapshotError,\n} from \"../types/public/index.js\";\nimport type { Locator } from \"./locator.js\";\nimport {\n  StagehandInvalidArgumentError,\n  StagehandEvalError,\n} from \"../types/public/sdkErrors.js\";\nimport { normalizeInitScriptSource } from \"./initScripts.js\";\nimport { buildLocatorInvocation } from \"./locatorInvocation.js\";\nimport type {\n  ScreenshotAnimationsOption,\n  ScreenshotCaretOption,\n  ScreenshotOptions,\n  ScreenshotScaleOption,\n} from \"../types/public/screenshotTypes.js\";\nimport {\n  applyMaskOverlays,\n  applyStyleToFrames,\n  collectFramesForScreenshot,\n  computeScreenshotScale,\n  disableAnimations,\n  hideCaret,\n  normalizeScreenshotClip,\n  runScreenshotCleanups,\n  setTransparentBackground,\n  type ScreenshotCleanup,\n} from \"./screenshotUtils.js\";\nimport { InitScriptSource } from \"../types/private/index.js\";\nimport { withTimeout } from \"../timeoutConfig.js\";\n\n/**\n * Page\n *\n * One instance per **top-level target**. It owns:\n *  - the top-level CDP session (for the page target)\n *  - all adopted OOPIF child sessions (Target.attachToTarget with flatten: true)\n *  - a **FrameRegistry** that is the single source of truth for BOTH:\n *      • frame topology (parent/children, root swaps, last-seen CDP Frame)\n *      • frame → session ownership (which session owns which frameId)\n *\n * Page exposes convenient APIs (goto/reload/url/screenshot/locator),\n * and simple bridges that Context uses to feed Page/Target events in.\n */\n\nconst LIFECYCLE_NAME: Record<LoadState, string> = {\n  load: \"load\",\n  domcontentloaded: \"DOMContentLoaded\",\n  networkidle: \"networkIdle\",\n};\n\nexport class Page {\n  /** Every CDP child session this page owns (top-level + adopted OOPIF sessions). */\n  private readonly sessions = new Map<string, CDPSessionLike>(); // sessionId -> session\n\n  /** Unified truth for frame topology + ownership. */\n  private readonly registry: FrameRegistry;\n\n  /** A convenience wrapper bound to the current main frame id (top-level session). */\n  private mainFrameWrapper: Frame;\n\n  /** Compact ordinal per frameId (used by snapshot encoding). */\n  private frameOrdinals = new Map<string, number>();\n  private nextOrdinal = 0;\n\n  /** cache Frames per frameId so everyone uses the same one */\n  private readonly frameCache = new Map<string, Frame>();\n  private readonly browserIsRemote: boolean;\n\n  /** Stable id for Frames created by this Page (use top-level TargetId). */\n  private readonly pageId: string;\n  /** Cached current URL for synchronous page.url() */\n  private _currentUrl: string = \"about:blank\";\n\n  private navigationCommandSeq = 0;\n  private latestNavigationCommandId = 0;\n\n  private readonly networkManager: NetworkManager;\n  /** Optional API client for routing page operations to the API */\n  private readonly apiClient: StagehandAPIClient | null = null;\n  private readonly consoleListeners = new Set<ConsoleListener>();\n  private readonly consoleHandlers = new Map<\n    string,\n    (evt: Protocol.Runtime.ConsoleAPICalledEvent) => void\n  >();\n  /** Document-start scripts installed across every session this page owns. */\n  private readonly initScripts: string[] = [];\n  private extraHTTPHeaders: Record<string, string>;\n\n  private constructor(\n    private readonly conn: CdpConnection,\n    private readonly mainSession: CDPSessionLike,\n    private readonly _targetId: string,\n    mainFrameId: string,\n    apiClient?: StagehandAPIClient | null,\n    browserIsRemote = false,\n  ) {\n    this.pageId = _targetId;\n    this.apiClient = apiClient ?? null;\n    this.browserIsRemote = browserIsRemote;\n\n    // own the main session\n    if (mainSession.id) this.sessions.set(mainSession.id, mainSession);\n\n    // initialize registry with root/main frame id\n    this.registry = new FrameRegistry(_targetId, mainFrameId);\n\n    // main-frame wrapper is always bound to the **top-level** session\n    this.mainFrameWrapper = new Frame(\n      this.mainSession,\n      mainFrameId,\n      this.pageId,\n      this.browserIsRemote,\n    );\n\n    this.networkManager = new NetworkManager();\n    this.networkManager.trackSession(this.mainSession);\n  }\n\n  // Send a single init script to a specific CDP session.\n  private async installInitScriptOnSession(\n    session: CDPSessionLike,\n    source: string,\n  ): Promise<void> {\n    await session.send(\"Page.addScriptToEvaluateOnNewDocument\", {\n      source: source,\n    });\n  }\n\n  // Replay every previously registered init script onto a newly adopted session.\n  private async applyInitScriptsToSession(\n    session: CDPSessionLike,\n  ): Promise<void> {\n    for (const source of this.initScripts) {\n      await this.installInitScriptOnSession(session, source);\n    }\n  }\n\n  // Register a new init script and fan it out to all active sessions for this page.\n  public async registerInitScript(source: string): Promise<void> {\n    if (this.initScripts.includes(source)) return;\n    this.initScripts.push(source);\n\n    const installs: Array<Promise<void>> = [];\n    installs.push(this.installInitScriptOnSession(this.mainSession, source));\n    for (const session of this.sessions.values()) {\n      if (session === this.mainSession) continue;\n      installs.push(this.installInitScriptOnSession(session, source));\n    }\n    await Promise.all(installs);\n  }\n\n  // Seed an init script without re-installing it on the current sessions.\n  public seedInitScript(source: string): void {\n    if (this.initScripts.includes(source)) return;\n    this.initScripts.push(source);\n  }\n\n  // --- Optional visual cursor overlay management ---\n  private cursorEnabled = false;\n  private async ensureCursorScript(): Promise<void> {\n    const script = `(() => {\n      const ID = '__v3_cursor_overlay__';\n      const state = { el: null, last: null };\n      // Expose API early so move() calls before install are buffered\n      try {\n        if (!window.__v3Cursor || !window.__v3Cursor.__installed) {\n          const api = {\n            __installed: false,\n            move(x, y) {\n              if (state.el) {\n                state.el.style.left = Math.max(0, x) + 'px';\n                state.el.style.top = Math.max(0, y) + 'px';\n              } else {\n                state.last = [x, y];\n              }\n            },\n            show() { if (state.el) state.el.style.display = 'block'; },\n            hide() { if (state.el) state.el.style.display = 'none'; },\n          };\n          window.__v3Cursor = api;\n        }\n      } catch {}\n\n      function install() {\n        try {\n          if (state.el) return; // already installed\n          let el = document.getElementById(ID);\n          if (!el) {\n            const root = document.documentElement || document.body || document.head;\n            if (!root) { setTimeout(install, 50); return; }\n            el = document.createElement('div');\n            el.id = ID;\n            el.style.position = 'fixed';\n            el.style.left = '0px';\n            el.style.top = '0px';\n            el.style.width = '16px';\n            el.style.height = '24px';\n            el.style.zIndex = '2147483647';\n            el.style.pointerEvents = 'none';\n            el.style.userSelect = 'none';\n            el.style.mixBlendMode = 'normal';\n            el.style.contain = 'layout style paint';\n            el.style.willChange = 'transform,left,top';\n            el.innerHTML = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"24\" viewBox=\"0 0 16 24\"><path d=\"M1 0 L1 22 L6 14 L15 14 Z\" fill=\"black\" stroke=\"white\" stroke-width=\"0.7\"/></svg>';\n            root.appendChild(el);\n          }\n          state.el = el;\n          try { window.__v3Cursor.__installed = true; } catch {}\n          if (state.last) {\n            window.__v3Cursor.move(state.last[0], state.last[1]);\n            state.last = null;\n          }\n        } catch {}\n      }\n\n      if (document.readyState === 'complete' || document.readyState === 'interactive') {\n        install();\n      } else {\n        document.addEventListener('DOMContentLoaded', install, { once: true });\n        setTimeout(install, 100);\n      }\n    })();`;\n\n    // Ensure future documents get the cursor at doc-start\n    await this.mainSession\n      .send(\"Page.addScriptToEvaluateOnNewDocument\", { source: script })\n      .catch(() => {});\n    // Inject into current document now\n    await this.mainSession\n      .send(\"Runtime.evaluate\", {\n        expression: script,\n        includeCommandLineAPI: false,\n      })\n      .catch(() => {});\n  }\n\n  public async enableCursorOverlay(): Promise<void> {\n    if (this.cursorEnabled) return;\n    await this.ensureCursorScript();\n    this.cursorEnabled = true;\n  }\n\n  private async updateCursor(x: number, y: number): Promise<void> {\n    if (!this.cursorEnabled) return;\n    try {\n      await this.mainSession.send(\"Runtime.evaluate\", {\n        expression: `typeof window.__v3Cursor!==\"undefined\"&&window.__v3Cursor.move(${Math.round(x)}, ${Math.round(y)})`,\n      });\n    } catch {\n      //\n    }\n  }\n\n  public async addInitScript<Arg>(\n    script: InitScriptSource<Arg>,\n    arg?: Arg,\n  ): Promise<void> {\n    const source = await normalizeInitScriptSource(\n      script,\n      arg,\n      \"page.addInitScript\",\n    );\n    await this.registerInitScript(source);\n  }\n\n  /**\n   * Factory: create Page and seed registry with the shallow tree from Page.getFrameTree.\n   * Assumes Page domain is already enabled on the session passed in.\n   */\n  static async create(\n    conn: CdpConnection,\n    session: CDPSessionLike,\n    targetId: string,\n    apiClient?: StagehandAPIClient | null,\n    localBrowserLaunchOptions?: LocalBrowserLaunchOptions | null,\n    browserIsRemote = false,\n  ): Promise<Page> {\n    // Context already issues Page.enable + lifecycle enable before resume.\n    // Re-issue here only as best-effort and do not block page registration on\n    // their acknowledgements; some remote CDP backends can delay these replies\n    // long after the target is otherwise ready.\n    void session.send(\"Page.enable\").catch(() => {});\n    void session\n      .send(\"Page.setLifecycleEventsEnabled\", { enabled: true })\n      .catch(() => {});\n    const { frameTree } = await session.send<{\n      frameTree: Protocol.Page.FrameTree;\n    }>(\"Page.getFrameTree\");\n    const mainFrameId = frameTree.frame.id;\n\n    const page = new Page(\n      conn,\n      session,\n      targetId,\n      mainFrameId,\n      apiClient,\n      browserIsRemote,\n    );\n    // Seed current URL from initial frame tree\n    try {\n      page._currentUrl = String(frameTree?.frame?.url ?? page._currentUrl);\n      if (localBrowserLaunchOptions?.viewport) {\n        await page.setViewportSize(\n          localBrowserLaunchOptions.viewport.width,\n          localBrowserLaunchOptions.viewport.height,\n          {\n            deviceScaleFactor: localBrowserLaunchOptions.deviceScaleFactor ?? 1,\n          },\n        );\n      }\n    } catch {\n      // ignore\n    }\n\n    // Seed topology + ownership for nodes known at creation time.\n    page.registry.seedFromFrameTree(session.id ?? \"root\", frameTree);\n\n    return page;\n  }\n\n  // ---------------- Event-driven updates from Context ----------------\n\n  /**\n   * Parent/child session emitted a `frameAttached`.\n   * Topology update + ownership stamped to **emitting session**.\n   */\n  public onFrameAttached(\n    frameId: string,\n    parentId: string | null,\n    session: CDPSessionLike,\n  ): void {\n    this.ensureOrdinal(frameId);\n    this.registry.onFrameAttached(frameId, parentId, session.id ?? \"root\");\n    // Cache is keyed by frameId → invalidate to ensure future frameForId resolves with latest owner\n    this.frameCache.delete(frameId);\n  }\n\n  /**\n   * Parent/child session emitted a `frameDetached`.\n   */\n  public onFrameDetached(\n    frameId: string,\n    reason: \"remove\" | \"swap\" | string = \"remove\",\n  ): void {\n    this.registry.onFrameDetached(frameId, reason);\n    this.frameCache.delete(frameId);\n  }\n\n  /**\n   * Parent/child session emitted a `frameNavigated`.\n   * Topology + ownership update. Handles root swaps.\n   */\n  public onFrameNavigated(\n    frame: Protocol.Page.Frame,\n    session: CDPSessionLike,\n  ): void {\n    const prevRoot = this.mainFrameId();\n    this.registry.onFrameNavigated(frame, session.id ?? \"root\");\n\n    // If the root changed, keep the convenience wrapper in sync\n    const newRoot = this.mainFrameId();\n    if (newRoot !== prevRoot) {\n      const oldOrd = this.frameOrdinals.get(prevRoot) ?? 0;\n      this.frameOrdinals.set(newRoot, oldOrd);\n      this.mainFrameWrapper = new Frame(\n        this.mainSession,\n        newRoot,\n        this.pageId,\n        this.browserIsRemote,\n      );\n    }\n\n    // Update cached URL if this navigation pertains to the current main frame\n    if (frame.id === this.mainFrameId()) {\n      try {\n        // Prefer frame.url; fallback keeps previous value\n        this._currentUrl = String(\n          (frame as { url?: string })?.url ?? this._currentUrl,\n        );\n      } catch {\n        // ignore\n      }\n    }\n\n    // Invalidate the cached Frame for this id (session may have changed)\n    this.frameCache.delete(frame.id);\n  }\n\n  public onNavigatedWithinDocument(\n    frameId: string,\n    url: string,\n    session: CDPSessionLike,\n  ): void {\n    const normalized = String(url ?? \"\").trim();\n    if (!normalized) return;\n\n    this.registry.onNavigatedWithinDocument(\n      frameId,\n      normalized,\n      session.id ?? \"root\",\n    );\n\n    if (frameId === this.mainFrameId()) {\n      this._currentUrl = normalized;\n    }\n  }\n\n  /**\n   * An OOPIF child session whose **main** frame id equals the parent iframe’s frameId\n   * has been attached; adopt the session into this Page and seed ownership for its subtree.\n   */\n  public adoptOopifSession(\n    childSession: CDPSessionLike,\n    childMainFrameId: string,\n  ): void {\n    if (childSession.id) this.sessions.set(childSession.id, childSession);\n\n    this.networkManager.trackSession(childSession);\n    if (this.extraHTTPHeaders)\n      void this.applyExtraHTTPHeadersToSession(\n        childSession,\n        this.extraHTTPHeaders,\n      ).catch(() => {});\n\n    void this.applyInitScriptsToSession(childSession).catch(() => {});\n\n    if (this.consoleListeners.size > 0) {\n      this.installConsoleTap(childSession);\n    }\n\n    // session will start emitting its own page events; mark ownership seed now\n    this.registry.adoptChildSession(\n      childSession.id ?? \"child\",\n      childMainFrameId,\n    );\n    this.frameCache.delete(childMainFrameId);\n\n    // Bridge events from the child session to keep registry in sync\n    childSession.on<Protocol.Page.FrameNavigatedEvent>(\n      \"Page.frameNavigated\",\n      (evt) => {\n        this.onFrameNavigated(evt.frame, childSession);\n      },\n    );\n    childSession.on<Protocol.Page.FrameAttachedEvent>(\n      \"Page.frameAttached\",\n      (evt) => {\n        this.onFrameAttached(\n          evt.frameId,\n          evt.parentFrameId ?? null,\n          childSession,\n        );\n      },\n    );\n    childSession.on<Protocol.Page.FrameDetachedEvent>(\n      \"Page.frameDetached\",\n      (evt) => {\n        this.onFrameDetached(evt.frameId, evt.reason ?? \"remove\");\n      },\n    );\n\n    // One-shot seed the child's subtree ownership from its current tree\n    void (async () => {\n      try {\n        await childSession.send(\"Page.enable\").catch(() => {});\n        let { frameTree } =\n          await childSession.send<Protocol.Page.GetFrameTreeResponse>(\n            \"Page.getFrameTree\",\n          );\n\n        // Normalize: ensure the child’s reported root id matches our known main id\n        if (frameTree.frame.id !== childMainFrameId) {\n          frameTree = {\n            ...frameTree,\n            frame: { ...frameTree.frame, id: childMainFrameId },\n          };\n        }\n\n        this.registry.seedFromFrameTree(childSession.id ?? \"child\", frameTree);\n      } catch {\n        // If snapshot races, live events will still converge the registry.\n      }\n    })();\n  }\n\n  /** Detach an adopted child session and prune its subtree */\n  public detachOopifSession(sessionId: string): void {\n    // Find which frames were owned by this session and prune by tree starting from each root.\n    for (const fid of this.registry.framesForSession(sessionId)) {\n      this.registry.onFrameDetached(fid, \"remove\");\n      this.frameCache.delete(fid);\n    }\n    this.teardownConsoleTap(sessionId);\n    this.sessions.delete(sessionId);\n    this.networkManager.untrackSession(sessionId);\n  }\n\n  // ---------------- Ownership helpers / lookups ----------------\n\n  /** Return the owning CDP session for a frameId (falls back to main session) */\n  public getSessionForFrame(frameId: string): CDPSessionLike {\n    const sid = this.registry.getOwnerSessionId(frameId);\n    if (!sid) return this.mainSession;\n    return this.sessions.get(sid) ?? this.mainSession;\n  }\n\n  /** Always returns a Frame bound to the owning session */\n  public frameForId(frameId: string): Frame {\n    const hit = this.frameCache.get(frameId);\n    if (hit) return hit;\n\n    const sess = this.getSessionForFrame(frameId);\n    const f = new Frame(sess, frameId, this.pageId, this.browserIsRemote);\n    this.frameCache.set(frameId, f);\n    return f;\n  }\n\n  /** Expose a session by id (used by snapshot to resolve session id -> session) */\n  public getSessionById(id: string): CDPSessionLike | undefined {\n    return this.sessions.get(id);\n  }\n\n  public registerSessionForNetwork(session: CDPSessionLike): void {\n    this.networkManager.trackSession(session);\n  }\n\n  public unregisterSessionForNetwork(sessionId: string | undefined): void {\n    this.networkManager.untrackSession(sessionId);\n  }\n\n  public on(event: \"console\", listener: ConsoleListener): Page {\n    if (event !== \"console\") {\n      throw new StagehandInvalidArgumentError(`Unsupported event: ${event}`);\n    }\n\n    const firstListener = this.consoleListeners.size === 0;\n    this.consoleListeners.add(listener);\n\n    if (firstListener) {\n      this.ensureConsoleTaps();\n    }\n\n    return this;\n  }\n\n  public once(event: \"console\", listener: ConsoleListener): Page {\n    if (event !== \"console\") {\n      throw new StagehandInvalidArgumentError(`Unsupported event: ${event}`);\n    }\n\n    const wrapper: ConsoleListener = (message) => {\n      this.off(\"console\", wrapper);\n      listener(message);\n    };\n\n    return this.on(\"console\", wrapper);\n  }\n\n  public off(event: \"console\", listener: ConsoleListener): Page {\n    if (event !== \"console\") {\n      throw new StagehandInvalidArgumentError(`Unsupported event: ${event}`);\n    }\n\n    this.consoleListeners.delete(listener);\n\n    if (this.consoleListeners.size === 0) {\n      this.removeAllConsoleTaps();\n    }\n\n    return this;\n  }\n\n  // ---------------- MAIN APIs ----------------\n\n  public targetId(): string {\n    return this._targetId;\n  }\n\n  /**\n   * Send a CDP command through the main session.\n   * Allows external consumers to execute arbitrary Chrome DevTools Protocol commands.\n   *\n   * @param method - The CDP method name (e.g., \"Page.enable\", \"Runtime.evaluate\")\n   * @param params - Optional parameters for the CDP command\n   * @returns Promise resolving to the typed CDP response\n   *\n   * @example\n   * // Enable the Runtime domain\n   * await page.sendCDP(\"Runtime.enable\");\n   *\n   * @example\n   * // Evaluate JavaScript with typed response\n   * const result = await page.sendCDP<Protocol.Runtime.EvaluateResponse>(\n   *   \"Runtime.evaluate\",\n   *   { expression: \"1 + 1\" }\n   * );\n   */\n  public sendCDP<T = unknown>(method: string, params?: object): Promise<T> {\n    return this.mainSession.send<T>(method, params);\n  }\n\n  /** Seed the cached URL before navigation events converge. */\n  public seedCurrentUrl(url: string | undefined | null): void {\n    if (!url) return;\n    try {\n      const normalized = String(url).trim();\n      if (!normalized) return;\n      this._currentUrl = normalized;\n    } catch {\n      // ignore invalid url seeds\n    }\n  }\n\n  public mainFrameId(): string {\n    return this.registry.mainFrameId();\n  }\n\n  public mainFrame(): Frame {\n    return this.mainFrameWrapper;\n  }\n\n  /**\n   * Close this top-level page (tab). Best-effort via Target.closeTarget.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageClose\" })\n  public async close(): Promise<void> {\n    try {\n      await this.conn.send(\"Target.closeTarget\", { targetId: this._targetId });\n    } catch {\n      // ignore\n    }\n    const deadline = Date.now() + 2000;\n    while (Date.now() < deadline) {\n      try {\n        const targets = await this.conn.getTargets();\n        if (!targets.some((t) => t.targetId === this._targetId)) {\n          this.networkManager.dispose();\n          return;\n        }\n      } catch {\n        // ignore and retry\n      }\n      await new Promise((r) => setTimeout(r, 25));\n    }\n    this.networkManager.dispose();\n    this.removeAllConsoleTaps();\n    this.consoleListeners.clear();\n  }\n\n  public getFullFrameTree(): Protocol.Page.FrameTree {\n    return this.asProtocolFrameTree(this.mainFrameId());\n  }\n\n  public asProtocolFrameTree(rootMainFrameId: string): Protocol.Page.FrameTree {\n    return this.registry.asProtocolFrameTree(rootMainFrameId);\n  }\n\n  private async applyExtraHTTPHeadersToSession(\n    session: CDPSessionLike,\n    headers: Record<string, string>,\n  ): Promise<void> {\n    await session.send(\"Network.enable\");\n    await session.send(\"Network.setExtraHTTPHeaders\", {\n      headers: headers,\n    });\n  }\n\n  private ensureOrdinal(frameId: string): number {\n    const hit = this.frameOrdinals.get(frameId);\n    if (hit !== undefined) return hit;\n    const ord = this.nextOrdinal++;\n    this.frameOrdinals.set(frameId, ord);\n    return ord;\n  }\n\n  /** Public getter for snapshot code / handlers. */\n  public getOrdinal(frameId: string): number {\n    return this.ensureOrdinal(frameId);\n  }\n\n  public listAllFrameIds(): string[] {\n    return this.registry.listAllFrames();\n  }\n\n  private ensureConsoleTaps(): void {\n    if (this.consoleListeners.size === 0) return;\n\n    this.installConsoleTap(this.mainSession);\n    for (const session of this.sessions.values()) {\n      this.installConsoleTap(session);\n    }\n  }\n\n  private installConsoleTap(session: CDPSessionLike): void {\n    const key = this.sessionKey(session);\n    if (this.consoleHandlers.has(key)) return;\n\n    void session.send(\"Runtime.enable\").catch(() => {});\n\n    const handler = (evt: Protocol.Runtime.ConsoleAPICalledEvent) => {\n      this.emitConsole(evt);\n    };\n\n    session.on<Protocol.Runtime.ConsoleAPICalledEvent>(\n      \"Runtime.consoleAPICalled\",\n      handler,\n    );\n\n    this.consoleHandlers.set(key, handler);\n  }\n\n  private sessionKey(session: CDPSessionLike): string {\n    return session.id ?? \"__root__\";\n  }\n\n  private resolveSessionByKey(key: string): CDPSessionLike | undefined {\n    if (this.mainSession.id) {\n      if (this.mainSession.id === key) return this.mainSession;\n    } else if (key === \"__root__\") {\n      return this.mainSession;\n    }\n\n    return this.sessions.get(key);\n  }\n\n  private teardownConsoleTap(key: string): void {\n    const handler = this.consoleHandlers.get(key);\n    if (!handler) return;\n\n    const session = this.resolveSessionByKey(key);\n    session?.off(\"Runtime.consoleAPICalled\", handler);\n    this.consoleHandlers.delete(key);\n  }\n\n  private removeAllConsoleTaps(): void {\n    for (const key of [...this.consoleHandlers.keys()]) {\n      this.teardownConsoleTap(key);\n    }\n  }\n\n  private emitConsole(evt: Protocol.Runtime.ConsoleAPICalledEvent): void {\n    if (this.consoleListeners.size === 0) return;\n\n    const message = new ConsoleMessage(evt, this);\n    const listeners = [...this.consoleListeners];\n\n    for (const listener of listeners) {\n      try {\n        listener(message);\n      } catch (error) {\n        v3Logger({\n          category: \"page\",\n          message: \"Console listener threw\",\n          level: 2,\n          auxiliary: {\n            error: { value: String(error), type: \"string\" },\n            type: { value: evt.type, type: \"string\" },\n          },\n        });\n      }\n    }\n  }\n\n  // -------- Convenience APIs delegated to the current main frame --------\n\n  /**\n   * Navigate the page; optionally wait for a lifecycle state.\n   * Waits on the **current** main frame and follows root swaps during navigation.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageGoto\" })\n  async goto(\n    url: string,\n    options?: { waitUntil?: LoadState; timeoutMs?: number },\n  ): Promise<Response | null> {\n    const waitUntil: LoadState = options?.waitUntil ?? \"domcontentloaded\";\n    const timeout = options?.timeoutMs ?? 15000;\n\n    const navigationCommandId = this.beginNavigationCommand();\n    const tracker = new NavigationResponseTracker({\n      page: this,\n      session: this.mainSession,\n      navigationCommandId,\n    });\n\n    const watcher = new LifecycleWatcher({\n      page: this,\n      mainSession: this.mainSession,\n      networkManager: this.networkManager,\n      waitUntil,\n      timeoutMs: timeout,\n      navigationCommandId,\n    });\n\n    try {\n      // Route to API if available\n      if (this.apiClient) {\n        const result = await this.apiClient.goto(\n          url,\n          { waitUntil: options?.waitUntil },\n          this.mainFrameId(),\n        );\n        this._currentUrl = url;\n\n        if (isSerializableResponse(result)) {\n          return Response.fromSerializable(result, {\n            page: this,\n            session: this.mainSession,\n          });\n        }\n        return result;\n      }\n      const response =\n        await this.mainSession.send<Protocol.Page.NavigateResponse>(\n          \"Page.navigate\",\n          { url },\n        );\n      this._currentUrl = url;\n      if (response?.loaderId) {\n        watcher.setExpectedLoaderId(response.loaderId);\n        tracker.setExpectedLoaderId(response.loaderId);\n      }\n      await watcher.wait();\n      return await tracker.navigationCompleted();\n    } finally {\n      watcher.dispose();\n      tracker.dispose();\n    }\n  }\n\n  /**\n   * Reload the page; optionally wait for a lifecycle state.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageReload\" })\n  async reload(options?: {\n    waitUntil?: LoadState;\n    timeoutMs?: number;\n    ignoreCache?: boolean;\n  }): Promise<Response | null> {\n    const waitUntil = options?.waitUntil;\n    const timeout = options?.timeoutMs ?? 15000;\n\n    const navigationCommandId = this.beginNavigationCommand();\n\n    const tracker = new NavigationResponseTracker({\n      page: this,\n      session: this.mainSession,\n      navigationCommandId,\n    });\n    tracker.expectNavigationWithoutKnownLoader();\n\n    const watcher = waitUntil\n      ? new LifecycleWatcher({\n          page: this,\n          mainSession: this.mainSession,\n          networkManager: this.networkManager,\n          waitUntil,\n          timeoutMs: timeout,\n          navigationCommandId,\n        })\n      : null;\n\n    try {\n      await this.mainSession.send(\"Page.reload\", {\n        ignoreCache: options?.ignoreCache ?? false,\n      });\n\n      if (watcher) {\n        await watcher.wait();\n      }\n      return await tracker.navigationCompleted();\n    } finally {\n      watcher?.dispose();\n      tracker.dispose();\n    }\n  }\n\n  /**\n   * Navigate back in history if possible; optionally wait for a lifecycle state.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageGoBack\" })\n  async goBack(options?: {\n    waitUntil?: LoadState;\n    timeoutMs?: number;\n  }): Promise<Response | null> {\n    const { entries, currentIndex } =\n      await this.mainSession.send<Protocol.Page.GetNavigationHistoryResponse>(\n        \"Page.getNavigationHistory\",\n      );\n    const prev = entries[currentIndex - 1];\n    if (!prev) return null; // nothing to do\n    const waitUntil = options?.waitUntil;\n    const timeout = options?.timeoutMs ?? 15000;\n\n    const navigationCommandId = this.beginNavigationCommand();\n\n    const tracker = new NavigationResponseTracker({\n      page: this,\n      session: this.mainSession,\n      navigationCommandId,\n    });\n    tracker.expectNavigationWithoutKnownLoader();\n\n    const watcher = waitUntil\n      ? new LifecycleWatcher({\n          page: this,\n          mainSession: this.mainSession,\n          networkManager: this.networkManager,\n          waitUntil,\n          timeoutMs: timeout,\n          navigationCommandId,\n        })\n      : null;\n\n    try {\n      await this.mainSession.send(\"Page.navigateToHistoryEntry\", {\n        entryId: prev.id,\n      });\n      this._currentUrl = prev.url ?? this._currentUrl;\n\n      if (watcher) {\n        await watcher.wait();\n      }\n      return await tracker.navigationCompleted();\n    } finally {\n      watcher?.dispose();\n      tracker.dispose();\n    }\n  }\n\n  /**\n   * Navigate forward in history if possible; optionally wait for a lifecycle state.\n   */\n  @FlowLogger.wrapWithLogging({\n    eventType: \"PageGoForward\",\n  })\n  async goForward(options?: {\n    waitUntil?: LoadState;\n    timeoutMs?: number;\n  }): Promise<Response | null> {\n    const { entries, currentIndex } =\n      await this.mainSession.send<Protocol.Page.GetNavigationHistoryResponse>(\n        \"Page.getNavigationHistory\",\n      );\n    const next = entries[currentIndex + 1];\n    if (!next) return null; // nothing to do\n    const waitUntil = options?.waitUntil;\n    const timeout = options?.timeoutMs ?? 15000;\n\n    const navigationCommandId = this.beginNavigationCommand();\n\n    const tracker = new NavigationResponseTracker({\n      page: this,\n      session: this.mainSession,\n      navigationCommandId,\n    });\n    tracker.expectNavigationWithoutKnownLoader();\n\n    const watcher = waitUntil\n      ? new LifecycleWatcher({\n          page: this,\n          mainSession: this.mainSession,\n          networkManager: this.networkManager,\n          waitUntil,\n          timeoutMs: timeout,\n          navigationCommandId,\n        })\n      : null;\n\n    try {\n      await this.mainSession.send(\"Page.navigateToHistoryEntry\", {\n        entryId: next.id,\n      });\n      this._currentUrl = next.url ?? this._currentUrl;\n\n      if (watcher) {\n        await watcher.wait();\n      }\n      return await tracker.navigationCompleted();\n    } finally {\n      watcher?.dispose();\n      tracker.dispose();\n    }\n  }\n\n  /**\n   * Return the current page URL (synchronous, cached from navigation events).\n   */\n  url(): string {\n    return this._currentUrl;\n  }\n\n  private beginNavigationCommand(): number {\n    const id = ++this.navigationCommandSeq;\n    this.latestNavigationCommandId = id;\n    return id;\n  }\n\n  public isCurrentNavigationCommand(id: number): boolean {\n    return this.latestNavigationCommandId === id;\n  }\n\n  /**\n   * Return the current page title.\n   * Prefers reading from the active document via Runtime.evaluate to reflect dynamic changes.\n   * Falls back to navigation history title if evaluation is unavailable.\n   */\n  async title(): Promise<string> {\n    try {\n      await this.mainSession.send(\"Runtime.enable\").catch(() => {});\n      const ctxId = await this.mainWorldExecutionContextId();\n      const { result } =\n        await this.mainSession.send<Protocol.Runtime.EvaluateResponse>(\n          \"Runtime.evaluate\",\n          {\n            expression: \"document.title\",\n            contextId: ctxId,\n            returnByValue: true,\n          },\n        );\n      return String(result?.value ?? \"\");\n    } catch {\n      // Fallback: use navigation history entry title\n      try {\n        const { entries, currentIndex } =\n          await this.mainSession.send<Protocol.Page.GetNavigationHistoryResponse>(\n            \"Page.getNavigationHistory\",\n          );\n        return entries[currentIndex]?.title ?? \"\";\n      } catch {\n        return \"\";\n      }\n    }\n  }\n\n  /**\n   * Capture a screenshot with Playwright-style options.\n   *\n   * @param options Optional screenshot configuration.\n   * @param options.animations Control CSS/Web animations during capture. Use\n   * \"disabled\" to fast-forward finite animations and pause infinite ones.\n   * @param options.caret Either hide the text caret (default) or leave it\n   * visible via \"initial\".\n   * @param options.clip Restrict capture to a specific rectangle (in CSS\n   * pixels). Cannot be combined with `fullPage`.\n   * @param options.fullPage Capture the full scrollable page instead of the\n   * current viewport.\n   * @param options.mask Array of locators that should be covered with an\n   * overlay while the screenshot is taken.\n   * @param options.maskColor CSS color used for the mask overlay (default\n   * `#FF00FF`).\n   * @param options.omitBackground Make the default page background transparent\n   * (PNG only).\n   * @param options.path File path to write the screenshot to. The file extension\n   * determines the image type when `type` is not explicitly provided.\n   * @param options.quality JPEG quality (0–100). Only applies when\n   * `type === \"jpeg\"`.\n   * @param options.scale Render scale: use \"css\" for one pixel per CSS pixel,\n   * otherwise the default \"device\" leverages the current device pixel ratio.\n   * @param options.style Additional CSS text injected into every frame before\n   * capture (removed afterwards).\n   * @param options.timeout Maximum capture duration in milliseconds before a\n   * timeout error is thrown.\n   * @param options.type Image format (`\"png\"` by default).\n   */\n  @FlowLogger.wrapWithLogging({\n    eventType: \"PageScreenshot\",\n  })\n  async screenshot(options?: ScreenshotOptions): Promise<Buffer> {\n    const opts = options ?? {};\n    const type = opts.type ?? \"png\";\n\n    if (type !== \"png\" && type !== \"jpeg\") {\n      throw new StagehandInvalidArgumentError(\n        `screenshot: unsupported image type \"${type}\"`,\n      );\n    }\n\n    if (opts.fullPage && opts.clip) {\n      throw new StagehandInvalidArgumentError(\n        \"screenshot: clip and fullPage cannot be used together\",\n      );\n    }\n\n    if (type === \"png\" && typeof opts.quality === \"number\") {\n      throw new StagehandInvalidArgumentError(\n        'screenshot: quality option is only valid for type=\"jpeg\"',\n      );\n    }\n\n    const caretMode: ScreenshotCaretOption = opts.caret ?? \"hide\";\n    const animationsMode: ScreenshotAnimationsOption =\n      opts.animations ?? \"allow\";\n    const scaleMode: ScreenshotScaleOption = opts.scale ?? \"device\";\n    const frames = collectFramesForScreenshot(this);\n    const clip = opts.clip ? normalizeScreenshotClip(opts.clip) : undefined;\n    const captureScale = await computeScreenshotScale(this, scaleMode);\n    const maskLocators = (opts.mask ?? []).filter(\n      (locator): locator is Locator => Boolean(locator),\n    );\n\n    const cleanupTasks: ScreenshotCleanup[] = [];\n\n    const exec = async (): Promise<Buffer> => {\n      try {\n        if (opts.omitBackground) {\n          cleanupTasks.push(await setTransparentBackground(this.mainSession));\n        }\n\n        if (animationsMode === \"disabled\") {\n          cleanupTasks.push(await disableAnimations(frames));\n        }\n\n        if (caretMode === \"hide\") {\n          cleanupTasks.push(await hideCaret(frames));\n        }\n\n        if (opts.style && opts.style.trim()) {\n          cleanupTasks.push(\n            await applyStyleToFrames(frames, opts.style, \"custom\"),\n          );\n        }\n\n        if (maskLocators.length > 0) {\n          cleanupTasks.push(\n            await applyMaskOverlays(maskLocators, opts.maskColor ?? \"#FF00FF\"),\n          );\n        }\n\n        const buffer = await this.mainFrameWrapper.screenshot({\n          fullPage: opts.fullPage,\n          clip,\n          type,\n          quality: type === \"jpeg\" ? opts.quality : undefined,\n          scale: captureScale,\n        });\n\n        if (opts.path) {\n          await fs.writeFile(opts.path, buffer);\n        }\n\n        return buffer;\n      } finally {\n        await runScreenshotCleanups(cleanupTasks);\n      }\n    };\n\n    return await withTimeout(exec(), opts.timeout, \"screenshot\");\n  }\n\n  /**\n   * specifies additional HTTP headers to be included in every request sent by\n   * the root CDP session of the page, and all of its child CDP sessions.\n   *\n   * @param headers - the headers to be set.\n   * @throws {StagehandSetExtraHTTPHeadersError}\n   * Thrown when one or more CDP sessions fail to enable the Network domain or fail\n   * to apply the headers (i.e. `Network.enable` and/or `Network.setExtraHTTPHeaders` rejects).\n   * @return void\n   */\n  async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {\n    const headersToSet = { ...headers };\n    this.extraHTTPHeaders = headersToSet;\n\n    // get the session(s) for this page:\n    const sessions: CDPSessionLike[] = [this.mainSession];\n    for (const session of this.sessions.values()) {\n      if (session === this.mainSession) continue;\n      sessions.push(session);\n    }\n\n    const results = await Promise.allSettled(\n      sessions.map(async (session) => {\n        await this.applyExtraHTTPHeadersToSession(session, headersToSet);\n      }),\n    );\n\n    // get list of objects containing results & corresponding session IDs\n    const pairs = results.map((result, index) => ({\n      result,\n      id: sessions[index].id,\n    }));\n\n    const filtered = pairs.filter(\n      (pair): pair is { result: PromiseRejectedResult; id: string | null } =>\n        pair.result.status === \"rejected\",\n    );\n\n    const errors = filtered.map((pair) => {\n      const reason = pair.result.reason;\n      const sessId = pair.id ?? \"root\";\n      const message = reason?.message ?? String(reason);\n      return `session=${sessId} error=${message}`;\n    });\n\n    if (errors.length > 0) {\n      throw new StagehandSetExtraHTTPHeadersError(errors);\n    }\n  }\n\n  /**\n   * Create a locator bound to the current main frame.\n   */\n  locator(selector: string): ReturnType<Frame[\"locator\"]> {\n    return this.mainFrameWrapper.locator(selector);\n  }\n\n  /**\n   * Deep locator that supports cross-iframe traversal.\n   * - Recognizes '>>' hop notation to enter iframe contexts.\n   * - Supports deep XPath that includes iframe steps (e.g., '/html/body/iframe[2]//div').\n   * Returns a Locator scoped to the appropriate frame.\n   */\n  deepLocator(selector: string) {\n    return deepLocatorFromPage(this, this.mainFrameWrapper, selector);\n  }\n\n  /**\n   * Frame locator similar to Playwright: targets iframe elements and scopes\n   * subsequent locators to that frame. Supports chaining.\n   */\n  frameLocator(selector: string): FrameLocator {\n    return new FrameLocator(this, selector);\n  }\n\n  /**\n   * List all frames belonging to this page as Frame objects bound to their owning sessions.\n   * The list is ordered by a stable ordinal assigned during the page lifetime.\n   */\n  frames(): Frame[] {\n    const ids = this.listAllFrameIds();\n    const withOrd = ids.map((id) => ({ id, ord: this.getOrdinal(id) }));\n    withOrd.sort((a, b) => a.ord - b.ord);\n    return withOrd.map(({ id }) => this.frameForId(id));\n  }\n\n  /**\n   * Wait until the page reaches a lifecycle state on the current main frame.\n   * Mirrors Playwright's API signatures.\n   */\n  @FlowLogger.wrapWithLogging({\n    eventType: \"PageWaitForLoadState\",\n  })\n  async waitForLoadState(state: LoadState, timeoutMs?: number): Promise<void> {\n    await this.waitForMainLoadState(state, timeoutMs ?? 15000);\n  }\n\n  /**\n   * Wait for a specified amount of time.\n   *\n   * @param ms The number of milliseconds to wait.\n   */\n  async waitForTimeout(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  /**\n   * Wait for an element matching the selector to appear in the DOM.\n   * Uses MutationObserver for efficiency\n   * Pierces shadow DOM by default.\n   * Supports iframe hop notation with '>>' (e.g., 'iframe#checkout >> .submit-btn').\n   *\n   * @param selector CSS selector to wait for (supports '>>' for iframe hops)\n   * @param options\n   * @param options.state Element state to wait for: 'attached' | 'detached' | 'visible' | 'hidden' (default: 'visible')\n   * @param options.timeout Maximum time to wait in milliseconds (default: 30000)\n   * @param options.pierceShadow Whether to search inside shadow DOM (default: true)\n   * @returns True when the condition is met\n   * @throws Error if timeout is reached before the condition is met\n   */\n  @FlowLogger.wrapWithLogging({\n    eventType: \"PageWaitForSelector\",\n  })\n  async waitForSelector(\n    selector: string,\n    options?: {\n      state?: \"attached\" | \"detached\" | \"visible\" | \"hidden\";\n      timeout?: number;\n      pierceShadow?: boolean;\n    },\n  ): Promise<boolean> {\n    const timeout = options?.timeout ?? 30000;\n    const state = options?.state ?? \"visible\";\n    const pierceShadow = options?.pierceShadow ?? true;\n    const startTime = Date.now();\n    const root = this.mainFrameWrapper;\n    const { frame: targetFrame, selector: finalSelector } =\n      await resolveLocatorTarget(this, root, selector);\n    const elapsed = Date.now() - startTime;\n    const remainingTimeout = Math.max(0, timeout - elapsed);\n\n    const expression = buildLocatorInvocation(\"waitForSelector\", [\n      JSON.stringify(finalSelector),\n      JSON.stringify(state),\n      String(remainingTimeout),\n      String(pierceShadow),\n    ]);\n    return targetFrame.evaluate(expression);\n  }\n\n  /**\n   * Evaluate a function or expression in the current main frame's main world.\n   * - If a string is provided, it is treated as a JS expression.\n   * - If a function is provided, it is stringified and invoked with the optional argument.\n   * - The return value should be JSON-serializable. Non-serializable objects will\n   *   best-effort serialize via JSON.stringify inside the page context.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageEvaluate\" })\n  async evaluate<R = unknown, Arg = unknown>(\n    pageFunctionOrExpression: string | ((arg: Arg) => R | Promise<R>),\n    arg?: Arg,\n  ): Promise<R> {\n    await this.mainSession.send(\"Runtime.enable\").catch(() => {});\n    const ctxId = await this.mainWorldExecutionContextId();\n\n    const isString = typeof pageFunctionOrExpression === \"string\";\n    let expression: string;\n\n    if (isString) {\n      expression = String(pageFunctionOrExpression);\n    } else {\n      const fnSrc = pageFunctionOrExpression.toString();\n      const argJson = JSON.stringify(arg);\n      expression = `(() => {\n          const __fn = ${fnSrc};\n          const __arg = ${argJson};\n          try {\n            const __res = __fn(__arg);\n            return Promise.resolve(__res).then(v => {\n              try { return JSON.parse(JSON.stringify(v)); } catch { return v; }\n            });\n          } catch (e) { throw e; }\n        })()`;\n    }\n\n    const { result, exceptionDetails } =\n      await this.mainSession.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression,\n          contextId: ctxId,\n          returnByValue: true,\n          awaitPromise: true,\n        },\n      );\n\n    if (exceptionDetails) {\n      const msg =\n        exceptionDetails.text ||\n        exceptionDetails.exception?.description ||\n        \"Evaluation failed\";\n      throw new StagehandEvalError(msg);\n    }\n\n    return result?.value as R;\n  }\n\n  /**\n   * Force the page viewport to an exact CSS size and device scale factor.\n   * Ensures screenshots match width x height pixels when deviceScaleFactor = 1.\n   */\n  // @FlowLogger.wrapWithLogging({ eventType: \"PageSetViewportSize\" })  // disabled because it's pretty noisy, can always re-enable if needed for debugging\n  async setViewportSize(\n    width: number,\n    height: number,\n    options?: { deviceScaleFactor?: number },\n  ): Promise<void> {\n    const dsf = Math.max(0.01, options?.deviceScaleFactor ?? 1);\n    await this.mainSession\n      .send(\"Emulation.setDeviceMetricsOverride\", {\n        width,\n        height,\n        deviceScaleFactor: dsf,\n        mobile: false,\n        screenWidth: width,\n        screenHeight: height,\n        positionX: 0,\n        positionY: 0,\n        scale: 1,\n      } as Protocol.Emulation.SetDeviceMetricsOverrideRequest)\n      .catch(() => {});\n\n    // Best-effort ensure visible size in headless\n    await this.mainSession\n      .send(\"Emulation.setVisibleSize\", { width, height })\n      .catch(() => {});\n  }\n\n  /**\n   * Click at absolute page coordinates (CSS pixels).\n   * Dispatches mouseMoved → mousePressed → mouseReleased via CDP Input domain\n   * on the top-level page target's session. Coordinates are relative to the\n   * viewport origin (top-left). Does not scroll.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageClick\" })\n  async click(\n    x: number,\n    y: number,\n    options?: {\n      button?: \"left\" | \"right\" | \"middle\";\n      clickCount?: number;\n      returnXpath?: boolean;\n    },\n  ): Promise<string> {\n    const button = options?.button ?? \"left\";\n    const clickCount = options?.clickCount ?? 1;\n\n    let xpathResult: string | undefined;\n    if (options?.returnXpath) {\n      // Resolve the deepest node at the given coordinates and compute absolute XPath efficiently\n      try {\n        const hit = await resolveXpathForLocation(this, x, y);\n        if (hit) {\n          v3Logger({\n            category: \"page\",\n            message: \"click resolved hit\",\n            level: 2,\n            auxiliary: {\n              frameId: { value: String(hit.frameId), type: \"string\" },\n              backendNodeId: {\n                value: String(hit.backendNodeId),\n                type: \"string\",\n              },\n              x: { value: String(x), type: \"integer\" },\n              y: { value: String(y), type: \"integer\" },\n            },\n          });\n          xpathResult = hit.absoluteXPath;\n          v3Logger({\n            category: \"page\",\n            message: `click resolved xpath`,\n            level: 2,\n            auxiliary: {\n              xpath: { value: String(xpathResult ?? \"\"), type: \"string\" },\n            },\n          });\n        }\n      } catch {\n        // best-effort; fall through if any step fails\n      }\n    }\n\n    // Synthesize a simple mouse move + press + release sequence.\n    await this.updateCursor(x, y);\n    // Dispatch click events in a pipelined burst to reduce inter-click delay\n    // from network/CPU jitter between round trips.\n    const dispatches: Array<Promise<unknown>> = [];\n    dispatches.push(\n      this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n        type: \"mouseMoved\",\n        x,\n        y,\n        button: \"none\",\n      } as Protocol.Input.DispatchMouseEventRequest),\n    );\n\n    for (let i = 1; i <= clickCount; i++) {\n      dispatches.push(\n        this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n          type: \"mousePressed\",\n          x,\n          y,\n          button,\n          clickCount: i,\n        } as Protocol.Input.DispatchMouseEventRequest),\n      );\n      dispatches.push(\n        this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n          type: \"mouseReleased\",\n          x,\n          y,\n          button,\n          clickCount: i,\n        } as Protocol.Input.DispatchMouseEventRequest),\n      );\n    }\n    await Promise.all(dispatches);\n\n    return xpathResult ?? \"\";\n  }\n\n  /**\n   * Hover at absolute page coordinates (CSS pixels).\n   * Dispatches mouseMoved via CDP Input domain on the top-level page target's\n   * session.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageHover\" })\n  async hover(\n    x: number,\n    y: number,\n    options?: { returnXpath?: boolean },\n  ): Promise<string> {\n    let xpathResult: string | undefined;\n    if (options?.returnXpath) {\n      try {\n        const hit = await resolveXpathForLocation(this, x, y);\n        if (hit) {\n          v3Logger({\n            category: \"page\",\n            message: \"hover resolved hit\",\n            level: 2,\n            auxiliary: {\n              frameId: { value: String(hit.frameId), type: \"string\" },\n              backendNodeId: {\n                value: String(hit.backendNodeId),\n                type: \"string\",\n              },\n              x: { value: String(x), type: \"integer\" },\n              y: { value: String(y), type: \"integer\" },\n            },\n          });\n          xpathResult = hit.absoluteXPath;\n        }\n      } catch {\n        v3Logger({\n          category: \"page\",\n          message: \"Failed to resolve xpath for hover\",\n          level: 2,\n          auxiliary: {\n            x: { value: String(x), type: \"integer\" },\n            y: { value: String(y), type: \"integer\" },\n          },\n        });\n      }\n    }\n\n    await this.updateCursor(x, y);\n    await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n      type: \"mouseMoved\",\n      x,\n      y,\n      button: \"none\",\n    } as Protocol.Input.DispatchMouseEventRequest);\n\n    return xpathResult ?? \"\";\n  }\n\n  @FlowLogger.wrapWithLogging({ eventType: \"PageScroll\" })\n  async scroll(\n    x: number,\n    y: number,\n    deltaX: number,\n    deltaY: number,\n    options?: { returnXpath?: boolean },\n  ): Promise<string> {\n    let xpathResult: string | undefined;\n    if (options?.returnXpath) {\n      try {\n        const hit = await resolveXpathForLocation(this, x, y);\n        if (hit) xpathResult = hit.absoluteXPath;\n      } catch {\n        // best-effort\n      }\n    }\n\n    await this.updateCursor(x, y);\n    await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n      type: \"mouseMoved\",\n      x,\n      y,\n      button: \"none\",\n    } as Protocol.Input.DispatchMouseEventRequest);\n\n    // Synthesize a simple mouse move + press + release sequence\n    await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n      type: \"mouseWheel\",\n      x,\n      y,\n      button: \"none\",\n      deltaX,\n      deltaY,\n    } as Protocol.Input.DispatchMouseEventRequest);\n\n    return xpathResult ?? \"\";\n  }\n\n  /**\n   * Drag from (fromX, fromY) to (toX, toY) using mouse events.\n   * Sends mouseMoved → mousePressed → mouseMoved (steps) → mouseReleased.\n   */\n  @FlowLogger.wrapWithLogging({\n    eventType: \"PageDragAndDrop\",\n  })\n  async dragAndDrop(\n    fromX: number,\n    fromY: number,\n    toX: number,\n    toY: number,\n    options?: {\n      button?: \"left\" | \"right\" | \"middle\";\n      steps?: number;\n      delay?: number;\n      returnXpath?: boolean;\n    },\n  ): Promise<[string, string]> {\n    const button = options?.button ?? \"left\";\n    const steps = Math.max(1, Math.floor(options?.steps ?? 1));\n    const delay = Math.max(0, options?.delay ?? 0);\n\n    const sleep = (ms: number) =>\n      new Promise<void>((r) => (ms > 0 ? setTimeout(r, ms) : r()));\n\n    const buttonMask = (b: typeof button): number => {\n      switch (b) {\n        case \"left\":\n          return 1;\n        case \"right\":\n          return 2;\n        case \"middle\":\n          return 4;\n        default:\n          return 1;\n      }\n    };\n\n    let fromXpath: string | undefined;\n    let toXpath: string | undefined;\n    if (options?.returnXpath) {\n      try {\n        const start = await resolveXpathForLocation(this, fromX, fromY);\n        if (start) fromXpath = start.absoluteXPath;\n      } catch {\n        //\n      }\n      try {\n        const end = await resolveXpathForLocation(this, toX, toY);\n        if (end) toXpath = end.absoluteXPath;\n      } catch {\n        //\n      }\n    }\n\n    // Move to start\n    await this.updateCursor(fromX, fromY);\n    await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n      type: \"mouseMoved\",\n      x: fromX,\n      y: fromY,\n      button: \"none\",\n    } as Protocol.Input.DispatchMouseEventRequest);\n\n    // Press\n    await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n      type: \"mousePressed\",\n      x: fromX,\n      y: fromY,\n      button,\n      buttons: buttonMask(button),\n      clickCount: 1,\n    } as Protocol.Input.DispatchMouseEventRequest);\n\n    // Intermediate moves\n    for (let i = 1; i <= steps; i++) {\n      const t = i / steps;\n      const x = fromX + (toX - fromX) * t;\n      const y = fromY + (toY - fromY) * t;\n      await this.updateCursor(x, y);\n      await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n        type: \"mouseMoved\",\n        x,\n        y,\n        button,\n        buttons: buttonMask(button),\n      } as Protocol.Input.DispatchMouseEventRequest);\n      if (delay) await sleep(delay);\n    }\n\n    // Release at end\n    await this.updateCursor(toX, toY);\n    await this.mainSession.send<never>(\"Input.dispatchMouseEvent\", {\n      type: \"mouseReleased\",\n      x: toX,\n      y: toY,\n      button,\n      buttons: buttonMask(button),\n      clickCount: 1,\n    } as Protocol.Input.DispatchMouseEventRequest);\n\n    return [fromXpath ?? \"\", toXpath ?? \"\"];\n  }\n\n  /**\n   * Type a string by dispatching keyDown/keyUp events per character.\n   * Focus must already be on the desired element. Uses CDP Input.dispatchKeyEvent\n   * and never falls back to Input.insertText. Optional delay applies between\n   * successive characters.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageType\" })\n  async type(\n    text: string,\n    options?: { delay?: number; withMistakes?: boolean },\n  ): Promise<void> {\n    const delay = Math.max(0, options?.delay ?? 0);\n    const withMistakes = !!options?.withMistakes;\n\n    const sleep = (ms: number) =>\n      new Promise<void>((r) => (ms > 0 ? setTimeout(r, ms) : r()));\n\n    const keyStroke = async (\n      ch: string,\n      override?: {\n        key?: string;\n        code?: string;\n        windowsVirtualKeyCode?: number;\n      },\n    ) => {\n      if (override) {\n        const base: Protocol.Input.DispatchKeyEventRequest = {\n          type: \"keyDown\",\n          key: override.key,\n          code: override.code,\n          windowsVirtualKeyCode: override.windowsVirtualKeyCode,\n        } as Protocol.Input.DispatchKeyEventRequest;\n        await this.mainSession.send(\"Input.dispatchKeyEvent\", base);\n        await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n          ...base,\n          type: \"keyUp\",\n        } as Protocol.Input.DispatchKeyEventRequest);\n        return;\n      }\n\n      // Printable character: include key, code, and text for maximum compatibility\n      // Some sites (like Wordle) check event.key rather than relying on text input\n      const isLetter = /^[a-zA-Z]$/.test(ch);\n      const isDigit = /^[0-9]$/.test(ch);\n\n      let key = ch;\n      let code = \"\";\n      let windowsVirtualKeyCode: number | undefined;\n\n      if (isLetter) {\n        // For letters, key is the character, code is KeyX where X is uppercase\n        key = ch;\n        code = `Key${ch.toUpperCase()}`;\n        windowsVirtualKeyCode = ch.toUpperCase().charCodeAt(0);\n      } else if (isDigit) {\n        key = ch;\n        code = `Digit${ch}`;\n        windowsVirtualKeyCode = ch.charCodeAt(0);\n      } else if (ch === \" \") {\n        key = \" \";\n        code = \"Space\";\n        windowsVirtualKeyCode = 32;\n      }\n\n      const down: Protocol.Input.DispatchKeyEventRequest = {\n        type: \"keyDown\",\n        key,\n        code: code || undefined,\n        text: ch,\n        unmodifiedText: ch,\n        windowsVirtualKeyCode,\n      };\n      await this.mainSession.send(\"Input.dispatchKeyEvent\", down);\n      await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n        type: \"keyUp\",\n        key,\n        code: code || undefined,\n        windowsVirtualKeyCode,\n      } as Protocol.Input.DispatchKeyEventRequest);\n    };\n\n    const pressBackspace = async () =>\n      keyStroke(\"\\b\", {\n        key: \"Backspace\",\n        code: \"Backspace\",\n        windowsVirtualKeyCode: 8,\n      });\n\n    const randomPrintable = (avoid: string): string => {\n      const pool =\n        \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,;:'\\\"!?@#$%^&*()-_=+[]{}<>/\\\\|`~\";\n      let c = avoid;\n      while (c === avoid) {\n        c = pool[Math.floor(Math.random() * pool.length)];\n      }\n      return c;\n    };\n\n    for (const ch of text) {\n      // Control keys that we explicitly map\n      if (ch === \"\\n\" || ch === \"\\r\") {\n        await keyStroke(ch, {\n          key: \"Enter\",\n          code: \"Enter\",\n          windowsVirtualKeyCode: 13,\n        });\n      } else if (ch === \"\\t\") {\n        await keyStroke(ch, {\n          key: \"Tab\",\n          code: \"Tab\",\n          windowsVirtualKeyCode: 9,\n        });\n      } else {\n        if (withMistakes && Math.random() < 0.12) {\n          // Type a wrong character, then backspace to correct\n          const wrong = randomPrintable(ch);\n          await keyStroke(wrong);\n          if (delay) await sleep(delay);\n          await pressBackspace();\n          if (delay) await sleep(delay);\n        }\n        await keyStroke(ch);\n      }\n\n      if (delay) await sleep(delay);\n    }\n  }\n\n  /**\n   * Press a single key or key combination (keyDown then keyUp).\n   * For printable characters, uses the text path on keyDown; for named keys, sets key/code/VK.\n   * Supports key combinations with modifiers like \"Cmd+A\", \"Ctrl+C\", \"Shift+Tab\", etc.\n   */\n  @FlowLogger.wrapWithLogging({ eventType: \"PageKeyPress\" })\n  async keyPress(key: string, options?: { delay?: number }): Promise<void> {\n    const delay = Math.max(0, options?.delay ?? 0);\n    const sleep = (ms: number) =>\n      new Promise<void>((r) => (ms > 0 ? setTimeout(r, ms) : r()));\n\n    // Split key combination by + but handle the special case of \"+\" key itself\n    function split(keyString: string): string[] {\n      // Special case: if the entire string is just \"+\", return it as-is\n      if (keyString === \"+\") {\n        return [\"+\"];\n      }\n\n      const keys: string[] = [];\n      let building = \"\";\n      for (const char of keyString) {\n        if (char === \"+\" && building) {\n          keys.push(building);\n          building = \"\";\n        } else {\n          building += char;\n        }\n      }\n      if (building) {\n        keys.push(building);\n      }\n      return keys;\n    }\n\n    const tokens = split(key);\n    const mainKey = tokens[tokens.length - 1];\n    const modifierKeys = tokens.slice(0, -1);\n\n    try {\n      for (const modKey of modifierKeys) {\n        await this.keyDown(modKey);\n      }\n\n      await this.keyDown(mainKey);\n      if (delay) await sleep(delay);\n      await this.keyUp(mainKey);\n\n      for (let i = modifierKeys.length - 1; i >= 0; i--) {\n        await this.keyUp(modifierKeys[i]);\n      }\n    } catch (error) {\n      // Clear stuck modifiers on error to prevent affecting subsequent keyPress calls\n      this._pressedModifiers.clear();\n      throw error;\n    }\n  }\n\n  @FlowLogger.wrapWithLogging({ eventType: \"PageSnapshot\" })\n  async snapshot(options?: PageSnapshotOptions): Promise<SnapshotResult> {\n    try {\n      const { combinedTree, combinedXpathMap, combinedUrlMap } =\n        await captureHybridSnapshot(this, {\n          pierceShadow: true,\n          includeIframes: options?.includeIframes,\n        });\n\n      return {\n        formattedTree: combinedTree,\n        xpathMap: combinedXpathMap,\n        urlMap: combinedUrlMap,\n      };\n    } catch (err) {\n      throw new StagehandSnapshotError(err);\n    }\n  }\n\n  // Track pressed modifier keys\n  private _pressedModifiers = new Set<string>();\n\n  /** Press a key down without releasing it */\n  private async keyDown(key: string): Promise<void> {\n    const normalizedKey = this.normalizeModifierKey(key);\n\n    const modifierKeys = [\"Alt\", \"Control\", \"Meta\", \"Shift\"];\n    if (modifierKeys.includes(normalizedKey)) {\n      this._pressedModifiers.add(normalizedKey);\n    }\n\n    let modifiers = 0;\n    if (this._pressedModifiers.has(\"Alt\")) modifiers |= 1;\n    if (this._pressedModifiers.has(\"Control\")) modifiers |= 2;\n    if (this._pressedModifiers.has(\"Meta\")) modifiers |= 4;\n    if (this._pressedModifiers.has(\"Shift\")) modifiers |= 8;\n\n    const named = this.getNamedKeys();\n\n    if (normalizedKey.length === 1) {\n      const hasNonShiftModifier =\n        this._pressedModifiers.has(\"Alt\") ||\n        this._pressedModifiers.has(\"Control\") ||\n        this._pressedModifiers.has(\"Meta\");\n      if (hasNonShiftModifier) {\n        // For accelerators (e.g., Cmd/Ctrl/Alt + key), do not send text. Use rawKeyDown with key/code/VK.\n        const desc = this.describePrintableKey(normalizedKey);\n        const macCommands = this.isMacOS()\n          ? this.macCommandsFor(desc.code ?? \"\")\n          : [];\n        const req: Protocol.Input.DispatchKeyEventRequest = {\n          type: \"rawKeyDown\",\n          modifiers,\n          key: desc.key,\n          ...(desc.code ? { code: desc.code } : {}),\n          ...(typeof desc.vk === \"number\"\n            ? { windowsVirtualKeyCode: desc.vk }\n            : {}),\n          ...(macCommands.length ? { commands: macCommands } : {}),\n        } as Protocol.Input.DispatchKeyEventRequest;\n        await this.mainSession.send(\"Input.dispatchKeyEvent\", req);\n      } else {\n        // Typing path (no non-Shift modifiers): send text to generate input\n        await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n          type: \"keyDown\",\n          text: normalizedKey,\n          unmodifiedText: normalizedKey,\n          modifiers,\n        } as Protocol.Input.DispatchKeyEventRequest);\n      }\n      return;\n    }\n\n    const entry = named[normalizedKey] ?? null;\n    if (entry) {\n      const macCommands = this.isMacOS() ? this.macCommandsFor(entry.code) : [];\n      const includeText = !!entry.text && modifiers === 0;\n      const keyDown: Protocol.Input.DispatchKeyEventRequest = {\n        type: includeText ? \"keyDown\" : \"rawKeyDown\",\n        key: entry.key,\n        code: entry.code,\n        windowsVirtualKeyCode: entry.vk,\n        modifiers,\n        ...(includeText\n          ? {\n              text: entry.text,\n              unmodifiedText: entry.unmodifiedText ?? entry.text,\n            }\n          : {}),\n        ...(macCommands.length ? { commands: macCommands } : {}),\n      } as Protocol.Input.DispatchKeyEventRequest;\n      await this.mainSession.send(\"Input.dispatchKeyEvent\", keyDown);\n      return;\n    }\n\n    // Fallback: send with key property only\n    await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n      type: \"keyDown\",\n      key: normalizedKey,\n      modifiers,\n    } as Protocol.Input.DispatchKeyEventRequest);\n  }\n\n  /** Release a pressed key */\n  private async keyUp(key: string): Promise<void> {\n    const normalizedKey = this.normalizeModifierKey(key);\n\n    let modifiers = 0;\n    if (this._pressedModifiers.has(\"Alt\")) modifiers |= 1;\n    if (this._pressedModifiers.has(\"Control\")) modifiers |= 2;\n    if (this._pressedModifiers.has(\"Meta\")) modifiers |= 4;\n    if (this._pressedModifiers.has(\"Shift\")) modifiers |= 8;\n\n    const modifierKeys = [\"Alt\", \"Control\", \"Meta\", \"Shift\"];\n    if (modifierKeys.includes(normalizedKey)) {\n      this._pressedModifiers.delete(normalizedKey);\n    }\n\n    const named = this.getNamedKeys();\n\n    if (normalizedKey.length === 1) {\n      const desc = this.describePrintableKey(normalizedKey);\n      await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n        type: \"keyUp\",\n        key: desc.key,\n        code: desc.code,\n        windowsVirtualKeyCode:\n          typeof desc.vk === \"number\" ? desc.vk : undefined,\n        modifiers,\n      } as Protocol.Input.DispatchKeyEventRequest);\n      return;\n    }\n\n    const entry = named[normalizedKey] ?? null;\n    if (entry) {\n      await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n        type: \"keyUp\",\n        key: entry.key,\n        code: entry.code,\n        windowsVirtualKeyCode: entry.vk,\n        modifiers,\n      } as Protocol.Input.DispatchKeyEventRequest);\n      return;\n    }\n\n    // Fallback: send with key property only\n    await this.mainSession.send(\"Input.dispatchKeyEvent\", {\n      type: \"keyUp\",\n      key: normalizedKey,\n      modifiers,\n    } as Protocol.Input.DispatchKeyEventRequest);\n  }\n\n  /** Normalize key names to match CDP expectations */\n  private normalizeModifierKey(key: string): string {\n    const lower = key.toLowerCase();\n    switch (lower) {\n      // Modifier keys\n      case \"cmd\":\n      case \"command\":\n      case \"controlormeta\":\n        // On Mac, Cmd is Meta; elsewhere map to Control for common shortcuts\n        return this.isMacOS() ? \"Meta\" : \"Control\";\n      case \"win\":\n      case \"windows\":\n        return \"Meta\";\n      case \"ctrl\":\n      case \"control\":\n        return \"Control\";\n      case \"option\":\n      case \"alt\":\n        return \"Alt\";\n      case \"shift\":\n        return \"Shift\";\n      case \"meta\":\n        return \"Meta\";\n      // Action keys\n      case \"enter\":\n      case \"return\":\n        return \"Enter\";\n      case \"esc\":\n      case \"escape\":\n        return \"Escape\";\n      case \"backspace\":\n        return \"Backspace\";\n      case \"tab\":\n        return \"Tab\";\n      case \"space\":\n      case \"spacebar\":\n        return \" \";\n      case \"delete\":\n      case \"del\":\n        return \"Delete\";\n      // Arrow keys\n      case \"left\":\n      case \"arrowleft\":\n        return \"ArrowLeft\";\n      case \"right\":\n      case \"arrowright\":\n        return \"ArrowRight\";\n      case \"up\":\n      case \"arrowup\":\n        return \"ArrowUp\";\n      case \"down\":\n      case \"arrowdown\":\n        return \"ArrowDown\";\n      // Navigation keys\n      case \"home\":\n        return \"Home\";\n      case \"end\":\n        return \"End\";\n      case \"pageup\":\n      case \"pgup\":\n        return \"PageUp\";\n      case \"pagedown\":\n      case \"pgdn\":\n        return \"PageDown\";\n      default:\n        return key;\n    }\n  }\n\n  /**\n   * Get the map of named keys with their properties\n   */\n  private getNamedKeys(): Record<\n    string,\n    {\n      key: string;\n      code: string;\n      vk: number;\n      text?: string;\n      unmodifiedText?: string;\n    }\n  > {\n    return {\n      Enter: {\n        key: \"Enter\",\n        code: \"Enter\",\n        vk: 13,\n        text: \"\\r\",\n        unmodifiedText: \"\\r\",\n      },\n      Tab: { key: \"Tab\", code: \"Tab\", vk: 9 },\n      Backspace: { key: \"Backspace\", code: \"Backspace\", vk: 8 },\n      Escape: { key: \"Escape\", code: \"Escape\", vk: 27 },\n      Delete: { key: \"Delete\", code: \"Delete\", vk: 46 },\n      ArrowLeft: { key: \"ArrowLeft\", code: \"ArrowLeft\", vk: 37 },\n      ArrowUp: { key: \"ArrowUp\", code: \"ArrowUp\", vk: 38 },\n      ArrowRight: { key: \"ArrowRight\", code: \"ArrowRight\", vk: 39 },\n      ArrowDown: { key: \"ArrowDown\", code: \"ArrowDown\", vk: 40 },\n      Home: { key: \"Home\", code: \"Home\", vk: 36 },\n      End: { key: \"End\", code: \"End\", vk: 35 },\n      PageUp: { key: \"PageUp\", code: \"PageUp\", vk: 33 },\n      PageDown: { key: \"PageDown\", code: \"PageDown\", vk: 34 },\n      // Modifier keys\n      Alt: { key: \"Alt\", code: \"AltLeft\", vk: 18 },\n      Control: { key: \"Control\", code: \"ControlLeft\", vk: 17 },\n      Meta: { key: \"Meta\", code: \"MetaLeft\", vk: 91 },\n      Shift: { key: \"Shift\", code: \"ShiftLeft\", vk: 16 },\n    };\n  }\n\n  /**\n   * Minimal description for printable keys (letters/digits/space) to provide code and VK.\n   * Used when non-Shift modifiers are pressed to avoid sending text while keeping accelerator info.\n   */\n  private describePrintableKey(ch: string): {\n    key: string;\n    code?: string;\n    vk?: number;\n  } {\n    const shiftDown = this._pressedModifiers.has(\"Shift\");\n    const isLetter = /^[a-zA-Z]$/.test(ch);\n    const isDigit = /^[0-9]$/.test(ch);\n\n    if (isLetter) {\n      const upper = ch.toUpperCase();\n      return {\n        key: shiftDown ? upper : upper.toLowerCase(),\n        code: `Key${upper}`,\n        vk: upper.charCodeAt(0), // 'A'..'Z' => 65..90\n      };\n    }\n\n    if (isDigit) {\n      return {\n        key: ch,\n        code: `Digit${ch}`,\n        vk: ch.charCodeAt(0), // '0'..'9' => 48..57\n      };\n    }\n\n    if (ch === \" \") {\n      return { key: \" \", code: \"Space\", vk: 32 };\n    }\n\n    // Fallback: just return the character as-is; VK best-effort from ASCII\n    return {\n      key: shiftDown ? ch.toUpperCase() : ch,\n      vk: ch.toUpperCase().charCodeAt(0),\n    };\n  }\n\n  private isMacOS(): boolean {\n    try {\n      return process.platform === \"darwin\";\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Return Chromium mac editing commands (without trailing ':') for a given code like 'KeyA'\n   * Only used on macOS to trigger system editing shortcuts (e.g., selectAll, copy, paste...).\n   */\n  private macCommandsFor(code: string): string[] {\n    if (!this.isMacOS()) return [];\n    const parts: string[] = [];\n    if (this._pressedModifiers.has(\"Shift\")) parts.push(\"Shift\");\n    if (this._pressedModifiers.has(\"Control\")) parts.push(\"Control\");\n    if (this._pressedModifiers.has(\"Alt\")) parts.push(\"Alt\");\n    if (this._pressedModifiers.has(\"Meta\")) parts.push(\"Meta\");\n    parts.push(code);\n    const shortcut = parts.join(\"+\");\n    const table: Record<string, string | string[]> = {\n      \"Meta+KeyA\": \"selectAll:\",\n      \"Meta+KeyC\": \"copy:\",\n      \"Meta+KeyX\": \"cut:\",\n      \"Meta+KeyV\": \"paste:\",\n      \"Meta+KeyZ\": \"undo:\",\n    };\n    const value = table[shortcut];\n    if (!value) return [];\n    const list = Array.isArray(value) ? value : [value];\n    return list\n      .filter((c) => !c.startsWith(\"insert\"))\n      .map((c) => c.substring(0, c.length - 1));\n  }\n\n  // ---- Page-level lifecycle waiter that follows main frame id swaps ----\n\n  /** Resolve the main-world execution context for the current main frame. */\n  private async mainWorldExecutionContextId(): Promise<number> {\n    return executionContexts.waitForMainWorld(\n      this.mainSession,\n      this.mainFrameId(),\n      1000,\n    );\n  }\n\n  private async isMainLoadStateReady(\n    state: \"domcontentloaded\" | \"load\",\n  ): Promise<boolean> {\n    try {\n      const ctxId = await this.mainWorldExecutionContextId();\n      const { result } =\n        await this.mainSession.send<Protocol.Runtime.EvaluateResponse>(\n          \"Runtime.evaluate\",\n          {\n            expression: \"document.readyState\",\n            contextId: ctxId,\n            returnByValue: true,\n          },\n        );\n      const readyState = String(result?.value ?? \"\");\n      if (state === \"domcontentloaded\") {\n        return readyState === \"interactive\" || readyState === \"complete\";\n      }\n      return readyState === \"complete\";\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Wait until the **current** main frame reaches a lifecycle state.\n   * - Fast path via `document.readyState`.\n   * - Event path listens at the session level and compares incoming `frameId`\n   *   to `mainFrameId()` **at event time** to follow root swaps.\n   */\n  async waitForMainLoadState(\n    state: LoadState,\n    timeoutMs = 15000,\n  ): Promise<void> {\n    await this.mainSession\n      .send(\"Page.setLifecycleEventsEnabled\", { enabled: true })\n      .catch(() => {});\n\n    // Fast path: check the *current* main frame's readyState.\n    if (\n      (state === \"domcontentloaded\" || state === \"load\") &&\n      (await this.isMainLoadStateReady(state))\n    ) {\n      return;\n    }\n\n    const wanted = LIFECYCLE_NAME[state];\n    return new Promise<void>((resolve, reject) => {\n      let done = false;\n      let timer: ReturnType<typeof setTimeout> | null = null;\n      let pollTimer: ReturnType<typeof setTimeout> | null = null;\n      let pollInFlight = false;\n\n      const off = () => {\n        this.mainSession.off(\"Page.lifecycleEvent\", onLifecycle);\n        this.mainSession.off(\"Page.domContentEventFired\", onDomContent);\n        this.mainSession.off(\"Page.loadEventFired\", onLoad);\n      };\n      const clearPollTimer = () => {\n        if (pollTimer) {\n          clearTimeout(pollTimer);\n          pollTimer = null;\n        }\n      };\n\n      const finish = () => {\n        if (done) return;\n        done = true;\n        if (timer) {\n          clearTimeout(timer);\n          timer = null;\n        }\n        clearPollTimer();\n        off();\n        resolve();\n      };\n\n      const onLifecycle = (evt: Protocol.Page.LifecycleEventEvent) => {\n        if (evt.name !== wanted) return;\n        // Compare against the *current* main frame id when the event arrives.\n        if (evt.frameId === this.mainFrameId()) finish();\n      };\n\n      const onDomContent = () => {\n        if (state === \"domcontentloaded\") finish();\n      };\n\n      const onLoad = () => {\n        if (state === \"load\") finish();\n      };\n\n      this.mainSession.on(\"Page.lifecycleEvent\", onLifecycle);\n      // Backups for sites that don't emit lifecycle consistently\n      this.mainSession.on(\"Page.domContentEventFired\", onDomContent);\n      this.mainSession.on(\"Page.loadEventFired\", onLoad);\n\n      // Fallback polling closes lifecycle-event races in remote environments\n      // where readyState has advanced but the corresponding event was missed.\n      const pollReadyState = async () => {\n        if (done || pollInFlight) return;\n        pollInFlight = true;\n        try {\n          if (done) return;\n          if (\n            (state === \"domcontentloaded\" || state === \"load\") &&\n            (await this.isMainLoadStateReady(state))\n          ) {\n            finish();\n            return;\n          }\n        } finally {\n          pollInFlight = false;\n        }\n        if (!done) {\n          clearPollTimer();\n          pollTimer = setTimeout(() => {\n            void pollReadyState();\n          }, 100);\n        }\n      };\n      void pollReadyState();\n\n      timer = setTimeout(() => {\n        if (done) return;\n        done = true;\n        clearPollTimer();\n        off();\n        reject(\n          new Error(\n            `waitForMainLoadState(${state}) timed out after ${timeoutMs}ms`,\n          ),\n        );\n      }, timeoutMs);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/piercer.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { v3Logger } from \"../logger.js\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport { v3ScriptContent } from \"../dom/build/scriptV3Content.js\";\nimport { reRenderScriptContent } from \"../dom/build/reRenderScriptContent.js\";\n\nexport async function installV3PiercerIntoSession(\n  session: CDPSessionLike,\n): Promise<boolean> {\n  const pageEnabled = await session\n    .send(\"Page.enable\")\n    .then(() => true)\n    .catch(() => false);\n  if (!pageEnabled) return false;\n\n  await session.send(\"Runtime.enable\").catch(() => {});\n  try {\n    await session.send<Protocol.Page.AddScriptToEvaluateOnNewDocumentResponse>(\n      \"Page.addScriptToEvaluateOnNewDocument\",\n      { source: v3ScriptContent, runImmediately: true },\n    );\n  } catch (e) {\n    const msg = String((e as Error)?.message ?? e ?? \"\");\n    // If the session vanished during attach (common with short-lived OOPIFs),\n    // swallow and report failure so callers can early-return.\n    if (msg.includes(\"Session with given id not found\")) return false;\n    // For other errors, keep going but don't throw — the next evaluate is idempotent.\n  }\n  await session\n    .send<Protocol.Runtime.EvaluateResponse>(\"Runtime.evaluate\", {\n      expression: v3ScriptContent,\n      returnByValue: true,\n      awaitPromise: true,\n    })\n    .catch(() => {});\n\n  // After the piercer is in place, re-render any custom elements whose\n  // shadow roots were created before we patched attachShadow so their\n  // closed roots are recreated under the hook.\n  await session\n    .send<Protocol.Runtime.EvaluateResponse>(\"Runtime.evaluate\", {\n      expression: reRenderScriptContent,\n      returnByValue: true,\n      awaitPromise: false,\n    })\n    .catch(() => {});\n  return true;\n}\n\n/** (Optional) stream patch logs in your node console during bring-up */\nexport function tapPiercerConsole(\n  session: CDPSessionLike,\n  label: string,\n): void {\n  session.on<Protocol.Runtime.ConsoleAPICalledEvent>(\n    \"Runtime.consoleAPICalled\",\n    (evt) => {\n      const head = evt.args?.[0]?.value as string | undefined;\n      if (head?.startsWith?.(\"[v3-piercer]\")) {\n        v3Logger({\n          category: \"piercer\",\n          message: `[${label}] ${head}`,\n          level: 2,\n          auxiliary: {\n            value: {\n              value: String(evt.args?.[1]?.value ?? \"\"),\n              type: \"string\",\n            },\n          },\n        });\n      }\n    },\n  );\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/response.ts",
    "content": "/**\n * Response\n * -----------------\n *\n * This module implements a Playwright-inspired response wrapper that exposes\n * navigation metadata and helpers for retrieving HTTP response bodies. The\n * abstraction is consumed by navigation routines (e.g. `Page.goto`) so callers\n * can synchronously inspect status codes, lazily fetch body text, or await the\n * network layer finishing the request. The implementation is built directly on\n * Chrome DevTools Protocol primitives – it holds the originating `requestId`\n * so it can request payloads via `Network.getResponseBody`, and it listens for\n * `responseReceivedExtraInfo`, `loadingFinished`, and `loadingFailed` events to\n * hydrate the richer header view and resolve callers waiting on completion.\n */\n\nimport type { Protocol } from \"devtools-protocol\";\nimport type { SerializableResponse } from \"../types/private/index.js\";\nimport {\n  ResponseBodyError,\n  ResponseParseError,\n} from \"../types/public/sdkErrors.js\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport type { Frame } from \"./frame.js\";\nimport type { Page } from \"./page.js\";\n\ntype ServerAddr = { ipAddress: string; port: number };\n\nexport function isSerializableResponse(\n  value: unknown,\n): value is SerializableResponse {\n  if (!value || typeof value !== \"object\") return false;\n  const candidate = value as Partial<SerializableResponse>;\n  if (typeof candidate.requestId !== \"string\") return false;\n  if (!candidate.response || typeof candidate.response !== \"object\") {\n    return false;\n  }\n  return true;\n}\n\n/**\n * Minimal deferred helper that lets navigation tracking hand out a promise and\n * later control the resolution from event callbacks. Each response owns a\n * single deferred covering the \"finished\" promise so consumers can mirror\n * Playwright's `response.finished()` behaviour.\n */\ntype Deferred<T> = {\n  promise: Promise<T>;\n  resolve: (value: T) => void;\n  reject: (error: Error) => void;\n};\n\nfunction createDeferred<T>(): Deferred<T> {\n  let resolve!: (value: T) => void;\n  let reject!: (error: Error) => void;\n  const promise = new Promise<T>((res, rej) => {\n    resolve = res;\n    reject = rej;\n  });\n  return { promise, resolve, reject };\n}\n\n/** Normalise header names to lowercase for case-insensitive lookups. */\nfunction normaliseHeaderName(name: string): string {\n  return name.toLowerCase();\n}\n\n/** Split multi-value header strings into discrete values while trimming. */\nfunction splitHeaderValues(value: string): string[] {\n  return value\n    .split(/\\r?\\n/)\n    .map((part) => part.trim())\n    .filter(Boolean);\n}\n\n/**\n * Parse an HTTP header text block (as emitted by CDP) into an ordered array of\n * name/value pairs while preserving the wire casing.\n */\nfunction parseHeadersText(\n  headersText: string | undefined,\n): Array<{ name: string; value: string }> {\n  if (!headersText) return [];\n  const lines = headersText.split(/\\r?\\n/);\n  const entries: Array<{ name: string; value: string }> = [];\n  for (const line of lines) {\n    if (!line || line.startsWith(\"HTTP/\")) continue;\n    const index = line.indexOf(\":\");\n    if (index === -1) continue;\n    const name = line.slice(0, index).trim();\n    const value = line.slice(index + 1).trim();\n    entries.push({ name, value });\n  }\n  return entries;\n}\n\n/**\n * Thin wrapper around CDP response metadata that mirrors the ergonomics of\n * Playwright's `Response` class. The class intentionally keeps the same method\n * names so upstream integrations can transition with minimal code changes.\n */\nexport class Response {\n  private readonly page: Page;\n  private readonly session: CDPSessionLike;\n  private readonly requestId: string;\n  private readonly frameId?: string;\n  private readonly loaderId?: string;\n  private readonly response: Protocol.Network.Response;\n  private readonly fromServiceWorkerFlag: boolean;\n  private readonly serverAddress?: ServerAddr | null;\n\n  private headersObject: Record<string, string>;\n  private headersArrayCache: Array<{ name: string; value: string }> | null =\n    null;\n  private allHeadersCache: Record<string, string> | null = null;\n  private readonly headerValuesMap = new Map<string, string[]>();\n\n  private finishedDeferred = createDeferred<null | Error>();\n  private finishedSettled = false;\n\n  private extraInfoHeaders: Protocol.Network.Headers | null = null;\n  private extraInfoHeadersText: string | undefined;\n\n  /**\n   * Build a response wrapper from the CDP notification associated with a\n   * navigation. The constructor captures the owning page/session so follow-up\n   * methods (body/text/json) can query CDP on-demand. The `response` payload is\n   * the raw `Protocol.Network.Response` object emitted by Chrome.\n   */\n  constructor(params: {\n    page: Page;\n    session: CDPSessionLike;\n    requestId: string;\n    frameId?: string;\n    loaderId?: string;\n    response: Protocol.Network.Response;\n    fromServiceWorker: boolean;\n  }) {\n    this.page = params.page;\n    this.session = params.session;\n    this.requestId = params.requestId;\n    this.frameId = params.frameId;\n    this.loaderId = params.loaderId;\n    this.response = params.response;\n    this.fromServiceWorkerFlag = params.fromServiceWorker;\n\n    if (\n      params.response.remoteIPAddress &&\n      params.response.remotePort !== undefined\n    ) {\n      this.serverAddress = {\n        ipAddress: params.response.remoteIPAddress,\n        port: params.response.remotePort,\n      };\n    } else {\n      this.serverAddress = null;\n    }\n\n    this.headersObject = {};\n    for (const [name, value] of Object.entries(this.response.headers ?? {})) {\n      const lower = normaliseHeaderName(name);\n      if (value === undefined) continue;\n      const values = splitHeaderValues(String(value));\n      this.headerValuesMap.set(lower, values);\n      this.headersObject[lower] = values.join(\", \");\n    }\n  }\n\n  /** URL associated with the navigation request. */\n  url(): string {\n    return this.response.url;\n  }\n\n  /** HTTP status code reported by Chrome. */\n  status(): number {\n    return this.response.status;\n  }\n\n  /** Human-readable status text that accompanied the response. */\n  statusText(): string {\n    return this.response.statusText;\n  }\n\n  /** Convenience predicate that checks for 2xx statuses. */\n  ok(): boolean {\n    const status = this.status();\n    return status >= 200 && status <= 299;\n  }\n\n  /** Returns the Stagehand frame object that initiated the navigation. */\n  frame(): Frame | null {\n    if (!this.frameId) return null;\n    try {\n      return this.page.frameForId(this.frameId);\n    } catch {\n      return null;\n    }\n  }\n\n  /** Indicates whether the response was serviced by a Service Worker. */\n  fromServiceWorker(): boolean {\n    return this.fromServiceWorkerFlag;\n  }\n\n  /**\n   * Returns TLS security metadata when provided by the browser. In practice\n   * this includes certificate issuer, protocol, and validity interval.\n   */\n  async securityDetails(): Promise<Protocol.Network.SecurityDetails | null> {\n    return this.response.securityDetails ?? null;\n  }\n\n  /** Returns the resolved server address for the navigation when available. */\n  async serverAddr(): Promise<ServerAddr | null> {\n    return this.serverAddress ?? null;\n  }\n\n  /**\n   * Returns the response headers normalised to lowercase keys. Matches the\n   * behaviour of Playwright's `headers()` by eliding duplicate header entries.\n   */\n  headers(): Record<string, string> {\n    return { ...this.headersObject };\n  }\n\n  /**\n   * Returns all headers including those only surfaced through\n   * `responseReceivedExtraInfo` such as `set-cookie`. Values are reported as the\n   * browser sends them (no further splitting or concatenation).\n   */\n  async allHeaders(): Promise<Record<string, string>> {\n    if (this.allHeadersCache) return { ...this.allHeadersCache };\n    const source = this.extraInfoHeaders ?? this.response.headers ?? {};\n    const map: Record<string, string> = {};\n    for (const [name, value] of Object.entries(source)) {\n      map[name] = String(value);\n    }\n    this.allHeadersCache = map;\n    return { ...map };\n  }\n\n  /** Returns a concatenated header string for the supplied header name. */\n  async headerValue(name: string): Promise<string | null> {\n    const values = await this.headerValues(name);\n    if (!values.length) return null;\n    return values.join(\", \");\n  }\n\n  /** Returns all values for a header (case-insensitive lookup). */\n  async headerValues(name: string): Promise<string[]> {\n    const lower = normaliseHeaderName(name);\n    if (this.extraInfoHeaders) {\n      const raw = this.extraInfoHeaders[name] ?? this.extraInfoHeaders[lower];\n      if (raw !== undefined) {\n        return splitHeaderValues(String(raw));\n      }\n    }\n    const values = this.headerValuesMap.get(lower);\n    return values ? [...values] : [];\n  }\n\n  /**\n   * Returns header entries preserving their original wire casing and ordering.\n   * Falls back to the CDP object when the raw header text is unavailable.\n   */\n  async headersArray(): Promise<Array<{ name: string; value: string }>> {\n    if (this.headersArrayCache) return [...this.headersArrayCache];\n\n    const entriesFromText = parseHeadersText(this.extraInfoHeadersText);\n    if (entriesFromText.length > 0) {\n      this.headersArrayCache = entriesFromText;\n      return [...entriesFromText];\n    }\n\n    const entries: Array<{ name: string; value: string }> = [];\n    const source = this.extraInfoHeaders ?? this.response.headers ?? {};\n    for (const [name, value] of Object.entries(source)) {\n      const values = splitHeaderValues(String(value));\n      for (const val of values) {\n        entries.push({ name, value: val });\n      }\n    }\n    this.headersArrayCache = entries;\n    return [...entries];\n  }\n\n  /**\n   * Requests the raw response body from Chrome DevTools Protocol. The method is\n   * intentionally lazy because not every caller needs the payload, and CDP only\n   * allows retrieving it once the response completes.\n   */\n  async body(): Promise<Buffer> {\n    const result = await this.session\n      .send<Protocol.Network.GetResponseBodyResponse>(\n        \"Network.getResponseBody\",\n        { requestId: this.requestId },\n      )\n      .catch((error) => {\n        throw new ResponseBodyError(String(error));\n      });\n\n    if (result.base64Encoded) {\n      return Buffer.from(result.body, \"base64\");\n    }\n    return Buffer.from(result.body, \"utf-8\");\n  }\n\n  /** Decodes the response body as UTF-8 text. */\n  async text(): Promise<string> {\n    const buffer = await this.body();\n    return buffer.toString(\"utf-8\");\n  }\n\n  /** Parses the response body as JSON and throws if parsing fails. */\n  async json<T = unknown>(): Promise<T> {\n    const text = await this.text();\n    try {\n      return JSON.parse(text) as T;\n    } catch (error) {\n      throw new ResponseParseError(String(error));\n    }\n  }\n\n  /**\n   * Resolves once the underlying network request completes or fails. Mirrors\n   * Playwright's behaviour by resolving to `null` on success and to an `Error`\n   * instance when Chrome reports `Network.loadingFailed`.\n   */\n  async finished(): Promise<null | Error> {\n    return this.finishedDeferred.promise;\n  }\n\n  /**\n   * Internal helper invoked by the navigation tracker when CDP reports extra\n   * header information. This keeps the cached header views in sync with the\n   * richer metadata.\n   */\n  public applyExtraInfo(\n    event: Protocol.Network.ResponseReceivedExtraInfoEvent,\n  ): void {\n    this.extraInfoHeaders = event.headers;\n    this.extraInfoHeadersText = event.headersText;\n    this.allHeadersCache = null;\n    this.headersArrayCache = null;\n    this.headersObject = {};\n    this.headerValuesMap.clear();\n\n    const source = event.headers ?? {};\n    for (const [name, value] of Object.entries(source)) {\n      const lower = normaliseHeaderName(name);\n      const segments = splitHeaderValues(String(value));\n      this.headerValuesMap.set(lower, segments);\n      this.headersObject[lower] = segments.join(\", \");\n    }\n  }\n\n  /**\n   * Internal helper for creating a Response object from a Serializable\n   * goto response from the Stagehand API\n   */\n  public static fromSerializable(\n    serialized: SerializableResponse,\n    context: { page: Page; session: CDPSessionLike },\n  ): Response {\n    const reconstructed = new Response({\n      page: context.page,\n      session: context.session,\n      requestId: serialized.requestId,\n      frameId: serialized.frameId,\n      loaderId: serialized.loaderId,\n      response: serialized.response as Protocol.Network.Response,\n      fromServiceWorker: serialized.fromServiceWorkerFlag ?? false,\n    });\n\n    if (serialized.extraInfoHeaders) {\n      reconstructed.applyExtraInfo({\n        requestId: serialized.requestId,\n        headers: serialized.extraInfoHeaders,\n        headersText: serialized.extraInfoHeadersText,\n      } as Protocol.Network.ResponseReceivedExtraInfoEvent);\n    }\n\n    if (serialized.finishedSettled) {\n      reconstructed.markFinished(null);\n    }\n\n    return reconstructed;\n  }\n\n  /** Marks the response as finished and resolves the `finished()` promise. */\n  public markFinished(error: Error | null): void {\n    if (this.finishedSettled) return;\n    this.finishedSettled = true;\n    if (error) {\n      this.finishedDeferred.resolve(error);\n    } else {\n      this.finishedDeferred.resolve(null);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/screenshotUtils.ts",
    "content": "import { Protocol } from \"devtools-protocol\";\nimport type { CDPSessionLike } from \"./cdp.js\";\nimport type { Frame } from \"./frame.js\";\nimport type { Locator } from \"./locator.js\";\nimport type { Page } from \"./page.js\";\nimport type {\n  ScreenshotClip,\n  ScreenshotScaleOption,\n} from \"../types/public/screenshotTypes.js\";\nimport { StagehandInvalidArgumentError } from \"../types/public/sdkErrors.js\";\nimport { screenshotScriptSources } from \"../dom/build/screenshotScripts.generated.js\";\n\nexport type ScreenshotCleanup = () => Promise<void> | void;\n\nexport function collectFramesForScreenshot(page: Page): Frame[] {\n  const seen = new Map<string, Frame>();\n  const main = page.mainFrame();\n  seen.set(main.frameId, main);\n  for (const frame of page.frames()) {\n    seen.set(frame.frameId, frame);\n  }\n  return Array.from(seen.values());\n}\n\nexport function normalizeScreenshotClip(clip: ScreenshotClip): ScreenshotClip {\n  const x = Number(clip.x);\n  const y = Number(clip.y);\n  const width = Number(clip.width);\n  const height = Number(clip.height);\n\n  for (const [key, value] of Object.entries({ x, y, width, height })) {\n    if (!Number.isFinite(value)) {\n      throw new StagehandInvalidArgumentError(\n        `screenshot: clip.${key} must be a finite number`,\n      );\n    }\n  }\n\n  if (width <= 0 || height <= 0) {\n    throw new StagehandInvalidArgumentError(\n      \"screenshot: clip width/height must be positive\",\n    );\n  }\n\n  return { x, y, width, height };\n}\n\nexport async function computeScreenshotScale(\n  page: Page,\n  mode: ScreenshotScaleOption,\n): Promise<number | undefined> {\n  if (mode !== \"css\") return undefined;\n  try {\n    const frame = page.mainFrame();\n    const dpr = await frame\n      .evaluate(() => {\n        const ratio = Number(window.devicePixelRatio || 1);\n        return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;\n      })\n      .catch(() => 1);\n    const safeRatio = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;\n    return Math.min(2, Math.max(0.1, 1 / safeRatio));\n  } catch {\n    return 1;\n  }\n}\n\nexport async function setTransparentBackground(\n  session: CDPSessionLike,\n): Promise<ScreenshotCleanup> {\n  await session\n    .send(\"Emulation.setDefaultBackgroundColorOverride\", {\n      color: { r: 0, g: 0, b: 0, a: 0 },\n    })\n    .catch(() => {});\n\n  return async () => {\n    await session\n      .send(\"Emulation.setDefaultBackgroundColorOverride\", {})\n      .catch(() => {});\n  };\n}\n\nexport async function applyStyleToFrames(\n  frames: Frame[],\n  css: string,\n  label: string,\n): Promise<ScreenshotCleanup> {\n  const trimmed = css.trim();\n  if (!trimmed) return async () => {};\n  const token = `__v3_style_${label}_${Date.now()}_${Math.random()\n    .toString(36)\n    .slice(2)}`;\n\n  await Promise.all(\n    frames.map((frame) =>\n      frame\n        .evaluate(\n          ({ css, token }) => {\n            try {\n              const doc = document;\n              if (!doc) return;\n              const style = doc.createElement(\"style\");\n              style.setAttribute(\"data-stagehand-style\", token);\n              style.textContent = css;\n              const parent = doc.head || doc.documentElement || doc.body;\n              parent?.appendChild(style);\n            } catch {\n              // ignore\n            }\n          },\n          { css: trimmed, token },\n        )\n        .catch(() => {}),\n    ),\n  );\n\n  return async () => {\n    await Promise.all(\n      frames.map((frame) =>\n        frame\n          .evaluate((token) => {\n            try {\n              const doc = document;\n              if (!doc) return;\n              const nodes = doc.querySelectorAll(\n                `[data-stagehand-style=\"${token}\"]`,\n              );\n              nodes.forEach((node) => node.remove());\n            } catch {\n              // ignore\n            }\n          }, token)\n          .catch(() => {}),\n      ),\n    );\n  };\n}\n\nexport async function disableAnimations(\n  frames: Frame[],\n): Promise<ScreenshotCleanup> {\n  const css = `\n*,\n*::before,\n*::after {\n  animation-delay: 0s !important;\n  animation-duration: 0s !important;\n  animation-iteration-count: 1 !important;\n  animation-play-state: paused !important;\n  transition-property: none !important;\n  transition-duration: 0s !important;\n  transition-delay: 0s !important;\n}`;\n\n  const cleanup = await applyStyleToFrames(frames, css, \"animations\");\n\n  await Promise.all(\n    frames.map((frame) =>\n      frame\n        .evaluate(() => {\n          try {\n            const animations =\n              typeof document.getAnimations === \"function\"\n                ? document.getAnimations()\n                : [];\n            for (const animation of animations) {\n              try {\n                const details = animation.effect?.getComputedTiming?.();\n                if (details && details.iterations !== Infinity) {\n                  animation.finish?.();\n                } else {\n                  animation.cancel?.();\n                }\n              } catch {\n                animation.cancel?.();\n              }\n            }\n          } catch {\n            // ignore\n          }\n        })\n        .catch(() => {}),\n    ),\n  );\n\n  return cleanup;\n}\n\nexport async function hideCaret(frames: Frame[]): Promise<ScreenshotCleanup> {\n  const css = `\ninput,\ntextarea,\n[contenteditable],\n[contenteditable=\"\"],\n[contenteditable=\"true\"],\n[contenteditable=\"plaintext-only\"],\n*:focus {\n  caret-color: transparent !important;\n}`;\n\n  return applyStyleToFrames(frames, css, \"caret\");\n}\n\nexport async function applyMaskOverlays(\n  locators: Locator[],\n  color: string,\n): Promise<ScreenshotCleanup> {\n  type MaskRectSpec = ScreenshotClip & { rootToken?: string | null };\n  const rectsByFrame = new Map<\n    Frame,\n    { rects: MaskRectSpec[]; rootTokens: Set<string> }\n  >();\n\n  const token = `__v3_mask_${Date.now()}_${Math.random().toString(36).slice(2)}`;\n\n  for (const locator of locators) {\n    try {\n      const info = await resolveMaskRects(locator, token);\n      if (!info) continue;\n      const entry = rectsByFrame.get(info.frame) ?? {\n        rects: [],\n        rootTokens: new Set<string>(),\n      };\n      entry.rects.push(...info.rects);\n      for (const rect of info.rects) {\n        if (rect.rootToken) entry.rootTokens.add(rect.rootToken);\n      }\n      rectsByFrame.set(info.frame, entry);\n    } catch {\n      // ignore individual locator failures\n    }\n  }\n\n  if (rectsByFrame.size === 0) {\n    return async () => {};\n  }\n\n  await Promise.all(\n    Array.from(rectsByFrame.entries()).map(([frame, { rects }]) =>\n      frame\n        .evaluate(\n          ({ rects, color, token }) => {\n            try {\n              const doc = document;\n              if (!doc) return;\n              for (const rect of rects) {\n                const defaultRoot = doc.documentElement || doc.body;\n                if (!defaultRoot) return;\n                const root = rect.rootToken\n                  ? doc.querySelector(\n                      `[data-stagehand-mask-root=\"${rect.rootToken}\"]`,\n                    ) || defaultRoot\n                  : defaultRoot;\n                if (!root) continue;\n                if (rect.rootToken) {\n                  try {\n                    const style = window.getComputedStyle(root as Element);\n                    if (style && style.position === \"static\") {\n                      const rootEl = root as HTMLElement;\n                      if (\n                        !rootEl.hasAttribute(\"data-stagehand-mask-root-pos\")\n                      ) {\n                        rootEl.setAttribute(\n                          \"data-stagehand-mask-root-pos\",\n                          rootEl.style.position || \"\",\n                        );\n                      }\n                      rootEl.style.position = \"relative\";\n                    }\n                  } catch {\n                    // ignore\n                  }\n                }\n                const el = doc.createElement(\"div\");\n                el.setAttribute(\"data-stagehand-mask\", token);\n                el.style.position = \"absolute\";\n                el.style.left = `${rect.x}px`;\n                el.style.top = `${rect.y}px`;\n                el.style.width = `${rect.width}px`;\n                el.style.height = `${rect.height}px`;\n                el.style.backgroundColor = color;\n                el.style.pointerEvents = \"none\";\n                el.style.zIndex = \"2147483647\";\n                el.style.opacity = \"1\";\n                el.style.mixBlendMode = \"normal\";\n                (root as Element).appendChild(el);\n              }\n            } catch {\n              // ignore\n            }\n          },\n          { rects, color, token },\n        )\n        .catch(() => {}),\n    ),\n  );\n\n  return async () => {\n    await Promise.all(\n      Array.from(rectsByFrame.entries()).map(([frame, { rootTokens }]) =>\n        frame\n          .evaluate(\n            ({ token, rootTokens }) => {\n              try {\n                const doc = document;\n                if (!doc) return;\n                const nodes = doc.querySelectorAll(\n                  `[data-stagehand-mask=\"${token}\"]`,\n                );\n                nodes.forEach((node) => node.remove());\n                for (const rootToken of rootTokens) {\n                  const root = doc.querySelector(\n                    `[data-stagehand-mask-root=\"${rootToken}\"]`,\n                  ) as HTMLElement | null;\n                  if (!root) continue;\n                  const prev = root.getAttribute(\n                    \"data-stagehand-mask-root-pos\",\n                  );\n                  if (prev !== null) {\n                    root.style.position = prev;\n                    root.removeAttribute(\"data-stagehand-mask-root-pos\");\n                  }\n                  root.removeAttribute(\"data-stagehand-mask-root\");\n                }\n              } catch {\n                // ignore\n              }\n            },\n            { token, rootTokens: Array.from(rootTokens) },\n          )\n          .catch(() => {}),\n      ),\n    );\n  };\n}\n\nasync function resolveMaskRects(\n  locator: Locator,\n  maskToken: string,\n): Promise<{\n  frame: Frame;\n  rects: Array<ScreenshotClip & { rootToken?: string | null }>;\n} | null> {\n  const frame = locator.getFrame();\n  const session = frame.session;\n  try {\n    const resolved: Array<{\n      objectId: Protocol.Runtime.RemoteObjectId;\n      nodeId: Protocol.DOM.NodeId | null;\n    }> = await locator.resolveNodesForMask();\n    const rects: Array<ScreenshotClip & { rootToken?: string | null }> = [];\n\n    for (const { objectId } of resolved) {\n      try {\n        const rect = await resolveMaskRectForObject(\n          session,\n          objectId,\n          maskToken,\n        );\n        if (rect) rects.push(rect);\n      } catch {\n        // ignore individual element failures\n      } finally {\n        await session\n          .send<never>(\"Runtime.releaseObject\", { objectId })\n          .catch(() => {});\n      }\n    }\n\n    if (!rects.length) return null;\n\n    return { frame, rects };\n  } catch {\n    return null;\n  }\n}\n\nasync function resolveMaskRectForObject(\n  session: CDPSessionLike,\n  objectId: Protocol.Runtime.RemoteObjectId,\n  maskToken: string,\n): Promise<(ScreenshotClip & { rootToken?: string | null }) | null> {\n  const result = await session.send<Protocol.Runtime.CallFunctionOnResponse>(\n    \"Runtime.callFunctionOn\",\n    {\n      objectId,\n      functionDeclaration: screenshotScriptSources.resolveMaskRect,\n      arguments: [{ value: maskToken }],\n      returnByValue: true,\n    },\n  );\n\n  if (result.exceptionDetails) {\n    return null;\n  }\n\n  const rect = result.result.value as\n    | (ScreenshotClip & { rootToken?: string | null })\n    | null;\n  if (!rect) return null;\n\n  const { x, y, width, height } = rect;\n  if (\n    !Number.isFinite(x) ||\n    !Number.isFinite(y) ||\n    !Number.isFinite(width) ||\n    !Number.isFinite(height) ||\n    width <= 0 ||\n    height <= 0\n  ) {\n    return null;\n  }\n\n  return {\n    x,\n    y,\n    width,\n    height,\n    rootToken:\n      rect.rootToken && typeof rect.rootToken === \"string\"\n        ? rect.rootToken\n        : undefined,\n  };\n}\n\nexport async function runScreenshotCleanups(\n  cleanups: ScreenshotCleanup[],\n): Promise<void> {\n  for (let i = cleanups.length - 1; i >= 0; i -= 1) {\n    const fn = cleanups[i];\n    if (!fn) continue;\n    try {\n      const result = fn();\n      if (result && typeof (result as Promise<void>).then === \"function\") {\n        await result;\n      }\n    } catch {\n      // ignore cleanup errors\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/understudy/selectorResolver.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport {\n  locatorScriptBootstrap,\n  locatorScriptGlobalRefs,\n  type LocatorScriptName,\n} from \"../dom/build/locatorScripts.generated.js\";\nimport { v3Logger } from \"../logger.js\";\nimport type { Frame } from \"./frame.js\";\nimport { executionContexts } from \"./executionContextRegistry.js\";\n\nexport type SelectorQuery =\n  | { kind: \"css\"; value: string }\n  | { kind: \"text\"; value: string }\n  | { kind: \"xpath\"; value: string };\n\nexport interface ResolvedNode {\n  objectId: Protocol.Runtime.RemoteObjectId;\n  nodeId: Protocol.DOM.NodeId | null;\n}\n\nexport interface ResolveManyOptions {\n  limit?: number;\n}\n\nexport class FrameSelectorResolver {\n  constructor(private readonly frame: Frame) {}\n\n  public static parseSelector(raw: string): SelectorQuery {\n    const trimmed = raw.trim();\n\n    const isText = /^text=/i.test(trimmed);\n    const looksLikeXPath =\n      /^xpath=/i.test(trimmed) ||\n      trimmed.startsWith(\"/\") ||\n      trimmed.startsWith(\"(\");\n    const isCssPrefixed = /^css=/i.test(trimmed);\n\n    if (isText) {\n      let value = trimmed.replace(/^text=/i, \"\").trim();\n      if (\n        (value.startsWith('\"') && value.endsWith('\"')) ||\n        (value.startsWith(\"'\") && value.endsWith(\"'\"))\n      ) {\n        value = value.slice(1, -1);\n      }\n      return { kind: \"text\", value };\n    }\n\n    if (looksLikeXPath) {\n      const value = trimmed.replace(/^xpath=/i, \"\");\n      return { kind: \"xpath\", value };\n    }\n\n    let selector = isCssPrefixed ? trimmed.replace(/^css=/i, \"\") : trimmed;\n    if (selector.includes(\">>\")) {\n      selector = selector\n        .split(\">>\")\n        .map((piece) => piece.trim())\n        .filter(Boolean)\n        .join(\" \");\n    }\n\n    return { kind: \"css\", value: selector };\n  }\n\n  public async resolveFirst(\n    query: SelectorQuery,\n  ): Promise<ResolvedNode | null> {\n    return this.resolveAtIndex(query, 0);\n  }\n\n  public async resolveAll(\n    query: SelectorQuery,\n    { limit = Infinity }: ResolveManyOptions = {},\n  ): Promise<ResolvedNode[]> {\n    if (limit <= 0) return [];\n    switch (query.kind) {\n      case \"css\":\n        return this.resolveCss(query.value, limit);\n      case \"text\":\n        return this.resolveText(query.value, limit);\n      case \"xpath\":\n        return this.resolveXPath(query.value, limit);\n      default:\n        return [];\n    }\n  }\n\n  public async count(query: SelectorQuery): Promise<number> {\n    switch (query.kind) {\n      case \"css\":\n        return this.countCss(query.value);\n      case \"text\":\n        return this.countText(query.value);\n      case \"xpath\":\n        return this.countXPath(query.value);\n      default:\n        return 0;\n    }\n  }\n\n  public async resolveAtIndex(\n    query: SelectorQuery,\n    index: number,\n  ): Promise<ResolvedNode | null> {\n    if (index < 0 || !Number.isFinite(index)) return null;\n    const results = await this.resolveAll(query, { limit: index + 1 });\n    return results[index] ?? null;\n  }\n\n  private buildLocatorInvocation(\n    name: LocatorScriptName,\n    args: string[],\n  ): string {\n    const call = `${locatorScriptGlobalRefs[name]}(${args.join(\", \")})`;\n    return `(() => { ${locatorScriptBootstrap}; return ${call}; })()`;\n  }\n\n  private async resolveCss(\n    selector: string,\n    limit: number,\n  ): Promise<ResolvedNode[]> {\n    if (limit <= 0) return [];\n\n    const session = this.frame.session;\n    const { executionContextId } = await session.send<{\n      executionContextId: Protocol.Runtime.ExecutionContextId;\n    }>(\"Page.createIsolatedWorld\", {\n      frameId: this.frame.frameId,\n      worldName: \"v3-world\",\n    });\n\n    const ctxId = await executionContexts.waitForMainWorld(\n      session,\n      this.frame.frameId,\n      1000,\n    );\n\n    const results: ResolvedNode[] = [];\n    let loggedFallback = false;\n\n    for (let index = 0; index < limit; index += 1) {\n      const primaryExpr = this.buildLocatorInvocation(\"resolveCssSelector\", [\n        JSON.stringify(selector),\n        String(index),\n      ]);\n      const primary = await this.evaluateElement(\n        primaryExpr,\n        executionContextId,\n      );\n      if (primary) {\n        results.push(primary);\n        continue;\n      }\n\n      if (!loggedFallback) {\n        v3Logger({\n          category: \"locator\",\n          message: \"css pierce-fallback\",\n          level: 2,\n          auxiliary: {\n            frameId: { value: String(this.frame.frameId), type: \"string\" },\n            selector: { value: selector, type: \"string\" },\n          },\n        });\n        loggedFallback = true;\n      }\n\n      const fallbackExpr = this.buildLocatorInvocation(\n        \"resolveCssSelectorPierce\",\n        [JSON.stringify(selector), String(index)],\n      );\n      const fallback = await this.evaluateElement(fallbackExpr, ctxId);\n      if (fallback) {\n        results.push(fallback);\n        continue;\n      }\n\n      break;\n    }\n\n    return results;\n  }\n\n  private async resolveText(\n    value: string,\n    limit: number,\n  ): Promise<ResolvedNode[]> {\n    if (limit <= 0) return [];\n\n    const session = this.frame.session;\n    const ctxId = await executionContexts.waitForMainWorld(\n      session,\n      this.frame.frameId,\n      1000,\n    );\n\n    const results: ResolvedNode[] = [];\n    for (let index = 0; index < limit; index += 1) {\n      const expr = this.buildLocatorInvocation(\"resolveTextSelector\", [\n        JSON.stringify(value),\n        String(index),\n      ]);\n      const resolved = await this.evaluateElement(expr, ctxId);\n      if (!resolved) break;\n      results.push(resolved);\n    }\n\n    return results;\n  }\n\n  private async resolveXPath(\n    value: string,\n    limit: number,\n  ): Promise<ResolvedNode[]> {\n    if (limit <= 0) return [];\n\n    const session = this.frame.session;\n    const ctxId = await executionContexts.waitForMainWorld(\n      session,\n      this.frame.frameId,\n      1000,\n    );\n\n    const results: ResolvedNode[] = [];\n    for (let index = 0; index < limit; index += 1) {\n      const expr = this.buildLocatorInvocation(\"resolveXPathMainWorld\", [\n        JSON.stringify(value),\n        String(index),\n      ]);\n      const resolved = await this.evaluateElement(expr, ctxId);\n      if (!resolved) break;\n      results.push(resolved);\n    }\n\n    return results;\n  }\n\n  private async countCss(selector: string): Promise<number> {\n    const session = this.frame.session;\n\n    const { executionContextId } = await session.send<{\n      executionContextId: Protocol.Runtime.ExecutionContextId;\n    }>(\"Page.createIsolatedWorld\", {\n      frameId: this.frame.frameId,\n      worldName: \"v3-world\",\n    });\n\n    const primaryExpr = this.buildLocatorInvocation(\"countCssMatchesPrimary\", [\n      JSON.stringify(selector),\n    ]);\n    const primary = await this.evaluateCount(primaryExpr, executionContextId);\n\n    const ctxId = await executionContexts.waitForMainWorld(\n      session,\n      this.frame.frameId,\n      1000,\n    );\n\n    const fallbackExpr = this.buildLocatorInvocation(\"countCssMatchesPierce\", [\n      JSON.stringify(selector),\n    ]);\n    const fallback = await this.evaluateCount(fallbackExpr, ctxId);\n\n    return Math.max(primary, fallback);\n  }\n\n  private async countText(value: string): Promise<number> {\n    const session = this.frame.session;\n    const ctxId = await executionContexts.waitForMainWorld(\n      session,\n      this.frame.frameId,\n      1000,\n    );\n\n    const expr = this.buildLocatorInvocation(\"countTextMatches\", [\n      JSON.stringify(value),\n    ]);\n\n    try {\n      const evalRes = await session.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression: expr,\n          contextId: ctxId,\n          returnByValue: true,\n          awaitPromise: true,\n        },\n      );\n\n      if (evalRes.exceptionDetails) {\n        const details = evalRes.exceptionDetails;\n        v3Logger({\n          category: \"locator\",\n          message: \"count text evaluate exception\",\n          level: 0,\n          auxiliary: {\n            frameId: { value: String(this.frame.frameId), type: \"string\" },\n            selector: { value: value, type: \"string\" },\n            exception: {\n              value:\n                details.text ??\n                String(\n                  details.exception?.description ??\n                    details.exception?.value ??\n                    \"\",\n                ),\n              type: \"string\",\n            },\n          },\n        });\n        return 0;\n      }\n\n      const data = (evalRes.result.value ?? {}) as {\n        count?: unknown;\n      };\n\n      const num =\n        typeof data.count === \"number\" ? data.count : Number(data.count);\n      if (!Number.isFinite(num)) return 0;\n      return Math.max(0, Math.floor(num));\n    } catch {\n      return 0;\n    }\n  }\n\n  private async countXPath(value: string): Promise<number> {\n    const session = this.frame.session;\n\n    const ctxId = await executionContexts.waitForMainWorld(\n      session,\n      this.frame.frameId,\n      1000,\n    );\n\n    const expr = this.buildLocatorInvocation(\"countXPathMatchesMainWorld\", [\n      JSON.stringify(value),\n    ]);\n\n    try {\n      const evalRes = await session.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression: expr,\n          contextId: ctxId,\n          returnByValue: true,\n          awaitPromise: true,\n        },\n      );\n\n      if (evalRes.exceptionDetails) {\n        return 0;\n      }\n\n      const num =\n        typeof evalRes.result.value === \"number\"\n          ? evalRes.result.value\n          : Number(evalRes.result.value);\n      if (!Number.isFinite(num)) return 0;\n      return Math.max(0, Math.floor(num));\n    } catch {\n      return 0;\n    }\n  }\n\n  private async resolveFromObjectId(\n    objectId: Protocol.Runtime.RemoteObjectId,\n  ): Promise<ResolvedNode | null> {\n    const session = this.frame.session;\n    let nodeId: Protocol.DOM.NodeId | null;\n    try {\n      const rn = await session.send<{ nodeId: Protocol.DOM.NodeId }>(\n        \"DOM.requestNode\",\n        { objectId },\n      );\n      nodeId = rn.nodeId ?? null;\n    } catch {\n      nodeId = null;\n    }\n\n    return { objectId, nodeId };\n  }\n\n  private async evaluateCount(\n    expression: string,\n    contextId: Protocol.Runtime.ExecutionContextId,\n  ): Promise<number> {\n    const session = this.frame.session;\n\n    try {\n      const evalRes = await session.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression,\n          contextId,\n          returnByValue: true,\n          awaitPromise: true,\n        },\n      );\n\n      if (evalRes.exceptionDetails) {\n        return 0;\n      }\n\n      const value = evalRes.result.value;\n      const num = typeof value === \"number\" ? value : Number(value);\n      if (!Number.isFinite(num)) return 0;\n      return Math.max(0, Math.floor(num));\n    } catch {\n      return 0;\n    }\n  }\n\n  private async evaluateElement(\n    expression: string,\n    contextId: Protocol.Runtime.ExecutionContextId,\n  ): Promise<ResolvedNode | null> {\n    const session = this.frame.session;\n\n    try {\n      const evalRes = await session.send<Protocol.Runtime.EvaluateResponse>(\n        \"Runtime.evaluate\",\n        {\n          expression,\n          contextId,\n          returnByValue: false,\n          awaitPromise: true,\n        },\n      );\n\n      if (evalRes.exceptionDetails || !evalRes.result.objectId) {\n        return null;\n      }\n\n      return this.resolveFromObjectId(evalRes.result.objectId);\n    } catch {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/lib/v3/v3.ts",
    "content": "import fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport process from \"process\";\nimport { v7 as uuidv7 } from \"uuid\";\nimport { z } from \"zod\";\nimport {\n  InferStagehandSchema,\n  StagehandZodSchema,\n  toJsonSchema,\n} from \"./zodCompat.js\";\nimport { loadApiKeyFromEnv } from \"../utils.js\";\nimport { extractModelName } from \"../modelUtils.js\";\nimport { StagehandLogger, LoggerOptions } from \"../logger.js\";\nimport { ActCache } from \"./cache/ActCache.js\";\nimport { AgentCache } from \"./cache/AgentCache.js\";\nimport { CacheStorage } from \"./cache/CacheStorage.js\";\nimport { ActHandler } from \"./handlers/actHandler.js\";\nimport { ExtractHandler } from \"./handlers/extractHandler.js\";\nimport { ObserveHandler } from \"./handlers/observeHandler.js\";\nimport { V3AgentHandler } from \"./handlers/v3AgentHandler.js\";\nimport { V3CuaAgentHandler } from \"./handlers/v3CuaAgentHandler.js\";\nimport { CAPTCHA_CUA_SYSTEM_PROMPT_NOTE } from \"./agent/utils/captchaSolver.js\";\nimport { createBrowserbaseSession } from \"./launch/browserbase.js\";\nimport { launchLocalChrome } from \"./launch/local.js\";\nimport { LLMClient } from \"./llm/LLMClient.js\";\nimport { LLMProvider } from \"./llm/LLMProvider.js\";\nimport {\n  bindInstanceLogger,\n  unbindInstanceLogger,\n  withInstanceLogContext,\n} from \"./logger.js\";\nimport { cleanupLocalBrowser } from \"./shutdown/cleanupLocal.js\";\nimport { startShutdownSupervisor } from \"./shutdown/supervisorClient.js\";\nimport { resolveTools } from \"./mcp/utils.js\";\nimport {\n  ActHandlerParams,\n  ExtractHandlerParams,\n  ObserveHandlerParams,\n  AgentReplayStep,\n  InitState,\n  AgentCacheContext,\n} from \"./types/private/index.js\";\nimport type {\n  ShutdownSupervisorConfig,\n  ShutdownSupervisorHandle,\n} from \"./types/private/shutdown.js\";\nimport {\n  AgentConfig,\n  AgentExecuteCallbacks,\n  AgentExecuteOptions,\n  AgentStreamExecuteOptions,\n  AgentResult,\n  AVAILABLE_CUA_MODELS,\n  LogLine,\n  StagehandMetrics,\n  Action,\n  ActOptions,\n  ActResult,\n  defaultExtractSchema,\n  ExtractOptions,\n  HistoryEntry,\n  ObserveOptions,\n  pageTextSchema,\n  V3FunctionName,\n  AvailableModel,\n  ClientOptions,\n  ModelConfiguration,\n  LocalBrowserLaunchOptions,\n  V3Options,\n  AnyPage,\n  PatchrightPage,\n  PlaywrightPage,\n  PuppeteerPage,\n  CuaModelRequiredError,\n  StagehandInvalidArgumentError,\n  StagehandNotInitializedError,\n  MissingEnvironmentVariableError,\n  StagehandInitError,\n  AgentStreamResult,\n} from \"./types/public/index.js\";\nimport { V3Context } from \"./understudy/context.js\";\nimport { Page } from \"./understudy/page.js\";\nimport { resolveModel } from \"../modelUtils.js\";\nimport { StagehandAPIClient } from \"./api.js\";\nimport { validateExperimentalFeatures } from \"./agent/utils/validateExperimentalFeatures.js\";\nimport { flattenVariables } from \"./agent/utils/variables.js\";\nimport { FlowLogger, type FlowLoggerContext } from \"./flowlogger/FlowLogger.js\";\nimport { EventEmitterWithWildcardSupport } from \"./flowlogger/EventEmitter.js\";\nimport { EventStore } from \"./flowlogger/EventStore.js\";\nimport { createTimeoutGuard } from \"./handlers/handlerUtils/timeoutGuard.js\";\nimport { ActTimeoutError } from \"./types/public/sdkErrors.js\";\n\nconst DEFAULT_MODEL_NAME = \"openai/gpt-4.1-mini\";\nconst DEFAULT_VIEWPORT = { width: 1288, height: 711 };\nconst DEFAULT_AGENT_TOOL_TIMEOUT_MS = 45000;\n\ntype ResolvedModelConfiguration = {\n  modelName: AvailableModel;\n  clientOptions?: ClientOptions;\n};\n\nfunction resolveModelConfiguration(\n  model?: V3Options[\"model\"],\n): ResolvedModelConfiguration {\n  if (!model) {\n    return { modelName: DEFAULT_MODEL_NAME };\n  }\n\n  if (typeof model === \"string\") {\n    return { modelName: model as AvailableModel };\n  }\n\n  if (model && typeof model === \"object\") {\n    const { modelName, ...clientOptions } = model;\n    if (!modelName) {\n      throw new StagehandInvalidArgumentError(\n        \"model.modelName is required when providing client options.\",\n      );\n    }\n    return {\n      modelName,\n      clientOptions: clientOptions as ClientOptions,\n    };\n  }\n\n  return { modelName: DEFAULT_MODEL_NAME };\n}\n\n/**\n * V3\n *\n * Purpose:\n * A high-level orchestrator for Stagehand V3. Abstracts away whether the browser\n * runs **locally via Chrome** or remotely on **Browserbase**, and exposes simple\n * entrypoints (`act`, `extract`, `observe`) that delegate to the corresponding\n * handler classes.\n *\n * Responsibilities:\n * - Bootstraps Chrome or Browserbase, ensures a working CDP WebSocket, and builds a `V3Context`.\n * - Manages lifecycle: init, context access, cleanup.\n * - Bridges external page objects (Playwright/Puppeteer) into internal frameIds for handlers.\n * - Provides a stable API surface for downstream code regardless of runtime environment.\n */\nexport class V3 {\n  private readonly opts: V3Options;\n  private state: InitState = { kind: \"UNINITIALIZED\" };\n  private actHandler: ActHandler | null = null;\n  private extractHandler: ExtractHandler | null = null;\n  private observeHandler: ObserveHandler | null = null;\n  private ctx: V3Context | null = null;\n  public llmClient!: LLMClient;\n\n  /**\n   * Event bus for internal communication.\n   * Emits events like 'screenshot' when screenshots are captured during agent execution.\n   */\n  public readonly bus: EventEmitterWithWildcardSupport =\n    new EventEmitterWithWildcardSupport();\n  private modelName: AvailableModel;\n  private modelClientOptions: ClientOptions;\n  private llmProvider: LLMProvider;\n  private overrideLlmClients: Map<string, LLMClient> = new Map();\n  private readonly domSettleTimeoutMs?: number;\n  private _isClosing = false;\n  public browserbaseSessionId?: string;\n  private browserbaseSessionUrl?: string;\n  private browserbaseDebugUrl?: string;\n  public get browserbaseSessionID(): string | undefined {\n    return this.browserbaseSessionId;\n  }\n  public get browserbaseSessionURL(): string | undefined {\n    return this.browserbaseSessionUrl;\n  }\n  public get browserbaseDebugURL(): string | undefined {\n    return this.browserbaseDebugUrl;\n  }\n  /**\n   * Returns true if the browser is running on Browserbase.\n   */\n  public get isBrowserbase(): boolean {\n    return this.state.kind === \"BROWSERBASE\";\n  }\n\n  /**\n   * Returns true if captcha auto-solving is enabled on Browserbase.\n   * Defaults to true when not explicitly set to false.\n   */\n  public get isCaptchaAutoSolveEnabled(): boolean {\n    return (\n      this.isBrowserbase &&\n      this.opts.browserbaseSessionCreateParams?.browserSettings\n        ?.solveCaptchas !== false\n    );\n  }\n\n  /**\n   * Returns true if advancedStealth is enabled in Browserbase settings.\n   */\n  public get isAdvancedStealth(): boolean {\n    return (\n      this.opts.browserbaseSessionCreateParams?.browserSettings\n        ?.advancedStealth === true\n    );\n  }\n\n  /**\n   * Returns the configured viewport dimensions from launch options.\n   * Falls back to default 1288x711 if not configured.\n   */\n  public get configuredViewport(): { width: number; height: number } {\n    const defaultWidth = 1288;\n    const defaultHeight = 711;\n\n    if (this.opts.env === \"BROWSERBASE\") {\n      const vp =\n        this.opts.browserbaseSessionCreateParams?.browserSettings?.viewport;\n      return {\n        width: vp?.width ?? defaultWidth,\n        height: vp?.height ?? defaultHeight,\n      };\n    }\n\n    // LOCAL env\n    const vp = this.opts.localBrowserLaunchOptions?.viewport;\n    return {\n      width: vp?.width ?? defaultWidth,\n      height: vp?.height ?? defaultHeight,\n    };\n  }\n\n  private _onCdpClosed = (why: string) => {\n    if (this.state.kind === \"BROWSERBASE\") {\n      void this._logBrowserbaseSessionStatus();\n    }\n\n    // Single place to react to the transport closing\n    this._immediateShutdown(`CDP transport closed: ${why}`).catch(() => {});\n  };\n  public readonly experimental: boolean = false;\n  public readonly logInferenceToFile: boolean = false;\n  public readonly disableAPI: boolean = false;\n  private externalLogger?: (logLine: LogLine) => void;\n  public verbose: 0 | 1 | 2 = 1;\n  private stagehandLogger: StagehandLogger;\n  private _history: Array<HistoryEntry> = [];\n  private readonly instanceId: string;\n  private readonly sessionId: string;\n  public readonly eventStore: EventStore;\n  public readonly flowLoggerContext: FlowLoggerContext;\n  private static _processGuardsInstalled = false;\n  private static _instances: Set<V3> = new Set();\n  private cacheStorage: CacheStorage;\n  private actCache: ActCache;\n  private agentCache: AgentCache;\n  private apiClient: StagehandAPIClient | null = null;\n  private keepAlive?: boolean;\n  private shutdownSupervisor: ShutdownSupervisorHandle | null = null;\n\n  public stagehandMetrics: StagehandMetrics = {\n    actPromptTokens: 0,\n    actCompletionTokens: 0,\n    actReasoningTokens: 0,\n    actCachedInputTokens: 0,\n    actInferenceTimeMs: 0,\n    extractPromptTokens: 0,\n    extractCompletionTokens: 0,\n    extractReasoningTokens: 0,\n    extractCachedInputTokens: 0,\n    extractInferenceTimeMs: 0,\n    observePromptTokens: 0,\n    observeCompletionTokens: 0,\n    observeReasoningTokens: 0,\n    observeCachedInputTokens: 0,\n    observeInferenceTimeMs: 0,\n    agentPromptTokens: 0,\n    agentCompletionTokens: 0,\n    agentReasoningTokens: 0,\n    agentCachedInputTokens: 0,\n    agentInferenceTimeMs: 0,\n    totalPromptTokens: 0,\n    totalCompletionTokens: 0,\n    totalReasoningTokens: 0,\n    totalCachedInputTokens: 0,\n    totalInferenceTimeMs: 0,\n  };\n\n  constructor(opts: V3Options) {\n    this.externalLogger = opts.logger;\n    this.verbose = opts.verbose ?? 1;\n    this.instanceId = uuidv7();\n    this.sessionId = opts.sessionId ?? this.instanceId;\n    this.keepAlive =\n      opts.keepAlive ?? opts.browserbaseSessionCreateParams?.keepAlive;\n\n    // Create per-instance StagehandLogger (handles usePino, verbose, externalLogger)\n    // This gives each V3 instance independent logger configuration\n    // while still sharing the underlying Pino worker thread via StagehandLogger.sharedPinoLogger\n    const loggerOptions: LoggerOptions = {\n      pretty: true,\n      level: \"info\", // Most permissive - filtering happens at instance level\n    };\n\n    if (opts.disablePino !== undefined) {\n      loggerOptions.usePino = !opts.disablePino;\n    }\n\n    this.stagehandLogger = new StagehandLogger(loggerOptions, opts.logger);\n    this.stagehandLogger.setVerbosity(this.verbose);\n\n    // Also bind to AsyncLocalStorage for v3Logger() calls from handlers\n    // This maintains backward compatibility with code that uses v3Logger() directly\n    try {\n      if (this.externalLogger) {\n        // Use external logger directly when provided\n        bindInstanceLogger(this.instanceId, this.externalLogger);\n      } else {\n        // Fall back to stagehandLogger when no external logger\n        bindInstanceLogger(this.instanceId, (line) => {\n          this.stagehandLogger.log(line);\n        });\n      }\n    } catch {\n      // ignore\n    }\n    const { modelName, clientOptions } = resolveModelConfiguration(opts.model);\n    this.modelName = modelName;\n    this.experimental = opts.experimental ?? false;\n    this.logInferenceToFile = opts.logInferenceToFile ?? false;\n    this.llmProvider = new LLMProvider(this.logger);\n    this.domSettleTimeoutMs = opts.domSettleTimeout;\n    this.disableAPI = opts.disableAPI ?? false;\n\n    const baseClientOptions: ClientOptions = clientOptions\n      ? ({ ...clientOptions } as ClientOptions)\n      : ({} as ClientOptions);\n    if (opts.llmClient) {\n      this.llmClient = opts.llmClient;\n      this.modelClientOptions = baseClientOptions;\n      this.disableAPI = true;\n    } else {\n      // Ensure API key is set\n      let apiKey = (baseClientOptions as { apiKey?: string }).apiKey;\n      if (!apiKey) {\n        try {\n          apiKey = loadApiKeyFromEnv(\n            this.modelName.split(\"/\")[0], // \"openai\", \"anthropic\", etc\n            this.logger,\n          );\n        } catch (error) {\n          this.logger({\n            category: \"init\",\n            message: `Error loading API key for model ${this.modelName}: ${error}. Continuing without LLM client.`,\n            level: 0,\n          });\n          throw error;\n        }\n      }\n      this.modelClientOptions = {\n        ...baseClientOptions,\n        apiKey,\n      } as ClientOptions;\n\n      // Get the default client for this model\n      this.llmClient = this.llmProvider.getClient(\n        this.modelName,\n        this.modelClientOptions,\n        { experimental: this.experimental, disableAPI: this.disableAPI },\n      );\n    }\n\n    this.cacheStorage = CacheStorage.create(opts.cacheDir, this.logger, {\n      label: \"cache directory\",\n    });\n    this.actCache = new ActCache({\n      storage: this.cacheStorage,\n      logger: this.logger,\n      getActHandler: () => this.actHandler,\n      getDefaultLlmClient: () => this.resolveLlmClient(),\n      domSettleTimeoutMs: this.domSettleTimeoutMs,\n    });\n    this.agentCache = new AgentCache({\n      storage: this.cacheStorage,\n      logger: this.logger,\n      getActHandler: () => this.actHandler,\n      getContext: () => this.ctx,\n      getDefaultLlmClient: () => this.resolveLlmClient(),\n      getBaseModelName: () => this.modelName,\n      getSystemPrompt: () => opts.systemPrompt,\n      domSettleTimeoutMs: this.domSettleTimeoutMs,\n      act: this.act.bind(this),\n    });\n\n    this.opts = opts;\n\n    // FlowLogger always gets a per-instance session context and shared event\n    // bus. The attached EventStore decides which sinks are active:\n    // `BROWSERBASE_FLOW_LOGS=1` enables pretty stderr output,\n    // and `BROWSERBASE_CONFIG_DIR` enables the pretty/jsonl file sinks for this session.\n    this.eventStore = new EventStore(this.sessionId, opts);\n    this.flowLoggerContext = FlowLogger.init(this.sessionId, this.bus);\n    // Flow event pipeline:\n    // FlowLogger -> this.bus -> this.eventStore -> configured sinks/query history.\n    // V3 owns the bus for this session. EventStore is not another bus; it just\n    // receives already-emitted FlowEvents here, then fans them out to sinks and\n    // keeps the queryable per-session history used by /v4/log, parent/ancestor lookups, and tests.\n    // `on()` stores a strong reference to the handler, so the EventStore\n    // stays alive until this bus is garbage-collected with the rest of the V3\n    // object graph.\n    this.bus.on(\"*\", this.eventStore.emit);\n\n    // Track instance for global process guard handling\n    V3._instances.add(this);\n  }\n  /**\n   * Async property for metrics so callers can `await v3.metrics`.\n   * When using API mode, fetches metrics from the API. Otherwise returns local metrics.\n   */\n  public get metrics(): Promise<StagehandMetrics> {\n    if (this.apiClient) {\n      // Fetch metrics from the API\n      return this.apiClient.getReplayMetrics().catch((error) => {\n        this.logger({\n          category: \"metrics\",\n          message: `Failed to fetch metrics from API: ${error}`,\n          level: 0,\n        });\n        // Fall back to local metrics on error\n        return this.stagehandMetrics;\n      });\n    }\n    // Return local metrics wrapped in a Promise for consistency\n    return Promise.resolve(this.stagehandMetrics);\n  }\n\n  private resolveLlmClient(model?: ModelConfiguration): LLMClient {\n    if (!model) {\n      return this.llmClient;\n    }\n\n    let modelName: AvailableModel | string;\n    let clientOptions: ClientOptions | undefined;\n\n    if (typeof model === \"string\") {\n      modelName = model;\n    } else {\n      const { modelName: overrideModelName, ...rest } = model;\n      modelName = overrideModelName;\n      clientOptions = rest as ClientOptions;\n    }\n\n    if (\n      modelName === this.modelName &&\n      (!clientOptions || Object.keys(clientOptions).length === 0)\n    ) {\n      return this.llmClient;\n    }\n\n    const overrideProvider = String(modelName).split(\"/\")[0];\n    const baseProvider = String(this.modelName).split(\"/\")[0];\n\n    const mergedOptions = {\n      ...(overrideProvider === baseProvider ? this.modelClientOptions : {}),\n      ...(clientOptions ?? {}),\n    } as ClientOptions;\n\n    const providerKey = overrideProvider;\n    if (!(mergedOptions as { apiKey?: string }).apiKey) {\n      const apiKey = loadApiKeyFromEnv(providerKey, this.logger);\n      if (apiKey) {\n        (mergedOptions as { apiKey?: string }).apiKey = apiKey;\n      }\n    }\n\n    const cacheKey = JSON.stringify({\n      modelName,\n      clientOptions: mergedOptions,\n    });\n\n    const cached = this.overrideLlmClients.get(cacheKey);\n    if (cached) {\n      return cached;\n    }\n\n    const client = this.llmProvider.getClient(\n      modelName as AvailableModel,\n      mergedOptions,\n      { experimental: this.experimental, disableAPI: this.disableAPI },\n    );\n\n    this.overrideLlmClients.set(cacheKey, client);\n    return client;\n  }\n\n  private beginAgentReplayRecording(): void {\n    this.agentCache.beginRecording();\n  }\n\n  private endAgentReplayRecording(): AgentReplayStep[] {\n    return this.agentCache.endRecording();\n  }\n\n  private discardAgentReplayRecording(): void {\n    this.agentCache.discardRecording();\n  }\n\n  private isAgentReplayRecording(): boolean {\n    return this.agentCache.isRecording();\n  }\n\n  public isAgentReplayActive(): boolean {\n    return this.agentCache.isReplayActive();\n  }\n\n  public recordAgentReplayStep(step: AgentReplayStep): void {\n    this.agentCache.recordStep(step);\n  }\n\n  /**\n   * Async property for history so callers can `await v3.history`.\n   * Returns a frozen copy to avoid external mutation.\n   */\n  public get history(): Promise<ReadonlyArray<HistoryEntry>> {\n    return Promise.resolve(Object.freeze([...this._history]));\n  }\n\n  public addToHistory(\n    method: HistoryEntry[\"method\"],\n    parameters: unknown,\n    result?: unknown,\n  ): void {\n    this._history.push({\n      method,\n      parameters,\n      result: result ?? null,\n      timestamp: new Date().toISOString(),\n    });\n  }\n\n  public updateMetrics(\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ): void {\n    switch (functionName) {\n      case V3FunctionName.ACT:\n        this.stagehandMetrics.actPromptTokens += promptTokens;\n        this.stagehandMetrics.actCompletionTokens += completionTokens;\n        this.stagehandMetrics.actReasoningTokens += reasoningTokens;\n        this.stagehandMetrics.actCachedInputTokens += cachedInputTokens;\n        this.stagehandMetrics.actInferenceTimeMs += inferenceTimeMs;\n        break;\n\n      case V3FunctionName.EXTRACT:\n        this.stagehandMetrics.extractPromptTokens += promptTokens;\n        this.stagehandMetrics.extractCompletionTokens += completionTokens;\n        this.stagehandMetrics.extractReasoningTokens += reasoningTokens;\n        this.stagehandMetrics.extractCachedInputTokens += cachedInputTokens;\n        this.stagehandMetrics.extractInferenceTimeMs += inferenceTimeMs;\n        break;\n\n      case V3FunctionName.OBSERVE:\n        this.stagehandMetrics.observePromptTokens += promptTokens;\n        this.stagehandMetrics.observeCompletionTokens += completionTokens;\n        this.stagehandMetrics.observeReasoningTokens += reasoningTokens;\n        this.stagehandMetrics.observeCachedInputTokens += cachedInputTokens;\n        this.stagehandMetrics.observeInferenceTimeMs += inferenceTimeMs;\n        break;\n\n      case V3FunctionName.AGENT:\n        this.stagehandMetrics.agentPromptTokens += promptTokens;\n        this.stagehandMetrics.agentCompletionTokens += completionTokens;\n        this.stagehandMetrics.agentReasoningTokens += reasoningTokens;\n        this.stagehandMetrics.agentCachedInputTokens += cachedInputTokens;\n        this.stagehandMetrics.agentInferenceTimeMs += inferenceTimeMs;\n        break;\n    }\n    this.updateTotalMetrics(\n      promptTokens,\n      completionTokens,\n      reasoningTokens,\n      cachedInputTokens,\n      inferenceTimeMs,\n    );\n  }\n\n  private updateTotalMetrics(\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ): void {\n    this.stagehandMetrics.totalPromptTokens += promptTokens;\n    this.stagehandMetrics.totalCompletionTokens += completionTokens;\n    this.stagehandMetrics.totalReasoningTokens += reasoningTokens;\n    this.stagehandMetrics.totalCachedInputTokens += cachedInputTokens;\n    this.stagehandMetrics.totalInferenceTimeMs += inferenceTimeMs;\n  }\n\n  private async _immediateShutdown(reason: string): Promise<void> {\n    try {\n      this.logger({\n        category: \"v3\",\n        message: `initiating shutdown → ${reason}`,\n        level: 0,\n      });\n    } catch {\n      //\n    }\n\n    try {\n      this.logger({\n        category: \"v3\",\n        message: `closing resources → ${reason}`,\n        level: 0,\n      });\n      await this.close({ force: true });\n    } catch {\n      // swallow — already shutting down\n    }\n  }\n\n  /** Spawn a crash-only supervisor that cleans up when this process dies. */\n  private startShutdownSupervisor(\n    config: ShutdownSupervisorConfig,\n  ): ShutdownSupervisorHandle | null {\n    if (this.shutdownSupervisor) return this.shutdownSupervisor;\n    this.shutdownSupervisor = startShutdownSupervisor(config, {\n      onError: (error, context) => {\n        try {\n          this.logger({\n            category: \"v3\",\n            message:\n              \"Shutdown supervisor unavailable; crash cleanup disabled. \" +\n              \"If this process exits unexpectedly, local Chrome or Browserbase \" +\n              \"sessions may remain running even with keepAlive=false.\",\n            level: 0,\n            auxiliary: {\n              context: { value: context, type: \"string\" },\n              error: { value: error.message, type: \"string\" },\n            },\n          });\n        } catch {\n          // ignore logging failures\n        }\n      },\n    });\n    return this.shutdownSupervisor;\n  }\n\n  /** Stop the supervisor during a normal shutdown. */\n  private stopShutdownSupervisor(): void {\n    if (!this.shutdownSupervisor) return;\n    try {\n      this.shutdownSupervisor.stop();\n    } catch {\n      // best-effort\n    }\n    this.shutdownSupervisor = null;\n  }\n\n  /**\n   * Entrypoint: initializes handlers, launches Chrome or Browserbase,\n   * and sets up a CDP context.\n   */\n  async init(): Promise<void> {\n    try {\n      return await withInstanceLogContext(this.instanceId, async () => {\n        this.actHandler = new ActHandler(\n          this.llmClient,\n          this.modelName,\n          this.modelClientOptions,\n          (model) => this.resolveLlmClient(model),\n          this.opts.systemPrompt ?? \"\",\n          this.logInferenceToFile,\n          this.opts.selfHeal ?? true,\n          (\n            functionName,\n            promptTokens,\n            completionTokens,\n            reasoningTokens,\n            cachedInputTokens,\n            inferenceTimeMs,\n          ) =>\n            this.updateMetrics(\n              functionName,\n              promptTokens,\n              completionTokens,\n              reasoningTokens,\n              cachedInputTokens,\n              inferenceTimeMs,\n            ),\n          this.domSettleTimeoutMs,\n        );\n        this.extractHandler = new ExtractHandler(\n          this.llmClient,\n          this.modelName,\n          this.modelClientOptions,\n          (model) => this.resolveLlmClient(model),\n          this.opts.systemPrompt ?? \"\",\n          this.logInferenceToFile,\n          this.experimental,\n          (\n            functionName,\n            promptTokens,\n            completionTokens,\n            reasoningTokens,\n            cachedInputTokens,\n            inferenceTimeMs,\n          ) =>\n            this.updateMetrics(\n              functionName,\n              promptTokens,\n              completionTokens,\n              reasoningTokens,\n              cachedInputTokens,\n              inferenceTimeMs,\n            ),\n        );\n        this.observeHandler = new ObserveHandler(\n          this.llmClient,\n          this.modelName,\n          this.modelClientOptions,\n          (model) => this.resolveLlmClient(model),\n          this.opts.systemPrompt ?? \"\",\n          this.logInferenceToFile,\n          this.experimental,\n          (\n            functionName,\n            promptTokens,\n            completionTokens,\n            reasoningTokens,\n            cachedInputTokens,\n            inferenceTimeMs,\n          ) =>\n            this.updateMetrics(\n              functionName,\n              promptTokens,\n              completionTokens,\n              reasoningTokens,\n              cachedInputTokens,\n              inferenceTimeMs,\n            ),\n        );\n        if (this.opts.env === \"LOCAL\") {\n          // chrome-launcher conditionally adds --headless when the environment variable\n          // HEADLESS is set, without parsing its value.\n          // if it is not equal to true, then we delete it from the process\n          const envHeadless = process.env.HEADLESS;\n          if (envHeadless !== undefined) {\n            const normalized = envHeadless.trim().toLowerCase();\n            if (normalized !== \"true\") {\n              delete process.env.HEADLESS;\n            }\n          }\n          const lbo: LocalBrowserLaunchOptions =\n            this.opts.localBrowserLaunchOptions ?? {};\n\n          if (lbo.cdpHeaders && !lbo.cdpUrl) {\n            this.logger({\n              category: \"init\",\n              message:\n                \"`cdpHeaders` was provided but `cdpUrl` is not set — cdpHeaders will be ignored. Set `cdpUrl` to connect to an existing browser via CDP.\",\n              level: 2,\n            });\n          }\n\n          // If a CDP URL is provided, attach instead of launching.\n          if (lbo.cdpUrl) {\n            this.logger({\n              category: \"init\",\n              message: \"Connecting to local browser\",\n              level: 1,\n            });\n            this.ctx = await V3Context.create(lbo.cdpUrl, {\n              env: \"LOCAL\",\n              cdpHeaders: lbo.cdpHeaders,\n            });\n            this.ctx.conn.flowLoggerContext = this.flowLoggerContext;\n            this.ctx.conn.onTransportClosed(this._onCdpClosed);\n            this.state = {\n              kind: \"LOCAL\",\n              // no LaunchedChrome when attaching externally; create a stub kill\n              chrome: {\n                kill: async () => {},\n              } as unknown as import(\"chrome-launcher\").LaunchedChrome,\n              ws: lbo.cdpUrl,\n            };\n            this.resetBrowserbaseSessionMetadata();\n            // Post-connect settings (downloads and viewport) if provided\n            await this._applyPostConnectLocalOptions(lbo);\n            return;\n          }\n          this.logger({\n            category: \"init\",\n            message: \"Launching local browser\",\n            level: 1,\n          });\n\n          // Determine or create user data dir\n          let userDataDir = lbo.userDataDir;\n          let createdTemp = false;\n          if (!userDataDir) {\n            const base = path.join(os.tmpdir(), \"stagehand-v3\");\n            fs.mkdirSync(base, { recursive: true });\n            userDataDir = fs.mkdtempSync(path.join(base, \"profile-\"));\n            createdTemp = true;\n          }\n\n          // Build chrome flags\n          const defaults = [\n            \"--remote-allow-origins=*\",\n            \"--no-first-run\",\n            \"--no-default-browser-check\",\n            \"--disable-dev-shm-usage\",\n            \"--site-per-process\",\n          ];\n          let chromeFlags: string[];\n          const ignore = lbo.ignoreDefaultArgs;\n          if (ignore === true) {\n            // drop defaults\n            chromeFlags = [];\n          } else if (Array.isArray(ignore)) {\n            chromeFlags = defaults.filter(\n              (f) => !ignore.some((ex) => f.includes(ex)),\n            );\n          } else {\n            chromeFlags = [...defaults];\n          }\n\n          // headless handled by launchLocalChrome\n          if (lbo.devtools) chromeFlags.push(\"--auto-open-devtools-for-tabs\");\n          if (lbo.locale) chromeFlags.push(`--lang=${lbo.locale}`);\n          if (!lbo.viewport) {\n            lbo.viewport = DEFAULT_VIEWPORT;\n          }\n          if (lbo.viewport?.width && lbo.viewport?.height) {\n            chromeFlags.push(\n              `--window-size=${lbo.viewport.width},${lbo.viewport.height + 87}`, // Added pixels to the window to account for the address bar\n            );\n          }\n          if (typeof lbo.deviceScaleFactor === \"number\") {\n            chromeFlags.push(\n              `--force-device-scale-factor=${Math.max(0.1, lbo.deviceScaleFactor)}`,\n            );\n          }\n          if (lbo.hasTouch) chromeFlags.push(\"--touch-events=enabled\");\n          if (lbo.ignoreHTTPSErrors)\n            chromeFlags.push(\"--ignore-certificate-errors\");\n          if (lbo.proxy?.server)\n            chromeFlags.push(`--proxy-server=${lbo.proxy.server}`);\n          if (lbo.proxy?.bypass)\n            chromeFlags.push(`--proxy-bypass-list=${lbo.proxy.bypass}`);\n\n          // add user-supplied args last\n          if (Array.isArray(lbo.args)) chromeFlags.push(...lbo.args);\n\n          const keepAlive = this.keepAlive === true;\n          const { ws, chrome } = await launchLocalChrome({\n            chromePath: lbo.executablePath,\n            chromeFlags,\n            port: lbo.port,\n            headless: lbo.headless,\n            userDataDir,\n            connectTimeoutMs: lbo.connectTimeoutMs,\n            handleSIGINT: !keepAlive,\n          });\n          if (keepAlive) {\n            try {\n              chrome.process?.unref?.();\n            } catch {\n              // best-effort: avoid keeping the event loop alive\n            }\n          }\n          this.ctx = await V3Context.create(ws, {\n            env: \"LOCAL\",\n            localBrowserLaunchOptions: lbo,\n          });\n          this.ctx.conn.flowLoggerContext = this.flowLoggerContext;\n          this.ctx.conn.onTransportClosed(this._onCdpClosed);\n          this.state = {\n            kind: \"LOCAL\",\n            chrome,\n            ws,\n            userDataDir,\n            createdTempProfile: createdTemp,\n            preserveUserDataDir: !!lbo.preserveUserDataDir,\n          };\n          this.resetBrowserbaseSessionMetadata();\n          const chromePid = chrome.process?.pid ?? chrome.pid;\n          if (!keepAlive && chromePid) {\n            this.startShutdownSupervisor({\n              kind: \"LOCAL\",\n              pid: chromePid,\n              userDataDir,\n              createdTempProfile: createdTemp,\n              preserveUserDataDir: !!lbo.preserveUserDataDir,\n            });\n          }\n\n          // Post-connect settings (downloads and viewport) if provided\n          await this._applyPostConnectLocalOptions(lbo);\n          return;\n        }\n\n        if (this.opts.env === \"BROWSERBASE\") {\n          const { apiKey, projectId } = this.requireBrowserbaseCreds();\n          this.logger({\n            category: \"init\",\n            message: \"Starting browserbase session\",\n            level: 1,\n          });\n          const baseSessionParams =\n            this.opts.browserbaseSessionCreateParams ?? {};\n          const resolvedKeepAlive = this.keepAlive;\n          const keepAlive = this.keepAlive === true;\n          let effectiveSessionParams = baseSessionParams;\n          if (resolvedKeepAlive !== undefined) {\n            effectiveSessionParams = {\n              ...baseSessionParams,\n              keepAlive: resolvedKeepAlive,\n            };\n          }\n          if (!this.disableAPI && !this.experimental) {\n            this.apiClient = new StagehandAPIClient({\n              apiKey,\n              projectId,\n              logger: this.logger,\n              serverCache: this.opts.serverCache,\n            });\n            const {\n              projectId: overrideProjectId,\n              browserSettings,\n              userMetadata,\n              ...restSessionParams\n            } = effectiveSessionParams;\n            const resolvedProjectId = overrideProjectId ?? projectId;\n            const createSessionPayload = {\n              ...(resolvedProjectId ? { projectId: resolvedProjectId } : {}),\n              ...restSessionParams,\n              browserSettings: {\n                ...(browserSettings ?? {}),\n                viewport: browserSettings?.viewport ?? {\n                  width: 1288,\n                  height: 711,\n                },\n              },\n              userMetadata: {\n                ...(userMetadata ?? {}),\n                stagehand: \"true\",\n              },\n            };\n            const { sessionId, available } = await this.apiClient.init({\n              modelName: this.modelName,\n              modelApiKey: this.modelClientOptions.apiKey,\n              domSettleTimeoutMs: this.domSettleTimeoutMs,\n              verbose: this.verbose,\n              systemPrompt: this.opts.systemPrompt,\n              selfHeal: this.opts.selfHeal,\n              browserbaseSessionCreateParams: createSessionPayload,\n              browserbaseSessionID: this.opts.browserbaseSessionID,\n            });\n            if (!available) {\n              this.apiClient = null;\n            }\n            this.opts.browserbaseSessionID = sessionId;\n          }\n          const { ws, sessionId, bb } = await createBrowserbaseSession(\n            apiKey,\n            projectId,\n            effectiveSessionParams,\n            this.opts.browserbaseSessionID,\n          );\n          this.ctx = await V3Context.create(ws, {\n            env: \"BROWSERBASE\",\n            apiClient: this.apiClient,\n          });\n          this.ctx.conn.flowLoggerContext = this.flowLoggerContext;\n          this.ctx.conn.onTransportClosed(this._onCdpClosed);\n          this.state = { kind: \"BROWSERBASE\", sessionId, ws, bb };\n          this.browserbaseSessionId = sessionId;\n          if (!keepAlive && !this.disableAPI) {\n            this.startShutdownSupervisor({\n              kind: \"STAGEHAND_API\",\n              sessionId,\n              apiKey,\n              projectId,\n            });\n          }\n\n          await this._ensureBrowserbaseDownloadsEnabled();\n\n          const resumed = !!this.opts.browserbaseSessionID;\n          let debugUrl: string | undefined;\n          try {\n            const dbg = (await bb.sessions.debug(sessionId)) as unknown as {\n              debuggerUrl?: string;\n            };\n            debugUrl = dbg?.debuggerUrl;\n          } catch {\n            // Ignore debug fetch failures; continue with sessionUrl only\n          }\n          const sessionUrl = `https://www.browserbase.com/sessions/${sessionId}`;\n          this.browserbaseSessionUrl = sessionUrl;\n          this.browserbaseDebugUrl = debugUrl;\n\n          try {\n            this.logger({\n              category: \"init\",\n              message: resumed\n                ? this.apiClient\n                  ? \"Browserbase session started\"\n                  : \"Browserbase session resumed\"\n                : \"Browserbase session started\",\n              level: 1,\n              auxiliary: {\n                sessionUrl: { value: sessionUrl, type: \"string\" },\n                ...(debugUrl && {\n                  debugUrl: { value: debugUrl, type: \"string\" },\n                }),\n                sessionId: { value: sessionId, type: \"string\" },\n              },\n            });\n          } catch {\n            // best-effort logging — ignore failures\n          }\n          return;\n        }\n\n        const neverEnv: never = this.opts.env;\n        throw new StagehandInitError(`Unsupported env: ${neverEnv}`);\n      });\n    } catch (error) {\n      // Cleanup instanceLoggers map on init failure to prevent memory leak\n      if (this.externalLogger) {\n        try {\n          unbindInstanceLogger(this.instanceId);\n        } catch {\n          // ignore cleanup errors\n        }\n      }\n      throw error;\n    }\n  }\n\n  /** Apply post-connect local browser options that require CDP. */\n  private async _applyPostConnectLocalOptions(\n    lbo: LocalBrowserLaunchOptions,\n  ): Promise<void> {\n    try {\n      // Downloads behavior\n      if (lbo.downloadsPath || lbo.acceptDownloads !== undefined) {\n        const behavior = lbo.acceptDownloads === false ? \"deny\" : \"allow\";\n        await this.ctx?.conn\n          .send(\"Browser.setDownloadBehavior\", {\n            behavior,\n            downloadPath: lbo.downloadsPath,\n            eventsEnabled: true,\n          })\n          .catch(() => {});\n      }\n    } catch {\n      // best-effort only\n    }\n  }\n\n  private async _ensureBrowserbaseDownloadsEnabled(): Promise<void> {\n    const conn = this.ctx?.conn;\n    if (!conn) return;\n    try {\n      await conn.send(\"Browser.setDownloadBehavior\", {\n        behavior: \"allow\",\n        downloadPath: \"downloads\",\n        eventsEnabled: true,\n      });\n    } catch {\n      // best-effort only\n    }\n  }\n\n  private resetBrowserbaseSessionMetadata(): void {\n    this.browserbaseSessionId = undefined;\n    this.browserbaseSessionUrl = undefined;\n    this.browserbaseDebugUrl = undefined;\n  }\n\n  /**\n   * Run an \"act\" instruction through the ActHandler.\n   *\n   * New API:\n   * - act(instruction: string, options?: ActOptions)\n   * - act(action: Action, options?: ActOptions)\n   */\n  async act(instruction: string, options?: ActOptions): Promise<ActResult>;\n  async act(action: Action, options?: ActOptions): Promise<ActResult>;\n\n  @FlowLogger.wrapWithLogging({\n    eventType: \"StagehandAct\",\n  })\n  async act(input: string | Action, options?: ActOptions): Promise<ActResult> {\n    return await withInstanceLogContext(this.instanceId, async () => {\n      if (!this.actHandler) throw new StagehandNotInitializedError(\"act()\");\n\n      let actResult: ActResult;\n\n      if (isObserveResult(input)) {\n        // Resolve page: use provided page if any, otherwise default active page\n        const v3Page = await this.resolvePage(options?.page);\n\n        // Use selector as provided to support XPath, CSS, and other engines\n        const selector = input.selector;\n        if (this.apiClient) {\n          actResult = await this.apiClient.act({\n            input,\n            options,\n            frameId: v3Page.mainFrameId(),\n          });\n        } else {\n          const ensureTimeRemaining = createTimeoutGuard(\n            options?.timeout,\n            (ms) => new ActTimeoutError(ms),\n          );\n          actResult = await this.actHandler.takeDeterministicAction(\n            { ...input, selector },\n            v3Page,\n            this.domSettleTimeoutMs,\n            this.resolveLlmClient(options?.model),\n            ensureTimeRemaining,\n            options?.variables,\n          );\n        }\n\n        // history: record ObserveResult-based act call\n        this.addToHistory(\n          \"act\",\n          {\n            observeResult: input,\n          },\n          actResult,\n        );\n        return actResult;\n      }\n      // instruction path\n      if (typeof input !== \"string\" || !input.trim()) {\n        throw new StagehandInvalidArgumentError(\n          \"act(): instruction string is required unless passing an Action\",\n        );\n      }\n\n      // Resolve page from options or default\n      const page = await this.resolvePage(options?.page);\n      const actCacheLlmClient = options?.model\n        ? this.resolveLlmClient(options.model)\n        : undefined;\n\n      let actCacheContext: Awaited<\n        ReturnType<typeof this.actCache.prepareContext>\n      > | null = null;\n      const canUseCache =\n        typeof input === \"string\" &&\n        !this.isAgentReplayRecording() &&\n        this.actCache.enabled;\n      if (canUseCache) {\n        actCacheContext = await this.actCache.prepareContext(\n          input,\n          page,\n          flattenVariables(options?.variables),\n        );\n        if (actCacheContext) {\n          const cachedResult = await this.actCache.tryReplay(\n            actCacheContext,\n            page,\n            options?.timeout,\n            actCacheLlmClient,\n          );\n          if (cachedResult) {\n            this.addToHistory(\n              \"act\",\n              {\n                instruction: input,\n                variables: options?.variables,\n                timeout: options?.timeout,\n                cacheHit: true,\n              },\n              cachedResult,\n            );\n            return cachedResult;\n          }\n        }\n      }\n\n      const handlerParams: ActHandlerParams = {\n        instruction: input,\n        page,\n        variables: options?.variables,\n        timeout: options?.timeout,\n        model: options?.model,\n      };\n      if (this.apiClient) {\n        const frameId = page.mainFrameId();\n        actResult = await this.apiClient.act({ input, options, frameId });\n      } else {\n        actResult = await this.actHandler.act(handlerParams);\n      }\n      // history: record instruction-based act call (omit page object)\n      this.addToHistory(\n        \"act\",\n        {\n          instruction: input,\n          variables: options?.variables,\n          timeout: options?.timeout,\n        },\n        actResult,\n      );\n\n      if (\n        actCacheContext &&\n        actResult.success &&\n        Array.isArray(actResult.actions) &&\n        actResult.actions.length > 0\n      ) {\n        await this.actCache.store(actCacheContext, actResult);\n      }\n      return actResult;\n    });\n  }\n\n  /**\n   * Run an \"extract\" instruction through the ExtractHandler.\n   *\n   * Accepted forms:\n   * - extract() → pageText\n   * - extract(options) → pageText\n   * - extract(instruction) → defaultExtractSchema\n   * - extract(instruction, schema) → schema-inferred\n   * - extract(instruction, schema, options)\n   */\n\n  async extract(): Promise<z.infer<typeof pageTextSchema>>;\n  async extract(\n    options: ExtractOptions,\n  ): Promise<z.infer<typeof pageTextSchema>>;\n  async extract(\n    instruction: string,\n    options?: ExtractOptions,\n  ): Promise<z.infer<typeof defaultExtractSchema>>;\n  async extract<T extends StagehandZodSchema>(\n    instruction: string,\n    schema: T,\n    options?: ExtractOptions,\n  ): Promise<InferStagehandSchema<T>>;\n\n  @FlowLogger.wrapWithLogging({\n    eventType: \"StagehandExtract\",\n  })\n  async extract(\n    a?: string | ExtractOptions,\n    b?: StagehandZodSchema | ExtractOptions,\n    c?: ExtractOptions,\n  ): Promise<unknown> {\n    return await withInstanceLogContext(this.instanceId, async () => {\n      if (!this.extractHandler) {\n        throw new StagehandNotInitializedError(\"extract()\");\n      }\n\n      // Normalize args\n      let instruction: string | undefined;\n      let schema: StagehandZodSchema | undefined;\n      let options: ExtractOptions | undefined;\n\n      if (typeof a === \"string\") {\n        instruction = a;\n        const isZodSchema = (val: unknown): val is StagehandZodSchema =>\n          !!val &&\n          typeof val === \"object\" &&\n          \"parse\" in val &&\n          \"safeParse\" in val;\n        if (isZodSchema(b)) {\n          schema = b as StagehandZodSchema;\n          options = c as ExtractOptions | undefined;\n        } else {\n          options = b as ExtractOptions | undefined;\n        }\n      } else {\n        // a is options or undefined\n        options = (a as ExtractOptions) || undefined;\n      }\n\n      if (!instruction && schema) {\n        throw new StagehandInvalidArgumentError(\n          \"extract(): schema provided without instruction\",\n        );\n      }\n\n      // If instruction without schema → defaultExtractSchema\n      const effectiveSchema =\n        instruction && !schema ? defaultExtractSchema : schema;\n\n      // Resolve page from options or use active page\n      const page = await this.resolvePage(options?.page);\n\n      const handlerParams: ExtractHandlerParams<StagehandZodSchema> = {\n        instruction,\n        schema: effectiveSchema as StagehandZodSchema | undefined,\n        model: options?.model,\n        timeout: options?.timeout,\n        selector: options?.selector,\n        page,\n      };\n      let result: z.infer<typeof effectiveSchema> | { pageText: string };\n      if (this.apiClient) {\n        const frameId = page.mainFrameId();\n        result = await this.apiClient.extract({\n          instruction: handlerParams.instruction,\n          schema: handlerParams.schema,\n          options,\n          frameId,\n        });\n      } else {\n        result =\n          await this.extractHandler.extract<StagehandZodSchema>(handlerParams);\n      }\n      const historySchemaDescriptor = effectiveSchema\n        ? toJsonSchema(effectiveSchema)\n        : undefined;\n      this.addToHistory(\n        \"extract\",\n        {\n          instruction,\n          selector: options?.selector,\n          timeout: options?.timeout,\n          schema: historySchemaDescriptor,\n        },\n        result,\n      );\n      return result;\n    });\n  }\n\n  /**\n   * Run an \"observe\" instruction through the ObserveHandler.\n   */\n  async observe(): Promise<Action[]>;\n  async observe(options: ObserveOptions): Promise<Action[]>;\n  async observe(\n    instruction: string,\n    options?: ObserveOptions,\n  ): Promise<Action[]>;\n  @FlowLogger.wrapWithLogging({\n    eventType: \"StagehandObserve\",\n  })\n  async observe(\n    a?: string | ObserveOptions,\n    b?: ObserveOptions,\n  ): Promise<Action[]> {\n    return await withInstanceLogContext(this.instanceId, async () => {\n      if (!this.observeHandler) {\n        throw new StagehandNotInitializedError(\"observe()\");\n      }\n\n      // Normalize args\n      let instruction: string | undefined;\n      let options: ObserveOptions | undefined;\n      if (typeof a === \"string\") {\n        instruction = a;\n        options = b;\n      } else {\n        options = a as ObserveOptions | undefined;\n      }\n\n      // Resolve to our internal Page type\n      const page = await this.resolvePage(options?.page);\n\n      const handlerParams: ObserveHandlerParams = {\n        instruction,\n        model: options?.model,\n        timeout: options?.timeout,\n        selector: options?.selector,\n        page: page!,\n      };\n\n      let results: Action[];\n      if (this.apiClient) {\n        const frameId = page.mainFrameId();\n        results = await this.apiClient.observe({\n          instruction,\n          options,\n          frameId,\n        });\n      } else {\n        results = await this.observeHandler.observe(handlerParams);\n      }\n\n      // history: record observe call (omit page object)\n      this.addToHistory(\n        \"observe\",\n        {\n          instruction,\n          timeout: options?.timeout,\n        },\n        results,\n      );\n      return results;\n    });\n  }\n\n  /** Return the browser-level CDP WebSocket endpoint. */\n  connectURL(): string {\n    if (this.state.kind === \"UNINITIALIZED\") {\n      throw new StagehandNotInitializedError(\"connectURL()\");\n    }\n    return this.state.ws;\n  }\n\n  /** Expose the current CDP-backed context. */\n  public get context(): V3Context {\n    return this.ctx;\n  }\n\n  /** Best-effort cleanup of context and launched resources. */\n  async close(opts?: { force?: boolean }): Promise<void> {\n    // If we're already closing and this isn't a forced close, no-op.\n    if (this._isClosing && !opts?.force) return;\n    this._isClosing = true;\n\n    const keepAlive = this.keepAlive === true;\n\n    // Unhook CDP transport close handler BEFORE ending the API session.\n    // apiClient.end() can cause the hosted API to terminate the Browserbase\n    // session, which closes the CDP WebSocket. If the handler is still\n    // registered, _onCdpClosed fires and re-enters close() with force=true,\n    // causing a double-close cascade.\n    try {\n      if (this.ctx?.conn && this._onCdpClosed) {\n        this.ctx.conn.offTransportClosed?.(this._onCdpClosed);\n      }\n    } catch {\n      // ignore\n    }\n\n    // End Browserbase session via API when keepAlive is not enabled\n    if (!keepAlive && this.apiClient) {\n      try {\n        await this.apiClient.end();\n      } catch {\n        // best-effort cleanup\n      }\n    }\n\n    try {\n      // Close session file logger\n      try {\n        await FlowLogger.close(this.flowLoggerContext);\n      } catch {\n        // ignore\n      }\n\n      // Close CDP context\n      try {\n        await this.ctx?.close();\n      } catch {\n        // ignore\n      }\n\n      // Kill local Chrome and clean up temp profile when keepAlive is not enabled\n      if (!keepAlive && this.state.kind === \"LOCAL\") {\n        const localState = this.state;\n        await cleanupLocalBrowser({\n          killChrome: () => localState.chrome.kill(),\n          userDataDir: localState.userDataDir,\n          createdTempProfile: localState.createdTempProfile,\n          preserveUserDataDir: localState.preserveUserDataDir,\n        });\n      }\n    } finally {\n      this.stopShutdownSupervisor();\n\n      // Reset internal state\n      this.state = { kind: \"UNINITIALIZED\" };\n      this.ctx = null;\n      this._isClosing = false;\n      this.resetBrowserbaseSessionMetadata();\n      try {\n        unbindInstanceLogger(this.instanceId);\n      } catch {\n        // ignore\n      }\n      try {\n        await this.eventStore.destroy();\n      } catch {\n        // ignore\n      }\n      try {\n        this.bus.removeAllListeners();\n      } catch {\n        // ignore\n      }\n      this._history = [];\n      this.actHandler = null;\n      this.extractHandler = null;\n      this.observeHandler = null;\n      V3._instances.delete(this);\n    }\n  }\n\n  /**\n   * Resolves the Browserbase API key from options or environment variables.\n   * Returns undefined if no key is found (does not throw).\n   */\n  public get browserbaseApiKey(): string | undefined {\n    return this.opts.apiKey || process.env.BROWSERBASE_API_KEY;\n  }\n\n  /** Guard: ensure Browserbase credentials exist in options. */\n  private requireBrowserbaseCreds(): {\n    apiKey: string;\n    projectId?: string;\n  } {\n    let { apiKey, projectId } = this.opts;\n\n    // Fall back to environment variables if not explicitly provided\n    if (!apiKey)\n      apiKey = process.env.BROWSERBASE_API_KEY ?? process.env.BB_API_KEY;\n    if (!projectId)\n      projectId =\n        process.env.BROWSERBASE_PROJECT_ID ?? process.env.BB_PROJECT_ID;\n\n    if (!apiKey) {\n      throw new MissingEnvironmentVariableError(\n        \"BROWSERBASE_API_KEY\",\n        \"Browserbase\",\n      );\n    }\n\n    // Cache resolved values back into opts for consistency\n    this.opts.apiKey = apiKey;\n    if (projectId) this.opts.projectId = projectId;\n\n    // Informational log\n    this.logger({\n      category: \"init\",\n      message: \"Using Browserbase credentials\",\n      level: 1,\n    });\n\n    return { apiKey, projectId };\n  }\n\n  public get logger(): (logLine: LogLine) => void {\n    // Delegate to per-instance StagehandLogger\n    // StagehandLogger handles: verbosity filtering, usePino selection, external logger routing\n    // This provides per-instance configuration while maintaining shared Pino optimization\n    return (logLine: LogLine) => {\n      const line = { ...logLine, level: logLine.level ?? 1 };\n      this.stagehandLogger.log(line);\n    };\n  }\n\n  /**\n   * Normalize a Playwright/Puppeteer page object into its top frame id,\n   * so handlers can resolve it to a `Page` within our V3Context.\n   */\n  private async resolveTopFrameId(\n    page: PlaywrightPage | PuppeteerPage | PatchrightPage,\n  ): Promise<string> {\n    if (this.isPlaywrightPage(page)) {\n      const cdp = await page.context().newCDPSession(page);\n      const { frameTree } = await cdp.send(\"Page.getFrameTree\");\n      return frameTree.frame.id;\n    }\n\n    if (this.isPatchrightPage(page)) {\n      const cdp = await page.context().newCDPSession(page);\n      const { frameTree } = await cdp.send(\"Page.getFrameTree\");\n      return frameTree.frame.id;\n    }\n\n    if (this.isPuppeteerPage(page)) {\n      const cdp = await page.createCDPSession();\n      const { frameTree } = await cdp.send(\"Page.getFrameTree\");\n      this.logger({\n        category: \"v3\",\n        message: \"Puppeteer frame id\",\n        level: 2,\n        auxiliary: { frameId: { value: frameTree.frame.id, type: \"string\" } },\n      });\n      return frameTree.frame.id;\n    }\n\n    throw new StagehandInvalidArgumentError(\n      \"Unsupported page object passed to V3.act()\",\n    );\n  }\n\n  private isPlaywrightPage(p: unknown): p is PlaywrightPage {\n    return (\n      typeof p === \"object\" &&\n      p !== null &&\n      typeof (p as PlaywrightPage).context === \"function\"\n    );\n  }\n\n  private isPatchrightPage(p: unknown): p is PatchrightPage {\n    return (\n      typeof p === \"object\" &&\n      p !== null &&\n      typeof (p as PatchrightPage).context === \"function\"\n    );\n  }\n\n  private isPuppeteerPage(p: unknown): p is PuppeteerPage {\n    return (\n      typeof p === \"object\" &&\n      p !== null &&\n      typeof (p as PuppeteerPage).target === \"function\"\n    );\n  }\n\n  /** Resolve an external page reference or fall back to the active V3 page. */\n  private async resolvePage(page?: AnyPage): Promise<Page> {\n    if (page) {\n      return await this.normalizeToV3Page(page);\n    }\n    const ctx = this.ctx;\n    if (!ctx) {\n      throw new StagehandNotInitializedError(\"resolvePage()\");\n    }\n    return await ctx.awaitActivePage();\n  }\n\n  private async normalizeToV3Page(input: AnyPage): Promise<Page> {\n    if (input instanceof (await import(\"./understudy/page.js\")).Page) {\n      return input as Page;\n    }\n    if (this.isPlaywrightPage(input)) {\n      const frameId = await this.resolveTopFrameId(input);\n      const page = this.ctx!.resolvePageByMainFrameId(frameId);\n      if (!page)\n        throw new StagehandInitError(\n          \"Failed to resolve V3 Page from Playwright page.\",\n        );\n      return page;\n    }\n    if (this.isPatchrightPage(input)) {\n      const frameId = await this.resolveTopFrameId(input);\n      const page = this.ctx!.resolvePageByMainFrameId(frameId);\n      if (!page)\n        throw new StagehandInitError(\n          \"Failed to resolve V3 Page from Patchright page.\",\n        );\n      return page;\n    }\n    if (this.isPuppeteerPage(input)) {\n      const frameId = await this.resolveTopFrameId(input);\n      const page = this.ctx!.resolvePageByMainFrameId(frameId);\n      if (!page)\n        throw new StagehandInitError(\n          \"Failed to resolve V3 Page from Puppeteer page.\",\n        );\n      return page;\n    }\n    throw new StagehandInvalidArgumentError(\"Unsupported page object.\");\n  }\n\n  private async _logBrowserbaseSessionStatus(): Promise<void> {\n    if (this.state.kind !== \"BROWSERBASE\") {\n      return;\n    }\n\n    try {\n      const snapshot = (await this.state.bb.sessions.retrieve(\n        this.state.sessionId,\n      )) as { id?: string; status?: string };\n      if (!snapshot?.status) return;\n\n      const sessionId = snapshot.id ?? this.state.sessionId;\n      const message =\n        snapshot.status === \"TIMED_OUT\"\n          ? `Browserbase session timed out (sessionId: ${sessionId})`\n          : `Browserbase session status: ${snapshot.status}`;\n\n      this.logger({\n        category: \"v3\",\n        message,\n        level: 0,\n      });\n    } catch {\n      // Ignore failures; nothing to log\n    }\n  }\n\n  /**\n   * Prepares shared context for agent execution (both execute and stream).\n   * Extracts duplicated setup logic into a single helper.\n   */\n  private async prepareAgentExecution(\n    options: AgentConfig | undefined,\n    instructionOrOptions:\n      | string\n      | AgentExecuteOptions\n      | AgentStreamExecuteOptions,\n    agentConfigSignature: string,\n  ): Promise<{\n    handler: V3AgentHandler;\n    resolvedOptions: AgentExecuteOptions | AgentStreamExecuteOptions;\n    instruction: string;\n    cacheContext: AgentCacheContext | null;\n    llmClient: LLMClient;\n  }> {\n    // Note: experimental validation is done at the call site before this method\n    // Warn if mode is not explicitly set (defaults to \"dom\")\n    if (options?.mode === undefined) {\n      this.logger({\n        category: \"agent\",\n        message:\n          \"Using agent in default DOM mode (legacy). Agent will default to 'hybrid' on an upcoming release for improved performance.\\n  → https://docs.stagehand.dev/v3/basics/agent\\n\",\n        level: 0,\n      });\n    }\n\n    const tools = options?.integrations\n      ? await resolveTools(options.integrations, options.tools)\n      : (options?.tools ?? {});\n\n    const agentLlmClient = options?.model\n      ? this.resolveLlmClient(options.model)\n      : this.llmClient;\n\n    const resolvedExecutionModel = options?.executionModel ?? options?.model;\n\n    const handler = new V3AgentHandler(\n      this,\n      this.logger,\n      agentLlmClient,\n      resolvedExecutionModel,\n      options?.systemPrompt,\n      tools,\n      options?.mode,\n      this.isCaptchaAutoSolveEnabled,\n    );\n\n    const resolvedOptions: AgentExecuteOptions | AgentStreamExecuteOptions =\n      typeof instructionOrOptions === \"string\"\n        ? {\n            instruction: instructionOrOptions,\n            toolTimeout: DEFAULT_AGENT_TOOL_TIMEOUT_MS,\n          }\n        : {\n            ...instructionOrOptions,\n            toolTimeout:\n              instructionOrOptions.toolTimeout ?? DEFAULT_AGENT_TOOL_TIMEOUT_MS,\n          };\n\n    const callbacksWithSafety = resolvedOptions.callbacks as\n      | AgentExecuteCallbacks\n      | undefined;\n    if (callbacksWithSafety?.onSafetyConfirmation) {\n      throw new StagehandInvalidArgumentError(\n        'onSafetyConfirmation callback is only supported when using mode: \"cua\" agents.',\n      );\n    }\n\n    if (resolvedOptions.page) {\n      const normalizedPage = await this.normalizeToV3Page(resolvedOptions.page);\n      this.ctx!.setActivePage(normalizedPage);\n    }\n\n    const instruction = resolvedOptions.instruction.trim();\n    const sanitizedOptions =\n      this.agentCache.sanitizeExecuteOptions(resolvedOptions);\n\n    const cacheVariables = flattenVariables(resolvedOptions.variables);\n\n    const cacheContext = this.agentCache.shouldAttemptCache(instruction)\n      ? await this.agentCache.prepareContext({\n          instruction,\n          options: sanitizedOptions,\n          configSignature: agentConfigSignature,\n          page: await this.ctx!.awaitActivePage(),\n          variables: cacheVariables,\n        })\n      : null;\n\n    return {\n      handler,\n      resolvedOptions,\n      instruction,\n      cacheContext,\n      llmClient: agentLlmClient,\n    };\n  }\n\n  /**\n   * Create a v3 agent instance (AISDK tool-based) with execute().\n   * Mirrors the v2 Stagehand.agent() tool mode (no CUA provider here).\n   *\n   * @overload When stream: true, returns a streaming agent where execute() returns AgentStreamResult\n   * @overload When stream is false/undefined, returns a non-streaming agent where execute() returns AgentResult\n   */\n  agent(options: AgentConfig & { stream: true }): {\n    execute: (\n      instructionOrOptions: string | AgentStreamExecuteOptions,\n    ) => Promise<AgentStreamResult>;\n  };\n  agent(options?: AgentConfig & { stream?: false }): {\n    execute: (\n      instructionOrOptions: string | AgentExecuteOptions,\n    ) => Promise<AgentResult>;\n  };\n  agent(options?: AgentConfig): {\n    execute: (\n      instructionOrOptions:\n        | string\n        | AgentExecuteOptions\n        | AgentStreamExecuteOptions,\n    ) => Promise<AgentResult | AgentStreamResult>;\n  } {\n    // Determine if CUA mode is enabled (via mode: \"cua\" or deprecated cua: true)\n    const isCuaMode =\n      options?.mode !== undefined\n        ? options.mode === \"cua\"\n        : options?.cua === true;\n\n    // Emit deprecation warning for cua: true\n    if (options?.cua === true) {\n      this.logger({\n        category: \"agent\",\n        message:\n          '[DEPRECATED] The \"cua: true\" option is deprecated. Use \"mode: \\'cua\\'\" instead. This option will be removed in a future version.',\n        level: 0,\n      });\n      console.warn(\n        '[Stagehand] DEPRECATED: The \"cua: true\" option is deprecated. Use \"mode: \\'cua\\'\" instead.',\n      );\n    }\n\n    this.logger({\n      category: \"agent\",\n      message: \"Creating v3 agent instance\",\n      level: 1,\n      auxiliary: {\n        cua: { value: isCuaMode ? \"true\" : \"false\", type: \"boolean\" },\n        mode: { value: options?.mode ?? \"dom\", type: \"string\" },\n        model: {\n          value: extractModelName(options?.model) ?? this.llmClient.modelName,\n          type: \"string\",\n        },\n        systemPrompt: { value: options?.systemPrompt ?? \"\", type: \"string\" },\n        tools: { value: JSON.stringify(options?.tools ?? {}), type: \"object\" },\n        ...(options?.integrations && {\n          integrations: {\n            value: JSON.stringify(options.integrations),\n            type: \"object\",\n          },\n        }),\n      },\n    });\n\n    // If CUA mode is enabled (via mode: \"cua\" or deprecated cua: true), use the computer-use agent path\n    if (isCuaMode) {\n      // Validate agent config at creation time (includes CUA+streaming conflict check)\n      validateExperimentalFeatures({\n        isExperimental: this.experimental,\n        agentConfig: options,\n      });\n\n      const modelToUse = options?.model || {\n        modelName: this.modelName,\n        ...this.modelClientOptions,\n      };\n\n      const { modelName, isCua, clientOptions } = resolveModel(modelToUse);\n\n      if (!isCua) {\n        throw new CuaModelRequiredError(AVAILABLE_CUA_MODELS);\n      }\n\n      const agentConfigSignature =\n        this.agentCache.buildConfigSignature(options);\n      const execute = async (\n        instructionOrOptions: string | AgentExecuteOptions,\n      ): Promise<AgentResult> =>\n        withInstanceLogContext(\n          this.instanceId,\n          async (): Promise<AgentResult> => {\n            validateExperimentalFeatures({\n              isExperimental: this.experimental,\n              agentConfig: options,\n              executeOptions:\n                typeof instructionOrOptions === \"object\"\n                  ? instructionOrOptions\n                  : null,\n            });\n\n            const tools = options?.integrations\n              ? await resolveTools(options.integrations, options.tools)\n              : (options?.tools ?? {});\n\n            const handler = new V3CuaAgentHandler(\n              this,\n              this.logger,\n              {\n                modelName,\n                clientOptions,\n                userProvidedInstructions:\n                  (options.systemPrompt ??\n                    `You are a helpful assistant that can use a web browser.\\nDo not ask follow up questions, the user will trust your judgement.`) +\n                  (this.isCaptchaAutoSolveEnabled\n                    ? CAPTCHA_CUA_SYSTEM_PROMPT_NOTE\n                    : \"\"),\n              },\n              tools,\n            );\n\n            const resolvedOptions: AgentExecuteOptions =\n              typeof instructionOrOptions === \"string\"\n                ? {\n                    instruction: instructionOrOptions,\n                    toolTimeout: DEFAULT_AGENT_TOOL_TIMEOUT_MS,\n                  }\n                : {\n                    ...instructionOrOptions,\n                    toolTimeout:\n                      instructionOrOptions.toolTimeout ??\n                      DEFAULT_AGENT_TOOL_TIMEOUT_MS,\n                  };\n            if (resolvedOptions.page) {\n              const normalizedPage = await this.normalizeToV3Page(\n                resolvedOptions.page,\n              );\n              this.ctx!.setActivePage(normalizedPage);\n            }\n            const instruction = resolvedOptions.instruction.trim();\n            const sanitizedOptions =\n              this.agentCache.sanitizeExecuteOptions(resolvedOptions);\n\n            const cacheVariables = flattenVariables(resolvedOptions.variables);\n\n            let cacheContext: AgentCacheContext | null = null;\n            if (this.agentCache.shouldAttemptCache(instruction)) {\n              const startPage = await this.ctx!.awaitActivePage();\n              cacheContext = await this.agentCache.prepareContext({\n                instruction,\n                options: sanitizedOptions,\n                configSignature: agentConfigSignature,\n                page: startPage,\n                variables: cacheVariables,\n              });\n              if (cacheContext) {\n                const replayed = await this.agentCache.tryReplay(cacheContext);\n                if (replayed) {\n                  return replayed;\n                }\n              }\n            }\n\n            let agentSteps: AgentReplayStep[] = [];\n            const shouldRecordLocally =\n              !!cacheContext && (!this.apiClient || this.experimental);\n            if (shouldRecordLocally) {\n              this.beginAgentReplayRecording();\n            }\n\n            let result: AgentResult;\n            try {\n              if (this.apiClient && !this.experimental) {\n                const page = await this.ctx!.awaitActivePage();\n                result = await this.apiClient.agentExecute(\n                  options,\n                  resolvedOptions,\n                  page.mainFrameId(),\n                  !!cacheContext,\n                );\n                if (cacheContext) {\n                  const transferredEntry =\n                    this.apiClient.consumeLatestAgentCacheEntry();\n                  await this.agentCache.storeTransferredEntry(transferredEntry);\n                }\n              } else {\n                result = await handler.execute(instructionOrOptions);\n              }\n              if (shouldRecordLocally) {\n                agentSteps = this.endAgentReplayRecording();\n              }\n\n              if (\n                shouldRecordLocally &&\n                cacheContext &&\n                result.success &&\n                agentSteps.length > 0\n              ) {\n                await this.agentCache.store(cacheContext, agentSteps, result);\n              }\n\n              return result;\n            } catch (err) {\n              if (shouldRecordLocally) this.discardAgentReplayRecording();\n              throw err;\n            } finally {\n              if (shouldRecordLocally) {\n                this.discardAgentReplayRecording();\n              }\n            }\n          },\n        );\n      return {\n        execute: FlowLogger.wrapWithLogging({\n          eventType: \"AgentExecute\",\n          context: this.flowLoggerContext,\n        })(execute),\n      };\n    }\n\n    // Default: AISDK tools-based agent\n    const agentConfigSignature = this.agentCache.buildConfigSignature(options);\n    const isStreaming = options?.stream ?? false;\n    const execute = async (\n      instructionOrOptions:\n        | string\n        | AgentExecuteOptions\n        | AgentStreamExecuteOptions,\n    ): Promise<AgentResult | AgentStreamResult> =>\n      withInstanceLogContext(\n        this.instanceId,\n        async (): Promise<AgentResult | AgentStreamResult> => {\n          validateExperimentalFeatures({\n            isExperimental: this.experimental,\n            agentConfig: options,\n            executeOptions:\n              typeof instructionOrOptions === \"object\"\n                ? instructionOrOptions\n                : null,\n            isStreaming,\n          });\n\n          // Streaming mode\n          if (isStreaming) {\n            const { handler, resolvedOptions, cacheContext, llmClient } =\n              await this.prepareAgentExecution(\n                options,\n                instructionOrOptions,\n                agentConfigSignature,\n              );\n\n            if (cacheContext) {\n              const replayed = await this.agentCache.tryReplayAsStream(\n                cacheContext,\n                llmClient,\n              );\n              if (replayed) {\n                return replayed;\n              }\n            }\n\n            const streamResult = await handler.stream(\n              resolvedOptions as AgentStreamExecuteOptions,\n            );\n\n            if (cacheContext) {\n              const wrappedStream = this.agentCache.wrapStreamForCaching(\n                cacheContext,\n                streamResult,\n                () => this.beginAgentReplayRecording(),\n                () => this.endAgentReplayRecording(),\n                () => this.discardAgentReplayRecording(),\n              );\n              return wrappedStream;\n            }\n\n            return streamResult;\n          }\n\n          // Non-streaming mode (default)\n          const { handler, resolvedOptions, cacheContext, llmClient } =\n            await this.prepareAgentExecution(\n              options,\n              instructionOrOptions,\n              agentConfigSignature,\n            );\n\n          if (cacheContext) {\n            const replayed = await this.agentCache.tryReplay(\n              cacheContext,\n              llmClient,\n            );\n            if (replayed) {\n              return replayed;\n            }\n          }\n\n          let agentSteps: AgentReplayStep[] = [];\n          const shouldRecordLocally =\n            !!cacheContext && (!this.apiClient || this.experimental);\n          if (shouldRecordLocally) {\n            this.beginAgentReplayRecording();\n          }\n          let result: AgentResult;\n\n          try {\n            if (this.apiClient && !this.experimental) {\n              const page = await this.ctx!.awaitActivePage();\n              result = await this.apiClient.agentExecute(\n                options ?? {},\n                resolvedOptions as AgentExecuteOptions,\n                page.mainFrameId(),\n                !!cacheContext,\n              );\n              if (cacheContext) {\n                const transferredEntry =\n                  this.apiClient.consumeLatestAgentCacheEntry();\n                await this.agentCache.storeTransferredEntry(transferredEntry);\n              }\n            } else {\n              result = await handler.execute(\n                resolvedOptions as AgentExecuteOptions,\n              );\n            }\n            if (shouldRecordLocally) {\n              agentSteps = this.endAgentReplayRecording();\n            }\n\n            if (\n              shouldRecordLocally &&\n              cacheContext &&\n              result.success &&\n              agentSteps.length > 0\n            ) {\n              await this.agentCache.store(cacheContext, agentSteps, result);\n            }\n\n            return result;\n          } catch (err) {\n            if (shouldRecordLocally) this.discardAgentReplayRecording();\n            throw err;\n          } finally {\n            if (shouldRecordLocally) {\n              this.discardAgentReplayRecording();\n            }\n          }\n        },\n      );\n    return {\n      execute: FlowLogger.wrapWithLogging({\n        eventType: \"AgentExecute\",\n        context: this.flowLoggerContext,\n      })(execute),\n    };\n  }\n}\n\nfunction isObserveResult(v: unknown): v is Action {\n  return (\n    !!v && typeof v === \"object\" && \"selector\" in (v as Record<string, unknown>)\n  );\n}\n"
  },
  {
    "path": "packages/core/lib/v3/zodCompat.ts",
    "content": "import { z } from \"zod\";\nimport type {\n  ZodObject as Zod4Object,\n  ZodRawShape as Zod4RawShape,\n  ZodTypeAny as Zod4TypeAny,\n} from \"zod\";\nimport zodToJsonSchema from \"zod-to-json-schema\";\nimport type * as z3 from \"zod/v3\";\nexport type StagehandZodSchema = Zod4TypeAny | z3.ZodTypeAny;\n\nexport type StagehandZodObject =\n  | Zod4Object<Zod4RawShape>\n  | z3.ZodObject<z3.ZodRawShape>;\n\nexport type InferStagehandSchema<T extends StagehandZodSchema> =\n  T extends z3.ZodTypeAny\n    ? z3.infer<T>\n    : T extends Zod4TypeAny\n      ? z.infer<T>\n      : never;\n\nexport const isZod4Schema = (\n  schema: StagehandZodSchema,\n): schema is Zod4TypeAny & { _zod: unknown } =>\n  typeof (schema as { _zod?: unknown })._zod !== \"undefined\";\n\nexport const isZod3Schema = (\n  schema: StagehandZodSchema,\n): schema is z3.ZodTypeAny => !isZod4Schema(schema);\n\nexport type JsonSchemaDocument = Record<string, unknown>;\n\nexport function toJsonSchema(schema: StagehandZodSchema): JsonSchemaDocument {\n  if (!isZod4Schema(schema)) {\n    return zodToJsonSchema(schema);\n  }\n\n  // For v4 schemas, use built-in z.toJSONSchema() method\n  const zodWithJsonSchema = z as typeof z & {\n    toJSONSchema?: (schema: Zod4TypeAny) => JsonSchemaDocument;\n  };\n\n  if (zodWithJsonSchema.toJSONSchema) {\n    return zodWithJsonSchema.toJSONSchema(schema as Zod4TypeAny);\n  }\n\n  // This should never happen with Zod v4.1+\n  throw new Error(\"Zod v4 toJSONSchema method not found\");\n}\n"
  },
  {
    "path": "packages/core/lib/v3Evaluator.ts",
    "content": "/**\n * V3Evaluator mirrors Evaluator but operates on a V3 instance instead of Stagehand.\n * It uses the V3 page/screenshot APIs and constructs an LLM client to run\n * structured evaluations (YES/NO with reasoning) on screenshots and/or text.\n */\n\nimport { z } from \"zod\";\nimport type { AvailableModel, ClientOptions } from \"./v3/types/public/model.js\";\nimport type {\n  EvaluateOptions,\n  BatchAskOptions,\n  EvaluationResult,\n} from \"./v3/types/private/evaluator.js\";\nimport { LLMParsedResponse } from \"./inference.js\";\nimport { LLMResponse, LLMClient } from \"./v3/llm/LLMClient.js\";\nimport { LogLine } from \"./v3/types/public/logs.js\";\nimport { V3 } from \"./v3/v3.js\";\nimport { LLMProvider } from \"./v3/llm/LLMProvider.js\";\nimport { StagehandInvalidArgumentError } from \"./v3/types/public/sdkErrors.js\";\n\nconst EvaluationSchema = z.object({\n  evaluation: z.enum([\"YES\", \"NO\"]),\n  reasoning: z.string(),\n});\n\nconst BatchEvaluationSchema = z.array(EvaluationSchema);\n\nexport class V3Evaluator {\n  private v3: V3;\n  private modelName: AvailableModel;\n  private modelClientOptions: ClientOptions | { apiKey: string };\n  private silentLogger: (message: LogLine) => void = () => {};\n\n  constructor(\n    v3: V3,\n    modelName?: AvailableModel,\n    modelClientOptions?: ClientOptions,\n  ) {\n    this.v3 = v3;\n    this.modelName = modelName || (\"google/gemini-2.5-flash\" as AvailableModel);\n    this.modelClientOptions = modelClientOptions || {\n      apiKey:\n        process.env.GEMINI_API_KEY ||\n        process.env.GOOGLE_GENERATIVE_AI_API_KEY ||\n        \"\",\n    };\n  }\n\n  private getClient(): LLMClient {\n    // Prefer a dedicated provider so we can override model per-evaluation\n    const provider = new LLMProvider(this.v3.logger);\n    return provider.getClient(this.modelName, this.modelClientOptions);\n  }\n\n  async ask(options: EvaluateOptions): Promise<EvaluationResult> {\n    const {\n      question,\n      answer,\n      screenshot = true,\n      systemPrompt,\n      screenshotDelayMs = 250,\n      agentReasoning,\n    } = options;\n    if (!question)\n      throw new StagehandInvalidArgumentError(\n        \"Question cannot be an empty string\",\n      );\n    if (!answer && !screenshot)\n      throw new StagehandInvalidArgumentError(\n        \"Either answer (text) or screenshot must be provided\",\n      );\n\n    if (Array.isArray(screenshot)) {\n      return this._evaluateWithMultipleScreenshots({\n        question,\n        screenshots: screenshot,\n        systemPrompt,\n        agentReasoning,\n      });\n    }\n\n    const defaultSystemPrompt = `You are an expert evaluator that confidently returns YES or NO based on if the original goal was achieved. You have access to  ${screenshot ? \"a screenshot\" : \"the agents reasoning and actions throughout the task\"} that you can use to evaluate the tasks completion. Provide detailed reasoning for your answer.\\n          Today's date is ${new Date().toLocaleDateString()}`;\n\n    await new Promise((r) => setTimeout(r, screenshotDelayMs));\n    let imageBuffer: Buffer | undefined;\n    if (screenshot) {\n      const page = await this.v3.context.awaitActivePage();\n      imageBuffer = await page.screenshot({ fullPage: false });\n    }\n\n    const llmClient = this.getClient();\n\n    const response = await llmClient.createChatCompletion<\n      LLMParsedResponse<LLMResponse>\n    >({\n      logger: this.silentLogger,\n      options: {\n        messages: [\n          { role: \"system\", content: systemPrompt || defaultSystemPrompt },\n          {\n            role: \"user\",\n            content: [\n              {\n                type: \"text\",\n                text: agentReasoning\n                  ? `Question: ${question}\\n\\nAgent's reasoning and actions taken:\\n${agentReasoning}`\n                  : question,\n              },\n              ...(screenshot && imageBuffer\n                ? [\n                    {\n                      type: \"image_url\" as const,\n                      image_url: {\n                        url: `data:image/jpeg;base64,${imageBuffer.toString(\"base64\")}`,\n                      },\n                    },\n                  ]\n                : []),\n              ...(answer\n                ? [{ type: \"text\" as const, text: `the answer is ${answer}` }]\n                : []),\n            ],\n          },\n        ],\n        response_model: { name: \"EvaluationResult\", schema: EvaluationSchema },\n      },\n    });\n\n    try {\n      const result = response.data as unknown as z.infer<\n        typeof EvaluationSchema\n      >;\n      return { evaluation: result.evaluation, reasoning: result.reasoning };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      return {\n        evaluation: \"INVALID\",\n        reasoning: `Failed to get structured response: ${errorMessage}`,\n      } as const;\n    }\n  }\n\n  async batchAsk(options: BatchAskOptions): Promise<EvaluationResult[]> {\n    const {\n      questions,\n      screenshot = true,\n      systemPrompt = \"You are an expert evaluator that returns YES or NO with a concise reasoning.\",\n      screenshotDelayMs = 250,\n    } = options;\n    if (!questions?.length)\n      throw new StagehandInvalidArgumentError(\n        \"Questions array cannot be empty\",\n      );\n\n    await new Promise((r) => setTimeout(r, screenshotDelayMs));\n    let imageBuffer: Buffer | undefined;\n    if (screenshot) {\n      const page = await this.v3.context.awaitActivePage();\n      imageBuffer = await page.screenshot({ fullPage: false });\n    }\n\n    const llmClient = this.getClient();\n\n    const formatted = questions\n      .map(\n        (item, i) =>\n          `${i + 1}. ${item.question}${item.answer ? `\\n   Answer: ${item.answer}` : \"\"}`,\n      )\n      .join(\"\\n\\n\");\n\n    const response = await llmClient.createChatCompletion<\n      LLMParsedResponse<LLMResponse>\n    >({\n      logger: this.silentLogger,\n      options: {\n        messages: [\n          {\n            role: \"system\",\n            content: `${systemPrompt}\\n\\nYou will be given multiple questions${screenshot ? \" with a screenshot\" : \"\"}. ${questions.some((q) => q.answer) ? \"Some questions include answers to evaluate.\" : \"\"} Answer each question by returning an object in the specified JSON format. Return a single JSON array containing one object for each question in the order they were asked.`,\n          },\n          {\n            role: \"user\",\n            content: [\n              { type: \"text\", text: formatted },\n              ...(screenshot && imageBuffer\n                ? [\n                    {\n                      type: \"image_url\" as const,\n                      image_url: {\n                        url: `data:image/jpeg;base64,${imageBuffer.toString(\"base64\")}`,\n                      },\n                    },\n                  ]\n                : []),\n            ],\n          },\n        ],\n        response_model: {\n          name: \"BatchEvaluationResult\",\n          schema: BatchEvaluationSchema,\n        },\n      },\n    });\n\n    try {\n      const results = response.data as unknown as z.infer<\n        typeof BatchEvaluationSchema\n      >;\n      return results.map((r) => ({\n        evaluation: r.evaluation,\n        reasoning: r.reasoning,\n      }));\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      return questions.map(() => ({\n        evaluation: \"INVALID\" as const,\n        reasoning: `Failed to get structured response: ${errorMessage}`,\n      }));\n    }\n  }\n\n  private async _evaluateWithMultipleScreenshots(options: {\n    question: string;\n    screenshots: Buffer[];\n    systemPrompt?: string;\n    agentReasoning?: string;\n  }): Promise<EvaluationResult> {\n    const {\n      question,\n      screenshots,\n      agentReasoning,\n      systemPrompt = `You are an expert evaluator that confidently returns YES or NO given a question and multiple screenshots showing the progression of a task.\n        ${agentReasoning ? \"You also have access to the agent's detailed reasoning and thought process throughout the task.\" : \"\"}\n        Analyze ALL screenshots to understand the complete journey. Look for evidence of task completion across all screenshots, not just the last one.\n        Success criteria may appear at different points in the sequence (confirmation messages, intermediate states, etc).\n        ${agentReasoning ? \"The agent's reasoning provides crucial context about what actions were attempted, what was observed, and the decision-making process. Use this alongside the visual evidence to make a comprehensive evaluation.\" : \"\"}\n        Today's date is ${new Date().toLocaleDateString()}`,\n    } = options;\n\n    if (!question)\n      throw new StagehandInvalidArgumentError(\n        \"Question cannot be an empty string\",\n      );\n    if (!screenshots || screenshots.length === 0)\n      throw new StagehandInvalidArgumentError(\n        \"At least one screenshot must be provided\",\n      );\n\n    const llmClient = this.getClient();\n\n    const imageContents = screenshots.map((s) => ({\n      type: \"image_url\" as const,\n      image_url: { url: `data:image/jpeg;base64,${s.toString(\"base64\")}` },\n    }));\n\n    const response = await llmClient.createChatCompletion<\n      LLMParsedResponse<LLMResponse>\n    >({\n      logger: this.silentLogger,\n      options: {\n        messages: [\n          { role: \"system\", content: systemPrompt },\n          {\n            role: \"user\",\n            content: [\n              {\n                type: \"text\",\n                text: agentReasoning\n                  ? `Question: ${question}\\n\\nAgent's reasoning and actions throughout the task:\\n${agentReasoning}\\n\\nI'm providing ${screenshots.length} screenshots showing the progression of the task. Please analyze both the agent's reasoning and all screenshots to determine if the task was completed successfully.`\n                  : `${question}\\n\\nI'm providing ${screenshots.length} screenshots showing the progression of the task. Please analyze all of them to determine if the task was completed successfully.`,\n              },\n              ...imageContents,\n            ],\n          },\n        ],\n        response_model: { name: \"EvaluationResult\", schema: EvaluationSchema },\n      },\n    });\n\n    try {\n      const result = response.data as unknown as z.infer<\n        typeof EvaluationSchema\n      >;\n      return { evaluation: result.evaluation, reasoning: result.reasoning };\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      return {\n        evaluation: \"INVALID\",\n        reasoning: `Failed to get structured response: ${errorMessage}`,\n      } as const;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@browserbasehq/stagehand\",\n  \"version\": \"3.2.0\",\n  \"description\": \"An AI web browsing framework focused on simplicity and extensibility.\",\n  \"type\": \"module\",\n  \"main\": \"./dist/cjs/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/esm/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/esm/index.d.ts\",\n      \"import\": \"./dist/esm/index.js\",\n      \"require\": \"./dist/cjs/index.js\"\n    },\n    \"./cli\": {\n      \"types\": \"./dist/esm/lib/v3/cli.d.ts\",\n      \"import\": \"./dist/esm/lib/v3/cli.js\",\n      \"require\": \"./dist/cjs/cli.js\"\n    },\n    \"./*.js\": {\n      \"types\": \"./dist/esm/*.d.ts\",\n      \"import\": \"./dist/esm/*.js\"\n    },\n    \"./*\": {\n      \"types\": \"./dist/esm/*.d.ts\",\n      \"import\": \"./dist/esm/*.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"engines\": {\n    \"node\": \"^20.19.0 || >=22.12.0\"\n  },\n  \"scripts\": {\n    \"gen-version\": \"tsx scripts/gen-version.ts\",\n    \"build-dom-scripts\": \"pnpm run --parallel \\\"/^build-dom-scripts:(dom|locator|screenshot|a11y)$/\\\"\",\n    \"build-dom-scripts:dom\": \"tsx lib/v3/dom/genDomScripts.ts\",\n    \"build-dom-scripts:locator\": \"tsx lib/v3/dom/genLocatorScripts.ts\",\n    \"build-dom-scripts:screenshot\": \"tsx lib/v3/dom/genScreenshotScripts.ts\",\n    \"build-dom-scripts:a11y\": \"tsx lib/v3/dom/genA11yScripts.ts\",\n    \"build:cjs\": \"tsx scripts/build-cjs.ts\",\n    \"build:esm\": \"tsx scripts/build-esm.ts\",\n    \"build\": \"pnpm --filter @browserbasehq/stagehand run --parallel \\\"/^build:(esm|cjs)$/\\\"\",\n    \"example\": \"node --import tsx -e \\\"const args=process.argv.slice(1).filter(a=>a!=='--'); const [p]=args; const n=(p||'example').replace(/^\\\\.\\\\//,'').replace(/\\\\.ts$/i,''); import('node:path').then(path=>import(new URL(path.resolve('examples', n + '.ts'), 'file:')));\\\" --\",\n    \"test\": \"pnpm -w --dir ../.. exec turbo run test:core test:e2e --filter=@browserbasehq/stagehand --\",\n    \"test:core\": \"tsx scripts/test-core.ts\",\n    \"test:e2e\": \"tsx scripts/test-e2e.ts\",\n    \"format\": \"prettier --write .\",\n    \"typecheck\": \"pnpm -w --dir ../.. exec tsc -p packages/core/tsconfig.json --noEmit\",\n    \"eslint\": \"eslint .\",\n    \"lint\": \"cd ../.. && prettier --check packages/core && cd packages/core && pnpm run eslint && pnpm run typecheck\"\n  },\n  \"files\": [\n    \"dist/esm\",\n    \"dist/cjs\"\n  ],\n  \"keywords\": [\n    \"ai\",\n    \"browser\",\n    \"automation\",\n    \"web-scraping\",\n    \"testing\"\n  ],\n  \"author\": \"Browserbase\",\n  \"license\": \"MIT\",\n  \"peerDependencies\": {\n    \"deepmerge\": \"^4.3.1\",\n    \"zod\": \"^3.25.76 || ^4.2.0\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/provider\": \"^2.0.0\",\n    \"@anthropic-ai/sdk\": \"0.39.0\",\n    \"@browserbasehq/sdk\": \"^2.7.0\",\n    \"@google/genai\": \"^1.22.0\",\n    \"@langchain/openai\": \"^0.4.4\",\n    \"@modelcontextprotocol/sdk\": \"^1.17.2\",\n    \"ai\": \"^5.0.133\",\n    \"devtools-protocol\": \"^0.0.1464554\",\n    \"fetch-cookie\": \"^3.1.0\",\n    \"openai\": \"^4.87.1\",\n    \"pino\": \"^9.6.0\",\n    \"pino-pretty\": \"^13.0.0\",\n    \"uuid\": \"^11.1.0\",\n    \"ws\": \"^8.18.0\",\n    \"zod-to-json-schema\": \"^3.25.0\"\n  },\n  \"optionalDependencies\": {\n    \"@ai-sdk/amazon-bedrock\": \"^3.0.73\",\n    \"@ai-sdk/anthropic\": \"^2.0.34\",\n    \"@ai-sdk/azure\": \"^2.0.54\",\n    \"@ai-sdk/cerebras\": \"^1.0.25\",\n    \"@ai-sdk/deepseek\": \"^1.0.23\",\n    \"@ai-sdk/google\": \"^2.0.53\",\n    \"@ai-sdk/google-vertex\": \"^3.0.70\",\n    \"@ai-sdk/groq\": \"^2.0.24\",\n    \"@ai-sdk/mistral\": \"^2.0.19\",\n    \"@ai-sdk/openai\": \"^2.0.53\",\n    \"@ai-sdk/perplexity\": \"^2.0.13\",\n    \"@ai-sdk/togetherai\": \"^1.0.23\",\n    \"@ai-sdk/xai\": \"^2.0.26\",\n    \"@langchain/core\": \"^0.3.80\",\n    \"bufferutil\": \"^4.0.9\",\n    \"chrome-launcher\": \"^1.2.0\",\n    \"ollama-ai-provider-v2\": \"^1.5.0\",\n    \"patchright-core\": \"^1.55.2\",\n    \"playwright\": \"^1.52.0\",\n    \"playwright-core\": \"^1.54.1\",\n    \"puppeteer-core\": \"^22.8.0\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.42.1\",\n    \"@types/adm-zip\": \"^0.5.7\",\n    \"@types/jsdom\": \"^27.0.0\",\n    \"@types/node\": \"^20.11.30\",\n    \"@types/ws\": \"^8.5.13\",\n    \"@vitest/coverage-v8\": \"^4.0.8\",\n    \"adm-zip\": \"^0.5.16\",\n    \"chalk\": \"^5.4.1\",\n    \"eslint\": \"10.0.2\",\n    \"jsdom\": \"^24.0.0\",\n    \"playwright\": \"^1.52.0\",\n    \"playwright-core\": \"^1.54.1\",\n    \"prettier\": \"^3.2.5\",\n    \"tsx\": \"*\",\n    \"vitest\": \"^4.0.8\",\n    \"zod\": \"^3.25.76 || ^4.2.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/browserbase/stagehand.git\",\n    \"directory\": \"packages/core\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/browserbase/stagehand/issues\"\n  },\n  \"homepage\": \"https://stagehand.dev\"\n}\n"
  },
  {
    "path": "packages/core/scripts/build-cjs.ts",
    "content": "/**\n * Build canonical dist/ (CJS) output for the core package (including tests).\n *\n * Prereqs: pnpm install; run gen-version + build-dom-scripts first (turbo handles).\n * Args: none.\n * Env: none.\n * Example: pnpm run build:cjs\n */\nimport fs from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"../lib/v3/runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nconst runNodeScript = (scriptPath: string, args: string[]) => {\n  const result = spawnSync(process.execPath, [scriptPath, ...args], {\n    stdio: \"inherit\",\n    cwd: repoRoot,\n  });\n  if (result.error) {\n    console.error(`Failed to run node ${scriptPath} ${args.join(\" \")}`);\n    console.error(result.error);\n    process.exit(1);\n  }\n  if (result.status !== 0) {\n    process.exit(result.status ?? 1);\n  }\n};\n\nfs.rmSync(`${repoRoot}/packages/core/dist/cjs`, {\n  recursive: true,\n  force: true,\n});\nfs.mkdirSync(`${repoRoot}/packages/core/dist/cjs`, { recursive: true });\n\nrunNodeScript(`${repoRoot}/node_modules/typescript/bin/tsc`, [\n  \"-p\",\n  \"packages/core/tsconfig.json\",\n  \"--module\",\n  \"commonjs\",\n  \"--declaration\",\n  \"--outDir\",\n  \"packages/core/dist/cjs\",\n]);\n\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/cjs/index.js`,\n  `\"use strict\";\nmodule.exports = require(\"./lib/v3/index.js\");\n`,\n);\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/cjs/cli.js`,\n  `#!/usr/bin/env node\n\"use strict\";\nrequire(\"./lib/v3/cli.js\");\n`,\n);\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/cjs/index.d.ts`,\n  `export * from \"./lib/v3/index\";\nexport { default } from \"./lib/v3/index\";\n`,\n);\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/cjs/package.json`,\n  '{\\n  \"type\": \"commonjs\"\\n}\\n',\n);\n\nfs.mkdirSync(`${repoRoot}/packages/core/dist/cjs/lib/v3/dom/build`, {\n  recursive: true,\n});\nif (fs.existsSync(`${repoRoot}/packages/core/lib/v3/dom/build`)) {\n  for (const file of fs.readdirSync(\n    `${repoRoot}/packages/core/lib/v3/dom/build`,\n  )) {\n    if (file.endsWith(\".js\")) {\n      fs.copyFileSync(\n        `${repoRoot}/packages/core/lib/v3/dom/build/${file}`,\n        `${repoRoot}/packages/core/dist/cjs/lib/v3/dom/build/${file}`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/scripts/build-esm.ts",
    "content": "/**\n * Build canonical dist/esm output for the core package (including tests).\n *\n * Prereqs: pnpm install; run gen-version + build-dom-scripts first (turbo handles).\n * Args: none.\n * Env: none.\n * Example: pnpm run build:esm\n */\nimport fs from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"../lib/v3/runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nconst runNodeScript = (scriptPath: string, args: string[]) => {\n  const result = spawnSync(process.execPath, [scriptPath, ...args], {\n    stdio: \"inherit\",\n    cwd: repoRoot,\n  });\n  if (result.error) {\n    console.error(`Failed to run node ${scriptPath} ${args.join(\" \")}`);\n    console.error(result.error);\n    process.exit(1);\n  }\n  if (result.status !== 0) {\n    process.exit(result.status ?? 1);\n  }\n};\n\nfs.rmSync(`${repoRoot}/packages/core/dist/esm`, {\n  recursive: true,\n  force: true,\n});\n\n// Core ESM emit includes generated lib/version.ts from gen-version (run in core build).\nrunNodeScript(`${repoRoot}/node_modules/typescript/bin/tsc`, [\n  \"-p\",\n  \"packages/core/tsconfig.json\",\n  \"--declaration\",\n]);\n\nfs.mkdirSync(`${repoRoot}/packages/core/dist/esm`, { recursive: true });\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/esm/package.json`,\n  '{\\n  \"type\": \"module\"\\n}\\n',\n);\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/esm/index.js`,\n  `export * from \"./lib/v3/index.js\";\nexport { default } from \"./lib/v3/index.js\";\n`,\n);\nfs.writeFileSync(\n  `${repoRoot}/packages/core/dist/esm/index.d.ts`,\n  `export * from \"./lib/v3/index.js\";\nexport { default } from \"./lib/v3/index.js\";\n`,\n);\n\nfs.mkdirSync(`${repoRoot}/packages/core/dist/esm/lib/v3/dom/build`, {\n  recursive: true,\n});\n// DOM script bundles are generated artifacts (not TS emit); copy into dist/esm for runtime.\nif (fs.existsSync(`${repoRoot}/packages/core/lib/v3/dom/build`)) {\n  for (const file of fs.readdirSync(\n    `${repoRoot}/packages/core/lib/v3/dom/build`,\n  )) {\n    if (file.endsWith(\".js\")) {\n      fs.copyFileSync(\n        `${repoRoot}/packages/core/lib/v3/dom/build/${file}`,\n        `${repoRoot}/packages/core/dist/esm/lib/v3/dom/build/${file}`,\n      );\n    }\n  }\n}\n\n// Note: evals + server test outputs are built by their respective packages.\n"
  },
  {
    "path": "packages/core/scripts/coverage.ts",
    "content": "/**\n * Coverage merge (V8 -> Istanbul).\n *\n * Prereqs: V8 coverage JSON files in `coverage/**` (from test scripts).\n * Args: `merge` only.\n * Env: none required.\n * Example: pnpm run coverage:merge\n */\nimport fs from \"node:fs\";\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport normalizeV8Coverage from \"./normalize-v8-coverage.js\";\nimport { getRepoRootDir } from \"../lib/v3/runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\nconst command = process.argv[2];\nconst terminationSignals: NodeJS.Signals[] = [\"SIGINT\", \"SIGTERM\"];\nconst log = (message: string) => console.log(`[coverage:merge] ${message}`);\n\nlet activeChild: ChildProcess | null = null;\nlet isCancelling = false;\n\nconst exitCodeForSignal = (signal: NodeJS.Signals): number =>\n  signal === \"SIGINT\" ? 130 : 143;\n\nconst handleTermination = (signal: NodeJS.Signals) => {\n  isCancelling = true;\n  log(`received ${signal}, exiting`);\n  if (activeChild && activeChild.pid && !activeChild.killed) {\n    activeChild.kill(signal);\n  }\n  process.exit(exitCodeForSignal(signal));\n};\n\nterminationSignals.forEach((signal) => {\n  process.once(signal, () => handleTermination(signal));\n});\n\nconst assertNotCancelling = () => {\n  if (isCancelling) {\n    throw new Error(\"Coverage merge cancelled\");\n  }\n};\n\nif (!command || command !== \"merge\") {\n  console.error(\"Usage: coverage merge\");\n  process.exit(1);\n}\n\nif (!process.env.V8_COVERAGE_SCAN_LIMIT) {\n  process.env.V8_COVERAGE_SCAN_LIMIT = \"2000\";\n}\nfs.rmSync(`${repoRoot}/coverage/merged`, { recursive: true, force: true });\nfs.rmSync(`${repoRoot}/coverage/.v8-tmp`, { recursive: true, force: true });\nlog(`normalizing v8 coverage in ${repoRoot}/coverage`);\nlog(`using V8_COVERAGE_SCAN_LIMIT=${process.env.V8_COVERAGE_SCAN_LIMIT}`);\nconst normalizeStart = Date.now();\nawait normalizeV8Coverage(`${repoRoot}/coverage`);\nlog(`normalize completed in ${Date.now() - normalizeStart}ms`);\nconst collectV8CoverageFiles = (dir: string): string[] => {\n  const results: string[] = [];\n  if (!fs.existsSync(dir)) return results;\n  const walk = (current: string) => {\n    assertNotCancelling();\n    const entries = fs.readdirSync(current, { withFileTypes: true });\n    for (const entry of entries) {\n      assertNotCancelling();\n      const fullPath = `${current}/${entry.name}`;\n      if (entry.isDirectory()) {\n        if (entry.name === \".v8-tmp\" || entry.name === \"merged\") {\n          continue;\n        }\n        walk(fullPath);\n        continue;\n      }\n      if (!entry.isFile() || !entry.name.endsWith(\".json\")) continue;\n      try {\n        const raw = fs.readFileSync(fullPath, \"utf8\");\n        if (!raw.trim()) continue;\n        const parsed = JSON.parse(raw) as { result?: unknown };\n        if (parsed?.result) results.push(fullPath);\n      } catch {\n        // ignore invalid JSON in coverage dir\n      }\n    }\n  };\n  walk(dir);\n  return results;\n};\n\nconst v8CoverageFiles = collectV8CoverageFiles(`${repoRoot}/coverage`);\nif (v8CoverageFiles.length === 0) {\n  console.log(\"No V8 coverage files found.\");\n  process.exit(0);\n}\nlog(`found ${v8CoverageFiles.length} v8 coverage files`);\n\nfs.mkdirSync(`${repoRoot}/coverage/merged`, { recursive: true });\nfs.rmSync(`${repoRoot}/coverage/.v8-tmp`, { recursive: true, force: true });\nfs.mkdirSync(`${repoRoot}/coverage/.v8-tmp`, { recursive: true });\nv8CoverageFiles.forEach((file, index) => {\n  assertNotCancelling();\n  fs.copyFileSync(file, `${repoRoot}/coverage/.v8-tmp/coverage-${index}.json`);\n});\nlog(`copied files to ${repoRoot}/coverage/.v8-tmp`);\n\nconst runC8Report = async () => {\n  assertNotCancelling();\n  log(\"running c8 report merge\");\n  const args = [\n    \"exec\",\n    \"c8\",\n    \"report\",\n    \"--temp-directory\",\n    `${repoRoot}/coverage/.v8-tmp`,\n    \"--merge-async\",\n    \"--reporter=html\",\n    \"--reporter=lcov\",\n    \"--reporter=json\",\n    \"--reporter=text-summary\",\n    \"--reports-dir\",\n    `${repoRoot}/coverage/merged`,\n    \"--cwd\",\n    repoRoot,\n    \"--include\",\n    \"packages/**\",\n    \"--exclude\",\n    \"**/node_modules/**\",\n    \"--exclude\",\n    \"**/dist/**\",\n    \"--exclude\",\n    \"**/examples/**\",\n    \"--exclude\",\n    \"**/scripts/**\",\n    \"--exclude\",\n    \"packages/**/test/**\",\n    \"--exclude\",\n    \"packages/**/tests/**\",\n    \"--exclude\",\n    \"packages/**/examples/**\",\n    \"--exclude\",\n    \"packages/**/lib/**/tests/**\",\n    \"--exclude\",\n    \"packages/**/scripts/**\",\n    \"--exclude-after-remap\",\n    \"--exclude\",\n    \"**/*.d.ts\",\n  ];\n  let stdout = \"\";\n\n  const status = await new Promise<number>((resolve, reject) => {\n    const child = spawn(\"pnpm\", args, {\n      cwd: repoRoot,\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n    });\n    activeChild = child;\n\n    child.stdout?.on(\"data\", (chunk) => {\n      const text = String(chunk);\n      stdout += text;\n      process.stdout.write(text);\n    });\n    child.stderr?.on(\"data\", (chunk) => {\n      process.stderr.write(String(chunk));\n    });\n\n    child.once(\"error\", (error) => {\n      activeChild = null;\n      reject(error);\n    });\n    child.once(\"close\", (code) => {\n      activeChild = null;\n      resolve(code ?? 1);\n    });\n  });\n\n  if (stdout) {\n    fs.writeFileSync(\n      `${repoRoot}/coverage/merged/coverage-summary.txt`,\n      stdout,\n    );\n  }\n  log(`c8 report completed with status ${status}`);\n  return status;\n};\n\ntry {\n  const status = await runC8Report();\n  process.exit(status);\n} catch (error) {\n  const message = error instanceof Error ? error.message : String(error);\n  if (!isCancelling) {\n    console.error(`Failed to run c8 coverage report: ${message}`);\n  }\n  process.exit(1);\n}\n"
  },
  {
    "path": "packages/core/scripts/gen-version.ts",
    "content": "import { readFileSync, writeFileSync } from \"node:fs\";\nimport { getPackageRootDir } from \"../lib/v3/runtimePaths.js\";\n\ntype PackageJson = { version: string };\n\nconst packageRoot = getPackageRootDir();\nconst pkgPath = `${packageRoot}/package.json`;\nconst pkg: PackageJson = JSON.parse(readFileSync(pkgPath, \"utf8\"));\n\nconst fullVersion: `${string}` = pkg.version;\n\nconst banner = `/**\n * AUTO-GENERATED — DO NOT EDIT BY HAND\n *  Run \\`pnpm run gen-version\\` to refresh.\n */\nexport const STAGEHAND_VERSION = \"${fullVersion}\" as const;\n`;\n\nwriteFileSync(`${packageRoot}/lib/version.ts`, banner);\n"
  },
  {
    "path": "packages/core/scripts/normalize-v8-coverage.ts",
    "content": "/**\n * Normalize V8 coverage ranges using sourcemaps to avoid offset/1x floor issues.\n *\n * Prereqs: V8 coverage JSON files plus JS files with inline or external sourcemaps.\n * Args: --coverage-dir <dir> (or NODE_V8_COVERAGE).\n * Env: NODE_V8_COVERAGE, V8_COVERAGE_SCAN_LIMIT.\n * Example: tsx packages/core/scripts/normalize-v8-coverage.ts --coverage-dir coverage/e2e-local\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n  SourceMapConsumer,\n  type RawIndexMap,\n  type RawSourceMap,\n} from \"source-map\";\nimport { getRepoRootDir, isMainModule } from \"../lib/v3/runtimePaths.js\";\n\ntype CoverageRange = {\n  startOffset: number;\n  endOffset: number;\n  count: number;\n};\n\ntype CoverageEntry = {\n  url?: string;\n  functions?: Array<{\n    ranges?: CoverageRange[];\n  }>;\n};\n\ntype CoverageFile = {\n  result?: CoverageEntry[];\n};\n\nconst toFilePath = (urlOrPath: string): string | null => {\n  if (!urlOrPath) return null;\n  if (urlOrPath.startsWith(\"node:\")) return null;\n  if (urlOrPath.startsWith(\"file:\")) {\n    try {\n      return fileURLToPath(urlOrPath);\n    } catch {\n      return null;\n    }\n  }\n  return path.isAbsolute(urlOrPath) ? urlOrPath : null;\n};\n\ntype SourceMapPayload = RawSourceMap | RawIndexMap;\n\nconst readSourceMap = (jsPath: string): SourceMapPayload | null => {\n  if (!fs.existsSync(jsPath)) return null;\n  const source = fs.readFileSync(jsPath, \"utf8\");\n  const inlineMatch = source.match(\n    /sourceMappingURL=data:application\\/json;base64,([A-Za-z0-9+/=]+)/,\n  );\n  if (inlineMatch) {\n    return JSON.parse(\n      Buffer.from(inlineMatch[1], \"base64\").toString(\"utf8\"),\n    ) as SourceMapPayload;\n  }\n  const mapMatch = source.match(/sourceMappingURL=([^\\s]+)/);\n  if (!mapMatch) return null;\n  const mapFile = mapMatch[1].trim();\n  if (mapFile.startsWith(\"data:\")) return null;\n  const mapPath = path.resolve(path.dirname(jsPath), mapFile);\n  if (!fs.existsSync(mapPath)) return null;\n  return JSON.parse(fs.readFileSync(mapPath, \"utf8\")) as SourceMapPayload;\n};\n\nconst buildLineStarts = (source: string) => {\n  const lineStarts = [0];\n  for (let i = 0; i < source.length; i++) {\n    if (source[i] === \"\\n\") lineStarts.push(i + 1);\n  }\n  return lineStarts;\n};\n\nconst offsetToLineCol = (lineStarts: number[], offset: number) => {\n  let low = 0;\n  let high = lineStarts.length - 1;\n  while (low <= high) {\n    const mid = Math.floor((low + high) / 2);\n    const start = lineStarts[mid];\n    const next = mid + 1 < lineStarts.length ? lineStarts[mid + 1] : Infinity;\n    if (start <= offset && offset < next) {\n      return { line: mid + 1, column: offset - start };\n    }\n    if (start > offset) {\n      high = mid - 1;\n    } else {\n      low = mid + 1;\n    }\n  }\n  return { line: 1, column: 0 };\n};\n\nconst lineColToOffset = (\n  lineStarts: number[],\n  line: number,\n  column: number,\n  sourceLength: number,\n) => {\n  const lineIndex = Math.max(0, line - 1);\n  const lineStart = lineStarts[lineIndex] ?? 0;\n  const lineEnd =\n    lineIndex + 1 < lineStarts.length\n      ? lineStarts[lineIndex + 1] - 1\n      : sourceLength;\n  const clampedColumn = Math.max(0, Math.min(column, lineEnd - lineStart));\n  return lineStart + clampedColumn;\n};\ntype NormalizerOptions = {\n  coverageDir: string;\n  maxScan: number;\n};\n\ntype SourceContext = {\n  lineStarts: number[];\n  sourceLength: number;\n  consumer: SourceMapConsumer;\n};\n\ntype MappedPosition = {\n  source: string;\n  line: number;\n  column: number;\n};\n\ntype OffsetMapping = {\n  mapped: MappedPosition;\n  offset: number;\n};\n\nconst mapOriginalPosition = (\n  consumer: SourceMapConsumer,\n  line: number,\n  column: number,\n  bias: number,\n) =>\n  consumer.originalPositionFor({\n    line,\n    column,\n    bias,\n  });\n\nconst findMappedStart = (\n  ctx: SourceContext,\n  startOffset: number,\n  endOffset: number,\n  options: NormalizerOptions,\n): OffsetMapping | null => {\n  const maxScan = Math.min(\n    options.maxScan,\n    Math.max(0, endOffset - startOffset),\n  );\n  const startPos = offsetToLineCol(ctx.lineStarts, startOffset);\n  let mapped = mapOriginalPosition(\n    ctx.consumer,\n    startPos.line,\n    startPos.column,\n    SourceMapConsumer.LEAST_UPPER_BOUND,\n  );\n  if (!mapped.source) {\n    mapped = mapOriginalPosition(\n      ctx.consumer,\n      startPos.line,\n      startPos.column,\n      SourceMapConsumer.GREATEST_LOWER_BOUND,\n    );\n  }\n  if (mapped.source) {\n    return {\n      mapped: {\n        source: mapped.source,\n        line: mapped.line ?? 1,\n        column: mapped.column ?? 0,\n      },\n      offset: startOffset,\n    };\n  }\n\n  const limit = Math.min(endOffset, startOffset + maxScan);\n  for (let off = startOffset + 1; off <= limit; off++) {\n    const pos = offsetToLineCol(ctx.lineStarts, off);\n    mapped = mapOriginalPosition(\n      ctx.consumer,\n      pos.line,\n      pos.column,\n      SourceMapConsumer.LEAST_UPPER_BOUND,\n    );\n    if (!mapped.source) {\n      mapped = mapOriginalPosition(\n        ctx.consumer,\n        pos.line,\n        pos.column,\n        SourceMapConsumer.GREATEST_LOWER_BOUND,\n      );\n    }\n    if (mapped.source) {\n      return {\n        mapped: {\n          source: mapped.source,\n          line: mapped.line ?? 1,\n          column: mapped.column ?? 0,\n        },\n        offset: off,\n      };\n    }\n  }\n\n  return null;\n};\n\nconst findMappedEnd = (\n  ctx: SourceContext,\n  startOffset: number,\n  endOffset: number,\n  options: NormalizerOptions,\n  targetSource?: string,\n): OffsetMapping | null => {\n  const maxScan = Math.min(\n    options.maxScan,\n    Math.max(0, endOffset - startOffset),\n  );\n  const endPos = offsetToLineCol(ctx.lineStarts, endOffset);\n  let mapped = mapOriginalPosition(\n    ctx.consumer,\n    endPos.line,\n    endPos.column,\n    SourceMapConsumer.GREATEST_LOWER_BOUND,\n  );\n  if (!mapped.source || (targetSource && mapped.source !== targetSource)) {\n    mapped = mapOriginalPosition(\n      ctx.consumer,\n      endPos.line,\n      endPos.column,\n      SourceMapConsumer.LEAST_UPPER_BOUND,\n    );\n  }\n  if (mapped.source && (!targetSource || mapped.source === targetSource)) {\n    return {\n      mapped: {\n        source: mapped.source,\n        line: mapped.line ?? 1,\n        column: mapped.column ?? 0,\n      },\n      offset: endOffset,\n    };\n  }\n\n  const limit = Math.max(startOffset, endOffset - maxScan);\n  for (let off = endOffset - 1; off >= limit; off--) {\n    const pos = offsetToLineCol(ctx.lineStarts, off);\n    mapped = mapOriginalPosition(\n      ctx.consumer,\n      pos.line,\n      pos.column,\n      SourceMapConsumer.GREATEST_LOWER_BOUND,\n    );\n    if (!mapped.source || (targetSource && mapped.source !== targetSource)) {\n      mapped = mapOriginalPosition(\n        ctx.consumer,\n        pos.line,\n        pos.column,\n        SourceMapConsumer.LEAST_UPPER_BOUND,\n      );\n    }\n    if (mapped.source && (!targetSource || mapped.source === targetSource)) {\n      return {\n        mapped: {\n          source: mapped.source,\n          line: mapped.line ?? 1,\n          column: mapped.column ?? 0,\n        },\n        offset: off,\n      };\n    }\n  }\n\n  return null;\n};\n\nconst normalizeRange = (\n  range: CoverageRange,\n  ctx: SourceContext,\n  options: NormalizerOptions,\n) => {\n  if (range.endOffset <= range.startOffset) return false;\n  const startMapping = findMappedStart(\n    ctx,\n    range.startOffset,\n    range.endOffset,\n    options,\n  );\n  if (!startMapping) return false;\n  const endMapping = findMappedEnd(\n    ctx,\n    range.startOffset,\n    range.endOffset,\n    options,\n    startMapping.mapped.source,\n  );\n  if (!endMapping) return false;\n\n  let startOffset = startMapping.offset;\n  let endOffset = endMapping.offset;\n\n  if (range.count === 0) {\n    const genStart = ctx.consumer.generatedPositionFor({\n      source: startMapping.mapped.source,\n      line: startMapping.mapped.line,\n      column: 0,\n      bias: SourceMapConsumer.LEAST_UPPER_BOUND,\n    });\n    const genEnd = ctx.consumer.generatedPositionFor({\n      source: startMapping.mapped.source,\n      line: endMapping.mapped.line ?? startMapping.mapped.line,\n      column: Number.MAX_SAFE_INTEGER,\n      bias: SourceMapConsumer.GREATEST_LOWER_BOUND,\n    });\n    if (genStart.line && genEnd.line) {\n      const expandedStart = lineColToOffset(\n        ctx.lineStarts,\n        genStart.line,\n        genStart.column ?? 0,\n        ctx.sourceLength,\n      );\n      const expandedEnd = lineColToOffset(\n        ctx.lineStarts,\n        genEnd.line,\n        genEnd.column ?? 0,\n        ctx.sourceLength,\n      );\n      startOffset = Math.min(startOffset, expandedStart);\n      endOffset = Math.max(endOffset, expandedEnd);\n    }\n  }\n\n  if (endOffset <= startOffset) return false;\n  if (startOffset !== range.startOffset || endOffset !== range.endOffset) {\n    range.startOffset = startOffset;\n    range.endOffset = endOffset;\n    return true;\n  }\n  return false;\n};\n\nconst normalizeCoverageDir = async (options: NormalizerOptions) => {\n  if (!fs.existsSync(options.coverageDir)) return;\n  const jsonFiles: string[] = [];\n  const walk = (dir: string) => {\n    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n      const full = `${dir}/${entry.name}`;\n      if (entry.isDirectory()) {\n        if (entry.name === \".v8-tmp\" || entry.name === \"merged\") {\n          continue;\n        }\n        walk(full);\n      } else if (entry.isFile() && entry.name.endsWith(\".json\")) {\n        jsonFiles.push(full);\n      }\n    }\n  };\n  walk(options.coverageDir);\n  if (jsonFiles.length === 0) return;\n\n  const sourceCache = new Map<string, SourceContext | null>();\n  try {\n    for (const file of jsonFiles) {\n      const data = JSON.parse(fs.readFileSync(file, \"utf8\")) as CoverageFile;\n      if (!Array.isArray(data.result)) continue;\n      let updated = false;\n\n      for (const entry of data.result) {\n        const jsPath = entry.url ? toFilePath(entry.url) : null;\n        if (!jsPath) continue;\n        let ctx = sourceCache.get(jsPath);\n        if (ctx === undefined) {\n          const map = readSourceMap(jsPath);\n          if (!map) {\n            sourceCache.set(jsPath, null);\n            continue;\n          }\n          const source = fs.readFileSync(jsPath, \"utf8\");\n          ctx = {\n            lineStarts: buildLineStarts(source),\n            sourceLength: source.length,\n            consumer: await new SourceMapConsumer(map),\n          };\n          sourceCache.set(jsPath, ctx);\n        }\n        if (!ctx) continue;\n        if (!entry?.functions) continue;\n        for (const block of entry.functions) {\n          if (!block?.ranges) continue;\n          for (const range of block.ranges) {\n            if (normalizeRange(range, ctx, options)) {\n              updated = true;\n            }\n          }\n        }\n      }\n\n      if (updated) {\n        fs.writeFileSync(file, JSON.stringify(data));\n      }\n    }\n  } finally {\n    for (const ctx of sourceCache.values()) {\n      ctx?.consumer.destroy();\n    }\n  }\n};\n\nexport const normalizeV8Coverage = async (coverageDir: string) => {\n  const repoRoot = getRepoRootDir();\n  const resolvedDir = path.isAbsolute(coverageDir)\n    ? coverageDir\n    : path.resolve(repoRoot, coverageDir);\n  const maxScan = Number(process.env.V8_COVERAGE_SCAN_LIMIT ?? 20000);\n  await normalizeCoverageDir({ coverageDir: resolvedDir, maxScan });\n};\n\nexport default normalizeV8Coverage;\n\nconst main = async () => {\n  const args = process.argv.slice(2);\n  const idx = args.indexOf(\"--coverage-dir\");\n  const coverageDir =\n    (idx >= 0 ? args[idx + 1] : undefined) ?? process.env.NODE_V8_COVERAGE;\n  if (!coverageDir) {\n    console.error(\n      \"Missing coverage dir (use --coverage-dir or NODE_V8_COVERAGE).\",\n    );\n    process.exit(1);\n  }\n  await normalizeV8Coverage(coverageDir);\n};\n\nif (isMainModule()) {\n  main().catch((error) => {\n    console.error(error);\n    process.exit(1);\n  });\n}\n"
  },
  {
    "path": "packages/core/scripts/prepare.js",
    "content": "import { spawnSync } from \"node:child_process\";\n\nconst isCi =\n  process.env.CI === \"true\" ||\n  process.env.CI === \"1\" ||\n  process.env.SKIP_PREPARE === \"1\";\n\nif (isCi) {\n  console.log(\"Skipping prepare script in CI.\");\n  process.exit(0);\n}\n\nconst result = spawnSync(\"pnpm\", [\"run\", \"build\"], {\n  stdio: \"inherit\",\n  shell: process.platform === \"win32\",\n});\n\nprocess.exit(result.status ?? 1);\n"
  },
  {
    "path": "packages/core/scripts/test-core.ts",
    "content": "/**\n * Core unit tests (Vitest) on dist/esm tests.\n *\n * Prereqs: pnpm run build:esm (packages/core/dist/esm/tests/unit present).\n * Args: [test paths...] -- [vitest args...] | --list (prints JSON matrix)\n * Env: NODE_V8_COVERAGE, NODE_OPTIONS, VITEST_CONSOLE_REPORTER;\n *      writes CTRF to ctrf/vitest-core.xml by default.\n * Example: pnpm run test:core -- packages/core/dist/esm/tests/unit/foo.test.js -- --reporter=junit\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport {\n  ensureParentDir,\n  parseListFlag,\n  splitArgs,\n  collectFiles,\n  toSafeName,\n  normalizeVitestArgs,\n  findJunitPath,\n  hasReporterName,\n  writeCtrfFromJunit,\n} from \"./test-utils.js\";\nimport { getRepoRootDir } from \"../lib/v3/runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nconst sourceTestsDir = `${repoRoot}/packages/core/tests/unit`;\nconst testsDir = `${repoRoot}/packages/core/dist/esm/tests/unit`;\nconst defaultConfigPath = `${repoRoot}/packages/core/vitest.esm.config.mjs`;\n\nconst resolveRepoRelative = (value: string) =>\n  path.isAbsolute(value) ? value : path.resolve(repoRoot, value);\n\nconst hasConfigArg = (argsList: string[]) =>\n  argsList.some((arg, i) => {\n    if (arg.startsWith(\"--config=\")) return true;\n    return arg === \"--config\" && Boolean(argsList[i + 1]);\n  });\n\nconst toTestName = (testPath: string) => {\n  const abs = resolveRepoRelative(testPath);\n  const rel = path.relative(testsDir, abs).replaceAll(\"\\\\\", \"/\");\n  if (!rel.startsWith(\"..\")) {\n    return rel.replace(/\\.test\\.(ts|js)$/i, \"\");\n  }\n  return path.basename(abs).replace(/\\.test\\.(ts|js)$/i, \"\");\n};\n\nconst listFlag = parseListFlag(process.argv.slice(2));\nconst { paths, extra } = splitArgs(listFlag.args);\n\nif (listFlag.list) {\n  const tests = collectFiles(sourceTestsDir, \".test.ts\");\n  const entries = tests.map((file) => {\n    const relSource = path.relative(sourceTestsDir, file).replaceAll(\"\\\\\", \"/\");\n    const rel = relSource.replace(/\\.test\\.ts$/, \"\");\n    const distPath = `${testsDir}/${relSource.replace(/\\.test\\.ts$/, \".test.js\")}`;\n    return {\n      path: path.relative(repoRoot, distPath).replaceAll(\"\\\\\", \"/\"),\n      name: rel,\n      safe_name: toSafeName(rel),\n    };\n  });\n  console.log(JSON.stringify(entries));\n  process.exit(0);\n}\n\nif (!fs.existsSync(testsDir)) {\n  console.error(\n    \"Missing packages/core/dist/esm/tests/unit. Run pnpm run build:esm first.\",\n  );\n  process.exit(1);\n}\n\nconst runtimePaths = paths.map(resolveRepoRelative);\nconst hasUserConfig = hasConfigArg(extra);\n\nconst baseNodeOptions = \"--enable-source-maps\";\nconst nodeOptions = [process.env.NODE_OPTIONS, baseNodeOptions]\n  .filter(Boolean)\n  .join(\" \");\n\nconst relTestName = paths.length === 1 ? toTestName(paths[0]) : null;\n\nconst coverageDir = resolveRepoRelative(\n  process.env.NODE_V8_COVERAGE ??\n    (relTestName\n      ? `${repoRoot}/coverage/core-unit/${relTestName}`\n      : `${repoRoot}/coverage/core-unit`),\n);\nfs.mkdirSync(coverageDir, { recursive: true });\n\nconst normalizedExtra = normalizeVitestArgs(repoRoot, extra);\nconst defaultJunitPath = (() => {\n  if (!relTestName) {\n    return `${repoRoot}/ctrf/core-unit/all.xml`;\n  }\n  return `${repoRoot}/ctrf/core-unit/${relTestName}.xml`;\n})();\nconst hasOutput = Boolean(findJunitPath(normalizedExtra));\nconst vitestArgs = [...normalizedExtra];\nconst consoleReporter = process.env.VITEST_CONSOLE_REPORTER ?? \"default\";\nif (!hasReporterName(vitestArgs, consoleReporter)) {\n  vitestArgs.push(`--reporter=${consoleReporter}`);\n}\nif (!hasReporterName(vitestArgs, \"junit\")) {\n  vitestArgs.push(\"--reporter=junit\");\n}\nif (!hasOutput) {\n  ensureParentDir(defaultJunitPath);\n  vitestArgs.push(`--outputFile.junit=${defaultJunitPath}`);\n}\nconst junitPath = findJunitPath(vitestArgs) ?? defaultJunitPath;\n\nconst env = {\n  ...process.env,\n  NODE_OPTIONS: nodeOptions,\n  NODE_V8_COVERAGE: coverageDir,\n};\n\nconst result = spawnSync(\n  \"pnpm\",\n  [\n    \"--filter\",\n    \"@browserbasehq/stagehand\",\n    \"exec\",\n    \"vitest\",\n    \"run\",\n    ...(hasUserConfig ? [] : [\"--config\", defaultConfigPath]),\n    ...vitestArgs,\n    ...runtimePaths,\n  ],\n  { stdio: \"inherit\", env },\n);\n\nwriteCtrfFromJunit(junitPath, \"vitest\");\n\nprocess.exit(result.status ?? 1);\n"
  },
  {
    "path": "packages/core/scripts/test-e2e.ts",
    "content": "/**\n * E2E tests (Playwright) on dist/esm tests.\n *\n * Prereqs: pnpm run build:esm (packages/core/dist/esm/tests/integration present).\n * Args: [test paths...] -- [playwright args...] | --list (prints JSON matrix).\n * Env: STAGEHAND_BROWSER_TARGET=local|browserbase, CHROME_PATH (local),\n *      NODE_V8_COVERAGE, PLAYWRIGHT_CONSOLE_REPORTER;\n *      writes CTRF to ctrf/playwright-*.xml by default.\n * Example: STAGEHAND_BROWSER_TARGET=browserbase pnpm run test:e2e -- packages/core/dist/esm/tests/integration/foo.spec.js\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport {\n  ensureParentDir,\n  parseListFlag,\n  splitArgs,\n  collectFiles,\n  toSafeName,\n  writeCtrfFromJunit,\n} from \"./test-utils.js\";\nimport {\n  createRequireFromCaller,\n  getRepoRootDir,\n} from \"../lib/v3/runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nconst sourceTestsDir = `${repoRoot}/packages/core/tests/integration`;\nconst testsDir = `${repoRoot}/packages/core/dist/esm/tests/integration`;\nconst defaultConfigPath = `${repoRoot}/packages/core/dist/esm/tests/integration/v3.playwright.config.js`;\n\nconst resolveRepoRelative = (value: string) =>\n  path.isAbsolute(value) ? value : path.resolve(repoRoot, value);\nconst require = createRequireFromCaller();\nconst playwrightCliPath = require.resolve(\"@playwright/test/cli\");\n\nconst hasConfigArg = (argsList: string[]) =>\n  argsList.some((arg, i) => {\n    if (arg.startsWith(\"--config=\")) return true;\n    return arg === \"--config\" && Boolean(argsList[i + 1]);\n  });\n\nconst stripReporterArgs = (argsList: string[]) => {\n  const filtered: string[] = [];\n  let removed = false;\n  for (let i = 0; i < argsList.length; i++) {\n    const arg = argsList[i];\n    if (\n      arg === \"--reporter\" ||\n      arg === \"-r\" ||\n      arg.startsWith(\"--reporter=\") ||\n      arg.startsWith(\"-r=\")\n    ) {\n      removed = true;\n      if ((arg === \"--reporter\" || arg === \"-r\") && argsList[i + 1]) {\n        i += 1;\n      }\n      continue;\n    }\n    filtered.push(arg);\n  }\n  return { filtered, removed };\n};\n\nconst toTestName = (testPath: string) => {\n  const abs = resolveRepoRelative(testPath);\n  const rel = path.relative(testsDir, abs).replaceAll(\"\\\\\", \"/\");\n  if (!rel.startsWith(\"..\")) {\n    return rel.replace(/\\.spec\\.(ts|js)$/i, \"\");\n  }\n  return path.basename(abs).replace(/\\.spec\\.(ts|js)$/i, \"\");\n};\n\nconst toPlaywrightPath = (testPath: string) => {\n  const abs = resolveRepoRelative(testPath);\n  const rel = path.relative(testsDir, abs).replaceAll(\"\\\\\", \"/\");\n  const value = rel.startsWith(\"..\") ? abs : rel;\n  return value.replace(/(\\.spec|\\.test)\\.(ts|js)$/i, \"$1\");\n};\n\nconst listFlag = parseListFlag(process.argv.slice(2));\nconst { paths, extra } = splitArgs(listFlag.args);\n\nif (listFlag.list) {\n  const tests = collectFiles(sourceTestsDir, \".spec.ts\");\n  const entries = tests.map((file) => {\n    const relSource = path.relative(sourceTestsDir, file).replaceAll(\"\\\\\", \"/\");\n    const rel = relSource.replace(/\\.spec\\.ts$/, \"\");\n    const distPath = `${testsDir}/${relSource.replace(/\\.spec\\.ts$/, \".spec.js\")}`;\n    return {\n      path: path.relative(repoRoot, distPath).replaceAll(\"\\\\\", \"/\"),\n      name: rel,\n      safe_name: toSafeName(rel),\n    };\n  });\n  console.log(JSON.stringify(entries));\n  process.exit(0);\n}\n\nif (!fs.existsSync(testsDir)) {\n  console.error(\n    \"Missing packages/core/dist/esm/tests/integration. Run pnpm run build:esm first.\",\n  );\n  process.exit(1);\n}\n\nconst { filtered: extraArgs, removed: removedReporterOverride } =\n  stripReporterArgs(extra);\nif (removedReporterOverride) {\n  console.warn(\n    \"Ignoring Playwright --reporter override to preserve console + JUnit output.\",\n  );\n}\n\nconst hasUserConfig = hasConfigArg(extraArgs);\nif (!hasUserConfig && !fs.existsSync(defaultConfigPath)) {\n  console.error(`Missing Playwright config at ${defaultConfigPath}.`);\n  process.exit(1);\n}\n\nconst playwrightPaths = paths.map(toPlaywrightPath);\n\nconst target = (process.env.STAGEHAND_BROWSER_TARGET ?? \"local\").toLowerCase();\nconst useBrowserbase = target === \"browserbase\";\nconst relTestName = paths.length === 1 ? toTestName(paths[0]) : null;\n\nconst coverageDir = resolveRepoRelative(\n  process.env.NODE_V8_COVERAGE ??\n    (relTestName\n      ? `${repoRoot}/coverage/${useBrowserbase ? \"e2e-bb\" : \"e2e-local\"}/${relTestName}`\n      : `${repoRoot}/coverage/${useBrowserbase ? \"e2e-bb\" : \"e2e-local\"}`),\n);\nfs.mkdirSync(coverageDir, { recursive: true });\n\nconst defaultJunitPath = relTestName\n  ? `${repoRoot}/ctrf/${useBrowserbase ? \"e2e-bb\" : \"e2e-local\"}/${relTestName}.xml`\n  : `${repoRoot}/ctrf/${useBrowserbase ? \"e2e-bb\" : \"e2e-local\"}/all.xml`;\nconst ctrfPath = process.env.CTRF_JUNIT_PATH\n  ? resolveRepoRelative(process.env.CTRF_JUNIT_PATH)\n  : defaultJunitPath;\nensureParentDir(ctrfPath);\n\nconst baseNodeOptions = \"--enable-source-maps\";\nconst nodeOptions = [process.env.NODE_OPTIONS, baseNodeOptions]\n  .filter(Boolean)\n  .join(\" \");\n\nconst env = {\n  ...process.env,\n  NODE_OPTIONS: nodeOptions,\n  NODE_V8_COVERAGE: coverageDir,\n  CTRF_JUNIT_PATH: ctrfPath,\n};\n\nconst result = spawnSync(\n  process.execPath,\n  [\n    playwrightCliPath,\n    \"test\",\n    ...(hasUserConfig ? [] : [\"--config\", defaultConfigPath]),\n    ...extraArgs,\n    ...playwrightPaths,\n  ],\n  { stdio: \"inherit\", env, cwd: repoRoot },\n);\n\nwriteCtrfFromJunit(ctrfPath, \"playwright\");\n\nprocess.exit(result.status ?? 1);\n"
  },
  {
    "path": "packages/core/scripts/test-utils.ts",
    "content": "/**\n * Shared helpers for scripts (not a runnable script).\n *\n * Prereqs: none.\n * Args: n/a.\n * Env: n/a.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"../lib/v3/runtimePaths.js\";\n\nconst workspaceRoot = getRepoRootDir();\n\nexport const ensureParentDir = (filePath: string) => {\n  fs.mkdirSync(path.dirname(filePath), { recursive: true });\n};\n\nexport const splitArgs = (args: string[]) => {\n  const tokens = [...args];\n  while (tokens[0] === \"--\") {\n    tokens.shift();\n  }\n\n  const leadingExtra: string[] = [];\n  while (tokens.length > 0 && tokens[0].startsWith(\"-\")) {\n    const arg = tokens.shift();\n    if (!arg) break;\n    if (arg === \"--\") break;\n    leadingExtra.push(arg);\n    if (\n      !arg.includes(\"=\") &&\n      tokens[0] &&\n      tokens[0] !== \"--\" &&\n      !tokens[0].startsWith(\"-\")\n    ) {\n      leadingExtra.push(tokens.shift() as string);\n    }\n  }\n\n  while (tokens[0] === \"--\") {\n    tokens.shift();\n  }\n\n  const separatorIndex = tokens.indexOf(\"--\");\n  return {\n    paths: separatorIndex === -1 ? tokens : tokens.slice(0, separatorIndex),\n    extra: [\n      ...leadingExtra,\n      ...(separatorIndex === -1 ? [] : tokens.slice(separatorIndex + 1)),\n    ],\n  };\n};\n\nexport const parseListFlag = (args: string[]) => {\n  const remaining: string[] = [];\n  let value: string | null = null;\n  for (let i = 0; i < args.length; i++) {\n    const arg = args[i];\n    if (arg === \"--list\") {\n      const next = args[i + 1];\n      if (next && !next.startsWith(\"--\")) {\n        value = next;\n        i += 1;\n      } else {\n        value = \"\";\n      }\n      continue;\n    }\n    if (arg.startsWith(\"--list=\")) {\n      value = arg.slice(\"--list=\".length);\n      continue;\n    }\n    remaining.push(arg);\n  }\n  return { list: value !== null, value: value ?? \"\", args: remaining };\n};\n\nexport const toSafeName = (name: string) => name.replace(/[\\\\/]/g, \"-\");\n\nexport const collectFiles = (dir: string, suffix: string) => {\n  const results: string[] = [];\n  const walk = (current: string) => {\n    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n      const full = `${current}/${entry.name}`;\n      if (entry.isDirectory()) {\n        walk(full);\n      } else if (entry.isFile() && entry.name.endsWith(suffix)) {\n        results.push(full);\n      }\n    }\n  };\n  if (fs.existsSync(dir)) walk(dir);\n  return results.sort();\n};\n\nexport const normalizeVitestArgs = (repoRoot: string, argsList: string[]) => {\n  const normalized = [...argsList];\n  const prefix = \"--outputFile.junit=\";\n  for (let i = 0; i < normalized.length; i++) {\n    const arg = normalized[i];\n    if (arg.startsWith(prefix)) {\n      const value = arg.slice(prefix.length);\n      const resolved = path.isAbsolute(value)\n        ? value\n        : path.resolve(repoRoot, value);\n      ensureParentDir(resolved);\n      normalized[i] = `${prefix}${resolved}`;\n      continue;\n    }\n    if (arg === \"--outputFile.junit\" && normalized[i + 1]) {\n      const resolved = path.isAbsolute(normalized[i + 1])\n        ? normalized[i + 1]\n        : path.resolve(repoRoot, normalized[i + 1]);\n      ensureParentDir(resolved);\n      normalized[i + 1] = resolved;\n      i += 1;\n    }\n  }\n  return normalized;\n};\n\nexport const findJunitPath = (argsList: string[]) => {\n  const prefix = \"--outputFile.junit=\";\n  for (let i = 0; i < argsList.length; i++) {\n    const arg = argsList[i];\n    if (arg.startsWith(prefix)) {\n      return arg.slice(prefix.length);\n    }\n    if (arg === \"--outputFile.junit\" && argsList[i + 1]) {\n      return argsList[i + 1];\n    }\n  }\n  return null;\n};\n\nconst parseReporters = (argsList: string[]) => {\n  const reporters: string[] = [];\n  for (let i = 0; i < argsList.length; i++) {\n    const arg = argsList[i];\n    if (arg.startsWith(\"--reporter=\")) {\n      reporters.push(arg.slice(\"--reporter=\".length));\n      continue;\n    }\n    if (arg === \"--reporter\" && argsList[i + 1]) {\n      reporters.push(argsList[i + 1]);\n      i += 1;\n    }\n  }\n  return reporters\n    .flatMap((value) => value.split(\",\"))\n    .map((value) => value.trim())\n    .filter(Boolean);\n};\n\nexport const hasReporterName = (argsList: string[], reporter: string) =>\n  parseReporters(argsList).some((value) => value === reporter);\n\nexport const writeCtrfFromJunit = (junitPath: string, tool: string) => {\n  if (!fs.existsSync(junitPath)) return;\n  const stat = fs.statSync(junitPath);\n  if (stat.size === 0) return;\n  const ctrfPath = junitPath.match(/\\.xml$/i)\n    ? junitPath.replace(/\\.xml$/i, \".json\")\n    : `${junitPath}.json`;\n  const result = spawnSync(\n    \"pnpm\",\n    [\"exec\", \"junit-to-ctrf\", junitPath, \"-o\", ctrfPath, \"-t\", tool],\n    { stdio: \"inherit\", cwd: workspaceRoot },\n  );\n  if (result.status !== 0) {\n    console.warn(`CTRF conversion failed for ${junitPath}.`);\n  }\n};\n"
  },
  {
    "path": "packages/core/tests/cache-variables.test.ts",
    "content": "import { describe, expect, it, vi } from \"vitest\";\nimport { ActCache } from \"../lib/v3/cache/ActCache\";\nimport type { CacheStorage } from \"../lib/v3/cache/CacheStorage\";\nimport type { ActHandler } from \"../lib/v3/handlers/actHandler\";\nimport type { LLMClient } from \"../lib/v3/llm/LLMClient\";\nimport type { Page } from \"../lib/v3/understudy/page\";\nimport type { ActCacheContext, CachedActEntry } from \"../lib/v3/types/private\";\nimport type { Action } from \"../lib/v3/types/public\";\n\nfunction createFakeStorage<T>(entry: T): CacheStorage {\n  return {\n    enabled: true,\n    readJson: vi.fn().mockResolvedValue({ value: entry }),\n    writeJson: vi.fn().mockResolvedValue({}),\n    directory: \"/tmp/cache\",\n  } as unknown as CacheStorage;\n}\n\ndescribe(\"ActCache variable handling\", () => {\n  it(\"cache key includes variable keys but not values\", async () => {\n    const storage = {\n      enabled: true,\n      readJson: vi.fn(),\n      writeJson: vi.fn().mockResolvedValue({}),\n      directory: \"/tmp/cache\",\n    } as unknown as CacheStorage;\n\n    const cache = new ActCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => null as unknown as ActHandler,\n      getDefaultLlmClient: () => ({}) as LLMClient,\n      domSettleTimeoutMs: undefined,\n    });\n\n    const fakePage = {\n      url: vi.fn().mockResolvedValue(\"https://example.com\"),\n    } as unknown as Page;\n\n    // First context with username=\"user1@example.com\"\n    const context1 = await cache.prepareContext(\n      \"type %username% into the email field\",\n      fakePage,\n      { username: \"user1@example.com\" },\n    );\n\n    // Second context with username=\"user2@example.com\"\n    const context2 = await cache.prepareContext(\n      \"type %username% into the email field\",\n      fakePage,\n      { username: \"user2@example.com\" },\n    );\n\n    // Third context with different variable key name\n    const context3 = await cache.prepareContext(\n      \"type %email% into the email field\",\n      fakePage,\n      { email: \"user3@example.com\" },\n    );\n\n    // Same instruction + same variable keys = same cache key\n    expect(context1?.cacheKey).toBe(context2?.cacheKey);\n\n    // Different variable keys = different cache key\n    expect(context1?.cacheKey).not.toBe(context3?.cacheKey);\n\n    // Verify variable keys are sorted and stored\n    expect(context1?.variableKeys).toEqual([\"username\"]);\n    expect(context2?.variableKeys).toEqual([\"username\"]);\n    expect(context3?.variableKeys).toEqual([\"email\"]);\n\n    // Verify variable values are preserved in context\n    expect(context1?.variables).toEqual({ username: \"user1@example.com\" });\n    expect(context2?.variables).toEqual({ username: \"user2@example.com\" });\n  });\n\n  it(\"replays cached actions with variable substitution\", async () => {\n    // Cached action contains variable placeholder %username%\n    const action: Action = {\n      selector: \"xpath=/html/body/input[@type='email']\",\n      description: \"type username into email field\",\n      method: \"type\",\n      arguments: [\"%username%\"], // Variable placeholder\n    };\n\n    const entry: CachedActEntry = {\n      version: 1,\n      instruction: \"type %username% into the email field\",\n      url: \"https://example.com\",\n      variableKeys: [\"username\"],\n      actions: [action],\n      actionDescription: \"type username\",\n      message: \"done\",\n    };\n\n    const storage = createFakeStorage(entry);\n\n    // Track what variables are passed to takeDeterministicAction\n    const capturedVariables: Record<string, string>[] = [];\n    const handler = {\n      takeDeterministicAction: vi\n        .fn()\n        .mockImplementation(\n          async (_action, _page, _timeout, _client, _ensure, variables) => {\n            capturedVariables.push(variables || {});\n            return {\n              success: true,\n              message: \"ok\",\n              actionDescription: \"type username\",\n              actions: [action],\n            };\n          },\n        ),\n    } as unknown as ActHandler;\n\n    const defaultClient = {} as LLMClient;\n\n    const cache = new ActCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => handler,\n      getDefaultLlmClient: () => defaultClient,\n      domSettleTimeoutMs: undefined,\n    });\n\n    // First replay with username=\"user1@example.com\"\n    const context1: ActCacheContext = {\n      instruction: \"type %username% into the email field\",\n      cacheKey: \"test-key\",\n      pageUrl: \"https://example.com\",\n      variableKeys: [\"username\"],\n      variables: { username: \"user1@example.com\" },\n    };\n\n    const result1 = await cache.tryReplay(context1, {} as Page);\n\n    expect(result1?.success).toBe(true);\n    expect(handler.takeDeterministicAction).toHaveBeenCalledTimes(1);\n    expect(capturedVariables[0]).toEqual({ username: \"user1@example.com\" });\n\n    // Reset\n    vi.clearAllMocks();\n    capturedVariables.length = 0;\n\n    // Second replay with username=\"user2@example.com\"\n    const context2: ActCacheContext = {\n      instruction: \"type %username% into the email field\",\n      cacheKey: \"test-key\", // Same cache key!\n      pageUrl: \"https://example.com\",\n      variableKeys: [\"username\"],\n      variables: { username: \"user2@example.com\" },\n    };\n\n    const result2 = await cache.tryReplay(context2, {} as Page);\n\n    expect(result2?.success).toBe(true);\n    expect(handler.takeDeterministicAction).toHaveBeenCalledTimes(1);\n    expect(capturedVariables[0]).toEqual({ username: \"user2@example.com\" });\n  });\n\n  it(\"cache miss when variable keys don't match\", async () => {\n    const action: Action = {\n      selector: \"xpath=/html/body/input\",\n      description: \"type username\",\n      method: \"type\",\n      arguments: [\"%username%\"],\n    };\n\n    // Cached entry expects \"username\" variable\n    const entry: CachedActEntry = {\n      version: 1,\n      instruction: \"type %username% into the field\",\n      url: \"https://example.com\",\n      variableKeys: [\"username\"],\n      actions: [action],\n    };\n\n    const storage = createFakeStorage(entry);\n    const cache = new ActCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => null as unknown as ActHandler,\n      getDefaultLlmClient: () => ({}) as LLMClient,\n      domSettleTimeoutMs: undefined,\n    });\n\n    // Context has different variable key \"email\"\n    const context: ActCacheContext = {\n      instruction: \"type %email% into the field\",\n      cacheKey: \"test-key\",\n      pageUrl: \"https://example.com\",\n      variableKeys: [\"email\"],\n      variables: { email: \"test@example.com\" },\n    };\n\n    const result = await cache.tryReplay(context, {} as Page);\n\n    // Should return null (cache miss) due to variable key mismatch\n    expect(result).toBeNull();\n  });\n\n  it(\"cache miss when required variables are missing\", async () => {\n    const action: Action = {\n      selector: \"xpath=/html/body/input\",\n      description: \"type username\",\n      method: \"type\",\n      arguments: [\"%username%\"],\n    };\n\n    const entry: CachedActEntry = {\n      version: 1,\n      instruction: \"type %username% into the field\",\n      url: \"https://example.com\",\n      variableKeys: [\"username\"],\n      actions: [action],\n    };\n\n    const storage = createFakeStorage(entry);\n    const logger = vi.fn();\n    const cache = new ActCache({\n      storage,\n      logger,\n      getActHandler: () => null as unknown as ActHandler,\n      getDefaultLlmClient: () => ({}) as LLMClient,\n      domSettleTimeoutMs: undefined,\n    });\n\n    // Context missing the username variable value\n    const context: ActCacheContext = {\n      instruction: \"type %username% into the field\",\n      cacheKey: \"test-key\",\n      pageUrl: \"https://example.com\",\n      variableKeys: [\"username\"],\n      variables: {}, // Missing username value!\n    };\n\n    const result = await cache.tryReplay(context, {} as Page);\n\n    // Should return null (cache miss)\n    expect(result).toBeNull();\n\n    // Should log the miss reason\n    expect(logger).toHaveBeenCalledWith(\n      expect.objectContaining({\n        category: \"cache\",\n        message: \"act cache miss: missing variables for replay\",\n        level: 2,\n      }),\n    );\n  });\n\n  it(\"handles multiple variables correctly\", async () => {\n    const storage = {\n      enabled: true,\n      readJson: vi.fn(),\n      writeJson: vi.fn().mockResolvedValue({}),\n      directory: \"/tmp/cache\",\n    } as unknown as CacheStorage;\n\n    const cache = new ActCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => null as unknown as ActHandler,\n      getDefaultLlmClient: () => ({}) as LLMClient,\n      domSettleTimeoutMs: undefined,\n    });\n\n    const fakePage = {\n      url: vi.fn().mockResolvedValue(\"https://example.com\"),\n    } as unknown as Page;\n\n    // Context with multiple variables\n    const context1 = await cache.prepareContext(\n      \"fill %username% and %password%\",\n      fakePage,\n      { username: \"user1\", password: \"pass1\" },\n    );\n\n    const context2 = await cache.prepareContext(\n      \"fill %username% and %password%\",\n      fakePage,\n      { username: \"user2\", password: \"pass2\" },\n    );\n\n    // Same cache key despite different values\n    expect(context1?.cacheKey).toBe(context2?.cacheKey);\n\n    // Variable keys should be sorted\n    expect(context1?.variableKeys).toEqual([\"password\", \"username\"]);\n    expect(context2?.variableKeys).toEqual([\"password\", \"username\"]);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-abort-signal.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { AgentAbortError } from \"../../lib/v3/types/public/sdkErrors.js\";\n\ntest.describe(\"Stagehand agent abort signal\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3({\n      ...v3TestConfig,\n      experimental: true,\n    });\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"non-streaming: abort signal stops execution and throws AgentAbortError\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    const controller = new AbortController();\n\n    // Abort after 500ms - should be enough for the LLM to start but not finish\n    setTimeout(() => controller.abort(), 500);\n\n    await expect(\n      agent.execute({\n        instruction:\n          \"Describe every visual element on this page in extreme detail. Describe at least 100 different elements.\",\n        maxSteps: 50,\n        signal: controller.signal,\n      }),\n    ).rejects.toThrow(AgentAbortError);\n  });\n\n  test(\"streaming: abort signal stops stream and rejects result with AgentAbortError\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      stream: true,\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    const controller = new AbortController();\n\n    // Abort after 500ms\n    setTimeout(() => controller.abort(), 500);\n\n    const streamResult = await agent.execute({\n      instruction:\n        \"Describe every visual element on this page in extreme detail. Describe at least 100 different elements.\",\n      maxSteps: 50,\n      signal: controller.signal,\n    });\n\n    // Handle both stream consumption and result promise together\n    // The result promise will reject with AgentAbortError when aborted\n    const consumeStream = async () => {\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume chunks until stream ends\n      }\n    };\n\n    // Both should complete - stream ends and result rejects\n    const [, resultError] = await Promise.allSettled([\n      consumeStream(),\n      streamResult.result,\n    ]);\n\n    // The result should have rejected with AgentAbortError\n    expect(resultError.status).toBe(\"rejected\");\n    expect((resultError as PromiseRejectedResult).reason).toBeInstanceOf(\n      AgentAbortError,\n    );\n  });\n\n  test(\"non-streaming: already aborted signal throws AgentAbortError immediately\", async () => {\n    test.setTimeout(20000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    // Create an already aborted controller\n    const controller = new AbortController();\n    controller.abort();\n\n    await expect(\n      agent.execute({\n        instruction: \"This should not run.\",\n        maxSteps: 3,\n        signal: controller.signal,\n      }),\n    ).rejects.toThrow(AgentAbortError);\n  });\n\n  test(\"non-streaming: execution completes normally without abort signal\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    // No signal provided - should complete normally\n    const result = await agent.execute({\n      instruction: \"Describe this page briefly.\",\n      maxSteps: 3,\n    });\n\n    expect(result.success).toBe(true);\n    expect(result.completed).toBe(true);\n  });\n\n  test(\"streaming: execution completes normally without abort signal\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      stream: true,\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    // No signal provided - should complete normally\n    const streamResult = await agent.execute({\n      instruction: \"Describe this page briefly.\",\n      maxSteps: 3,\n    });\n\n    // Consume the stream first\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    for await (const _ of streamResult.textStream) {\n      // Just consume\n    }\n\n    // Now get the final result\n    const result = await streamResult.result;\n\n    expect(result.success).toBe(true);\n    expect(result.completed).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-cache-self-heal.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport type {\n  AgentReplayActStep,\n  AgentReplayFillFormStep,\n  CachedAgentEntry,\n} from \"../../lib/v3/types/private/cache.js\";\n\ntest.describe(\"Agent cache self-heal (e2e)\", () => {\n  let v3: V3;\n  let cacheDir: string;\n\n  // eslint-disable-next-line no-empty-pattern\n  test.beforeEach(async ({}, testInfo) => {\n    await fs.mkdir(testInfo.outputDir, { recursive: true });\n    cacheDir = await fs.mkdtemp(path.join(testInfo.outputDir, \"agent-cache-\"));\n    v3 = new V3({\n      ...v3TestConfig,\n      cacheDir,\n      selfHeal: true,\n    });\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"replays heal corrupted selectors\", async () => {\n    test.setTimeout(120_000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n    const page = v3.context.pages()[0];\n    const url =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/shadow-dom/\";\n    const instruction = \"click the button\";\n\n    await page.goto(url, { waitUntil: \"networkidle\" });\n    const firstResult = await agent.execute({ instruction, maxSteps: 20 });\n    expect(firstResult.success).toBe(true);\n\n    const cachePath = await locateAgentCacheFile(cacheDir);\n    const originalEntry = await readCacheEntry(cachePath);\n    const originalActionStep = findFirstActionStep(originalEntry);\n    expect(originalActionStep).toBeDefined();\n    const originalSelector = originalActionStep?.actions?.[0]?.selector;\n    expect(typeof originalSelector).toBe(\"string\");\n\n    // Corrupt the cached selector so the replay needs to self-heal.\n    if (originalActionStep?.actions?.[0]) {\n      originalActionStep.actions[0].selector = \"xpath=/yeee\";\n    }\n    await fs.writeFile(\n      cachePath,\n      JSON.stringify(originalEntry, null, 2),\n      \"utf8\",\n    );\n\n    // Second run should replay from cache, self-heal, and update the file.\n    await page.goto(url, { waitUntil: \"networkidle\" });\n    const replayResult = await agent.execute({ instruction, maxSteps: 20 });\n    expect(replayResult.success).toBe(true);\n\n    const healedEntry = await readCacheEntry(cachePath);\n    const healedActionStep = findFirstActionStep(healedEntry);\n    expect(healedActionStep?.actions?.[0]?.selector).toBe(originalSelector);\n    expect(healedActionStep?.actions?.[0]?.selector).not.toBe(\"xpath=/yeee\");\n    expect(healedEntry.timestamp).not.toBe(originalEntry.timestamp);\n  });\n});\n\nasync function locateAgentCacheFile(cacheDir: string): Promise<string> {\n  const deadline = Date.now() + 10_000;\n  while (Date.now() < deadline) {\n    const entries = await fs.readdir(cacheDir);\n    const agentFiles = entries.filter((file) => file.startsWith(\"agent-\"));\n    if (agentFiles.length > 0) {\n      return path.join(cacheDir, agentFiles[0]!);\n    }\n    await new Promise((resolve) => setTimeout(resolve, 200));\n  }\n  throw new Error(\"Timed out waiting for agent cache entry to be written\");\n}\n\nasync function readCacheEntry(cachePath: string): Promise<CachedAgentEntry> {\n  const raw = await fs.readFile(cachePath, \"utf8\");\n  return JSON.parse(raw) as CachedAgentEntry;\n}\n\ntype StepWithActions = AgentReplayActStep | AgentReplayFillFormStep;\n\nfunction findFirstActionStep(\n  entry: CachedAgentEntry,\n): StepWithActions | undefined {\n  return entry.steps.find((step) => {\n    const actions = (step as StepWithActions).actions;\n    return Array.isArray(actions) && actions.length > 0;\n  }) as StepWithActions | undefined;\n}\n"
  },
  {
    "path": "packages/core/tests/integration/agent-callbacks.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport type { StepResult, ToolSet } from \"ai\";\nimport { StreamingCallbacksInNonStreamingModeError } from \"../../lib/v3/types/public/sdkErrors.js\";\n\ntest.describe(\"Stagehand agent callbacks behavior\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3({\n      ...v3TestConfig,\n      experimental: true, // Required for callbacks and streaming\n    });\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test.describe(\"Non-streaming callbacks (stream: false)\", () => {\n    test(\"onStepFinish callback is called for each step\", async () => {\n      test.setTimeout(60000);\n\n      const stepFinishEvents: StepResult<ToolSet>[] = [];\n\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      await agent.execute({\n        instruction:\n          \"What is the title of this page? Mark the task as complete after answering.\",\n        maxSteps: 5,\n        callbacks: {\n          onStepFinish: async (event) => {\n            stepFinishEvents.push(event);\n          },\n        },\n      });\n\n      // Should have at least one step finish event\n      expect(stepFinishEvents.length).toBeGreaterThan(0);\n\n      // Each event should have expected properties\n      for (const event of stepFinishEvents) {\n        expect(event).toHaveProperty(\"finishReason\");\n        expect(event).toHaveProperty(\"text\");\n      }\n    });\n\n    test(\"prepareStep callback is called before each step\", async () => {\n      test.setTimeout(60000);\n\n      let prepareStepCallCount = 0;\n\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      await agent.execute({\n        instruction: \"Simply describe the page briefly.\",\n        maxSteps: 3,\n        callbacks: {\n          prepareStep: async (stepContext) => {\n            prepareStepCallCount++;\n            return stepContext;\n          },\n        },\n      });\n\n      // prepareStep should have been called at least once\n      expect(prepareStepCallCount).toBeGreaterThan(0);\n    });\n\n    test(\"callbacks receive tool call information\", async () => {\n      test.setTimeout(60000);\n\n      const toolCalls: Array<{ toolName: string; input: unknown }> = [];\n\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      await agent.execute({\n        instruction:\n          \"Take a screenshot and describe what you see briefly. Then mark the task as complete.\",\n        maxSteps: 3,\n        callbacks: {\n          onStepFinish: async (event) => {\n            if (event.toolCalls) {\n              for (const tc of event.toolCalls) {\n                toolCalls.push({\n                  toolName: tc.toolName,\n                  input: tc.input,\n                });\n              }\n            }\n          },\n        },\n      });\n\n      // Should have captured at least one tool call (e.g. screenshot)\n      expect(toolCalls.length).toBeGreaterThan(0);\n      expect(\n        toolCalls.some(\n          (tc) => tc.toolName === \"screenshot\" || tc.toolName === \"ariaTree\",\n        ),\n      ).toBe(true);\n    });\n  });\n\n  test.describe(\"Streaming callbacks (stream: true)\", () => {\n    test(\"onStepFinish callback is called for each step in stream mode\", async () => {\n      test.setTimeout(60000);\n\n      const stepFinishEvents: StepResult<ToolSet>[] = [];\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"What is this page? Describe it briefly.\",\n        maxSteps: 5,\n        callbacks: {\n          onStepFinish: async (event) => {\n            stepFinishEvents.push(event);\n          },\n        },\n      });\n\n      // Consume the stream\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume\n      }\n\n      // Wait for result to complete\n      await streamResult.result;\n\n      // Should have at least one step finish event\n      expect(stepFinishEvents.length).toBeGreaterThan(0);\n    });\n\n    test(\"onChunk callback is called for each chunk\", async () => {\n      test.setTimeout(60000);\n\n      let chunkCount = 0;\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"Say hello briefly and describe the page.\",\n        maxSteps: 3,\n        callbacks: {\n          onChunk: async () => {\n            chunkCount++;\n          },\n        },\n      });\n\n      // Consume the stream\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume\n      }\n\n      await streamResult.result;\n\n      // Should have received chunks\n      expect(chunkCount).toBeGreaterThan(0);\n    });\n\n    test(\"onFinish callback is called when stream completes\", async () => {\n      test.setTimeout(60000);\n\n      let finishCalled = false;\n      let finishEvent: unknown = null;\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"Simply describe the page briefly.\",\n        maxSteps: 3,\n        callbacks: {\n          onFinish: (event) => {\n            finishCalled = true;\n            finishEvent = event;\n          },\n        },\n      });\n\n      // Consume the stream\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume\n      }\n\n      await streamResult.result;\n\n      // onFinish should have been called\n      expect(finishCalled).toBe(true);\n      expect(finishEvent).not.toBeNull();\n    });\n\n    test(\"prepareStep callback works in stream mode\", async () => {\n      test.setTimeout(60000);\n\n      let prepareStepCallCount = 0;\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"Simply describe the page briefly.\",\n        maxSteps: 3,\n        callbacks: {\n          prepareStep: async (stepContext) => {\n            prepareStepCallCount++;\n            return stepContext;\n          },\n        },\n      });\n\n      // Consume the stream\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume\n      }\n\n      await streamResult.result;\n\n      // prepareStep should have been called at least once\n      expect(prepareStepCallCount).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe(\"Streaming-only callbacks runtime validation\", () => {\n    test(\"throws StreamingCallbacksInNonStreamingModeError when onChunk is used\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: {\n            onChunk: (() => {}) as never,\n          },\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StreamingCallbacksInNonStreamingModeError);\n        expect(\n          (error as StreamingCallbacksInNonStreamingModeError).invalidCallbacks,\n        ).toEqual([\"onChunk\"]);\n      }\n    });\n\n    test(\"throws StreamingCallbacksInNonStreamingModeError when onFinish is used\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: {\n            onFinish: (() => {}) as never,\n          },\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StreamingCallbacksInNonStreamingModeError);\n        expect(\n          (error as StreamingCallbacksInNonStreamingModeError).invalidCallbacks,\n        ).toEqual([\"onFinish\"]);\n      }\n    });\n\n    test(\"throws StreamingCallbacksInNonStreamingModeError when onError is used\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: {\n            onError: (() => {}) as never,\n          },\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StreamingCallbacksInNonStreamingModeError);\n        expect(\n          (error as StreamingCallbacksInNonStreamingModeError).invalidCallbacks,\n        ).toEqual([\"onError\"]);\n      }\n    });\n\n    test(\"throws StreamingCallbacksInNonStreamingModeError when onAbort is used\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: {\n            onAbort: (() => {}) as never,\n          },\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StreamingCallbacksInNonStreamingModeError);\n        expect(\n          (error as StreamingCallbacksInNonStreamingModeError).invalidCallbacks,\n        ).toEqual([\"onAbort\"]);\n      }\n    });\n\n    test(\"error includes all invalid callbacks when multiple are used\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: {\n            onChunk: (() => {}) as never,\n            onFinish: (() => {}) as never,\n          },\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StreamingCallbacksInNonStreamingModeError);\n        expect(\n          (error as StreamingCallbacksInNonStreamingModeError).invalidCallbacks,\n        ).toEqual([\"onChunk\", \"onFinish\"]);\n      }\n    });\n  });\n\n  test.describe(\"Combined callbacks\", () => {\n    test(\"multiple callbacks can be used together\", async () => {\n      test.setTimeout(60000);\n\n      let prepareStepCount = 0;\n      let stepFinishCount = 0;\n\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      await agent.execute({\n        instruction: \"Simply describe the page briefly.\",\n        maxSteps: 3,\n        callbacks: {\n          prepareStep: async (stepContext) => {\n            prepareStepCount++;\n            return stepContext;\n          },\n          onStepFinish: async () => {\n            stepFinishCount++;\n          },\n        },\n      });\n\n      // Both callbacks should have been called\n      expect(prepareStepCount).toBeGreaterThan(0);\n      expect(stepFinishCount).toBeGreaterThan(0);\n    });\n\n    test(\"streaming with multiple callbacks\", async () => {\n      test.setTimeout(60000);\n\n      let prepareStepCount = 0;\n      let stepFinishCount = 0;\n      let chunkCount = 0;\n      let finishCalled = false;\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"Say hello briefly and describe the page.\",\n        maxSteps: 3,\n        callbacks: {\n          prepareStep: async (stepContext) => {\n            prepareStepCount++;\n            return stepContext;\n          },\n          onStepFinish: async () => {\n            stepFinishCount++;\n          },\n          onChunk: async () => {\n            chunkCount++;\n          },\n          onFinish: () => {\n            finishCalled = true;\n          },\n        },\n      });\n\n      // Consume the stream\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume\n      }\n\n      await streamResult.result;\n\n      // All callbacks should have been called\n      expect(prepareStepCount).toBeGreaterThan(0);\n      expect(stepFinishCount).toBeGreaterThan(0);\n      expect(chunkCount).toBeGreaterThan(0);\n      expect(finishCalled).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-captcha-autosolve.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { getV3TestConfig } from \"./v3.config.js\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\n\nconst isBrowserbase =\n  (process.env.STAGEHAND_BROWSER_TARGET ?? \"local\").toLowerCase() ===\n  \"browserbase\";\n\ntest.describe(\"Agent captcha auto-solve on Browserbase\", () => {\n  test.skip(!isBrowserbase, \"Requires Browserbase environment\");\n\n  let v3: V3;\n  let logs: LogLine[];\n\n  test.beforeEach(async () => {\n    logs = [];\n    v3 = new V3(\n      getV3TestConfig({\n        env: \"BROWSERBASE\",\n        verbose: 2,\n        logger: (line: LogLine) => {\n          logs.push(line);\n          console.log(`[${line.category}] ${line.message}`);\n        },\n        browserbaseSessionCreateParams: {\n          browserSettings: {\n            solveCaptchas: true,\n          },\n        },\n      }),\n    );\n    await v3.init();\n    console.log(\"BB session URL:\", v3.browserbaseSessionURL);\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"reCAPTCHA v2 auto-solve (Google demo)\", async () => {\n    test.setTimeout(180_000);\n    const page = v3.context.pages()[0];\n    // Google's official reCAPTCHA v2 demo — same URL the stealth team tests.\n    // Use domcontentloaded since BB's route interception can delay full load.\n    await page.goto(\"https://www.google.com/recaptcha/api2/demo\", {\n      waitUntil: \"domcontentloaded\",\n    });\n\n    // Give BB time to intercept the anchor request and solve the captcha\n    await new Promise((r) => setTimeout(r, 30_000));\n\n    const agent = v3.agent({\n      mode: \"dom\",\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const result = await agent.execute({\n      instruction:\n        'Click the \"Submit\" button and report the exact text shown on the result page.',\n      maxSteps: 15,\n    });\n\n    console.log(\"reCAPTCHA v2 result:\", result.message);\n\n    expect(result.completed).toBe(true);\n    expect(result.message.toLowerCase()).toContain(\"success\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-experimental-validation.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { z } from \"zod\";\nimport { tool } from \"ai\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport {\n  ExperimentalNotConfiguredError,\n  StagehandInvalidArgumentError,\n} from \"../../lib/v3/types/public/sdkErrors.js\";\n\n// Define a mock custom tool for testing\nconst mockCustomTool = tool({\n  description: \"A mock tool for testing\",\n  inputSchema: z.object({\n    input: z.string().describe(\"The input string\"),\n  }),\n  execute: async ({ input }) => {\n    return `Processed: ${input}`;\n  },\n});\n\ntest.describe(\"Stagehand agent experimental feature validation\", () => {\n  test.describe(\"Invalid argument errors\", () => {\n    let v3: V3;\n\n    test.beforeEach(async () => {\n      v3 = new V3({\n        ...v3TestConfig,\n        experimental: false,\n      });\n      await v3.init();\n    });\n\n    test.afterEach(async () => {\n      await v3?.close?.().catch(() => {});\n    });\n\n    test(\"throws StagehandInvalidArgumentError when CUA and streaming are both enabled\", async () => {\n      try {\n        v3.agent({\n          cua: true,\n          stream: true,\n          model: \"anthropic/claude-sonnet-4-20250514\",\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StagehandInvalidArgumentError);\n        expect((error as Error).message).toContain(\"streaming\");\n        expect((error as Error).message).toContain(\"not supported with CUA\");\n      }\n    });\n\n    test(\"throws StagehandInvalidArgumentError for CUA + streaming even with experimental: true\", async () => {\n      // Close the non-experimental instance\n      await v3.close();\n\n      // Create an experimental instance\n      const v3Experimental = new V3({\n        ...v3TestConfig,\n        experimental: true,\n      });\n      await v3Experimental.init();\n\n      try {\n        v3Experimental.agent({\n          cua: true,\n          stream: true,\n          model: \"anthropic/claude-sonnet-4-20250514\",\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StagehandInvalidArgumentError);\n        expect((error as Error).message).toContain(\"streaming\");\n        expect((error as Error).message).toContain(\"not supported with CUA\");\n      } finally {\n        await v3Experimental.close();\n      }\n    });\n  });\n\n  test.describe(\"Experimental feature errors without experimental: true\", () => {\n    let v3: V3;\n\n    test.beforeEach(async () => {\n      v3 = new V3({\n        ...v3TestConfig,\n        experimental: false,\n      });\n      await v3.init();\n    });\n\n    test.afterEach(async () => {\n      await v3?.close?.().catch(() => {});\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for MCP integrations\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-sonnet-4-20250514\",\n        integrations: [\"https://mcp.example.com\"],\n      });\n\n      try {\n        await agent.execute(\"test\");\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\n          \"MCP integrations and custom tools\",\n        );\n      }\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for custom tools\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-sonnet-4-20250514\",\n        tools: {\n          mockCustomTool,\n        },\n      });\n\n      try {\n        await agent.execute(\"test\");\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\n          \"MCP integrations and custom tools\",\n        );\n      }\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for streaming mode\", async () => {\n      try {\n        const agent = v3.agent({\n          stream: true,\n          model: \"anthropic/claude-sonnet-4-20250514\",\n        });\n        await agent.execute(\"test instruction\");\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\"streaming\");\n      }\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for callbacks\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: {\n            onStepFinish: async () => {},\n          },\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\"callbacks\");\n      }\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for abort signal\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      const controller = new AbortController();\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          signal: controller.signal,\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\"abort signal\");\n      }\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for message continuation\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          messages: [{ role: \"user\", content: \"previous message\" }],\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\"message continuation\");\n      }\n    });\n\n    test(\"throws ExperimentalNotConfiguredError listing multiple features\", async () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      const controller = new AbortController();\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          callbacks: { onStepFinish: async () => {} },\n          signal: controller.signal,\n          messages: [{ role: \"user\", content: \"previous\" }],\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        const message = (error as Error).message;\n        expect(message).toContain(\"callbacks\");\n        expect(message).toContain(\"abort signal\");\n        expect(message).toContain(\"message continuation\");\n      }\n    });\n  });\n\n  test.describe(\"CUA agent unsupported features\", () => {\n    let v3: V3;\n\n    test.beforeEach(async () => {\n      v3 = new V3({\n        ...v3TestConfig,\n        experimental: false,\n      });\n      await v3.init();\n    });\n\n    test.afterEach(async () => {\n      await v3?.close?.().catch(() => {});\n    });\n\n    test(\"throws ExperimentalNotConfiguredError for CUA with integrations\", async () => {\n      // MCP integrations are still an experimental feature check (not unsupported)\n      try {\n        v3.agent({\n          cua: true,\n          model: \"anthropic/claude-sonnet-4-20250514\",\n          integrations: [\"https://mcp.example.com\"],\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(ExperimentalNotConfiguredError);\n        expect((error as Error).message).toContain(\n          \"MCP integrations and custom tools\",\n        );\n      }\n    });\n\n    test(\"throws StagehandInvalidArgumentError for CUA with abort signal (not supported)\", async () => {\n      const agent = v3.agent({\n        cua: true,\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      const controller = new AbortController();\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          signal: controller.signal,\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StagehandInvalidArgumentError);\n        expect((error as Error).message).toContain(\"abort signal\");\n        expect((error as Error).message).toContain(\"not supported with CUA\");\n      }\n    });\n\n    test(\"throws StagehandInvalidArgumentError for CUA with message continuation (not supported)\", async () => {\n      const agent = v3.agent({\n        cua: true,\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          messages: [{ role: \"user\", content: \"previous message\" }],\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StagehandInvalidArgumentError);\n        expect((error as Error).message).toContain(\"message continuation\");\n        expect((error as Error).message).toContain(\"not supported with CUA\");\n      }\n    });\n\n    test(\"throws StagehandInvalidArgumentError for CUA with multiple unsupported features\", async () => {\n      const agent = v3.agent({\n        cua: true,\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      const controller = new AbortController();\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          signal: controller.signal,\n          messages: [{ role: \"user\", content: \"previous message\" }],\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StagehandInvalidArgumentError);\n        const message = (error as Error).message;\n        expect(message).toContain(\"abort signal\");\n        expect(message).toContain(\"message continuation\");\n        expect(message).toContain(\"are not supported with CUA\");\n      }\n    });\n\n    test(\"throws StagehandInvalidArgumentError for CUA unsupported features even with experimental: true\", async () => {\n      // Close the non-experimental instance\n      await v3.close();\n\n      // Create an experimental instance\n      const v3Experimental = new V3({\n        ...v3TestConfig,\n        experimental: true,\n      });\n      await v3Experimental.init();\n\n      const agent = v3Experimental.agent({\n        cua: true,\n        model: \"anthropic/claude-sonnet-4-20250514\",\n      });\n\n      const controller = new AbortController();\n      try {\n        await agent.execute({\n          instruction: \"test\",\n          signal: controller.signal,\n        });\n        throw new Error(\"Expected error to be thrown\");\n      } catch (error) {\n        expect(error).toBeInstanceOf(StagehandInvalidArgumentError);\n        expect((error as Error).message).toContain(\"not supported with CUA\");\n      } finally {\n        await v3Experimental.close();\n      }\n    });\n  });\n\n  test.describe(\"Valid configurations with experimental: true\", () => {\n    let v3: V3;\n\n    test.beforeEach(async () => {\n      v3 = new V3({\n        ...v3TestConfig,\n        experimental: true,\n      });\n      await v3.init();\n    });\n\n    test.afterEach(async () => {\n      await v3?.close?.().catch(() => {});\n    });\n\n    test(\"allows CUA without streaming\", () => {\n      expect(() =>\n        v3.agent({\n          cua: true,\n          model: \"anthropic/claude-sonnet-4-20250514\",\n        }),\n      ).not.toThrow();\n    });\n\n    test(\"allows streaming mode\", () => {\n      expect(() =>\n        v3.agent({\n          stream: true,\n          model: \"anthropic/claude-sonnet-4-20250514\",\n        }),\n      ).not.toThrow();\n    });\n\n    test(\"allows basic agent without experimental features\", async () => {\n      const v3NonExperimental = new V3({\n        ...v3TestConfig,\n        experimental: false,\n      });\n      await v3NonExperimental.init();\n\n      try {\n        // This should work - just creating a basic agent with no experimental features\n        expect(() =>\n          v3NonExperimental.agent({\n            model: \"anthropic/claude-sonnet-4-20250514\",\n          }),\n        ).not.toThrow();\n      } finally {\n        await v3NonExperimental.close();\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-hybrid-mode.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { createAgentTools } from \"../../lib/v3/agent/tools/index.js\";\nimport { buildAgentSystemPrompt } from \"../../lib/v3/agent/prompts/agentSystemPrompt.js\";\nimport type { StepResult, ToolSet } from \"ai\";\n\ntest.describe(\"Stagehand agent hybrid mode\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3({\n      ...v3TestConfig,\n      experimental: true,\n    });\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test.describe(\"Tool filtering by mode\", () => {\n    test(\"DOM mode includes DOM-based tools and excludes coordinate-based tools\", () => {\n      const tools = createAgentTools(v3, { mode: \"dom\" });\n\n      // DOM mode should have these tools\n      expect(tools).toHaveProperty(\"act\");\n      expect(tools).toHaveProperty(\"fillForm\");\n      expect(tools).toHaveProperty(\"ariaTree\");\n      expect(tools).toHaveProperty(\"screenshot\");\n      expect(tools).toHaveProperty(\"extract\");\n      expect(tools).toHaveProperty(\"goto\");\n      expect(tools).toHaveProperty(\"scroll\");\n      expect(tools).toHaveProperty(\"wait\");\n      expect(tools).toHaveProperty(\"navback\");\n      expect(tools).toHaveProperty(\"keys\");\n      expect(tools).toHaveProperty(\"think\");\n\n      // DOM mode should NOT have coordinate-based tools\n      expect(tools).not.toHaveProperty(\"click\");\n      expect(tools).not.toHaveProperty(\"type\");\n      expect(tools).not.toHaveProperty(\"dragAndDrop\");\n      expect(tools).not.toHaveProperty(\"clickAndHold\");\n      expect(tools).not.toHaveProperty(\"fillFormVision\");\n    });\n\n    test(\"Hybrid mode includes coordinate-based tools and excludes DOM fillForm\", () => {\n      const tools = createAgentTools(v3, { mode: \"hybrid\" });\n\n      // Hybrid mode should have coordinate-based tools\n      expect(tools).toHaveProperty(\"click\");\n      expect(tools).toHaveProperty(\"type\");\n      expect(tools).toHaveProperty(\"dragAndDrop\");\n      expect(tools).toHaveProperty(\"clickAndHold\");\n      expect(tools).toHaveProperty(\"fillFormVision\");\n\n      // Hybrid mode should also have common tools\n      expect(tools).toHaveProperty(\"act\");\n      expect(tools).toHaveProperty(\"ariaTree\");\n      expect(tools).toHaveProperty(\"screenshot\");\n      expect(tools).toHaveProperty(\"extract\");\n      expect(tools).toHaveProperty(\"goto\");\n      expect(tools).toHaveProperty(\"scroll\");\n      expect(tools).toHaveProperty(\"wait\");\n      expect(tools).toHaveProperty(\"navback\");\n      expect(tools).toHaveProperty(\"keys\");\n      expect(tools).toHaveProperty(\"think\");\n\n      // Hybrid mode should NOT have DOM-based fillForm\n      expect(tools).not.toHaveProperty(\"fillForm\");\n    });\n\n    test(\"Default mode is DOM when not specified\", () => {\n      const tools = createAgentTools(v3, {});\n\n      // Should behave like DOM mode\n      expect(tools).toHaveProperty(\"fillForm\");\n      expect(tools).not.toHaveProperty(\"click\");\n      expect(tools).not.toHaveProperty(\"type\");\n    });\n  });\n\n  test.describe(\"System prompt generation\", () => {\n    test(\"DOM mode system prompt emphasizes ariaTree and act tool\", () => {\n      const prompt = buildAgentSystemPrompt({\n        url: \"https://example.com\",\n        executionInstruction: \"Test instruction\",\n        mode: \"dom\",\n      });\n\n      // DOM mode should prioritize ariaTree\n      expect(prompt).toContain(\"ariaTree\");\n      expect(prompt).toContain(\"act\");\n      expect(prompt).toContain(\"fillForm\");\n\n      // Should have DOM-specific strategy\n      expect(prompt).toContain(\"Use act tool for all clicking and typing\");\n      expect(prompt).toContain(\"Always check ariaTree first\");\n    });\n\n    test(\"Hybrid mode system prompt emphasizes screenshot and coordinate tools\", () => {\n      const prompt = buildAgentSystemPrompt({\n        url: \"https://example.com\",\n        executionInstruction: \"Test instruction\",\n        mode: \"hybrid\",\n      });\n\n      // Hybrid mode should have coordinate-based tools mentioned\n      expect(prompt).toContain(\"click\");\n      expect(prompt).toContain(\"type\");\n      expect(prompt).toContain(\"fillFormVision\");\n      expect(prompt).toContain(\"dragAndDrop\");\n\n      // Should have hybrid-specific strategy\n      expect(prompt).toContain(\n        \"Use specific tools (click, type) when elements are visible\",\n      );\n      expect(prompt).toContain(\"Always use screenshot\");\n    });\n\n    test(\"System prompt includes custom instructions when provided\", () => {\n      const customInstructions = \"Always be polite and thorough\";\n      const prompt = buildAgentSystemPrompt({\n        url: \"https://example.com\",\n        executionInstruction: \"Test instruction\",\n        mode: \"dom\",\n        systemInstructions: customInstructions,\n      });\n\n      expect(prompt).toContain(\"customInstructions\");\n      expect(prompt).toContain(customInstructions);\n    });\n\n    test(\"System prompt includes captcha instructions when captchasAutoSolve is true\", () => {\n      const prompt = buildAgentSystemPrompt({\n        url: \"https://example.com\",\n        executionInstruction: \"Test instruction\",\n        mode: \"dom\",\n        captchasAutoSolve: true,\n      });\n\n      expect(prompt).toContain(\"captcha\");\n      expect(prompt).toContain(\"automatically detected and solved\");\n    });\n\n    test(\"System prompt does not include captcha instructions when captchasAutoSolve is false\", () => {\n      const prompt = buildAgentSystemPrompt({\n        url: \"https://example.com\",\n        executionInstruction: \"Test instruction\",\n        mode: \"dom\",\n        captchasAutoSolve: false,\n      });\n\n      expect(prompt).not.toContain(\"automatically detected and solved\");\n    });\n  });\n\n  test.describe(\"Agent creation with mode\", () => {\n    test(\"agent({ mode: 'dom' }) creates DOM-mode agent\", () => {\n      const agent = v3.agent({\n        mode: \"dom\",\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      expect(agent).toHaveProperty(\"execute\");\n    });\n\n    test(\"agent({ mode: 'hybrid' }) creates hybrid-mode agent\", () => {\n      const agent = v3.agent({\n        mode: \"hybrid\",\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      expect(agent).toHaveProperty(\"execute\");\n    });\n\n    test(\"agent without mode defaults to DOM mode\", () => {\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      expect(agent).toHaveProperty(\"execute\");\n    });\n\n    test(\"hybrid mode can be combined with streaming\", () => {\n      const agent = v3.agent({\n        mode: \"hybrid\",\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      expect(agent).toHaveProperty(\"execute\");\n    });\n  });\n\n  test.describe(\"Hybrid mode execution\", () => {\n    test(\"hybrid mode agent uses coordinate-based tools when available\", async () => {\n      test.setTimeout(90000);\n\n      const toolCalls: Array<{ toolName: string; input: unknown }> = [];\n\n      const agent = v3.agent({\n        mode: \"hybrid\",\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      await agent.execute({\n        instruction:\n          \"Take a screenshot to see the page, then describe what you see briefly and mark the task as complete.\",\n        maxSteps: 5,\n        callbacks: {\n          onStepFinish: async (event: StepResult<ToolSet>) => {\n            if (event.toolCalls) {\n              for (const tc of event.toolCalls) {\n                toolCalls.push({\n                  toolName: tc.toolName,\n                  input: tc.input,\n                });\n              }\n            }\n          },\n        },\n      });\n\n      // Should have captured tool calls\n      expect(toolCalls.length).toBeGreaterThan(0);\n\n      const toolNames = toolCalls.map((tc) => tc.toolName);\n      // Should include screenshot (hybrid mode emphasizes visual)\n      expect(toolNames).toContain(\"screenshot\");\n    });\n\n    test(\"DOM mode agent uses DOM-based tools\", async () => {\n      test.setTimeout(90000);\n\n      const toolCalls: Array<{ toolName: string; input: unknown }> = [];\n\n      const agent = v3.agent({\n        mode: \"dom\",\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      await agent.execute({\n        instruction:\n          \"Use the ariaTree to understand the page, then provide the final requested output or a summary of the page.\",\n        maxSteps: 5,\n        callbacks: {\n          onStepFinish: async (event: StepResult<ToolSet>) => {\n            if (event.toolCalls) {\n              for (const tc of event.toolCalls) {\n                toolCalls.push({\n                  toolName: tc.toolName,\n                  input: tc.input,\n                });\n              }\n            }\n          },\n        },\n      });\n\n      // Should have captured tool calls\n      expect(toolCalls.length).toBeGreaterThan(0);\n\n      // Should include ariaTree (DOM mode emphasizes aria-based interaction)\n      const toolNames = toolCalls.map((tc) => tc.toolName);\n      expect(toolNames).toContain(\"ariaTree\");\n    });\n  });\n\n  test.describe(\"Scroll tool variants by mode\", () => {\n    test(\"DOM mode uses simple scroll tool without coordinates\", () => {\n      const tools = createAgentTools(v3, { mode: \"dom\" });\n\n      expect(tools).toHaveProperty(\"scroll\");\n      // The DOM scroll tool should exist\n      expect(typeof tools.scroll).toBe(\"object\");\n    });\n\n    test(\"Hybrid mode uses vision scroll tool with optional coordinates\", () => {\n      const tools = createAgentTools(v3, { mode: \"hybrid\" });\n\n      expect(tools).toHaveProperty(\"scroll\");\n      // The hybrid scroll tool should exist\n      expect(typeof tools.scroll).toBe(\"object\");\n    });\n  });\n\n  test.describe(\"Keys tool availability in both modes\", () => {\n    test(\"Keys tool is available in DOM mode\", () => {\n      const tools = createAgentTools(v3, { mode: \"dom\" });\n      expect(tools).toHaveProperty(\"keys\");\n    });\n\n    test(\"Keys tool is available in hybrid mode\", () => {\n      const tools = createAgentTools(v3, { mode: \"hybrid\" });\n      expect(tools).toHaveProperty(\"keys\");\n    });\n  });\n\n  test.describe(\"Think tool availability\", () => {\n    test(\"Think tool is available in DOM mode\", () => {\n      const tools = createAgentTools(v3, { mode: \"dom\" });\n      expect(tools).toHaveProperty(\"think\");\n    });\n\n    test(\"Think tool is available in hybrid mode\", () => {\n      const tools = createAgentTools(v3, { mode: \"hybrid\" });\n      expect(tools).toHaveProperty(\"think\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-message-continuation.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport type { ModelMessage } from \"ai\";\n\ntest.describe(\"Stagehand agent message continuation\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3({\n      ...v3TestConfig,\n      experimental: true,\n    });\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"execute returns messages in the result\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    const result = await agent.execute({\n      instruction: \"What is the title of this page? Describe it briefly.\",\n      maxSteps: 5,\n    });\n\n    // Result should contain messages\n    expect(result.messages).toBeDefined();\n    expect(Array.isArray(result.messages)).toBe(true);\n    expect(result.messages!.length).toBeGreaterThan(0);\n\n    // First message should be the user instruction\n    const firstMessage = result.messages![0];\n    expect(firstMessage.role).toBe(\"user\");\n  });\n\n  test(\"can continue conversation with previous messages\", async () => {\n    test.setTimeout(120000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    // First execution\n    const result1 = await agent.execute({\n      instruction: \"What is the title of this page? Describe it briefly.\",\n      maxSteps: 5,\n    });\n\n    expect(result1.messages).toBeDefined();\n    expect(result1.messages!.length).toBeGreaterThan(0);\n\n    // Second execution continuing from first\n    const result2 = await agent.execute({\n      instruction:\n        \"Based on what you just told me, is this a simple or complex website? Answer briefly.\",\n      maxSteps: 5,\n      messages: result1.messages,\n    });\n\n    expect(result2.messages).toBeDefined();\n    // Second result should have more messages (includes first conversation)\n    expect(result2.messages!.length).toBeGreaterThan(result1.messages!.length);\n  });\n\n  test(\"messages include tool calls and results\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    const result = await agent.execute({\n      instruction:\n        \"Use the ariaTree tool to see the page, then describe what you found briefly.\",\n      maxSteps: 5,\n    });\n\n    expect(result.messages).toBeDefined();\n\n    // Verify there are assistant messages\n    const assistantMessages = result.messages!.filter(\n      (m: ModelMessage) => m.role === \"assistant\",\n    );\n    expect(assistantMessages.length).toBeGreaterThan(0);\n\n    // Verify at least one assistant message contains tool calls\n    const hasToolCalls = assistantMessages.some((m: ModelMessage) => {\n      if (Array.isArray(m.content)) {\n        return m.content.some(\n          (part) => typeof part === \"object\" && part.type === \"tool-call\",\n        );\n      }\n      return false;\n    });\n    expect(hasToolCalls).toBe(true);\n\n    // Verify there are tool result messages\n    const hasToolResults = result.messages!.some(\n      (m: ModelMessage) => m.role === \"tool\",\n    );\n    expect(hasToolResults).toBe(true);\n  });\n\n  test(\"streaming mode also returns messages\", async () => {\n    test.setTimeout(60000);\n\n    const agent = v3.agent({\n      stream: true,\n      model: \"anthropic/claude-haiku-4-5-20251001\",\n    });\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    const streamResult = await agent.execute({\n      instruction: \"What is this page? Describe it briefly.\",\n      maxSteps: 5,\n    });\n\n    // Consume the stream\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    for await (const _ of streamResult.textStream) {\n      // Just consume\n    }\n\n    const result = await streamResult.result;\n\n    // Result should contain messages\n    expect(result.messages).toBeDefined();\n    expect(Array.isArray(result.messages)).toBe(true);\n    expect(result.messages!.length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/agent-streaming.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport type { AgentResult } from \"../../lib/v3/types/public/agent.js\";\n\ntest.describe(\"Stagehand agent streaming behavior\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3({\n      ...v3TestConfig,\n      experimental: true, // Required for streaming\n    });\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test.describe(\"agent({ stream: true })\", () => {\n    test(\"AgentStreamResult has textStream as async iterable\", async () => {\n      test.setTimeout(60000);\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      // Navigate to a simple page first\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"What is the title of this page? Describe it briefly.\",\n        maxSteps: 3,\n      });\n\n      // Verify it's an AgentStreamResult with streaming capabilities\n      expect(streamResult).toHaveProperty(\"textStream\");\n      expect(streamResult).toHaveProperty(\"result\");\n\n      // textStream should be async iterable\n      expect(typeof streamResult.textStream[Symbol.asyncIterator]).toBe(\n        \"function\",\n      );\n\n      // result should be a promise\n      expect(streamResult.result).toBeInstanceOf(Promise);\n    });\n\n    test(\"textStream yields chunks incrementally\", async () => {\n      test.setTimeout(60000);\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"Say hello briefly.\",\n        maxSteps: 3,\n      });\n\n      // Collect chunks from the stream\n      const chunks: string[] = [];\n      for await (const chunk of streamResult.textStream) {\n        chunks.push(chunk);\n      }\n\n      // Should have received at least some chunks (streaming behavior)\n      // The exact content depends on the LLM response\n      expect(Array.isArray(chunks)).toBe(true);\n      expect(chunks.length).toBeGreaterThan(0);\n    });\n\n    test(\"result promise resolves to AgentResult after stream completes\", async () => {\n      test.setTimeout(60000);\n\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const streamResult = await agent.execute({\n        instruction: \"What is this page about? Describe it briefly.\",\n        maxSteps: 5,\n      });\n\n      // Consume the stream first\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      for await (const _ of streamResult.textStream) {\n        // Just consume\n      }\n\n      // Now get the final result\n      const finalResult: AgentResult = await streamResult.result;\n\n      // Verify it's a proper AgentResult\n      expect(finalResult).toHaveProperty(\"success\");\n      expect(finalResult).toHaveProperty(\"message\");\n      expect(finalResult).toHaveProperty(\"actions\");\n      expect(finalResult).toHaveProperty(\"completed\");\n      expect(typeof finalResult.success).toBe(\"boolean\");\n      expect(typeof finalResult.message).toBe(\"string\");\n      expect(Array.isArray(finalResult.actions)).toBe(true);\n    });\n  });\n\n  test.describe(\"agent({ stream: false }) or agent()\", () => {\n    test(\"execute returns AgentResult without streaming properties\", async () => {\n      test.setTimeout(60000);\n\n      const agent = v3.agent({\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      const page = v3.context.pages()[0];\n      await page.goto(\"https://example.com\");\n\n      const result = await agent.execute({\n        instruction: \"What is this page? Describe it briefly.\",\n        maxSteps: 3,\n      });\n      // Should be AgentResult, not AgentStreamResult\n      expect(result).toHaveProperty(\"success\");\n      expect(result).toHaveProperty(\"message\");\n      expect(result).toHaveProperty(\"actions\");\n      expect(result).toHaveProperty(\"completed\");\n\n      // Should NOT have streaming properties\n      expect(result).not.toHaveProperty(\"textStream\");\n    });\n  });\n\n  test.describe(\"CUA disables streaming\", () => {\n    test(\"throws StagehandInvalidArgumentError when cua: true and stream: true\", () => {\n      expect(() => {\n        v3.agent({\n          cua: true,\n          stream: true,\n          model: \"anthropic/claude-haiku-4-5-20251001\",\n        });\n      }).toThrow(\"streaming is not supported with CUA\");\n    });\n\n    test(\"allows cua: true without stream\", () => {\n      // Should not throw\n      const agent = v3.agent({\n        cua: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      expect(agent).toHaveProperty(\"execute\");\n    });\n\n    test(\"allows stream: true without cua\", () => {\n      // Should not throw\n      const agent = v3.agent({\n        stream: true,\n        model: \"anthropic/claude-haiku-4-5-20251001\",\n      });\n\n      expect(agent).toHaveProperty(\"execute\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/cdp-close-api-region.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { getV3TestConfig } from \"./v3.config.js\";\nimport { raceTimeout } from \"./testUtils.js\";\n\n/**\n * Full production trigger chain:\n *\n *   v3.close()\n *     → apiClient.end()           (tells hosted API to kill the BB session)\n *     → hosted API terminates BB   (CDP WebSocket closes from server side)\n *     → ctx.close() → conn.close() (awaits \"close\" on already-CLOSED WS → hangs)\n *\n * Requires:\n *   - BROWSERBASE_API_KEY / BROWSERBASE_PROJECT_ID\n *   - The Stagehand hosted API to be reachable\n *   - A non-us-west-2 region (higher latency makes the race reliably trigger)\n */\ntest.describe(\"v3.close() with Stagehand API + non-default region\", () => {\n  test(\"close resolves instead of hanging\", async () => {\n    const apiKey = process.env.BROWSERBASE_API_KEY;\n    const projectId = process.env.BROWSERBASE_PROJECT_ID;\n\n    test.skip(\n      !apiKey || !projectId,\n      \"BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are required\",\n    );\n\n    const v3 = new V3(\n      getV3TestConfig({\n        disableAPI: false,\n        browserbaseSessionCreateParams: { region: \"ap-southeast-1\" },\n      }),\n    );\n\n    await v3.init();\n\n    // Verify the instance is functional.\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>api-region-test</body></html>\");\n\n    // Call v3.close() — the normal production shutdown path.\n    // Internally: apiClient.end() → hosted API kills BB session →\n    // CDP WebSocket closes → conn.close() tries to close already-closed WS.\n    // Without the fix this hangs forever.\n    const result = await raceTimeout(\n      v3.close().then(() => \"resolved\" as const),\n      30_000,\n    );\n\n    expect(result).toBe(\"resolved\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/cdp-connection-close.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport WebSocket from \"ws\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { raceTimeout } from \"./testUtils.js\";\n\ntest.describe(\"CdpConnection.close() after external WebSocket close\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    // Best-effort teardown – don't let a hung close block the suite.\n    try {\n      await raceTimeout(v3?.close?.(), 5_000);\n    } catch {\n      // ignore\n    }\n  });\n\n  test(\"v3.close() resolves after the CDP WebSocket is already closed\", async () => {\n    // Verify the V3 instance is functional.\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>close-test</body></html>\");\n\n    const conn = v3.context.conn;\n\n    // Unhook the V3-level _onCdpClosed handler so it doesn't trigger\n    // _immediateShutdown in the background (we want to isolate the\n    // CdpConnection.close() hang).\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const onCdpClosed = (v3 as any)._onCdpClosed;\n    if (onCdpClosed) {\n      conn.offTransportClosed(onCdpClosed);\n    }\n\n    // Wait for the transport-close event to be fully processed.\n    const transportClosed = new Promise<void>((resolve) => {\n      conn.onTransportClosed(() => resolve());\n    });\n\n    // Terminate the underlying WebSocket – simulates the hosted API\n    // killing the Browserbase session, which closes the CDP socket\n    // from the server side.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const ws: WebSocket = (conn as any).ws;\n    ws.terminate();\n\n    await transportClosed;\n\n    // Now call v3.close(). Internally this calls ctx.close() →\n    // conn.close(), which awaits a \"close\" event on an already-CLOSED\n    // WebSocket. Without the fix this promise never resolves.\n    const result = await raceTimeout(\n      v3.close().then(() => \"resolved\" as const),\n      5_000,\n    );\n\n    expect(result).toBe(\"resolved\");\n  });\n\n  test(\"inflight CDP calls reject when the WebSocket is terminated\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>inflight-test</body></html>\");\n\n    const conn = v3.context.conn;\n\n    // Unhook the V3-level handler as above.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const onCdpClosed = (v3 as any)._onCdpClosed;\n    if (onCdpClosed) {\n      conn.offTransportClosed(onCdpClosed);\n    }\n\n    // Send a long-running CDP call that the server will never answer.\n    const pending = conn.send(\"Runtime.evaluate\", {\n      expression: \"new Promise(r => setTimeout(() => r('done'), 60000))\",\n      awaitPromise: true,\n    });\n\n    // Terminate the WebSocket while the call is inflight.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const ws: WebSocket = (conn as any).ws;\n    ws.terminate();\n\n    // The pending promise must reject – not hang forever.\n    const result = await raceTimeout(\n      pending.then(() => \"resolved\" as const).catch(() => \"rejected\" as const),\n      5_000,\n    );\n\n    expect(result).toBe(\"rejected\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/cdp-session-detached.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { chromium as playwrightChromium } from \"playwright\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"CDP session detach handling\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"rejects inflight CDP calls when a target is closed\", async () => {\n    const unhandled: unknown[] = [];\n    const onUnhandled = (reason: unknown) => {\n      unhandled.push(reason);\n    };\n\n    process.on(\"unhandledRejection\", onUnhandled);\n\n    let pwBrowser: Awaited<\n      ReturnType<typeof playwrightChromium.connectOverCDP>\n    > | null = null;\n\n    try {\n      pwBrowser = await playwrightChromium.connectOverCDP(v3.connectURL());\n      const pwContext = pwBrowser.contexts()[0];\n      const pwPage = pwContext.pages()[0];\n\n      const v3Page = v3.context.pages()[0];\n      await v3Page.goto(\"data:text/html,<html><body>cdp</body></html>\");\n\n      const pending = v3Page.sendCDP(\"Runtime.evaluate\", {\n        expression: \"new Promise(r => setTimeout(() => r('done'), 5000))\",\n        awaitPromise: true,\n        returnByValue: true,\n      });\n\n      await pwPage.close();\n\n      await expect(pending).rejects.toThrow(\n        /No Page found for target closed before CDP response/,\n      );\n\n      await new Promise((r) => setTimeout(r, 50));\n      expect(unhandled).toHaveLength(0);\n    } finally {\n      process.off(\"unhandledRejection\", onUnhandled);\n      await pwBrowser?.close().catch(() => {});\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/click-count.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\n// Keep double-click verification event-based and deterministic.\n// Time-delta counters (Date.now() between mousedowns) are flaky at ms boundaries\n// and can miss valid double-clicks when synthetic input lands in the same millisecond.\nconst doubleClickFixtureUrl = `data:text/html,${encodeURIComponent(`<!DOCTYPE html>\n<html>\n  <body>\n    <div id=\"target\" style=\"width: 240px; height: 120px; border: 1px solid #000;\">target</div>\n    <input id=\"clickCount\" value=\"0\" readonly />\n    <input id=\"dblClickCount\" value=\"0\" readonly />\n    <input id=\"lastClickDetail\" value=\"0\" readonly />\n    <input id=\"lastDblClickDetail\" value=\"0\" readonly />\n    <script>\n      const target = document.getElementById(\"target\");\n      const clickCount = document.getElementById(\"clickCount\");\n      const dblClickCount = document.getElementById(\"dblClickCount\");\n      const lastClickDetail = document.getElementById(\"lastClickDetail\");\n      const lastDblClickDetail = document.getElementById(\"lastDblClickDetail\");\n      let clicks = 0;\n      let dblClicks = 0;\n\n      target.addEventListener(\"click\", (event) => {\n        clicks += 1;\n        clickCount.value = String(clicks);\n        lastClickDetail.value = String(event.detail);\n      });\n\n      target.addEventListener(\"dblclick\", (event) => {\n        dblClicks += 1;\n        dblClickCount.value = String(dblClicks);\n        lastDblClickDetail.value = String(event.detail);\n      });\n    </script>\n  </body>\n</html>`)}`;\n\ntest.describe(\"Locator and Page click methods\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"locator.click() performs single click by default\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/click-test/\",\n    );\n\n    // Wait for page to be fully loaded\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    // Get initial count\n    const countDisplay = page.locator(\"#count\");\n    const initialCount = await countDisplay.inputValue();\n    expect(initialCount).toBe(\"0\");\n\n    // Perform single click on the textarea (the clickable area)\n    const clickArea = page.locator(\"#textarea\");\n    await clickArea.click();\n\n    // Verify count incremented by 1\n    const newCount = await countDisplay.inputValue();\n    expect(newCount).toBe(\"1\");\n  });\n\n  test(\"locator.click() with clickCount: 2 performs double-click\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(doubleClickFixtureUrl);\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    const countDisplay = page.locator(\"#clickCount\");\n    const dcCountDisplay = page.locator(\"#dblClickCount\");\n    const clickDetailDisplay = page.locator(\"#lastClickDetail\");\n    const dblClickDetailDisplay = page.locator(\"#lastDblClickDetail\");\n\n    const initialCount = await countDisplay.inputValue();\n    const initialDcCount = await dcCountDisplay.inputValue();\n    expect(initialCount).toBe(\"0\");\n    expect(initialDcCount).toBe(\"0\");\n\n    const clickArea = page.locator(\"#target\");\n    await clickArea.click({ clickCount: 2 });\n\n    const newCount = await countDisplay.inputValue();\n    expect(newCount).toBe(\"2\");\n\n    const newDcCount = await dcCountDisplay.inputValue();\n    expect(newDcCount).toBe(\"1\");\n    // `dblclick` is the browser-level contract for double-click behavior.\n    // Verifying `detail=2` ensures the click sequence is recognized as a true multi-click.\n    expect(await clickDetailDisplay.inputValue()).toBe(\"2\");\n    expect(await dblClickDetailDisplay.inputValue()).toBe(\"2\");\n  });\n\n  test(\"locator.click() with clickCount: 3 performs triple-click\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/click-test/\",\n    );\n\n    // Wait for page to be fully loaded\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    const countDisplay = page.locator(\"#count\");\n    const initialCount = await countDisplay.inputValue();\n    expect(initialCount).toBe(\"0\");\n\n    // Perform triple-click on the textarea\n    const clickArea = page.locator(\"#textarea\");\n    await clickArea.click({ clickCount: 3 });\n\n    // Verify count incremented by 3\n    const newCount = await countDisplay.inputValue();\n    expect(newCount).toBe(\"3\");\n  });\n\n  test(\"page.click() performs single click with coordinates\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/click-test/\",\n    );\n\n    // Wait for page to be fully loaded\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    // Get initial count\n    const countDisplay = page.locator(\"#count\");\n    const initialCount = await countDisplay.inputValue();\n    expect(initialCount).toBe(\"0\");\n\n    // Get the centroid of the textarea to click\n    const clickArea = page.locator(\"#textarea\");\n    const { x, y } = await clickArea.centroid();\n\n    // Perform single click using page.click() with coordinates\n    await page.click(x, y);\n\n    // Verify count incremented by 1\n    const newCount = await countDisplay.inputValue();\n    expect(newCount).toBe(\"1\");\n  });\n\n  test(\"page.click() with clickCount: 2 performs double-click\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(doubleClickFixtureUrl);\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    const countDisplay = page.locator(\"#clickCount\");\n    const dcCountDisplay = page.locator(\"#dblClickCount\");\n    const clickDetailDisplay = page.locator(\"#lastClickDetail\");\n    const dblClickDetailDisplay = page.locator(\"#lastDblClickDetail\");\n\n    const initialCount = await countDisplay.inputValue();\n    const initialDcCount = await dcCountDisplay.inputValue();\n    expect(initialCount).toBe(\"0\");\n    expect(initialDcCount).toBe(\"0\");\n\n    const clickArea = page.locator(\"#target\");\n    const { x, y } = await clickArea.centroid();\n\n    await page.click(x, y, { clickCount: 2 });\n\n    const newCount = await countDisplay.inputValue();\n    expect(newCount).toBe(\"2\");\n\n    const newDcCount = await dcCountDisplay.inputValue();\n    expect(newDcCount).toBe(\"1\");\n    // `dblclick` is the browser-level contract for double-click behavior.\n    // Verifying `detail=2` ensures the click sequence is recognized as a true multi-click.\n    expect(await clickDetailDisplay.inputValue()).toBe(\"2\");\n    expect(await dblClickDetailDisplay.inputValue()).toBe(\"2\");\n  });\n\n  test(\"page.click() with clickCount: 3 performs triple-click\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/click-test/\",\n    );\n\n    // Wait for page to be fully loaded\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    const countDisplay = page.locator(\"#count\");\n    const initialCount = await countDisplay.inputValue();\n    expect(initialCount).toBe(\"0\");\n\n    // Get the centroid of the textarea to click\n    const clickArea = page.locator(\"#textarea\");\n    const { x, y } = await clickArea.centroid();\n\n    // Perform triple-click using page.click() with coordinates\n    await page.click(x, y, { clickCount: 3 });\n\n    // Verify count incremented by 3\n    const newCount = await countDisplay.inputValue();\n    expect(newCount).toBe(\"3\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/connect-to-existing-browser.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\nconst PAGE_TARGET_COUNT = 5;\n\ntest.describe(\"connect to existing Browserbase session\", () => {\n  test(\"new Stagehand instance reuses an existing Browserbase session\", async () => {\n    const browserTarget = (\n      process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n    ).toLowerCase();\n    const isBrowserbase = browserTarget === \"browserbase\";\n    test.skip(!isBrowserbase, \"Requires STAGEHAND_BROWSER_TARGET=browserbase\");\n    test.skip(\n      !process.env.BROWSERBASE_API_KEY || !process.env.BROWSERBASE_PROJECT_ID,\n      \"BROWSERBASE credentials are required\",\n    );\n\n    const initialStagehand = new V3({\n      ...v3DynamicTestConfig,\n      disableAPI: true,\n    });\n    await initialStagehand.init();\n\n    let resumedStagehand: V3 | null = null;\n\n    try {\n      const ctx = initialStagehand.context;\n      const initialPage = ctx.pages()[0];\n      expect(initialPage).toBeDefined();\n\n      for (let i = 0; i < PAGE_TARGET_COUNT; i++) {\n        await ctx.newPage(`https://example.com/?tab=${i}`);\n      }\n\n      await initialPage?.close();\n      await expect\n        .poll(() => ctx.pages().length, { timeout: 15_000 })\n        .toBe(PAGE_TARGET_COUNT);\n\n      const sessionUrl = initialStagehand.connectURL();\n      expect(sessionUrl).toBeTruthy();\n\n      resumedStagehand = new V3({\n        env: \"LOCAL\",\n        verbose: 0,\n        disablePino: true,\n        disableAPI: true,\n        logger: v3DynamicTestConfig.logger,\n        localBrowserLaunchOptions: {\n          cdpUrl: sessionUrl,\n        },\n      });\n      await resumedStagehand.init();\n\n      await expect\n        .poll(() => resumedStagehand!.context.pages().length, {\n          timeout: 15_000,\n        })\n        .toBe(PAGE_TARGET_COUNT);\n\n      const resumedPagesCount = resumedStagehand.context.pages().length;\n      expect(resumedPagesCount).toBe(PAGE_TARGET_COUNT);\n    } finally {\n      await closeV3(resumedStagehand);\n      await closeV3(initialStagehand);\n    }\n  });\n\n  test(\"new Stagehand instance initializes when existing browser has zero pages\", async () => {\n    const browserTarget = (\n      process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n    ).toLowerCase();\n    const isLocal = browserTarget !== \"browserbase\";\n    test.skip(!isLocal, \"Requires STAGEHAND_BROWSER_TARGET=local\");\n\n    const initialStagehand = new V3({\n      ...v3DynamicTestConfig,\n      disableAPI: true,\n      env: \"LOCAL\",\n    });\n    await initialStagehand.init();\n\n    let resumedStagehand: V3 | null = null;\n\n    try {\n      const ctx = initialStagehand.context;\n      const pages = ctx.pages();\n      for (const page of pages) {\n        await page.close();\n      }\n\n      await expect.poll(() => ctx.pages().length, { timeout: 15_000 }).toBe(0);\n\n      const sessionUrl = initialStagehand.connectURL();\n      resumedStagehand = new V3({\n        env: \"LOCAL\",\n        verbose: 0,\n        disablePino: true,\n        disableAPI: true,\n        logger: v3DynamicTestConfig.logger,\n        localBrowserLaunchOptions: {\n          cdpUrl: sessionUrl,\n        },\n      });\n\n      await resumedStagehand.init();\n\n      await expect\n        .poll(() => resumedStagehand!.context.pages().length, {\n          timeout: 15_000,\n        })\n        .toBeGreaterThan(0);\n    } finally {\n      await closeV3(resumedStagehand);\n      await closeV3(initialStagehand);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/context-addInitScript.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { V3Context } from \"../../lib/v3/understudy/context.js\";\nimport type { Page as V3Page } from \"../../lib/v3/understudy/page.js\";\n\nconst POPUP_TIMEOUT_MS = 20_000;\n\nconst toDataUrl = (html: string): string =>\n  `data:text/html,${encodeURIComponent(html)}`;\n\nconst waitForPopupPage = async (\n  ctx: V3Context,\n  knownTargetIds: Set<string>,\n  timeoutMs = POPUP_TIMEOUT_MS,\n): Promise<V3Page> => {\n  const deadline = Date.now() + timeoutMs;\n  while (Date.now() < deadline) {\n    const popup = ctx\n      .pages()\n      .find((page) => !knownTargetIds.has(page.targetId()));\n    if (popup) return popup;\n    try {\n      const active = await ctx.awaitActivePage(500);\n      if (!knownTargetIds.has(active.targetId())) return active;\n    } catch {\n      // keep polling\n    }\n    await new Promise((resolve) => setTimeout(resolve, 50));\n  }\n  throw new Error(\"Popup page was not created\");\n};\n\ntest.describe(\"context.addInitScript\", () => {\n  let v3: V3;\n  let ctx: V3Context;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n    ctx = v3.context;\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"runs before inline document scripts on navigation\", async () => {\n    const page = await ctx.awaitActivePage();\n\n    await ctx.addInitScript(() => {\n      (window as unknown as { __fromContextInit?: string }).__fromContextInit =\n        \"injected-value\";\n    });\n\n    const html = `<!DOCTYPE html>\n      <html>\n        <body>\n          <script>\n            var value = (window && window.__fromContextInit) || 'missing';\n            document.body.dataset.initWitness = value;\n          </script>\n        </body>\n      </html>`;\n\n    await page.goto(toDataUrl(html), { waitUntil: \"load\" });\n\n    const observed = await page.evaluate(() => {\n      return document.body.dataset.initWitness;\n    });\n    expect(observed).toBe(\"injected-value\");\n  });\n\n  test(\"re-applies the script on every navigation for the same page\", async () => {\n    const page = await ctx.awaitActivePage();\n\n    await ctx.addInitScript(`\n      (function () {\n        function markVisit() {\n          var root = document.documentElement;\n          if (!root) return;\n          var current = Number(window.name || \"0\");\n          var next = current + 1;\n          window.name = String(next);\n          root.dataset.visitCount = String(next);\n        }\n        if (document.readyState === \"loading\") {\n          document.addEventListener(\"DOMContentLoaded\", markVisit, {\n            once: true,\n          });\n        } else {\n          markVisit();\n        }\n      })();\n    `);\n\n    await page.goto(toDataUrl(\"<html><body>first</body></html>\"), {\n      waitUntil: \"load\",\n    });\n    const first = await page.evaluate(() => {\n      return Number(document.documentElement.dataset.visitCount ?? \"0\");\n    });\n    expect(first).toBe(1);\n\n    await page.goto(toDataUrl(\"<html><body>second</body></html>\"), {\n      waitUntil: \"load\",\n    });\n    const second = await page.evaluate(() => {\n      return Number(document.documentElement.dataset.visitCount ?? \"0\");\n    });\n    expect(second).toBe(2);\n  });\n\n  test(\"applies script (with args) to newly created pages\", async () => {\n    const payload = { greeting: \"hi\", nested: { count: 2 } };\n\n    const initPayload = ((arg) => {\n      function setPayload() {\n        const root = document.documentElement;\n        if (!root) return;\n        root.dataset.initPayload = JSON.stringify(arg);\n      }\n      if (document.readyState === \"loading\") {\n        document.addEventListener(\"DOMContentLoaded\", setPayload, {\n          once: true,\n        });\n      } else {\n        setPayload();\n      }\n    }) as (arg: typeof payload) => void;\n    await ctx.addInitScript(initPayload, payload);\n\n    const newPage = await ctx.newPage();\n    await newPage.goto(toDataUrl(\"<html><body>child</body></html>\"), {\n      waitUntil: \"load\",\n    });\n\n    const observed = await newPage.evaluate(() => {\n      const raw = document.documentElement.dataset.initPayload;\n      return raw ? JSON.parse(raw) : undefined;\n    });\n    expect(observed).toEqual(payload);\n  });\n\n  test(\"applies script to newPage(url) on initial document\", async () => {\n    const payload = { marker: \"newPageUrl\" };\n\n    await ctx.addInitScript((arg) => {\n      function setPayload(): void {\n        const root = document.documentElement;\n        if (!root) return;\n        root.dataset.initPayload = JSON.stringify(arg);\n      }\n      if (document.readyState === \"loading\") {\n        document.addEventListener(\"DOMContentLoaded\", setPayload, {\n          once: true,\n        });\n      } else {\n        setPayload();\n      }\n    }, payload);\n\n    const newPage = await ctx.newPage(\n      toDataUrl(\"<html><body>new page</body></html>\"),\n    );\n    await newPage.waitForLoadState(\"load\");\n\n    const observed = await newPage.evaluate(() => {\n      const raw = document.documentElement.dataset.initPayload;\n      return raw ? JSON.parse(raw) : undefined;\n    });\n    expect(observed).toEqual(payload);\n  });\n\n  test(\"applies script to pages opened via link clicks\", async () => {\n    const payload = { marker: \"linkClick\" };\n\n    await ctx.addInitScript((arg) => {\n      function setPayload(): void {\n        const root = document.documentElement;\n        if (!root) return;\n        root.dataset.initPayload = JSON.stringify(arg);\n      }\n      if (document.readyState === \"loading\") {\n        document.addEventListener(\"DOMContentLoaded\", setPayload, {\n          once: true,\n        });\n      } else {\n        setPayload();\n      }\n    }, payload);\n\n    const popupUrl = \"https://example.com/\";\n    const openerHtml =\n      \"<!DOCTYPE html>\" +\n      \"<html><body>\" +\n      '<a id=\"open\" target=\"_blank\" href=\"' +\n      popupUrl +\n      '\">open</a>' +\n      \"</body></html>\";\n\n    const opener = await ctx.awaitActivePage();\n    await opener.goto(toDataUrl(openerHtml), { waitUntil: \"load\" });\n    const knownTargetIds = new Set(ctx.pages().map((p) => p.targetId()));\n    await opener.locator(\"#open\").click();\n\n    const popup = await waitForPopupPage(ctx, knownTargetIds);\n\n    await popup.waitForLoadState(\"load\");\n\n    const observed = await popup.evaluate(() => {\n      const raw = document.documentElement.dataset.initPayload;\n      return raw ? JSON.parse(raw) : undefined;\n    });\n    expect(observed).toEqual(payload);\n\n    await popup.reload({ waitUntil: \"load\" });\n    const observedAfterReload = await popup.evaluate(() => {\n      const raw = document.documentElement.dataset.initPayload;\n      return raw ? JSON.parse(raw) : undefined;\n    });\n    expect(observedAfterReload).toEqual(payload);\n  });\n\n  test(\"applies script to in-process popup\", async () => {\n    await ctx.addInitScript(() => {\n      (window as unknown as { __injected?: number }).__injected = 123;\n    });\n\n    const opener = await ctx.awaitActivePage();\n    const openerHtml =\n      \"<!DOCTYPE html>\" +\n      \"<html><body>\" +\n      '<a id=\"open\" target=\"_blank\" href=\"about:blank\">open</a>' +\n      \"</body></html>\";\n    await opener.goto(toDataUrl(openerHtml), { waitUntil: \"load\" });\n    const knownTargetIds = new Set(ctx.pages().map((p) => p.targetId()));\n    await opener.locator(\"#open\").click();\n\n    const popup = await waitForPopupPage(ctx, knownTargetIds);\n    await popup.waitForLoadState(\"load\");\n    const injected = await popup.evaluate(() => {\n      return (window as unknown as { __injected?: number }).__injected;\n    });\n    expect(injected).toBe(123);\n  });\n\n  test(\"applies script to cross-process popup and survives reload\", async () => {\n    await ctx.addInitScript(() => {\n      (window as unknown as { __injected?: number }).__injected = 123;\n    });\n\n    const opener = await ctx.awaitActivePage();\n    const openerHtml =\n      \"<!DOCTYPE html>\" +\n      \"<html><body>\" +\n      '<a id=\"open\" target=\"_blank\" href=\"https://example.com/\">open</a>' +\n      \"</body></html>\";\n    await opener.goto(toDataUrl(openerHtml), {\n      waitUntil: \"load\",\n    });\n    const knownTargetIds = new Set(ctx.pages().map((p) => p.targetId()));\n    await opener.locator(\"#open\").click();\n\n    const popup = await waitForPopupPage(ctx, knownTargetIds);\n    await popup.waitForLoadState(\"load\");\n\n    const injected = await popup.evaluate(() => {\n      return (window as unknown as { __injected?: number }).__injected;\n    });\n    expect(injected).toBe(123);\n\n    await popup.reload({ waitUntil: \"load\" });\n    const injectedAfterReload = await popup.evaluate(() => {\n      return (window as unknown as { __injected?: number }).__injected;\n    });\n    expect(injectedAfterReload).toBe(123);\n  });\n\n  test(\"applies script to cross-process popup opened via window.open and survives reload\", async () => {\n    await ctx.addInitScript(() => {\n      (window as unknown as { __injected?: number }).__injected = 789;\n    });\n\n    const opener = await ctx.awaitActivePage();\n    await opener.goto(\"about:blank\", { waitUntil: \"load\" });\n    await opener.mainFrame().evaluate(() => {\n      const button = document.createElement(\"button\");\n      button.id = \"open-via-window-open\";\n      button.textContent = \"open popup\";\n      button.addEventListener(\"click\", () => {\n        window.open(\"https://example.com/\", \"_blank\");\n      });\n      document.body.appendChild(button);\n    });\n\n    const knownTargetIds = new Set(ctx.pages().map((p) => p.targetId()));\n    await opener.locator(\"#open-via-window-open\").click();\n\n    const popup = await waitForPopupPage(ctx, knownTargetIds);\n    await popup.waitForLoadState(\"load\");\n\n    const injected = await popup.evaluate(() => {\n      return (window as unknown as { __injected?: number }).__injected;\n    });\n    expect(injected).toBe(789);\n\n    await popup.reload({ waitUntil: \"load\" });\n    const injectedAfterReload = await popup.evaluate(() => {\n      return (window as unknown as { __injected?: number }).__injected;\n    });\n    expect(injectedAfterReload).toBe(789);\n  });\n\n  test(\"context.addInitScript installs a function callable from page.evaluate\", async () => {\n    const page = await ctx.awaitActivePage();\n\n    await ctx.addInitScript(() => {\n      // installed before any navigation\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-expect-error\n      window.sayHelloFromStagehand = () => \"hello from stagehand\";\n    });\n\n    await page.goto(\"https://example.com\", { waitUntil: \"domcontentloaded\" });\n\n    const result = await page.evaluate(() => {\n      // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n      // @ts-expect-error\n      return window.sayHelloFromStagehand();\n    });\n\n    expect(result).toBe(\"hello from stagehand\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/context-extra-http-headers.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport type { Protocol } from \"devtools-protocol\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\nconst TEST_URL =\n  \"https://browserbase.github.io/stagehand-eval-sites/sites/example/\";\n\ntest.describe(\"context.setExtraHTTPHeaders\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"applies headers to navigation requests\", async () => {\n    const ctx = v3.context;\n    const page = await ctx.awaitActivePage();\n\n    await ctx.setExtraHTTPHeaders({ \"x-stagehand-test\": \"yes\" });\n\n    const internal = page as unknown as {\n      mainSession: {\n        send: (method: string, params?: unknown) => Promise<unknown>;\n        on: (event: string, handler: (params: unknown) => void) => void;\n        off: (event: string, handler: (params: unknown) => void) => void;\n      };\n    };\n\n    await internal.mainSession.send(\"Network.enable\");\n\n    const requestPromise = new Promise<Protocol.Network.RequestWillBeSentEvent>(\n      (resolve, reject) => {\n        const timeout = setTimeout(() => {\n          internal.mainSession.off(\"Network.requestWillBeSent\", handler);\n          reject(new Error(\"Timed out waiting for request\"));\n        }, 5000);\n\n        const handler = (evt: Protocol.Network.RequestWillBeSentEvent) => {\n          if (evt.type !== \"Document\") return;\n          const url = String(evt.request?.url ?? \"\");\n          if (!url.startsWith(TEST_URL)) return;\n          clearTimeout(timeout);\n          internal.mainSession.off(\"Network.requestWillBeSent\", handler);\n          resolve(evt);\n        };\n\n        internal.mainSession.on(\"Network.requestWillBeSent\", handler);\n      },\n    );\n\n    await page.goto(TEST_URL, { waitUntil: \"domcontentloaded\" });\n\n    const request = await requestPromise;\n    const headers = Object.fromEntries(\n      Object.entries(request.request.headers ?? {}).map(([key, value]) => [\n        key.toLowerCase(),\n        String(value),\n      ]),\n    );\n\n    expect(headers[\"x-stagehand-test\"]).toBe(\"yes\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/cookies.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\nconst BASE_URL =\n  \"https://browserbase.github.io/stagehand-eval-sites/sites/example/\";\n\ntest.describe(\"cookies\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"addCookies sets a cookie visible to the page\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0];\n    expect(page).toBeDefined();\n\n    await page!.goto(BASE_URL);\n\n    const name = `stagehand_cookie_${Date.now()}`;\n    await ctx.addCookies([\n      {\n        name,\n        value: \"1\",\n        url: BASE_URL,\n        httpOnly: false,\n      },\n    ]);\n\n    await page!.reload();\n\n    const cookieString = await page!.evaluate(() => document.cookie);\n    expect(cookieString).toContain(`${name}=1`);\n\n    const cookies = await ctx.cookies(BASE_URL);\n    expect(cookies.some((c) => c.name === name && c.value === \"1\")).toBe(true);\n  });\n\n  test(\"cookies() with no URL returns all cookies\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    const name = `stagehand_all_${Date.now()}`;\n    await ctx.addCookies([\n      { name, value: \"all\", url: BASE_URL, httpOnly: false },\n    ]);\n\n    const all = await ctx.cookies();\n    expect(all.some((c) => c.name === name)).toBe(true);\n  });\n\n  test(\"clearCookies() removes all cookies\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    await ctx.addCookies([\n      { name: \"to_clear_a\", value: \"1\", url: BASE_URL, httpOnly: false },\n      { name: \"to_clear_b\", value: \"2\", url: BASE_URL, httpOnly: false },\n    ]);\n\n    // Verify cookies were set\n    let cookies = await ctx.cookies(BASE_URL);\n    expect(cookies.some((c) => c.name === \"to_clear_a\")).toBe(true);\n    expect(cookies.some((c) => c.name === \"to_clear_b\")).toBe(true);\n\n    await ctx.clearCookies();\n\n    cookies = await ctx.cookies(BASE_URL);\n    expect(cookies.some((c) => c.name === \"to_clear_a\")).toBe(false);\n    expect(cookies.some((c) => c.name === \"to_clear_b\")).toBe(false);\n  });\n\n  test(\"clearCookies() with name filter removes only matching cookies\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    await ctx.addCookies([\n      { name: \"keep_me\", value: \"1\", url: BASE_URL, httpOnly: false },\n      { name: \"remove_me\", value: \"2\", url: BASE_URL, httpOnly: false },\n    ]);\n\n    await ctx.clearCookies({ name: \"remove_me\" });\n\n    const cookies = await ctx.cookies(BASE_URL);\n    expect(cookies.some((c) => c.name === \"keep_me\")).toBe(true);\n    expect(cookies.some((c) => c.name === \"remove_me\")).toBe(false);\n  });\n\n  test(\"clearCookies() with regex filter removes matching cookies\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    await ctx.addCookies([\n      { name: \"_ga_ABC\", value: \"1\", url: BASE_URL, httpOnly: false },\n      { name: \"_ga_DEF\", value: \"2\", url: BASE_URL, httpOnly: false },\n      { name: \"session\", value: \"3\", url: BASE_URL, httpOnly: false },\n    ]);\n\n    await ctx.clearCookies({ name: /^_ga/ });\n\n    const cookies = await ctx.cookies(BASE_URL);\n    expect(cookies.some((c) => c.name === \"session\")).toBe(true);\n    expect(cookies.some((c) => c.name === \"_ga_ABC\")).toBe(false);\n    expect(cookies.some((c) => c.name === \"_ga_DEF\")).toBe(false);\n  });\n\n  test(\"cookies are visible from a second page on the same domain\", async () => {\n    const ctx = v3.context;\n    const page1 = ctx.pages()[0]!;\n    await page1.goto(BASE_URL);\n\n    const name = `stagehand_multi_${Date.now()}`;\n    await ctx.addCookies([\n      { name, value: \"shared\", url: BASE_URL, httpOnly: false },\n    ]);\n\n    const page2 = await ctx.newPage();\n    await page2.goto(BASE_URL);\n\n    const cookieString = await page2.evaluate(() => document.cookie);\n    expect(cookieString).toContain(`${name}=shared`);\n  });\n\n  test(\"cookies persist across navigation to a different path\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    const name = `stagehand_nav_${Date.now()}`;\n    await ctx.addCookies([\n      {\n        name,\n        value: \"persisted\",\n        domain: \"browserbase.github.io\",\n        path: \"/\",\n        httpOnly: false,\n      },\n    ]);\n\n    // Navigate to a different path on the same domain\n    await page.goto(\"https://browserbase.github.io/stagehand-eval-sites/\");\n\n    const cookieString = await page.evaluate(() => document.cookie);\n    expect(cookieString).toContain(`${name}=persisted`);\n  });\n\n  test(\"httpOnly cookie is hidden from document.cookie but returned by cookies()\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    const name = `stagehand_http_${Date.now()}`;\n    await ctx.addCookies([\n      { name, value: \"secret\", url: BASE_URL, httpOnly: true },\n    ]);\n\n    await page.reload();\n\n    // document.cookie must NOT include httpOnly cookies\n    const cookieString = await page.evaluate(() => document.cookie);\n    expect(cookieString).not.toContain(name);\n\n    // But the context API should still return it\n    const cookies = await ctx.cookies(BASE_URL);\n    const match = cookies.find((c) => c.name === name);\n    expect(match).toBeDefined();\n    expect(match!.value).toBe(\"secret\");\n    expect(match!.httpOnly).toBe(true);\n  });\n\n  test(\"cookies() returns correct shape for a fully-specified cookie\", async () => {\n    const ctx = v3.context;\n    const page = ctx.pages()[0]!;\n    await page.goto(BASE_URL);\n\n    const name = `stagehand_shape_${Date.now()}`;\n    const expires = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now\n    await ctx.addCookies([\n      {\n        name,\n        value: \"full\",\n        domain: \"browserbase.github.io\",\n        path: \"/\",\n        expires,\n        httpOnly: true,\n        secure: true,\n        sameSite: \"Lax\",\n      },\n    ]);\n\n    const cookies = await ctx.cookies(BASE_URL);\n    const match = cookies.find((c) => c.name === name);\n    expect(match).toBeDefined();\n\n    // Validate every field on the returned Cookie object\n    expect(match!.value).toBe(\"full\");\n    expect(match!.domain).toMatch(/browserbase\\.github\\.io/);\n    expect(match!.path).toBe(\"/\");\n    expect(match!.expires).toBeGreaterThan(0);\n    expect(match!.httpOnly).toBe(true);\n    expect(match!.secure).toBe(true);\n    expect(match!.sameSite).toBe(\"Lax\");\n\n    // Ensure no extra fields leak through from CDP\n    const keys = Object.keys(match!);\n    expect(keys.sort()).toEqual(\n      [\n        \"name\",\n        \"value\",\n        \"domain\",\n        \"path\",\n        \"expires\",\n        \"httpOnly\",\n        \"secure\",\n        \"sameSite\",\n      ].sort(),\n    );\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/default-page-tracking.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe.configure({ mode: \"parallel\" });\ntest.describe(\"V3 default page tracking\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"activePage points to initial page\", async () => {\n    const ctx = v3.context;\n    // Should have at least one top-level page\n    const pages = ctx.pages();\n    expect(pages.length).toBeGreaterThanOrEqual(1);\n    const active = ctx.activePage();\n    expect(active).toBeTruthy();\n    // mainFrameId should be a non-empty string\n    expect(active!.mainFrameId().length).toBeGreaterThan(0);\n  });\n\n  test(\"activePage switches to most recent top-level page and reverts on close\", async () => {\n    const ctx = v3.context;\n    const newPage = await ctx.newPage(\"https://example.com/\");\n\n    const activeAfterCreate = await ctx.awaitActivePage();\n    expect(activeAfterCreate.url()).toContain(newPage.url());\n  });\n\n  test(\"popup default-page flow via five-tab site\", async () => {\n    const ctx = v3.context;\n\n    // 1) Navigate the default page to the site\n    const root = await ctx.awaitActivePage();\n    await root!.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/\",\n      { waitUntil: \"load\", timeoutMs: 15000 },\n    );\n    // 2) Click button on the page to open a new tab → page2.html\n    await root.locator(\"xpath=/html/body/button\").click();\n    const page2 = await ctx.awaitActivePage();\n    expect(page2!.url()).toBe(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/page2.html\",\n    );\n\n    // 3) On the default page (now page2), click its button → open page3 popup\n\n    await page2.locator(\"xpath=/html/body/button\").click();\n    const page3 = await ctx.awaitActivePage();\n    expect(page3!.url()).toBe(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/page3.html\",\n    );\n\n    // 4) Close the current page (page3) and ensure the default page reverts to page2\n    await page3!.close();\n    const backToPage2 = await ctx.awaitActivePage();\n    expect(backToPage2!.url()).toBe(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/page2.html\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/downloads.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport Browserbase from \"@browserbasehq/sdk\";\nimport AdmZip from \"adm-zip\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\nconst pdfRe = /sample-(\\d{13})+\\.pdf/;\ntest.describe(\"downloads on browserbase\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"downloaded pdf is available via downloads api\", async () => {\n    const browserTarget = (\n      process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n    ).toLowerCase();\n    const isBrowserbase = browserTarget === \"browserbase\";\n    // Skip this test in LOCAL mode as it requires Browserbase session\n    test.skip(\n      !isBrowserbase,\n      \"Skipping Browserbase-only downloads test in LOCAL mode\",\n    );\n\n    // Skip if BROWSERBASE_API_KEY is not set\n    test.skip(\n      !process.env.BROWSERBASE_API_KEY,\n      \"Skipping test: BROWSERBASE_API_KEY not set\",\n    );\n\n    // Tiny timeout to force the race to hit the timeout branch\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/download-on-click/\",\n    );\n    await page.locator(\"/html/body/button\").click();\n\n    await expect(async () => {\n      const bb = new Browserbase();\n      const zipBuffer = await bb.sessions.downloads.list(\n        v3.browserbaseSessionId,\n      );\n      if (!zipBuffer) {\n        throw new Error(\n          `Download buffer is empty for session ${v3.browserbaseSessionId}`,\n        );\n      }\n\n      const zip = new AdmZip(Buffer.from(await zipBuffer.arrayBuffer()));\n      const zipEntries = zip.getEntries();\n      const pdfEntry = zipEntries.find((entry) => pdfRe.test(entry.entryName));\n\n      if (!pdfEntry) {\n        throw new Error(\n          `Session ${v3.browserbaseSessionId} is missing a file matching \"${pdfRe.toString()}\" in its zip entries: ${JSON.stringify(zipEntries.map((entry) => entry.entryName))}`,\n        );\n      }\n\n      const expectedFileSize = 13264;\n      expect(pdfEntry.header.size).toBe(expectedFileSize);\n    }).toPass({\n      timeout: 30_000,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/flowLogger.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { z } from \"zod\";\nimport { InMemoryEventSink } from \"../../lib/v3/flowlogger/EventSink.js\";\nimport { FlowEvent } from \"../../lib/v3/flowlogger/FlowLogger.js\";\nimport { performUnderstudyMethod } from \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport {\n  createScriptedAisdkTestLlmClient,\n  closeV3,\n  doneToolResponse,\n  findLastEncodedId,\n  toolCallResponse,\n} from \"./testUtils.js\";\nimport { getV3TestConfig } from \"./v3.config.js\";\n\nfunction encodeHtml(html: string): string {\n  return `data:text/html,${encodeURIComponent(html)}`;\n}\n\nfunction createRecordedFlowLoggerV3(\n  overrides: Parameters<typeof getV3TestConfig>[0] = {},\n): V3 {\n  const v3 = new V3(getV3TestConfig(overrides));\n  const sink = new InMemoryEventSink();\n  v3.bus.on(\"*\", (event: unknown) => {\n    if (event instanceof FlowEvent) {\n      void sink.emit(event);\n    }\n  });\n  v3.eventStore.query = (query) =>\n    sink.query({ ...query, sessionId: v3.eventStore.sessionId });\n  return v3;\n}\n\nasync function listRecordedFlowEvents(v3: V3): Promise<FlowEvent[]> {\n  return v3.eventStore.query({});\n}\n\nasync function captureFlowEventBaseline(v3: V3): Promise<Set<string>> {\n  const events = await listRecordedFlowEvents(v3);\n  return new Set(events.map((event) => event.eventId));\n}\n\nasync function listRecordedFlowEventsSince(\n  v3: V3,\n  baseline: Set<string>,\n): Promise<FlowEvent[]> {\n  const events = await listRecordedFlowEvents(v3);\n  return events.filter((event) => !baseline.has(event.eventId));\n}\n\nfunction eventsOfType(events: FlowEvent[], eventType: string): FlowEvent[] {\n  return events.filter((event) => event.eventType === eventType);\n}\n\nfunction requireSingleEvent(events: FlowEvent[], eventType: string): FlowEvent {\n  const matches = eventsOfType(events, eventType);\n  expect(matches, `expected a single ${eventType}`).toHaveLength(1);\n  return matches[0];\n}\n\nfunction expectRootEvent(event: FlowEvent): void {\n  expect(event.eventParentIds).toEqual([]);\n}\n\nfunction expectDirectParent(child: FlowEvent, parent: FlowEvent): void {\n  expect(child.eventParentIds).toEqual([\n    ...parent.eventParentIds,\n    parent.eventId,\n  ]);\n}\n\nfunction assertAllParentIdsResolve(events: FlowEvent[]): void {\n  const eventIds = new Set(events.map((event) => event.eventId));\n\n  for (const event of events) {\n    for (const parentId of event.eventParentIds) {\n      expect(\n        eventIds.has(parentId),\n        `${event.eventType} references missing parent ${parentId}`,\n      ).toBe(true);\n    }\n  }\n}\n\nfunction assertSessionIds(events: FlowEvent[], sessionId: string): void {\n  for (const event of events) {\n    expect(event.sessionId).toBe(sessionId);\n  }\n}\n\nfunction directChildrenOfType(\n  events: FlowEvent[],\n  parent: FlowEvent,\n  eventType: string,\n): FlowEvent[] {\n  const expectedParentIds = [...parent.eventParentIds, parent.eventId];\n  return events.filter(\n    (event) =>\n      event.eventType === eventType &&\n      JSON.stringify(event.eventParentIds) ===\n        JSON.stringify(expectedParentIds),\n  );\n}\n\nfunction assertCompletedEnvelope(\n  events: FlowEvent[],\n  eventType: string,\n  completedEventType = `${eventType.replace(/Event$/, \"\")}CompletedEvent`,\n): FlowEvent {\n  const root = requireSingleEvent(events, eventType);\n  const completed = requireSingleEvent(events, completedEventType);\n  expectDirectParent(completed, root);\n  return root;\n}\n\nfunction assertNoFloatingLlmEvents(events: FlowEvent[]): void {\n  const llmEvents = events.filter(\n    (event) =>\n      event.eventType === \"LlmRequestEvent\" ||\n      event.eventType === \"LlmResponseEvent\",\n  );\n  const byId = new Map(events.map((event) => [event.eventId, event]));\n\n  expect(llmEvents.length).toBeGreaterThan(0);\n\n  for (const event of llmEvents) {\n    expect(\n      event.eventParentIds.length,\n      `${event.eventType} is floating`,\n    ).toBeGreaterThan(0);\n    const lastParentId = event.eventParentIds.at(-1);\n    const lastParent = lastParentId ? byId.get(lastParentId) : undefined;\n    expect(\n      lastParent,\n      `${event.eventType} has no resolved parent`,\n    ).toBeDefined();\n    expect(lastParent?.eventType.startsWith(\"Llm\")).toBe(false);\n  }\n}\n\nfunction assertNoFloatingCdpEvents(events: FlowEvent[]): void {\n  const cdpEvents = events.filter((event) => event.eventType.startsWith(\"Cdp\"));\n  const byId = new Map(events.map((event) => [event.eventId, event]));\n\n  expect(cdpEvents.length).toBeGreaterThan(0);\n\n  for (const event of cdpEvents) {\n    expect(\n      event.eventParentIds.length,\n      `${event.eventType} is floating`,\n    ).toBeGreaterThan(0);\n    const lastParentId = event.eventParentIds.at(-1);\n    const lastParent = lastParentId ? byId.get(lastParentId) : undefined;\n    expect(\n      lastParent,\n      `${event.eventType} has no resolved parent`,\n    ).toBeDefined();\n\n    if (event.eventType === \"CdpCallEvent\") {\n      expect(lastParent?.eventType.startsWith(\"Cdp\")).toBe(false);\n    } else {\n      expect(lastParent?.eventType).toBe(\"CdpCallEvent\");\n    }\n  }\n}\n\nfunction assertDirectRootCdpEvents(\n  events: FlowEvent[],\n  sessionId: string,\n): void {\n  const call = requireSingleEvent(events, \"CdpCallEvent\");\n  const responseTypes = [\"CdpResponseEvent\", \"CdpResponseErrorEvent\"];\n  const response = events.find((event) =>\n    responseTypes.includes(event.eventType),\n  );\n\n  expect(response, \"expected a direct CDP response event\").toBeDefined();\n  assertSessionIds(events, sessionId);\n  expectRootEvent(call);\n  expect(response?.eventParentIds).toEqual([call.eventId]);\n}\n\nfunction sortCountRecord(\n  input: Record<string, number>,\n): Record<string, number> {\n  return Object.fromEntries(\n    Object.entries(input).sort(([left], [right]) => left.localeCompare(right)),\n  );\n}\n\nfunction assertNonCdpEventCounts(\n  events: FlowEvent[],\n  expected: Record<string, number>,\n): void {\n  const actual = events.reduce<Record<string, number>>((counts, event) => {\n    if (event.eventType.startsWith(\"Cdp\")) {\n      return counts;\n    }\n\n    counts[event.eventType] = (counts[event.eventType] ?? 0) + 1;\n    return counts;\n  }, {});\n\n  expect(sortCountRecord(actual)).toEqual(sortCountRecord(expected));\n}\n\ntest.describe(\"flow logger integration\", () => {\n  test.describe.configure({ mode: \"serial\" });\n\n  test(\"act emits a rooted tree with nested understudy, llm, and cdp events\", async () => {\n    const buttonText = \"Flow Logger Act Button\";\n    const llmClient = createScriptedAisdkTestLlmClient({\n      jsonResponses: {\n        act: (options) => ({\n          elementId: findLastEncodedId(options),\n          description: `click ${buttonText}`,\n          method: \"click\",\n          arguments: [],\n          twoStep: false,\n        }),\n      },\n    });\n\n    const v3 = createRecordedFlowLoggerV3({\n      llmClient,\n    });\n\n    await v3.init();\n\n    try {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        encodeHtml(`\n          <!doctype html>\n          <html>\n            <body>\n              <button\n                id=\"act-target\"\n                onclick=\"document.body.dataset.clicked='true'\"\n              >\n                ${buttonText}\n              </button>\n            </body>\n          </html>\n        `),\n      );\n\n      const baseline = await captureFlowEventBaseline(v3);\n      const result = await v3.act(`Click the ${buttonText}`);\n      const events = await listRecordedFlowEventsSince(v3, baseline);\n\n      expect(result.success).toBe(true);\n      expect(\n        await page.evaluate(() => document.body.dataset.clicked ?? \"\"),\n      ).toBe(\"true\");\n      const root = requireSingleEvent(events, \"StagehandActEvent\");\n      const completed = requireSingleEvent(\n        events,\n        \"StagehandActCompletedEvent\",\n      );\n      const llmRequest = requireSingleEvent(events, \"LlmRequestEvent\");\n      const llmResponse = requireSingleEvent(events, \"LlmResponseEvent\");\n      const understudy = requireSingleEvent(events, \"UnderstudyClickEvent\");\n      const understudyCompleted = requireSingleEvent(\n        events,\n        \"UnderstudyClickCompletedEvent\",\n      );\n\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        LlmRequestEvent: 1,\n        LlmResponseEvent: 1,\n        StagehandActCompletedEvent: 1,\n        StagehandActEvent: 1,\n        UnderstudyClickCompletedEvent: 1,\n        UnderstudyClickEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(root);\n      expectDirectParent(completed, root);\n      expect(llmRequest.eventParentIds).toEqual([root.eventId]);\n      expect(llmResponse.eventParentIds).toEqual([root.eventId]);\n      expect(understudy.eventParentIds).toEqual([root.eventId]);\n      expectDirectParent(understudyCompleted, understudy);\n      assertNoFloatingLlmEvents(events);\n      assertNoFloatingCdpEvents(events);\n    } finally {\n      await closeV3(v3);\n    }\n  });\n\n  test(\"observe and extract emit rooted trees with complete nested llm and cdp events\", async () => {\n    const observeText = \"Flow Logger Observe Button\";\n    const extractTitle = \"Flow Logger Extract Title\";\n    const llmClient = createScriptedAisdkTestLlmClient({\n      jsonResponses: {\n        Observation: (options) => ({\n          elements: [\n            {\n              elementId: findLastEncodedId(options),\n              description: observeText,\n              method: \"click\",\n              arguments: [],\n            },\n          ],\n        }),\n        Extraction: {\n          title: extractTitle,\n        },\n        Metadata: {\n          completed: true,\n          progress: \"done\",\n        },\n      },\n    });\n\n    const v3 = createRecordedFlowLoggerV3({\n      llmClient,\n    });\n\n    await v3.init();\n\n    try {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        encodeHtml(`\n          <!doctype html>\n          <html>\n            <body>\n              <button id=\"observe-target\">${observeText}</button>\n              <h1>${extractTitle}</h1>\n            </body>\n          </html>\n        `),\n      );\n\n      const observeBaseline = await captureFlowEventBaseline(v3);\n      const observeResult = await v3.observe(`Find the ${observeText}`);\n\n      expect(observeResult).toHaveLength(1);\n      expect(observeResult[0].method).toBe(\"click\");\n\n      const observeEvents = await listRecordedFlowEventsSince(\n        v3,\n        observeBaseline,\n      );\n      const observeRoot = requireSingleEvent(\n        observeEvents,\n        \"StagehandObserveEvent\",\n      );\n      const observeCompleted = requireSingleEvent(\n        observeEvents,\n        \"StagehandObserveCompletedEvent\",\n      );\n      const observeLlmRequests = eventsOfType(observeEvents, \"LlmRequestEvent\");\n      const observeLlmResponses = eventsOfType(\n        observeEvents,\n        \"LlmResponseEvent\",\n      );\n\n      assertAllParentIdsResolve(observeEvents);\n      assertNonCdpEventCounts(observeEvents, {\n        LlmRequestEvent: 1,\n        LlmResponseEvent: 1,\n        StagehandObserveCompletedEvent: 1,\n        StagehandObserveEvent: 1,\n      });\n      assertSessionIds(observeEvents, v3.flowLoggerContext.sessionId);\n      expectRootEvent(observeRoot);\n      expectDirectParent(observeCompleted, observeRoot);\n      expect(observeLlmRequests).toHaveLength(1);\n      expect(observeLlmResponses).toHaveLength(1);\n      expect(observeLlmRequests[0].eventParentIds).toEqual([\n        observeRoot.eventId,\n      ]);\n      expect(observeLlmResponses[0].eventParentIds).toEqual([\n        observeRoot.eventId,\n      ]);\n      assertNoFloatingLlmEvents(observeEvents);\n      assertNoFloatingCdpEvents(observeEvents);\n\n      const extractBaseline = await captureFlowEventBaseline(v3);\n      const extractResult = await v3.extract(\n        \"Extract the title\",\n        z.object({ title: z.string() }),\n      );\n\n      expect(extractResult).toEqual({ title: extractTitle });\n\n      const extractEvents = await listRecordedFlowEventsSince(\n        v3,\n        extractBaseline,\n      );\n      const extractRoot = requireSingleEvent(\n        extractEvents,\n        \"StagehandExtractEvent\",\n      );\n      const extractCompleted = requireSingleEvent(\n        extractEvents,\n        \"StagehandExtractCompletedEvent\",\n      );\n      const extractLlmRequests = eventsOfType(extractEvents, \"LlmRequestEvent\");\n      const extractLlmResponses = eventsOfType(\n        extractEvents,\n        \"LlmResponseEvent\",\n      );\n\n      assertAllParentIdsResolve(extractEvents);\n      assertNonCdpEventCounts(extractEvents, {\n        LlmRequestEvent: 2,\n        LlmResponseEvent: 2,\n        StagehandExtractCompletedEvent: 1,\n        StagehandExtractEvent: 1,\n      });\n      assertSessionIds(extractEvents, v3.flowLoggerContext.sessionId);\n      expectRootEvent(extractRoot);\n      expectDirectParent(extractCompleted, extractRoot);\n      expect(extractLlmRequests).toHaveLength(2);\n      expect(extractLlmResponses).toHaveLength(2);\n\n      for (const event of [...extractLlmRequests, ...extractLlmResponses]) {\n        expect(event.eventParentIds).toEqual([extractRoot.eventId]);\n      }\n\n      assertNoFloatingLlmEvents(extractEvents);\n      assertNoFloatingCdpEvents(extractEvents);\n    } finally {\n      await closeV3(v3);\n    }\n  });\n\n  test(\"agent.execute -> act carries the full agent -> stagehand -> understudy -> cdp + llm hierarchy\", async () => {\n    const buttonText = \"Agent Act Button\";\n    const llmClient = createScriptedAisdkTestLlmClient({\n      jsonResponses: {\n        act: (options) => ({\n          elementId: findLastEncodedId(options),\n          description: `click ${buttonText}`,\n          method: \"click\",\n          arguments: [],\n          twoStep: false,\n        }),\n      },\n      generateResponses: [\n        toolCallResponse(\"act\", { action: `click the ${buttonText}` }, \"act-1\"),\n        doneToolResponse(\"finished\", true, \"done-1\"),\n      ],\n    });\n\n    const v3 = createRecordedFlowLoggerV3({\n      experimental: true,\n      llmClient,\n    });\n\n    await v3.init();\n\n    try {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        encodeHtml(`\n          <!doctype html>\n          <html>\n            <body>\n              <button\n                id=\"agent-act-target\"\n                onclick=\"document.body.dataset.agentAct='true'\"\n              >\n                ${buttonText}\n              </button>\n            </body>\n          </html>\n        `),\n      );\n\n      const baseline = await captureFlowEventBaseline(v3);\n      const result = await v3.agent().execute({\n        instruction: `Click the ${buttonText} and finish.`,\n        maxSteps: 2,\n      });\n      const events = await listRecordedFlowEventsSince(v3, baseline);\n\n      expect(result.success).toBe(true);\n      expect(\n        await page.evaluate(() => document.body.dataset.agentAct ?? \"\"),\n      ).toBe(\"true\");\n      const agentRoot = assertCompletedEnvelope(events, \"AgentExecuteEvent\");\n      const actRoot = requireSingleEvent(events, \"StagehandActEvent\");\n      const actCompleted = requireSingleEvent(\n        events,\n        \"StagehandActCompletedEvent\",\n      );\n      const understudy = requireSingleEvent(events, \"UnderstudyClickEvent\");\n      const understudyCompleted = requireSingleEvent(\n        events,\n        \"UnderstudyClickCompletedEvent\",\n      );\n\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        AgentExecuteCompletedEvent: 1,\n        AgentExecuteEvent: 1,\n        LlmRequestEvent: 3,\n        LlmResponseEvent: 3,\n        StagehandActCompletedEvent: 1,\n        StagehandActEvent: 1,\n        UnderstudyClickCompletedEvent: 1,\n        UnderstudyClickEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(agentRoot);\n      expect(actRoot.eventParentIds).toEqual([agentRoot.eventId]);\n      expectDirectParent(actCompleted, actRoot);\n      expectDirectParent(understudy, actRoot);\n      expectDirectParent(understudyCompleted, understudy);\n      expect(\n        directChildrenOfType(events, agentRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, agentRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, actRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(1);\n      expect(\n        directChildrenOfType(events, actRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(1);\n      assertNoFloatingLlmEvents(events);\n      assertNoFloatingCdpEvents(events);\n    } finally {\n      await closeV3(v3);\n    }\n  });\n\n  test(\"agent.execute -> fillForm carries the observe -> act -> understudy hierarchy with no missing layers\", async () => {\n    const llmClient = createScriptedAisdkTestLlmClient({\n      jsonResponses: {\n        Observation: (options) => ({\n          elements: [\n            {\n              elementId: findLastEncodedId(options),\n              description: \"name input\",\n              method: \"fill\",\n              arguments: [\"hello\"],\n            },\n          ],\n        }),\n      },\n      generateResponses: [\n        toolCallResponse(\n          \"fillForm\",\n          {\n            fields: [\n              {\n                action: \"type hello into the name field\",\n                value: \"hello\",\n              },\n            ],\n          },\n          \"fillform-1\",\n        ),\n        doneToolResponse(\"finished\", true, \"done-1\"),\n      ],\n    });\n\n    const v3 = createRecordedFlowLoggerV3({\n      experimental: true,\n      llmClient,\n    });\n\n    await v3.init();\n\n    try {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        encodeHtml(`\n          <!doctype html>\n          <html>\n            <body>\n              <input id=\"name\" />\n            </body>\n          </html>\n        `),\n      );\n\n      const baseline = await captureFlowEventBaseline(v3);\n      const result = await v3.agent().execute({\n        instruction: \"Fill the form and finish.\",\n        maxSteps: 2,\n      });\n      const events = await listRecordedFlowEventsSince(v3, baseline);\n\n      expect(result.success).toBe(true);\n      expect(await page.locator(\"#name\").inputValue()).toBe(\"hello\");\n\n      const agentRoot = assertCompletedEnvelope(events, \"AgentExecuteEvent\");\n      const observeRoot = requireSingleEvent(events, \"StagehandObserveEvent\");\n      const observeCompleted = requireSingleEvent(\n        events,\n        \"StagehandObserveCompletedEvent\",\n      );\n      const actRoot = requireSingleEvent(events, \"StagehandActEvent\");\n      const actCompleted = requireSingleEvent(\n        events,\n        \"StagehandActCompletedEvent\",\n      );\n      const understudyFill = requireSingleEvent(events, \"UnderstudyFillEvent\");\n      const understudyFillCompleted = requireSingleEvent(\n        events,\n        \"UnderstudyFillCompletedEvent\",\n      );\n\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        AgentExecuteCompletedEvent: 1,\n        AgentExecuteEvent: 1,\n        LlmRequestEvent: 3,\n        LlmResponseEvent: 3,\n        StagehandActCompletedEvent: 1,\n        StagehandActEvent: 1,\n        StagehandObserveCompletedEvent: 1,\n        StagehandObserveEvent: 1,\n        UnderstudyFillCompletedEvent: 1,\n        UnderstudyFillEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(agentRoot);\n      expect(observeRoot.eventParentIds).toEqual([agentRoot.eventId]);\n      expectDirectParent(observeCompleted, observeRoot);\n      expect(actRoot.eventParentIds).toEqual([agentRoot.eventId]);\n      expectDirectParent(actCompleted, actRoot);\n      expectDirectParent(understudyFill, actRoot);\n      expectDirectParent(understudyFillCompleted, understudyFill);\n      expect(\n        directChildrenOfType(events, observeRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(1);\n      expect(\n        directChildrenOfType(events, observeRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(1);\n      expect(\n        directChildrenOfType(events, agentRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, agentRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, actRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(0);\n      expect(\n        directChildrenOfType(events, actRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(0);\n      assertNoFloatingLlmEvents(events);\n      assertNoFloatingCdpEvents(events);\n    } finally {\n      await closeV3(v3);\n    }\n  });\n\n  test(\"agent.execute -> extract carries the full agent -> extract -> cdp + llm hierarchy\", async () => {\n    const extractTitle = \"Agent Extract Title\";\n    const llmClient = createScriptedAisdkTestLlmClient({\n      jsonResponses: {\n        Extraction: {\n          title: extractTitle,\n        },\n        Metadata: {\n          completed: true,\n          progress: \"done\",\n        },\n      },\n      generateResponses: [\n        toolCallResponse(\n          \"extract\",\n          {\n            instruction: \"extract the title\",\n            schema: {\n              type: \"object\",\n              properties: {\n                title: { type: \"string\" },\n              },\n            },\n          },\n          \"extract-1\",\n        ),\n        doneToolResponse(\"finished\", true, \"done-1\"),\n      ],\n    });\n\n    const v3 = createRecordedFlowLoggerV3({\n      experimental: true,\n      llmClient,\n    });\n\n    await v3.init();\n\n    try {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        encodeHtml(`\n          <!doctype html>\n          <html>\n            <body>\n              <h1>${extractTitle}</h1>\n            </body>\n          </html>\n        `),\n      );\n\n      const baseline = await captureFlowEventBaseline(v3);\n      const result = await v3.agent().execute({\n        instruction: \"Extract the title and finish.\",\n        maxSteps: 2,\n      });\n\n      expect(result.success).toBe(true);\n\n      const events = await listRecordedFlowEventsSince(v3, baseline);\n      const agentRoot = assertCompletedEnvelope(events, \"AgentExecuteEvent\");\n      const extractRoot = requireSingleEvent(events, \"StagehandExtractEvent\");\n      const extractCompleted = requireSingleEvent(\n        events,\n        \"StagehandExtractCompletedEvent\",\n      );\n\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        AgentExecuteCompletedEvent: 1,\n        AgentExecuteEvent: 1,\n        LlmRequestEvent: 4,\n        LlmResponseEvent: 4,\n        StagehandExtractCompletedEvent: 1,\n        StagehandExtractEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(agentRoot);\n      expect(extractRoot.eventParentIds).toEqual([agentRoot.eventId]);\n      expectDirectParent(extractCompleted, extractRoot);\n      expect(\n        directChildrenOfType(events, agentRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, agentRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, extractRoot, \"LlmRequestEvent\"),\n      ).toHaveLength(2);\n      expect(\n        directChildrenOfType(events, extractRoot, \"LlmResponseEvent\"),\n      ).toHaveLength(2);\n      assertNoFloatingLlmEvents(events);\n      assertNoFloatingCdpEvents(events);\n    } finally {\n      await closeV3(v3);\n    }\n  });\n\n  test(\"agent.execute nests page events under the agent root and direct page calls root themselves\", async () => {\n    const agentPageUrl = encodeHtml(`\n      <!doctype html>\n      <html>\n        <body>\n          <h1>Agent Flow Logger Page</h1>\n        </body>\n      </html>\n    `);\n    const agentLlmClient = createScriptedAisdkTestLlmClient({\n      generateResponses: [\n        toolCallResponse(\"goto\", { url: agentPageUrl }, \"goto-1\"),\n        toolCallResponse(\"screenshot\", {}, \"screenshot-1\"),\n        doneToolResponse(\"finished\", true, \"done-1\"),\n      ],\n    });\n\n    const agentV3 = createRecordedFlowLoggerV3({\n      experimental: true,\n      llmClient: agentLlmClient,\n    });\n\n    await agentV3.init();\n\n    try {\n      const baseline = await captureFlowEventBaseline(agentV3);\n      const result = await agentV3.agent().execute({\n        instruction: \"Go to the test page, take a screenshot, and finish.\",\n        maxSteps: 3,\n      });\n\n      expect(result.success).toBe(true);\n      expect(result.completed).toBe(true);\n\n      const events = await listRecordedFlowEventsSince(agentV3, baseline);\n      const root = assertCompletedEnvelope(events, \"AgentExecuteEvent\");\n      const pageGoto = requireSingleEvent(events, \"PageGotoEvent\");\n      const pageGotoCompleted = requireSingleEvent(\n        events,\n        \"PageGotoCompletedEvent\",\n      );\n      const pageScreenshot = requireSingleEvent(events, \"PageScreenshotEvent\");\n      const pageScreenshotCompleted = requireSingleEvent(\n        events,\n        \"PageScreenshotCompletedEvent\",\n      );\n      const llmRequests = eventsOfType(events, \"LlmRequestEvent\");\n      const llmResponses = eventsOfType(events, \"LlmResponseEvent\");\n\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        AgentExecuteCompletedEvent: 1,\n        AgentExecuteEvent: 1,\n        LlmRequestEvent: 3,\n        LlmResponseEvent: 3,\n        PageGotoCompletedEvent: 1,\n        PageGotoEvent: 1,\n        PageScreenshotCompletedEvent: 1,\n        PageScreenshotEvent: 1,\n      });\n      assertSessionIds(events, agentV3.flowLoggerContext.sessionId);\n      expectRootEvent(root);\n      expect(pageGoto.eventParentIds).toEqual([root.eventId]);\n      expectDirectParent(pageGotoCompleted, pageGoto);\n      expect(pageScreenshot.eventParentIds).toEqual([root.eventId]);\n      expectDirectParent(pageScreenshotCompleted, pageScreenshot);\n      expect(llmRequests).toHaveLength(3);\n      expect(llmResponses).toHaveLength(3);\n\n      for (const event of [...llmRequests, ...llmResponses]) {\n        expect(event.eventParentIds).toEqual([root.eventId]);\n      }\n\n      assertNoFloatingLlmEvents(events);\n      assertNoFloatingCdpEvents(events);\n    } finally {\n      await closeV3(agentV3);\n    }\n\n    const directV3 = createRecordedFlowLoggerV3();\n    await directV3.init();\n\n    try {\n      const page = directV3.context.pages()[0];\n      const baseline = await captureFlowEventBaseline(directV3);\n\n      await page.goto(agentPageUrl);\n      await page.screenshot({ fullPage: false });\n\n      const events = await listRecordedFlowEventsSince(directV3, baseline);\n      const pageGoto = requireSingleEvent(events, \"PageGotoEvent\");\n      const pageGotoCompleted = requireSingleEvent(\n        events,\n        \"PageGotoCompletedEvent\",\n      );\n      const pageScreenshot = requireSingleEvent(events, \"PageScreenshotEvent\");\n      const pageScreenshotCompleted = requireSingleEvent(\n        events,\n        \"PageScreenshotCompletedEvent\",\n      );\n\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        PageGotoCompletedEvent: 1,\n        PageGotoEvent: 1,\n        PageScreenshotCompletedEvent: 1,\n        PageScreenshotEvent: 1,\n      });\n      assertSessionIds(events, directV3.flowLoggerContext.sessionId);\n      expectRootEvent(pageGoto);\n      expectDirectParent(pageGotoCompleted, pageGoto);\n      expectRootEvent(pageScreenshot);\n      expectDirectParent(pageScreenshotCompleted, pageScreenshot);\n      expect(eventsOfType(events, \"LlmRequestEvent\")).toHaveLength(0);\n      expect(eventsOfType(events, \"LlmResponseEvent\")).toHaveLength(0);\n      assertNoFloatingCdpEvents(events);\n    } finally {\n      await closeV3(directV3);\n    }\n  });\n\n  test(\"direct page methods, direct understudy calls, and direct sendCDP all attach complete event trees to the session\", async () => {\n    const v3 = createRecordedFlowLoggerV3();\n    await v3.init();\n\n    try {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        encodeHtml(`\n          <!doctype html>\n          <html>\n            <body>\n              <button\n                id=\"direct-click\"\n                onclick=\"document.body.dataset.directClick='true'\"\n              >\n                Direct Click\n              </button>\n              <div id=\"ready\">ready</div>\n            </body>\n          </html>\n        `),\n      );\n\n      let baseline = await captureFlowEventBaseline(v3);\n      await page.evaluate(() => document.getElementById(\"ready\")?.textContent);\n      let events = await listRecordedFlowEventsSince(v3, baseline);\n      let root = assertCompletedEnvelope(events, \"PageEvaluateEvent\");\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        PageEvaluateCompletedEvent: 1,\n        PageEvaluateEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(root);\n      expect(eventsOfType(events, \"LlmRequestEvent\")).toHaveLength(0);\n      expect(eventsOfType(events, \"LlmResponseEvent\")).toHaveLength(0);\n      assertNoFloatingCdpEvents(events);\n\n      baseline = await captureFlowEventBaseline(v3);\n      await page.snapshot();\n      events = await listRecordedFlowEventsSince(v3, baseline);\n      root = assertCompletedEnvelope(events, \"PageSnapshotEvent\");\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        PageSnapshotCompletedEvent: 1,\n        PageSnapshotEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(root);\n      expect(eventsOfType(events, \"LlmRequestEvent\")).toHaveLength(0);\n      expect(eventsOfType(events, \"LlmResponseEvent\")).toHaveLength(0);\n      assertNoFloatingCdpEvents(events);\n\n      baseline = await captureFlowEventBaseline(v3);\n      await performUnderstudyMethod(\n        page,\n        page.mainFrame(),\n        \"click\",\n        \"/html/body/button\",\n        [],\n        30_000,\n      );\n      events = await listRecordedFlowEventsSince(v3, baseline);\n      root = assertCompletedEnvelope(events, \"UnderstudyClickEvent\");\n      assertAllParentIdsResolve(events);\n      assertNonCdpEventCounts(events, {\n        UnderstudyClickCompletedEvent: 1,\n        UnderstudyClickEvent: 1,\n      });\n      assertSessionIds(events, v3.flowLoggerContext.sessionId);\n      expectRootEvent(root);\n      expect(eventsOfType(events, \"LlmRequestEvent\")).toHaveLength(0);\n      expect(eventsOfType(events, \"LlmResponseEvent\")).toHaveLength(0);\n      assertNoFloatingCdpEvents(events);\n      expect(\n        await page.evaluate(() => document.body.dataset.directClick ?? \"\"),\n      ).toBe(\"true\");\n\n      baseline = await captureFlowEventBaseline(v3);\n      const cdpResult = await page.sendCDP<{\n        result?: { value?: number };\n      }>(\"Runtime.evaluate\", {\n        expression: \"2 + 2\",\n        returnByValue: true,\n      });\n      events = await listRecordedFlowEventsSince(v3, baseline);\n      expect(cdpResult.result?.value).toBe(4);\n      expect(eventsOfType(events, \"LlmRequestEvent\")).toHaveLength(0);\n      expect(eventsOfType(events, \"LlmResponseEvent\")).toHaveLength(0);\n      assertAllParentIdsResolve(events);\n      assertDirectRootCdpEvents(events, v3.flowLoggerContext.sessionId);\n    } finally {\n      await closeV3(v3);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/frame-get-location-and-click.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Coordinate-based clicking\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"clicking by coordinates toggles a button state\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <button id=\"btn\" onclick=\"this.dataset.clicked = (this.dataset.clicked==='1'?'0':'1')\">Click</button>\n            <div id=\"out\"></div>\n            <script>\n              const btn = document.getElementById('btn');\n              const out = document.getElementById('out');\n              const update = () => { out.textContent = btn.dataset.clicked === '1' ? 'clicked' : 'idle'; };\n              update();\n              btn.addEventListener('click', update);\n            </script>\n          </body></html>`,\n        ),\n    );\n\n    // Initial state should be idle\n    let state = await page.mainFrame().evaluate(() => {\n      const out = document.getElementById(\"out\");\n      return out?.textContent || \"\";\n    });\n    expect(state).toBe(\"idle\");\n\n    // Compute button location via Frame.getLocationForSelector\n    const { x, y, width, height } = await page\n      .mainFrame()\n      .getLocationForSelector(\"#btn\");\n\n    // Click near the center of the button using Page.click coordinates\n    const cx = Math.round(x + width / 2);\n    const cy = Math.round(y + height / 2);\n    await page.click(cx, cy);\n\n    state = await page.mainFrame().evaluate(() => {\n      const out = document.getElementById(\"out\");\n      return out?.textContent || \"\";\n    });\n    expect(state).toBe(\"clicked\");\n\n    // Click again to toggle back to idle\n    await page.click(cx, cy);\n    state = await page.mainFrame().evaluate(() => {\n      const out = document.getElementById(\"out\");\n      return out?.textContent || \"\";\n    });\n    expect(state).toBe(\"idle\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/iframe-ctx-addInitScript-race.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport type { V3Context } from \"../../lib/v3/understudy/context.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\n\nconst DEFAULT_INIT_SCRIPT_DELAY_MS = 250;\nconst INIT_SCRIPT_DELAY_MS = (() => {\n  const rawValue = process.env.IFRAME_INIT_SCRIPT_SEND_DELAY_MS;\n  if (rawValue === undefined) return DEFAULT_INIT_SCRIPT_DELAY_MS;\n  const parsed = Number(rawValue);\n  if (!Number.isFinite(parsed) || parsed <= 0)\n    return DEFAULT_INIT_SCRIPT_DELAY_MS;\n  return parsed;\n})();\n\nconst POPUP_TIMEOUT_MS = 20_000;\nconst RACE_INIT_SCRIPT_SENTINEL = \"__stagehand_init_script_race_sentinel__\";\nconst INIT_SCRIPT_MARKER_KEY = \"__stagehand_init_script_loaded__\";\nconst POPUP_URL = \"https://example.com/\";\nconst POPUP_IFRAME_URL = \"https://example.org/\";\n\nconst INIT_SCRIPT_SOURCE = `\n(() => {\n  /* ${RACE_INIT_SCRIPT_SENTINEL} */\n  window[\"${INIT_SCRIPT_MARKER_KEY}\"] = true;\n})();\n`;\n\ntype PatchedConn = {\n  _sendViaSession: (\n    sessionId: string,\n    method: string,\n    params?: object,\n  ) => Promise<unknown>;\n};\n\ntype SessionCommandRecord = {\n  sequence: number;\n  sessionId: string;\n  method: string;\n  isRaceInitScript: boolean;\n};\n\ntype PopupTriggerCase = {\n  name: string;\n  prepare: (opener: Page) => Promise<void>;\n};\n\nasync function closeAllPages(ctx: V3Context): Promise<void> {\n  const pages = ctx.pages();\n  await Promise.allSettled(pages.map((page) => page.close()));\n}\n\nasync function waitForPopupPage(\n  ctx: V3Context,\n  knownTargetIds: Set<string>,\n  timeoutMs = POPUP_TIMEOUT_MS,\n): Promise<Page> {\n  const deadline = Date.now() + timeoutMs;\n\n  while (Date.now() < deadline) {\n    const popup = ctx\n      .pages()\n      .find((candidate) => !knownTargetIds.has(candidate.targetId()));\n    if (popup) return popup;\n    try {\n      const active = await ctx.awaitActivePage(500);\n      if (!knownTargetIds.has(active.targetId())) return active;\n    } catch {\n      // keep polling\n    }\n    await new Promise((resolve) => setTimeout(resolve, 50));\n  }\n\n  throw new Error(\"Timed out waiting for popup page\");\n}\n\nasync function waitForChildFrame(\n  page: Page,\n  expectedUrl: string,\n  timeoutMs = POPUP_TIMEOUT_MS,\n): Promise<ReturnType<Page[\"frames\"]>[number]> {\n  const mainFrameId = page.mainFrame().frameId;\n  const deadline = Date.now() + timeoutMs;\n\n  while (Date.now() < deadline) {\n    for (const frame of page.frames()) {\n      if (frame.frameId === mainFrameId) continue;\n      try {\n        const href = await frame.evaluate(() => window.location.href);\n        if (href === expectedUrl) return frame;\n      } catch {\n        // frame context may not be ready yet\n      }\n    }\n    await new Promise((resolve) => setTimeout(resolve, 50));\n  }\n\n  throw new Error(\"Timed out waiting for child frame\");\n}\n\nasync function prepareTargetBlankPopupOpener(opener: Page): Promise<void> {\n  await opener.goto(\"about:blank\", { waitUntil: \"domcontentloaded\" });\n  await opener.mainFrame().evaluate((popupUrl) => {\n    const link = document.createElement(\"a\");\n    link.id = \"open-popup\";\n    link.target = \"_blank\";\n    link.href = popupUrl;\n    link.textContent = \"open popup\";\n    document.body.appendChild(link);\n  }, POPUP_URL);\n}\n\nasync function prepareWindowOpenPopupOpener(opener: Page): Promise<void> {\n  await opener.goto(\"about:blank\", { waitUntil: \"domcontentloaded\" });\n  await opener.mainFrame().evaluate((popupUrl) => {\n    const button = document.createElement(\"button\");\n    button.id = \"open-popup\";\n    button.textContent = \"open popup\";\n    button.addEventListener(\"click\", () => {\n      window.open(popupUrl, \"_blank\");\n    });\n    document.body.appendChild(button);\n  }, POPUP_URL);\n}\n\nconst POPUP_TRIGGER_CASES: PopupTriggerCase[] = [\n  {\n    name: 'target=\"_blank\" link click',\n    prepare: prepareTargetBlankPopupOpener,\n  },\n  {\n    name: \"window.open from click handler\",\n    prepare: prepareWindowOpenPopupOpener,\n  },\n];\n\ntest.describe(\"repro: popup iframe addInitScript race under delayed CDP send\", () => {\n  test.describe.configure({ mode: \"serial\" });\n\n  let restoreSend: (() => void) | undefined;\n  let v3: V3 | undefined;\n  let ctx: V3Context | undefined;\n  let sequence = 0;\n  let records: SessionCommandRecord[] = [];\n\n  test.beforeAll(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n    ctx = v3.context;\n\n    const conn = (ctx as unknown as { conn?: PatchedConn }).conn;\n    if (!conn || typeof conn._sendViaSession !== \"function\") {\n      throw new Error(\"Unable to access CDP connection for race repro patch\");\n    }\n\n    const originalSendViaSession = conn._sendViaSession.bind(conn);\n    conn._sendViaSession = function patchedSendViaSession(\n      sessionId: string,\n      method: string,\n      params?: object,\n    ) {\n      const source =\n        typeof (params as { source?: unknown } | undefined)?.source === \"string\"\n          ? (params as { source: string }).source\n          : \"\";\n      const isRaceInitScript =\n        method === \"Page.addScriptToEvaluateOnNewDocument\" &&\n        source.includes(RACE_INIT_SCRIPT_SENTINEL);\n\n      const sendNow = () => {\n        records.push({\n          sequence: ++sequence,\n          sessionId,\n          method,\n          isRaceInitScript,\n        });\n        return originalSendViaSession(sessionId, method, params);\n      };\n\n      if (isRaceInitScript && INIT_SCRIPT_DELAY_MS > 0) {\n        return new Promise((resolve, reject) => {\n          setTimeout(() => {\n            sendNow().then(resolve, reject);\n          }, INIT_SCRIPT_DELAY_MS);\n        });\n      }\n\n      return sendNow();\n    };\n\n    restoreSend = () => {\n      conn._sendViaSession = originalSendViaSession;\n    };\n\n    await ctx.addInitScript(INIT_SCRIPT_SOURCE);\n  });\n\n  test.afterAll(async () => {\n    restoreSend?.();\n    await v3?.close?.().catch(() => {});\n  });\n\n  test.beforeEach(async () => {\n    records = [];\n    sequence = 0;\n    if (!ctx) return;\n    await closeAllPages(ctx);\n  });\n\n  test.afterEach(async () => {\n    if (!ctx) return;\n    await closeAllPages(ctx);\n  });\n\n  for (const popupCase of POPUP_TRIGGER_CASES) {\n    test(`should send addScript before resume for popup targets via ${popupCase.name}`, async () => {\n      if (!ctx) throw new Error(\"Context not initialized\");\n\n      const opener = await ctx.newPage();\n      await popupCase.prepare(opener);\n\n      const knownTargetIds = new Set(ctx.pages().map((p) => p.targetId()));\n      const knownSessionIds = new Set(\n        records.map((record) => record.sessionId),\n      );\n\n      await opener.locator(\"#open-popup\").click();\n\n      const popup = await waitForPopupPage(ctx, knownTargetIds);\n      await popup.waitForLoadState(\"load\", POPUP_TIMEOUT_MS);\n      await popup.mainFrame().evaluate((iframeUrl) => {\n        const iframe = document.createElement(\"iframe\");\n        iframe.id = \"race-child-iframe\";\n        iframe.src = iframeUrl;\n        document.body.appendChild(iframe);\n      }, POPUP_IFRAME_URL);\n      const iframe = await waitForChildFrame(\n        popup,\n        POPUP_IFRAME_URL,\n        POPUP_TIMEOUT_MS,\n      );\n\n      const popupInitScriptMarker = await popup.mainFrame().evaluate((key) => {\n        return Boolean(Reflect.get(window, key));\n      }, INIT_SCRIPT_MARKER_KEY);\n      const iframeInitScriptMarker = await iframe.evaluate((key) => {\n        return Boolean(Reflect.get(window, key));\n      }, INIT_SCRIPT_MARKER_KEY);\n\n      const perSession = new Map<\n        string,\n        {\n          raceInitScriptSequence?: number;\n          resumeSequence?: number;\n        }\n      >();\n\n      for (const record of records) {\n        if (knownSessionIds.has(record.sessionId)) continue;\n        const entry = perSession.get(record.sessionId) ?? {};\n        if (\n          record.isRaceInitScript &&\n          entry.raceInitScriptSequence === undefined\n        ) {\n          entry.raceInitScriptSequence = record.sequence;\n        }\n        if (\n          record.method === \"Runtime.runIfWaitingForDebugger\" &&\n          entry.resumeSequence === undefined\n        ) {\n          entry.resumeSequence = record.sequence;\n        }\n        perSession.set(record.sessionId, entry);\n      }\n\n      const comparableSessions = [...perSession.entries()]\n        .map(([sessionId, entry]) => ({ sessionId, ...entry }))\n        .filter(\n          (entry) =>\n            entry.raceInitScriptSequence !== undefined &&\n            entry.resumeSequence !== undefined,\n        );\n      expect(comparableSessions.length).toBeGreaterThan(0);\n\n      const orderingViolations = comparableSessions.filter((entry) => {\n        return (\n          (entry.raceInitScriptSequence as number) >\n          (entry.resumeSequence as number)\n        );\n      });\n\n      expect(\n        orderingViolations,\n        `Expected addScript before resume for ${popupCase.name}. initScriptDelayMs=${INIT_SCRIPT_DELAY_MS}; comparableSessions=${JSON.stringify(comparableSessions)}`,\n      ).toEqual([]);\n      expect(popupInitScriptMarker).toBe(true);\n      expect(iframeInitScriptMarker).toBe(true);\n    });\n  }\n});\n"
  },
  {
    "path": "packages/core/tests/integration/iframe-ctx-addInitScript.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { V3Context } from \"../../lib/v3/understudy/context.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\n\nconst isBrowserbase =\n  (process.env.STAGEHAND_BROWSER_TARGET ?? \"local\").toLowerCase() ===\n  \"browserbase\";\nconst MIN_TIMEOUT_MS = 3_000;\nconst MAX_TIMEOUT_MS = 120_000;\n\nconst parseBoundedTimeoutMs = (\n  value: string | undefined,\n  fallbackMs: number,\n): number => {\n  const parsed = Number(value ?? fallbackMs);\n  if (!Number.isFinite(parsed)) return fallbackMs;\n  return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, parsed));\n};\n\nconst CHILD_FRAME_TIMEOUT_MS = parseBoundedTimeoutMs(\n  process.env.IFRAME_CHILD_FRAME_TIMEOUT_MS,\n  isBrowserbase ? 80_000 : 40_000,\n);\nconst POPUP_TIMEOUT_MS = parseBoundedTimeoutMs(\n  process.env.IFRAME_POPUP_TIMEOUT_MS,\n  isBrowserbase ? 60_000 : 40_000,\n);\nconst POPUP_URL_TIMEOUT_MS = parseBoundedTimeoutMs(\n  process.env.IFRAME_POPUP_URL_TIMEOUT_MS,\n  isBrowserbase ? 80_000 : 40_000,\n);\nconst DEBUG_INTERVAL_MS = 5_000;\nconst iframeDebugEnabled = isBrowserbase || process.env.IFRAME_DEBUG === \"1\";\nconst TEST_VIEWPORT = { width: 1288, height: 711 };\n\ntype FrameTreeNode = {\n  frame: { id: string; parentId?: string; url?: string };\n  childFrames?: FrameTreeNode[];\n};\ntype ChildFrame = ReturnType<Page[\"frames\"]>[number];\ntype ChildFrameProbe = {\n  child: ChildFrame;\n  href?: string;\n  readyState?: DocumentReadyState;\n  error?: string;\n};\n\nconst formatError = (error: unknown): string => {\n  if (error instanceof Error) return error.message;\n  return String(error);\n};\n\nconst flattenFrameTree = (\n  node: FrameTreeNode,\n  out: Array<{ id: string; parentId: string | null; url: string }> = [],\n): Array<{ id: string; parentId: string | null; url: string }> => {\n  out.push({\n    id: node.frame.id,\n    parentId: node.frame.parentId ?? null,\n    url: node.frame.url ?? \"\",\n  });\n  for (const child of node.childFrames ?? []) {\n    flattenFrameTree(child, out);\n  }\n  return out;\n};\n\nfunction debugLog(\n  step: string,\n  payload?: Record<string, unknown> | string,\n): void {\n  if (!iframeDebugEnabled) return;\n  if (payload === undefined) {\n    console.log(`[iframe-debug] ${step}`);\n    return;\n  }\n  if (typeof payload === \"string\") {\n    console.log(`[iframe-debug] ${step}: ${payload}`);\n    return;\n  }\n  try {\n    console.log(`[iframe-debug] ${step}: ${JSON.stringify(payload)}`);\n  } catch {\n    console.log(`[iframe-debug] ${step}: <unserializable payload>`);\n  }\n}\n\nasync function collectFrameSnapshot(\n  page: Page,\n): Promise<Array<Record<string, unknown>>> {\n  const known = new Map<string, ReturnType<Page[\"frames\"]>[number]>();\n  known.set(page.mainFrame().frameId, page.mainFrame());\n  for (const frame of page.frames()) known.set(frame.frameId, frame);\n\n  return Promise.all(\n    [...known.values()].map(async (frame) => {\n      try {\n        const state = await frame.evaluate(() => {\n          return {\n            href: location.href,\n            readyState: document.readyState,\n            visibilityState: document.visibilityState,\n            iframeCount: document.querySelectorAll(\"iframe\").length,\n            hasShadowHost: Boolean(document.querySelector(\"shadow-host\")),\n          };\n        });\n        return {\n          frameId: frame.frameId,\n          sessionId: frame.sessionId ?? \"root\",\n          ...state,\n        };\n      } catch (error) {\n        return {\n          frameId: frame.frameId,\n          sessionId: frame.sessionId ?? \"root\",\n          error: formatError(error),\n        };\n      }\n    }),\n  );\n}\n\nasync function logPageDiagnostics(\n  page: Page,\n  reason: string,\n  markerSelector?: string,\n): Promise<void> {\n  if (!iframeDebugEnabled) return;\n  const diagnostics: Record<string, unknown> = {\n    reason,\n    pageUrl: page.url(),\n    mainFrameId: page.mainFrame().frameId,\n    knownFrameCount: page.frames().length,\n  };\n\n  try {\n    const domState = await page.mainFrame().evaluate((marker) => {\n      const el = marker ? document.querySelector(marker) : null;\n      const rect =\n        el instanceof Element ? el.getBoundingClientRect().toJSON() : null;\n      return {\n        href: location.href,\n        readyState: document.readyState,\n        visibilityState: document.visibilityState,\n        hidden: document.hidden,\n        hasFocus: document.hasFocus(),\n        innerWidth: window.innerWidth,\n        innerHeight: window.innerHeight,\n        devicePixelRatio: window.devicePixelRatio,\n        markerSelector: marker,\n        markerPresent: Boolean(el),\n        markerRect: rect,\n        iframeCount: document.querySelectorAll(\"iframe\").length,\n      };\n    }, markerSelector);\n    diagnostics.domState = domState;\n  } catch (error) {\n    diagnostics.domStateError = formatError(error);\n  }\n\n  try {\n    const frameTreeResponse = (await page.sendCDP(\"Page.getFrameTree\")) as {\n      frameTree?: FrameTreeNode;\n    };\n    if (frameTreeResponse.frameTree) {\n      diagnostics.cdpFrameTree = flattenFrameTree(frameTreeResponse.frameTree);\n    }\n  } catch (error) {\n    diagnostics.cdpFrameTreeError = formatError(error);\n  }\n\n  diagnostics.frameSnapshot = await collectFrameSnapshot(page);\n  debugLog(\"page-diagnostics\", diagnostics);\n}\n\nasync function closeAllPages(ctx: V3Context): Promise<void> {\n  const pages = ctx.pages();\n  await Promise.allSettled(pages.map((page) => page.close()));\n}\n\n/**\n * Poll until a child frame (non-main) appears on `page` and its document\n * has finished loading.  Returns the child frame.\n */\nasync function waitForChildFrame(\n  page: Page,\n  expectedChildUrl: string,\n  timeoutMs = CHILD_FRAME_TIMEOUT_MS,\n): Promise<ChildFrame> {\n  const mainFrameId = page.mainFrame().frameId;\n  const deadline = Date.now() + timeoutMs;\n  let observedFrameCount = 0;\n  const observedChildFrameIds = new Set<string>();\n  let lastUrl = \"\";\n  let lastLogAt = Date.now();\n\n  while (Date.now() < deadline) {\n    const frames = page.frames();\n    observedFrameCount = Math.max(observedFrameCount, frames.length);\n    lastUrl = page.url();\n    const childIds = frames\n      .filter((f) => f.frameId !== mainFrameId)\n      .map((f) => f.frameId);\n    if (iframeDebugEnabled && Date.now() - lastLogAt >= DEBUG_INTERVAL_MS) {\n      debugLog(\"waitForChildFrame:progress\", {\n        url: lastUrl,\n        mainFrameId,\n        observedFrameCount,\n        childIds,\n        expectedChildUrl,\n      });\n      lastLogAt = Date.now();\n    }\n    for (const childId of childIds) observedChildFrameIds.add(childId);\n\n    const childFrames = frames\n      .filter((f) => f.frameId !== mainFrameId)\n      // Prefer recently-discovered frames first; stale swapped frame ids\n      // can remain visible in the registry while the live OOPIF is ready.\n      .reverse();\n\n    if (childFrames.length) {\n      const probes = await Promise.all(\n        childFrames.map(async (child): Promise<ChildFrameProbe> => {\n          try {\n            const state = await child.evaluate(\n              (): { href: string; readyState: DocumentReadyState } => ({\n                href: location.href,\n                readyState: document.readyState,\n              }),\n            );\n            return {\n              child,\n              href: state.href,\n              readyState: state.readyState,\n            };\n          } catch (error) {\n            const failedProbe: ChildFrameProbe = {\n              child,\n              href: undefined,\n              readyState: undefined,\n              error: formatError(error),\n            };\n            return failedProbe;\n          }\n        }),\n      );\n\n      const ready = probes.find(\n        (probe) =>\n          probe.readyState === \"complete\" && probe.href === expectedChildUrl,\n      );\n      if (ready) {\n        debugLog(\"waitForChildFrame:ready\", {\n          childFrameId: ready.child.frameId,\n          childSessionId: ready.child.sessionId ?? \"root\",\n          childUrl: ready.href ?? \"<unknown>\",\n          expectedChildUrl,\n          url: lastUrl,\n        });\n        return ready.child;\n      }\n\n      if (iframeDebugEnabled && Date.now() - lastLogAt >= DEBUG_INTERVAL_MS) {\n        debugLog(\"waitForChildFrame:not-ready\", {\n          url: lastUrl,\n          mainFrameId,\n          expectedChildUrl,\n          probes: probes.map((probe) => ({\n            frameId: probe.child.frameId,\n            sessionId: probe.child.sessionId ?? \"root\",\n            readyState: probe.readyState ?? \"<unknown>\",\n            href: probe.href ?? \"<unknown>\",\n            error: probe.error ?? \"<none>\",\n          })),\n        });\n        lastLogAt = Date.now();\n      }\n    }\n    await new Promise((r) => setTimeout(r, 100));\n  }\n  await logPageDiagnostics(page, \"waitForChildFrame timeout\");\n  throw new Error(\n    `Timed out waiting for child frame to load (timeout=${timeoutMs}ms, mainFrameId=${mainFrameId}, expectedChildUrl=${expectedChildUrl}, maxObservedFrames=${observedFrameCount}, observedChildFrameIds=[${[...observedChildFrameIds].join(\",\")}], url=${lastUrl || \"<unknown>\"})`,\n  );\n}\n\nasync function waitForPageUrl(\n  page: Page,\n  expectedUrlSubstring: string,\n  timeoutMs = POPUP_URL_TIMEOUT_MS,\n): Promise<void> {\n  const deadline = Date.now() + timeoutMs;\n  let lastUrl = \"\";\n  let lastLogAt = Date.now();\n  while (Date.now() < deadline) {\n    lastUrl = page.url();\n    if (iframeDebugEnabled && Date.now() - lastLogAt >= DEBUG_INTERVAL_MS) {\n      debugLog(\"waitForPageUrl:progress\", {\n        expectedUrlSubstring,\n        lastUrl,\n      });\n      lastLogAt = Date.now();\n    }\n    if (lastUrl.includes(expectedUrlSubstring)) {\n      debugLog(\"waitForPageUrl:ready\", {\n        expectedUrlSubstring,\n        lastUrl,\n      });\n      return;\n    }\n    await new Promise((r) => setTimeout(r, 100));\n  }\n  await logPageDiagnostics(\n    page,\n    `waitForPageUrl timeout for ${expectedUrlSubstring}`,\n  );\n  throw new Error(\n    `Timed out waiting for popup URL to include \"${expectedUrlSubstring}\" (timeout=${timeoutMs}ms, lastUrl=${lastUrl || \"<unknown>\"})`,\n  );\n}\n\nasync function preparePopupForFrameAttach(\n  page: Page,\n  markerSelector: string,\n  timeoutMs = CHILD_FRAME_TIMEOUT_MS,\n): Promise<void> {\n  debugLog(\"preparePopupForFrameAttach:start\", {\n    markerSelector,\n    timeoutMs,\n    url: page.url(),\n  });\n  await page.waitForLoadState(\"domcontentloaded\", timeoutMs);\n  await page.waitForSelector(markerSelector, {\n    state: \"attached\",\n    timeout: timeoutMs,\n  });\n  await page.mainFrame().evaluate(() => {\n    const host = document.querySelector(\"shadow-host\");\n    if (host instanceof HTMLElement) {\n      host.scrollIntoView({ block: \"center\", inline: \"center\" });\n    } else {\n      window.scrollTo(0, document.body.scrollHeight);\n      window.scrollTo(0, 0);\n    }\n    window.dispatchEvent(new Event(\"scroll\"));\n  });\n  await logPageDiagnostics(\n    page,\n    \"preparePopupForFrameAttach:ready\",\n    markerSelector,\n  );\n}\n\nasync function ensurePopupViewport(page: Page): Promise<void> {\n  await page.setViewportSize(TEST_VIEWPORT.width, TEST_VIEWPORT.height);\n  await logPageDiagnostics(page, \"ensurePopupViewport\");\n}\n\nasync function waitForPopupPage(\n  ctx: V3Context,\n  opener: Page,\n  timeoutMs = POPUP_TIMEOUT_MS,\n): Promise<Page> {\n  const openerMainFrameId = opener.mainFrame().frameId;\n  const deadline = Date.now() + timeoutMs;\n  let lastLogAt = Date.now();\n\n  while (Date.now() < deadline) {\n    const pages = ctx.pages();\n    const popup = pages.find((candidate) => {\n      return candidate.mainFrame().frameId !== openerMainFrameId;\n    });\n    if (popup) {\n      debugLog(\"waitForPopupPage:found\", {\n        openerMainFrameId,\n        popupMainFrameId: popup.mainFrame().frameId,\n        popupUrl: popup.url(),\n      });\n      return popup;\n    }\n\n    if (iframeDebugEnabled && Date.now() - lastLogAt >= DEBUG_INTERVAL_MS) {\n      debugLog(\"waitForPopupPage:progress\", {\n        openerMainFrameId,\n        observedPageIds: pages.map((p) => p.mainFrame().frameId),\n      });\n      lastLogAt = Date.now();\n    }\n\n    try {\n      const active = await ctx.awaitActivePage(500);\n      if (active.mainFrame().frameId !== openerMainFrameId) {\n        debugLog(\"waitForPopupPage:active-non-opener\", {\n          openerMainFrameId,\n          activeMainFrameId: active.mainFrame().frameId,\n          activeUrl: active.url(),\n        });\n        return active;\n      }\n    } catch {\n      // keep polling until timeout\n    }\n\n    await new Promise((r) => setTimeout(r, 100));\n  }\n\n  const pageIds = ctx\n    .pages()\n    .map((p) => p.mainFrame().frameId)\n    .join(\", \");\n  throw new Error(\n    `Timed out waiting for popup page (timeout=${timeoutMs}ms, openerMainFrameId=${openerMainFrameId}, observedPages=[${pageIds}])`,\n  );\n}\n\ntest.describe(\"context.addInitScript with iframes\", () => {\n  const OOPIF_CHILD_URL =\n    \"https://seanmcguire12.github.io/stagehand-oopif-sites/sites/form-filling/\";\n  const SPIF_CHILD_URL =\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-closed-shadow-dom/iframe.html\";\n  const POPUP_SPIF_CHILD_URL =\n    \"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-dom-in-spif/embedded.html\";\n\n  if (isBrowserbase) {\n    test.describe.configure({ mode: \"serial\" });\n  }\n\n  let v3: V3;\n  let ctx: V3Context;\n\n  test.beforeAll(async () => {\n    debugLog(\"beforeAll:config\", {\n      browserTarget: process.env.STAGEHAND_BROWSER_TARGET ?? \"local\",\n      childFrameTimeoutMs: CHILD_FRAME_TIMEOUT_MS,\n      popupTimeoutMs: POPUP_TIMEOUT_MS,\n      popupUrlTimeoutMs: POPUP_URL_TIMEOUT_MS,\n    });\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n    ctx = v3.context;\n\n    // Add init script that sets background to red\n    await ctx.addInitScript(`\n      (() => {\n        document.addEventListener('DOMContentLoaded', () => {\n          document.documentElement.style.backgroundColor = 'red';\n        });\n      })();\n    `);\n  });\n\n  test.beforeEach(async () => {\n    await closeAllPages(ctx);\n  });\n\n  test.afterEach(async () => {\n    await closeAllPages(ctx);\n  });\n\n  test.afterAll(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test.describe(\"direct navigation\", () => {\n    test(\"with OOPIF - sets background red in main page and iframe\", async () => {\n      const page = await ctx.newPage();\n\n      await page.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n        { waitUntil: \"networkidle\" },\n      );\n\n      const iframe = await waitForChildFrame(page, OOPIF_CHILD_URL);\n\n      // Check main page background\n      const mainBgColor = await page.mainFrame().evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(mainBgColor).toBe(\"rgb(255, 0, 0)\");\n\n      const iframeBgColor = await iframe.evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(iframeBgColor).toBe(\"rgb(255, 0, 0)\");\n    });\n\n    test(\"with SPIF - sets background red in main page and iframe\", async () => {\n      const page = await ctx.newPage();\n\n      await page.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-closed-shadow-dom/\",\n        { waitUntil: \"networkidle\" },\n      );\n\n      const iframe = await waitForChildFrame(page, SPIF_CHILD_URL);\n\n      // Check main page background\n      const mainBgColor = await page.mainFrame().evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(mainBgColor).toBe(\"rgb(255, 0, 0)\");\n\n      const iframeBgColor = await iframe.evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(iframeBgColor).toBe(\"rgb(255, 0, 0)\");\n    });\n  });\n\n  test.describe(\"via newPage\", () => {\n    test(\"with OOPIF - sets background red in main page and iframe\", async () => {\n      const page = await ctx.newPage();\n\n      await page.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n        { waitUntil: \"networkidle\" },\n      );\n\n      const iframe = await waitForChildFrame(page, OOPIF_CHILD_URL);\n\n      // Check main page background\n      const mainBgColor = await page.mainFrame().evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(mainBgColor).toBe(\"rgb(255, 0, 0)\");\n\n      const iframeBgColor = await iframe.evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(iframeBgColor).toBe(\"rgb(255, 0, 0)\");\n    });\n\n    test(\"with SPIF - sets background red in main page and iframe\", async () => {\n      const page = await ctx.newPage();\n\n      await page.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-closed-shadow-dom/\",\n        { waitUntil: \"networkidle\" },\n      );\n\n      const iframe = await waitForChildFrame(page, SPIF_CHILD_URL);\n\n      // Check main page background\n      const mainBgColor = await page.mainFrame().evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(mainBgColor).toBe(\"rgb(255, 0, 0)\");\n\n      const iframeBgColor = await iframe.evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(iframeBgColor).toBe(\"rgb(255, 0, 0)\");\n    });\n  });\n\n  test.describe(\"via popup\", () => {\n    test(\"with OOPIF - sets background red in main page and iframe\", async () => {\n      const page = await ctx.newPage();\n\n      await page.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/ctx-add-init-script-oopif/\",\n        { waitUntil: \"networkidle\" },\n      );\n\n      // Click link to open popup\n      await page.locator(\"a\").click();\n      debugLog(\"popup-oopif:clicked-link\", { openerUrl: page.url() });\n\n      // Wait for popup to open and become active\n      const popup = await waitForPopupPage(ctx, page);\n      ctx.setActivePage(popup);\n      await ensurePopupViewport(popup);\n      await waitForPageUrl(\n        popup,\n        \"/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n      );\n      debugLog(\"popup-oopif:refresh-navigation\", { url: popup.url() });\n      await popup.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n        { waitUntil: \"networkidle\" },\n      );\n      await logPageDiagnostics(\n        popup,\n        \"popup-oopif:after-refresh\",\n        \"shadow-host\",\n      );\n      await preparePopupForFrameAttach(popup, \"shadow-host\");\n      const iframe = await waitForChildFrame(popup, OOPIF_CHILD_URL);\n\n      // Check popup main page background\n      const mainBgColor = await popup.mainFrame().evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(mainBgColor).toBe(\"rgb(255, 0, 0)\");\n\n      const iframeBgColor = await iframe.evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(iframeBgColor).toBe(\"rgb(255, 0, 0)\");\n    });\n\n    test(\"with SPIF - sets background red in main page and iframe\", async () => {\n      const page = await ctx.newPage();\n\n      await page.goto(\n        \"https://browserbase.github.io/stagehand-eval-sites/sites/ctx-add-init-script-spif/\",\n        { waitUntil: \"networkidle\" },\n      );\n\n      // Click link to open popup\n      await page.locator(\"a\").click();\n      debugLog(\"popup-spif:clicked-link\", { openerUrl: page.url() });\n\n      // Wait for popup to open and become active\n      const popup = await waitForPopupPage(ctx, page);\n      ctx.setActivePage(popup);\n      await ensurePopupViewport(popup);\n      await waitForPageUrl(\n        popup,\n        \"/stagehand-eval-sites/sites/closed-shadow-dom-in-spif/\",\n      );\n      await preparePopupForFrameAttach(popup, \"iframe\");\n      const iframe = await waitForChildFrame(popup, POPUP_SPIF_CHILD_URL);\n\n      // Check popup main page background\n      const mainBgColor = await popup.mainFrame().evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(mainBgColor).toBe(\"rgb(255, 0, 0)\");\n\n      const iframeBgColor = await iframe.evaluate(() => {\n        return getComputedStyle(document.documentElement).backgroundColor;\n      });\n      expect(iframeBgColor).toBe(\"rgb(255, 0, 0)\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/keep-alive.child.ts",
    "content": "import { V3 } from \"../../lib/v3/v3.js\";\n\nasync function main(): Promise<void> {\n  const encoded = process.argv.find((arg) => arg.startsWith(\"cfg:\"));\n  if (!encoded) {\n    throw new Error(\"Missing child config payload.\");\n  }\n  const raw = Buffer.from(encoded.slice(4), \"base64\").toString(\"utf8\");\n  const cfg = JSON.parse(raw) as {\n    env: \"LOCAL\" | \"BROWSERBASE\";\n    keepAlive: boolean;\n    disableAPI: boolean;\n    scenario: string;\n    apiKey?: string;\n    projectId?: string;\n    debug?: boolean;\n    viewMs?: number;\n  };\n  const {\n    env,\n    keepAlive,\n    disableAPI,\n    scenario,\n    apiKey,\n    projectId,\n    debug = false,\n    viewMs = 0,\n  } = cfg;\n\n  const log = (message: string): void => {\n    if (debug) {\n      console.log(message);\n    }\n  };\n\n  if (env !== \"LOCAL\" && env !== \"BROWSERBASE\") {\n    throw new Error(\"KEEP_ALIVE_ENV must be LOCAL or BROWSERBASE\");\n  }\n  if (!scenario) {\n    throw new Error(\"KEEP_ALIVE_SCENARIO is required\");\n  }\n\n  log(\n    `[keep-alive-child] env=${env} keepAlive=${keepAlive} disableAPI=${disableAPI} ` +\n      `scenario=${scenario} apiKey=${apiKey ? \"set\" : \"missing\"} ` +\n      `projectId=${projectId ? \"set\" : \"missing\"}`,\n  );\n\n  const showBrowser = viewMs > 0;\n  const v3 = new V3({\n    env,\n    keepAlive,\n    disableAPI,\n    apiKey,\n    projectId,\n    browserbaseSessionCreateParams: undefined,\n    localBrowserLaunchOptions:\n      env === \"LOCAL\"\n        ? {\n            executablePath: process.env.CHROME_PATH,\n            args: process.env.CI ? [\"--no-sandbox\"] : undefined,\n            headless: !showBrowser,\n            viewport: { width: 1288, height: 711 },\n          }\n        : undefined,\n    verbose: debug ? 2 : 0,\n    disablePino: true,\n    logger: debug ? (line) => console.log(line) : undefined,\n  });\n\n  await v3.init();\n\n  const info = {\n    connectURL: v3.connectURL(),\n    sessionId: v3.browserbaseSessionId ?? null,\n  };\n  await new Promise<void>((resolve, reject) => {\n    process.stdout.write(`__KEEPALIVE__${JSON.stringify(info)}\\n`, (error) => {\n      if (error) {\n        reject(error);\n        return;\n      }\n      resolve();\n    });\n  });\n\n  if (env === \"LOCAL\" && viewMs > 0) {\n    await new Promise((r) => setTimeout(r, viewMs));\n  }\n\n  if (scenario === \"close\") {\n    await v3.close().catch(() => {});\n    process.exit(0);\n  }\n\n  if (scenario === \"sigterm\") {\n    return;\n  }\n\n  if (scenario === \"sigint\") {\n    return;\n  }\n\n  if (scenario === \"unhandled\") {\n    setTimeout(() => {\n      void Promise.reject(new Error(\"keepAlive unhandled rejection\"));\n    }, 0);\n    return;\n  }\n\n  throw new Error(`Unknown scenario: ${scenario}`);\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/core/tests/integration/keep-alive.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport Browserbase from \"@browserbasehq/sdk\";\nimport WebSocket from \"ws\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { getPackageRootDir } from \"../../lib/v3/runtimePaths.js\";\n\nexport type EnvKind = \"LOCAL\" | \"BROWSERBASE\";\nexport type ScenarioKind = \"unhandled\" | \"close\" | \"sigterm\" | \"sigint\";\n\nexport type KeepAliveCase = {\n  title: string;\n  env: EnvKind;\n  envLabel: string;\n  keepAlive: boolean;\n  disableAPI: boolean;\n  kind: ScenarioKind;\n  requiresBrowserbase: boolean;\n};\n\ntype ScenarioConfig = {\n  env: EnvKind;\n  keepAlive: boolean;\n  disableAPI: boolean;\n  kind: ScenarioKind;\n  debug: boolean;\n  viewMs: number;\n  apiKey?: string;\n  projectId?: string;\n};\n\ntype ChildInfo = {\n  connectURL: string;\n  sessionId: string | null;\n};\n\ntype ChildLogs = {\n  stdout: string[];\n  stderr: string[];\n};\n\ntype CheckResult = {\n  alive: boolean;\n  status?: string;\n};\n\ntype Outcome = {\n  expected: \"open\" | \"closed\";\n  actual: \"open\" | \"closed\";\n  durationMs: number;\n  lastStatus?: string;\n};\n\nconst coreDir = getPackageRootDir();\n\nconst resolveChildRunner = (): { command: string; args: string[] } | null => {\n  const distJsPath = `${coreDir}/dist/esm/tests/integration/keep-alive.child.js`;\n  if (fs.existsSync(distJsPath)) {\n    return { command: process.execPath, args: [distJsPath] };\n  }\n\n  return null;\n};\n\nconst childRunner = resolveChildRunner();\n\nconst DEBUG = process.env.KEEP_ALIVE_DEBUG === \"1\";\nconst VIEW_MS = Number(process.env.KEEP_ALIVE_VIEW_MS ?? \"0\");\nconst LOCAL_TIMEOUT_MS = Number(\n  process.env.KEEP_ALIVE_LOCAL_TIMEOUT_MS ?? \"8000\",\n);\nconst BB_TIMEOUT_MS = Number(process.env.KEEP_ALIVE_BB_TIMEOUT_MS ?? \"30000\");\nconst STAY_OPEN_MS = Number(process.env.KEEP_ALIVE_STAY_OPEN_MS ?? \"6000\");\nconst ACTION_EXIT_TIMEOUT_MS = Number(\n  process.env.KEEP_ALIVE_ACTION_EXIT_TIMEOUT_MS ?? \"3000\",\n);\nconst LOCAL_INFO_TIMEOUT_MS = Number(\n  process.env.KEEP_ALIVE_LOCAL_INFO_TIMEOUT_MS ?? \"15000\",\n);\nconst BB_INFO_TIMEOUT_MS = Number(\n  process.env.KEEP_ALIVE_BB_INFO_TIMEOUT_MS ??\n    (process.env.CI ? \"45000\" : \"30000\"),\n);\n\nconst getInfoTimeoutMs = (env: EnvKind): number =>\n  env === \"BROWSERBASE\" ? BB_INFO_TIMEOUT_MS : LOCAL_INFO_TIMEOUT_MS;\n\nfunction debugLog(message: string): void {\n  if (DEBUG) {\n    console.log(message);\n  }\n}\n\nfunction parseChildInfo(line: string): ChildInfo | null {\n  const prefix = \"__KEEPALIVE__\";\n  if (!line.startsWith(prefix)) return null;\n  try {\n    return JSON.parse(line.slice(prefix.length)) as ChildInfo;\n  } catch {\n    return null;\n  }\n}\n\nasync function runScenario(config: ScenarioConfig): Promise<{\n  info: ChildInfo;\n  child: ReturnType<typeof spawn>;\n  logs: ChildLogs;\n}> {\n  const payload = {\n    env: config.env,\n    keepAlive: config.keepAlive,\n    disableAPI: config.disableAPI,\n    scenario: config.kind,\n    apiKey: config.apiKey,\n    projectId: config.projectId,\n    debug: config.debug,\n    viewMs: config.viewMs,\n  };\n  const encoded = `cfg:${Buffer.from(JSON.stringify(payload)).toString(\"base64\")}`;\n\n  if (!childRunner) {\n    throw new Error(\n      \"keep-alive child script not found at dist/esm/tests/integration/keep-alive.child.js\",\n    );\n  }\n\n  const child = spawn(childRunner.command, [...childRunner.args, encoded], {\n    cwd: coreDir,\n    env: { ...process.env },\n    stdio: [\"ignore\", \"pipe\", \"pipe\"],\n  });\n\n  const logs: ChildLogs = { stdout: [], stderr: [] };\n  let buffer = \"\";\n  let stderr = \"\";\n  let resolved = false;\n  const infoTimeoutMs = getInfoTimeoutMs(config.env);\n\n  const infoPromise = new Promise<ChildInfo>((resolve, reject) => {\n    const timeout = setTimeout(() => {\n      child.kill(\"SIGKILL\");\n      const stdoutDetails =\n        logs.stdout.length > 0\n          ? `\\nChild stdout:\\n${logs.stdout.join(\"\\n\")}`\n          : \"\";\n      const details = stderr.trim();\n      const suffix = details\n        ? `\\nChild stderr:\\n${details}`\n        : \"\\nChild did not emit keepAlive info.\";\n      reject(\n        new Error(\n          `Child timed out waiting for info after ${infoTimeoutMs}ms (env=${config.env}, keepAlive=${config.keepAlive}, disableAPI=${config.disableAPI}, scenario=${config.kind}).${suffix}${stdoutDetails}`,\n        ),\n      );\n    }, infoTimeoutMs);\n\n    child.stdout.on(\"data\", (chunk) => {\n      buffer += chunk.toString();\n      let idx = buffer.indexOf(\"\\n\");\n      while (idx !== -1) {\n        const line = buffer.slice(0, idx).trim();\n        buffer = buffer.slice(idx + 1);\n        const parsed = parseChildInfo(line);\n        if (parsed && !resolved) {\n          resolved = true;\n          clearTimeout(timeout);\n          resolve(parsed);\n        } else if (line.length > 0) {\n          logs.stdout.push(line);\n          debugLog(`[keep-alive-child] ${line}`);\n        }\n        idx = buffer.indexOf(\"\\n\");\n      }\n    });\n\n    child.on(\"exit\", (code, signal) => {\n      if (resolved) return;\n      clearTimeout(timeout);\n      const stdoutDetails =\n        logs.stdout.length > 0\n          ? `\\nChild stdout:\\n${logs.stdout.join(\"\\n\")}`\n          : \"\";\n      const details = stderr.trim();\n      const suffix = details\n        ? `\\nChild stderr:\\n${details}`\n        : \"\\nChild exited without emitting keepAlive info.\";\n      reject(\n        new Error(\n          `Child exited (code=${code ?? \"null\"}, signal=${signal ?? \"null\"}) before emitting keepAlive info (env=${config.env}, keepAlive=${config.keepAlive}, disableAPI=${config.disableAPI}, scenario=${config.kind}).${suffix}${stdoutDetails}`,\n        ),\n      );\n    });\n\n    child.on(\"error\", (error) => {\n      if (resolved) return;\n      clearTimeout(timeout);\n      reject(error);\n    });\n  });\n\n  child.stderr.on(\"data\", (chunk) => {\n    const text = chunk.toString();\n    stderr += text;\n    const trimmed = text.trim();\n    if (trimmed.length > 0) {\n      logs.stderr.push(trimmed);\n      debugLog(`[keep-alive-child] ${trimmed}`);\n    }\n  });\n\n  const info = await infoPromise;\n  return { info, child, logs };\n}\n\nasync function stopChild(child: ReturnType<typeof spawn>): Promise<void> {\n  if (child.exitCode !== null) return;\n  try {\n    child.kill(\"SIGKILL\");\n  } catch {\n    return;\n  }\n  await new Promise<void>((resolve) => {\n    const timer = setTimeout(() => resolve(), 2000);\n    child.once(\"exit\", () => {\n      clearTimeout(timer);\n      resolve();\n    });\n  });\n}\n\nasync function waitForChildExit(\n  child: ReturnType<typeof spawn>,\n  timeoutMs: number,\n): Promise<void> {\n  if (child.exitCode !== null) return;\n  await new Promise<void>((resolve) => {\n    const timer = setTimeout(() => resolve(), timeoutMs);\n    child.once(\"exit\", () => {\n      clearTimeout(timer);\n      resolve();\n    });\n  });\n}\n\nasync function checkLocalAlive(connectURL: string): Promise<CheckResult> {\n  let port: string;\n  try {\n    port = new URL(connectURL).port;\n  } catch {\n    return { alive: false, status: \"INVALID_URL\" };\n  }\n  if (!port) return { alive: false, status: \"MISSING_PORT\" };\n\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), 1500);\n  try {\n    const resp = await fetch(`http://127.0.0.1:${port}/json/version`, {\n      signal: controller.signal,\n    });\n    if (!resp.ok) {\n      return { alive: false, status: `HTTP_${resp.status}` };\n    }\n    const json = (await resp.json()) as { webSocketDebuggerUrl?: string };\n    const ws = json?.webSocketDebuggerUrl;\n    if (!ws) {\n      return { alive: false, status: \"MISSING_WS\" };\n    }\n    if (ws !== connectURL) {\n      return { alive: false, status: \"WS_MISMATCH\" };\n    }\n    return { alive: true, status: \"MATCH\" };\n  } catch {\n    return { alive: false, status: \"FETCH_ERROR\" };\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\nasync function closeLocalBrowser(connectURL: string): Promise<void> {\n  await new Promise<void>((resolve) => {\n    const ws = new WebSocket(connectURL);\n    const timer = setTimeout(() => {\n      ws.terminate();\n      resolve();\n    }, 2000);\n    ws.on(\"open\", () => {\n      ws.send(JSON.stringify({ id: 1, method: \"Browser.close\" }));\n    });\n    ws.on(\"error\", () => {\n      clearTimeout(timer);\n      resolve();\n    });\n    ws.on(\"close\", () => {\n      clearTimeout(timer);\n      resolve();\n    });\n  });\n}\n\nasync function checkBrowserbaseAlive(\n  sessionId: string,\n  apiKey?: string,\n): Promise<CheckResult> {\n  if (!apiKey) return { alive: false, status: \"NO_API_KEY\" };\n\n  const bb = new Browserbase({ apiKey });\n  try {\n    const snapshot = (await bb.sessions.retrieve(sessionId)) as {\n      status?: string;\n    };\n    if (DEBUG) {\n      const status = snapshot?.status ?? \"<missing>\";\n      debugLog(`[keep-alive] session ${sessionId} status=${status}`);\n    }\n    const status = snapshot?.status;\n    return { alive: status === \"RUNNING\", status };\n  } catch (error) {\n    debugLog(\n      `[keep-alive] session ${sessionId} retrieve failed: ${String(error)}`,\n    );\n    return { alive: false, status: \"RETRIEVE_FAILED\" };\n  }\n}\n\nasync function endBrowserbaseSession(\n  sessionId: string,\n  apiKey?: string,\n  projectId?: string,\n): Promise<void> {\n  if (!apiKey || !projectId) return;\n  const bb = new Browserbase({ apiKey });\n  try {\n    await bb.sessions.update(sessionId, {\n      status: \"REQUEST_RELEASE\",\n      projectId,\n    });\n  } catch {\n    // best-effort cleanup\n  }\n}\n\nasync function assertStaysOpen(\n  check: () => Promise<CheckResult>,\n  durationMs: number,\n  intervalMs = 500,\n): Promise<{ durationMs: number; lastStatus?: string }> {\n  const start = Date.now();\n  const deadline = start + durationMs;\n  let lastStatus: string | undefined;\n  while (Date.now() < deadline) {\n    const result = await check();\n    lastStatus = result.status ?? lastStatus;\n    if (!result.alive) {\n      const elapsed = Date.now() - start;\n      const status = lastStatus ? ` (last status ${lastStatus})` : \"\";\n      throw new Error(\n        `Browser closed after ${elapsed}ms (expected ${durationMs}ms)${status}.`,\n      );\n    }\n    await new Promise((r) => setTimeout(r, intervalMs));\n  }\n  return { durationMs: Date.now() - start, lastStatus };\n}\n\nasync function waitForClosed(\n  check: () => Promise<CheckResult>,\n  timeoutMs: number,\n  intervalMs = 500,\n): Promise<{ durationMs: number; lastStatus?: string }> {\n  const start = Date.now();\n  let lastStatus: string | undefined;\n  while (Date.now() - start < timeoutMs) {\n    const result = await check();\n    lastStatus = result.status ?? lastStatus;\n    if (!result.alive) {\n      return { durationMs: Date.now() - start, lastStatus };\n    }\n    await new Promise((r) => setTimeout(r, intervalMs));\n  }\n  const status = lastStatus ? ` (last status ${lastStatus})` : \"\";\n  throw new Error(`Browser still alive after ${timeoutMs}ms${status}.`);\n}\n\nasync function assertBrowserState(\n  env: EnvKind,\n  info: ChildInfo,\n  shouldStayOpen: boolean,\n  apiKey?: string,\n  projectId?: string,\n): Promise<Outcome> {\n  const expected: Outcome[\"expected\"] = shouldStayOpen ? \"open\" : \"closed\";\n  if (env === \"LOCAL\") {\n    if (shouldStayOpen) {\n      const result = await assertStaysOpen(\n        () => checkLocalAlive(info.connectURL),\n        STAY_OPEN_MS,\n      );\n      const outcome: Outcome = {\n        expected,\n        actual: \"open\",\n        durationMs: result.durationMs,\n        lastStatus: result.lastStatus,\n      };\n      await closeLocalBrowser(info.connectURL);\n      return outcome;\n    }\n\n    const result = await waitForClosed(\n      () => checkLocalAlive(info.connectURL),\n      LOCAL_TIMEOUT_MS,\n    );\n    return {\n      expected,\n      actual: \"closed\",\n      durationMs: result.durationMs,\n      lastStatus: result.lastStatus,\n    };\n  }\n\n  if (!info.sessionId) {\n    throw new Error(\"Browserbase sessionId missing\");\n  }\n\n  if (shouldStayOpen) {\n    const result = await assertStaysOpen(\n      () => checkBrowserbaseAlive(info.sessionId!, apiKey),\n      STAY_OPEN_MS,\n      1000,\n    );\n    const outcome: Outcome = {\n      expected,\n      actual: \"open\",\n      durationMs: result.durationMs,\n      lastStatus: result.lastStatus,\n    };\n    await endBrowserbaseSession(info.sessionId, apiKey, projectId);\n    return outcome;\n  }\n\n  const result = await waitForClosed(\n    () => checkBrowserbaseAlive(info.sessionId!, apiKey),\n    BB_TIMEOUT_MS,\n    1000,\n  );\n  return {\n    expected,\n    actual: \"closed\",\n    durationMs: result.durationMs,\n    lastStatus: result.lastStatus,\n  };\n}\n\nfunction dumpLogs(logs: ChildLogs): void {\n  if (logs.stdout.length > 0) {\n    console.log(\"[keep-alive] child stdout:\");\n    for (const line of logs.stdout) {\n      console.log(`  ${line}`);\n    }\n  }\n  if (logs.stderr.length > 0) {\n    console.log(\"[keep-alive] child stderr:\");\n    for (const line of logs.stderr) {\n      console.log(`  ${line}`);\n    }\n  }\n}\n\nfunction logCaseResult(\n  label: string,\n  envLabel: string,\n  keepAlive: boolean,\n  outcome?: Outcome,\n  error?: Error,\n): void {\n  const prefix = `[keep-alive] ${envLabel} keepAlive=${keepAlive} ${label}`;\n  if (error) {\n    console.log(`${prefix} FAIL: ${error.message}`);\n    return;\n  }\n  if (!outcome) {\n    console.log(`${prefix} FAIL: missing outcome`);\n    return;\n  }\n  const status =\n    outcome.lastStatus !== undefined\n      ? ` (last status ${outcome.lastStatus})`\n      : \"\";\n  if (outcome.actual === \"open\") {\n    console.log(\n      `${prefix} PASS: stayed open for ${outcome.durationMs}ms${status}`,\n    );\n  } else {\n    console.log(\n      `${prefix} PASS: closed after ${outcome.durationMs}ms${status}`,\n    );\n  }\n}\n\nexport function getKeepAliveEnvConfig(): {\n  testEnv: EnvKind;\n  apiKey?: string;\n  projectId?: string;\n  hasBrowserbaseCreds: boolean;\n} {\n  const testEnv = v3DynamicTestConfig.env;\n  const apiKey =\n    testEnv === \"BROWSERBASE\"\n      ? (v3DynamicTestConfig.apiKey as string | undefined)\n      : undefined;\n  const projectId =\n    testEnv === \"BROWSERBASE\"\n      ? (v3DynamicTestConfig.projectId as string | undefined)\n      : undefined;\n  const hasBrowserbaseCreds = Boolean(apiKey && projectId);\n  return { testEnv, apiKey, projectId, hasBrowserbaseCreds };\n}\n\nexport function buildKeepAliveCases(testEnv: EnvKind): KeepAliveCase[] {\n  const scenarios: Array<{ kind: ScenarioKind; label: string }> = [\n    { kind: \"unhandled\", label: \"unhandled rejection\" },\n    { kind: \"close\", label: \"stagehand.close()\" },\n    { kind: \"sigterm\", label: \"SIGTERM\" },\n    { kind: \"sigint\", label: \"SIGINT\" },\n  ];\n\n  const environments: Array<{\n    env: EnvKind;\n    label: string;\n    disableAPI: boolean;\n    requiresBrowserbase: boolean;\n  }> =\n    testEnv === \"BROWSERBASE\"\n      ? [\n          {\n            env: \"BROWSERBASE\",\n            label: \"bb direct ws\",\n            disableAPI: true,\n            requiresBrowserbase: true,\n          },\n          {\n            env: \"BROWSERBASE\",\n            label: \"bb via api\",\n            disableAPI: false,\n            requiresBrowserbase: true,\n          },\n        ]\n      : [\n          {\n            env: \"LOCAL\",\n            label: \"local\",\n            disableAPI: false,\n            requiresBrowserbase: false,\n          },\n        ];\n\n  const cases: KeepAliveCase[] = [];\n  for (const keepAlive of [true, false]) {\n    for (const envConfig of environments) {\n      for (const scenario of scenarios) {\n        const expectation = keepAlive ? \"expect open\" : \"expect closed\";\n        cases.push({\n          title: `${envConfig.label} keepAlive=${keepAlive} ${scenario.label} (${expectation})`,\n          env: envConfig.env,\n          envLabel: envConfig.label,\n          keepAlive,\n          disableAPI: envConfig.disableAPI,\n          kind: scenario.kind,\n          requiresBrowserbase: envConfig.requiresBrowserbase,\n        });\n      }\n    }\n  }\n  return cases;\n}\n\nexport async function runKeepAliveCase(\n  testCase: KeepAliveCase,\n  envConfig: {\n    apiKey?: string;\n    projectId?: string;\n  },\n): Promise<void> {\n  let info: ChildInfo | undefined;\n  let child: ReturnType<typeof spawn> | undefined;\n  let logs: ChildLogs | undefined;\n  try {\n    ({ info, child, logs } = await runScenario({\n      env: testCase.env,\n      keepAlive: testCase.keepAlive,\n      disableAPI: testCase.disableAPI,\n      kind: testCase.kind,\n      debug: DEBUG,\n      viewMs: VIEW_MS,\n      apiKey: envConfig.apiKey,\n      projectId: envConfig.projectId,\n    }));\n  } catch (error) {\n    logCaseResult(\n      testCase.title,\n      testCase.envLabel,\n      testCase.keepAlive,\n      undefined,\n      error as Error,\n    );\n    throw error;\n  }\n\n  if (testCase.kind === \"sigterm\") {\n    child.kill(\"SIGTERM\");\n  } else if (testCase.kind === \"sigint\") {\n    child.kill(\"SIGINT\");\n  }\n\n  let outcome: Outcome | undefined;\n  let failure: Error | undefined;\n  try {\n    if (\n      testCase.kind === \"close\" ||\n      testCase.kind === \"unhandled\" ||\n      testCase.kind === \"sigterm\" ||\n      testCase.kind === \"sigint\"\n    ) {\n      await waitForChildExit(child, ACTION_EXIT_TIMEOUT_MS);\n    }\n    outcome = await assertBrowserState(\n      testCase.env,\n      info,\n      testCase.keepAlive,\n      envConfig.apiKey,\n      envConfig.projectId,\n    );\n  } catch (error) {\n    failure = error as Error;\n    if (logs) {\n      dumpLogs(logs);\n    }\n    throw error;\n  } finally {\n    logCaseResult(\n      testCase.title,\n      testCase.envLabel,\n      testCase.keepAlive,\n      outcome,\n      failure,\n    );\n    await stopChild(child);\n    if (testCase.env === \"LOCAL\" && info.connectURL) {\n      await closeLocalBrowser(info.connectURL);\n    }\n    if (testCase.env === \"BROWSERBASE\" && info.sessionId) {\n      await endBrowserbaseSession(\n        info.sessionId,\n        envConfig.apiKey,\n        envConfig.projectId,\n      );\n    }\n  }\n}\n\ntest.describe.parallel(\"keepAlive behavior\", () => {\n  const { testEnv, apiKey, projectId, hasBrowserbaseCreds } =\n    getKeepAliveEnvConfig();\n  const cases = buildKeepAliveCases(testEnv);\n\n  for (const testCase of cases) {\n    test(testCase.title, async () => {\n      if (testCase.requiresBrowserbase) {\n        test.skip(!hasBrowserbaseCreds, \"Browserbase credentials required\");\n      }\n\n      await runKeepAliveCase(testCase, { apiKey, projectId });\n    });\n  }\n});\n"
  },
  {
    "path": "packages/core/tests/integration/keyboard.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\nfunction dataUrl(html: string): string {\n  return \"data:text/html;charset=utf-8,\" + encodeURIComponent(html);\n}\n\ntest.describe(\"V3 keyboard shortcuts and typing\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"typing, select-all + delete clears input (Cmd maps cross-OS)\", async () => {\n    const html = `<!doctype html>\n      <input id=\"i1\" autofocus />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#i1\").click();\n    await page.type(\"Hello World\");\n\n    await page.keyPress(\"Cmd+A\");\n    await page.keyPress(\"Delete\");\n\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#i1\",\n    );\n    expect(value).toBe(\"\");\n  });\n\n  test(\"accelerator does not inject printable text (Cmd+B does not type 'b')\", async () => {\n    const html = `<!doctype html>\n      <input id=\"i\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#i\").click();\n    await page.type(\"xyz\");\n\n    await page.keyPress(\"Cmd+B\");\n\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#i\",\n    );\n    expect(value).toBe(\"xyz\");\n  });\n\n  test(\"Tab and Shift+Tab move focus\", async () => {\n    const html = `<!doctype html>\n      <input id=\"a\" />\n      <input id=\"b\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#a\").click();\n    await page.keyPress(\"Tab\");\n    const active1 = await page.evaluate(\n      () => (document.activeElement as HTMLElement)?.id || \"\",\n    );\n    expect(active1).toBe(\"b\");\n\n    await page.keyPress(\"Shift+Tab\");\n    const active2 = await page.evaluate(\n      () => (document.activeElement as HTMLElement)?.id || \"\",\n    );\n    expect(active2).toBe(\"a\");\n  });\n\n  test(\"cut clears the field (Cmd+X)\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.type(\"cut-me\");\n    await page.keyPress(\"Cmd+A\");\n    await page.keyPress(\"Cmd+X\");\n\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"\");\n  });\n\n  test(\"single printable via keyPress types characters (a, Shift+A, space)\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" autofocus />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.keyPress(\"a\");\n    await page.keyPress(\"Shift+A\");\n    await page.keyPress(\" \");\n\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"aA \");\n  });\n\n  test(\"Backspace removes last char\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.type(\"ab\");\n    await page.keyPress(\"Backspace\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"a\");\n  });\n\n  test(\"Delete removes next char at caret\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.type(\"abc\");\n    // place caret between a|bc\n    await page.evaluate(() => {\n      const el = document.getElementById(\"t\") as HTMLInputElement;\n      el.focus();\n      el.setSelectionRange(1, 1);\n    });\n    await page.keyPress(\"Delete\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"ac\");\n  });\n\n  test(\"ArrowLeft moves caret and typing inserts in middle\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.type(\"ac\");\n    await page.keyPress(\"ArrowLeft\");\n    await page.keyPress(\"b\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"abc\");\n  });\n\n  test(\"Enter inserts newline in textarea\", async () => {\n    const html = `<!doctype html>\n      <textarea id=\"ta\"></textarea>`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#ta\").click();\n    await page.keyPress(\"a\");\n    await page.keyPress(\"Enter\");\n    await page.keyPress(\"b\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLTextAreaElement)!.value,\n      \"#ta\",\n    );\n    expect(value).toBe(\"a\\nb\");\n  });\n\n  test(\"Insert key (no-op for value)\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.type(\"abc\");\n    await page.keyPress(\"Insert\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"abc\");\n  });\n\n  test(\"Enter submits form from text input\", async () => {\n    const html = `<!doctype html>\n      <form id=\"f\">\n        <input id=\"name\" />\n        <button id=\"submit\">Go</button>\n        <input id=\"submitted\" />\n      </form>\n      <script>\n        document.getElementById('f').addEventListener('submit', (e) => {\n          e.preventDefault();\n          document.getElementById('submitted').value = 'yes';\n        });\n      </script>`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#name\").click();\n    await page.type(\"foo\");\n    await page.keyPress(\"Enter\");\n\n    const submitted = await page.evaluate(\n      () =>\n        (document.getElementById(\"submitted\") as HTMLInputElement)?.value || \"\",\n    );\n    expect(submitted).toBe(\"yes\");\n  });\n\n  test(\"Enter in textarea does not submit form (inserts newline)\", async () => {\n    const html = `<!doctype html>\n      <form id=\"f\">\n        <textarea id=\"ta\"></textarea>\n        <button id=\"submit\">Go</button>\n        <input id=\"submitted\" />\n      </form>\n      <script>\n        document.getElementById('f').addEventListener('submit', (e) => {\n          e.preventDefault();\n          document.getElementById('submitted').value = 'yes';\n        });\n      </script>`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#ta\").click();\n    await page.keyPress(\"a\");\n    await page.keyPress(\"Enter\");\n    await page.keyPress(\"b\");\n\n    const submitted = await page.evaluate(\n      () =>\n        (document.getElementById(\"submitted\") as HTMLInputElement)?.value || \"\",\n    );\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLTextAreaElement)!.value,\n      \"#ta\",\n    );\n    expect(submitted).toBe(\"\");\n    expect(value).toBe(\"a\\nb\");\n  });\n\n  test('pressing \"+\" key types plus sign', async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    await page.keyPress(\"+\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"+\");\n  });\n\n  test(\"modifier state clears on keyPress error\", async () => {\n    const html = `<!doctype html>\n      <input id=\"t\" />`;\n    const page = await v3.context.awaitActivePage();\n    await page.goto(dataUrl(html), {\n      waitUntil: \"domcontentloaded\",\n      timeoutMs: 15000,\n    });\n\n    await page.locator(\"#t\").click();\n    // Try invalid key that might throw\n    try {\n      await page.keyPress(\"Cmd+InvalidKey123\");\n    } catch {\n      // Expected to fail\n    }\n\n    // Now try normal typing - should work if modifiers were cleared\n    await page.type(\"ok\");\n    const value = await page.evaluate(\n      (sel) => (document.querySelector(sel) as HTMLInputElement)!.value,\n      \"#t\",\n    );\n    expect(value).toBe(\"ok\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-backend-node-id.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Locator.backendNodeId() - CDP DOM node ID\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"returns a valid backend node ID for an element\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <button id=\"btn\">Click me</button>\n          </body></html>`,\n        ),\n    );\n\n    const locator = page.locator(\"button#btn\");\n    const nodeId = await locator.backendNodeId();\n\n    // Backend node ID should be a valid number\n    expect(typeof nodeId).toBe(\"number\");\n    expect(nodeId).toBeGreaterThan(0);\n  });\n\n  test(\"returns different node IDs for different elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"div1\">First</div>\n            <div id=\"div2\">Second</div>\n            <p id=\"p1\">Third</p>\n          </body></html>`,\n        ),\n    );\n\n    const nodeId1 = await page.locator(\"div#div1\").backendNodeId();\n    const nodeId2 = await page.locator(\"div#div2\").backendNodeId();\n    const nodeId3 = await page.locator(\"p#p1\").backendNodeId();\n\n    // All node IDs should be unique\n    expect(nodeId1).not.toBe(nodeId2);\n    expect(nodeId2).not.toBe(nodeId3);\n    expect(nodeId1).not.toBe(nodeId3);\n  });\n\n  test(\"returns consistent node ID for the same element\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input type=\"text\" id=\"input\" />\n          </body></html>`,\n        ),\n    );\n\n    const locator = page.locator(\"input#input\");\n\n    // Call multiple times on the same element\n    const nodeId1 = await locator.backendNodeId();\n    const nodeId2 = await locator.backendNodeId();\n\n    // Should return the same ID (same element)\n    expect(nodeId1).toBe(nodeId2);\n  });\n\n  test(\"returns node ID for nested elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"outer\">\n              <div id=\"middle\">\n                <span id=\"inner\">Deep</span>\n              </div>\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const outerNodeId = await page.locator(\"div#outer\").backendNodeId();\n    const middleNodeId = await page.locator(\"div#middle\").backendNodeId();\n    const innerNodeId = await page.locator(\"span#inner\").backendNodeId();\n\n    // All should be valid and unique\n    expect(outerNodeId).toBeGreaterThan(0);\n    expect(middleNodeId).toBeGreaterThan(0);\n    expect(innerNodeId).toBeGreaterThan(0);\n    expect(new Set([outerNodeId, middleNodeId, innerNodeId]).size).toBe(3);\n  });\n\n  test(\"returns node ID for elements with various attributes\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <button class=\"btn primary\" data-test=\"submit\" aria-label=\"Submit form\">Save</button>\n          </body></html>`,\n        ),\n    );\n\n    const locator = page.locator(\"button\");\n    const nodeId = await locator.backendNodeId();\n\n    // Should work with complex elements\n    expect(typeof nodeId).toBe(\"number\");\n    expect(nodeId).toBeGreaterThan(0);\n  });\n\n  test(\"returns node ID for form elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <form>\n              <input type=\"email\" id=\"email\" placeholder=\"Email\" />\n              <textarea id=\"message\"></textarea>\n              <select id=\"country\">\n                <option value=\"us\">USA</option>\n                <option value=\"ca\">Canada</option>\n              </select>\n              <button type=\"submit\">Submit</button>\n            </form>\n          </body></html>`,\n        ),\n    );\n\n    const emailNodeId = await page.locator(\"input#email\").backendNodeId();\n    const textareaNodeId = await page\n      .locator(\"textarea#message\")\n      .backendNodeId();\n    const selectNodeId = await page.locator(\"select#country\").backendNodeId();\n    const submitNodeId = await page\n      .locator(\"button[type='submit']\")\n      .backendNodeId();\n\n    // All form elements should have valid node IDs\n    expect(emailNodeId).toBeGreaterThan(0);\n    expect(textareaNodeId).toBeGreaterThan(0);\n    expect(selectNodeId).toBeGreaterThan(0);\n    expect(submitNodeId).toBeGreaterThan(0);\n\n    // All should be unique\n    const nodeIds = [emailNodeId, textareaNodeId, selectNodeId, submitNodeId];\n    expect(new Set(nodeIds).size).toBe(4);\n  });\n\n  test(\"returns node ID for dynamically created elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"container\"></div>\n            <script>\n              const container = document.getElementById('container');\n              const newBtn = document.createElement('button');\n              newBtn.id = 'dynamic-btn';\n              newBtn.textContent = 'Dynamically created';\n              container.appendChild(newBtn);\n            </script>\n          </body></html>`,\n        ),\n    );\n\n    const locator = page.locator(\"button#dynamic-btn\");\n    const nodeId = await locator.backendNodeId();\n\n    // Should work with dynamically created elements\n    expect(typeof nodeId).toBe(\"number\");\n    expect(nodeId).toBeGreaterThan(0);\n  });\n\n  test(\"returns node ID for elements with text selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <button>Submit Form</button>\n          </body></html>`,\n        ),\n    );\n\n    const locator = page.locator(\"text=Submit Form\");\n    const nodeId = await locator.backendNodeId();\n\n    // Should work with text-based selectors\n    expect(typeof nodeId).toBe(\"number\");\n    expect(nodeId).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-content-methods.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Locator content methods (textContent, innerHtml, innerText, inputValue)\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch((e) => {\n      void e;\n    });\n  });\n\n  test(\"Locator.textContent() returns raw text including hidden content\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"content\">\n              Hello\n              <span style=\"display:none\">Hidden</span>\n              World\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const content = await page.mainFrame().locator(\"#content\").textContent();\n    // textContent includes all text nodes, even hidden ones\n    expect(content).toContain(\"Hello\");\n    expect(content).toContain(\"Hidden\");\n    expect(content).toContain(\"World\");\n  });\n\n  test(\"Locator.innerText() returns visible text only\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"content\">\n              Visible\n              <span style=\"display:none\">Hidden</span>\n              Text\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const text = await page.mainFrame().locator(\"#content\").innerText();\n    // innerText is layout-aware and excludes hidden elements\n    expect(text).toContain(\"Visible\");\n    expect(text).toContain(\"Text\");\n    expect(text).not.toContain(\"Hidden\");\n  });\n\n  test(\"Locator.innerHtml() returns HTML markup\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"container\">\n              <p class=\"para\">Hello</p>\n              <strong>World</strong>\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const html = await page.mainFrame().locator(\"#container\").innerHtml();\n    expect(html).toContain('<p class=\"para\">Hello</p>');\n    expect(html).toContain(\"<strong>World</strong>\");\n  });\n\n  test(\"Locator.inputValue() reads value from input elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"text-input\" type=\"text\" value=\"hello world\" />\n            <textarea id=\"textarea\">multi\nline\ntext</textarea>\n            <input id=\"number-input\" type=\"number\" value=\"42\" />\n          </body></html>`,\n        ),\n    );\n\n    const textValue = await page\n      .mainFrame()\n      .locator(\"#text-input\")\n      .inputValue();\n    expect(textValue).toBe(\"hello world\");\n\n    const taValue = await page.mainFrame().locator(\"#textarea\").inputValue();\n    expect(taValue).toBe(\"multi\\nline\\ntext\");\n\n    const numValue = await page\n      .mainFrame()\n      .locator(\"#number-input\")\n      .inputValue();\n    expect(numValue).toBe(\"42\");\n  });\n\n  test(\"Locator.textContent() on empty elements returns empty string\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"empty\"></div>\n            <span id=\"whitespace\">   </span>\n          </body></html>`,\n        ),\n    );\n\n    const empty = await page.mainFrame().locator(\"#empty\").textContent();\n    expect(empty).toBe(\"\");\n\n    const whitespace = await page\n      .mainFrame()\n      .locator(\"#whitespace\")\n      .textContent();\n    expect(whitespace.trim()).toBe(\"\");\n  });\n\n  test(\"Locator.innerText() with nested elements and formatting\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"formatted\">\n              <p>Line 1</p>\n              <p>Line 2</p>\n              <ul>\n                <li>Item 1</li>\n                <li>Item 2</li>\n              </ul>\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const text = await page.mainFrame().locator(\"#formatted\").innerText();\n    expect(text).toContain(\"Line 1\");\n    expect(text).toContain(\"Line 2\");\n    expect(text).toContain(\"Item 1\");\n    expect(text).toContain(\"Item 2\");\n  });\n\n  test(\"Locator.inputValue() on contenteditable elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"editable\" contenteditable=\"true\">Editable content</div>\n          </body></html>`,\n        ),\n    );\n\n    const value = await page.mainFrame().locator(\"#editable\").inputValue();\n    expect(value).toBe(\"Editable content\");\n  });\n\n  test(\"Locator.innerHtml() preserves attributes and structure\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"complex\">\n              <a href=\"/link\" class=\"link-class\">Link</a>\n              <img src=\"image.png\" alt=\"test\" />\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const html = await page.mainFrame().locator(\"#complex\").innerHtml();\n    expect(html).toContain('href=\"/link\"');\n    expect(html).toContain('class=\"link-class\"');\n    expect(html).toContain('src=\"image.png\"');\n    expect(html).toContain('alt=\"test\"');\n  });\n\n  test(\"Locator.textContent() vs innerText() with script/style tags\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"mixed\">\n              Visible text\n              <script>console.log('script');</script>\n              <style>body { color: red; }</style>\n              More visible\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    const textContent = await page.mainFrame().locator(\"#mixed\").textContent();\n    // textContent includes script content\n    expect(textContent).toContain(\"Visible text\");\n    expect(textContent).toContain(\"More visible\");\n\n    const innerText = await page.mainFrame().locator(\"#mixed\").innerText();\n    // innerText excludes script/style\n    expect(innerText).toContain(\"Visible text\");\n    expect(innerText).toContain(\"More visible\");\n    expect(innerText).not.toContain(\"console.log\");\n  });\n\n  test(\"Locator.inputValue() returns empty string for non-input elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"div\">Not an input</div>\n            <input id=\"empty-input\" type=\"text\" value=\"\" />\n          </body></html>`,\n        ),\n    );\n\n    const divValue = await page.mainFrame().locator(\"#div\").inputValue();\n    expect(divValue).toBe(\"\");\n\n    const emptyInput = await page\n      .mainFrame()\n      .locator(\"#empty-input\")\n      .inputValue();\n    expect(emptyInput).toBe(\"\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-count-iframe.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"Locator count() method with iframes\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"count() does not search inside iframes by default\", async () => {\n    const page = v3.context.pages()[0];\n\n    // Create a page with buttons in main frame and iframe\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <button>Main Frame Button 1</button>\n        <button>Main Frame Button 2</button>\n        <iframe id=\"test-iframe\" srcdoc=\"\n          <button>Iframe Button 1</button>\n          <button>Iframe Button 2</button>\n          <button>Iframe Button 3</button>\n        \"></iframe>\n      `),\n    );\n\n    // Wait for iframe to load\n    await new Promise((resolve) => setTimeout(resolve, 500));\n\n    // Count buttons in main frame only\n    const mainFrameCount = await page.mainFrame().locator(\"button\").count();\n    expect(mainFrameCount).toBe(2); // Should only find buttons in main frame\n  });\n\n  test(\"count() works with frameLocator for iframe content\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <button>Main Frame Button</button>\n        <iframe id=\"test-iframe\" srcdoc=\"\n          <button>Iframe Button 1</button>\n          <button>Iframe Button 2</button>\n          <button>Iframe Button 3</button>\n        \"></iframe>\n      `),\n    );\n\n    // Wait for iframe to load\n    await new Promise((resolve) => setTimeout(resolve, 500));\n\n    // Count buttons in iframe using frameLocator\n    const iframeLocator = page.frameLocator(\"#test-iframe\");\n    const iframeCount = await iframeLocator.locator(\"button\").count();\n    expect(iframeCount).toBe(3); // Should find 3 buttons in iframe\n  });\n\n  test(\"count() with nested iframes\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <div class=\"level-0\">Main Frame</div>\n        <iframe id=\"frame1\" srcdoc=\"\n          <div class='level-1'>Frame 1</div>\n          <iframe id='frame2' srcdoc='\n            <div class=&quot;level-2&quot;>Frame 2</div>\n            <div class=&quot;level-2&quot;>Frame 2</div>\n          '></iframe>\n        \"></iframe>\n      `),\n    );\n\n    // Wait for all iframes to load\n    await new Promise((resolve) => setTimeout(resolve, 800));\n\n    // Count at each level\n    const mainCount = await page.mainFrame().locator(\".level-0\").count();\n    expect(mainCount).toBe(1);\n\n    const frame1Count = await page\n      .frameLocator(\"#frame1\")\n      .locator(\".level-1\")\n      .count();\n    expect(frame1Count).toBe(1);\n\n    const frame2Count = await page\n      .frameLocator(\"#frame1\")\n      .frameLocator(\"#frame2\")\n      .locator(\".level-2\")\n      .count();\n    expect(frame2Count).toBe(2);\n  });\n\n  test(\"count() with same selector in multiple contexts\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <span class=\"item\">Main 1</span>\n        <span class=\"item\">Main 2</span>\n        <iframe id=\"frame1\" srcdoc=\"\n          <span class='item'>Frame1 Item</span>\n        \"></iframe>\n        <iframe id=\"frame2\" srcdoc=\"\n          <span class='item'>Frame2 Item 1</span>\n          <span class='item'>Frame2 Item 2</span>\n          <span class='item'>Frame2 Item 3</span>\n        \"></iframe>\n      `),\n    );\n\n    // Wait for iframes to load\n    await new Promise((resolve) => setTimeout(resolve, 500));\n\n    // Count in each context\n    const mainCount = await page.mainFrame().locator(\".item\").count();\n    const frame1Count = await page\n      .frameLocator(\"#frame1\")\n      .locator(\".item\")\n      .count();\n    const frame2Count = await page\n      .frameLocator(\"#frame2\")\n      .locator(\".item\")\n      .count();\n\n    expect(mainCount).toBe(2); // Main frame items only\n    expect(frame1Count).toBe(1); // Frame 1 items only\n    expect(frame2Count).toBe(3); // Frame 2 items only\n  });\n\n  test(\"count() returns 0 for non-existent iframe\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\"data:text/html,<div>No iframes here</div>\");\n\n    try {\n      const frameLocator = page.frameLocator(\"#non-existent\");\n      await frameLocator.locator(\"button\").count();\n      // If we get here, the test should fail\n      expect(true).toBe(false);\n    } catch (error) {\n      // Expected behavior - frameLocator should throw when iframe doesn't exist\n      expect(error.message).toContain(\n        \"Could not find an element for the given xPath(s):\",\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-count.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"Locator count() method tests\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"count() returns correct number for CSS selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,<div class='test'>1</div><div class='test'>2</div><div class='test'>3</div><span>4</span>\",\n    );\n\n    const locator = page.mainFrame().locator(\".test\");\n    const count = await locator.count();\n\n    expect(count).toBe(3);\n  });\n\n  test(\"count() returns 0 for non-matching selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\"data:text/html,<div>Test</div>\");\n\n    const locator = page.mainFrame().locator(\".non-existent\");\n    const count = await locator.count();\n\n    expect(count).toBe(0);\n  });\n\n  test(\"count() works with XPath selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,<button>Button 1</button><button>Button 2</button><button>Button 3</button>\",\n    );\n\n    const locator = page.mainFrame().locator(\"//button\");\n    const count = await locator.count();\n\n    expect(count).toBe(3);\n  });\n\n  test(\"count() works with text selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,<div>Click me</div><button>Click me</button><span>Don't click me</span>\",\n    );\n\n    const locator = page.mainFrame().locator(\"text=Click me\");\n    const count = await locator.count();\n\n    // Case-insensitive substring match: also matches \"Don't click me\"\n    expect(count).toBe(3);\n  });\n\n  test(\"count() handles shadow DOM elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div id=\"host\"></div>' +\n            \"<script>\" +\n            'const host = document.getElementById(\"host\");' +\n            'const shadow = host.attachShadow({mode: \"open\"});' +\n            'shadow.innerHTML = \"<button>1</button><button>2</button>\";' +\n            \"</script>\",\n        ),\n      { waitUntil: \"load\", timeoutMs: 30000 },\n    );\n\n    // Wait a bit for shadow DOM to be attached\n    await new Promise((resolve) => setTimeout(resolve, 100));\n\n    const locator = page.mainFrame().locator(\"button\");\n    const count = await locator.count();\n\n    expect(count).toBe(2);\n  });\n\n  test(\"count() works with complex CSS selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,<div class='container'><span class='item'>1</span><span class='item'>2</span></div><div><span class='item'>3</span></div>\",\n    );\n\n    const locator = page.mainFrame().locator(\".container .item\");\n    const count = await locator.count();\n\n    expect(count).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-fill.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { StagehandLocatorError } from \"../../lib/v3/types/public/sdkErrors.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Locator.fill()\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch((e) => {\n      void e;\n    });\n  });\n\n  test(\"fills date inputs via value setter even when beforeinput blocks insertText\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"date\" type=\"date\" />\n            <script>\n              const input = document.getElementById('date');\n              input.addEventListener('beforeinput', (e) => {\n                if (e && e.inputType === 'insertText') e.preventDefault();\n              });\n            </script>\n          </body></html>`,\n        ),\n    );\n\n    const dateInput = page.mainFrame().locator(\"xpath=/html/body/input\");\n    await dateInput.fill(\"2026-01-01\");\n\n    const value = await dateInput.inputValue();\n    expect(value).toBe(\"2026-01-01\");\n  });\n\n  test(\"xpath case: throws StagehandLocatorError when fill encounters an exception\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"date\" type=\"date\" />\n          </body></html>`,\n        ),\n    );\n\n    await page.waitForSelector(\"xpath=/html/body/input\");\n\n    await page.evaluate(() => {\n      const input = document.querySelector(\"input\");\n      Object.defineProperty(input, \"isConnected\", {\n        get() {\n          throw new Error(\"boom\");\n        },\n      });\n    });\n\n    const dateInput = page.mainFrame().locator(\"xpath=/html/body/input\");\n    let error: unknown;\n    try {\n      await dateInput.fill(\"2026-01-01\");\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error).toBeInstanceOf(StagehandLocatorError);\n    if (error instanceof Error) {\n      // Log the message so it's visible in test output.\n      expect(error.message).toContain(\"Error Filling Element\");\n      expect(error.message).toContain(\"selector: xpath=/html/body/input\");\n      expect(error.message).toContain(\"boom\");\n    }\n  });\n\n  test(\"css selector case: throws StagehandLocatorError when fill encounters an exception\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"date\" type=\"date\" />\n          </body></html>`,\n        ),\n    );\n\n    await page.waitForSelector(\"#date\");\n\n    // Override in main world\n    await page.evaluate(() => {\n      const input = document.querySelector(\"input\");\n      Object.defineProperty(input, \"isConnected\", {\n        get() {\n          throw new Error(\"boom\");\n        },\n        configurable: true,\n      });\n    });\n\n    // Also override in the isolated world that CSS selectors use\n    const frameId = page.mainFrameId();\n    const { executionContextId } = await page.sendCDP<{\n      executionContextId: number;\n    }>(\"Page.createIsolatedWorld\", {\n      frameId,\n      worldName: \"v3-world\",\n    });\n\n    await page.sendCDP(\"Runtime.evaluate\", {\n      expression: `(() => {\n        const input = document.querySelector('input');\n        if (input) {\n          Object.defineProperty(input, 'isConnected', {\n            get() { throw new Error(\"boom\"); },\n            configurable: true\n          });\n        }\n      })()`,\n      contextId: executionContextId,\n    });\n\n    const dateInput = page.mainFrame().locator(\"#date\");\n    let error: unknown;\n    try {\n      await dateInput.fill(\"2026-01-01\");\n    } catch (err) {\n      error = err;\n    }\n\n    expect(error).toBeInstanceOf(StagehandLocatorError);\n    if (error instanceof Error) {\n      expect(error.message).toContain(\"Error Filling Element\");\n      expect(error.message).toContain(\"selector: #date\");\n      expect(error.message).toContain(\"boom\");\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-input-methods.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Locator input methods (fill, type, hover, isVisible, isChecked)\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch((e) => {\n      void e;\n    });\n  });\n\n  test(\"Locator.fill() sets input value directly\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"name\" type=\"text\" />\n            <div id=\"out\"></div>\n          </body></html>`,\n        ),\n    );\n\n    const input = page.mainFrame().locator(\"#name\");\n    await input.fill(\"Hello World\");\n\n    const value = await input.inputValue();\n    expect(value).toBe(\"Hello World\");\n  });\n\n  test(\"Locator.type() types text character by character\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"search\" type=\"text\" />\n          </body></html>`,\n        ),\n    );\n\n    const input = page.mainFrame().locator(\"#search\");\n    await input.type(\"test123\", { delay: 10 });\n\n    const value = await input.inputValue();\n    expect(value).toBe(\"test123\");\n  });\n\n  test(\"Locator.hover() moves mouse to element center\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <button id=\"btn\" onmouseover=\"this.dataset.hovered='true'\" onmouseout=\"this.dataset.hovered='false'\">Hover Me</button>\n          </body></html>`,\n        ),\n    );\n\n    const btn = page.mainFrame().locator(\"#btn\");\n    await btn.hover();\n\n    const hovered = await page.mainFrame().evaluate(() => {\n      const b = document.getElementById(\"btn\") as HTMLButtonElement | null;\n      return b?.dataset.hovered === \"true\";\n    });\n\n    expect(hovered).toBe(true);\n  });\n\n  test(\"Locator.isVisible() returns true for visible elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <div id=\"visible\">I am visible</div>\n            <div id=\"hidden\" style=\"display:none\">I am hidden</div>\n            <div id=\"invisible\" style=\"visibility:hidden\">I am invisible</div>\n            <div id=\"transparent\" style=\"opacity:0\">I am transparent</div>\n            <div id=\"zero-size\" style=\"width:0;height:0\">Zero size</div>\n          </body></html>`,\n        ),\n    );\n\n    const visible = await page.mainFrame().locator(\"#visible\").isVisible();\n    expect(visible).toBe(true);\n\n    const hidden = await page.mainFrame().locator(\"#hidden\").isVisible();\n    expect(hidden).toBe(false);\n\n    const invisible = await page.mainFrame().locator(\"#invisible\").isVisible();\n    expect(invisible).toBe(false);\n\n    const transparent = await page\n      .mainFrame()\n      .locator(\"#transparent\")\n      .isVisible();\n    expect(transparent).toBe(false);\n\n    const zeroSize = await page.mainFrame().locator(\"#zero-size\").isVisible();\n    expect(zeroSize).toBe(false);\n  });\n\n  test(\"Locator.isChecked() detects checkbox state\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"checked\" type=\"checkbox\" checked />\n            <input id=\"unchecked\" type=\"checkbox\" />\n            <input id=\"radio-selected\" type=\"radio\" name=\"opt\" checked />\n            <input id=\"radio-unselected\" type=\"radio\" name=\"opt\" />\n          </body></html>`,\n        ),\n    );\n\n    const checked = await page.mainFrame().locator(\"#checked\").isChecked();\n    expect(checked).toBe(true);\n\n    const unchecked = await page.mainFrame().locator(\"#unchecked\").isChecked();\n    expect(unchecked).toBe(false);\n\n    const radioSelected = await page\n      .mainFrame()\n      .locator(\"#radio-selected\")\n      .isChecked();\n    expect(radioSelected).toBe(true);\n\n    const radioUnselected = await page\n      .mainFrame()\n      .locator(\"#radio-unselected\")\n      .isChecked();\n    expect(radioUnselected).toBe(false);\n  });\n\n  test(\"Locator.fill() on textarea\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <textarea id=\"ta\"></textarea>\n          </body></html>`,\n        ),\n    );\n\n    const ta = page.mainFrame().locator(\"#ta\");\n    await ta.fill(\"Multi\\nline\\ntext\");\n\n    const value = await ta.inputValue();\n    expect(value).toBe(\"Multi\\nline\\ntext\");\n  });\n\n  test(\"Locator.fill() clears and sets new value\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <input id=\"inp\" type=\"text\" value=\"initial\" />\n          </body></html>`,\n        ),\n    );\n\n    const inp = page.mainFrame().locator(\"#inp\");\n\n    let value = await inp.inputValue();\n    expect(value).toBe(\"initial\");\n\n    await inp.fill(\"replaced\");\n    value = await inp.inputValue();\n    expect(value).toBe(\"replaced\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-nth.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"Locator nth() method tests\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"nth() returns correct element for CSS selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div class=\"test\" id=\"first\">1</div>' +\n            '<div class=\"test\" id=\"second\">2</div>' +\n            '<div class=\"test\" id=\"third\">3</div>' +\n            '<span id=\"other\">4</span>',\n        ),\n    );\n\n    // Test nth() with CSS selectors\n    const locator0 = page.mainFrame().locator(\".test\").nth(0);\n    const text0 = await locator0.textContent();\n    expect(text0).toBe(\"1\");\n\n    const locator1 = page.mainFrame().locator(\".test\").nth(1);\n    const text1 = await locator1.textContent();\n    expect(text1).toBe(\"2\");\n\n    const locator2 = page.mainFrame().locator(\".test\").nth(2);\n    const text2 = await locator2.textContent();\n    expect(text2).toBe(\"3\");\n  });\n\n  test(\"nth() returns correct element for XPath selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<button id=\"btn1\">Button 1</button>' +\n            '<button id=\"btn2\">Button 2</button>' +\n            '<button id=\"btn3\">Button 3</button>',\n        ),\n    );\n\n    // Test nth() with XPath selectors\n    const locator0 = page.mainFrame().locator(\"//button\").nth(0);\n    const text0 = await locator0.textContent();\n    expect(text0).toBe(\"Button 1\");\n\n    const locator1 = page.mainFrame().locator(\"//button\").nth(1);\n    const text1 = await locator1.textContent();\n    expect(text1).toBe(\"Button 2\");\n\n    const locator2 = page.mainFrame().locator(\"//button\").nth(2);\n    const text2 = await locator2.textContent();\n    expect(text2).toBe(\"Button 3\");\n  });\n\n  test(\"nth() returns correct element for text selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div id=\"d1\">Click me</div>' +\n            '<button id=\"b1\">Click me</button>' +\n            '<span id=\"s1\">Click me</span>',\n        ),\n    );\n\n    // Test nth() with text selectors\n    const locator0 = page.mainFrame().locator(\"text=Click me\").nth(0);\n    const text0 = await locator0.textContent();\n    expect(text0).toBe(\"Click me\");\n\n    const locator1 = page.mainFrame().locator(\"text=Click me\").nth(1);\n    const text1 = await locator1.textContent();\n    expect(text1).toBe(\"Click me\");\n\n    const locator2 = page.mainFrame().locator(\"text=Click me\").nth(2);\n    const text2 = await locator2.textContent();\n    expect(text2).toBe(\"Click me\");\n  });\n\n  test(\"nth() with shadow DOM\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div id=\"host\"></div>' +\n            \"<script>\" +\n            'const host = document.getElementById(\"host\");' +\n            'const shadow = host.attachShadow({mode: \"open\"});' +\n            'shadow.innerHTML = \"<button>Shadow Button 1</button><button>Shadow Button 2</button><button>Shadow Button 3</button>\";' +\n            \"</script>\",\n        ),\n      { waitUntil: \"load\", timeoutMs: 30000 },\n    );\n\n    // Wait a bit for shadow DOM to be attached\n    await new Promise((resolve) => setTimeout(resolve, 100));\n\n    // Test nth() with shadow DOM elements\n    const locator0 = page.mainFrame().locator(\"button\").nth(0);\n    const text0 = await locator0.textContent();\n    expect(text0).toBe(\"Shadow Button 1\");\n\n    const locator1 = page.mainFrame().locator(\"button\").nth(1);\n    const text1 = await locator1.textContent();\n    expect(text1).toBe(\"Shadow Button 2\");\n\n    const locator2 = page.mainFrame().locator(\"button\").nth(2);\n    const text2 = await locator2.textContent();\n    expect(text2).toBe(\"Shadow Button 3\");\n  });\n\n  test(\"nth() with out of bounds index throws error\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div class=\"test\">1</div>' + '<div class=\"test\">2</div>',\n        ),\n    );\n\n    // Test with out of bounds index - should throw an error\n    const locator = page.mainFrame().locator(\".test\").nth(5);\n    let error = null;\n    try {\n      await locator.textContent();\n    } catch (e) {\n      error = e;\n    }\n\n    expect(error).not.toBeNull();\n  });\n\n  test(\"nth() works with complex CSS selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div class=\"container\">' +\n            '<span class=\"item\">1</span>' +\n            '<span class=\"item\">2</span>' +\n            \"</div>\" +\n            \"<div>\" +\n            '<span class=\"item\">3</span>' +\n            \"</div>\",\n        ),\n    );\n\n    // Test nth() with complex CSS selectors\n    const locator0 = page.mainFrame().locator(\".container .item\").nth(0);\n    const text0 = await locator0.textContent();\n    expect(text0).toBe(\"1\");\n\n    const locator1 = page.mainFrame().locator(\".container .item\").nth(1);\n    const text1 = await locator1.textContent();\n    expect(text1).toBe(\"2\");\n  });\n\n  test(\"nth() can be chained with other locator methods\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div class=\"test\">First</div>' +\n            '<div class=\"test\">Second</div>' +\n            '<div class=\"test\">Third</div>',\n        ),\n    );\n\n    // Test that nth() returns a Locator that can be used for other actions\n    const locator = page.mainFrame().locator(\".test\").nth(1);\n    const text = await locator.textContent();\n    expect(text).toBe(\"Second\");\n\n    // Verify it's visible\n    const isVisible = await locator.isVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"nth(0) is equivalent to first()\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<div class=\"test\">First</div>' +\n            '<div class=\"test\">Second</div>' +\n            '<div class=\"test\">Third</div>',\n        ),\n    );\n\n    // Verify nth(0) returns the same element as first()\n    const nthLocator = page.mainFrame().locator(\".test\").nth(0);\n    const nthText = await nthLocator.textContent();\n\n    const firstLocator = page.mainFrame().locator(\".test\").first();\n    const firstText = await firstLocator.textContent();\n\n    expect(nthText).toBe(firstText);\n    expect(nthText).toBe(\"First\");\n  });\n\n  test(\"nth() works correctly with iframe selectors\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          '<button id=\"main1\">Main Button 1</button>' +\n            '<button id=\"main2\">Main Button 2</button>' +\n            '<iframe id=\"frame1\"></iframe>' +\n            \"<script>\" +\n            'const frame = document.getElementById(\"frame1\");' +\n            \"const doc = frame.contentDocument;\" +\n            \"doc.open();\" +\n            'doc.write(\"<button>Frame Button 1</button><button>Frame Button 2</button>\");' +\n            \"doc.close();\" +\n            \"</script>\",\n        ),\n    );\n\n    // Wait for iframe to load\n    await new Promise((resolve) => setTimeout(resolve, 100));\n\n    // Test that nth() works correctly with buttons in the main frame\n    const mainLocator0 = page.mainFrame().locator(\"button\").nth(0);\n    const mainText0 = await mainLocator0.textContent();\n    expect(mainText0).toBe(\"Main Button 1\");\n\n    const mainLocator1 = page.mainFrame().locator(\"button\").nth(1);\n    const mainText1 = await mainLocator1.textContent();\n    expect(mainText1).toBe(\"Main Button 2\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/locator-select-option.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Locator.selectOption() method\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch((e) => {\n      void e; // ignore cleanup errors\n    });\n  });\n\n  test(\"selectOption() selects single option by value\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"fruit\">\n              <option value=\"\">-- Choose --</option>\n              <option value=\"apple\">Apple</option>\n              <option value=\"banana\">Banana</option>\n              <option value=\"cherry\">Cherry</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#fruit\");\n    const selected = await select.selectOption(\"banana\");\n\n    expect(selected).toEqual([\"banana\"]);\n\n    const value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"fruit\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"banana\");\n  });\n\n  test(\"selectOption() selects option by label/text\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"country\">\n              <option value=\"us\">United States</option>\n              <option value=\"uk\">United Kingdom</option>\n              <option value=\"ca\">Canada</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#country\");\n    const selected = await select.selectOption(\"United Kingdom\");\n\n    expect(selected).toEqual([\"uk\"]);\n  });\n\n  test(\"selectOption() selects multiple options in multiple select\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"colors\" multiple>\n              <option value=\"red\">Red</option>\n              <option value=\"green\">Green</option>\n              <option value=\"blue\">Blue</option>\n              <option value=\"yellow\">Yellow</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#colors\");\n    const selected = await select.selectOption([\"red\", \"blue\"]);\n\n    expect(selected.sort()).toEqual([\"blue\", \"red\"]);\n\n    const values = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"colors\") as HTMLSelectElement | null;\n      return Array.from(s?.selectedOptions ?? []).map((o) => o.value);\n    });\n    expect(values.sort()).toEqual([\"blue\", \"red\"]);\n  });\n\n  test(\"selectOption() deselects previous option on single select\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"size\">\n              <option value=\"s\">Small</option>\n              <option value=\"m\" selected>Medium</option>\n              <option value=\"l\">Large</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#size\");\n\n    let value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"size\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"m\");\n\n    await select.selectOption(\"l\");\n\n    value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"size\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"l\");\n  });\n\n  test(\"selectOption() triggers change event\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"opt\">\n              <option value=\"a\">Option A</option>\n              <option value=\"b\">Option B</option>\n            </select>\n            <div id=\"out\"></div>\n            <script>\n              const select = document.getElementById('opt');\n              const out = document.getElementById('out');\n              select.addEventListener('change', () => {\n                out.textContent = 'changed-' + select.value;\n              });\n            </script>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#opt\");\n    await select.selectOption(\"b\");\n\n    const output = await page.mainFrame().evaluate(() => {\n      const out = document.getElementById(\"out\");\n      return out?.textContent;\n    });\n    expect(output).toBe(\"changed-b\");\n  });\n\n  test(\"selectOption() with optgroup structure\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"grouped\">\n              <optgroup label=\"Fruits\">\n                <option value=\"apple\">Apple</option>\n                <option value=\"orange\">Orange</option>\n              </optgroup>\n              <optgroup label=\"Vegetables\">\n                <option value=\"carrot\">Carrot</option>\n                <option value=\"celery\">Celery</option>\n              </optgroup>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#grouped\");\n    await select.selectOption(\"celery\");\n\n    const value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"grouped\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"celery\");\n  });\n\n  test(\"selectOption() returns array of selected values\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"multi\" multiple>\n              <option value=\"1\">One</option>\n              <option value=\"2\">Two</option>\n              <option value=\"3\">Three</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#multi\");\n    const selected = await select.selectOption([\"1\", \"3\"]);\n\n    expect(selected).toContain(\"1\");\n    expect(selected).toContain(\"3\");\n    expect(selected.length).toBe(2);\n  });\n\n  test(\"selectOption() with empty string value\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"opt\">\n              <option value=\"\">None</option>\n              <option value=\"yes\">Yes</option>\n              <option value=\"no\">No</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#opt\");\n    const selected = await select.selectOption(\"\");\n\n    expect(selected).toEqual([\"\"]);\n\n    const value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"opt\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"\");\n  });\n\n  test(\"selectOption() with numeric values\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"nums\">\n              <option value=\"1\">One</option>\n              <option value=\"2\">Two</option>\n              <option value=\"10\">Ten</option>\n              <option value=\"100\">Hundred</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#nums\");\n    await select.selectOption(\"10\");\n\n    const value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"nums\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"10\");\n  });\n\n  test(\"selectOption() with disabled option\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body>\n            <select id=\"mixed\">\n              <option value=\"a\">Available</option>\n              <option value=\"b\" disabled>Unavailable</option>\n              <option value=\"c\">Available</option>\n            </select>\n          </body></html>`,\n        ),\n    );\n\n    const select = page.mainFrame().locator(\"#mixed\");\n    // Should still select disabled option if explicitly requested\n    await select.selectOption(\"b\");\n\n    const value = await page.mainFrame().evaluate(() => {\n      const s = document.getElementById(\"mixed\") as HTMLSelectElement | null;\n      return s?.value;\n    });\n    expect(value).toBe(\"b\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/logger-initialization.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport {\n  bindInstanceLogger,\n  unbindInstanceLogger,\n  withInstanceLogContext,\n  v3Logger,\n} from \"../../lib/v3/logger.js\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\n\ntest.describe(\"V3 Logger Instance Routing\", () => {\n  test.afterEach(() => {\n    // Clean up is handled by unbindInstanceLogger calls in tests\n  });\n\n  test(\"bindInstanceLogger routes logs to correct instance\", () => {\n    const instanceId = \"test-instance-001\";\n    const capturedLogs: LogLine[] = [];\n\n    bindInstanceLogger(instanceId, (line) => {\n      capturedLogs.push(line);\n    });\n\n    try {\n      // Log within context\n      withInstanceLogContext(instanceId, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"Test message for instance\",\n          level: 1,\n        });\n      });\n\n      // Should have captured the log\n      expect(capturedLogs.length).toBe(1);\n      expect(capturedLogs[0].message).toBe(\"Test message for instance\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"unbindInstanceLogger stops routing\", () => {\n    const instanceId = \"test-instance-002\";\n    const capturedLogs: LogLine[] = [];\n    const consoleOutput: string[] = [];\n    const originalConsoleLog = console.log;\n\n    try {\n      console.log = (msg: string) => {\n        consoleOutput.push(msg);\n      };\n\n      bindInstanceLogger(instanceId, (line) => {\n        capturedLogs.push(line);\n      });\n\n      // Unbind immediately\n      unbindInstanceLogger(instanceId);\n\n      // Log - should fall back to console\n      withInstanceLogContext(instanceId, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"After unbind\",\n          level: 1,\n        });\n      });\n\n      // Should not have captured via instance logger\n      expect(capturedLogs.length).toBe(0);\n      // But should have logged to console\n      expect(consoleOutput.length).toBeGreaterThan(0);\n    } finally {\n      console.log = originalConsoleLog;\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"multiple instances have isolated log routing\", () => {\n    const instance1Id = \"test-instance-1\";\n    const instance2Id = \"test-instance-2\";\n    const instance1Logs: LogLine[] = [];\n    const instance2Logs: LogLine[] = [];\n\n    bindInstanceLogger(instance1Id, (line) => instance1Logs.push(line));\n    bindInstanceLogger(instance2Id, (line) => instance2Logs.push(line));\n\n    try {\n      // Log from instance 1\n      withInstanceLogContext(instance1Id, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"From instance 1\",\n          level: 1,\n        });\n      });\n\n      // Log from instance 2\n      withInstanceLogContext(instance2Id, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"From instance 2\",\n          level: 1,\n        });\n      });\n\n      // Each instance should have only its own log\n      expect(instance1Logs.length).toBe(1);\n      expect(instance2Logs.length).toBe(1);\n      expect(instance1Logs[0].message).toBe(\"From instance 1\");\n      expect(instance2Logs[0].message).toBe(\"From instance 2\");\n    } finally {\n      unbindInstanceLogger(instance1Id);\n      unbindInstanceLogger(instance2Id);\n    }\n  });\n\n  test(\"v3Logger falls back to console when no instance context\", () => {\n    const capturedLogs: string[] = [];\n    const originalConsoleLog = console.log;\n\n    try {\n      console.log = (msg: string) => {\n        capturedLogs.push(msg);\n      };\n\n      // Log without any instance context\n      v3Logger({\n        category: \"test\",\n        message: \"Console fallback log\",\n        level: 1,\n      });\n\n      // Should have used console logger\n      expect(capturedLogs.length).toBeGreaterThan(0);\n      const logOutput = capturedLogs.join(\"\\n\");\n      expect(logOutput).toContain(\"Console fallback log\");\n    } finally {\n      console.log = originalConsoleLog;\n    }\n  });\n\n  test(\"v3Logger falls back to console when instance logger throws\", () => {\n    const instanceId = \"failing-instance\";\n    const capturedConsoleLogs: string[] = [];\n    const originalConsoleLog = console.log;\n\n    try {\n      console.log = (msg: string) => {\n        capturedConsoleLogs.push(msg);\n      };\n\n      // Bind a logger that throws\n      bindInstanceLogger(instanceId, () => {\n        throw new Error(\"Instance logger failed\");\n      });\n\n      // Should fall back to console without throwing\n      withInstanceLogContext(instanceId, () => {\n        expect(() => {\n          v3Logger({\n            category: \"test\",\n            message: \"Test with failing instance logger\",\n            level: 1,\n          });\n        }).not.toThrow();\n      });\n\n      // Console should have received the log as fallback\n      expect(capturedConsoleLogs.length).toBeGreaterThan(0);\n      const logOutput = capturedConsoleLogs.join(\"\\n\");\n      expect(logOutput).toContain(\"Test with failing instance logger\");\n    } finally {\n      console.log = originalConsoleLog;\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"withInstanceLogContext nests properly\", () => {\n    const outerInstanceId = \"outer-instance\";\n    const innerInstanceId = \"inner-instance\";\n    const outerLogs: LogLine[] = [];\n    const innerLogs: LogLine[] = [];\n\n    bindInstanceLogger(outerInstanceId, (line) => outerLogs.push(line));\n    bindInstanceLogger(innerInstanceId, (line) => innerLogs.push(line));\n\n    try {\n      withInstanceLogContext(outerInstanceId, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"Outer context\",\n          level: 1,\n        });\n\n        withInstanceLogContext(innerInstanceId, () => {\n          v3Logger({\n            category: \"test\",\n            message: \"Inner context\",\n            level: 1,\n          });\n        });\n\n        v3Logger({\n          category: \"test\",\n          message: \"Back to outer context\",\n          level: 1,\n        });\n      });\n\n      // Outer instance should have 2 logs\n      expect(outerLogs.length).toBe(2);\n      expect(outerLogs[0].message).toBe(\"Outer context\");\n      expect(outerLogs[1].message).toBe(\"Back to outer context\");\n\n      // Inner instance should have 1 log\n      expect(innerLogs.length).toBe(1);\n      expect(innerLogs[0].message).toBe(\"Inner context\");\n    } finally {\n      unbindInstanceLogger(outerInstanceId);\n      unbindInstanceLogger(innerInstanceId);\n    }\n  });\n\n  test(\"withInstanceLogContext returns function result\", () => {\n    const instanceId = \"return-test-instance\";\n    bindInstanceLogger(instanceId, () => {});\n\n    try {\n      const result = withInstanceLogContext(instanceId, () => {\n        return { success: true, value: 42 };\n      });\n\n      expect(result).toEqual({ success: true, value: 42 });\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"withInstanceLogContext works with async functions\", async () => {\n    const instanceId = \"async-test-instance\";\n    const capturedLogs: LogLine[] = [];\n\n    bindInstanceLogger(instanceId, (line) => capturedLogs.push(line));\n\n    try {\n      const asyncResult = await withInstanceLogContext(instanceId, async () => {\n        v3Logger({\n          category: \"test\",\n          message: \"Log from async context\",\n          level: 1,\n        });\n\n        await new Promise((resolve) => setTimeout(resolve, 10));\n\n        v3Logger({\n          category: \"test\",\n          message: \"Log after await\",\n          level: 1,\n        });\n\n        return \"async result\";\n      });\n\n      expect(asyncResult).toBe(\"async result\");\n      expect(capturedLogs.length).toBe(2);\n      expect(capturedLogs[0].message).toBe(\"Log from async context\");\n      expect(capturedLogs[1].message).toBe(\"Log after await\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"console fallback formats different log levels correctly\", () => {\n    const consoleOutput: { level: string; msg: string }[] = [];\n    const originalConsoleLog = console.log;\n    const originalConsoleError = console.error;\n    const originalConsoleDebug = console.debug;\n\n    try {\n      console.log = (msg: string) => {\n        consoleOutput.push({ level: \"log\", msg });\n      };\n      console.error = (msg: string) => {\n        consoleOutput.push({ level: \"error\", msg });\n      };\n      console.debug = (msg: string) => {\n        consoleOutput.push({ level: \"debug\", msg });\n      };\n\n      // Test error level (0)\n      v3Logger({\n        category: \"test\",\n        message: \"Error message\",\n        level: 0,\n      });\n\n      // Test info level (1)\n      v3Logger({\n        category: \"test\",\n        message: \"Info message\",\n        level: 1,\n      });\n\n      // Test debug level (2)\n      v3Logger({\n        category: \"test\",\n        message: \"Debug message\",\n        level: 2,\n      });\n\n      expect(consoleOutput.length).toBe(3);\n      expect(consoleOutput[0].level).toBe(\"error\");\n      expect(consoleOutput[0].msg).toContain(\"ERROR\");\n      expect(consoleOutput[0].msg).toContain(\"Error message\");\n\n      expect(consoleOutput[1].level).toBe(\"log\");\n      expect(consoleOutput[1].msg).toContain(\"INFO\");\n      expect(consoleOutput[1].msg).toContain(\"Info message\");\n\n      expect(consoleOutput[2].level).toBe(\"debug\");\n      expect(consoleOutput[2].msg).toContain(\"DEBUG\");\n      expect(consoleOutput[2].msg).toContain(\"Debug message\");\n    } finally {\n      console.log = originalConsoleLog;\n      console.error = originalConsoleError;\n      console.debug = originalConsoleDebug;\n    }\n  });\n\n  test(\"console fallback formats auxiliary data\", () => {\n    const consoleOutput: string[] = [];\n    const originalConsoleLog = console.log;\n\n    try {\n      console.log = (msg: string) => {\n        consoleOutput.push(msg);\n      };\n\n      v3Logger({\n        category: \"test\",\n        message: \"Message with auxiliary\",\n        level: 1,\n        auxiliary: {\n          stringValue: { value: \"test\", type: \"string\" },\n          integerValue: { value: \"42\", type: \"integer\" },\n          objectValue: {\n            value: JSON.stringify({ nested: \"data\" }),\n            type: \"object\",\n          },\n        },\n      });\n\n      expect(consoleOutput.length).toBe(1);\n      const output = consoleOutput[0];\n      expect(output).toContain(\"Message with auxiliary\");\n      expect(output).toContain(\"stringValue\");\n      expect(output).toContain(\"integerValue\");\n      expect(output).toContain(\"objectValue\");\n    } finally {\n      console.log = originalConsoleLog;\n    }\n  });\n\n  test(\"concurrent instances don't interfere\", () => {\n    const instances = Array.from({ length: 10 }, (_, i) => `instance-${i}`);\n    const logsByInstance = new Map<string, LogLine[]>();\n\n    // Bind all instances\n    instances.forEach((id) => {\n      const logs: LogLine[] = [];\n      logsByInstance.set(id, logs);\n      bindInstanceLogger(id, (line) => logs.push(line));\n    });\n\n    try {\n      // Log from each instance\n      instances.forEach((id, index) => {\n        withInstanceLogContext(id, () => {\n          v3Logger({\n            category: \"test\",\n            message: `Message from ${id}`,\n            level: 1,\n            auxiliary: {\n              index: { value: String(index), type: \"integer\" },\n            },\n          });\n        });\n      });\n\n      // Verify each instance received only its own log\n      instances.forEach((id) => {\n        const logs = logsByInstance.get(id)!;\n        expect(logs.length).toBe(1);\n        expect(logs[0].message).toBe(`Message from ${id}`);\n      });\n    } finally {\n      instances.forEach((id) => unbindInstanceLogger(id));\n    }\n  });\n});\n\ntest.describe(\"V3 Logger with External Logger (Production Pattern)\", () => {\n  test.afterEach(() => {\n    // Clean up instance loggers\n  });\n\n  test(\"external logger receives all logs from v3Logger\", () => {\n    const instanceId = \"v3-instance-with-external\";\n    const externalLogs: LogLine[] = [];\n\n    // Simulate V3 constructor behavior with external logger\n    const externalLogger = (line: LogLine) => {\n      externalLogs.push(line);\n    };\n\n    bindInstanceLogger(instanceId, externalLogger);\n\n    try {\n      withInstanceLogContext(instanceId, () => {\n        v3Logger({\n          category: \"a11y/snapshot\",\n          message: \"Capturing hybrid snapshot\",\n          level: 0,\n        });\n\n        v3Logger({\n          category: \"handlers/act\",\n          message: \"Executing action\",\n          level: 1,\n          auxiliary: {\n            action: { value: \"click\", type: \"string\" },\n          },\n        });\n\n        v3Logger({\n          category: \"debug\",\n          message: \"Debug details\",\n          level: 2,\n        });\n      });\n\n      // All logs should be captured by external logger\n      expect(externalLogs.length).toBe(3);\n      expect(externalLogs[0].message).toBe(\"Capturing hybrid snapshot\");\n      expect(externalLogs[1].message).toBe(\"Executing action\");\n      expect(externalLogs[2].message).toBe(\"Debug details\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"StagehandLogger wrapper forwards to external logger\", () => {\n    const instanceId = \"v3-with-stagehand-wrapper\";\n    const externalLogs: LogLine[] = [];\n\n    // Simulate V3's stagehandLogger.log() wrapping pattern\n    const mockStagehandLogger = {\n      log: (line: LogLine) => {\n        // This simulates StagehandLogger.log() which internally calls externalLogger\n        externalLogs.push(line);\n      },\n    };\n\n    bindInstanceLogger(instanceId, (line) => mockStagehandLogger.log(line));\n\n    try {\n      withInstanceLogContext(instanceId, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"Log through StagehandLogger wrapper\",\n          level: 1,\n        });\n      });\n\n      expect(externalLogs.length).toBe(1);\n      expect(externalLogs[0].message).toBe(\n        \"Log through StagehandLogger wrapper\",\n      );\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"multiple V3 instances with different external loggers\", () => {\n    const instance1Id = \"v3-instance-1\";\n    const instance2Id = \"v3-instance-2\";\n    const external1Logs: LogLine[] = [];\n    const external2Logs: LogLine[] = [];\n\n    // Simulate two V3 instances with different external loggers\n    bindInstanceLogger(instance1Id, (line) => external1Logs.push(line));\n    bindInstanceLogger(instance2Id, (line) => external2Logs.push(line));\n\n    try {\n      // Instance 1 logs\n      withInstanceLogContext(instance1Id, () => {\n        v3Logger({\n          category: \"instance1\",\n          message: \"Instance 1 activity\",\n          level: 1,\n        });\n      });\n\n      // Instance 2 logs\n      withInstanceLogContext(instance2Id, () => {\n        v3Logger({\n          category: \"instance2\",\n          message: \"Instance 2 activity\",\n          level: 1,\n        });\n      });\n\n      // Each external logger should only have its instance's logs\n      expect(external1Logs.length).toBe(1);\n      expect(external2Logs.length).toBe(1);\n      expect(external1Logs[0].message).toBe(\"Instance 1 activity\");\n      expect(external2Logs[0].message).toBe(\"Instance 2 activity\");\n    } finally {\n      unbindInstanceLogger(instance1Id);\n      unbindInstanceLogger(instance2Id);\n    }\n  });\n\n  test(\"external logger receives logs with auxiliary data preserved\", () => {\n    const instanceId = \"v3-with-auxiliary\";\n    const externalLogs: LogLine[] = [];\n\n    bindInstanceLogger(instanceId, (line) => externalLogs.push(line));\n\n    try {\n      withInstanceLogContext(instanceId, () => {\n        v3Logger({\n          category: \"extract\",\n          message: \"Extracting data\",\n          level: 1,\n          auxiliary: {\n            selector: { value: \"xpath=/html/body\", type: \"string\" },\n            timeout: { value: \"5000\", type: \"integer\" },\n            retries: { value: \"3\", type: \"integer\" },\n            metadata: {\n              value: JSON.stringify({ key: \"value\" }),\n              type: \"object\",\n            },\n          },\n        });\n      });\n\n      expect(externalLogs.length).toBe(1);\n      const log = externalLogs[0];\n      expect(log.auxiliary).toBeDefined();\n      expect(log.auxiliary?.selector?.value).toBe(\"xpath=/html/body\");\n      expect(log.auxiliary?.timeout?.value).toBe(\"5000\");\n      expect(log.auxiliary?.retries?.value).toBe(\"3\");\n      expect(log.auxiliary?.metadata?.type).toBe(\"object\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"external logger handles rapid concurrent logs\", () => {\n    const instanceId = \"v3-rapid-logs\";\n    const externalLogs: LogLine[] = [];\n\n    bindInstanceLogger(instanceId, (line) => externalLogs.push(line));\n\n    try {\n      withInstanceLogContext(instanceId, () => {\n        // Simulate rapid logging like during snapshot capture\n        for (let i = 0; i < 50; i++) {\n          v3Logger({\n            category: \"perf\",\n            message: `Operation ${i}`,\n            level: 2,\n            auxiliary: {\n              iteration: { value: String(i), type: \"integer\" },\n            },\n          });\n        }\n      });\n\n      // All logs should be captured\n      expect(externalLogs.length).toBe(50);\n      expect(externalLogs[0].message).toBe(\"Operation 0\");\n      expect(externalLogs[49].message).toBe(\"Operation 49\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"external logger can filter by log level\", () => {\n    const instanceId = \"v3-with-filtering\";\n    const errorLogs: LogLine[] = [];\n\n    // External logger that only captures errors\n    const filteringLogger = (line: LogLine) => {\n      if (line.level === 0) {\n        errorLogs.push(line);\n      }\n    };\n\n    bindInstanceLogger(instanceId, filteringLogger);\n\n    try {\n      withInstanceLogContext(instanceId, () => {\n        v3Logger({\n          category: \"test\",\n          message: \"Info message\",\n          level: 1,\n        });\n\n        v3Logger({\n          category: \"test\",\n          message: \"Error message\",\n          level: 0,\n        });\n\n        v3Logger({\n          category: \"test\",\n          message: \"Debug message\",\n          level: 2,\n        });\n\n        v3Logger({\n          category: \"test\",\n          message: \"Another error\",\n          level: 0,\n        });\n      });\n\n      // Only error logs should be captured\n      expect(errorLogs.length).toBe(2);\n      expect(errorLogs[0].message).toBe(\"Error message\");\n      expect(errorLogs[1].message).toBe(\"Another error\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n\n  test(\"external logger persists across async operations\", async () => {\n    const instanceId = \"v3-async-ops\";\n    const externalLogs: LogLine[] = [];\n\n    bindInstanceLogger(instanceId, (line) => externalLogs.push(line));\n\n    try {\n      await withInstanceLogContext(instanceId, async () => {\n        v3Logger({\n          category: \"async\",\n          message: \"Before async operation\",\n          level: 1,\n        });\n\n        await new Promise((resolve) => setTimeout(resolve, 50));\n\n        v3Logger({\n          category: \"async\",\n          message: \"After async operation\",\n          level: 1,\n        });\n\n        await Promise.all([\n          Promise.resolve().then(() =>\n            v3Logger({\n              category: \"async\",\n              message: \"Parallel operation 1\",\n              level: 1,\n            }),\n          ),\n          Promise.resolve().then(() =>\n            v3Logger({\n              category: \"async\",\n              message: \"Parallel operation 2\",\n              level: 1,\n            }),\n          ),\n        ]);\n      });\n\n      // All logs should be captured despite async boundaries\n      expect(externalLogs.length).toBe(4);\n      expect(externalLogs[0].message).toBe(\"Before async operation\");\n      expect(externalLogs[1].message).toBe(\"After async operation\");\n    } finally {\n      unbindInstanceLogger(instanceId);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/multi-instance-logger.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { getV3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"V3 Multi-Instance Logger Isolation\", () => {\n  // Run tests serially to avoid resource exhaustion from creating many Chrome instances\n  test.describe.configure({ mode: \"serial\" });\n  // Increase timeout for stress tests that create/destroy multiple instances\n  test.setTimeout(120_000);\n\n  test(\"multiple V3 instances can be created concurrently without logger conflicts\", async () => {\n    const instanceCount = 5;\n    const instances: V3[] = [];\n    const instanceLogs: Map<number, LogLine[]> = new Map();\n\n    try {\n      // Create multiple instances with individual loggers\n      const creationPromises = Array.from({ length: instanceCount }, (_, i) => {\n        const logs: LogLine[] = [];\n        instanceLogs.set(i, logs);\n\n        const config = getV3DynamicTestConfig({\n          verbose: 2,\n          disablePino: true,\n          logger: (line: LogLine) => {\n            logs.push({\n              ...line,\n              auxiliary: {\n                ...line.auxiliary,\n                index: { value: String(i), type: \"integer\" },\n              },\n            });\n          },\n        });\n\n        const v3 = new V3(config);\n        instances.push(v3);\n        return v3.init();\n      });\n\n      // All instances should initialize successfully\n      await Promise.all(creationPromises);\n\n      // Each instance should be initialized\n      expect(instances.length).toBe(instanceCount);\n      for (const instance of instances) {\n        expect(instance.context).toBeDefined();\n      }\n\n      // Perform operations that generate logs\n      await Promise.all(\n        instances.map(async (instance) => {\n          const page = await instance.context.awaitActivePage();\n          await page.goto(\"about:blank\");\n        }),\n      );\n\n      // Each instance should have logged to its own logger\n      for (let i = 0; i < instanceCount; i++) {\n        const logs = instanceLogs.get(i)!;\n        // Each instance should have some logs\n        expect(logs.length).toBeGreaterThan(0);\n\n        // Logs should not contain data from other instances\n        // (though this is harder to verify without more specific markers)\n        const hasOwnLogs = logs.some(\n          (log) =>\n            log.auxiliary?.index?.value === String(i) ||\n            log.category === \"init\",\n        );\n        expect(hasOwnLogs).toBe(true);\n      }\n    } finally {\n      // Clean up all instances\n      await Promise.all(instances.map((instance) => closeV3(instance)));\n    }\n  });\n\n  test(\"V3 instances with external loggers don't leak logs to each other\", async () => {\n    const instance1Logs: LogLine[] = [];\n    const instance2Logs: LogLine[] = [];\n\n    const v3Instance1 = new V3(\n      getV3DynamicTestConfig({\n        verbose: 2,\n        disablePino: true,\n        logger: (line: LogLine) => instance1Logs.push(line),\n      }),\n    );\n\n    const v3Instance2 = new V3(\n      getV3DynamicTestConfig({\n        verbose: 2,\n        disablePino: true,\n        logger: (line: LogLine) => instance2Logs.push(line),\n      }),\n    );\n\n    try {\n      // Initialize both instances\n      await Promise.all([v3Instance1.init(), v3Instance2.init()]);\n\n      // Perform operations on each instance\n      const page1 = await v3Instance1.context.awaitActivePage();\n      await page1.goto(\"about:blank\");\n\n      const page2 = await v3Instance2.context.awaitActivePage();\n      await page2.goto(\"data:text/html,<h1>Instance 2</h1>\");\n\n      // Both instances should have logs\n      expect(instance1Logs.length).toBeGreaterThan(0);\n      expect(instance2Logs.length).toBeGreaterThan(0);\n\n      // Logs should be distinct (no exact duplicates)\n      // This is a weak check, but verifies basic isolation\n      const instance1Messages = new Set(instance1Logs.map((l) => l.message));\n      const instance2Messages = new Set(instance2Logs.map((l) => l.message));\n\n      // At least some messages should be unique to each instance\n      // (This might not always be true for very generic messages like \"init\",\n      // but serves as a smoke test)\n      const allMessages = new Set([...instance1Messages, ...instance2Messages]);\n      expect(allMessages.size).toBeGreaterThanOrEqual(\n        Math.max(instance1Messages.size, instance2Messages.size),\n      );\n    } finally {\n      await Promise.all([closeV3(v3Instance1), closeV3(v3Instance2)]);\n    }\n  });\n\n  test(\"V3 instances without external loggers use shared global logger\", async () => {\n    // Create instances without external loggers\n    const v3Instance1 = new V3(\n      getV3DynamicTestConfig({\n        verbose: 1,\n        disablePino: true,\n      }),\n    );\n\n    const v3Instance2 = new V3(\n      getV3DynamicTestConfig({\n        verbose: 1,\n        disablePino: true,\n      }),\n    );\n\n    try {\n      // Initialize both instances concurrently\n      await Promise.all([v3Instance1.init(), v3Instance2.init()]);\n\n      // Both should work fine\n      expect(v3Instance1.context).toBeDefined();\n      expect(v3Instance2.context).toBeDefined();\n\n      // Perform basic operations to ensure logging doesn't cause issues\n      const page1 = await v3Instance1.context.awaitActivePage();\n      const page2 = await v3Instance2.context.awaitActivePage();\n\n      await Promise.all([page1.goto(\"about:blank\"), page2.goto(\"about:blank\")]);\n\n      // Both should still be operational\n      expect(page1.url()).toContain(\"about:blank\");\n      expect(page2.url()).toContain(\"about:blank\");\n    } finally {\n      await Promise.all([closeV3(v3Instance1), closeV3(v3Instance2)]);\n    }\n  });\n\n  test(\"rapidly creating and destroying instances doesn't cause logger issues\", async () => {\n    const iterations = 5;\n    const results: boolean[] = [];\n\n    for (let i = 0; i < iterations; i++) {\n      const logs: LogLine[] = [];\n      const v3 = new V3(\n        getV3DynamicTestConfig({\n          verbose: 1, // Capture INFO logs for verification\n          disablePino: true,\n          logger: (line: LogLine) => logs.push(line),\n        }),\n      );\n\n      try {\n        await v3.init();\n        const page = await v3.context.awaitActivePage();\n        await page.goto(\"about:blank\");\n        results.push(true);\n\n        // Verify some logs were captured\n        expect(logs.length).toBeGreaterThan(0);\n      } finally {\n        await closeV3(v3);\n      }\n    }\n\n    // All iterations should succeed\n    expect(results.length).toBe(iterations);\n    expect(results.every((r) => r === true)).toBe(true);\n  });\n\n  test(\"concurrent instance creation with mixed logger configurations\", async () => {\n    const instances: V3[] = [];\n    const configs = [\n      // With Pino disabled\n      getV3DynamicTestConfig({ verbose: 1, disablePino: true }),\n      // With external logger\n      getV3DynamicTestConfig({\n        verbose: 2,\n        disablePino: true,\n        //eslint-disable-next-line @typescript-eslint/no-unused-vars\n        logger: (_line: LogLine) => {\n          // External logger\n        },\n      }),\n      // Without external logger\n      getV3DynamicTestConfig({ verbose: 0, disablePino: true }),\n      // High verbosity\n      getV3DynamicTestConfig({ verbose: 2, disablePino: true }),\n    ];\n\n    try {\n      // Create all instances concurrently\n      const creationPromises = configs.map((config) => {\n        const v3 = new V3(config);\n        instances.push(v3);\n        return v3.init();\n      });\n\n      await Promise.all(creationPromises);\n\n      // All should be initialized successfully\n      expect(instances.length).toBe(configs.length);\n      for (const instance of instances) {\n        expect(instance.context).toBeDefined();\n      }\n\n      // All should be able to perform operations\n      await Promise.all(\n        instances.map(async (instance) => {\n          const page = await instance.context.awaitActivePage();\n          await page.goto(\"about:blank\");\n          expect(page.url()).toContain(\"about:blank\");\n        }),\n      );\n    } finally {\n      await Promise.all(instances.map((instance) => closeV3(instance)));\n    }\n  });\n\n  test(\"V3 instance logger is properly cleaned up on close\", async () => {\n    const logs: LogLine[] = [];\n    const v3 = new V3(\n      getV3DynamicTestConfig({\n        verbose: 2,\n        disablePino: true,\n        logger: (line: LogLine) => logs.push(line),\n      }),\n    );\n\n    await v3.init();\n    const initialLogCount = logs.length;\n    expect(initialLogCount).toBeGreaterThan(0);\n\n    await closeV3(v3);\n\n    // After close, the instance should not generate new logs\n    // (This is hard to test directly, but we can verify the instance is closed)\n    expect(v3[\"state\"].kind).toBe(\"UNINITIALIZED\");\n  });\n\n  test(\"logger works correctly across instance lifecycle\", async () => {\n    const logs: LogLine[] = [];\n    const v3 = new V3(\n      getV3DynamicTestConfig({\n        verbose: 2,\n        disablePino: true,\n        logger: (line: LogLine) => logs.push(line),\n      }),\n    );\n\n    try {\n      // Before init\n      expect(logs.length).toBe(0);\n\n      // After init\n      await v3.init();\n      const afterInitCount = logs.length;\n      expect(afterInitCount).toBeGreaterThan(0);\n\n      // During operation\n      const page = await v3.context.awaitActivePage();\n      await page.goto(\"data:text/html,<h1>Test</h1>\");\n      const afterOperationCount = logs.length;\n      expect(afterOperationCount).toBeGreaterThanOrEqual(afterInitCount);\n\n      // Verify log structure\n      const initLogs = logs.filter((log) => log.category === \"init\");\n      expect(initLogs.length).toBeGreaterThan(0);\n\n      // All logs should have required fields\n      for (const log of logs) {\n        expect(log.category).toBeDefined();\n        expect(log.message).toBeDefined();\n        expect(typeof log.level).toBe(\"number\");\n      }\n    } finally {\n      await closeV3(v3);\n    }\n  });\n\n  test(\"multiple instances can navigate concurrently without logger interference\", async () => {\n    const instanceCount = 3;\n    const instances: V3[] = [];\n    const instanceLogs: Map<number, LogLine[]> = new Map();\n\n    try {\n      // Create instances\n      for (let i = 0; i < instanceCount; i++) {\n        const logs: LogLine[] = [];\n        instanceLogs.set(i, logs);\n\n        const v3 = new V3(\n          getV3DynamicTestConfig({\n            verbose: 1,\n            disablePino: true,\n            logger: (line: LogLine) => logs.push(line),\n          }),\n        );\n\n        instances.push(v3);\n        await v3.init();\n      }\n\n      // Navigate all instances concurrently to different URLs\n      const urls = [\n        \"data:text/html,<h1>Page 1</h1>\",\n        \"data:text/html,<h1>Page 2</h1>\",\n        \"data:text/html,<h1>Page 3</h1>\",\n      ];\n\n      await Promise.all(\n        instances.map(async (instance, i) => {\n          const page = await instance.context.awaitActivePage();\n          await page.goto(urls[i]);\n        }),\n      );\n\n      // Verify each instance navigated to the correct URL\n      for (let i = 0; i < instanceCount; i++) {\n        const page = await instances[i].context.awaitActivePage();\n        expect(page.url()).toContain(`Page ${i + 1}`);\n      }\n\n      // Each instance should have its own logs\n      for (let i = 0; i < instanceCount; i++) {\n        const logs = instanceLogs.get(i)!;\n        expect(logs.length).toBeGreaterThan(0);\n      }\n    } finally {\n      await Promise.all(instances.map((instance) => closeV3(instance)));\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/nested-div.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { captureHybridSnapshot } from \"../../lib/v3/understudy/a11y/snapshot/index.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"tests captureHybridSnapshot() does not break due to -32000 Failed to convert response to JSON: CBOR: stack limit exceeded\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"captureHybridSnapshot does not throw\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/nested-div/\",\n    );\n\n    await expect(captureHybridSnapshot(page)).resolves.toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-addInitScript.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { V3Context } from \"../../lib/v3/understudy/context.js\";\n\nconst EXAMPLE_URL = \"https://example.com\";\n\ntest.describe(\"page.addInitScript\", () => {\n  let v3: V3;\n  let ctx: V3Context;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n    ctx = v3.context;\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"runs scripts on real network navigations\", async () => {\n    const page = await ctx.awaitActivePage();\n\n    await page.addInitScript(() => {\n      (window as unknown as { __fromPageInit?: string }).__fromPageInit =\n        \"page-level\";\n    });\n\n    await page.goto(EXAMPLE_URL, { waitUntil: \"domcontentloaded\" });\n\n    const observed = await page.evaluate(() => {\n      return (window as unknown as { __fromPageInit?: string }).__fromPageInit;\n    });\n\n    expect(observed).toBe(\"page-level\");\n  });\n\n  test(\"scopes scripts to the page only\", async () => {\n    const first = await ctx.awaitActivePage();\n\n    await first.addInitScript(`\n      (function () {\n        function markScope() {\n          var root = document.documentElement;\n          if (!root) return;\n          root.dataset.scopeWitness = \"page-one\";\n        }\n        if (document.readyState === \"loading\") {\n          document.addEventListener(\"DOMContentLoaded\", markScope, {\n            once: true,\n          });\n        } else {\n          markScope();\n        }\n      })();\n    `);\n\n    await first.goto(`${EXAMPLE_URL}/?page=one`, {\n      waitUntil: \"domcontentloaded\",\n    });\n\n    const second = await ctx.newPage();\n    await second.goto(`${EXAMPLE_URL}/?page=two`, {\n      waitUntil: \"domcontentloaded\",\n    });\n\n    const firstValue = await first.evaluate(() => {\n      return document.documentElement.dataset.scopeWitness ?? \"missing\";\n    });\n    const secondValue = await second.evaluate(() => {\n      return document.documentElement.dataset.scopeWitness ?? \"missing\";\n    });\n\n    expect(firstValue).toBe(\"page-one\");\n    expect(secondValue).toBe(\"missing\");\n  });\n\n  test(\"supports passing arguments to function sources\", async () => {\n    const page = await ctx.awaitActivePage();\n    const payload = { greeting: \"hi\", nested: { count: 1 } };\n\n    const initPayload = ((arg) => {\n      function setPayload() {\n        const root = document.documentElement;\n        if (!root) return;\n        root.dataset.pageInitPayload = JSON.stringify(arg);\n      }\n      if (document.readyState === \"loading\") {\n        document.addEventListener(\"DOMContentLoaded\", setPayload, {\n          once: true,\n        });\n      } else {\n        setPayload();\n      }\n    }) as (arg: typeof payload) => void;\n    await page.addInitScript(initPayload, payload);\n\n    await page.goto(`${EXAMPLE_URL}/?page=payload`, {\n      waitUntil: \"domcontentloaded\",\n    });\n\n    const observed = await page.evaluate(() => {\n      const raw = document.documentElement.dataset.pageInitPayload;\n      return raw ? JSON.parse(raw) : undefined;\n    });\n\n    expect(observed).toEqual(payload);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-console.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Page console events\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"captures console messages emitted by the page\", async () => {\n    const browserTarget = (\n      process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n    ).toLowerCase();\n    const isBrowserbase = browserTarget === \"browserbase\";\n    if (isBrowserbase) {\n      console.warn(\n        \"[page-console] TODO: re-enable once BB cloud browsers support Runtime.consoleAPICalled events again. See https://browserbase.slack.com/archives/C06U6CM7YS1/p1769483322836589\",\n      );\n      test.skip(\n        true,\n        \"TODO: re-enable once BB cloud browsers support Runtime.consoleAPICalled events again.\",\n      );\n    }\n    const page = v3.context.pages()[0];\n    const received: Array<{ type: string; text: string }> = [];\n\n    page.on(\"console\", (message) => {\n      received.push({ type: message.type(), text: message.text() });\n    });\n\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/\",\n    );\n\n    await page.evaluate(() => {\n      console.log(\"stagehand console\", { ok: true });\n      console.error(\"stagehand console error\");\n    });\n\n    const waitForConsole = async (\n      predicate: () => boolean,\n      timeoutMs = 2000,\n    ) => {\n      const deadline = Date.now() + timeoutMs;\n      while (Date.now() < deadline) {\n        if (predicate()) return;\n        await new Promise((resolve) => setTimeout(resolve, 50));\n      }\n    };\n\n    await waitForConsole(\n      () =>\n        received.some((m) => m.type === \"log\") &&\n        received.some((m) => m.type === \"error\" && m.text.includes(\"error\")),\n      5000,\n    );\n\n    expect(received.length).toBeGreaterThanOrEqual(2);\n    expect(received.some((m) => m.type === \"log\")).toBeTruthy();\n    expect(\n      received.some((m) => m.type === \"error\" && m.text.includes(\"error\")),\n    ).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-drag-and-drop.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Page.dragAndDrop() - dragging elements\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"drags and drops element to target zone\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { font-family: Arial; margin: 0; padding: 20px; }\n              .container { display: flex; gap: 20px; }\n              .source-box {\n                width: 150px;\n                height: 100px;\n                background: lightblue;\n                border: 2px solid blue;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                cursor: move;\n                user-select: none;\n              }\n              .drop-zone {\n                width: 200px;\n                height: 150px;\n                background: lightyellow;\n                border: 2px dashed orange;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n              }\n              .result { margin-top: 20px; font-weight: bold; }\n            </style>\n          </head>\n          <body>\n            <div class=\"container\">\n              <div id=\"source\" class=\"source-box\" draggable=\"true\">Drag Me</div>\n              <div id=\"dropZone\" class=\"drop-zone\">Drop Here</div>\n            </div>\n            <div id=\"result\" class=\"result\">Status: Waiting</div>\n            <script>\n              const source = document.getElementById('source');\n              const dropZone = document.getElementById('dropZone');\n              const result = document.getElementById('result');\n              \n              source.addEventListener('dragstart', (e) => {\n                e.dataTransfer.effectAllowed = 'move';\n                e.dataTransfer.setData('text/plain', 'Dragged Element');\n              });\n              \n              dropZone.addEventListener('dragover', (e) => {\n                e.preventDefault();\n                e.dataTransfer.dropEffect = 'move';\n                dropZone.style.background = 'lightgreen';\n              });\n              \n              dropZone.addEventListener('dragleave', () => {\n                dropZone.style.background = 'lightyellow';\n              });\n              \n              dropZone.addEventListener('drop', (e) => {\n                e.preventDefault();\n                result.textContent = 'Status: DROP SUCCESSFUL';\n                result.style.color = 'green';\n                dropZone.style.background = 'lightgreen';\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    // Get coordinates for drag and drop\n    const sourceLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#source\");\n    const dropZoneLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#dropZone\");\n\n    const fromX = sourceLocation.x + sourceLocation.width / 2;\n    const fromY = sourceLocation.y + sourceLocation.height / 2;\n    const toX = dropZoneLocation.x + dropZoneLocation.width / 2;\n    const toY = dropZoneLocation.y + dropZoneLocation.height / 2;\n\n    // Perform drag and drop\n    await page.dragAndDrop(fromX, fromY, toX, toY);\n\n    // Wait for events to be processed\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 100)));\n\n    // Verify visual result\n    const resultText = await page.evaluate(\n      () => document.getElementById(\"result\").textContent,\n    );\n    expect(resultText).toContain(\"DROP SUCCESSFUL\");\n  });\n\n  test(\"drag and drop with steps parameter\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { margin: 0; padding: 20px; }\n              .box {\n                width: 100px;\n                height: 100px;\n                background: lightblue;\n                margin: 20px;\n                cursor: move;\n              }\n              .target {\n                width: 200px;\n                height: 200px;\n                background: lightyellow;\n                margin: 20px;\n                border: 2px dashed orange;\n              }\n            </style>\n          </head>\n          <body>\n            <div id=\"box\" class=\"box\" draggable=\"true\"></div>\n            <div id=\"target\" class=\"target\"></div>\n            <div id=\"status\">Not dropped</div>\n            <script>\n              document.getElementById('box').addEventListener('dragstart', (e) => {\n                e.dataTransfer.effectAllowed = 'move';\n              });\n              document.getElementById('target').addEventListener('drop', (e) => {\n                e.preventDefault();\n                document.getElementById('status').textContent = 'Dropped with steps';\n              });\n              document.getElementById('target').addEventListener('dragover', (e) => {\n                e.preventDefault();\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    const boxLocation = await page.frames()[0].getLocationForSelector(\"#box\");\n    const targetLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#target\");\n\n    const fromX = boxLocation.x + boxLocation.width / 2;\n    const fromY = boxLocation.y + boxLocation.height / 2;\n    const toX = targetLocation.x + targetLocation.width / 2;\n    const toY = targetLocation.y + targetLocation.height / 2;\n\n    // Drag with multiple steps for smoother motion\n    await page.dragAndDrop(fromX, fromY, toX, toY, { steps: 5 });\n\n    // Wait for events to be processed\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 100)));\n\n    const status = await page.evaluate(\n      () => document.getElementById(\"status\").textContent,\n    );\n    expect(status).toContain(\"Dropped\");\n  });\n\n  test(\"drag and drop with delay between steps\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { margin: 0; padding: 20px; }\n              #dragItem { width: 80px; height: 80px; background: lightcoral; cursor: move; }\n              #dropArea { width: 150px; height: 150px; background: lightgray; margin-top: 20px; }\n            </style>\n          </head>\n          <body>\n            <div id=\"dragItem\" draggable=\"true\"></div>\n            <div id=\"dropArea\"></div>\n            <div id=\"complete\">false</div>\n            <script>\n              const item = document.getElementById('dragItem');\n              const area = document.getElementById('dropArea');\n              const complete = document.getElementById('complete');\n              \n              item.addEventListener('dragstart', (e) => {\n                e.dataTransfer.effectAllowed = 'move';\n              });\n              \n              area.addEventListener('drop', (e) => {\n                e.preventDefault();\n                complete.textContent = 'true';\n              });\n              \n              area.addEventListener('dragover', (e) => {\n                e.preventDefault();\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    const itemLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#dragItem\");\n    const areaLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#dropArea\");\n\n    const fromX = itemLocation.x + itemLocation.width / 2;\n    const fromY = itemLocation.y + itemLocation.height / 2;\n    const toX = areaLocation.x + areaLocation.width / 2;\n    const toY = areaLocation.y + areaLocation.height / 2;\n\n    // Drag with delay between steps\n    await page.dragAndDrop(fromX, fromY, toX, toY, { steps: 3, delay: 50 });\n\n    // Wait for events to be processed\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 100)));\n\n    const isComplete = await page.evaluate(\n      () => document.getElementById(\"complete\").textContent === \"true\",\n    );\n    expect(isComplete).toBe(true);\n  });\n\n  test(\"drag and drop returns xpath when requested\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { margin: 20px; }\n              #source { width: 100px; height: 100px; background: blue; cursor: move; }\n              #target { width: 150px; height: 150px; background: green; margin-top: 20px; }\n            </style>\n          </head>\n          <body>\n            <div id=\"source\" draggable=\"true\"></div>\n            <div id=\"target\"></div>\n            <script>\n              document.getElementById('source').addEventListener('dragstart', (e) => {\n                e.dataTransfer.effectAllowed = 'move';\n              });\n              document.getElementById('target').addEventListener('drop', (e) => {\n                e.preventDefault();\n              });\n              document.getElementById('target').addEventListener('dragover', (e) => {\n                e.preventDefault();\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    const sourceLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#source\");\n    const targetLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#target\");\n\n    const fromX = sourceLocation.x + sourceLocation.width / 2;\n    const fromY = sourceLocation.y + sourceLocation.height / 2;\n    const toX = targetLocation.x + targetLocation.width / 2;\n    const toY = targetLocation.y + targetLocation.height / 2;\n\n    const [fromXpath, toXpath] = await page.dragAndDrop(\n      fromX,\n      fromY,\n      toX,\n      toY,\n      {\n        returnXpath: true,\n      },\n    );\n\n    // Should return xpaths for both start and end positions\n    expect(typeof fromXpath).toBe(\"string\");\n    expect(typeof toXpath).toBe(\"string\");\n    expect(fromXpath.length).toBeGreaterThan(0);\n    expect(toXpath.length).toBeGreaterThan(0);\n  });\n\n  test(\"drag and drop without returnXpath returns empty strings\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { margin: 20px; }\n              #item1 { width: 80px; height: 80px; background: red; cursor: move; }\n              #item2 { width: 100px; height: 100px; background: yellow; margin-top: 20px; }\n            </style>\n          </head>\n          <body>\n            <div id=\"item1\" draggable=\"true\"></div>\n            <div id=\"item2\"></div>\n            <script>\n              document.getElementById('item1').addEventListener('dragstart', (e) => {\n                e.dataTransfer.effectAllowed = 'move';\n              });\n              document.getElementById('item2').addEventListener('drop', (e) => {\n                e.preventDefault();\n              });\n              document.getElementById('item2').addEventListener('dragover', (e) => {\n                e.preventDefault();\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    const item1Location = await page\n      .frames()[0]\n      .getLocationForSelector(\"#item1\");\n    const item2Location = await page\n      .frames()[0]\n      .getLocationForSelector(\"#item2\");\n\n    const fromX = item1Location.x + item1Location.width / 2;\n    const fromY = item1Location.y + item1Location.height / 2;\n    const toX = item2Location.x + item2Location.width / 2;\n    const toY = item2Location.y + item2Location.height / 2;\n\n    const [fromXpath, toXpath] = await page.dragAndDrop(fromX, fromY, toX, toY);\n\n    // Should return empty strings when returnXpath is not set\n    expect(fromXpath).toBe(\"\");\n    expect(toXpath).toBe(\"\");\n  });\n\n  test(\"drag and drop with different mouse buttons\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { margin: 20px; }\n              .draggable { width: 100px; height: 100px; background: lightblue; cursor: move; }\n              .drop-area { width: 200px; height: 200px; background: lightgray; margin-top: 20px; }\n            </style>\n          </head>\n          <body>\n            <div id=\"source\" class=\"draggable\" draggable=\"true\"></div>\n            <div id=\"target\" class=\"drop-area\"></div>\n            <div id=\"buttonUsed\">none</div>\n            <script>\n              document.getElementById('source').addEventListener('dragstart', (e) => {\n                e.dataTransfer.effectAllowed = 'move';\n              });\n              document.getElementById('target').addEventListener('drop', (e) => {\n                e.preventDefault();\n                document.getElementById('buttonUsed').textContent = 'left';\n              });\n              document.getElementById('target').addEventListener('dragover', (e) => {\n                e.preventDefault();\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    const sourceLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#source\");\n    const targetLocation = await page\n      .frames()[0]\n      .getLocationForSelector(\"#target\");\n\n    const fromX = sourceLocation.x + sourceLocation.width / 2;\n    const fromY = sourceLocation.y + sourceLocation.height / 2;\n    const toX = targetLocation.x + targetLocation.width / 2;\n    const toY = targetLocation.y + targetLocation.height / 2;\n\n    // Test with left button (default)\n    await page.dragAndDrop(fromX, fromY, toX, toY, { button: \"left\" });\n\n    // Wait for events to be processed\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 100)));\n\n    const buttonUsed = await page.evaluate(\n      () => document.getElementById(\"buttonUsed\").textContent,\n    );\n    expect(buttonUsed).toBe(\"left\");\n  });\n\n  test(\"multiple sequential drag and drops\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n          <!doctype html>\n          <html>\n          <head>\n            <style>\n              body { margin: 20px; font-family: Arial; }\n              .item { width: 80px; height: 80px; background: lightblue; margin: 10px; cursor: move; display: inline-block; }\n              .zone { width: 150px; height: 150px; background: lightyellow; margin: 10px; display: inline-block; border: 2px dashed orange; }\n              #log { margin-top: 20px; }\n            </style>\n          </head>\n          <body>\n            <div id=\"item1\" class=\"item\" draggable=\"true\">Item 1</div>\n            <div id=\"zone1\" class=\"zone\"></div>\n            <div id=\"item2\" class=\"item\" draggable=\"true\">Item 2</div>\n            <div id=\"zone2\" class=\"zone\"></div>\n            <div id=\"log\">Drops: 0</div>\n            <script>\n              let dropCount = 0;\n              const items = ['item1', 'item2'];\n              const zones = ['zone1', 'zone2'];\n              \n              items.forEach(id => {\n                document.getElementById(id).addEventListener('dragstart', (e) => {\n                  e.dataTransfer.effectAllowed = 'move';\n                });\n              });\n              \n              zones.forEach(id => {\n                const zone = document.getElementById(id);\n                zone.addEventListener('drop', (e) => {\n                  e.preventDefault();\n                  dropCount++;\n                  document.getElementById('log').textContent = 'Drops: ' + dropCount;\n                });\n                zone.addEventListener('dragover', (e) => {\n                  e.preventDefault();\n                });\n              });\n            </script>\n          </body>\n          </html>\n        `),\n    );\n\n    const item1Location = await page\n      .frames()[0]\n      .getLocationForSelector(\"#item1\");\n    const zone1Location = await page\n      .frames()[0]\n      .getLocationForSelector(\"#zone1\");\n\n    const from1X = item1Location.x + item1Location.width / 2;\n    const from1Y = item1Location.y + item1Location.height / 2;\n    const to1X = zone1Location.x + zone1Location.width / 2;\n    const to1Y = zone1Location.y + zone1Location.height / 2;\n\n    await page.dragAndDrop(from1X, from1Y, to1X, to1Y);\n\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 100)));\n\n    let dropCountText = await page.evaluate(\n      () => document.getElementById(\"log\").textContent,\n    );\n    expect(dropCountText).toContain(\"Drops: 1\");\n\n    const item2Location = await page\n      .frames()[0]\n      .getLocationForSelector(\"#item2\");\n    const zone2Location = await page\n      .frames()[0]\n      .getLocationForSelector(\"#zone2\");\n\n    const from2X = item2Location.x + item2Location.width / 2;\n    const from2Y = item2Location.y + item2Location.height / 2;\n    const to2X = zone2Location.x + zone2Location.width / 2;\n    const to2Y = zone2Location.y + zone2Location.height / 2;\n\n    await page.dragAndDrop(from2X, from2Y, to2X, to2Y);\n\n    // Wait for events to be processed\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 100)));\n\n    dropCountText = await page.evaluate(\n      () => document.getElementById(\"log\").textContent,\n    );\n    expect(dropCountText).toContain(\"Drops: 2\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-extra-http-headers.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport type { Protocol } from \"devtools-protocol\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\nconst TEST_URL =\n  \"https://browserbase.github.io/stagehand-eval-sites/sites/example/\";\n\ntest.describe(\"page.setExtraHTTPHeaders\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"applies headers to navigation requests\", async () => {\n    const ctx = v3.context;\n    const page = await ctx.awaitActivePage();\n\n    await page.setExtraHTTPHeaders({ \"x-page-header\": \"from-page\" });\n\n    const internal = page as unknown as {\n      mainSession: {\n        send: (method: string, params?: unknown) => Promise<unknown>;\n        on: (event: string, handler: (params: unknown) => void) => void;\n        off: (event: string, handler: (params: unknown) => void) => void;\n      };\n    };\n\n    await internal.mainSession.send(\"Network.enable\");\n\n    const requestPromise = new Promise<Protocol.Network.RequestWillBeSentEvent>(\n      (resolve, reject) => {\n        const timeout = setTimeout(() => {\n          internal.mainSession.off(\"Network.requestWillBeSent\", handler);\n          reject(new Error(\"Timed out waiting for request\"));\n        }, 5000);\n\n        const handler = (evt: Protocol.Network.RequestWillBeSentEvent) => {\n          if (evt.type !== \"Document\") return;\n          const url = String(evt.request?.url ?? \"\");\n          if (!url.startsWith(TEST_URL)) return;\n          clearTimeout(timeout);\n          internal.mainSession.off(\"Network.requestWillBeSent\", handler);\n          resolve(evt);\n        };\n\n        internal.mainSession.on(\"Network.requestWillBeSent\", handler);\n      },\n    );\n\n    await page.goto(TEST_URL, { waitUntil: \"domcontentloaded\" });\n\n    const request = await requestPromise;\n    const headers = Object.fromEntries(\n      Object.entries(request.request.headers ?? {}).map(([key, value]) => [\n        key.toLowerCase(),\n        String(value),\n      ]),\n    );\n\n    expect(headers[\"x-page-header\"]).toBe(\"from-page\");\n  });\n\n  test(\"updated headers replace previous ones\", async () => {\n    const ctx = v3.context;\n    const page = await ctx.awaitActivePage();\n\n    const internal = page as unknown as {\n      mainSession: {\n        send: (method: string, params?: unknown) => Promise<unknown>;\n        on: (event: string, handler: (params: unknown) => void) => void;\n        off: (event: string, handler: (params: unknown) => void) => void;\n      };\n    };\n\n    await internal.mainSession.send(\"Network.enable\");\n\n    // Set initial headers and navigate\n    await page.setExtraHTTPHeaders({ \"x-first\": \"yes\" });\n    await page.goto(TEST_URL, { waitUntil: \"domcontentloaded\" });\n\n    // Update headers\n    await page.setExtraHTTPHeaders({ \"x-second\": \"yes\" });\n\n    const requestPromise = new Promise<Protocol.Network.RequestWillBeSentEvent>(\n      (resolve, reject) => {\n        const timeout = setTimeout(() => {\n          internal.mainSession.off(\"Network.requestWillBeSent\", handler);\n          reject(new Error(\"Timed out waiting for request\"));\n        }, 5000);\n\n        const handler = (evt: Protocol.Network.RequestWillBeSentEvent) => {\n          if (evt.type !== \"Document\") return;\n          const url = String(evt.request?.url ?? \"\");\n          if (!url.startsWith(TEST_URL)) return;\n          clearTimeout(timeout);\n          internal.mainSession.off(\"Network.requestWillBeSent\", handler);\n          resolve(evt);\n        };\n\n        internal.mainSession.on(\"Network.requestWillBeSent\", handler);\n      },\n    );\n\n    await page.goto(TEST_URL, { waitUntil: \"domcontentloaded\" });\n\n    const request = await requestPromise;\n    const headers = Object.fromEntries(\n      Object.entries(request.request.headers ?? {}).map(([key, value]) => [\n        key.toLowerCase(),\n        String(value),\n      ]),\n    );\n\n    expect(headers[\"x-second\"]).toBe(\"yes\");\n    expect(headers[\"x-first\"]).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-goto-response.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Page.goto() response surface\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"returns a response object for network navigations\", async () => {\n    const page = v3.context.pages()[0];\n\n    const response = await page.goto(\"https://example.com\");\n\n    expect(response).not.toBeNull();\n    expect(response!.status()).toBe(200);\n    expect(response!.ok()).toBeTruthy();\n\n    const headers = await response.headersArray();\n    expect(headers.length).toBeGreaterThan(0);\n\n    const body = await response.text();\n    expect(body).toContain(\"Example Domain\");\n\n    const finished = await response.finished();\n    expect(finished).toBeNull();\n  });\n\n  test(\"falls back to null for data URLs\", async () => {\n    const page = v3.context.pages()[0];\n\n    const response = await page.goto(\n      \"data:text/html,<html><body data-testid='fallback'>inline</body></html>\",\n    );\n\n    expect(response).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-hover.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Page.hover() - mouse hover at coordinates\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"hover triggers mouseover event at coordinates\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"margin: 0; padding: 0;\">\n            <div id=\"target\" \n                 style=\"position: absolute; top: 100px; left: 100px; width: 200px; height: 200px; background: lightblue;\"\n                 onmouseover=\"this.dataset.hovered='true'\"\n                 onmouseout=\"this.dataset.hovered='false'\">\n              Hover Me\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    // Check initial state\n    let hovered = await page.evaluate(() => {\n      const el = document.getElementById(\"target\");\n      return el?.dataset.hovered === \"true\";\n    });\n    expect(hovered).toBe(false);\n\n    // Hover at coordinates within the target element (200, 200 is center of the div)\n    await page.hover(200, 200);\n\n    // Verify mouseover was triggered\n    hovered = await page.evaluate(() => {\n      const el = document.getElementById(\"target\");\n      return el?.dataset.hovered === \"true\";\n    });\n    expect(hovered).toBe(true);\n  });\n\n  test(\"hover moves mouse without clicking\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"margin: 0; padding: 0;\">\n            <button id=\"btn\" \n                    style=\"position: absolute; top: 100px; left: 100px; width: 200px; height: 100px;\"\n                    onclick=\"this.dataset.clicked='true'\"\n                    onmouseover=\"this.dataset.hovered='true'\">\n              Click Me\n            </button>\n          </body></html>`,\n        ),\n    );\n\n    // Hover over the button\n    await page.hover(200, 150);\n\n    // Check that hover happened but click did not\n    const state = await page.evaluate(() => {\n      const btn = document.getElementById(\"btn\");\n      return {\n        hovered: btn?.dataset.hovered === \"true\",\n        clicked: btn?.dataset.clicked === \"true\",\n      };\n    });\n\n    expect(state.hovered).toBe(true);\n    expect(state.clicked).toBe(false);\n  });\n\n  test(\"hover returns xpath when requested\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"margin: 0; padding: 0;\">\n            <div id=\"target\" style=\"position: absolute; top: 0px; left: 400px; width: 300px; height: 100px; background: blue;\">\n              Target element\n            </div>\n            <p style=\"position: absolute; top: 200px; left: 0px;\">Content below</p>\n          </body></html>`,\n        ),\n    );\n\n    // Hover at coordinate (550, 50) which should be directly over the target div\n    const xpath = await page.hover(550, 50, { returnXpath: true });\n\n    // Should return a non-empty xpath string for the element at that coordinate\n    expect(typeof xpath).toBe(\"string\");\n    expect(xpath.length).toBeGreaterThan(0);\n    // Xpath should reference the div\n    expect(xpath.toLowerCase()).toMatch(/div|target/);\n  });\n\n  test(\"hover without returnXpath returns empty string\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"margin: 0; padding: 0;\">\n            <div style=\"width: 100px; height: 100px; background: lightblue;\">Content</div>\n          </body></html>`,\n        ),\n    );\n\n    // Hover without returnXpath\n    const result = await page.hover(50, 50);\n\n    // Should return empty string\n    expect(result).toBe(\"\");\n  });\n\n  test(\"hover triggers CSS :hover styles\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html>\n          <head>\n            <style>\n              #hoverable {\n                position: absolute;\n                top: 100px;\n                left: 100px;\n                width: 200px;\n                height: 200px;\n                background: red;\n              }\n              #hoverable:hover {\n                background: green;\n              }\n            </style>\n          </head>\n          <body style=\"margin: 0; padding: 0;\">\n            <div id=\"hoverable\">Hover to change color</div>\n          </body></html>`,\n        ),\n    );\n\n    // Get initial background color\n    let bgColor = await page.evaluate(() => {\n      const el = document.getElementById(\"hoverable\");\n      return getComputedStyle(el!).backgroundColor;\n    });\n    expect(bgColor).toBe(\"rgb(255, 0, 0)\"); // red\n\n    // Hover over the element\n    await page.hover(200, 200);\n\n    // Check that CSS :hover state is applied\n    bgColor = await page.evaluate(() => {\n      const el = document.getElementById(\"hoverable\");\n      return getComputedStyle(el!).backgroundColor;\n    });\n    expect(bgColor).toBe(\"rgb(0, 128, 0)\"); // green\n  });\n\n  test(\"multiple hovers move the mouse correctly\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"margin: 0; padding: 0;\">\n            <div id=\"box1\" \n                 style=\"position: absolute; top: 0; left: 0; width: 100px; height: 100px; background: red;\"\n                 onmouseover=\"this.dataset.hovered='true'\"\n                 onmouseout=\"this.dataset.hovered='false'\">\n              Box 1\n            </div>\n            <div id=\"box2\" \n                 style=\"position: absolute; top: 0; left: 200px; width: 100px; height: 100px; background: blue;\"\n                 onmouseover=\"this.dataset.hovered='true'\"\n                 onmouseout=\"this.dataset.hovered='false'\">\n              Box 2\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    // Hover over box1\n    await page.hover(50, 50);\n\n    let state = await page.evaluate(() => ({\n      box1: document.getElementById(\"box1\")?.dataset.hovered === \"true\",\n      box2: document.getElementById(\"box2\")?.dataset.hovered === \"true\",\n    }));\n\n    expect(state.box1).toBe(true);\n    expect(state.box2).toBe(false);\n\n    // Move hover to box2\n    await page.hover(250, 50);\n\n    state = await page.evaluate(() => ({\n      box1: document.getElementById(\"box1\")?.dataset.hovered === \"true\",\n      box2: document.getElementById(\"box2\")?.dataset.hovered === \"true\",\n    }));\n\n    expect(state.box1).toBe(false);\n    expect(state.box2).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-screenshot.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { promises as fs } from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport { Frame } from \"../../lib/v3/understudy/frame.js\";\n\nconst wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\ntest.describe(\"Page.screenshot options\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"rejects clip combined with fullPage\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>test</body></html>\");\n\n    await expect(\n      page.screenshot({\n        fullPage: true,\n        clip: { x: 0, y: 0, width: 100, height: 100 },\n      }),\n    ).rejects.toThrow(/clip and fullPage/);\n  });\n\n  test(\"rejects unsupported image type\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>noop</body></html>\");\n\n    await expect(\n      page.screenshot({\n        // @ts-expect-error intentional invalid type for runtime validation\n        type: \"webp\",\n      }),\n    ).rejects.toThrow(/unsupported image type/);\n  });\n\n  test(\"rejects jpeg quality for png screenshots\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>noop</body></html>\");\n\n    await expect(page.screenshot({ type: \"png\", quality: 50 })).rejects.toThrow(\n      /quality option is only valid/,\n    );\n  });\n\n  test(\"honours timeout option\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\"data:text/html,<html><body>noop</body></html>\");\n\n    const mainFrame = page.mainFrame();\n    const originalScreenshot = mainFrame.screenshot.bind(mainFrame);\n\n    (\n      mainFrame as typeof mainFrame & {\n        screenshot: typeof mainFrame.screenshot;\n      }\n    ).screenshot = async () => {\n      await wait(50);\n      return Buffer.from(\"late\");\n    };\n\n    try {\n      await expect(page.screenshot({ timeout: 10 })).rejects.toThrow(\n        /timed out|timeout/i,\n      );\n    } finally {\n      (\n        mainFrame as typeof mainFrame & {\n          screenshot: typeof mainFrame.screenshot;\n        }\n      ).screenshot = originalScreenshot;\n    }\n  });\n\n  test(\"applies advanced options and cleans up overlays\", async () => {\n    const page = v3.context.pages()[0];\n    const screenshotTimeout = process.env.CI ? 15000 : 5000;\n    const testStart = Date.now();\n    console.log(\n      `[screenshot-test] start ${new Date(testStart).toISOString()} timeout=${screenshotTimeout}`,\n    );\n\n    const html = `\n      <!doctype html>\n      <html>\n        <head>\n          <meta charset=\"utf-8\" />\n          <style>\n            body { background: #aaccee; margin: 0; height: 100vh; display: flex; flex-direction: column; align-items: flex-start; }\n            .mask-target { width: 80px; height: 80px; margin: 40px; background: rgb(0, 180, 60); animation: pulse 1s infinite alternate; }\n            @keyframes pulse { from { transform: scale(1); } to { transform: scale(1.2); } }\n          </style>\n        </head>\n        <body>\n          <div class=\"mask-target\"></div>\n          <div class=\"mask-target\"></div>\n          <input id=\"focus-me\" value=\"focus\" />\n          <script>document.getElementById('focus-me').focus();</script>\n        </body>\n      </html>\n    `;\n\n    await page.goto(\"data:text/html,\" + encodeURIComponent(html));\n    console.log(`[screenshot-test] page loaded in ${Date.now() - testStart}ms`);\n\n    const maskLocator = page.locator(\".mask-target\");\n    const tempPath = path.join(\n      os.tmpdir(),\n      `stagehand-screenshot-${Date.now()}-${Math.random().toString(36).slice(2)}.jpeg`,\n    );\n    console.log(`[screenshot-test] tempPath=${tempPath}`);\n\n    const targetId = page.targetId();\n    const screenshotCalls: Array<{\n      frameId: string;\n      options: Parameters<Frame[\"screenshot\"]>[0];\n    }> = [];\n    const evaluateCalls: Array<{ frameId: string; arg: unknown }> = [];\n    const originalScreenshot = Frame.prototype.screenshot;\n    const originalEvaluate = Frame.prototype.evaluate;\n\n    // Hook Frame.screenshot so we can assert which options reach CDP without writing real data.\n    Frame.prototype.screenshot = async function screenshotSpy(options) {\n      const frame = this as Frame;\n      if (frame.pageId === targetId) {\n        screenshotCalls.push({ frameId: frame.frameId, options });\n        return Buffer.from(\"stub-image\");\n      }\n      return originalScreenshot.call(this, options);\n    };\n\n    // Spy on Frame.evaluate to capture the arguments used to inject CSS/masks.\n    Frame.prototype.evaluate = async function evaluateSpy(expression, arg?) {\n      const frame = this as Frame;\n      if (frame.pageId === targetId) {\n        evaluateCalls.push({ frameId: frame.frameId, arg });\n      }\n      return originalEvaluate.call(this, expression as never, arg);\n    } as Frame[\"evaluate\"];\n\n    const internalPage = page as unknown as {\n      mainSession: {\n        send: (method: string, params?: unknown) => Promise<unknown>;\n      };\n    };\n    const sendCalls: Array<{ method: string; params: unknown }> = [];\n    const originalSend = internalPage.mainSession.send.bind(\n      internalPage.mainSession,\n    ) as (method: string, params?: unknown) => Promise<unknown>;\n    // Capture background overrides so we can confirm omitBackground toggles on/off.\n    internalPage.mainSession.send = async (\n      method: string,\n      params?: unknown,\n    ) => {\n      sendCalls.push({ method, params });\n      return originalSend(method, params);\n    };\n\n    try {\n      const maskCount = await maskLocator.count();\n      console.log(`[screenshot-test] maskLocator.count=${maskCount}`);\n\n      const buffer = await page.screenshot({\n        animations: \"disabled\",\n        caret: \"hide\",\n        clip: { x: 0, y: 0, width: 200, height: 200 },\n        mask: [maskLocator],\n        maskColor: \"rgba(255, 0, 0, 0.4)\",\n        omitBackground: true,\n        path: tempPath,\n        quality: 80,\n        scale: \"css\",\n        style: \"body { border: 3px solid black; }\",\n        timeout: screenshotTimeout,\n        type: \"jpeg\",\n      });\n      console.log(\n        `[screenshot-test] screenshot returned bytes=${buffer.length} elapsed=${Date.now() - testStart}ms`,\n      );\n\n      expect(Buffer.isBuffer(buffer)).toBeTruthy();\n      expect(screenshotCalls.length).toBeGreaterThanOrEqual(1);\n      console.log(\n        `[screenshot-test] screenshotCalls=${screenshotCalls.length} evaluateCalls=${evaluateCalls.length} sendCalls=${sendCalls.length}`,\n      );\n      const recorded = screenshotCalls[0]?.options ?? {};\n      expect(recorded).toMatchObject({ type: \"jpeg\", quality: 80 });\n      expect(recorded?.clip).toMatchObject({\n        x: 0,\n        y: 0,\n        width: 200,\n        height: 200,\n      });\n      if (typeof recorded?.scale === \"number\") {\n        expect(recorded.scale).toBeGreaterThan(0);\n        expect(recorded.scale).toBeLessThanOrEqual(2);\n      }\n\n      await fs.stat(tempPath);\n\n      const maskNodes = await page.evaluate(\n        () => document.querySelectorAll(\"[data-stagehand-mask]\").length,\n      );\n      expect(maskNodes).toBe(0);\n\n      const styleNodes = await page.evaluate(\n        () => document.querySelectorAll(\"[data-stagehand-style]\").length,\n      );\n      expect(styleNodes).toBe(0);\n\n      const backgroundCalls = sendCalls.filter(\n        (call) => call.method === \"Emulation.setDefaultBackgroundColorOverride\",\n      );\n      expect(backgroundCalls.length).toBeGreaterThan(1);\n      expect(\n        backgroundCalls.some(\n          (call) =>\n            call.params &&\n            typeof call.params === \"object\" &&\n            \"color\" in (call.params as Record<string, unknown>),\n        ),\n      ).toBeTruthy();\n      expect(\n        backgroundCalls.some(\n          (call) =>\n            !call.params ||\n            Object.keys(call.params as Record<string, unknown>).length === 0,\n        ),\n      ).toBeTruthy();\n\n      const cssArgs = evaluateCalls\n        .map((entry) => {\n          const value = entry.arg as { css?: string } | undefined;\n          return value?.css ?? null;\n        })\n        .filter((css): css is string => typeof css === \"string\");\n\n      const tokens = evaluateCalls\n        .map((entry) => {\n          const arg = entry.arg as { token?: string } | undefined;\n          return arg?.token ?? null;\n        })\n        .filter((token): token is string => typeof token === \"string\");\n\n      // Tokens include which helper injected the style (animations/caret/custom).\n      expect(tokens.some((token) => token.includes(\"animations\"))).toBeTruthy();\n      expect(tokens.some((token) => token.includes(\"caret\"))).toBeTruthy();\n      expect(tokens.some((token) => token.includes(\"custom\"))).toBeTruthy();\n      // Custom style should bubble through so we check the actual CSS text.\n      expect(\n        cssArgs.some((css) => css.includes(\"border: 3px solid black\")),\n      ).toBeTruthy();\n\n      const maskCalls = evaluateCalls.filter((entry) => {\n        const arg = entry.arg;\n        return (\n          arg &&\n          typeof arg === \"object\" &&\n          \"rects\" in (arg as Record<string, unknown>)\n        );\n      });\n      expect(maskCalls.length).toBeGreaterThan(0);\n      const rects = (maskCalls[0]?.arg as { rects?: unknown } | undefined)\n        ?.rects;\n      expect(Array.isArray(rects)).toBeTruthy();\n      expect((rects as unknown[]).length).toBe(2);\n    } finally {\n      Frame.prototype.screenshot = originalScreenshot;\n      Frame.prototype.evaluate = originalEvaluate;\n      internalPage.mainSession.send = originalSend;\n      await fs.unlink(tempPath).catch(() => {});\n    }\n  });\n\n  test(\"masks elements inside dialog top layer\", async () => {\n    const page = v3.context.pages()[0];\n\n    const html = `\n      <!doctype html>\n      <html>\n        <head>\n          <meta charset=\"utf-8\" />\n          <style>\n            dialog { padding: 16px; border: 2px solid #444; }\n            #dialog-input { display: block; width: 160px; height: 32px; }\n          </style>\n        </head>\n        <body>\n          <dialog id=\"dialog\">\n            <label>Secret <input id=\"dialog-input\" value=\"top-layer\" /></label>\n          </dialog>\n          <script>\n            const dialog = document.getElementById(\"dialog\");\n            if (dialog) {\n              if (typeof dialog.showModal === \"function\") {\n                try {\n                  dialog.showModal();\n                } catch {\n                  dialog.setAttribute(\"open\", \"\");\n                }\n              } else {\n                dialog.setAttribute(\"open\", \"\");\n              }\n            }\n          </script>\n        </body>\n      </html>\n    `;\n\n    await page.goto(\"data:text/html,\" + encodeURIComponent(html));\n\n    const targetId = page.targetId();\n    const originalScreenshot = Frame.prototype.screenshot;\n    let dialogMaskCount = 0;\n\n    Frame.prototype.screenshot = async function screenshotSpy(options) {\n      const frame = this as Frame;\n      if (frame.pageId === targetId) {\n        dialogMaskCount = await frame.evaluate(() => {\n          const dialog = document.querySelector(\"dialog[open]\");\n          if (!dialog) return 0;\n          return dialog.querySelectorAll(\"[data-stagehand-mask]\").length;\n        });\n        return Buffer.from(\"stub-image\");\n      }\n      return originalScreenshot.call(this, options);\n    };\n\n    try {\n      await page.screenshot({\n        mask: [page.locator(\"#dialog-input\")],\n      });\n      expect(dialogMaskCount).toBeGreaterThan(0);\n    } finally {\n      Frame.prototype.screenshot = originalScreenshot;\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-scroll.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Page.scroll() - mouse wheel scrolling\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"scrolls page vertically with positive deltaY\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 2000px;\">\n            <div style=\"height: 400px; background: lightblue;\">Section 1</div>\n            <div style=\"height: 400px; background: lightgreen;\">Section 2</div>\n            <div style=\"height: 400px; background: lightyellow;\">Section 3</div>\n            <div style=\"height: 400px; background: lightcoral;\">Section 4</div>\n            <div style=\"height: 400px; background: lightgray;\">Section 5</div>\n          </body></html>`,\n        ),\n    );\n\n    // Get initial scroll position\n    let scrollY = await page.evaluate(() => window.scrollY);\n    expect(scrollY).toBe(0);\n\n    // Scroll down (positive deltaY)\n    await page.scroll(640, 400, 0, 300);\n\n    // Wait for scroll to complete\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    // Check that we've scrolled down\n    scrollY = await page.evaluate(() => window.scrollY);\n    expect(scrollY).toBeGreaterThan(0);\n  });\n\n  test(\"scrolls page horizontally with positive deltaX\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"width: 2000px; height: 600px;\">\n            <div style=\"display: inline-block; width: 400px; height: 100%; background: lightblue;\">Section 1</div>\n            <div style=\"display: inline-block; width: 400px; height: 100%; background: lightgreen;\">Section 2</div>\n            <div style=\"display: inline-block; width: 400px; height: 100%; background: lightyellow;\">Section 3</div>\n            <div style=\"display: inline-block; width: 400px; height: 100%; background: lightcoral;\">Section 4</div>\n            <div style=\"display: inline-block; width: 400px; height: 100%; background: lightgray;\">Section 5</div>\n          </body></html>`,\n        ),\n    );\n\n    let scrollX = await page.evaluate(() => window.scrollX);\n    expect(scrollX).toBe(0);\n\n    // Scroll right (positive deltaX)\n    await page.scroll(640, 400, 300, 0);\n\n    // Wait for scroll to complete\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    // Check that we've scrolled right\n    scrollX = await page.evaluate(() => window.scrollX);\n    expect(scrollX).toBeGreaterThan(0);\n  });\n\n  test(\"scrolls in both directions simultaneously\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"width: 2000px; height: 2000px;\">\n            <div style=\"width: 100%; height: 100%; background: linear-gradient(135deg, lightblue, lightcoral);\">\n              Diagonal content\n            </div>\n          </body></html>`,\n        ),\n    );\n\n    // Scroll both horizontally and vertically\n    await page.scroll(640, 400, 200, 200);\n\n    // Wait for scroll to complete\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    // Check both directions changed\n    const scrollPos = await page.evaluate(() => ({\n      x: window.scrollX,\n      y: window.scrollY,\n    }));\n\n    expect(scrollPos.x).toBeGreaterThan(0);\n    expect(scrollPos.y).toBeGreaterThan(0);\n  });\n\n  test(\"scrolls at specific coordinate on page\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 2000px;\">\n            <div id=\"marker\" style=\"position: fixed; top: 400px; left: 640px; width: 2px; height: 2px; background: red;\"></div>\n            <div style=\"height: 500px; background: lightblue;\">Top</div>\n            <div style=\"height: 500px; background: lightgreen;\">Middle</div>\n            <div style=\"height: 500px; background: lightyellow;\">Bottom</div>\n          </body></html>`,\n        ),\n    );\n\n    // Scroll from specific coordinates\n    await page.scroll(640, 400, 0, 400);\n\n    // Wait for scroll to complete\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    // Verify scroll happened\n    const scrollY = await page.evaluate(() => window.scrollY);\n    expect(scrollY).toBeGreaterThan(0);\n  });\n\n  test(\"scrolls with large deltaY values\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 5000px;\">\n            <div style=\"height: 1000px; background: lightblue;\">Section 1</div>\n            <div style=\"height: 1000px; background: lightgreen;\">Section 2</div>\n            <div style=\"height: 1000px; background: lightyellow;\">Section 3</div>\n            <div style=\"height: 1000px; background: lightcoral;\">Section 4</div>\n            <div style=\"height: 1000px; background: lightgray;\">Section 5</div>\n          </body></html>`,\n        ),\n    );\n\n    // Scroll with large delta\n    await page.scroll(640, 400, 0, 1000);\n\n    // Wait for scroll to complete\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    // Should scroll significantly\n    const scrollY = await page.evaluate(() => window.scrollY);\n    expect(scrollY).toBeGreaterThan(500);\n  });\n\n  test(\"negative deltaY scrolls up\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 2000px;\">\n            <div style=\"height: 500px; background: lightblue;\">Top</div>\n            <div style=\"height: 500px; background: lightgreen;\">Middle 1</div>\n            <div style=\"height: 500px; background: lightyellow;\">Middle 2</div>\n            <div style=\"height: 500px; background: lightcoral;\">Bottom</div>\n          </body></html>`,\n        ),\n    );\n\n    // First scroll down\n    await page.scroll(640, 400, 0, 500);\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    let scrollY = await page.evaluate(() => window.scrollY);\n    const scrolledDown = scrollY;\n    expect(scrolledDown).toBeGreaterThan(0);\n\n    // Now scroll up (negative delta)\n    await page.scroll(640, 400, 0, -300);\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    scrollY = await page.evaluate(() => window.scrollY);\n    expect(scrollY).toBeLessThan(scrolledDown);\n  });\n\n  test(\"scroll returns xpath when requested\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 2000px; margin: 0; padding: 0;\">\n            <div id=\"target\" style=\"position: absolute; top: 0px; left: 400px; width: 300px; height: 100px; background: blue;\">\n              Target element\n            </div>\n            <p style=\"position: absolute; top: 200px; left: 0px;\">Content below</p>\n          </body></html>`,\n        ),\n    );\n\n    // Scroll at coordinate (550, 50) which should be directly over the target div\n    // div spans: left 400-700px, top 0-100px\n    // coordinate 550,50 is within that range\n    const xpath = await page.scroll(550, 50, 0, 200, { returnXpath: true });\n\n    // Should return a non-empty xpath string for the element at that coordinate\n    expect(typeof xpath).toBe(\"string\");\n    expect(xpath.length).toBeGreaterThan(0);\n    // Xpath should reference the div or contain \"target\"\n    expect(xpath.toLowerCase()).toMatch(/div|target/);\n  });\n\n  test(\"scroll without returnXpath returns empty string\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 2000px;\">\n            <div style=\"height: 500px; background: lightblue;\">Content</div>\n          </body></html>`,\n        ),\n    );\n\n    // Scroll without returnXpath\n    const result = await page.scroll(640, 400, 0, 200);\n\n    // Should return empty string\n    expect(result).toBe(\"\");\n  });\n\n  test(\"multiple sequential scrolls accumulate\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          `<!doctype html><html><body style=\"height: 3000px;\">\n            <div style=\"height: 750px; background: lightblue;\">Section 1</div>\n            <div style=\"height: 750px; background: lightgreen;\">Section 2</div>\n            <div style=\"height: 750px; background: lightyellow;\">Section 3</div>\n            <div style=\"height: 750px; background: lightcoral;\">Section 4</div>\n          </body></html>`,\n        ),\n    );\n\n    // First scroll\n    await page.scroll(640, 400, 0, 200);\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    const after1 = await page.evaluate(() => window.scrollY);\n    expect(after1).toBeGreaterThan(0);\n\n    // Second scroll\n    await page.scroll(640, 400, 0, 200);\n    await page.evaluate(() => new Promise((r) => setTimeout(r, 200)));\n\n    const after2 = await page.evaluate(() => window.scrollY);\n\n    expect(after2).toBeGreaterThan(after1);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/page-send-cdp.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\ntest.describe(\"Page sendCDP method\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n  });\n\n  test(\"sends CDP commands and requires domain to be enabled first\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n\n    // Try to add a virtual authenticator without enabling WebAuthn first\n    // This should fail because the domain needs to be enabled\n    await expect(\n      page.sendCDP(\"WebAuthn.addVirtualAuthenticator\", {\n        options: {\n          protocol: \"ctap2\",\n          transport: \"usb\",\n          hasResidentKey: false,\n          hasUserVerification: false,\n          isUserVerified: false,\n        },\n      }),\n    ).rejects.toThrow();\n\n    // Enable the WebAuthn domain\n    await page.sendCDP(\"WebAuthn.enable\");\n\n    // Now adding a virtual authenticator should succeed\n    const result = await page.sendCDP<{ authenticatorId: string }>(\n      \"WebAuthn.addVirtualAuthenticator\",\n      {\n        options: {\n          protocol: \"ctap2\",\n          transport: \"usb\",\n          hasResidentKey: false,\n          hasUserVerification: false,\n          isUserVerified: false,\n        },\n      },\n    );\n\n    // Verify we got an authenticator ID back\n    expect(result).toHaveProperty(\"authenticatorId\");\n    expect(typeof result.authenticatorId).toBe(\"string\");\n    expect(result.authenticatorId.length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/perform-understudy-method.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { performUnderstudyMethod } from \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"tests performUnderstudyMethod\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"tests that clicking works\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/no-js-click/\",\n    );\n\n    await performUnderstudyMethod(\n      page,\n      page.mainFrame(),\n      \"click\",\n      \"/html/body/button\",\n      [],\n      30000,\n    );\n\n    const isVisible = await page.locator(\"#success-msg\").isVisible();\n    expect(isVisible).toBe(true);\n  });\n\n  test(\"fill sets input value\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/login/\",\n    );\n\n    await performUnderstudyMethod(\n      page,\n      page.mainFrame(),\n      \"fill\",\n      \"/html/body/main/form/div[1]/input\",\n      [\"Alice\"],\n      30000,\n    );\n\n    const textContent = await page\n      .locator(\"/html/body/main/form/div[1]/input\")\n      .inputValue();\n    expect(textContent).toBe(\"Alice\");\n  });\n\n  test(\"tests that key presses work\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/key-press/\",\n    );\n\n    await performUnderstudyMethod(\n      page,\n      page.mainFrame(),\n      \"press\",\n      \"xpath=/html\",\n      [\"Enter\"],\n      30000,\n    );\n\n    const textContent = await page\n      .locator(\"/html/body/div/div/h1\")\n      .textContent();\n    expect(textContent).toContain(\"Enter\");\n  });\n\n  test(\"tests select option from a dropdown\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/nested-dropdown/\",\n    );\n\n    await performUnderstudyMethod(\n      page,\n      page.mainFrame(),\n      \"selectOptionFromDropdown\",\n      \"xpath=//*[@id='licenseType']\",\n      [\"Smog Check Technician\"],\n      30000,\n    );\n\n    const inputValue = await page\n      .locator(\"#licenseType >> option:checked\")\n      .textContent();\n    expect(inputValue).toBe(\"Smog Check Technician\");\n  });\n\n  test(\"tests drag & drop works (start xpath & end xpath)\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/drag-drop/\",\n    );\n\n    await performUnderstudyMethod(\n      page,\n      page.mainFrame(),\n      \"dragAndDrop\",\n      \"xpath=/html/body/div/section[1]/div[1]/div[1]\", // start xpath\n      [\"/html/body/div/section[2]/div/div[1]\"], // end xpath\n      30000,\n    );\n\n    const droppedContent = await page\n      .locator(\"/html/body/div/section[2]/div/div[1]/div\")\n      .textContent();\n    expect(droppedContent).toBe(\"TEXT: Hello from text\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/setinputfiles.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { Buffer } from \"buffer\";\nimport { promises as fs } from \"fs\";\nimport path from \"path\";\nimport crypto from \"crypto\";\nimport type { Page as V3Page } from \"../../lib/v3/understudy/page.js\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\n\nconst FILE_UPLOAD_IFRAME_URL =\n  \"https://browserbase.github.io/stagehand-eval-sites/sites/file-uploads-iframe/\";\nconst FILE_UPLOAD_V2_URL =\n  \"https://browserbase.github.io/stagehand-eval-sites/sites/file-uploads-2/\";\n\nconst RESUME_INPUT = \"#resumeUpload\";\nconst RESUME_SUCCESS = \"#resumeSuccess\";\nconst IMAGES_INPUT = \"#imagesUpload\";\nconst IMAGES_SUCCESS = \"#imagesSuccess\";\nconst AUDIO_INPUT = \"#audioUpload\";\nconst AUDIO_SUCCESS = \"#audioSuccess\";\nconst IFRAME_UPLOAD_INPUT = \"/html/body/div/iframe/html/body/div/div[1]/input\";\nconst IFRAME_SUCCESS =\n  \"body > div > iframe >> html > body > div > div:nth-of-type(2)\";\n\ntest.describe(\"tests setInputFiles()\", () => {\n  let v3: V3;\n  const fixtures: string[] = [];\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3TestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n    await Promise.all(\n      fixtures.splice(0).map((file) => fs.unlink(file).catch(() => {})),\n    );\n  });\n\n  const createFixture = async (\n    namePrefix: string,\n    contents: string,\n    ext = \".txt\",\n  ): Promise<string> => {\n    const normalizedExt = ext.startsWith(\".\") ? ext : `.${ext}`;\n    const filename = `${namePrefix}-${crypto.randomBytes(4).toString(\"hex\")}${normalizedExt}`;\n    const filePath = path.resolve(process.cwd(), filename);\n    await fs.writeFile(filePath, contents, \"utf-8\");\n    fixtures.push(filePath);\n    return filePath;\n  };\n\n  const expectUploadSuccess = async (\n    page: V3Page,\n    successSelector: string,\n    expectedText: string,\n  ) => {\n    await expect\n      .poll(\n        () =>\n          page.evaluate((selector) => {\n            const el = document.querySelector(selector);\n            if (!el) return \"\";\n            const display = window.getComputedStyle(el).display;\n            if (display === \"none\") return \"\";\n            return el.textContent ?? \"\";\n          }, successSelector),\n        { message: `wait for success message at ${successSelector}` },\n      )\n      .toContain(expectedText);\n  };\n\n  const getInputFileCount = async (page: V3Page, inputSelector: string) => {\n    return await page.evaluate((selector) => {\n      const el = document.querySelector(selector);\n      if (!(el instanceof HTMLInputElement)) return 0;\n      return el.files?.length ?? 0;\n    }, inputSelector);\n  };\n\n  const expectFileCount = async (\n    page: V3Page,\n    inputSelector: string,\n    expected: number,\n  ) => {\n    await expect\n      .poll(() => getInputFileCount(page, inputSelector), {\n        message: `wait for file count on ${inputSelector}`,\n      })\n      .toBe(expected);\n  };\n\n  test(\"deepLocator uploads and validates within iframe\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(FILE_UPLOAD_IFRAME_URL);\n    const fixture = await createFixture(\n      \"iframe-upload\",\n      \"<p>iframe upload</p>\",\n      \".txt\",\n    );\n    await page\n      .deepLocator(IFRAME_UPLOAD_INPUT)\n      .setInputFiles(path.relative(process.cwd(), fixture));\n\n    const successLocator = page.deepLocator(IFRAME_SUCCESS);\n    await expect\n      .poll(async () => (await successLocator.textContent()) ?? \"\", {\n        message: \"wait for iframe upload success\",\n      })\n      .toContain(\"file uploaded successfully\");\n  });\n\n  test(\"locator uploads resume via relative path string\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(FILE_UPLOAD_V2_URL);\n    const fixture = await createFixture(\"resume\", \"<p>resume</p>\", \".pdf\");\n    await page\n      .locator(RESUME_INPUT)\n      .setInputFiles(path.relative(process.cwd(), fixture));\n    await expectUploadSuccess(page, RESUME_SUCCESS, \"Resume uploaded!\");\n    await expectFileCount(page, RESUME_INPUT, 1);\n  });\n\n  test(\"locator uploads multiple images via absolute paths\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(FILE_UPLOAD_V2_URL);\n    const first = await createFixture(\"image-a\", \"<p>A</p>\", \".png\");\n    const second = await createFixture(\"image-b\", \"<p>B</p>\", \".jpeg\");\n    await page.locator(IMAGES_INPUT).setInputFiles([first, second]);\n    await expectUploadSuccess(page, IMAGES_SUCCESS, \"Images uploaded!\");\n    await expectFileCount(page, IMAGES_INPUT, 2);\n  });\n\n  test(\"locator uploads audio via payload object\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(FILE_UPLOAD_V2_URL);\n    await page.locator(AUDIO_INPUT).setInputFiles({\n      name: \"voice-sample.mp3\",\n      mimeType: \"audio/mpeg\",\n      buffer: Buffer.from(\"fake audio bytes\", \"utf-8\"),\n    });\n    await expectUploadSuccess(page, AUDIO_SUCCESS, \"Audio file uploaded!\");\n    await expectFileCount(page, AUDIO_INPUT, 1);\n  });\n\n  test(\"locator uploads multiple payload objects to images input\", async () => {\n    const page = v3.context.pages()[0];\n    await page.goto(FILE_UPLOAD_V2_URL);\n    await page.locator(IMAGES_INPUT).setInputFiles([\n      {\n        name: \"payload-a.png\",\n        mimeType: \"image/png\",\n        buffer: Buffer.from(\"payload-a\", \"utf-8\"),\n      },\n      {\n        name: \"payload-b.png\",\n        mimeType: \"image/png\",\n        buffer: Buffer.from(\"payload-b\", \"utf-8\"),\n      },\n    ]);\n    await expectUploadSuccess(page, IMAGES_SUCCESS, \"Images uploaded!\");\n    await expectFileCount(page, IMAGES_INPUT, 2);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/shadow-iframe-oopif.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport puppeteer from \"puppeteer-core\";\nimport { chromium as playwrightChromium } from \"playwright-core\";\nimport { chromium as patchrightChromium } from \"patchright-core\";\nimport { Action } from \"../../lib/v3/types/public/methods.js\";\nimport { AnyPage } from \"../../lib/v3/types/public/page.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\n/**\n * IMPORTANT:\n * - We create a single V3 instance/test to avoid cross-test state. Increase parallelism later if needed.\n * - We assert an *effect* when feasible (e.g. input value). For pure clicks we assert no thrown error.\n */\n\ntype Case = {\n  title: string;\n  url: string;\n  action: Action;\n  expectedSubstrings: string[]; // check v3.extract().pageText contains these\n};\n\ntype Framework = \"v3\" | \"puppeteer\" | \"playwright\" | \"patchright\";\n\nasync function runCase(v3: V3, c: Case, framework: Framework): Promise<void> {\n  let cleanup: (() => Promise<void> | void) | null = null;\n\n  // Acquire the correct page for the requested framework\n  let page: AnyPage | undefined;\n  switch (framework) {\n    case \"v3\": {\n      const v3Page = v3.context.pages()[0];\n      await v3Page.goto(c.url, { waitUntil: \"networkidle\" });\n      page = v3Page;\n      break;\n    }\n    case \"puppeteer\": {\n      const browser = await puppeteer.connect({\n        browserWSEndpoint: v3.connectURL(),\n        defaultViewport: null,\n      });\n      const pages = await browser.pages();\n      const puppeteerPage = pages[0];\n      await puppeteerPage.goto(c.url, { waitUntil: \"networkidle0\" });\n      page = puppeteerPage;\n      cleanup = async () => {\n        try {\n          await browser.close();\n        } catch {\n          //\n        }\n      };\n      break;\n    }\n    case \"playwright\": {\n      const pwBrowser = await playwrightChromium.connectOverCDP(\n        v3.connectURL(),\n      );\n      const pwContext = pwBrowser.contexts()[0];\n      const pwPage = pwContext.pages()[0];\n      await pwPage.goto(c.url, { waitUntil: \"networkidle\" as never });\n      page = pwPage as unknown as AnyPage;\n      cleanup = async () => {\n        try {\n          await pwBrowser.close();\n        } catch {\n          // ignore\n        }\n      };\n      break;\n    }\n    case \"patchright\": {\n      const prBrowser = await patchrightChromium.connectOverCDP(\n        v3.connectURL(),\n      );\n      const prContext = prBrowser.contexts()[0];\n      const prPage = prContext.pages()[0];\n      await prPage.goto(c.url, { waitUntil: \"networkidle\" as never });\n      page = prPage as unknown as AnyPage;\n      cleanup = async () => {\n        try {\n          await prBrowser.close();\n        } catch {\n          // ignore\n        }\n      };\n      break;\n    }\n  }\n\n  try {\n    if (!page) throw new Error(\"Missing page for selected framework\");\n    await v3.act(c.action, { page });\n    // Post-action extraction; verify expected text appears\n    const extraction = await v3.extract({ page });\n    const text = extraction.pageText ?? \"\";\n    for (const s of c.expectedSubstrings) {\n      expect(\n        text.includes(s),\n        `expected pageText to include substring: ${s}`,\n      ).toBeTruthy();\n    }\n  } finally {\n    await cleanup?.();\n  }\n}\n\nconst cases: Case[] = [\n  {\n    title: \"Closed shadow root inside OOPIF\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-root-in-oopif/\",\n    action: {\n      selector:\n        \"xpath=/html/body/main/section/iframe/html/body/shadow-demo//div/button\",\n      method: \"click\",\n      arguments: [\"\"],\n      description: \"click button inside closed shadow root in OOPIF\",\n    },\n    expectedSubstrings: [\"button successfully clicked\"],\n  },\n  {\n    title: \"Open shadow root inside OOPIF\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/open-shadow-root-in-oopif/\",\n    action: {\n      selector:\n        \"xpath=/html/body/main/section/iframe/html/body/shadow-demo//div/button\",\n      method: \"click\",\n      arguments: [\"\"],\n      description: \"\",\n    },\n    expectedSubstrings: [\"button successfully clicked\"],\n  },\n  {\n    title: \"OOPIF inside open shadow root\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-open-shadow-dom/\",\n    action: {\n      selector:\n        \"xpath=/html/body/shadow-host//section/iframe/html/body/main/section[1]/form/div/div[1]/input\",\n      method: \"fill\",\n      arguments: [\"nunya\"],\n      description: \"\",\n    },\n    expectedSubstrings: [\"nunya\"],\n  },\n  {\n    title: \"OOPIF inside closed shadow root\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-closed-shadow-dom/\",\n    action: {\n      selector:\n        \"xpath=/html/body/shadow-host//section/iframe/html/body/main/section[1]/form/div/div[1]/input\",\n      method: \"fill\",\n      arguments: [\"nunya\"],\n      description: \"fill input inside OOPIF\",\n    },\n    expectedSubstrings: [\"nunya\"],\n  },\n];\n\ntest.describe\n  .parallel(\"Stagehand v3: shadow <-> iframe OOPIF scenarios\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  const frameworks: Framework[] = [\n    \"v3\",\n    \"playwright\",\n    \"puppeteer\",\n    \"patchright\",\n  ];\n  for (const fw of frameworks) {\n    for (const c of cases) {\n      test(`[${fw}] ${c.title}`, async () => {\n        await runCase(v3, c, fw);\n      });\n    }\n  }\n});\n"
  },
  {
    "path": "packages/core/tests/integration/shadow-iframe-spif.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport puppeteer from \"puppeteer-core\";\nimport { chromium as playwrightChromium } from \"playwright-core\";\nimport { chromium as patchrightChromium } from \"patchright-core\";\nimport { Action } from \"../../lib/v3/types/public/methods.js\";\nimport { AnyPage } from \"../../lib/v3/types/public/page.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\n/**\n * IMPORTANT:\n * - We create a single V3 instance/test to avoid cross-test state. Increase parallelism later if needed.\n * - We assert an *effect* when feasible (e.g. input value). For pure clicks we assert no thrown error.\n */\n\ntype Case = {\n  title: string;\n  url: string;\n  action: Action;\n  expectedSubstrings: string[]; // check v3.extract().pageText contains these\n};\n\ntype Framework = \"v3\" | \"puppeteer\" | \"playwright\" | \"patchright\";\n\nasync function runCase(v3: V3, c: Case, framework: Framework): Promise<void> {\n  let cleanup: (() => Promise<void> | void) | null = null;\n\n  // Acquire the correct page for the requested framework\n  let page: AnyPage | undefined;\n  switch (framework) {\n    case \"v3\": {\n      const v3Page = v3.context.pages()[0];\n      await v3Page.goto(c.url, { waitUntil: \"networkidle\" });\n      page = v3Page;\n      break;\n    }\n    case \"puppeteer\": {\n      const browser = await puppeteer.connect({\n        browserWSEndpoint: v3.connectURL(),\n        defaultViewport: null,\n      });\n      const pages = await browser.pages();\n      const puppeteerPage = pages[0];\n      await puppeteerPage.goto(c.url, { waitUntil: \"networkidle0\" });\n      page = puppeteerPage;\n      cleanup = async () => {\n        try {\n          await browser.close();\n        } catch {\n          //\n        }\n      };\n      break;\n    }\n    case \"playwright\": {\n      const pwBrowser = await playwrightChromium.connectOverCDP(\n        v3.connectURL(),\n      );\n      const pwContext = pwBrowser.contexts()[0];\n      const pwPage = pwContext.pages()[0];\n      await pwPage.goto(c.url, { waitUntil: \"networkidle\" as never });\n      page = pwPage as unknown as AnyPage;\n      cleanup = async () => {\n        try {\n          await pwBrowser.close();\n        } catch {\n          // ignore\n        }\n      };\n      break;\n    }\n    case \"patchright\": {\n      const prBrowser = await patchrightChromium.connectOverCDP(\n        v3.connectURL(),\n      );\n      const prContext = prBrowser.contexts()[0];\n      const prPage = prContext.pages()[0];\n      await prPage.goto(c.url, { waitUntil: \"networkidle\" as never });\n      page = prPage as unknown as AnyPage;\n      cleanup = async () => {\n        try {\n          await prBrowser.close();\n        } catch {\n          // ignore\n        }\n      };\n      break;\n    }\n  }\n\n  try {\n    if (!page) throw new Error(\"Missing page for selected framework\");\n    await v3.act(c.action, { page });\n    // Post-action extraction; verify expected text appears\n    const extraction = await v3.extract({ page });\n    const text = extraction.pageText ?? \"\";\n    for (const s of c.expectedSubstrings) {\n      expect(\n        text.includes(s),\n        `expected pageText to include substring: ${s}`,\n      ).toBeTruthy();\n    }\n  } finally {\n    await cleanup?.();\n  }\n}\n\nconst cases: Case[] = [\n  {\n    title: \"Open shadow root inside SPIF\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/open-shadow-root-in-spif/\",\n    action: {\n      selector:\n        \"xpath=/html/body/main/section/iframe/html/body/shadow-demo//div/button\",\n      method: \"click\",\n      arguments: [\"\"],\n      description: \"\",\n    },\n    expectedSubstrings: [\"button successfully clicked\"],\n  },\n  {\n    title: \"Closed shadow root inside SPIF\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-dom-in-spif/\",\n    action: {\n      selector: \"xpath=/html/body/div/iframe/html/body/shadow-demo//div/button\",\n      method: \"click\",\n      arguments: [\"\"],\n      description: \"\",\n    },\n    expectedSubstrings: [\"button successfully clicked\"],\n  },\n  {\n    title: \"SPIF inside closed shadow root\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-closed-shadow-dom/\",\n    action: {\n      selector: \"xpath=/html/body/shadow-host//div/iframe/html/body/button\",\n      method: \"click\",\n      arguments: [\"\"],\n      description: \"\",\n    },\n    expectedSubstrings: [\"button successfully clicked\"],\n  },\n  {\n    title: \"SPIF inside open shadow root\",\n    url: \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-open-shadow-dom/\",\n    action: {\n      selector: \"xpath=/html/body/shadow-host//div/iframe/html/body/button\",\n      method: \"click\",\n      arguments: [\"\"],\n      description: \"click button inside SPIF under open shadow\",\n    },\n    expectedSubstrings: [\"button successfully clicked\"],\n  },\n];\n\ntest.describe.parallel(\"Stagehand v3: shadow <-> iframe SPIF scenarios\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  const frameworks: Framework[] = [\n    \"v3\",\n    \"playwright\",\n    \"puppeteer\",\n    \"patchright\",\n  ];\n  for (const fw of frameworks) {\n    for (const c of cases) {\n      test(`[${fw}] ${c.title}`, async () => {\n        await runCase(v3, c, fw);\n      });\n    }\n  }\n});\n"
  },
  {
    "path": "packages/core/tests/integration/testUtils.ts",
    "content": "import type { V3 } from \"../../lib/v3/v3.js\";\nimport type {\n  LanguageModelV2,\n  LanguageModelV2CallOptions,\n  LanguageModelV2Content,\n  LanguageModelV2FinishReason,\n  LanguageModelV2Usage,\n} from \"@ai-sdk/provider\";\nimport { AISdkClient } from \"../../lib/v3/llm/aisdk.js\";\n\n/**\n * Races a promise against a timeout.\n * Resolves to the promise value or \"timeout\" if the deadline expires.\n */\nexport function raceTimeout<T>(\n  promise: Promise<T>,\n  ms: number,\n): Promise<T | \"timeout\"> {\n  let timer: ReturnType<typeof setTimeout>;\n  const timeout = new Promise<\"timeout\">((resolve) => {\n    timer = setTimeout(() => resolve(\"timeout\"), ms);\n  });\n  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));\n}\n\nconst CLOSE_TIMEOUT_MS = 5_000;\n\nasync function settleWithTimeout(\n  promise: Promise<unknown>,\n  timeoutMs: number,\n): Promise<void> {\n  let timeoutId: NodeJS.Timeout | undefined;\n  const timeout = new Promise<void>((resolve) => {\n    timeoutId = setTimeout(resolve, timeoutMs);\n  });\n  try {\n    await Promise.race([promise.catch(() => {}), timeout]);\n  } finally {\n    if (timeoutId) clearTimeout(timeoutId);\n  }\n}\n\nexport async function closeV3(v3?: V3 | null): Promise<void> {\n  if (!v3) return;\n  const isBrowserbase = v3.isBrowserbase;\n  if (isBrowserbase) {\n    try {\n      await settleWithTimeout(\n        v3.context.conn.send(\"Browser.close\"),\n        CLOSE_TIMEOUT_MS,\n      );\n    } catch {\n      // best-effort cleanup\n    }\n  }\n\n  await settleWithTimeout(v3.close(), CLOSE_TIMEOUT_MS);\n}\n\ntype JsonResponseKey =\n  | \"act\"\n  | \"Observation\"\n  | \"Metadata\"\n  | \"Extraction\"\n  | \"default\";\n\ntype JsonResponseValue =\n  | Record<string, unknown>\n  | ((options: LanguageModelV2CallOptions) => Record<string, unknown>);\n\ntype JsonResponseScript = JsonResponseValue | JsonResponseValue[];\n\ntype GenerateResponseValue =\n  | {\n      content: LanguageModelV2Content[];\n      finishReason?: LanguageModelV2FinishReason;\n      usage?: Partial<LanguageModelV2Usage>;\n    }\n  | ((options: LanguageModelV2CallOptions) => {\n      content: LanguageModelV2Content[];\n      finishReason?: LanguageModelV2FinishReason;\n      usage?: Partial<LanguageModelV2Usage>;\n    });\n\ntype ScriptedLanguageModel = LanguageModelV2 & {\n  doGenerateCalls: LanguageModelV2CallOptions[];\n};\n\ntype ScriptedGenerateResult = {\n  content: LanguageModelV2Content[];\n  finishReason?: LanguageModelV2FinishReason;\n  usage?: Partial<LanguageModelV2Usage>;\n};\n\nconst DEFAULT_USAGE: LanguageModelV2Usage = {\n  inputTokens: 1,\n  outputTokens: 1,\n  totalTokens: 2,\n  reasoningTokens: 0,\n  cachedInputTokens: 0,\n};\n\nconst mergeUsage = (\n  usage?: Partial<LanguageModelV2Usage>,\n): LanguageModelV2Usage => ({\n  ...DEFAULT_USAGE,\n  ...(usage ?? {}),\n});\n\nfunction consumeScriptValue<T>(value: T | T[] | undefined, fallback: T): T {\n  if (!Array.isArray(value)) {\n    return value ?? fallback;\n  }\n\n  if (value.length <= 1) {\n    return value[0] ?? fallback;\n  }\n\n  return value.shift() ?? fallback;\n}\n\nfunction resolveJsonResponseKey(\n  options: LanguageModelV2CallOptions,\n): JsonResponseKey {\n  const responseFormat = options.responseFormat;\n  if (!responseFormat || responseFormat.type !== \"json\") {\n    return \"default\";\n  }\n\n  const schema = responseFormat.schema as {\n    type?: string;\n    properties?: Record<string, unknown>;\n  };\n  const properties = schema?.properties ?? {};\n\n  if (\"elementId\" in properties && \"twoStep\" in properties) {\n    return \"act\";\n  }\n\n  if (\"elements\" in properties) {\n    return \"Observation\";\n  }\n\n  if (\"completed\" in properties && \"progress\" in properties) {\n    return \"Metadata\";\n  }\n\n  return \"Extraction\";\n}\n\nexport function promptToText(\n  prompt: LanguageModelV2CallOptions[\"prompt\"],\n): string {\n  return (prompt ?? [])\n    .flatMap((message) => {\n      if (typeof message.content === \"string\") {\n        return [message.content];\n      }\n\n      return (message.content ?? [])\n        .map((part) => (part.type === \"text\" ? part.text : \"\"))\n        .filter((text): text is string => text.length > 0);\n    })\n    .join(\"\\n\");\n}\n\nfunction findEncodedIds(options: LanguageModelV2CallOptions): string[] {\n  return [...promptToText(options.prompt).matchAll(/\\b\\d+-\\d+\\b/g)].map(\n    (match) => match[0],\n  );\n}\n\nexport function findEncodedIdForText(\n  options: LanguageModelV2CallOptions,\n  text: string,\n): string {\n  const promptText = promptToText(options.prompt);\n  const lines = promptText.split(\"\\n\");\n  const line = lines.find((entry) => entry.includes(text));\n  const match = line?.match(/\\b\\d+-\\d+\\b/);\n\n  if (!match) {\n    throw new Error(`Could not find encoded id for text: ${text}`);\n  }\n\n  return match[0];\n}\n\nexport function findLastEncodedId(options: LanguageModelV2CallOptions): string {\n  const matches = findEncodedIds(options);\n  if (matches.length === 0) {\n    throw new Error(\"Could not find any encoded ids in the prompt.\");\n  }\n\n  return matches[matches.length - 1];\n}\n\nexport function toolCallResponse(\n  toolName: string,\n  input: Record<string, unknown>,\n  toolCallId = `${toolName}-1`,\n): {\n  content: LanguageModelV2Content[];\n  finishReason: LanguageModelV2FinishReason;\n  usage: LanguageModelV2Usage;\n} {\n  return {\n    content: [\n      {\n        type: \"tool-call\",\n        toolCallId,\n        toolName,\n        input: JSON.stringify(input),\n      },\n    ],\n    finishReason: \"tool-calls\",\n    usage: DEFAULT_USAGE,\n  };\n}\n\nexport function doneToolResponse(\n  reasoning = \"done\",\n  taskComplete = true,\n  toolCallId = \"done-1\",\n): {\n  content: LanguageModelV2Content[];\n  finishReason: LanguageModelV2FinishReason;\n  usage: LanguageModelV2Usage;\n} {\n  return toolCallResponse(\"done\", { reasoning, taskComplete }, toolCallId);\n}\n\nfunction createGenerateResult(result: ScriptedGenerateResult): {\n  content: LanguageModelV2Content[];\n  finishReason: LanguageModelV2FinishReason;\n  usage: LanguageModelV2Usage;\n  warnings: [];\n} {\n  return {\n    content: result.content,\n    finishReason: result.finishReason ?? \"stop\",\n    usage: mergeUsage(result.usage),\n    warnings: [],\n  };\n}\n\nexport function createScriptedAisdkTestLlmClient(options?: {\n  modelId?: string;\n  jsonResponses?: Partial<Record<JsonResponseKey, JsonResponseScript>>;\n  generateResponses?: GenerateResponseValue[];\n}): AISdkClient {\n  const jsonResponses = Object.fromEntries(\n    Object.entries(options?.jsonResponses ?? {}).map(([key, value]) => [\n      key,\n      Array.isArray(value) ? [...value] : value,\n    ]),\n  ) as Partial<Record<JsonResponseKey, JsonResponseScript>>;\n  const generateResponses = [...(options?.generateResponses ?? [])];\n\n  const model: ScriptedLanguageModel = {\n    provider: \"mock\",\n    modelId: options?.modelId ?? \"mock/stagehand-flow-logger\",\n    specificationVersion: \"v2\",\n    supportedUrls: {},\n    doGenerateCalls: [],\n    doGenerate: async (callOptions) => {\n      model.doGenerateCalls.push(callOptions);\n\n      if (callOptions.responseFormat?.type === \"json\") {\n        const key = resolveJsonResponseKey(callOptions);\n        const responseScripts = consumeScriptValue<\n          JsonResponseScript | undefined\n        >(jsonResponses[key], jsonResponses.default);\n        const responseScript = consumeScriptValue<\n          JsonResponseValue | undefined\n        >(responseScripts, undefined);\n        const response =\n          typeof responseScript === \"function\"\n            ? responseScript(callOptions)\n            : (responseScript ?? {});\n\n        return createGenerateResult({\n          content: [{ type: \"text\", text: JSON.stringify(response) }],\n        });\n      }\n\n      const responseScript = consumeScriptValue<\n        GenerateResponseValue | undefined\n      >(generateResponses, undefined);\n\n      if (!responseScript) {\n        return createGenerateResult({\n          content: [{ type: \"text\", text: \"done\" }],\n        });\n      }\n\n      const response =\n        typeof responseScript === \"function\"\n          ? responseScript(callOptions)\n          : responseScript;\n\n      return createGenerateResult(response);\n    },\n    doStream: async () => {\n      throw new Error(\"Streaming is not implemented for this test model.\");\n    },\n  };\n\n  return new AISdkClient({ model });\n}\n"
  },
  {
    "path": "packages/core/tests/integration/text-selector-innermost.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { Protocol } from \"devtools-protocol\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"Text selector innermost element matching\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"text selector matches only innermost elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <div id=\"outer\">\n          <span id=\"middle\">\n            <button id=\"inner\">Click me</button>\n          </span>\n        </div>\n      `),\n    );\n\n    // Only the button should be counted, not the parent elements\n    const count = await page.mainFrame().locator(\"text=Click me\").count();\n    expect(count).toBe(1);\n\n    // Verify it finds the button element specifically\n    const session = page.mainFrame().session;\n    const { executionContextId } = await session.send<{\n      executionContextId: number;\n    }>(\"Page.createIsolatedWorld\", {\n      frameId: page.mainFrame().frameId,\n      worldName: \"test-world\",\n    });\n\n    const evalRes = await session.send<Protocol.Runtime.EvaluateResponse>(\n      \"Runtime.evaluate\",\n      {\n        expression: `(() => {\n          const candidates = [];\n          const iter = document.createNodeIterator(document.documentElement, NodeFilter.SHOW_ELEMENT);\n          let n;\n          while ((n = iter.nextNode())) {\n            const el = n;\n            const t = (el.innerText ?? el.textContent ?? '').trim();\n            if (t && t.includes(\"Click me\")) {\n              candidates.push(el);\n            }\n          }\n          \n          // Find innermost\n          for (const candidate of candidates) {\n            let isInnermost = true;\n            for (const other of candidates) {\n              if (candidate !== other && candidate.contains(other)) {\n                isInnermost = false;\n                break;\n              }\n            }\n            if (isInnermost) return candidate.id;\n          }\n          return null;\n        })()`,\n        contextId: executionContextId,\n        returnByValue: true,\n      },\n    );\n\n    expect(evalRes.result.value).toBe(\"inner\");\n  });\n\n  test(\"multiple innermost elements with same text\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <div>\n          <button>Submit</button>\n          <span>Some other content</span>\n          <button>Submit</button>\n        </div>\n        <div>\n          <a href=\"#\">Submit</a>\n        </div>\n      `),\n    );\n\n    // Should find all three innermost elements (2 buttons + 1 link)\n    const count = await page.mainFrame().locator(\"text=Submit\").count();\n    expect(count).toBe(3);\n  });\n\n  test(\"nested text with different innermost elements\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(`\n        <div id=\"parent\">\n          Hello <span id=\"child\">World</span>\n        </div>\n      `),\n    );\n\n    // \"Hello\" is only in the parent div\n    const helloCount = await page.mainFrame().locator(\"text=Hello\").count();\n    expect(helloCount).toBe(1); // Only the div\n\n    // \"World\" is only in the span\n    const worldCount = await page.mainFrame().locator(\"text=World\").count();\n    expect(worldCount).toBe(1); // Only the span\n\n    // \"Hello World\" matches only the parent div (as it's the innermost containing both words)\n    const bothCount = await page\n      .mainFrame()\n      .locator(\"text=Hello World\")\n      .count();\n    expect(bothCount).toBe(1); // Only the div\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/timeouts.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { z } from \"zod\";\nimport { closeV3 } from \"./testUtils.js\";\nimport type { LLMClient } from \"../../lib/v3/llm/LLMClient.js\";\nimport { generateText } from \"ai\";\n\ntype AgentToolNameWithTimeout =\n  | \"act\"\n  | \"extract\"\n  | \"fillForm\"\n  | \"ariaTree\"\n  | \"click\"\n  | \"type\"\n  | \"dragAndDrop\"\n  | \"clickAndHold\"\n  | \"fillFormVision\"\n  | \"goto\"\n  | \"navback\"\n  | \"screenshot\"\n  | \"scroll\"\n  | \"keys\";\n\ntype ToolTimeoutTestModel = {\n  provider: string;\n  modelId: string;\n  specificationVersion: \"v2\";\n  supportedUrls: Record<string, RegExp[]>;\n  doGenerate: () => Promise<{\n    content: Array<{\n      type: \"tool-call\";\n      toolCallId: string;\n      toolName: string;\n      input: string;\n    }>;\n    finishReason: \"tool-calls\";\n    usage: { inputTokens: number; outputTokens: number; totalTokens: number };\n    warnings: [];\n  }>;\n  doStream: (_options: unknown) => Promise<never>;\n};\n\ntype ToolTimeoutTestLLMClient = LLMClient & {\n  model: ToolTimeoutTestModel;\n};\n\nfunction createToolTimeoutTestLlmClient(\n  toolName: AgentToolNameWithTimeout,\n  toolInput: Record<string, unknown>,\n): ToolTimeoutTestLLMClient {\n  const usage = {\n    prompt_tokens: 0,\n    completion_tokens: 0,\n    reasoning_tokens: 0,\n    cached_input_tokens: 0,\n    total_tokens: 0,\n  };\n  let generateCallCount = 0;\n\n  const model: ToolTimeoutTestModel = {\n    provider: \"mock\",\n    modelId: \"mock/tool-timeout-test\",\n    specificationVersion: \"v2\",\n    supportedUrls: {},\n    doGenerate: async () => {\n      generateCallCount += 1;\n      if (generateCallCount === 1) {\n        return {\n          content: [\n            {\n              type: \"tool-call\",\n              toolCallId: \"tool-1\",\n              toolName,\n              input: JSON.stringify(toolInput),\n            },\n          ],\n          finishReason: \"tool-calls\",\n          usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },\n          warnings: [],\n        };\n      }\n\n      return {\n        content: [\n          {\n            type: \"tool-call\",\n            toolCallId: \"done-1\",\n            toolName: \"done\",\n            input: JSON.stringify({ reasoning: \"done\", taskComplete: true }),\n          },\n        ],\n        finishReason: \"tool-calls\",\n        usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },\n        warnings: [],\n      };\n    },\n    doStream: async () => {\n      throw new Error(\"doStream not implemented in timeout test model\");\n    },\n  };\n\n  const llm = {\n    type: \"openai\",\n    modelName: \"openai/gpt-4.1-mini\",\n    hasVision: false,\n    clientOptions: {},\n    model,\n    getLanguageModel: () => model,\n    generateText,\n    createChatCompletion: async <T = unknown>(options: unknown): Promise<T> => {\n      const responseModelName = (\n        options as { options?: { response_model?: { name?: string } } }\n      )?.options?.response_model?.name;\n\n      if (responseModelName === \"act\") {\n        return {\n          data: {\n            elementId: \"1-0\",\n            description: \"click body\",\n            method: \"click\",\n            arguments: [],\n            twoStep: false,\n          },\n          usage,\n        } as T;\n      }\n      if (responseModelName === \"Observation\") {\n        return { data: { elements: [] }, usage } as T;\n      }\n      if (responseModelName === \"Extraction\") {\n        return { data: {}, usage } as T;\n      }\n      if (responseModelName === \"Metadata\") {\n        return { data: { completed: true, progress: \"\" }, usage } as T;\n      }\n      return { data: {}, usage } as T;\n    },\n  };\n\n  return llm as unknown as ToolTimeoutTestLLMClient;\n}\n\nfunction findToolOutput(\n  stepEvents: Array<{\n    toolCalls?: Array<{ toolName?: string }>;\n    toolResults?: Array<{ output?: unknown }>;\n  }>,\n  toolName: string,\n) {\n  for (const event of stepEvents) {\n    if (!event.toolCalls || !event.toolResults) continue;\n    const toolIndex = event.toolCalls.findIndex(\n      (tc) => tc.toolName === toolName,\n    );\n    if (toolIndex !== -1) {\n      return event.toolResults[toolIndex]?.output;\n    }\n  }\n  return undefined;\n}\n\nasync function runAgentToolTimeoutScenario(\n  toolName: AgentToolNameWithTimeout,\n  toolInput: Record<string, unknown>,\n  options?: { mode?: \"dom\" | \"hybrid\" },\n) {\n  const llmClient = createToolTimeoutTestLlmClient(toolName, toolInput);\n  const stepEvents: Array<{\n    toolCalls?: Array<{ toolName?: string }>;\n    toolResults?: Array<{ output?: unknown }>;\n  }> = [];\n  const v3 = new V3({\n    ...v3DynamicTestConfig,\n    experimental: true,\n    llmClient,\n  });\n  await v3.init();\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://example.com\");\n    const agent = v3.agent({\n      ...(options?.mode ? { mode: options.mode } : {}),\n    });\n    await agent.execute({\n      instruction: `Use ${toolName} and then finish`,\n      maxSteps: 2,\n      toolTimeout: 1,\n      callbacks: {\n        onStepFinish: (event) => {\n          stepEvents.push({\n            toolCalls: event.toolCalls?.map((tc) => ({\n              toolName: tc.toolName,\n            })),\n            toolResults: event.toolResults?.map((tr) => ({\n              output: tr.output,\n            })),\n          });\n        },\n      },\n    });\n    const toolOutput = findToolOutput(stepEvents, toolName);\n    if (!toolOutput) {\n      throw new Error(`No tool output captured for ${toolName}`);\n    }\n    return { toolOutput };\n  } finally {\n    await closeV3(v3);\n  }\n}\n\ntest.describe(\"V3 hard timeouts\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"observe() enforces timeoutMs\", async () => {\n    // Tiny timeout to force the race to hit the timeout branch\n    await expect(v3.observe(\"find something\", { timeout: 5 })).rejects.toThrow(\n      /timed out/i,\n    );\n  });\n\n  test(\"extract() enforces timeoutMs\", async () => {\n    const schema = z.object({ title: z.string().optional() });\n    await expect(\n      v3.extract(\"Extract title\", schema, { timeout: 5 }),\n    ).rejects.toThrow(/timed out/i);\n  });\n\n  test(\"act() enforces timeoutMs\", async () => {\n    await expect(v3.act(\"do nothing\", { timeout: 5 })).rejects.toThrow(\n      /timed out/i,\n    );\n  });\n\n  test(\"agent toolTimeout enforces timeout for act tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"act\", {\n      action: \"click somewhere\",\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for extract tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"extract\", {\n      instruction: \"extract the page title\",\n      schema: { type: \"object\", properties: { title: { type: \"string\" } } },\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for fillForm tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"fillForm\", {\n      fields: [{ action: \"type hello into name\" }],\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for ariaTree\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"ariaTree\", {});\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for goto tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"goto\", {\n      url: \"https://example.com/slow\",\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for navback tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"navback\", {\n      reasoningText: \"going back\",\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for screenshot tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"screenshot\", {});\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for scroll tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"scroll\", {\n      direction: \"down\",\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for keys tool\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\"keys\", {\n      method: \"press\",\n      value: \"Enter\",\n    });\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for click tool (hybrid)\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\n      \"click\",\n      { describe: \"click element\", coordinates: [100, 100] },\n      { mode: \"hybrid\" },\n    );\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for type tool (hybrid)\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\n      \"type\",\n      {\n        describe: \"type into field\",\n        text: \"hello\",\n        coordinates: [100, 100],\n      },\n      { mode: \"hybrid\" },\n    );\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for dragAndDrop tool (hybrid)\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\n      \"dragAndDrop\",\n      {\n        describe: \"drag element\",\n        startCoordinates: [100, 100],\n        endCoordinates: [200, 200],\n      },\n      { mode: \"hybrid\" },\n    );\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for clickAndHold tool (hybrid)\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\n      \"clickAndHold\",\n      {\n        describe: \"hold element\",\n        coordinates: [100, 100],\n        duration: 1000,\n      },\n      { mode: \"hybrid\" },\n    );\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n\n  test(\"agent toolTimeout enforces timeout for fillFormVision tool (hybrid)\", async () => {\n    const { toolOutput } = await runAgentToolTimeoutScenario(\n      \"fillFormVision\",\n      {\n        fields: [\n          {\n            action: \"type hello into name\",\n            value: \"hello\",\n            coordinates: { x: 100, y: 100 },\n          },\n          {\n            action: \"type world into email\",\n            value: \"world\",\n            coordinates: { x: 100, y: 200 },\n          },\n        ],\n      },\n      { mode: \"hybrid\" },\n    );\n    const output = toolOutput as { success: boolean; error: string };\n    expect(output.success).toBe(false);\n    expect(output.error).toContain(\"TimeoutError\");\n    expect(output.error).toContain(\"1ms\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/user-data-dir.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3TestConfig } from \"./v3.config.js\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as os from \"os\";\n\ntest.describe(\"userDataDir persistence\", () => {\n  let v3: V3;\n  let testDir: string;\n\n  test.beforeEach(() => {\n    testDir = fs.mkdtempSync(\n      path.join(os.tmpdir(), \"stagehand-userdata-test-\"),\n    );\n  });\n\n  test.afterEach(async () => {\n    await v3?.close?.().catch(() => {});\n    if (testDir && fs.existsSync(testDir)) {\n      fs.rmSync(testDir, { recursive: true, force: true });\n    }\n  });\n\n  test(\"Chrome uses the specified userDataDir\", async () => {\n    const browserTarget = (\n      process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n    ).toLowerCase();\n    const isBrowserbase = browserTarget === \"browserbase\";\n    test.skip(isBrowserbase, \"Requires local Chromium for userDataDir checks\");\n\n    v3 = new V3({\n      ...v3TestConfig,\n      localBrowserLaunchOptions: {\n        ...(v3TestConfig.localBrowserLaunchOptions ?? {}),\n        userDataDir: testDir,\n        preserveUserDataDir: true,\n      },\n    });\n\n    await v3.init();\n\n    const page = v3.context.pages()[0];\n    await page.goto(\"about:blank\");\n\n    await expect\n      .poll(() => fs.existsSync(path.join(testDir, \"Default\")), {\n        timeout: 10_000,\n      })\n      .toBe(true);\n\n    expect(fs.existsSync(path.join(testDir, \"Local State\"))).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/v3.config.ts",
    "content": "import type { V3Options } from \"../../lib/v3/types/public/options.js\";\nimport {\n  v3DynamicTestConfig,\n  getV3DynamicTestConfig,\n} from \"./v3.dynamic.config.js\";\n\nexport const v3TestConfig: V3Options = v3DynamicTestConfig;\n\nexport function getV3TestConfig(overrides: Partial<V3Options> = {}): V3Options {\n  return getV3DynamicTestConfig(overrides);\n}\n\nexport default getV3TestConfig;\n"
  },
  {
    "path": "packages/core/tests/integration/v3.dynamic.config.ts",
    "content": "import type { V3Options } from \"../../lib/v3/types/public/options.js\";\nimport type { BrowserbaseSessionCreateParams } from \"../../lib/v3/types/public/api.js\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\n\nconst browserTarget = (\n  process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n).toLowerCase();\nconst isBrowserbase = browserTarget === \"browserbase\";\nconst browserbaseRegionRaw = process.env.BROWSERBASE_REGION;\nconst browserbaseRegion = (\n  [\n    \"us-west-2\",\n    \"us-east-1\",\n    \"eu-central-1\",\n    \"ap-southeast-1\",\n  ] as BrowserbaseSessionCreateParams[\"region\"][]\n).includes(browserbaseRegionRaw as BrowserbaseSessionCreateParams[\"region\"])\n  ? (browserbaseRegionRaw as BrowserbaseSessionCreateParams[\"region\"])\n  : undefined;\n\nconst baseConfig = {\n  verbose: 0 as const,\n  disablePino: true,\n  logger: (line: LogLine) => console.log(line),\n  disableAPI: true,\n};\n\nexport const v3DynamicTestConfig: V3Options = isBrowserbase\n  ? {\n      ...baseConfig,\n      env: \"BROWSERBASE\",\n      apiKey: process.env.BROWSERBASE_API_KEY!,\n      projectId: process.env.BROWSERBASE_PROJECT_ID!,\n      disableAPI: true,\n      selfHeal: false,\n      ...(browserbaseRegion\n        ? { browserbaseSessionCreateParams: { region: browserbaseRegion } }\n        : {}),\n    }\n  : {\n      ...baseConfig,\n      env: \"LOCAL\",\n      localBrowserLaunchOptions: {\n        executablePath: process.env.CHROME_PATH,\n        args: process.env.CI ? [\"--no-sandbox\"] : undefined,\n        headless: true,\n        viewport: { width: 1288, height: 711 },\n      },\n    };\n\nexport function getV3DynamicTestConfig(\n  overrides: Partial<V3Options> = {},\n): V3Options {\n  return { ...v3DynamicTestConfig, ...overrides };\n}\n\nexport default getV3DynamicTestConfig;\n"
  },
  {
    "path": "packages/core/tests/integration/v3.playwright.config.ts",
    "content": "import { defineConfig, type ReporterDescription } from \"@playwright/test\";\nimport { getPackageRootDir } from \"../../lib/v3/runtimePaths.js\";\nconst coreDir = getPackageRootDir();\nconst testDir = `${coreDir}/dist/esm/tests/integration`;\n\nconst browserTarget = (\n  process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n).toLowerCase();\nconst isBrowserbase = browserTarget === \"browserbase\";\nconst consoleReporter = process.env.PLAYWRIGHT_CONSOLE_REPORTER ?? \"list\";\n\nconst localWorkerOverride = Number(\n  process.env.LOCAL_SESSION_LIMIT_PER_E2E_TEST,\n);\nconst localWorkers =\n  Number.isFinite(localWorkerOverride) && localWorkerOverride > 0\n    ? localWorkerOverride\n    : process.env.CI\n      ? 3\n      : 5;\n\nconst ciWorkerOverride = Number(\n  process.env.BROWSERBASE_SESSION_LIMIT_PER_E2E_TEST,\n);\nconst bbWorkers =\n  process.env.CI && Number.isFinite(ciWorkerOverride) && ciWorkerOverride > 0\n    ? ciWorkerOverride\n    : 3;\n\nconst ctrfJunitPath = process.env.CTRF_JUNIT_PATH;\nconst reporter: ReporterDescription[] = ctrfJunitPath\n  ? [\n      [consoleReporter] as ReporterDescription,\n      [\n        \"junit\",\n        { outputFile: ctrfJunitPath, includeProjectInTestName: true },\n      ] as ReporterDescription,\n    ]\n  : [[consoleReporter] as ReporterDescription];\n\nexport default defineConfig({\n  testDir,\n  timeout: 90_000,\n  expect: { timeout: 10_000 },\n  retries: process.env.CI ? 1 : 0,\n  workers: isBrowserbase ? bbWorkers : localWorkers,\n  fullyParallel: true,\n  projects: [\n    {\n      name: isBrowserbase ? \"e2e-bb\" : \"e2e-local\",\n    },\n  ],\n  reporter,\n  use: {\n    // we're not launching Playwright browsers in these tests; we connect via Puppeteer/CDP to V3.\n    headless: false,\n  },\n});\n"
  },
  {
    "path": "packages/core/tests/integration/wait-for-selector.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe.configure({ mode: \"serial\" });\ntest.describe(\"Page.waitForSelector tests\", () => {\n  let v3: V3;\n\n  test.beforeAll(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.beforeEach(async () => {\n    const pages = v3.context.pages();\n    if (pages.length === 0) {\n      await v3.context.newPage(\"about:blank\");\n      return;\n    }\n\n    const [primary, ...extras] = pages;\n    for (const page of extras) {\n      await page.close().catch(() => {});\n    }\n\n    v3.context.setActivePage(primary);\n    await primary.goto(\"about:blank\", {\n      waitUntil: \"load\",\n      timeoutMs: 15_000,\n    });\n  });\n\n  test.afterAll(async () => {\n    await closeV3(v3);\n  });\n\n  test.describe(\"Basic state tests\", () => {\n    test(\"resolves when element is already visible\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent('<button id=\"submit-btn\">Submit</button>'),\n      );\n\n      const result = await page.waitForSelector(\"#submit-btn\");\n      expect(result).toBe(true);\n    });\n\n    test(\"resolves when element appears after delay\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<div id='container'></div>\" +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              \"  const btn = document.createElement('button');\" +\n              \"  btn.id = 'delayed-btn';\" +\n              \"  btn.textContent = 'Delayed Button';\" +\n              \"  document.getElementById('container').appendChild(btn);\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#delayed-btn\", {\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"state 'attached' resolves for hidden elements\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"hidden-div\" style=\"display: none;\">Hidden Content</div>',\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#hidden-div\", {\n        state: \"attached\",\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"state 'visible' waits for element to become visible\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"show-later\" style=\"display: none;\">Now Visible</div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              \"  document.getElementById('show-later').style.display = 'block';\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#show-later\", {\n        state: \"visible\",\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"state 'hidden' waits for element to become hidden\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"hide-later\" style=\"display: block;\">Will Hide</div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              \"  document.getElementById('hide-later').style.display = 'none';\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#hide-later\", {\n        state: \"hidden\",\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"state 'detached' waits for element to be removed\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"remove-me\">Will Be Removed</div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              \"  const el = document.getElementById('remove-me');\" +\n              \"  el.parentNode.removeChild(el);\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#remove-me\", {\n        state: \"detached\",\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"state 'detached' resolves immediately for non-existent element\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" + encodeURIComponent(\"<div>Content</div>\"),\n      );\n\n      const result = await page.waitForSelector(\"#does-not-exist\", {\n        state: \"detached\",\n        timeout: 1000,\n      });\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Timeout behavior\", () => {\n    test(\"throws on timeout when element never appears\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" + encodeURIComponent(\"<div>No button here</div>\"),\n      );\n\n      let error: Error | null = null;\n      try {\n        await page.waitForSelector(\"#nonexistent\", { timeout: 300 });\n      } catch (e) {\n        error = e as Error;\n      }\n\n      expect(error).not.toBeNull();\n      expect(error?.message).toContain(\"Timeout\");\n      expect(error?.message).toContain(\"#nonexistent\");\n    });\n\n    test(\"respects custom timeout duration\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" + encodeURIComponent(\"<div>Content</div>\"),\n      );\n\n      const startTime = Date.now();\n      try {\n        await page.waitForSelector(\"#nonexistent\", { timeout: 500 });\n      } catch {\n        // Expected to timeout\n      }\n      const elapsed = Date.now() - startTime;\n\n      // Should timeout around 500ms (allow some margin)\n      expect(elapsed).toBeGreaterThanOrEqual(450);\n      expect(elapsed).toBeLessThan(2000);\n    });\n  });\n\n  test.describe(\"CSS selector variants\", () => {\n    test(\"handles complex CSS selectors\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div class=\"container\">' +\n              '<form id=\"login-form\">' +\n              '<button type=\"submit\">Login</button>' +\n              \"</form>\" +\n              \"</div>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\n        \".container #login-form button[type='submit']\",\n      );\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Open shadow DOM\", () => {\n    test(\"finds element inside open shadow DOM with pierceShadow: true\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"host\"></div>' +\n              \"<script>\" +\n              'const host = document.getElementById(\"host\");' +\n              'const shadow = host.attachShadow({mode: \"open\"});' +\n              'shadow.innerHTML = \"<button id=\\\\\"shadow-btn\\\\\">Shadow Button</button>\";' +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\"#shadow-btn\", {\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"does NOT find shadow DOM element with pierceShadow: false\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"host\"></div>' +\n              \"<script>\" +\n              'const host = document.getElementById(\"host\");' +\n              'const shadow = host.attachShadow({mode: \"open\"});' +\n              'shadow.innerHTML = \"<button id=\\\\\"shadow-only-btn\\\\\">Shadow Only</button>\";' +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      let error: Error | null = null;\n      try {\n        await page.waitForSelector(\"#shadow-only-btn\", {\n          pierceShadow: false,\n          timeout: 300,\n        });\n      } catch (e) {\n        error = e as Error;\n      }\n\n      expect(error).not.toBeNull();\n      expect(error?.message).toContain(\"Timeout\");\n    });\n\n    test(\"finds element in nested open shadow DOM\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"outer-host\"></div>' +\n              \"<script>\" +\n              'const outerHost = document.getElementById(\"outer-host\");' +\n              'const outerShadow = outerHost.attachShadow({mode: \"open\"});' +\n              'outerShadow.innerHTML = \"<div id=\\\\\"inner-host\\\\\"></div>\";' +\n              'const innerHost = outerShadow.getElementById(\"inner-host\");' +\n              'const innerShadow = innerHost.attachShadow({mode: \"open\"});' +\n              'innerShadow.innerHTML = \"<span id=\\\\\"deep-element\\\\\">Deep!</span>\";' +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\"#deep-element\", {\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Closed shadow DOM (via piercer)\", () => {\n    test(\"finds element inside closed shadow DOM via custom element\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<closed-shadow-host></closed-shadow-host>\" +\n              \"<script>\" +\n              \"class ClosedShadowHost extends HTMLElement {\" +\n              \"  constructor() {\" +\n              \"    super();\" +\n              '    const shadow = this.attachShadow({mode: \"closed\"});' +\n              '    shadow.innerHTML = \"<button id=\\\\\"closed-btn\\\\\">Closed Shadow Button</button>\";' +\n              \"  }\" +\n              \"}\" +\n              \"customElements.define('closed-shadow-host', ClosedShadowHost);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      // The piercer hooks attachShadow and stores closed shadow roots\n      const result = await page.waitForSelector(\"#closed-btn\", {\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"finds element in nested closed shadow DOM\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<outer-closed></outer-closed>\" +\n              \"<script>\" +\n              \"class InnerClosed extends HTMLElement {\" +\n              \"  constructor() {\" +\n              \"    super();\" +\n              '    const shadow = this.attachShadow({mode: \"closed\"});' +\n              '    shadow.innerHTML = \"<span id=\\\\\"deeply-closed\\\\\">Deeply Nested Closed</span>\";' +\n              \"  }\" +\n              \"}\" +\n              \"customElements.define('inner-closed', InnerClosed);\" +\n              \"\" +\n              \"class OuterClosed extends HTMLElement {\" +\n              \"  constructor() {\" +\n              \"    super();\" +\n              '    const shadow = this.attachShadow({mode: \"closed\"});' +\n              '    shadow.innerHTML = \"<inner-closed></inner-closed>\";' +\n              \"  }\" +\n              \"}\" +\n              \"customElements.define('outer-closed', OuterClosed);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\"#deeply-closed\", {\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"finds element in mixed open/closed nested shadow DOM\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"open-host\"></div>' +\n              \"<script>\" +\n              // Inner closed component\n              \"class ClosedInner extends HTMLElement {\" +\n              \"  constructor() {\" +\n              \"    super();\" +\n              '    const shadow = this.attachShadow({mode: \"closed\"});' +\n              '    shadow.innerHTML = \"<button id=\\\\\"mixed-deep-btn\\\\\">Mixed Deep Button</button>\";' +\n              \"  }\" +\n              \"}\" +\n              \"customElements.define('closed-inner', ClosedInner);\" +\n              // Outer open shadow\n              'const openHost = document.getElementById(\"open-host\");' +\n              'const openShadow = openHost.attachShadow({mode: \"open\"});' +\n              'openShadow.innerHTML = \"<closed-inner></closed-inner>\";' +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\"#mixed-deep-btn\", {\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"waits for element to appear inside closed shadow DOM\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<delayed-closed-host></delayed-closed-host>\" +\n              \"<script>\" +\n              \"class DelayedClosedHost extends HTMLElement {\" +\n              \"  constructor() {\" +\n              \"    super();\" +\n              '    const shadow = this.attachShadow({mode: \"closed\"});' +\n              '    shadow.innerHTML = \"<div id=\\\\\"container\\\\\"></div>\";' +\n              \"    setTimeout(() => {\" +\n              '      shadow.getElementById(\"container\").innerHTML = ' +\n              '        \"<button id=\\\\\"delayed-closed-btn\\\\\">Appeared!</button>\";' +\n              \"    }, 300);\" +\n              \"  }\" +\n              \"}\" +\n              \"customElements.define('delayed-closed-host', DelayedClosedHost);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n\n      const result = await page.waitForSelector(\"#delayed-closed-btn\", {\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"XPath selectors\", () => {\n    test(\"finds element with basic XPath\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent('<button id=\"xpath-btn\">XPath Button</button>'),\n      );\n\n      const result = await page.waitForSelector(\"//button[@id='xpath-btn']\", {\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"finds element with xpath= prefix\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"container\"><span class=\"target\">Target</span></div>',\n          ),\n      );\n\n      const result = await page.waitForSelector(\n        \"xpath=//span[@class='target']\",\n        {\n          timeout: 5000,\n        },\n      );\n      expect(result).toBe(true);\n    });\n\n    test(\"waits for element to appear with XPath\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<div id='container'></div>\" +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              '  document.getElementById(\"container\").innerHTML = ' +\n              '    \"<span id=\\\\\"delayed-xpath\\\\\">Delayed XPath</span>\";' +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"//span[@id='delayed-xpath']\", {\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"finds element in open shadow DOM with XPath\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"host\"></div>' +\n              \"<script>\" +\n              'const host = document.getElementById(\"host\");' +\n              'const shadow = host.attachShadow({mode: \"open\"});' +\n              'shadow.innerHTML = \"<button id=\\\\\"shadow-xpath-btn\\\\\">Shadow XPath</button>\";' +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\n        \"//button[@id='shadow-xpath-btn']\",\n        {\n          pierceShadow: true,\n          timeout: 5000,\n        },\n      );\n      expect(result).toBe(true);\n    });\n\n    test(\"finds element in closed shadow DOM with XPath\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<xpath-closed-host></xpath-closed-host>\" +\n              \"<script>\" +\n              \"class XPathClosedHost extends HTMLElement {\" +\n              \"  constructor() {\" +\n              \"    super();\" +\n              '    const shadow = this.attachShadow({mode: \"closed\"});' +\n              '    shadow.innerHTML = \"<span id=\\\\\"xpath-closed-target\\\\\">Closed XPath Target</span>\";' +\n              \"  }\" +\n              \"}\" +\n              \"customElements.define('xpath-closed-host', XPathClosedHost);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\n        \"//span[@id='xpath-closed-target']\",\n        {\n          pierceShadow: true,\n          timeout: 5000,\n        },\n      );\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Iframe hop notation (>>)\", () => {\n    test(\"finds element inside single iframe\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<button id=\"main-btn\">Main Button</button>' +\n              '<iframe id=\"my-frame\"></iframe>' +\n              \"<script>\" +\n              'const frame = document.getElementById(\"my-frame\");' +\n              \"const doc = frame.contentDocument;\" +\n              \"doc.open();\" +\n              'doc.write(\"<button id=\\\\\"frame-btn\\\\\">Frame Button</button>\");' +\n              \"doc.close();\" +\n              \"</script>\",\n          ),\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\n        \"iframe#my-frame >> #frame-btn\",\n        {\n          timeout: 5000,\n        },\n      );\n      expect(result).toBe(true);\n    });\n\n    test(\"finds element through multiple iframe hops\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<iframe id=\"outer-frame\"></iframe>' +\n              \"<script>\" +\n              'const outerFrame = document.getElementById(\"outer-frame\");' +\n              \"const outerDoc = outerFrame.contentDocument;\" +\n              \"outerDoc.open();\" +\n              'outerDoc.write(\"<iframe id=\\\\\"inner-frame\\\\\"></iframe>\");' +\n              \"outerDoc.close();\" +\n              \"setTimeout(() => {\" +\n              '  const innerFrame = outerDoc.getElementById(\"inner-frame\");' +\n              \"  const innerDoc = innerFrame.contentDocument;\" +\n              \"  innerDoc.open();\" +\n              '  innerDoc.write(\"<div id=\\\\\"nested-content\\\\\">Deeply Nested</div>\");' +\n              \"  innerDoc.close();\" +\n              \"}, 100);\" +\n              \"</script>\",\n          ),\n      );\n      await page.waitForTimeout(300);\n\n      const result = await page.waitForSelector(\n        \"iframe#outer-frame >> iframe#inner-frame >> #nested-content\",\n        { timeout: 5000 },\n      );\n      expect(result).toBe(true);\n    });\n\n    test(\"waits for element to appear inside iframe\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<iframe id=\"delay-frame\"></iframe>' +\n              \"<script>\" +\n              'const frame = document.getElementById(\"delay-frame\");' +\n              \"const doc = frame.contentDocument;\" +\n              \"doc.open();\" +\n              'doc.write(\"<div id=\\\\\"container\\\\\"></div>\");' +\n              \"doc.close();\" +\n              \"setTimeout(() => {\" +\n              '  doc.getElementById(\"container\").innerHTML = ' +\n              '    \"<span id=\\\\\"delayed-in-frame\\\\\">Appeared!</span>\";' +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\n        \"iframe#delay-frame >> #delayed-in-frame\",\n        {\n          timeout: 5000,\n        },\n      );\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Visibility edge cases\", () => {\n    test(\"visibility: hidden is not visible\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"vis-hidden\" style=\"visibility: hidden;\">Hidden</div>',\n          ),\n      );\n\n      // Should be attached but not visible\n      const attached = await page.waitForSelector(\"#vis-hidden\", {\n        state: \"attached\",\n      });\n      expect(attached).toBe(true);\n\n      let error: Error | null = null;\n      try {\n        await page.waitForSelector(\"#vis-hidden\", {\n          state: \"visible\",\n          timeout: 200,\n        });\n      } catch (e) {\n        error = e as Error;\n      }\n      expect(error).not.toBeNull();\n    });\n\n    test(\"opacity: 0 is not visible\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"transparent\" style=\"opacity: 0;\">Transparent</div>',\n          ),\n      );\n\n      const attached = await page.waitForSelector(\"#transparent\", {\n        state: \"attached\",\n      });\n      expect(attached).toBe(true);\n\n      let error: Error | null = null;\n      try {\n        await page.waitForSelector(\"#transparent\", {\n          state: \"visible\",\n          timeout: 200,\n        });\n      } catch (e) {\n        error = e as Error;\n      }\n      expect(error).not.toBeNull();\n    });\n\n    test(\"zero dimensions is not visible\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"zero-size\" style=\"width: 0; height: 0;\">Zero</div>',\n          ),\n      );\n\n      const attached = await page.waitForSelector(\"#zero-size\", {\n        state: \"attached\",\n      });\n      expect(attached).toBe(true);\n\n      let error: Error | null = null;\n      try {\n        await page.waitForSelector(\"#zero-size\", {\n          state: \"visible\",\n          timeout: 200,\n        });\n      } catch (e) {\n        error = e as Error;\n      }\n      expect(error).not.toBeNull();\n    });\n\n    test(\"detects visibility change via class toggle\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<style>.hidden { display: none; }</style>\" +\n              '<div id=\"class-toggle\" class=\"hidden\">Class Toggle</div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              \"  document.getElementById('class-toggle').classList.remove('hidden');\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#class-toggle\", {\n        state: \"visible\",\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"detects visibility change via style attribute\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"style-toggle\" style=\"display: none;\">Style Toggle</div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              \"  document.getElementById('style-toggle').style.display = 'block';\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#style-toggle\", {\n        state: \"visible\",\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Dynamic DOM scenarios\", () => {\n    test(\"handles rapid DOM mutations\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            \"<div id='container'></div>\" +\n              \"<script>\" +\n              \"let count = 0;\" +\n              \"const interval = setInterval(() => {\" +\n              \"  count++;\" +\n              \"  const div = document.createElement('div');\" +\n              \"  div.id = 'item-' + count;\" +\n              \"  div.textContent = 'item';\" +\n              \"  document.getElementById('container').appendChild(div);\" +\n              \"  if (count >= 10) clearInterval(interval);\" +\n              \"}, 50);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      // Small delay to ensure script starts\n      await page.waitForTimeout(50);\n\n      const result = await page.waitForSelector(\"#item-7\", { timeout: 10000 });\n      expect(result).toBe(true);\n    });\n\n    test(\"handles element removed and re-added\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent('<div id=\"toggle-me\">Toggle</div>'),\n      );\n\n      const browserTarget = (\n        process.env.STAGEHAND_BROWSER_TARGET ?? \"local\"\n      ).toLowerCase();\n      const isBrowserbase = browserTarget === \"browserbase\";\n      const removeDelayMs = isBrowserbase ? 1000 : 200;\n      const addDelayMs = isBrowserbase ? 1600 : 500;\n      const waitTimeoutMs = isBrowserbase ? 10000 : 5000;\n\n      // Start waiting before scheduling DOM changes to avoid racey timing in CI.\n      const detachedPromise = page.waitForSelector(\"#toggle-me\", {\n        state: \"detached\",\n        timeout: waitTimeoutMs,\n      });\n      await page.evaluate(\n        ({ removeDelay, addDelay }) => {\n          const el = document.getElementById(\"toggle-me\");\n          const parent = el?.parentNode;\n          if (!el || !parent) return;\n          setTimeout(() => parent.removeChild(el), removeDelay);\n          setTimeout(() => parent.appendChild(el), addDelay);\n        },\n        { removeDelay: removeDelayMs, addDelay: addDelayMs },\n      );\n\n      const detached = await detachedPromise;\n      expect(detached).toBe(true);\n\n      // Then wait for visible again\n      const visible = await page.waitForSelector(\"#toggle-me\", {\n        state: \"visible\",\n        timeout: waitTimeoutMs,\n      });\n      expect(visible).toBe(true);\n    });\n\n    test(\"handles dynamically replaced innerHTML\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"container\">Loading...</div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              '  document.getElementById(\"container\").innerHTML = ' +\n              '    \"<button id=\\\\\"loaded-btn\\\\\">Loaded!</button>\";' +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#loaded-btn\", {\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"handles element created via insertAdjacentHTML\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"anchor\"></div>' +\n              \"<script>\" +\n              \"setTimeout(() => {\" +\n              '  document.getElementById(\"anchor\").insertAdjacentHTML(' +\n              '    \"afterend\", \"<div id=\\\\\"inserted\\\\\">Inserted</div>\"' +\n              \"  );\" +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n      );\n\n      const result = await page.waitForSelector(\"#inserted\", { timeout: 5000 });\n      expect(result).toBe(true);\n    });\n  });\n\n  test.describe(\"Shadow DOM visibility changes\", () => {\n    test(\"detects element becoming visible inside open shadow DOM\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"host\"></div>' +\n              \"<script>\" +\n              'const host = document.getElementById(\"host\");' +\n              'const shadow = host.attachShadow({mode: \"open\"});' +\n              'shadow.innerHTML = \"<button id=\\\\\"shadow-btn\\\\\" style=\\\\\"display:none\\\\\">Shadow</button>\";' +\n              \"setTimeout(() => {\" +\n              '  shadow.getElementById(\"shadow-btn\").style.display = \"block\";' +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n\n      const result = await page.waitForSelector(\"#shadow-btn\", {\n        state: \"visible\",\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n\n    test(\"detects element becoming hidden inside shadow DOM\", async () => {\n      const page = v3.context.pages()[0];\n      await page.goto(\n        \"data:text/html,\" +\n          encodeURIComponent(\n            '<div id=\"host\"></div>' +\n              \"<script>\" +\n              'const host = document.getElementById(\"host\");' +\n              'const shadow = host.attachShadow({mode: \"open\"});' +\n              'shadow.innerHTML = \"<button id=\\\\\"hide-shadow-btn\\\\\">Will Hide</button>\";' +\n              \"setTimeout(() => {\" +\n              '  shadow.getElementById(\"hide-shadow-btn\").style.display = \"none\";' +\n              \"}, 300);\" +\n              \"</script>\",\n          ),\n        { waitUntil: \"load\", timeoutMs: 30000 },\n      );\n      await page.waitForTimeout(100);\n\n      const result = await page.waitForSelector(\"#hide-shadow-btn\", {\n        state: \"hidden\",\n        pierceShadow: true,\n        timeout: 5000,\n      });\n      expect(result).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/wait-for-timeout.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"Page.waitForTimeout tests\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"waitForTimeout resolves after specified duration\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" + encodeURIComponent(\"<div>Test Page</div>\"),\n    );\n\n    const startTime = Date.now();\n    await page.waitForTimeout(200);\n    const elapsed = Date.now() - startTime;\n\n    // Should have waited at least 200ms (allow some tolerance)\n    expect(elapsed).toBeGreaterThanOrEqual(190);\n  });\n\n  test(\"waitForTimeout resolves immediately for 0ms\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" + encodeURIComponent(\"<div>Test Page</div>\"),\n    );\n\n    const startTime = Date.now();\n    await page.waitForTimeout(0);\n    const elapsed = Date.now() - startTime;\n\n    // Should resolve nearly immediately (within 50ms tolerance)\n    expect(elapsed).toBeLessThan(50);\n  });\n\n  test(\"waitForTimeout can be chained with other operations\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          \"<div id='counter'>0</div>\" +\n            \"<script>\" +\n            \"let count = 0;\" +\n            \"setInterval(() => {\" +\n            \"  count++;\" +\n            \"  document.getElementById('counter').textContent = count;\" +\n            \"}, 100);\" +\n            \"</script>\",\n        ),\n    );\n\n    // Wait for counter to increment\n    await page.waitForTimeout(350);\n\n    // Counter should have incremented at least 3 times\n    const text = await page.mainFrame().locator(\"#counter\").textContent();\n    expect(parseInt(text ?? \"0\")).toBeGreaterThanOrEqual(3);\n  });\n\n  test(\"waitForTimeout works with async/await syntax\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\"data:text/html,\" + encodeURIComponent(\"<div>Test</div>\"));\n\n    const results: number[] = [];\n\n    results.push(1);\n    await page.waitForTimeout(50);\n    results.push(2);\n    await page.waitForTimeout(50);\n    results.push(3);\n\n    expect(results).toEqual([1, 2, 3]);\n  });\n\n  test(\"waitForTimeout allows DOM to update\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          \"<div id='delayed'></div>\" +\n            \"<script>\" +\n            \"window.startUpdate = () => {\" +\n            \"  setTimeout(() => {\" +\n            \"    document.getElementById('delayed').textContent = 'Loaded';\" +\n            \"  }, 200);\" +\n            \"};\" +\n            \"</script>\",\n        ),\n    );\n\n    // Trigger the delayed update\n    await page.evaluate(() => {\n      (window as unknown as { startUpdate: () => void }).startUpdate();\n    });\n\n    // Wait for the timeout to allow DOM update\n    await page.waitForTimeout(300);\n\n    // Content should now be loaded\n    const afterText = await page.mainFrame().locator(\"#delayed\").textContent();\n    expect(afterText).toBe(\"Loaded\");\n  });\n\n  test(\"waitForTimeout with small increments\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\"data:text/html,\" + encodeURIComponent(\"<div>Test</div>\"));\n\n    const startTime = Date.now();\n\n    // Multiple small waits\n    await page.waitForTimeout(50);\n    await page.waitForTimeout(50);\n    await page.waitForTimeout(50);\n    await page.waitForTimeout(50);\n\n    const elapsed = Date.now() - startTime;\n\n    // Should have waited at least 200ms total (4 * 50ms)\n    expect(elapsed).toBeGreaterThanOrEqual(190);\n  });\n\n  test(\"waitForTimeout does not block other async operations\", async () => {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\n      \"data:text/html,\" +\n        encodeURIComponent(\n          \"<div id='async-test'>Initial</div>\" +\n            \"<script>\" +\n            \"window.updateText = () => {\" +\n            \"  document.getElementById('async-test').textContent = 'Updated';\" +\n            \"};\" +\n            \"</script>\",\n        ),\n    );\n\n    // Start a timeout\n    const timeoutPromise = page.waitForTimeout(100);\n\n    // Execute something else while waiting\n    await page.evaluate(() => {\n      (window as unknown as { updateText: () => void }).updateText();\n    });\n\n    // Verify the update happened\n    const text = await page.mainFrame().locator(\"#async-test\").textContent();\n    expect(text).toBe(\"Updated\");\n\n    // Wait for the timeout to complete\n    await timeoutPromise;\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/integration/xpath-for-location-deep.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { V3 } from \"../../lib/v3/v3.js\";\nimport { v3DynamicTestConfig } from \"./v3.dynamic.config.js\";\nimport { resolveXpathForLocation } from \"../../lib/v3/understudy/a11y/snapshot/index.js\";\nimport { executionContexts } from \"../../lib/v3/understudy/executionContextRegistry.js\";\nimport { closeV3 } from \"./testUtils.js\";\n\ntest.describe(\"resolveNodeForLocationDeep\", () => {\n  let v3: V3;\n\n  test.beforeEach(async () => {\n    v3 = new V3(v3DynamicTestConfig);\n    await v3.init();\n  });\n\n  test.afterEach(async () => {\n    await closeV3(v3);\n  });\n\n  test(\"click resolves inside same-process iframe and returns absolute XPath\", async () => {\n    const page = await v3.context.awaitActivePage();\n\n    // Set consistent viewport size to ensure stable rendering across environments\n    await page.setViewportSize(1280, 720);\n\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/\",\n      { waitUntil: \"networkidle\" },\n    );\n\n    await page.waitForSelector(\"section iframe\", {\n      state: \"attached\",\n      timeout: 10000,\n    });\n    const frame = await page.frameLocator(\"section iframe\").resolveFrame();\n    await executionContexts.waitForMainWorld(\n      frame.session,\n      frame.frameId,\n      5000,\n    );\n\n    // scroll to the bottom of the page\n    await page.evaluate(() => {\n      window.scrollTo(0, document.body.scrollHeight);\n    });\n\n    // scroll to the bottom of the iframe\n    await frame.evaluate(() => {\n      window.scrollTo(0, document.body.scrollHeight);\n    });\n\n    // Wait a bit for the iframe content to settle after scrolling\n    await new Promise((resolve) => setTimeout(resolve, 500));\n\n    // Get the iframe's position in the main page\n    const iframeOffset = await page.evaluate(() => {\n      const iframe = document.querySelector(\"section iframe\");\n      if (!iframe) return null;\n      const rect = iframe.getBoundingClientRect();\n      return {\n        left: rect.left,\n        top: rect.top,\n      };\n    });\n\n    // Get the link's position within the iframe\n    const linkOffsetInFrame = await frame.evaluate(() => {\n      // Find the 88th row, 3rd column link (the one we're testing)\n      const table = document.querySelector(\n        \"center > table > tbody > tr:nth-child(3) > td > table\",\n      );\n      if (!table) return null;\n\n      const row88 = table.querySelector(\"tbody > tr:nth-child(88)\");\n      if (!row88) return null;\n\n      const cell3 = row88.querySelector(\"td:nth-child(3)\");\n      if (!cell3) return null;\n\n      const link = cell3.querySelector(\"span > a\");\n      if (!link) return null;\n\n      const rect = link.getBoundingClientRect();\n      // Return center coordinates of the link relative to iframe\n      return {\n        x: rect.left + rect.width / 2,\n        y: rect.top + rect.height / 2,\n      };\n    });\n\n    // Combine iframe offset and link offset to get page-level coordinates\n    // Fallback to hardcoded coordinates if element not found (shouldn't happen)\n    const x =\n      iframeOffset && linkOffsetInFrame\n        ? iframeOffset.left + linkOffsetInFrame.x\n        : 356;\n    const y =\n      iframeOffset && linkOffsetInFrame\n        ? iframeOffset.top + linkOffsetInFrame.y\n        : 503;\n\n    const result = await resolveXpathForLocation(page, x, y);\n    console.log(\"=== Coordinates used:\", { x, y });\n    console.log(\"=== Result:\", result);\n    const xpath = result.absoluteXPath;\n    expect(xpath).toBe(\n      \"/html[1]/body[1]/main[1]/section[3]/iframe[1]/html[1]/body[1]/center[1]/table[1]/tbody[1]/tr[3]/td[1]/table[1]/tbody[1]/tr[88]/td[3]/span[1]/a[1]\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/agent-captcha-hooks.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\nimport { CaptchaSolver } from \"../../lib/v3/agent/utils/captchaSolver.js\";\nimport { V3AgentHandler } from \"../../lib/v3/handlers/v3AgentHandler.js\";\n\nconst SOLVING_STARTED = \"browserbase-solving-started\";\nconst SOLVING_FINISHED = \"browserbase-solving-finished\";\nconst SOLVING_ERRORED = \"browserbase-solving-errored\";\n\ntype ConsoleListener = (message: { text: () => string }) => void;\n\nclass MockPage {\n  private listeners = new Set<ConsoleListener>();\n  public captchaBoxes: Array<{\n    left: number;\n    top: number;\n    right: number;\n    bottom: number;\n  }> = [];\n\n  on(event: string, listener: ConsoleListener): void {\n    if (event === \"console\") {\n      this.listeners.add(listener);\n    }\n  }\n\n  off(event: string, listener: ConsoleListener): void {\n    if (event === \"console\") {\n      this.listeners.delete(listener);\n    }\n  }\n\n  emitConsole(text: string): void {\n    const message = { text: () => text };\n    for (const listener of this.listeners) {\n      listener(message);\n    }\n  }\n\n  url(): string {\n    return \"https://example.com\";\n  }\n\n  async screenshot(): Promise<Buffer> {\n    return Buffer.from(\"fake-image\");\n  }\n\n  async evaluate<T>(): Promise<T> {\n    return this.captchaBoxes as T;\n  }\n\n  mainFrame(): { evaluate: () => Promise<{ w: number; h: number }> } {\n    return {\n      evaluate: async () => ({ w: 1288, h: 711 }),\n    };\n  }\n}\n\nclass FakeCuaClient {\n  public contextNotes: string[] = [];\n  public preStepHook?: () => Promise<void>;\n  public actionHandler?: (action: Record<string, unknown>) => Promise<void>;\n  public executeImpl = vi.fn(async (options: unknown) => {\n    void options;\n    return {\n      success: true,\n      message: \"ok\",\n      actions: [],\n      completed: true,\n    };\n  });\n  public captureScreenshot = vi.fn(async () => null);\n  public setViewport = vi.fn();\n  public setCurrentUrl = vi.fn();\n  public setScreenshotProvider = vi.fn();\n  public setSafetyConfirmationHandler = vi.fn();\n\n  setActionHandler(\n    handler: (action: Record<string, unknown>) => Promise<void>,\n  ): void {\n    this.actionHandler = handler;\n  }\n\n  setPreStepHook(handler: () => Promise<void>): void {\n    this.preStepHook = handler;\n  }\n\n  addContextNote(note: string): void {\n    this.contextNotes.push(note);\n  }\n\n  async execute(options: unknown): Promise<{\n    success: boolean;\n    message: string;\n    actions: unknown[];\n    completed: boolean;\n  }> {\n    return this.executeImpl(options);\n  }\n}\n\nlet fakeCuaClient: FakeCuaClient;\n\nvi.mock(\"../../lib/v3/agent/AgentProvider\", () => ({\n  AgentProvider: class {\n    constructor(logger: unknown) {\n      void logger;\n    }\n\n    getClient(): FakeCuaClient {\n      return fakeCuaClient;\n    }\n  },\n}));\n\nimport { V3CuaAgentHandler } from \"../../lib/v3/handlers/v3CuaAgentHandler.js\";\n\nfunction collectUserMessages(\n  messages: Array<{ role: string; content: unknown }>,\n): Array<{ role: \"user\"; content: string }> {\n  return messages.filter(\n    (message): message is { role: \"user\"; content: string } =>\n      message.role === \"user\" && typeof message.content === \"string\",\n  );\n}\n\ndescribe(\"agent captcha hooks\", () => {\n  let page: MockPage;\n  let logs: LogLine[];\n  let logger: (line: LogLine) => void;\n\n  beforeEach(() => {\n    page = new MockPage();\n    logs = [];\n    logger = (line) => {\n      logs.push(line);\n    };\n    fakeCuaClient = new FakeCuaClient();\n  });\n\n  it(\"blocks regular agent prepareStep until the solver finishes and injects one solved message\", async () => {\n    const handler = new V3AgentHandler(\n      {\n        isCaptchaAutoSolveEnabled: true,\n      } as never,\n      logger,\n      {} as never,\n    );\n    const solver = new CaptchaSolver();\n    solver.init(async () => page as never);\n\n    const userCallback = vi.fn(async (options) => options);\n    const prepareStep = (\n      handler as unknown as {\n        createPrepareStep: (\n          callback?: (options: Record<string, unknown>) => Promise<unknown>,\n          captchaSolver?: CaptchaSolver,\n        ) => (options: Record<string, unknown>) => Promise<unknown>;\n      }\n    ).createPrepareStep(userCallback, solver);\n\n    const options = {\n      messages: [{ role: \"user\", content: \"start\" }],\n    };\n\n    await prepareStep(options);\n    page.emitConsole(SOLVING_STARTED);\n\n    const secondCall = prepareStep(options);\n    await Promise.resolve();\n    expect(userCallback).toHaveBeenCalledTimes(1);\n\n    page.emitConsole(SOLVING_FINISHED);\n    await secondCall;\n\n    expect(userCallback).toHaveBeenCalledTimes(2);\n    expect(\n      collectUserMessages(\n        options.messages as Array<{ role: string; content: unknown }>,\n      ).filter((message) =>\n        message.content.includes(\"automatically detected and solved\"),\n      ),\n    ).toHaveLength(1);\n  });\n\n  it(\"injects one error message when the regular agent solver errors\", async () => {\n    const handler = new V3AgentHandler(\n      {\n        isCaptchaAutoSolveEnabled: true,\n      } as never,\n      logger,\n      {} as never,\n    );\n    const solver = new CaptchaSolver();\n    solver.init(async () => page as never);\n\n    const prepareStep = (\n      handler as unknown as {\n        createPrepareStep: (\n          callback?: (options: Record<string, unknown>) => Promise<unknown>,\n          captchaSolver?: CaptchaSolver,\n        ) => (options: Record<string, unknown>) => Promise<unknown>;\n      }\n    ).createPrepareStep(undefined, solver);\n\n    const options = {\n      messages: [{ role: \"user\", content: \"start\" }],\n    };\n\n    await prepareStep(options);\n    page.emitConsole(SOLVING_STARTED);\n\n    const pending = prepareStep(options);\n    page.emitConsole(SOLVING_ERRORED);\n    await pending;\n\n    expect(\n      collectUserMessages(\n        options.messages as Array<{ role: string; content: unknown }>,\n      ).filter((message) =>\n        message.content.includes(\"automatic captcha solver failed\"),\n      ),\n    ).toHaveLength(1);\n  });\n\n  it(\"pauses the CUA loop at prepareStep while Browserbase solves a captcha\", async () => {\n    let secondPrepareStarted = false;\n\n    fakeCuaClient.executeImpl = vi.fn(async () => {\n      await fakeCuaClient.preStepHook?.();\n      page.emitConsole(SOLVING_STARTED);\n\n      const blockedPrepare = fakeCuaClient.preStepHook?.() ?? Promise.resolve();\n      secondPrepareStarted = true;\n      await blockedPrepare;\n\n      return {\n        success: true,\n        message: \"ok\",\n        actions: [],\n        completed: true,\n      };\n    });\n\n    const handler = new V3CuaAgentHandler(\n      {\n        context: {\n          awaitActivePage: async () => page,\n        },\n        bus: { emit: vi.fn() },\n        isCaptchaAutoSolveEnabled: true,\n        isAdvancedStealth: false,\n        configuredViewport: { width: 1288, height: 711 },\n        isAgentReplayActive: () => false,\n        updateMetrics: vi.fn(),\n      } as never,\n      logger,\n      {\n        modelName: \"anthropic/claude-haiku-4-5-20251001\",\n        clientOptions: { waitBetweenActions: 1 },\n      } as never,\n    );\n\n    const execution = handler.execute({\n      instruction: \"Describe the page briefly.\",\n      highlightCursor: false,\n    });\n\n    await vi.waitFor(() => {\n      expect(secondPrepareStarted).toBe(true);\n      expect(\n        logs.some((line) =>\n          line.message.includes(\"waiting for Browserbase to solve\"),\n        ),\n      ).toBe(true);\n    });\n\n    expect(logs.some((line) => line.message.includes(\"Captcha solved\"))).toBe(\n      false,\n    );\n\n    page.emitConsole(SOLVING_FINISHED);\n    await execution;\n\n    expect(fakeCuaClient.contextNotes).toEqual([\n      expect.stringContaining(\"automatically detected and solved\"),\n    ]);\n    expect(logs.some((line) => line.message.includes(\"Captcha solved\"))).toBe(\n      true,\n    );\n  });\n\n  it(\"pauses CUA actions until the captcha solver finishes\", async () => {\n    let actionStarted = false;\n\n    fakeCuaClient.executeImpl = vi.fn(async () => {\n      await fakeCuaClient.preStepHook?.();\n      page.emitConsole(SOLVING_STARTED);\n\n      const pendingAction =\n        fakeCuaClient.actionHandler?.({ type: \"screenshot\" }) ??\n        Promise.resolve();\n      actionStarted = true;\n      await pendingAction;\n\n      return {\n        success: true,\n        message: \"ok\",\n        actions: [],\n        completed: true,\n      };\n    });\n\n    const handler = new V3CuaAgentHandler(\n      {\n        context: {\n          awaitActivePage: async () => page,\n        },\n        bus: { emit: vi.fn() },\n        isCaptchaAutoSolveEnabled: true,\n        isAdvancedStealth: false,\n        configuredViewport: { width: 1288, height: 711 },\n        isAgentReplayActive: () => false,\n        updateMetrics: vi.fn(),\n      } as never,\n      logger,\n      {\n        modelName: \"anthropic/claude-haiku-4-5-20251001\",\n        clientOptions: { waitBetweenActions: 1 },\n      } as never,\n    );\n    const executeActionSpy = vi\n      .spyOn(\n        handler as unknown as {\n          executeAction: (action: Record<string, unknown>) => Promise<unknown>;\n        },\n        \"executeAction\",\n      )\n      .mockResolvedValue({ success: true });\n    vi.spyOn(handler, \"captureAndSendScreenshot\").mockResolvedValue(null);\n\n    const execution = handler.execute({\n      instruction: \"Describe the page briefly.\",\n      highlightCursor: false,\n    });\n\n    await vi.waitFor(() => {\n      expect(actionStarted).toBe(true);\n    });\n\n    expect(executeActionSpy).not.toHaveBeenCalled();\n    page.emitConsole(SOLVING_FINISHED);\n    await execution;\n\n    expect(executeActionSpy).toHaveBeenCalledTimes(1);\n    expect(fakeCuaClient.contextNotes).toEqual([\n      expect.stringContaining(\"automatically detected and solved\"),\n    ]);\n    expect(logs.some((line) => line.message.includes(\"Captcha solved\"))).toBe(\n      true,\n    );\n  });\n\n  it(\"skips post-solve clicks on the captcha widget and injects another note\", async () => {\n    page.captchaBoxes = [{ left: 0, top: 400, right: 140, bottom: 470 }];\n\n    fakeCuaClient.executeImpl = vi.fn(async () => {\n      await fakeCuaClient.preStepHook?.();\n      page.emitConsole(SOLVING_STARTED);\n\n      const blockedPrepare = fakeCuaClient.preStepHook?.() ?? Promise.resolve();\n      page.emitConsole(SOLVING_FINISHED);\n      await blockedPrepare;\n\n      await fakeCuaClient.actionHandler?.({\n        type: \"click\",\n        button: \"left\",\n        x: 63,\n        y: 436,\n      });\n\n      return {\n        success: true,\n        message: \"ok\",\n        actions: [],\n        completed: true,\n      };\n    });\n\n    const handler = new V3CuaAgentHandler(\n      {\n        context: {\n          awaitActivePage: async () => page,\n        },\n        bus: { emit: vi.fn() },\n        isCaptchaAutoSolveEnabled: true,\n        isAdvancedStealth: false,\n        configuredViewport: { width: 1288, height: 711 },\n        isAgentReplayActive: () => false,\n        updateMetrics: vi.fn(),\n      } as never,\n      logger,\n      {\n        modelName: \"anthropic/claude-haiku-4-5-20251001\",\n        clientOptions: { waitBetweenActions: 1 },\n      } as never,\n    );\n    const executeActionSpy = vi\n      .spyOn(\n        handler as unknown as {\n          executeAction: (action: Record<string, unknown>) => Promise<unknown>;\n        },\n        \"executeAction\",\n      )\n      .mockResolvedValue({ success: true });\n    vi.spyOn(handler, \"captureAndSendScreenshot\").mockResolvedValue(null);\n\n    await handler.execute({\n      instruction: \"Describe the page briefly.\",\n      highlightCursor: false,\n    });\n\n    expect(executeActionSpy).not.toHaveBeenCalled();\n    expect(fakeCuaClient.contextNotes).toEqual([\n      expect.stringContaining(\"automatically detected and solved\"),\n      expect.stringContaining(\"Original task: Describe the page briefly.\"),\n    ]);\n    expect(\n      logs.some((line) =>\n        line.message.includes(\"Skipped click on solved captcha widget\"),\n      ),\n    ).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/agent-execution-model.test.ts",
    "content": "import { describe, expect, it, vi } from \"vitest\";\nimport { actTool } from \"../../lib/v3/agent/tools/act.js\";\nimport { extractTool } from \"../../lib/v3/agent/tools/extract.js\";\nimport { fillFormTool } from \"../../lib/v3/agent/tools/fillform.js\";\nimport type { V3 } from \"../../lib/v3/v3.js\";\n\n/**\n * Minimal mock of V3 that captures how tools pass `model` options\n * into v3.act(), v3.extract(), and v3.observe().\n */\nfunction createMockV3() {\n  const calls: { method: string; model: unknown }[] = [];\n\n  const mock = {\n    logger: vi.fn(),\n    recordAgentReplayStep: vi.fn(),\n    act: vi.fn(async (_instruction: unknown, options?: { model?: unknown }) => {\n      calls.push({ method: \"act\", model: options?.model });\n      return {\n        success: true,\n        message: \"ok\",\n        actionDescription: \"clicked\",\n        actions: [],\n      };\n    }),\n    extract: vi.fn(\n      async (\n        _instruction: unknown,\n        _schema: unknown,\n        options?: { model?: unknown },\n      ) => {\n        calls.push({ method: \"extract\", model: options?.model });\n        return { extraction: \"data\" };\n      },\n    ),\n    observe: vi.fn(\n      async (_instruction: unknown, options?: { model?: unknown }) => {\n        calls.push({ method: \"observe\", model: options?.model });\n        return [];\n      },\n    ),\n    calls,\n  };\n\n  return mock as unknown as V3 & { calls: typeof calls };\n}\n\ndescribe(\"agent tools pass full executionModel config to v3 methods\", () => {\n  const modelConfig = {\n    modelName: \"openai/gpt-4o-mini\",\n    apiKey: \"sk-test-key\",\n    baseURL: \"https://custom.api\",\n  };\n\n  it(\"actTool passes AgentModelConfig object to v3.act()\", async () => {\n    const v3 = createMockV3();\n    const tool = actTool(v3, modelConfig);\n    await tool.execute!(\n      { action: \"click the button\" },\n      {\n        toolCallId: \"t1\",\n        messages: [],\n        abortSignal: new AbortController().signal,\n      },\n    );\n\n    expect(v3.calls).toHaveLength(1);\n    expect(v3.calls[0].method).toBe(\"act\");\n    expect(v3.calls[0].model).toBe(modelConfig);\n  });\n\n  it(\"extractTool passes AgentModelConfig object to v3.extract()\", async () => {\n    const v3 = createMockV3();\n    const tool = extractTool(v3, modelConfig);\n    await tool.execute!(\n      { instruction: \"get the title\", schema: undefined },\n      {\n        toolCallId: \"t2\",\n        messages: [],\n        abortSignal: new AbortController().signal,\n      },\n    );\n\n    expect(v3.calls).toHaveLength(1);\n    expect(v3.calls[0].method).toBe(\"extract\");\n    expect(v3.calls[0].model).toBe(modelConfig);\n  });\n\n  it(\"fillFormTool passes AgentModelConfig object to v3.observe()\", async () => {\n    const v3 = createMockV3();\n    const tool = fillFormTool(v3, modelConfig);\n    await tool.execute!(\n      { fields: [{ action: \"type hello into name\" }] },\n      {\n        toolCallId: \"t3\",\n        messages: [],\n        abortSignal: new AbortController().signal,\n      },\n    );\n\n    expect(v3.calls).toHaveLength(1);\n    expect(v3.calls[0].method).toBe(\"observe\");\n    expect(v3.calls[0].model).toBe(modelConfig);\n  });\n\n  it(\"actTool passes undefined when no executionModel is set\", async () => {\n    const v3 = createMockV3();\n    const tool = actTool(v3, undefined);\n    await tool.execute!(\n      { action: \"click the button\" },\n      {\n        toolCallId: \"t4\",\n        messages: [],\n        abortSignal: new AbortController().signal,\n      },\n    );\n\n    expect(v3.calls).toHaveLength(1);\n    expect(v3.calls[0].model).toBeUndefined();\n  });\n\n  it(\"actTool passes plain string executionModel to v3.act()\", async () => {\n    const v3 = createMockV3();\n    const tool = actTool(v3, \"openai/gpt-4o-mini\");\n    await tool.execute!(\n      { action: \"click the button\" },\n      {\n        toolCallId: \"t5\",\n        messages: [],\n        abortSignal: new AbortController().signal,\n      },\n    );\n\n    expect(v3.calls).toHaveLength(1);\n    expect(v3.calls[0].model).toBe(\"openai/gpt-4o-mini\");\n  });\n});\n\ndescribe(\"executionModel fallback logic\", () => {\n  // This mirrors the resolution in V3.prepareAgentExecution (v3.ts:1682):\n  //   const resolvedExecutionModel = options?.executionModel ?? options?.model;\n  function resolveExecutionModel(options?: {\n    executionModel?: string | { modelName: string };\n    model?: string | { modelName: string };\n  }) {\n    return options?.executionModel ?? options?.model;\n  }\n\n  it(\"prefers explicit executionModel over model\", () => {\n    const result = resolveExecutionModel({\n      executionModel: \"openai/gpt-4o-mini\",\n      model: \"anthropic/claude-sonnet-4-20250514\",\n    });\n    expect(result).toBe(\"openai/gpt-4o-mini\");\n  });\n\n  it(\"falls back to model when executionModel is not set\", () => {\n    const modelConfig = {\n      modelName: \"anthropic/claude-sonnet-4-20250514\",\n      apiKey: \"sk-test\",\n    };\n    const result = resolveExecutionModel({ model: modelConfig });\n    expect(result).toBe(modelConfig);\n  });\n\n  it(\"returns undefined when neither is set\", () => {\n    expect(resolveExecutionModel({})).toBeUndefined();\n    expect(resolveExecutionModel(undefined)).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/api-multiregion.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { getApiUrlForRegion, REGION_API_URLS } from \"../../lib/v3/api\";\n\ndescribe(\"Multi-region API URL mapping\", () => {\n  describe(\"REGION_API_URLS constant\", () => {\n    it(\"should have the correct URL for us-west-2 (default)\", () => {\n      expect(REGION_API_URLS[\"us-west-2\"]).toBe(\n        \"https://api.stagehand.browserbase.com\",\n      );\n    });\n\n    it(\"should have the correct URL for us-east-1\", () => {\n      expect(REGION_API_URLS[\"us-east-1\"]).toBe(\n        \"https://api.use1.stagehand.browserbase.com\",\n      );\n    });\n\n    it(\"should have the correct URL for eu-central-1\", () => {\n      expect(REGION_API_URLS[\"eu-central-1\"]).toBe(\n        \"https://api.euc1.stagehand.browserbase.com\",\n      );\n    });\n\n    it(\"should have the correct URL for ap-southeast-1\", () => {\n      expect(REGION_API_URLS[\"ap-southeast-1\"]).toBe(\n        \"https://api.apse1.stagehand.browserbase.com\",\n      );\n    });\n  });\n\n  describe(\"getApiUrlForRegion\", () => {\n    it(\"should return the correct URL for us-west-2\", () => {\n      expect(getApiUrlForRegion(\"us-west-2\")).toBe(\n        \"https://api.stagehand.browserbase.com/v1\",\n      );\n    });\n\n    it(\"should return the correct URL for us-east-1\", () => {\n      expect(getApiUrlForRegion(\"us-east-1\")).toBe(\n        \"https://api.use1.stagehand.browserbase.com/v1\",\n      );\n    });\n\n    it(\"should return the correct URL for eu-central-1\", () => {\n      expect(getApiUrlForRegion(\"eu-central-1\")).toBe(\n        \"https://api.euc1.stagehand.browserbase.com/v1\",\n      );\n    });\n\n    it(\"should return the correct URL for ap-southeast-1\", () => {\n      expect(getApiUrlForRegion(\"ap-southeast-1\")).toBe(\n        \"https://api.apse1.stagehand.browserbase.com/v1\",\n      );\n    });\n\n    it(\"should return the default us-west-2 URL when no region is specified\", () => {\n      expect(getApiUrlForRegion(undefined)).toBe(\n        \"https://api.stagehand.browserbase.com/v1\",\n      );\n    });\n\n    it(\"should return the default us-west-2 URL for unknown regions\", () => {\n      // @ts-expect-error - testing invalid region\n      expect(getApiUrlForRegion(\"invalid-region\")).toBe(\n        \"https://api.stagehand.browserbase.com/v1\",\n      );\n    });\n  });\n\n  describe(\"URL /v1 suffix handling\", () => {\n    it(\"getApiUrlForRegion always includes /v1 suffix for consistency\", () => {\n      // getApiUrlForRegion returns a URL with /v1\n      // This documents the expected contract that all API base URLs include /v1\n      const url = getApiUrlForRegion(\"us-west-2\");\n      expect(url.endsWith(\"/v1\")).toBe(true);\n    });\n\n    it(\"all regional URLs should be base URLs without /v1 in REGION_API_URLS\", () => {\n      // Verify REGION_API_URLS contains base URLs (without /v1)\n      // The /v1 suffix is added by getApiUrlForRegion\n      for (const [region, baseUrl] of Object.entries(REGION_API_URLS)) {\n        expect(baseUrl.endsWith(\"/v1\")).toBe(false);\n        expect(getApiUrlForRegion(region as keyof typeof REGION_API_URLS)).toBe(\n          `${baseUrl}/v1`,\n        );\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/browserbase-session-accessors.test.ts",
    "content": "import { describe, expect, it, vi, beforeEach, afterEach } from \"vitest\";\nimport { V3 } from \"../../lib/v3/v3.js\";\n\nconst MOCK_SESSION_ID = \"session-123\";\nconst MOCK_SESSION_URL = `https://www.browserbase.com/sessions/${MOCK_SESSION_ID}`;\nconst MOCK_DEBUG_URL = `https://debug.browserbase.com/${MOCK_SESSION_ID}`;\n\nvi.mock(\"../../lib/v3/understudy/context\", () => {\n  class MockConnection {\n    onTransportClosed = vi.fn();\n    offTransportClosed = vi.fn();\n    send = vi.fn(async () => {});\n  }\n\n  class MockV3Context {\n    static async create(): Promise<MockV3Context> {\n      return new MockV3Context();\n    }\n\n    conn = new MockConnection();\n\n    pages(): never[] {\n      return [];\n    }\n\n    async close(): Promise<void> {\n      // noop\n    }\n  }\n\n  return { V3Context: MockV3Context };\n});\n\nvi.mock(\"../../lib/v3/launch/browserbase\", () => ({\n  createBrowserbaseSession: vi.fn(async () => ({\n    ws: \"wss://mock-browserbase\",\n    sessionId: MOCK_SESSION_ID,\n    bb: {\n      sessions: {\n        debug: vi.fn(async () => ({ debuggerUrl: MOCK_DEBUG_URL })),\n      },\n    },\n  })),\n}));\n\nvi.mock(\"../../lib/v3/launch/local\", () => ({\n  launchLocalChrome: vi.fn(async () => ({\n    ws: \"ws://local-cdp\",\n    chrome: { kill: vi.fn(async () => {}) },\n  })),\n}));\n\ndescribe(\"browserbase accessors\", () => {\n  beforeEach(() => {\n    process.env.BROWSERBASE_API_KEY = \"fake-key\";\n    process.env.BROWSERBASE_PROJECT_ID = \"fake-project\";\n  });\n\n  afterEach(() => {\n    delete process.env.BROWSERBASE_API_KEY;\n    delete process.env.BROWSERBASE_PROJECT_ID;\n    vi.clearAllMocks();\n  });\n\n  it(\"exposes Browserbase session and debug URLs after init\", async () => {\n    const v3 = new V3({\n      env: \"BROWSERBASE\",\n      disableAPI: true,\n      verbose: 0,\n    });\n\n    try {\n      await v3.init();\n\n      expect(v3.browserbaseSessionURL).toBe(MOCK_SESSION_URL);\n      expect(v3.browserbaseDebugURL).toBe(MOCK_DEBUG_URL);\n      expect(v3.isCaptchaAutoSolveEnabled).toBe(true);\n    } finally {\n      await v3.close().catch(() => {});\n    }\n  });\n\n  it(\"clears stored URLs after close\", async () => {\n    const v3 = new V3({\n      env: \"BROWSERBASE\",\n      disableAPI: true,\n      verbose: 0,\n    });\n\n    await v3.init();\n    await v3.close();\n\n    expect(v3.browserbaseSessionURL).toBeUndefined();\n    expect(v3.browserbaseDebugURL).toBeUndefined();\n  });\n\n  it(\"disables captcha solving when solveCaptchas is explicitly false\", async () => {\n    const v3 = new V3({\n      env: \"BROWSERBASE\",\n      disableAPI: true,\n      verbose: 0,\n      browserbaseSessionCreateParams: {\n        browserSettings: {\n          solveCaptchas: false,\n        },\n      },\n    });\n\n    try {\n      await v3.init();\n      expect(v3.isCaptchaAutoSolveEnabled).toBe(false);\n    } finally {\n      await v3.close().catch(() => {});\n    }\n  });\n});\n\ndescribe(\"local accessors\", () => {\n  it(\"stay empty for LOCAL environments\", async () => {\n    const v3 = new V3({\n      env: \"LOCAL\",\n      disableAPI: true,\n      verbose: 0,\n      localBrowserLaunchOptions: {\n        cdpUrl: \"ws://local-existing-session\",\n      },\n    });\n\n    try {\n      await v3.init();\n      expect(v3.browserbaseSessionURL).toBeUndefined();\n      expect(v3.browserbaseDebugURL).toBeUndefined();\n    } finally {\n      await v3.close().catch(() => {});\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/cache-llm-resolution.test.ts",
    "content": "import { describe, expect, it, vi } from \"vitest\";\nimport { ActCache } from \"../../lib/v3/cache/ActCache.js\";\nimport { AgentCache } from \"../../lib/v3/cache/AgentCache.js\";\nimport type { CacheStorage } from \"../../lib/v3/cache/CacheStorage.js\";\nimport type { ActHandler } from \"../../lib/v3/handlers/actHandler.js\";\nimport type { LLMClient } from \"../../lib/v3/llm/LLMClient.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\nimport type { V3Context } from \"../../lib/v3/understudy/context.js\";\nimport type {\n  ActCacheContext,\n  CachedActEntry,\n  CachedAgentEntry,\n  AgentCacheContext,\n  AgentReplayActStep,\n} from \"../../lib/v3/types/private/index.js\";\nimport type {\n  Action,\n  AgentResult,\n  AvailableModel,\n} from \"../../lib/v3/types/public/index.js\";\n\nfunction createFakeStorage<T>(entry: T): CacheStorage {\n  return {\n    enabled: true,\n    readJson: vi.fn().mockResolvedValue({ value: entry }),\n    writeJson: vi.fn().mockResolvedValue({}),\n    directory: \"/tmp/cache\",\n  } as unknown as CacheStorage;\n}\n\ndescribe(\"Cache LLM client selection\", () => {\n  it(\"ActCache uses provided override client during replay\", async () => {\n    const action: Action = {\n      selector: \"xpath=/html/body/button\",\n      description: \"click button\",\n      method: \"click\",\n      arguments: [],\n    };\n\n    const entry: CachedActEntry = {\n      version: 1,\n      instruction: \"click button\",\n      url: \"https://example.com\",\n      variableKeys: [],\n      actions: [action],\n      actionDescription: \"click button\",\n      message: \"done\",\n    };\n\n    const storage = createFakeStorage(entry);\n    const handler = {\n      takeDeterministicAction: vi.fn().mockResolvedValue({\n        success: true,\n        message: \"ok\",\n        actionDescription: \"click button\",\n        actions: [action],\n      }),\n    } as unknown as ActHandler;\n    const defaultClient = { id: \"default\" } as unknown as LLMClient;\n    const overrideClient = { id: \"override\" } as unknown as LLMClient;\n\n    const cache = new ActCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => handler,\n      getDefaultLlmClient: () => defaultClient,\n      domSettleTimeoutMs: undefined,\n    });\n\n    const context: ActCacheContext = {\n      instruction: \"click button\",\n      cacheKey: \"abc\",\n      pageUrl: \"https://example.com\",\n      variableKeys: [],\n      variables: undefined,\n    };\n\n    const result = await cache.tryReplay(\n      context,\n      {} as Page,\n      undefined,\n      overrideClient,\n    );\n\n    expect(result?.success).toBe(true);\n    expect(handler.takeDeterministicAction).toHaveBeenCalledTimes(1);\n    const call = vi.mocked(handler.takeDeterministicAction).mock.calls[0];\n    expect(call?.[3]).toBe(overrideClient);\n  });\n\n  it(\"AgentCache uses provided override client during replay\", async () => {\n    const action: Action = {\n      selector: \"xpath=/html/body/input\",\n      description: \"type email\",\n      method: \"type\",\n      arguments: [\"test@example.com\"],\n    };\n\n    const agentStep: AgentReplayActStep = {\n      type: \"act\",\n      instruction: \"type email\",\n      actions: [action],\n    };\n\n    const entry: CachedAgentEntry = {\n      version: 1,\n      instruction: \"fill form\",\n      startUrl: \"https://example.com\",\n      options: {},\n      configSignature: \"sig\",\n      steps: [agentStep],\n      result: { success: true, actions: [] } as AgentResult,\n      timestamp: new Date().toISOString(),\n    };\n\n    const storage = {\n      enabled: true,\n      readJson: vi.fn().mockImplementation(async () => ({ value: entry })),\n      writeJson: vi.fn().mockResolvedValue({}),\n      directory: \"/tmp/cache\",\n    } as unknown as CacheStorage;\n\n    const handler = {\n      takeDeterministicAction: vi.fn().mockResolvedValue({\n        success: true,\n        message: \"ok\",\n        actionDescription: \"type email\",\n        actions: [action],\n      }),\n    } as unknown as ActHandler;\n\n    const fakePage = {} as Page;\n    const ctx = {\n      awaitActivePage: vi.fn().mockResolvedValue(fakePage),\n    } as unknown as V3Context;\n\n    const defaultClient = { id: \"default-agent\" } as unknown as LLMClient;\n    const overrideClient = { id: \"override-agent\" } as unknown as LLMClient;\n\n    const cache = new AgentCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => handler,\n      getContext: () => ctx,\n      getDefaultLlmClient: () => defaultClient,\n      getBaseModelName: () => \"openai/gpt-4.1-mini\" as AvailableModel,\n      getSystemPrompt: () => undefined,\n      domSettleTimeoutMs: undefined,\n      act: vi.fn(),\n    });\n\n    const context: AgentCacheContext = {\n      instruction: \"fill form\",\n      startUrl: \"https://example.com\",\n      options: {},\n      configSignature: \"sig\",\n      cacheKey: \"agent-key\",\n      variableKeys: [],\n    };\n\n    const result = await cache.tryReplay(context, overrideClient);\n\n    expect(result?.success).toBe(true);\n    expect(handler.takeDeterministicAction).toHaveBeenCalledTimes(1);\n    const call = vi.mocked(handler.takeDeterministicAction).mock.calls[0];\n    expect(call?.[3]).toBe(overrideClient);\n  });\n\n  it(\"AgentCache replays non-act steps without requiring an override client\", async () => {\n    const gotoEntry: CachedAgentEntry = {\n      version: 1,\n      instruction: \"navigate home\",\n      startUrl: \"https://example.com/source\",\n      options: {},\n      configSignature: \"sig\",\n      steps: [\n        {\n          type: \"goto\",\n          url: \"https://example.com/target\",\n          waitUntil: \"load\",\n        },\n      ],\n      result: { success: true, actions: [] } as AgentResult,\n      timestamp: new Date().toISOString(),\n    };\n\n    const storage = {\n      enabled: true,\n      readJson: vi.fn().mockResolvedValue({ value: gotoEntry }),\n      writeJson: vi.fn().mockResolvedValue({}),\n      directory: \"/tmp/cache\",\n    } as unknown as CacheStorage;\n\n    const handler = {\n      takeDeterministicAction: vi.fn(),\n    } as unknown as ActHandler;\n\n    const fakePage = { goto: vi.fn() } as unknown as Page;\n    const ctx = {\n      awaitActivePage: vi.fn().mockResolvedValue(fakePage),\n    } as unknown as V3Context;\n\n    const cache = new AgentCache({\n      storage,\n      logger: vi.fn(),\n      getActHandler: () => handler,\n      getContext: () => ctx,\n      getDefaultLlmClient: () => ({ id: \"default\" }) as unknown as LLMClient,\n      getBaseModelName: () => \"openai/gpt-4.1-mini\" as AvailableModel,\n      getSystemPrompt: () => undefined,\n      domSettleTimeoutMs: undefined,\n      act: vi.fn(),\n    });\n\n    const context: AgentCacheContext = {\n      instruction: \"navigate home\",\n      startUrl: \"https://example.com/source\",\n      options: {},\n      configSignature: \"sig\",\n      cacheKey: \"agent-goto\",\n      variableKeys: [],\n    };\n\n    const result = await cache.tryReplay(context);\n\n    expect(result?.success).toBe(true);\n    expect(handler.takeDeterministicAction).not.toHaveBeenCalled();\n    expect(fakePage.goto).toHaveBeenCalledWith(\"https://example.com/target\", {\n      waitUntil: \"load\",\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/captcha-solver.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { CaptchaSolver } from \"../../lib/v3/agent/utils/captchaSolver.js\";\n\nconst SOLVING_STARTED = \"browserbase-solving-started\";\nconst SOLVING_FINISHED = \"browserbase-solving-finished\";\nconst SOLVING_ERRORED = \"browserbase-solving-errored\";\n\ntype ConsoleListener = (message: { text: () => string }) => void;\n\nclass MockPage {\n  private listeners = new Set<ConsoleListener>();\n  public onCalls = 0;\n  public offCalls = 0;\n\n  on(event: string, listener: ConsoleListener): void {\n    if (event !== \"console\") return;\n    this.onCalls++;\n    this.listeners.add(listener);\n  }\n\n  off(event: string, listener: ConsoleListener): void {\n    if (event !== \"console\") return;\n    this.offCalls++;\n    this.listeners.delete(listener);\n  }\n\n  emitConsole(text: string): void {\n    const message = { text: () => text };\n    for (const listener of this.listeners) {\n      listener(message);\n    }\n  }\n\n  listenerCount(): number {\n    return this.listeners.size;\n  }\n}\n\ndescribe(\"CaptchaSolver\", () => {\n  it(\"resolves all concurrent waiters when a solve finishes\", async () => {\n    const page = new MockPage();\n    const solver = new CaptchaSolver();\n    solver.init(async () => page as never);\n\n    await solver.ensureAttached();\n    page.emitConsole(SOLVING_STARTED);\n\n    const firstWait = solver.waitIfSolving();\n    const secondWait = solver.waitIfSolving();\n    await new Promise((resolve) => setTimeout(resolve, 0));\n\n    const sharedWaitPromise = (\n      solver as unknown as { waitPromise: Promise<void> | null }\n    ).waitPromise;\n\n    expect(sharedWaitPromise).not.toBeNull();\n    expect(\n      (solver as unknown as { waitPromise: Promise<void> | null }).waitPromise,\n    ).toBe(sharedWaitPromise);\n\n    let firstResolved = false;\n    let secondResolved = false;\n    void firstWait.then(() => {\n      firstResolved = true;\n    });\n    void secondWait.then(() => {\n      secondResolved = true;\n    });\n\n    await Promise.resolve();\n    expect(firstResolved).toBe(false);\n    expect(secondResolved).toBe(false);\n\n    page.emitConsole(SOLVING_FINISHED);\n    await Promise.all([firstWait, secondWait]);\n\n    expect(firstResolved).toBe(true);\n    expect(secondResolved).toBe(true);\n    expect(solver.consumeSolveResult()).toEqual({\n      solved: true,\n      errored: false,\n    });\n    expect(solver.consumeSolveResult()).toEqual({\n      solved: false,\n      errored: false,\n    });\n  });\n\n  it(\"re-attaches to a new page and settles stale waiters when the active page changes\", async () => {\n    const firstPage = new MockPage();\n    const secondPage = new MockPage();\n    let activePage = firstPage;\n\n    const solver = new CaptchaSolver();\n    solver.init(async () => activePage as never);\n\n    await solver.ensureAttached();\n    firstPage.emitConsole(SOLVING_STARTED);\n\n    const pendingWait = solver.waitIfSolving();\n    let settled = false;\n    void pendingWait.then(() => {\n      settled = true;\n    });\n\n    activePage = secondPage;\n    await solver.waitIfSolving();\n    await pendingWait;\n\n    expect(settled).toBe(true);\n    expect(firstPage.offCalls).toBe(1);\n    expect(firstPage.listenerCount()).toBe(0);\n    expect(secondPage.onCalls).toBe(1);\n    expect(secondPage.listenerCount()).toBe(1);\n    expect(solver.isSolving()).toBe(false);\n  });\n\n  it(\"surfaces solver errors exactly once per consume\", async () => {\n    const page = new MockPage();\n    const solver = new CaptchaSolver();\n    solver.init(async () => page as never);\n\n    await solver.ensureAttached();\n    page.emitConsole(SOLVING_STARTED);\n\n    const wait = solver.waitIfSolving();\n    page.emitConsole(SOLVING_ERRORED);\n    await wait;\n\n    expect(solver.consumeSolveResult()).toEqual({\n      solved: false,\n      errored: true,\n    });\n    expect(solver.consumeSolveResult()).toEqual({\n      solved: false,\n      errored: false,\n    });\n  });\n\n  it(\"disposes cleanly while a solve is in progress\", async () => {\n    const page = new MockPage();\n    const solver = new CaptchaSolver();\n    solver.init(async () => page as never);\n\n    await solver.ensureAttached();\n    page.emitConsole(SOLVING_STARTED);\n\n    const wait = solver.waitIfSolving();\n    await new Promise((resolve) => setTimeout(resolve, 0));\n    let settled = false;\n    void wait.then(() => {\n      settled = true;\n    });\n\n    solver.dispose();\n    await wait;\n\n    expect(settled).toBe(true);\n    expect(solver.isSolving()).toBe(false);\n    expect(page.listenerCount()).toBe(0);\n    expect(solver.consumeSolveResult()).toEqual({\n      solved: false,\n      errored: false,\n    });\n  });\n\n  it(\"marks errored when detached mid-solve due to page change\", async () => {\n    const firstPage = new MockPage();\n    const secondPage = new MockPage();\n    let activePage = firstPage;\n\n    const solver = new CaptchaSolver();\n    solver.init(async () => activePage as never);\n\n    await solver.ensureAttached();\n    firstPage.emitConsole(SOLVING_STARTED);\n\n    const wait = solver.waitIfSolving();\n\n    // Switch to a new page while the solve is in progress\n    activePage = secondPage;\n    await solver.waitIfSolving();\n    await wait;\n\n    // The interrupted solve should be reported as errored\n    expect(solver.consumeSolveResult()).toEqual({\n      solved: false,\n      errored: true,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/cdp-connection-close.test.ts",
    "content": "import { describe, it, expect, afterEach } from \"vitest\";\nimport { WebSocketServer, type WebSocket as ServerWebSocket } from \"ws\";\nimport { CdpConnection } from \"../../lib/v3/understudy/cdp.js\";\n\n/**\n * Races a promise against a timeout. Returns \"resolved\" if the promise\n * settles before the deadline, or \"timeout\" if it doesn't.\n */\n// TODO: dedupe this with the implementation in testUtils.ts after we unify the test directories\nfunction raceTimeout<T>(\n  promise: Promise<T>,\n  ms: number,\n): Promise<T | \"timeout\"> {\n  let timer: ReturnType<typeof setTimeout>;\n  const timeout = new Promise<\"timeout\">((resolve) => {\n    timer = setTimeout(() => resolve(\"timeout\"), ms);\n  });\n  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));\n}\n\n/**\n * Creates a local WebSocket server and connects a CdpConnection to it.\n * Returns the connection plus a handle to the server-side socket.\n */\nasync function createPair(): Promise<{\n  conn: CdpConnection;\n  serverSocket: ServerWebSocket;\n  wss: WebSocketServer;\n}> {\n  const wss = new WebSocketServer({ port: 0 });\n  const port = (wss.address() as { port: number }).port;\n\n  const serverSocketPromise = new Promise<ServerWebSocket>((resolve) => {\n    wss.once(\"connection\", resolve);\n  });\n\n  const conn = await CdpConnection.connect(`ws://localhost:${port}`);\n  const serverSocket = await serverSocketPromise;\n\n  return { conn, serverSocket, wss };\n}\n\ndescribe(\"CdpConnection\", () => {\n  let wss: WebSocketServer | null = null;\n\n  afterEach(async () => {\n    if (wss) {\n      await new Promise<void>((resolve) => wss!.close(() => resolve()));\n      wss = null;\n    }\n  });\n\n  describe(\"close() when WebSocket is already closed\", () => {\n    it(\"resolves instead of hanging forever\", async () => {\n      const pair = await createPair();\n      wss = pair.wss;\n\n      // Wait for the client-side close event to be fully processed.\n      const transportClosed = new Promise<void>((resolve) => {\n        pair.conn.onTransportClosed(() => resolve());\n      });\n\n      // Simulate the hosted API terminating the Browserbase session:\n      // the server closes the WebSocket from its side.\n      pair.serverSocket.close();\n      await transportClosed;\n\n      // conn.close() on an already-CLOSED WebSocket must resolve.\n      // Without the fix it awaits a \"close\" event that already fired → hangs.\n      const result = await raceTimeout(\n        pair.conn.close().then(() => \"resolved\"),\n        3_000,\n      );\n\n      expect(result).toBe(\"resolved\");\n    });\n  });\n\n  describe(\"inflight CDP calls on unexpected close\", () => {\n    it(\"rejects pending calls instead of hanging forever\", async () => {\n      const pair = await createPair();\n      wss = pair.wss;\n\n      // Send a CDP command; the mock server will never reply.\n      const pending = pair.conn.send(\"Runtime.evaluate\", {\n        expression: \"1+1\",\n      });\n\n      // Server terminates the connection while the call is inflight.\n      pair.serverSocket.close();\n\n      // The pending promise must reject, not hang.\n      const result = await raceTimeout(\n        pending.then(() => \"resolved\").catch(() => \"rejected\"),\n        3_000,\n      );\n\n      expect(result).toBe(\"rejected\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/context-extra-http-headers.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { V3Context } from \"../../lib/v3/understudy/context.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\nimport { StagehandSetExtraHTTPHeadersError } from \"../../lib/v3/types/public/sdkErrors.js\";\n\ntype ContextStub = {\n  _sessionInit: Set<string>;\n  conn: {\n    getSession: (id: string) => MockCDPSession | undefined;\n  };\n  extraHttpHeaders: Record<string, string> | null;\n};\n\nconst makeContext = (sessions: MockCDPSession[]): ContextStub => {\n  const sessionsById = new Map(\n    sessions.map((session) => [session.id, session]),\n  );\n  return {\n    _sessionInit: new Set(sessions.map((session) => session.id)),\n    conn: {\n      getSession: (id: string) => sessionsById.get(id),\n    },\n    extraHttpHeaders: null,\n  };\n};\n\ndescribe(\"V3Context.setExtraHTTPHeaders\", () => {\n  const setExtraHTTPHeaders = V3Context.prototype.setExtraHTTPHeaders as (\n    this: ContextStub,\n    headers: Record<string, string>,\n  ) => Promise<void>;\n\n  it(\"sends headers to all sessions\", async () => {\n    const sessionA = new MockCDPSession({}, \"session-a\");\n    const sessionB = new MockCDPSession({}, \"session-b\");\n    const ctx = makeContext([sessionA, sessionB]);\n\n    await setExtraHTTPHeaders.call(ctx, {\n      \"x-stagehand-test\": \"yes\",\n    });\n\n    for (const session of [sessionA, sessionB]) {\n      expect(session.callsFor(\"Network.enable\").length).toBe(1);\n      expect(\n        session.callsFor(\"Network.setExtraHTTPHeaders\")[0]?.params,\n      ).toEqual({\n        headers: { \"x-stagehand-test\": \"yes\" },\n      });\n    }\n  });\n\n  it(\"throws a custom error with session failure details\", async () => {\n    const sessionA = new MockCDPSession(\n      {\n        \"Network.setExtraHTTPHeaders\": () => {\n          throw new Error(\"boom\");\n        },\n      },\n      \"session-a\",\n    );\n    const sessionB = new MockCDPSession({}, \"session-b\");\n    const ctx = makeContext([sessionA, sessionB]);\n\n    const promise = setExtraHTTPHeaders.call(ctx, {\n      \"x-stagehand-test\": \"yes\",\n    });\n\n    await expect(promise).rejects.toBeInstanceOf(\n      StagehandSetExtraHTTPHeadersError,\n    );\n\n    try {\n      await promise;\n    } catch (error) {\n      const err = error as StagehandSetExtraHTTPHeadersError;\n      expect(err.failures).toHaveLength(1);\n      expect(err.failures[0]).toContain(\"session=session-a\");\n      expect(err.failures[0]).toContain(\"boom\");\n    }\n\n    expect(sessionA.callsFor(\"Network.setExtraHTTPHeaders\").length).toBe(1);\n    expect(sessionB.callsFor(\"Network.setExtraHTTPHeaders\").length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/cookies.test.ts",
    "content": "import { beforeEach, describe, expect, it } from \"vitest\";\nimport {\n  filterCookies,\n  normalizeCookieParams,\n  cookieMatchesFilter,\n} from \"../../lib/v3/understudy/cookies.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\nimport type { V3Context } from \"../../lib/v3/understudy/context.js\";\nimport { Cookie, CookieParam } from \"../../lib/v3/types/public/context.js\";\n\nfunction makeCookie(overrides: Partial<Cookie> = {}): Cookie {\n  return {\n    name: \"sid\",\n    value: \"abc123\",\n    domain: \"example.com\",\n    path: \"/\",\n    expires: -1,\n    httpOnly: false,\n    secure: false,\n    sameSite: \"Lax\",\n    ...overrides,\n  };\n}\n\n/** Convert our Cookie type into the shape CDP's Storage.getCookies returns. */\nfunction toCdpCookie(c: Cookie) {\n  return {\n    name: c.name,\n    value: c.value,\n    domain: c.domain,\n    path: c.path,\n    expires: c.expires,\n    httpOnly: c.httpOnly,\n    secure: c.secure,\n    sameSite: c.sameSite,\n    size: c.name.length + c.value.length,\n    session: c.expires === -1,\n    priority: \"Medium\",\n    sameParty: false,\n    sourceScheme: \"Secure\",\n    sourcePort: 443,\n  };\n}\n\ndescribe(\"filterCookies\", () => {\n  const cookies: Cookie[] = [\n    makeCookie({ name: \"a\", domain: \"example.com\", path: \"/\", secure: false }),\n    makeCookie({\n      name: \"b\",\n      domain: \".example.com\",\n      path: \"/app\",\n      secure: true,\n    }),\n    makeCookie({ name: \"c\", domain: \"other.com\", path: \"/\", secure: false }),\n    makeCookie({\n      name: \"d\",\n      domain: \"sub.example.com\",\n      path: \"/\",\n      secure: false,\n    }),\n  ];\n\n  it(\"returns all cookies when urls is empty\", () => {\n    expect(filterCookies(cookies, [])).toEqual(cookies);\n  });\n\n  it(\"filters by domain (exact host match)\", () => {\n    const result = filterCookies(cookies, [\"http://example.com/\"]);\n    const names = result.map((c) => c.name);\n    expect(names).toContain(\"a\");\n    // \"b\" (.example.com) domain-matches but is secure — excluded on http://\n    expect(names).not.toContain(\"b\");\n    expect(names).not.toContain(\"c\");\n    expect(names).not.toContain(\"d\");\n  });\n\n  it(\"filters by domain (dot-prefixed domain matches on https)\", () => {\n    const result = filterCookies(cookies, [\"https://example.com/app/settings\"]);\n    const names = result.map((c) => c.name);\n    expect(names).toContain(\"a\"); // example.com domain match, path \"/\" prefix\n    expect(names).toContain(\"b\"); // .example.com domain match + secure + https\n  });\n\n  it(\"filters by domain (subdomain matches dot-prefixed domain)\", () => {\n    const result = filterCookies(cookies, [\"http://sub.example.com/\"]);\n    const names = result.map((c) => c.name);\n    // \"a\" (example.com) → prepend dot → .example.com → matches .sub.example.com\n    expect(names).toContain(\"a\");\n    // \"b\" (.example.com) domain-matches sub.example.com but is secure — excluded on http://\n    expect(names).not.toContain(\"b\");\n    expect(names).toContain(\"d\"); // sub.example.com matches exactly\n    expect(names).not.toContain(\"c\");\n  });\n\n  it(\"filters by path prefix\", () => {\n    const result = filterCookies(cookies, [\"https://example.com/app/settings\"]);\n    const names = result.map((c) => c.name);\n    expect(names).toContain(\"a\"); // path \"/\" is a prefix of \"/app/settings\"\n    expect(names).toContain(\"b\"); // path \"/app\" is a prefix of \"/app/settings\"\n  });\n\n  it(\"excludes secure cookies for non-https URLs\", () => {\n    const result = filterCookies(cookies, [\"http://example.com/app/page\"]);\n    const names = result.map((c) => c.name);\n    expect(names).toContain(\"a\");\n    expect(names).not.toContain(\"b\"); // secure cookie, http URL\n  });\n\n  it(\"allows secure cookies on loopback addresses regardless of protocol\", () => {\n    const cases = [\n      { domain: \"localhost\", url: \"http://localhost/\" },\n      { domain: \"127.0.0.1\", url: \"http://127.0.0.1/\" },\n      { domain: \"[::1]\", url: \"http://[::1]/\" },\n    ];\n    for (const { domain, url } of cases) {\n      const cookie = makeCookie({ name: \"loop\", domain, secure: true });\n      const result = filterCookies([cookie], [url]);\n      expect(result).toHaveLength(1);\n      expect(result[0]!.name).toBe(\"loop\");\n    }\n  });\n\n  it(\"matches against multiple URLs (union)\", () => {\n    const result = filterCookies(cookies, [\n      \"http://example.com/\",\n      \"http://other.com/\",\n    ]);\n    const names = result.map((c) => c.name);\n    expect(names).toContain(\"a\");\n    expect(names).toContain(\"c\");\n  });\n\n  it(\"returns empty array when no cookies match any URL\", () => {\n    const result = filterCookies(cookies, [\"http://nomatch.dev/\"]);\n    expect(result).toHaveLength(0);\n  });\n\n  it(\"returns empty array when cookie list is empty\", () => {\n    const result = filterCookies([], [\"http://example.com/\"]);\n    expect(result).toHaveLength(0);\n  });\n\n  it(\"does not match a sibling subdomain against a host-only domain\", () => {\n    // Cookie for \"api.example.com\" should NOT match \"www.example.com\"\n    const apiCookie = makeCookie({ name: \"api\", domain: \"api.example.com\" });\n    const result = filterCookies([apiCookie], [\"http://www.example.com/\"]);\n    expect(result).toHaveLength(0);\n  });\n\n  it(\"does not match a parent domain against a more specific cookie\", () => {\n    // Cookie for \"sub.example.com\" should NOT match \"example.com\"\n    const subCookie = makeCookie({ name: \"sub\", domain: \"sub.example.com\" });\n    const result = filterCookies([subCookie], [\"http://example.com/\"]);\n    expect(result).toHaveLength(0);\n  });\n\n  it(\"does not match when path does not prefix the URL path\", () => {\n    const deepCookie = makeCookie({\n      name: \"deep\",\n      domain: \"example.com\",\n      path: \"/admin\",\n    });\n    const result = filterCookies([deepCookie], [\"http://example.com/public\"]);\n    expect(result).toHaveLength(0);\n  });\n\n  it(\"does not match when cookie path is a string prefix but not a path boundary\", () => {\n    // \"/foo\" should NOT match \"/foobar\" — only \"/foo\", \"/foo/\", \"/foo/bar\"\n    const cookie = makeCookie({\n      name: \"boundary\",\n      domain: \"example.com\",\n      path: \"/foo\",\n    });\n    expect(filterCookies([cookie], [\"http://example.com/foobar\"])).toHaveLength(\n      0,\n    );\n    expect(filterCookies([cookie], [\"http://example.com/foo\"])).toHaveLength(1);\n    expect(\n      filterCookies([cookie], [\"http://example.com/foo/bar\"]),\n    ).toHaveLength(1);\n  });\n\n  it(\"matches root path against any URL path\", () => {\n    const rootCookie = makeCookie({\n      name: \"root\",\n      domain: \"example.com\",\n      path: \"/\",\n    });\n    const result = filterCookies(\n      [rootCookie],\n      [\"http://example.com/deeply/nested/page\"],\n    );\n    expect(result).toHaveLength(1);\n  });\n\n  it(\"handles URL with port numbers\", () => {\n    const c = makeCookie({ name: \"port\", domain: \"localhost\", path: \"/\" });\n    const result = filterCookies([c], [\"http://localhost:3000/api\"]);\n    expect(result).toHaveLength(1);\n  });\n\n  it(\"handles URL with query string and fragment\", () => {\n    const c = makeCookie({ name: \"q\", domain: \"example.com\", path: \"/\" });\n    const result = filterCookies(\n      [c],\n      [\"http://example.com/page?q=1&r=2#section\"],\n    );\n    expect(result).toHaveLength(1);\n  });\n\n  it(\"throws CookieValidationError for malformed URL\", () => {\n    const c = makeCookie({ name: \"a\", domain: \"example.com\" });\n    expect(() => filterCookies([c], [\"not-a-valid-url\"])).toThrow(\n      /Invalid URL passed to cookies\\(\\)/,\n    );\n  });\n});\n\ndescribe(\"normalizeCookieParams\", () => {\n  it(\"passes through cookies with domain+path\", () => {\n    const input: CookieParam[] = [\n      { name: \"a\", value: \"1\", domain: \"example.com\", path: \"/\" },\n    ];\n    const result = normalizeCookieParams(input);\n    expect(result[0]!.domain).toBe(\"example.com\");\n    expect(result[0]!.path).toBe(\"/\");\n    expect(result[0]!.url).toBeUndefined();\n  });\n\n  it(\"derives domain, path, and secure from url\", () => {\n    const input: CookieParam[] = [\n      { name: \"a\", value: \"1\", url: \"https://example.com/app/page\" },\n    ];\n    const result = normalizeCookieParams(input);\n    expect(result[0]!.domain).toBe(\"example.com\");\n    expect(result[0]!.path).toBe(\"/app/\");\n    expect(result[0]!.secure).toBe(true);\n    expect(result[0]!.url).toBeUndefined();\n  });\n\n  it(\"sets secure to false for http urls\", () => {\n    const input: CookieParam[] = [\n      { name: \"a\", value: \"1\", url: \"http://example.com/\" },\n    ];\n    const result = normalizeCookieParams(input);\n    expect(result[0]!.secure).toBe(false);\n  });\n\n  it(\"throws when neither url nor domain+path is provided\", () => {\n    expect(() => normalizeCookieParams([{ name: \"a\", value: \"1\" }])).toThrow(\n      /must have a url or a domain\\/path pair/,\n    );\n  });\n\n  it(\"throws when both url and domain are provided\", () => {\n    expect(() =>\n      normalizeCookieParams([\n        { name: \"a\", value: \"1\", url: \"https://x.com/\", domain: \"x.com\" },\n      ]),\n    ).toThrow(/should have either url or domain/);\n  });\n\n  it(\"throws when both url and path are provided\", () => {\n    expect(() =>\n      normalizeCookieParams([\n        { name: \"a\", value: \"1\", url: \"https://x.com/\", path: \"/\" },\n      ]),\n    ).toThrow(/should have either url or path/);\n  });\n\n  it(\"throws for invalid expires (negative, not -1)\", () => {\n    expect(() =>\n      normalizeCookieParams([\n        { name: \"a\", value: \"1\", domain: \"x.com\", path: \"/\", expires: -5 },\n      ]),\n    ).toThrow(/invalid expires/);\n  });\n\n  it(\"allows expires of -1 (session cookie)\", () => {\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", domain: \"x.com\", path: \"/\", expires: -1 },\n    ]);\n    expect(result[0]!.expires).toBe(-1);\n  });\n\n  it(\"allows a positive expires timestamp\", () => {\n    const future = Math.floor(Date.now() / 1000) + 3600;\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", domain: \"x.com\", path: \"/\", expires: future },\n    ]);\n    expect(result[0]!.expires).toBe(future);\n  });\n\n  it(\"throws for about:blank url\", () => {\n    expect(() =>\n      normalizeCookieParams([{ name: \"a\", value: \"1\", url: \"about:blank\" }]),\n    ).toThrow(/Blank page/);\n  });\n\n  it(\"throws for data: url\", () => {\n    expect(() =>\n      normalizeCookieParams([\n        { name: \"a\", value: \"1\", url: \"data:text/html,hi\" },\n      ]),\n    ).toThrow(/Data URL/);\n  });\n\n  it(\"throws CookieValidationError for malformed url\", () => {\n    expect(() =>\n      normalizeCookieParams([{ name: \"a\", value: \"1\", url: \"not-a-url\" }]),\n    ).toThrow(/Cookie \"a\" has an invalid url/);\n  });\n\n  it(\"throws when sameSite is None but secure is false\", () => {\n    expect(() =>\n      normalizeCookieParams([\n        {\n          name: \"a\",\n          value: \"1\",\n          domain: \"x.com\",\n          path: \"/\",\n          sameSite: \"None\",\n          secure: false,\n        },\n      ]),\n    ).toThrow(/sameSite: \"None\" without secure: true/);\n  });\n\n  it(\"throws when sameSite is None and secure is omitted (undefined)\", () => {\n    // CDP defaults secure to false when omitted, so the browser will reject it.\n    expect(() =>\n      normalizeCookieParams([\n        { name: \"a\", value: \"1\", domain: \"x.com\", path: \"/\", sameSite: \"None\" },\n      ]),\n    ).toThrow(/sameSite: \"None\" without secure: true/);\n  });\n\n  it(\"does NOT throw when sameSite is None and secure is true\", () => {\n    const result = normalizeCookieParams([\n      {\n        name: \"a\",\n        value: \"1\",\n        domain: \"x.com\",\n        path: \"/\",\n        sameSite: \"None\",\n        secure: true,\n      },\n    ]);\n    expect(result[0]!.sameSite).toBe(\"None\");\n    expect(result[0]!.secure).toBe(true);\n  });\n\n  it(\"derives root path from URL with no trailing path segments\", () => {\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", url: \"https://example.com\" },\n    ]);\n    // URL(\"https://example.com\").pathname is \"/\", lastIndexOf(\"/\") + 1 = 1 → \"/\"\n    expect(result[0]!.path).toBe(\"/\");\n  });\n\n  it(\"handles URL with port number\", () => {\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", url: \"https://localhost:3000/api/v1\" },\n    ]);\n    expect(result[0]!.domain).toBe(\"localhost\");\n    expect(result[0]!.path).toBe(\"/api/\");\n    expect(result[0]!.secure).toBe(true);\n  });\n\n  it(\"handles URL with query string (ignores query)\", () => {\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", url: \"https://example.com/page?q=1\" },\n    ]);\n    expect(result[0]!.domain).toBe(\"example.com\");\n    expect(result[0]!.path).toBe(\"/\");\n  });\n\n  it(\"normalises multiple cookies in a single call\", () => {\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", url: \"https://one.com/x\" },\n      { name: \"b\", value: \"2\", domain: \"two.com\", path: \"/\" },\n      { name: \"c\", value: \"3\", url: \"http://three.com/y/z\" },\n    ]);\n    expect(result).toHaveLength(3);\n    expect(result[0]!.domain).toBe(\"one.com\");\n    expect(result[1]!.domain).toBe(\"two.com\");\n    expect(result[2]!.domain).toBe(\"three.com\");\n    expect(result[2]!.secure).toBe(false);\n  });\n\n  it(\"does not mutate the original input array\", () => {\n    const input: CookieParam[] = [\n      { name: \"a\", value: \"1\", url: \"https://example.com/app\" },\n    ];\n    const frozen = { ...input[0]! };\n    normalizeCookieParams(input);\n    expect(input[0]).toEqual(frozen);\n  });\n\n  it(\"preserves optional fields that are explicitly set\", () => {\n    const result = normalizeCookieParams([\n      {\n        name: \"full\",\n        value: \"val\",\n        domain: \"x.com\",\n        path: \"/p\",\n        expires: 9999999999,\n        httpOnly: true,\n        secure: true,\n        sameSite: \"Strict\",\n      },\n    ]);\n    const c = result[0]!;\n    expect(c.httpOnly).toBe(true);\n    expect(c.secure).toBe(true);\n    expect(c.sameSite).toBe(\"Strict\");\n    expect(c.expires).toBe(9999999999);\n  });\n\n  it(\"allows expires of 0 (epoch — effectively expired)\", () => {\n    // 0 is a positive-ish edge case; browsers treat it as already expired\n    const result = normalizeCookieParams([\n      { name: \"a\", value: \"1\", domain: \"x.com\", path: \"/\", expires: 0 },\n    ]);\n    expect(result[0]!.expires).toBe(0);\n  });\n\n  it(\"throws on the first invalid cookie in a batch\", () => {\n    expect(() =>\n      normalizeCookieParams([\n        { name: \"ok\", value: \"1\", domain: \"x.com\", path: \"/\" },\n        { name: \"bad\", value: \"2\" }, // missing url/domain+path\n      ]),\n    ).toThrow(/Cookie \"bad\"/);\n  });\n\n  it(\"includes cookie name in every error message\", () => {\n    const cases = [\n      () => normalizeCookieParams([{ name: \"NAMED\", value: \"1\" }]),\n      () =>\n        normalizeCookieParams([\n          { name: \"NAMED\", value: \"1\", url: \"https://x.com/\", domain: \"x\" },\n        ]),\n      () =>\n        normalizeCookieParams([\n          { name: \"NAMED\", value: \"1\", url: \"about:blank\" },\n        ]),\n      () =>\n        normalizeCookieParams([\n          {\n            name: \"NAMED\",\n            value: \"1\",\n            domain: \"x.com\",\n            path: \"/\",\n            sameSite: \"None\",\n            secure: false,\n          },\n        ]),\n    ];\n    for (const fn of cases) {\n      expect(fn).toThrow(/NAMED/);\n    }\n  });\n});\n\ndescribe(\"cookieMatchesFilter\", () => {\n  const cookie = makeCookie({\n    name: \"session\",\n    domain: \".example.com\",\n    path: \"/app\",\n  });\n\n  it(\"matches when all filters match (exact strings)\", () => {\n    expect(\n      cookieMatchesFilter(cookie, {\n        name: \"session\",\n        domain: \".example.com\",\n        path: \"/app\",\n      }),\n    ).toBe(true);\n  });\n\n  it(\"does not match when name differs\", () => {\n    expect(cookieMatchesFilter(cookie, { name: \"other\" })).toBe(false);\n  });\n\n  it(\"does not match when domain differs\", () => {\n    expect(cookieMatchesFilter(cookie, { domain: \"other.com\" })).toBe(false);\n  });\n\n  it(\"does not match when path differs\", () => {\n    expect(cookieMatchesFilter(cookie, { path: \"/other\" })).toBe(false);\n  });\n\n  it(\"matches with regex name\", () => {\n    expect(cookieMatchesFilter(cookie, { name: /^sess/ })).toBe(true);\n    expect(cookieMatchesFilter(cookie, { name: /^nope/ })).toBe(false);\n  });\n\n  it(\"matches with regex domain\", () => {\n    expect(cookieMatchesFilter(cookie, { domain: /example\\.com$/ })).toBe(true);\n    expect(cookieMatchesFilter(cookie, { domain: /^other/ })).toBe(false);\n  });\n\n  it(\"matches with regex path\", () => {\n    expect(cookieMatchesFilter(cookie, { path: /^\\/app/ })).toBe(true);\n  });\n\n  it(\"undefined filters match everything\", () => {\n    expect(cookieMatchesFilter(cookie, {})).toBe(true);\n    expect(cookieMatchesFilter(cookie, { name: undefined })).toBe(true);\n  });\n\n  it(\"requires ALL filters to match (AND logic)\", () => {\n    // name matches but domain does not\n    expect(\n      cookieMatchesFilter(cookie, { name: \"session\", domain: \"wrong.com\" }),\n    ).toBe(false);\n  });\n\n  it(\"handles global regex lastIndex correctly\", () => {\n    const re = /sess/g;\n    re.lastIndex = 999;\n    expect(cookieMatchesFilter(cookie, { name: re })).toBe(true);\n  });\n\n  it(\"exact string does not do substring matching\", () => {\n    // filter name \"sess\" should NOT match cookie name \"session\"\n    expect(cookieMatchesFilter(cookie, { name: \"sess\" })).toBe(false);\n  });\n\n  it(\"regex can do substring matching\", () => {\n    // regex /sess/ SHOULD match cookie name \"session\" (substring)\n    expect(cookieMatchesFilter(cookie, { name: /sess/ })).toBe(true);\n  });\n\n  it(\"works with all three regex filters combined\", () => {\n    expect(\n      cookieMatchesFilter(cookie, {\n        name: /^session$/,\n        domain: /example/,\n        path: /^\\/app$/,\n      }),\n    ).toBe(true);\n\n    // One of three fails\n    expect(\n      cookieMatchesFilter(cookie, {\n        name: /^session$/,\n        domain: /example/,\n        path: /^\\/wrong$/,\n      }),\n    ).toBe(false);\n  });\n\n  it(\"empty string filter only matches empty cookie property\", () => {\n    const emptyPathCookie = makeCookie({\n      name: \"x\",\n      domain: \"a.com\",\n      path: \"\",\n    });\n    expect(cookieMatchesFilter(emptyPathCookie, { path: \"\" })).toBe(true);\n    expect(cookieMatchesFilter(cookie, { path: \"\" })).toBe(false);\n  });\n\n  it(\"is called once per cookie (no cross-contamination between calls)\", () => {\n    const c1 = makeCookie({ name: \"alpha\", domain: \"a.com\", path: \"/\" });\n    const c2 = makeCookie({ name: \"beta\", domain: \"b.com\", path: \"/x\" });\n    const filter = { name: \"alpha\", domain: \"a.com\" };\n    expect(cookieMatchesFilter(c1, filter)).toBe(true);\n    expect(cookieMatchesFilter(c2, filter)).toBe(false);\n  });\n});\n\ndescribe(\"V3Context cookie methods\", () => {\n  // We test V3Context methods by constructing a minimal instance with a mock\n  // CDP connection. V3Context.create() requires a real WebSocket, so we build\n  // one via type-casting a MockCDPSession into the `conn` slot.\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  let V3ContextClass: { prototype: V3Context } & Record<string, any>;\n\n  beforeEach(async () => {\n    const mod = await import(\"../../lib/v3/understudy/context.js\");\n    V3ContextClass = mod.V3Context as typeof V3ContextClass;\n  });\n\n  function makeContext(\n    cdpHandlers: Record<string, (params?: Record<string, unknown>) => unknown>,\n  ): V3Context {\n    const mockConn = new MockCDPSession(cdpHandlers, \"root\");\n    // V3Context stores the connection as `conn` (readonly). We create an\n    // object with the real prototype so we get the actual method implementations.\n    const ctx = Object.create(V3ContextClass.prototype) as V3Context & {\n      conn: MockCDPSession;\n    };\n    // Assign the mock connection\n    Object.defineProperty(ctx, \"conn\", { value: mockConn, writable: false });\n    return ctx;\n  }\n\n  function getMockConn(ctx: V3Context): MockCDPSession {\n    return (ctx as unknown as { conn: MockCDPSession }).conn;\n  }\n\n  describe(\"cookies()\", () => {\n    it(\"returns all cookies from Storage.getCookies\", async () => {\n      const cdpCookies = [\n        toCdpCookie(makeCookie({ name: \"a\", domain: \"example.com\" })),\n        toCdpCookie(makeCookie({ name: \"b\", domain: \"other.com\" })),\n      ];\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: cdpCookies }),\n      });\n\n      const result = await ctx.cookies();\n      expect(result).toHaveLength(2);\n      expect(result.map((c) => c.name)).toEqual([\"a\", \"b\"]);\n    });\n\n    it(\"filters by URL when provided as string\", async () => {\n      const cdpCookies = [\n        toCdpCookie(makeCookie({ name: \"a\", domain: \"example.com\" })),\n        toCdpCookie(makeCookie({ name: \"b\", domain: \"other.com\" })),\n      ];\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: cdpCookies }),\n      });\n\n      const result = await ctx.cookies(\"http://example.com/\");\n      expect(result).toHaveLength(1);\n      expect(result[0]!.name).toBe(\"a\");\n    });\n\n    it(\"filters by URL when provided as array\", async () => {\n      const cdpCookies = [\n        toCdpCookie(makeCookie({ name: \"a\", domain: \"example.com\" })),\n        toCdpCookie(makeCookie({ name: \"b\", domain: \"other.com\" })),\n      ];\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: cdpCookies }),\n      });\n\n      const result = await ctx.cookies([\"http://other.com/\"]);\n      expect(result).toHaveLength(1);\n      expect(result[0]!.name).toBe(\"b\");\n    });\n\n    it(\"defaults sameSite to Lax when CDP returns undefined\", async () => {\n      const cdpCookie = {\n        ...toCdpCookie(makeCookie()),\n        sameSite: undefined as string | undefined,\n      };\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [cdpCookie] }),\n      });\n\n      const result = await ctx.cookies();\n      expect(result[0]!.sameSite).toBe(\"Lax\");\n    });\n\n    it(\"returns empty array when browser has no cookies\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [] }),\n      });\n      const result = await ctx.cookies();\n      expect(result).toEqual([]);\n    });\n\n    it(\"maps all CDP cookie fields to our Cookie type\", async () => {\n      const cdpCookie = toCdpCookie(\n        makeCookie({\n          name: \"full\",\n          value: \"v\",\n          domain: \".test.com\",\n          path: \"/p\",\n          expires: 1700000000,\n          httpOnly: true,\n          secure: true,\n          sameSite: \"Strict\",\n        }),\n      );\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [cdpCookie] }),\n      });\n\n      const result = await ctx.cookies();\n      expect(result[0]).toEqual({\n        name: \"full\",\n        value: \"v\",\n        domain: \".test.com\",\n        path: \"/p\",\n        expires: 1700000000,\n        httpOnly: true,\n        secure: true,\n        sameSite: \"Strict\",\n      });\n    });\n\n    it(\"strips extra CDP fields (size, priority, etc.) from result\", async () => {\n      const cdpCookie = toCdpCookie(makeCookie({ name: \"stripped\" }));\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [cdpCookie] }),\n      });\n\n      const result = await ctx.cookies();\n      const keys = Object.keys(result[0]!);\n      expect(keys).not.toContain(\"size\");\n      expect(keys).not.toContain(\"priority\");\n      expect(keys).not.toContain(\"sourceScheme\");\n      expect(keys).not.toContain(\"sourcePort\");\n    });\n\n    it(\"calls Storage.getCookies exactly once per invocation\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [] }),\n      });\n\n      await ctx.cookies();\n      await ctx.cookies(\"http://example.com\");\n\n      const calls = getMockConn(ctx).callsFor(\"Storage.getCookies\");\n      expect(calls).toHaveLength(2);\n    });\n  });\n\n  describe(\"addCookies()\", () => {\n    it(\"sends all cookies in a single Storage.setCookies call\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.addCookies([\n        { name: \"a\", value: \"1\", domain: \"example.com\", path: \"/\" },\n        { name: \"b\", value: \"2\", domain: \"other.com\", path: \"/\" },\n      ]);\n\n      const calls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(calls).toHaveLength(1);\n      expect(calls[0]!.params).toMatchObject({\n        cookies: [\n          { name: \"a\", domain: \"example.com\" },\n          { name: \"b\", domain: \"other.com\" },\n        ],\n      });\n    });\n\n    it(\"derives domain/path/secure from url\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.addCookies([\n        { name: \"a\", value: \"1\", url: \"https://example.com/app/page\" },\n      ]);\n\n      const calls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(calls[0]!.params).toMatchObject({\n        cookies: [\n          { name: \"a\", domain: \"example.com\", path: \"/app/\", secure: true },\n        ],\n      });\n    });\n\n    it(\"throws when Storage.setCookies fails\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => {\n          throw new Error(\"CDP failure\");\n        },\n      });\n\n      await expect(\n        ctx.addCookies([\n          { name: \"bad\", value: \"x\", domain: \"example.com\", path: \"/\" },\n        ]),\n      ).rejects.toThrow(/Failed to set cookies \\[\"bad\"\\]/);\n    });\n\n    it(\"throws for sameSite None without secure\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await expect(\n        ctx.addCookies([\n          {\n            name: \"x\",\n            value: \"1\",\n            domain: \"example.com\",\n            path: \"/\",\n            sameSite: \"None\",\n            secure: false,\n          },\n        ]),\n      ).rejects.toThrow(/sameSite: \"None\" without secure: true/);\n    });\n\n    it(\"does nothing when passed an empty array\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.addCookies([]);\n\n      const calls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(calls).toHaveLength(0);\n    });\n\n    it(\"sends all cookie fields to CDP (including optional ones)\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.addCookies([\n        {\n          name: \"full\",\n          value: \"val\",\n          domain: \"x.com\",\n          path: \"/p\",\n          expires: 9999999999,\n          httpOnly: true,\n          secure: true,\n          sameSite: \"Strict\",\n        },\n      ]);\n\n      const calls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(calls[0]!.params).toEqual({\n        cookies: [\n          {\n            name: \"full\",\n            value: \"val\",\n            domain: \"x.com\",\n            path: \"/p\",\n            expires: 9999999999,\n            httpOnly: true,\n            secure: true,\n            sameSite: \"Strict\",\n          },\n        ],\n      });\n    });\n\n    it(\"error message includes all cookie names when batch fails\", async () => {\n      const ctx = makeContext({\n        \"Storage.setCookies\": () => {\n          throw new Error(\"CDP failure\");\n        },\n      });\n\n      await expect(\n        ctx.addCookies([\n          { name: \"alpha\", value: \"1\", domain: \"a.com\", path: \"/\" },\n          { name: \"beta\", value: \"2\", domain: \"b.com\", path: \"/\" },\n        ]),\n      ).rejects.toThrow(/Failed to set cookies \\[\"alpha\", \"beta\"\\]/);\n    });\n  });\n\n  describe(\"clearCookies()\", () => {\n    const cdpCookies = [\n      toCdpCookie(\n        makeCookie({ name: \"session\", domain: \"example.com\", path: \"/\" }),\n      ),\n      toCdpCookie(\n        makeCookie({ name: \"_ga\", domain: \".example.com\", path: \"/\" }),\n      ),\n      toCdpCookie(\n        makeCookie({ name: \"pref\", domain: \"other.com\", path: \"/settings\" }),\n      ),\n    ];\n\n    it(\"uses atomic Storage.clearCookies when called with no options\", async () => {\n      const ctx = makeContext({\n        \"Storage.clearCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies();\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(1);\n\n      // Should NOT have fetched or re-set anything\n      const getCalls = getMockConn(ctx).callsFor(\"Storage.getCookies\");\n      expect(getCalls).toHaveLength(0);\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(0);\n    });\n\n    it(\"clears and re-adds only non-matching cookies (name filter)\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ name: \"_ga\" });\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(1);\n\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(1);\n      const kept = (\n        setCalls[0]!.params?.cookies as Array<{ name: string }>\n      ).map((c) => c.name);\n      expect(kept).toEqual([\"session\", \"pref\"]);\n    });\n\n    it(\"clears and re-adds only non-matching cookies (domain filter)\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ domain: \"other.com\" });\n\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      const kept = (\n        setCalls[0]!.params?.cookies as Array<{ name: string }>\n      ).map((c) => c.name);\n      expect(kept).toEqual([\"session\", \"_ga\"]);\n    });\n\n    it(\"clears and re-adds only non-matching cookies (regex name)\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ name: /^_ga/ });\n\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      const kept = (\n        setCalls[0]!.params?.cookies as Array<{ name: string }>\n      ).map((c) => c.name);\n      expect(kept).toEqual([\"session\", \"pref\"]);\n    });\n\n    it(\"applies AND logic across multiple filters\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ name: \"session\", domain: \"example.com\" });\n\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      const kept = (\n        setCalls[0]!.params?.cookies as Array<{ name: string }>\n      ).map((c) => c.name);\n      expect(kept).toEqual([\"_ga\", \"pref\"]);\n    });\n\n    it(\"does nothing when filter matches no cookies\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ name: \"nonexistent\" });\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(0);\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(0);\n    });\n\n    it(\"clears without re-adding when filter matches all cookies\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ name: /.*/ });\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(1);\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(0);\n    });\n\n    it(\"handles regex that matches multiple cookies\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({\n          cookies: [\n            toCdpCookie(\n              makeCookie({ name: \"_ga_ABC\", domain: \"example.com\", path: \"/\" }),\n            ),\n            toCdpCookie(\n              makeCookie({ name: \"_ga_DEF\", domain: \"example.com\", path: \"/\" }),\n            ),\n            toCdpCookie(\n              makeCookie({ name: \"_gid\", domain: \"example.com\", path: \"/\" }),\n            ),\n            toCdpCookie(\n              makeCookie({ name: \"session\", domain: \"example.com\", path: \"/\" }),\n            ),\n          ],\n        }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ name: /^_ga/ });\n\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      const kept = (\n        setCalls[0]!.params?.cookies as Array<{ name: string }>\n      ).map((c) => c.name);\n      expect(kept).toContain(\"_gid\");\n      expect(kept).toContain(\"session\");\n      expect(kept).not.toContain(\"_ga_ABC\");\n      expect(kept).not.toContain(\"_ga_DEF\");\n    });\n\n    it(\"regex domain filter combined with path filter\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ domain: /example/, path: \"/settings\" });\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(0);\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(0);\n    });\n\n    it(\"clearCookies with empty options object uses atomic clear (same as no args)\", async () => {\n      const ctx = makeContext({\n        \"Storage.clearCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({});\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(1);\n    });\n\n    it(\"clears and re-adds only non-matching cookies (path filter)\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await ctx.clearCookies({ path: \"/settings\" });\n\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(1);\n\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(1);\n      const kept = (\n        setCalls[0]!.params?.cookies as Array<{ name: string }>\n      ).map((c) => c.name);\n      expect(kept).toEqual([\"session\", \"_ga\"]);\n      expect(kept).not.toContain(\"pref\");\n    });\n\n    it(\"throws when Storage.getCookies fails during filtered clear\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => {\n          throw new Error(\"CDP getCookies failure\");\n        },\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await expect(ctx.clearCookies({ name: \"session\" })).rejects.toThrow(\n        /CDP getCookies failure/,\n      );\n\n      // clearCookies and setCookies should never have been called\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(0);\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(0);\n    });\n\n    it(\"throws when Storage.clearCookies fails during filtered clear\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => {\n          throw new Error(\"CDP clearCookies failure\");\n        },\n        \"Storage.setCookies\": () => ({}),\n      });\n\n      await expect(ctx.clearCookies({ name: \"session\" })).rejects.toThrow(\n        /CDP clearCookies failure/,\n      );\n\n      // setCookies should never have been called — cookies are untouched\n      const setCalls = getMockConn(ctx).callsFor(\"Storage.setCookies\");\n      expect(setCalls).toHaveLength(0);\n    });\n\n    it(\"throws when Storage.setCookies fails during re-add, cookies are already wiped\", async () => {\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [...cdpCookies] }),\n        \"Storage.clearCookies\": () => ({}),\n        \"Storage.setCookies\": () => {\n          throw new Error(\"CDP setCookies failure\");\n        },\n      });\n\n      await expect(ctx.clearCookies({ name: \"session\" })).rejects.toThrow(\n        /cookie jar is now empty/,\n      );\n\n      // clearCookies WAS called — cookies are gone\n      const clearCalls = getMockConn(ctx).callsFor(\"Storage.clearCookies\");\n      expect(clearCalls).toHaveLength(1);\n    });\n  });\n\n  describe(\"cookies() sameSite mapping\", () => {\n    it(\"passes through valid sameSite values as-is\", async () => {\n      for (const sameSite of [\"Strict\", \"Lax\", \"None\"] as const) {\n        const cdpCookie = { ...toCdpCookie(makeCookie()), sameSite };\n        const ctx = makeContext({\n          \"Storage.getCookies\": () => ({ cookies: [cdpCookie] }),\n        });\n        const result = await ctx.cookies();\n        expect(result[0]!.sameSite).toBe(sameSite);\n      }\n    });\n\n    it(\"does not normalize lowercase sameSite values from CDP\", async () => {\n      // CDP may return lowercase values; the current implementation casts\n      // without normalizing, so \"none\" passes through as-is.\n      const cdpCookie = { ...toCdpCookie(makeCookie()), sameSite: \"none\" };\n      const ctx = makeContext({\n        \"Storage.getCookies\": () => ({ cookies: [cdpCookie] }),\n      });\n      const result = await ctx.cookies();\n      // This documents the current behavior: lowercase is NOT normalized.\n      expect(result[0]!.sameSite).toBe(\"none\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/flowlogger-capturing-cdp.test.ts",
    "content": "import { EventEmitter } from \"node:events\";\nimport { describe, it, expect } from \"vitest\";\nimport { CdpConnection } from \"../../lib/v3/understudy/cdp.js\";\nimport { InMemoryEventSink } from \"../../lib/v3/flowlogger/EventSink.js\";\nimport { EventEmitterWithWildcardSupport } from \"../../lib/v3/flowlogger/EventEmitter.js\";\nimport { EventStore } from \"../../lib/v3/flowlogger/EventStore.js\";\nimport { FlowEvent, FlowLogger } from \"../../lib/v3/flowlogger/FlowLogger.js\";\n\nfunction attachEventStoreToBus(\n  store: EventStore,\n  bus: EventEmitterWithWildcardSupport,\n): () => void {\n  const onFlowEvent = (event: unknown) => {\n    if (event instanceof FlowEvent) {\n      void store.emit(event);\n    }\n  };\n\n  bus.on(\"*\", onFlowEvent);\n  return () => {\n    bus.off(\"*\", onFlowEvent);\n  };\n}\n\nclass FakeSocket extends EventEmitter {\n  sentPayloads: string[] = [];\n  readyState = 1;\n\n  send(payload: string): void {\n    this.sentPayloads.push(payload);\n  }\n\n  close(): void {\n    this.readyState = 3;\n    this.emit(\"close\", 1000, \"\");\n  }\n}\n\nfunction createConnection(socket: FakeSocket): CdpConnection {\n  // The production constructor is private; tests instantiate it directly so\n  // they can drive raw websocket messages without a real browser.\n  const ConnectionCtor = CdpConnection as unknown as {\n    new (ws: FakeSocket): CdpConnection;\n  };\n  return new ConnectionCtor(socket);\n}\n\nfunction requireEvent(\n  events: FlowEvent[],\n  predicate: (event: FlowEvent) => boolean,\n  description: string,\n): FlowEvent {\n  const match = events.find(predicate);\n  expect(match, `missing ${description}`).toBeDefined();\n  return match as FlowEvent;\n}\n\ndescribe(\"flow logger cdp context\", () => {\n  it(\"preserves the active parent chain when a session event handler issues a nested CDP call\", async () => {\n    const sessionId = \"session-test\";\n    const socket = new FakeSocket();\n    const eventBus = new EventEmitterWithWildcardSupport();\n    const sink = new InMemoryEventSink();\n    const eventStore = new EventStore(sessionId, undefined, sink);\n\n    const detachBus = attachEventStoreToBus(eventStore, eventBus);\n\n    const conn = createConnection(socket);\n    conn.flowLoggerContext = FlowLogger.init(sessionId, eventBus);\n\n    // Seed the target/session mapping the same way a real attach flow would\n    // before any session-scoped messages are dispatched.\n    (conn as unknown as { onMessage(json: string): void }).onMessage(\n      JSON.stringify({\n        method: \"Target.attachedToTarget\",\n        params: {\n          sessionId: \"target-session\",\n          targetInfo: { targetId: \"target-1\" },\n        },\n      }),\n    );\n\n    const session = conn.getSession(\"target-session\");\n    expect(session).toBeDefined();\n\n    session!.on(\"Runtime.consoleAPICalled\", () => {\n      // This nested send used to lose its parent chain because the callback ran\n      // after the original ALS scope had already unwound.\n      void session!.send(\"Runtime.evaluate\", {\n        expression: \"2 + 2\",\n      });\n    });\n\n    await FlowLogger.runWithLogging(\n      {\n        context: conn.flowLoggerContext,\n        eventType: \"SyntheticParentEvent\",\n      },\n      async () => {\n        void session!.send(\"Page.navigate\", {\n          url: \"https://example.com\",\n        });\n      },\n      [],\n    );\n\n    (conn as unknown as { onMessage(json: string): void }).onMessage(\n      JSON.stringify({\n        method: \"Runtime.consoleAPICalled\",\n        sessionId: \"target-session\",\n        params: { type: \"log\" },\n      }),\n    );\n\n    // The nested Runtime.evaluate call should still attach under the synthetic\n    // parent event even though it was triggered by a later session callback.\n    const events = await eventStore.query({});\n    const parentEvent = requireEvent(\n      events,\n      (event) => event.eventType === \"SyntheticParentEvent\",\n      \"SyntheticParentEvent\",\n    );\n    const nestedCallEvent = requireEvent(\n      events,\n      (event) =>\n        event.eventType === \"CdpCallEvent\" &&\n        String(event.data.method) === \"Runtime.evaluate\",\n      \"nested Runtime.evaluate CdpCallEvent\",\n    );\n\n    expect(nestedCallEvent.eventParentIds).toEqual([parentEvent.eventId]);\n\n    detachBus();\n    await eventStore.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/flowlogger-capturing-llm.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { FlowLogger } from \"../../lib/v3/flowlogger/FlowLogger.js\";\n\ndescribe(\"flow logger llm logging\", () => {\n  it(\"no-ops direct llm logging calls when no flow context is active\", () => {\n    // These helpers are called from multiple model adapters, so they must stay\n    // safe even when a test or utility invokes them outside any ALS flow scope.\n    expect(() =>\n      FlowLogger.logLlmRequest({\n        requestId: \"req-1\",\n        model: \"mock-model\",\n        prompt: \"hello\",\n      }),\n    ).not.toThrow();\n\n    expect(() =>\n      FlowLogger.logLlmResponse({\n        requestId: \"req-1\",\n        model: \"mock-model\",\n        output: \"world\",\n        inputTokens: 1,\n        outputTokens: 1,\n      }),\n    ).not.toThrow();\n  });\n\n  it(\"does not throw from llm middleware when no flow context is active\", async () => {\n    const middleware = FlowLogger.createLlmLoggingMiddleware(\"mock-model\");\n\n    // Missing flow context should degrade to a silent no-op and preserve the\n    // underlying model result.\n    await expect(\n      middleware.wrapGenerate({\n        doGenerate: async () => ({\n          text: \"done\",\n          usage: {\n            inputTokens: 1,\n            outputTokens: 1,\n            totalTokens: 2,\n          },\n        }),\n        params: {\n          prompt: [],\n        },\n      } as never),\n    ).resolves.toMatchObject({\n      text: \"done\",\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/flowlogger-eventstore.test.ts",
    "content": "import { afterEach, describe, expect, it } from \"vitest\";\nimport { EventStore } from \"../../lib/v3/flowlogger/EventStore.js\";\nimport { EventEmitterWithWildcardSupport } from \"../../lib/v3/flowlogger/EventEmitter.js\";\nimport { FlowEvent } from \"../../lib/v3/flowlogger/FlowLogger.js\";\n\nfunction attachEventStoreToBus(\n  store: EventStore,\n  bus: EventEmitterWithWildcardSupport,\n): () => void {\n  const onFlowEvent = (event: unknown) => {\n    if (event instanceof FlowEvent) {\n      void store.emit(event);\n    }\n  };\n\n  bus.on(\"*\", onFlowEvent);\n  return () => {\n    bus.off(\"*\", onFlowEvent);\n  };\n}\n\nfunction createVerboseStoreHarness(): {\n  writes: string[];\n  store: EventStore;\n  bus: EventEmitterWithWildcardSupport;\n  detachBus: () => void;\n} {\n  const writes: string[] = [];\n  process.stderr.write = ((\n    chunk: string,\n    cb?: (error?: Error | null) => void,\n  ) => {\n    writes.push(String(chunk));\n    cb?.(null);\n    return true;\n  }) as typeof process.stderr.write;\n\n  const store = new EventStore(\"session-test\");\n  const bus = new EventEmitterWithWildcardSupport();\n  const detachBus = attachEventStoreToBus(store, bus);\n\n  return { writes, store, bus, detachBus };\n}\n\ndescribe(\"flow logger event store\", () => {\n  const stderrWrite = process.stderr.write.bind(process.stderr);\n\n  afterEach(() => {\n    process.stderr.write = stderrWrite;\n  });\n\n  it(\"queries recent events from the default in-memory sink\", async () => {\n    const store = new EventStore(\"session-test\");\n\n    await store.emit(\n      new FlowEvent({\n        eventType: \"StagehandExtractEvent\",\n        sessionId: \"session-test\",\n        eventId: \"stagehand-1234\",\n        eventCreatedAt: \"2026-03-16T21:45:00.000Z\",\n        data: { params: [\"grab title\"] },\n      }),\n    );\n\n    const events = await store.query({});\n    expect(events).toHaveLength(1);\n    expect(events[0].eventType).toBe(\"StagehandExtractEvent\");\n\n    await store.destroy();\n  });\n\n  it(\"drops payloads from the default in-memory sink\", async () => {\n    const store = new EventStore(\"session-test\");\n\n    await store.emit(\n      new FlowEvent({\n        eventType: \"LlmRequestEvent\",\n        sessionId: \"session-test\",\n        eventId: \"llm-1234\",\n        eventCreatedAt: \"2026-03-16T21:45:00.000Z\",\n        data: {\n          prompt: [{ type: \"image_url\", image_url: { url: \"huge\" } }],\n          output: \"huge\",\n        },\n      }),\n    );\n\n    const [event] = await store.query({});\n    expect(event.eventType).toBe(\"LlmRequestEvent\");\n    expect(event.eventId).toBe(\"llm-1234\");\n    expect(event.data).toEqual({});\n\n    await store.destroy();\n  });\n\n  it(\"renders semantic hierarchy tags for non-cdp stderr events only\", async () => {\n    // Intercept stderr so the pretty sink can be asserted without polluting the\n    // real test runner output.\n    const { writes, store, bus, detachBus } = createVerboseStoreHarness();\n\n    const stepEvent = new FlowEvent({\n      eventType: \"StagehandExtractEvent\",\n      sessionId: \"session-test\",\n      eventId: \"stagehand-1234\",\n      eventCreatedAt: \"2026-03-16T21:45:00.000Z\",\n      data: { params: [\"grab title\"] },\n    });\n    const cdpEvent = new FlowEvent({\n      eventType: \"CdpCallEvent\",\n      sessionId: \"session-test\",\n      eventId: \"cdp-call-5678\",\n      eventCreatedAt: \"2026-03-16T21:45:00.100Z\",\n      eventParentIds: [stepEvent.eventId],\n      data: {\n        method: \"Runtime.evaluate\",\n        params: { expression: \"2 + 2\" },\n        targetId: \"1234567890ABCDEF1234567890ABCDEF\",\n      },\n    });\n\n    // The stderr sink intentionally suppresses CDP noise even though the event\n    // still exists for in-memory and file-backed sinks.\n    bus.emit(stepEvent.eventType, stepEvent);\n    bus.emit(cdpEvent.eventType, cdpEvent);\n    await new Promise((resolve) => setTimeout(resolve, 0));\n\n    expect(writes).toHaveLength(1);\n    expect(writes[0]).toContain(\"[🆂 #1234 EXTRACT]\");\n    expect(writes[0]).toContain(\"Stagehand.extract\");\n    expect(writes[0]).not.toContain(\"Runtime.evaluate\");\n\n    detachBus();\n    await store.destroy();\n  });\n\n  it(\"renders generic stagehand events without crashing the stderr sink\", async () => {\n    const { writes, store, bus, detachBus } = createVerboseStoreHarness();\n\n    // `StagehandEvent` has no action suffix, so this guards the formatter path\n    // that cannot assume a method name exists.\n    bus.emit(\n      \"StagehandEvent\",\n      new FlowEvent({\n        eventType: \"StagehandEvent\",\n        sessionId: \"session-test\",\n        eventId: \"stagehand-0001\",\n        eventCreatedAt: \"2026-03-16T21:45:00.000Z\",\n        data: { params: [\"noop\"] },\n      }),\n    );\n    await new Promise((resolve) => setTimeout(resolve, 0));\n\n    expect(writes).toHaveLength(1);\n    expect(writes[0]).toContain(\"[🆂 #0001\");\n    expect(writes[0]).toContain(\"Stagehand(\");\n\n    detachBus();\n    await store.destroy();\n  });\n\n  it(\"colorizes pretty stderr output with ansi escapes when enabled\", async () => {\n    const previousForceColor = process.env.FORCE_COLOR;\n    const previousNoColor = process.env.NO_COLOR;\n    delete process.env.NO_COLOR;\n    process.env.FORCE_COLOR = \"1\";\n\n    const { writes, store, bus, detachBus } = createVerboseStoreHarness();\n\n    try {\n      bus.emit(\n        \"StagehandActEvent\",\n        new FlowEvent({\n          eventType: \"StagehandActEvent\",\n          sessionId: \"session-test\",\n          eventId: \"stagehand-0002\",\n          eventCreatedAt: \"2026-03-16T21:45:00.000Z\",\n          data: { params: [\"click submit\"] },\n        }),\n      );\n      await new Promise((resolve) => setTimeout(resolve, 0));\n\n      expect(writes).toHaveLength(1);\n      expect(writes[0]).toContain(\"\\u001B[\");\n    } finally {\n      if (previousNoColor === undefined) {\n        delete process.env.NO_COLOR;\n      } else {\n        process.env.NO_COLOR = previousNoColor;\n      }\n\n      if (previousForceColor === undefined) {\n        delete process.env.FORCE_COLOR;\n      } else {\n        process.env.FORCE_COLOR = previousForceColor;\n      }\n\n      detachBus();\n      await store.destroy();\n    }\n  });\n\n  it(\"keeps agent ancestry and start ids for completion events after many child events\", async () => {\n    const { writes, store, bus, detachBus } = createVerboseStoreHarness();\n\n    const agentEvent = new FlowEvent({\n      eventType: \"AgentExecuteEvent\",\n      sessionId: \"session-test\",\n      eventId: \"agent-1234\",\n      eventCreatedAt: \"2026-03-16T21:45:00.000Z\",\n      data: { params: [{ instruction: \"click the button\" }] },\n    });\n    const actEvent = new FlowEvent({\n      eventType: \"StagehandActEvent\",\n      sessionId: \"session-test\",\n      eventId: \"stagehand-2222\",\n      eventCreatedAt: \"2026-03-16T21:45:00.001Z\",\n      eventParentIds: [agentEvent.eventId],\n      data: { params: [\"click the button\"] },\n    });\n    const clickEvent = new FlowEvent({\n      eventType: \"UnderstudyClickEvent\",\n      sessionId: \"session-test\",\n      eventId: \"action-3333\",\n      eventCreatedAt: \"2026-03-16T21:45:00.002Z\",\n      eventParentIds: [agentEvent.eventId, actEvent.eventId],\n      data: { target: \"xpath=/button[1]\" },\n    });\n\n    bus.emit(agentEvent.eventType, agentEvent);\n    bus.emit(actEvent.eventType, actEvent);\n    bus.emit(clickEvent.eventType, clickEvent);\n\n    // Flood the retained history with child events so the completion lines have\n    // to recover their displayed ancestry from the queryable sink.\n    for (let index = 0; index < 150; index += 1) {\n      bus.emit(\n        \"CdpCallEvent\",\n        new FlowEvent({\n          eventType: \"CdpCallEvent\",\n          sessionId: \"session-test\",\n          eventId: `cdp-${String(index).padStart(4, \"0\")}`,\n          eventCreatedAt: `2026-03-16T21:45:00.${String(index + 10).padStart(3, \"0\")}Z`,\n          eventParentIds: [\n            agentEvent.eventId,\n            actEvent.eventId,\n            clickEvent.eventId,\n          ],\n          data: {\n            method: \"Runtime.evaluate\",\n            params: { expression: `${index}` },\n            targetId: \"1234567890ABCDEF1234567890ABCDEF\",\n          },\n        }),\n      );\n    }\n\n    bus.emit(\n      \"UnderstudyClickCompletedEvent\",\n      new FlowEvent({\n        eventType: \"UnderstudyClickCompletedEvent\",\n        sessionId: \"session-test\",\n        eventId: \"done-4444\",\n        eventCreatedAt: \"2026-03-16T21:45:01.000Z\",\n        eventParentIds: [\n          agentEvent.eventId,\n          actEvent.eventId,\n          clickEvent.eventId,\n        ],\n        data: { durationMs: 250 },\n      }),\n    );\n    bus.emit(\n      \"StagehandActCompletedEvent\",\n      new FlowEvent({\n        eventType: \"StagehandActCompletedEvent\",\n        sessionId: \"session-test\",\n        eventId: \"done-5555\",\n        eventCreatedAt: \"2026-03-16T21:45:01.001Z\",\n        eventParentIds: [agentEvent.eventId, actEvent.eventId],\n        data: { durationMs: 500 },\n      }),\n    );\n    bus.emit(\n      \"AgentExecuteCompletedEvent\",\n      new FlowEvent({\n        eventType: \"AgentExecuteCompletedEvent\",\n        sessionId: \"session-test\",\n        eventId: \"done-6666\",\n        eventCreatedAt: \"2026-03-16T21:45:01.002Z\",\n        eventParentIds: [agentEvent.eventId],\n        data: { durationMs: 750 },\n      }),\n    );\n    await new Promise((resolve) => setTimeout(resolve, 0));\n\n    // Completion lines should reference the original started-event ids, not the\n    // synthetic completed-event ids emitted at the end of the lifecycle.\n    const clickCompletedLine = writes.find((line) =>\n      line.includes(\"CLICK completed\"),\n    );\n    const actCompletedLine = writes.find((line) =>\n      line.includes(\"ACT completed\"),\n    );\n    const agentCompletedLine = writes.find((line) =>\n      line.includes(\"Agent.execute() completed\"),\n    );\n\n    expect(clickCompletedLine).toContain(\"[🅰 #1234]\");\n    expect(clickCompletedLine).toContain(\"[🆂 #2222 ACT]\");\n    expect(clickCompletedLine).toContain(\"[🆄 #3333 CLICK]\");\n    expect(clickCompletedLine).not.toContain(\"#4444\");\n\n    expect(actCompletedLine).toContain(\"[🅰 #1234]\");\n    expect(actCompletedLine).toContain(\"[🆂 #2222 ACT]\");\n    expect(actCompletedLine).not.toContain(\"#5555\");\n\n    expect(agentCompletedLine).toContain(\"[🅰 #1234]\");\n    expect(agentCompletedLine).not.toContain(\"#6666\");\n\n    detachBus();\n    await store.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/helpers/mockCDPSession.ts",
    "content": "import type { CDPSessionLike } from \"../../../lib/v3/understudy/cdp.js\";\n\ntype Handler = (params?: Record<string, unknown>) => Promise<unknown> | unknown;\n\nexport class MockCDPSession implements CDPSessionLike {\n  public readonly id: string;\n  public readonly calls: Array<{\n    method: string;\n    params?: Record<string, unknown>;\n  }> = [];\n\n  constructor(\n    private readonly handlers: Record<string, Handler> = {},\n    sessionId = \"mock-session\",\n  ) {\n    this.id = sessionId;\n  }\n\n  async send<R = unknown>(\n    method: string,\n    params: Record<string, unknown> = {},\n  ): Promise<R> {\n    this.calls.push({ method, params });\n    const handler = this.handlers[method];\n    if (!handler) return {} as R;\n    return (await handler(params)) as R;\n  }\n\n  on(): void {}\n  off(): void {}\n  async close(): Promise<void> {}\n\n  callsFor(method: string): Array<{ params?: Record<string, unknown> }> {\n    return this.calls\n      .filter((call) => call.method === method)\n      .map(({ params }) => ({ params }));\n  }\n}\n"
  },
  {
    "path": "packages/core/tests/unit/llm-provider.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { getAISDKLanguageModel } from \"../../lib/v3/llm/LLMProvider.js\";\n\ndescribe(\"getAISDKLanguageModel\", () => {\n  describe(\"ollama provider\", () => {\n    it(\"works without clientOptions\", () => {\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\");\n      expect(model).toBeDefined();\n    });\n\n    it(\"works with empty clientOptions\", () => {\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\", {});\n      expect(model).toBeDefined();\n    });\n\n    it(\"works with clientOptions containing only undefined values\", () => {\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\", {\n        apiKey: undefined,\n      });\n      expect(model).toBeDefined();\n    });\n\n    it(\"works with clientOptions containing only null values\", () => {\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\", {\n        apiKey: null as unknown as string,\n      });\n      expect(model).toBeDefined();\n    });\n\n    it(\"works with custom baseURL\", () => {\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\", {\n        baseURL: \"http://custom-ollama:11434\",\n      });\n      expect(model).toBeDefined();\n    });\n\n    it(\"works even when apiKey is mistakenly provided\", () => {\n      // Ollama doesn't need an API key, but users might set one anyway\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\", {\n        apiKey: \"unnecessary-key\",\n      });\n      expect(model).toBeDefined();\n    });\n  });\n\n  describe(\"providers with API keys\", () => {\n    it(\"openai requires valid clientOptions for custom configuration\", () => {\n      // Without clientOptions, uses default provider\n      const defaultModel = getAISDKLanguageModel(\"openai\", \"gpt-4o\");\n      expect(defaultModel).toBeDefined();\n\n      // With valid apiKey, uses custom provider\n      const customModel = getAISDKLanguageModel(\"openai\", \"gpt-4o\", {\n        apiKey: \"test-key\",\n      });\n      expect(customModel).toBeDefined();\n    });\n  });\n\n  describe(\"hasValidOptions logic\", () => {\n    it(\"treats undefined apiKey as no options\", () => {\n      // This should use the default provider path (AISDKProviders)\n      // not the custom provider path (AISDKProvidersWithAPIKey)\n      const model = getAISDKLanguageModel(\"ollama\", \"llama3.2\", {\n        apiKey: undefined,\n      });\n      expect(model).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/model-deprecation.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { LLMProvider } from \"../../lib/v3/llm/LLMProvider.js\";\nimport {\n  UnsupportedModelError,\n  UnsupportedAISDKModelProviderError,\n} from \"../../lib/v3/types/public/sdkErrors.js\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\n\n// Mock client options with fake API keys for testing\nconst mockClientOptions = { apiKey: \"test-api-key-for-testing\" };\n\ndescribe(\"Model format deprecation\", () => {\n  describe(\"UnsupportedModelError\", () => {\n    it(\"includes guidance to use provider/model format for unknown model names\", () => {\n      const error = new UnsupportedModelError([\"gpt-4o\", \"gemini-2.0-flash\"]);\n\n      // Should mention the new format\n      expect(error.message).toContain(\"provider/model\");\n      // Should include link to docs\n      expect(error.message).toContain(\n        \"https://docs.stagehand.dev/v3/configuration/models\",\n      );\n    });\n\n    it(\"includes example of provider/model format\", () => {\n      const error = new UnsupportedModelError([\"gpt-4o\"]);\n\n      // Should provide examples like openai/gpt-4o\n      expect(error.message).toContain(\"openai/gpt-4o\");\n      expect(error.message).toContain(\"anthropic/claude-sonnet-4\");\n    });\n\n    it(\"works with feature parameter\", () => {\n      const error = new UnsupportedModelError([\"gpt-4o\"], \"extract\");\n\n      expect(error.message).toContain(\"extract\");\n      expect(error.message).toContain(\"provider/model\");\n      expect(error.message).toContain(\n        \"https://docs.stagehand.dev/v3/configuration/models\",\n      );\n    });\n  });\n\n  describe(\"LLMProvider.getClient deprecation warning\", () => {\n    it(\"logs deprecation warning for legacy model names\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      // Using a legacy model name like \"gpt-4o\" instead of \"openai/gpt-4o\"\n      // Should not throw, but should log a deprecation warning\n      const client = provider.getClient(\"gpt-4o\", mockClientOptions);\n\n      // Should return a client (not throw)\n      expect(client).toBeDefined();\n\n      // Should have logged a deprecation warning at level 0\n      const deprecationWarning = logs.find(\n        (log) =>\n          log.message.toLowerCase().includes(\"deprecated\") ||\n          log.message.toLowerCase().includes(\"deprecation\"),\n      );\n      expect(deprecationWarning).toBeDefined();\n      expect(deprecationWarning!.level).toBe(0);\n    });\n\n    it(\"deprecation warning mentions provider/model format\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      provider.getClient(\"gpt-4o\", mockClientOptions);\n\n      const deprecationWarning = logs.find(\n        (log) =>\n          log.message.toLowerCase().includes(\"deprecated\") ||\n          log.message.toLowerCase().includes(\"deprecation\"),\n      );\n\n      expect(deprecationWarning).toBeDefined();\n      const message = deprecationWarning!.message;\n      // Should mention the provider/model format\n      expect(message).toContain(\"provider/model\");\n      // Should give an example\n      expect(message).toContain(\"openai/gpt-5\");\n    });\n\n    it(\"returns OpenAIClient for legacy OpenAI model names\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      const client = provider.getClient(\"gpt-4o\", mockClientOptions);\n\n      // Should return a client\n      expect(client).toBeDefined();\n      // The client should be an OpenAIClient (check constructor name)\n      expect(client.constructor.name).toBe(\"OpenAIClient\");\n    });\n\n    it(\"returns GoogleClient for legacy Google model names\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      const client = provider.getClient(\"gemini-2.0-flash\", mockClientOptions);\n\n      // Should return a client\n      expect(client).toBeDefined();\n      // The client should be a GoogleClient\n      expect(client.constructor.name).toBe(\"GoogleClient\");\n    });\n  });\n\n  describe(\"LLMProvider.getClient error handling\", () => {\n    it(\"throws UnsupportedModelError for unknown model without slash\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      // Unknown model without slash should throw UnsupportedModelError\n      expect(() => {\n        provider.getClient(\"some-unknown-model\", mockClientOptions);\n      }).toThrow(UnsupportedModelError);\n    });\n\n    it(\"UnsupportedModelError includes provider/model format guidance\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      try {\n        provider.getClient(\"some-unknown-model\", mockClientOptions);\n      } catch (error) {\n        expect((error as Error).message).toContain(\"provider/model\");\n      }\n    });\n\n    it(\"throws UnsupportedAISDKModelProviderError for invalid provider in provider/model format\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      // Invalid provider but correct format\n      expect(() => {\n        provider.getClient(\"invalid-provider/some-model\", mockClientOptions);\n      }).toThrow(UnsupportedAISDKModelProviderError);\n    });\n\n    it(\"UnsupportedAISDKModelProviderError lists valid providers\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      try {\n        provider.getClient(\"invalid-provider/some-model\", mockClientOptions);\n      } catch (error) {\n        const message = (error as Error).message;\n        // Should list valid providers\n        expect(message).toContain(\"openai\");\n        expect(message).toContain(\"anthropic\");\n        expect(message).toContain(\"google\");\n      }\n    });\n  });\n\n  describe(\"new provider/model format\", () => {\n    it(\"does not log deprecation warning for provider/model format\", () => {\n      const logs: LogLine[] = [];\n      const logger = (line: LogLine) => logs.push(line);\n      const provider = new LLMProvider(logger);\n\n      // Using the new format\n      const client = provider.getClient(\"openai/gpt-4o\", mockClientOptions);\n\n      expect(client).toBeDefined();\n\n      // Should NOT have a deprecation warning\n      const deprecationWarning = logs.find(\n        (log) =>\n          log.message.toLowerCase().includes(\"deprecated\") ||\n          log.message.toLowerCase().includes(\"deprecation\"),\n      );\n      expect(deprecationWarning).toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/model-utils.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { extractModelName, resolveModel } from \"../../lib/modelUtils.js\";\n\ndescribe(\"extractModelName\", () => {\n  it(\"returns undefined for undefined input\", () => {\n    expect(extractModelName(undefined)).toBeUndefined();\n  });\n\n  it(\"returns the string as-is for a string input\", () => {\n    expect(extractModelName(\"openai/gpt-4o\")).toBe(\"openai/gpt-4o\");\n  });\n\n  it(\"returns modelName from an object input\", () => {\n    expect(\n      extractModelName({ modelName: \"anthropic/claude-sonnet-4-20250514\" }),\n    ).toBe(\"anthropic/claude-sonnet-4-20250514\");\n  });\n\n  it(\"returns modelName from an object with extra properties\", () => {\n    expect(\n      extractModelName({\n        modelName: \"openai/gpt-4o-mini\",\n        apiKey: \"sk-test\",\n        baseURL: \"https://custom.endpoint\",\n      }),\n    ).toBe(\"openai/gpt-4o-mini\");\n  });\n});\n\ndescribe(\"resolveModel\", () => {\n  it(\"extracts provider and modelName from a string\", () => {\n    const result = resolveModel(\"openai/gpt-4o\");\n    expect(result.provider).toBe(\"openai\");\n    expect(result.modelName).toBe(\"gpt-4o\");\n    expect(result.clientOptions).toEqual({});\n  });\n\n  it(\"extracts clientOptions from an object config\", () => {\n    const result = resolveModel({\n      modelName: \"openai/gpt-4o\" as never,\n      apiKey: \"sk-test\",\n    });\n    expect(result.provider).toBe(\"openai\");\n    expect(result.modelName).toBe(\"gpt-4o\");\n    expect(result.clientOptions).toMatchObject({ apiKey: \"sk-test\" });\n    // modelName should not leak into clientOptions\n    expect(result.clientOptions).not.toHaveProperty(\"modelName\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/openai-cua-client.test.ts",
    "content": "import { describe, expect, it, vi } from \"vitest\";\nimport { OpenAICUAClient } from \"../../lib/v3/agent/OpenAICUAClient.js\";\n\nfunction createClient() {\n  return new OpenAICUAClient(\n    \"openai\",\n    \"computer-use-preview-2025-03-11\",\n    undefined,\n    { apiKey: \"test-key\" },\n  );\n}\n\ndescribe(\"OpenAICUAClient\", () => {\n  it(\"exposes captchaSolvedProceed tool after a captcha context note\", () => {\n    const client = createClient();\n\n    // Before captcha note — tool should not be active\n    expect(\n      (client as unknown as { captchaSolvedToolActive: boolean })\n        .captchaSolvedToolActive,\n    ).toBe(false);\n\n    // Simulate a captcha context note being added (as the CUA handler does)\n    client.addContextNote(\n      \"A captcha was automatically detected and solved — no further interaction needed.\",\n    );\n\n    expect(\n      (client as unknown as { captchaSolvedToolActive: boolean })\n        .captchaSolvedToolActive,\n    ).toBe(true);\n  });\n\n  it(\"does NOT activate captcha tool for non-captcha context notes\", () => {\n    const client = createClient();\n\n    client.addContextNote(\"The page has finished loading.\");\n\n    expect(\n      (client as unknown as { captchaSolvedToolActive: boolean })\n        .captchaSolvedToolActive,\n    ).toBe(false);\n  });\n\n  it(\"deactivates captcha tool after takeAction handles the function call\", async () => {\n    const client = createClient();\n    client.addContextNote(\"A captcha was solved.\");\n\n    expect(\n      (client as unknown as { captchaSolvedToolActive: boolean })\n        .captchaSolvedToolActive,\n    ).toBe(true);\n\n    // Simulate the model calling the captchaSolvedProceed tool\n    const result = await (\n      client as unknown as {\n        takeAction: (\n          output: unknown[],\n          logger: (msg: unknown) => void,\n        ) => Promise<unknown[]>;\n      }\n    ).takeAction(\n      [\n        {\n          type: \"function_call\",\n          name: \"captchaSolvedProceed\",\n          call_id: \"call-1\",\n          arguments: \"{}\",\n        },\n      ],\n      vi.fn(),\n    );\n\n    // Tool should be deactivated\n    expect(\n      (client as unknown as { captchaSolvedToolActive: boolean })\n        .captchaSolvedToolActive,\n    ).toBe(false);\n\n    // Result should contain a function_call_output confirming proceed\n    expect(result).toEqual([\n      {\n        type: \"function_call_output\",\n        call_id: \"call-1\",\n        output: expect.stringContaining(\"Continue completing\"),\n      },\n    ]);\n  });\n\n  it(\"does NOT auto-continue follow-up questions without a captcha context\", async () => {\n    const client = createClient();\n    // No captcha context note — no tool should be exposed\n\n    type ExecuteStepResult = {\n      actions: Array<{ type: string }>;\n      message: string;\n      completed: boolean;\n      nextInputItems: unknown[];\n      responseId: string;\n      usage: {\n        input_tokens: number;\n        output_tokens: number;\n        inference_time_ms: number;\n      };\n    };\n\n    const executeStepSpy = vi.spyOn(\n      client as unknown as {\n        executeStep: (\n          inputItems: unknown[],\n          previousResponseId: string | undefined,\n          logger: (message: { message: string }) => void,\n        ) => Promise<ExecuteStepResult>;\n      },\n      \"executeStep\",\n    );\n\n    executeStepSpy.mockResolvedValueOnce({\n      actions: [],\n      message:\n        \"I've located the Submit button. Should I go ahead and submit it?\",\n      completed: true,\n      nextInputItems: [],\n      responseId: \"response-1\",\n      usage: { input_tokens: 1, output_tokens: 1, inference_time_ms: 1 },\n    });\n\n    const result = await client.execute({\n      options: { instruction: \"Submit the form.\", maxSteps: 10 } as never,\n      logger: vi.fn(),\n    });\n\n    // Should NOT have continued — the model's follow-up is treated as completion\n    expect(executeStepSpy).toHaveBeenCalledTimes(1);\n    expect(result.completed).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/page-extra-http-headers.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { Page } from \"../../lib/v3/understudy/page.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\nimport { StagehandSetExtraHTTPHeadersError } from \"../../lib/v3/types/public/sdkErrors.js\";\n\ntype PageStub = {\n  mainSession: MockCDPSession;\n  sessions: Map<string, MockCDPSession>;\n  extraHTTPHeaders: Record<string, string>;\n  applyExtraHTTPHeadersToSession: (\n    session: MockCDPSession,\n    headers: Record<string, string>,\n  ) => Promise<void>;\n};\n\nconst makePage = (sessions: MockCDPSession[]): PageStub => {\n  const mainSession = sessions[0] ?? new MockCDPSession({}, \"main\");\n  const stub: PageStub = {\n    mainSession,\n    sessions: new Map(sessions.map((s) => [s.id, s])),\n    extraHTTPHeaders: {},\n    // Bind the private helper from Page.prototype so setExtraHTTPHeaders can call it\n    applyExtraHTTPHeadersToSession: (Page.prototype as unknown as PageStub)\n      .applyExtraHTTPHeadersToSession,\n  };\n  return stub;\n};\n\ndescribe(\"Page.setExtraHTTPHeaders\", () => {\n  const setExtraHTTPHeaders = Page.prototype.setExtraHTTPHeaders as (\n    this: PageStub,\n    headers: Record<string, string>,\n  ) => Promise<void>;\n\n  it(\"sends headers to all sessions owned by the page\", async () => {\n    const sessionA = new MockCDPSession({}, \"session-a\");\n    const sessionB = new MockCDPSession({}, \"session-b\");\n    const page = makePage([sessionA, sessionB]);\n\n    await setExtraHTTPHeaders.call(page, {\n      \"x-stagehand-test\": \"hello\",\n    });\n\n    for (const session of [sessionA, sessionB]) {\n      expect(session.callsFor(\"Network.enable\").length).toBe(1);\n      expect(\n        session.callsFor(\"Network.setExtraHTTPHeaders\")[0]?.params,\n      ).toEqual({\n        headers: { \"x-stagehand-test\": \"hello\" },\n      });\n    }\n  });\n\n  it(\"applies headers to mainSession even when sessions map is empty\", async () => {\n    const page = makePage([]);\n\n    await setExtraHTTPHeaders.call(page, { \"x-test\": \"value\" });\n\n    // mainSession should still receive headers even though it's not in the sessions map\n    expect(page.mainSession.callsFor(\"Network.enable\").length).toBe(1);\n    expect(\n      page.mainSession.callsFor(\"Network.setExtraHTTPHeaders\")[0]?.params,\n    ).toEqual({\n      headers: { \"x-test\": \"value\" },\n    });\n  });\n\n  it(\"throws StagehandSetExtraHTTPHeadersError with session failure details\", async () => {\n    const sessionA = new MockCDPSession(\n      {\n        \"Network.setExtraHTTPHeaders\": () => {\n          throw new Error(\"connection closed\");\n        },\n      },\n      \"session-a\",\n    );\n    const sessionB = new MockCDPSession({}, \"session-b\");\n    const page = makePage([sessionA, sessionB]);\n\n    let caughtError: StagehandSetExtraHTTPHeadersError | undefined;\n    try {\n      await setExtraHTTPHeaders.call(page, {\n        \"x-stagehand-test\": \"yes\",\n      });\n    } catch (error) {\n      caughtError = error as StagehandSetExtraHTTPHeadersError;\n    }\n\n    expect(caughtError).toBeInstanceOf(StagehandSetExtraHTTPHeadersError);\n    expect(caughtError?.failures).toHaveLength(1);\n    expect(caughtError?.failures[0]).toContain(\"session=session-a\");\n    expect(caughtError?.failures[0]).toContain(\"connection closed\");\n\n    // sessionB should still have been called successfully\n    expect(sessionB.callsFor(\"Network.setExtraHTTPHeaders\").length).toBe(1);\n  });\n\n  it(\"applies headers to sessions adopted after the call\", async () => {\n    const sessionA = new MockCDPSession({}, \"session-a\");\n    const page = makePage([sessionA]);\n\n    await setExtraHTTPHeaders.call(page, { \"x-before\": \"yes\" });\n\n    // A new OOPIF session is adopted after headers were set\n    const sessionB = new MockCDPSession({}, \"session-b\");\n    page.sessions.set(sessionB.id, sessionB);\n\n    // Simulate what adoptOopifSession does: replay headers onto the new session\n    await page.applyExtraHTTPHeadersToSession.call(\n      page,\n      sessionB,\n      page.extraHTTPHeaders,\n    );\n\n    // The late-arriving session should have received the headers\n    expect(sessionB.callsFor(\"Network.enable\").length).toBe(1);\n    expect(sessionB.callsFor(\"Network.setExtraHTTPHeaders\")[0]?.params).toEqual(\n      {\n        headers: { \"x-before\": \"yes\" },\n      },\n    );\n  });\n\n  it(\"does not mutate the original headers object\", async () => {\n    const session = new MockCDPSession({}, \"session-a\");\n    const page = makePage([session]);\n\n    const original = { \"x-custom\": \"value\" };\n    const frozen = { ...original };\n\n    await setExtraHTTPHeaders.call(page, original);\n\n    expect(original).toEqual(frozen);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/page-snapshot.test.ts",
    "content": "import { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { promises as fs } from \"fs\";\nimport { Page } from \"../../lib/v3/understudy/page.js\";\nimport * as snapshotModule from \"../../lib/v3/understudy/a11y/snapshot/index.js\";\nimport type { HybridSnapshot } from \"../../lib/v3/types/private/index.js\";\n\nconst baseSnapshot: HybridSnapshot = {\n  combinedTree: \"tree\",\n  combinedXpathMap: {},\n  combinedUrlMap: {},\n  perFrame: [],\n};\n\ndescribe(\"Page.snapshot\", () => {\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"forwards the includeIframes flag to captureHybridSnapshot\", async () => {\n    vi.spyOn(fs, \"writeFile\").mockResolvedValue();\n    const captureSpy = vi\n      .spyOn(snapshotModule, \"captureHybridSnapshot\")\n      .mockResolvedValue(baseSnapshot);\n\n    const fakePage = {} as Page;\n    await Page.prototype.snapshot.call(fakePage, { includeIframes: false });\n\n    expect(captureSpy).toHaveBeenCalledWith(fakePage, {\n      pierceShadow: true,\n      includeIframes: false,\n    });\n  });\n\n  it(\"falls back to default iframe inclusion when option is omitted\", async () => {\n    vi.spyOn(fs, \"writeFile\").mockResolvedValue();\n    const captureSpy = vi\n      .spyOn(snapshotModule, \"captureHybridSnapshot\")\n      .mockResolvedValue(baseSnapshot);\n\n    const fakePage = {} as Page;\n    await Page.prototype.snapshot.call(fakePage);\n\n    expect(captureSpy).toHaveBeenCalledWith(fakePage, {\n      pierceShadow: true,\n      includeIframes: undefined,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/export-surface.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport StagehandDefaultExport, * as Stagehand from \"@browserbasehq/stagehand\";\nimport { publicErrorTypes } from \"./public-error-types.test.js\";\n\n// Type matcher guidelines:\n//\n// toEqualTypeOf – Default. Assert full, deep type equality; any type change should fail.\n//   e.g. expectTypeOf<ReturnType<typeof foo>>().toEqualTypeOf<FooResult>()\n//\n// toMatchObjectType – Assert (part of) an object's shape while allowing extra fields.\n//   e.g. expectTypeOf(user).toMatchObjectType<{ id: string; email: string }>()\n//\n// toExtend – Assert that a type is compatible with a broader contract (assignable/extends).\n//   e.g. expectTypeOf<User>().toExtend<BaseUser>()\n\nconst publicApiShape = {\n  __internalMaybeRunShutdownSupervisorFromArgv:\n    Stagehand.__internalMaybeRunShutdownSupervisorFromArgv,\n  __internalCreateInMemoryAgentCacheHandle:\n    Stagehand.__internalCreateInMemoryAgentCacheHandle,\n  AISdkClient: Stagehand.AISdkClient,\n  Api: Stagehand.Api,\n  AVAILABLE_CUA_MODELS: Stagehand.AVAILABLE_CUA_MODELS,\n  AgentProvider: Stagehand.AgentProvider,\n  AnnotatedScreenshotText: Stagehand.AnnotatedScreenshotText,\n  ConsoleMessage: Stagehand.ConsoleMessage,\n  CustomOpenAIClient: Stagehand.CustomOpenAIClient,\n  LLMClient: Stagehand.LLMClient,\n  LOG_LEVEL_NAMES: Stagehand.LOG_LEVEL_NAMES,\n  Response: Stagehand.Response,\n  Stagehand: Stagehand.Stagehand,\n  V3: Stagehand.V3,\n  V3Evaluator: Stagehand.V3Evaluator,\n  V3FunctionName: Stagehand.V3FunctionName,\n  connectToMCPServer: Stagehand.connectToMCPServer,\n  default: StagehandDefaultExport,\n  defaultExtractSchema: Stagehand.defaultExtractSchema,\n  getAISDKLanguageModel: Stagehand.getAISDKLanguageModel,\n  getZodType: Stagehand.getZodType,\n  injectUrls: Stagehand.injectUrls,\n  isRunningInBun: Stagehand.isRunningInBun,\n  isZod3Schema: Stagehand.isZod3Schema,\n  isZod4Schema: Stagehand.isZod4Schema,\n  jsonSchemaToZod: Stagehand.jsonSchemaToZod,\n  loadApiKeyFromEnv: Stagehand.loadApiKeyFromEnv,\n  localBrowserLaunchOptionsSchema: Stagehand.localBrowserLaunchOptionsSchema,\n  modelToAgentProviderMap: Stagehand.modelToAgentProviderMap,\n  pageTextSchema: Stagehand.pageTextSchema,\n  providerEnvVarMap: Stagehand.providerEnvVarMap,\n  toGeminiSchema: Stagehand.toGeminiSchema,\n  toJsonSchema: Stagehand.toJsonSchema,\n  tool: Stagehand.tool,\n  transformSchema: Stagehand.transformSchema,\n  trimTrailingTextNode: Stagehand.trimTrailingTextNode,\n  validateZodSchema: Stagehand.validateZodSchema,\n  ...publicErrorTypes,\n} as const;\n\ntype StagehandExports = typeof Stagehand & {\n  default: typeof StagehandDefaultExport;\n};\n\ntype PublicAPI = {\n  [K in keyof typeof publicApiShape]: StagehandExports[K];\n};\n\ndescribe(\"Stagehand public API export surface\", () => {\n  it(\"public API shape matches module exports\", () => {\n    const _check: PublicAPI = publicApiShape;\n    void _check;\n  });\n\n  it(\"does not expose unexpected top-level exports\", () => {\n    const expected = Object.keys(publicApiShape).sort();\n    const actual = Object.keys(Stagehand).sort();\n    expect(actual).toStrictEqual(expected);\n  });\n\n  it(\"default export mirrors the named export surface\", () => {\n    const expected = Object.keys(Stagehand)\n      .filter((key) => key !== \"default\")\n      .sort();\n    const actual = Object.keys(StagehandDefaultExport).sort();\n    expect(actual).toStrictEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/llm-and-agents.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\ndescribe(\"LLM and Agents public API types\", () => {\n  describe(\"ModelConfiguration\", () => {\n    it(\"accepts Vertex headers in model config\", () => {\n      const googleConfig = {\n        modelName: \"google/gemini-3-flash-preview\",\n        project: \"test-project\",\n        location: \"global\",\n        headers: {\n          \"X-Goog-Priority\": \"high\",\n        },\n      } satisfies Stagehand.ModelConfiguration;\n\n      void googleConfig;\n    });\n  });\n\n  describe(\"AISdkClient\", () => {\n    type AISdkClientInstance = InstanceType<typeof Stagehand.AISdkClient>;\n\n    it(\"is exported\", () => {\n      expect(Stagehand.AISdkClient).toBeDefined();\n    });\n\n    it(\"extends LLMClient\", () => {\n      expectTypeOf<AISdkClientInstance>().toExtend<Stagehand.LLMClient>();\n    });\n\n    it(\"constructor accepts model parameter\", () => {\n      // AISdkClient constructor takes { model: LanguageModelV2 }\n      type CtorParams = ConstructorParameters<typeof Stagehand.AISdkClient>;\n      expectTypeOf<CtorParams[\"length\"]>().toEqualTypeOf<1>();\n    });\n  });\n\n  describe(\"AVAILABLE_CUA_MODELS\", () => {\n    const expectedModels = [\n      \"openai/computer-use-preview\",\n      \"openai/computer-use-preview-2025-03-11\",\n      \"anthropic/claude-opus-4-5-20251101\",\n      \"anthropic/claude-opus-4-6\",\n      \"anthropic/claude-sonnet-4-6\",\n      \"anthropic/claude-haiku-4-5-20251001\",\n      \"anthropic/claude-sonnet-4-20250514\",\n      \"anthropic/claude-sonnet-4-5-20250929\",\n      \"google/gemini-2.5-computer-use-preview-10-2025\",\n      \"google/gemini-3-flash-preview\",\n      \"google/gemini-3-pro-preview\",\n      \"microsoft/fara-7b\",\n    ] as const;\n\n    it(\"AvailableCuaModel matches the known literals\", () => {\n      expectTypeOf<Stagehand.AvailableCuaModel>().toEqualTypeOf<\n        (typeof expectedModels)[number]\n      >();\n      void expectedModels; // Mark as used to satisfy ESLint\n    });\n  });\n\n  describe(\"AgentProvider\", () => {\n    type AgentProviderInstance = InstanceType<typeof Stagehand.AgentProvider>;\n\n    it(\"is exported\", () => {\n      expect(Stagehand.AgentProvider).toBeDefined();\n    });\n\n    it(\"has getClient method\", () => {\n      expectTypeOf<AgentProviderInstance[\"getClient\"]>().toBeCallableWith(\n        \"test-model\",\n      );\n    });\n\n    it(\"constructor accepts logger parameter\", () => {\n      expectTypeOf<\n        ConstructorParameters<typeof Stagehand.AgentProvider>\n      >().toEqualTypeOf<[(message: Stagehand.LogLine) => void]>();\n    });\n  });\n\n  describe(\"AnnotatedScreenshotText\", () => {\n    type ExpectedAnnotatedScreenshotText = string;\n\n    it(\"is a string literal\", () => {\n      expectTypeOf<\n        typeof Stagehand.AnnotatedScreenshotText\n      >().toExtend<ExpectedAnnotatedScreenshotText>();\n    });\n  });\n\n  describe(\"ConsoleMessage\", () => {\n    type ExpectedShape = {\n      type: () => string;\n      text: () => string;\n      args: () => unknown[];\n      location: () => {\n        url?: string;\n        lineNumber?: number;\n        columnNumber?: number;\n      };\n      page: () => unknown;\n      timestamp: () => number | undefined;\n      raw: () => unknown;\n      toString: () => string;\n    };\n\n    type ConsoleMessageInstance = InstanceType<typeof Stagehand.ConsoleMessage>;\n\n    it(\"has correct public interface shape\", () => {\n      expectTypeOf<ConsoleMessageInstance>().toExtend<ExpectedShape>();\n    });\n  });\n\n  describe(\"AgentClient\", () => {\n    type AgentProviderInstance = InstanceType<typeof Stagehand.AgentProvider>;\n    type GetClientReturn = ReturnType<AgentProviderInstance[\"getClient\"]>;\n\n    it(\"getClient returns object with expected methods\", () => {\n      type ExpectedShape = {\n        execute: (\n          options: Stagehand.AgentExecutionOptions,\n        ) => Promise<Stagehand.AgentResult>;\n        captureScreenshot: (\n          options?: Record<string, unknown>,\n        ) => Promise<unknown>;\n        setViewport: (width: number, height: number) => void;\n        setCurrentUrl: (url: string) => void;\n        setScreenshotProvider: (provider: () => Promise<string>) => void;\n        setActionHandler: (\n          handler: (action: Stagehand.AgentAction) => Promise<void>,\n        ) => void;\n      };\n      expectTypeOf<GetClientReturn>().toExtend<ExpectedShape>();\n    });\n  });\n\n  describe(\"LLMClient\", () => {\n    type ExpectedShape = {\n      type: \"openai\" | \"anthropic\" | \"cerebras\" | \"groq\" | (string & {});\n      modelName: Stagehand.AvailableModel | (string & {});\n      hasVision: boolean;\n      clientOptions: Stagehand.ClientOptions;\n      userProvidedInstructions?: string;\n    };\n\n    type ExpectedCtorParams = [Stagehand.AvailableModel, string?];\n\n    type ExpectedBasicOptions = {\n      options: {\n        messages: Array<{\n          role: \"system\" | \"user\" | \"assistant\";\n          content: string | Array<unknown>;\n        }>;\n      };\n      logger: (message: unknown) => void;\n      retries?: number;\n    };\n\n    type ExpectedWithResponseModel = ExpectedBasicOptions & {\n      options: ExpectedBasicOptions[\"options\"] & {\n        response_model: {\n          name: string;\n          schema: Stagehand.StagehandZodSchema;\n        };\n      };\n    };\n\n    type LLMClientInstance = InstanceType<typeof Stagehand.LLMClient>;\n\n    it(\"has correct public interface shape\", () => {\n      expectTypeOf<LLMClientInstance>().toExtend<ExpectedShape>();\n    });\n\n    it(\"constructor parameters match expected signature\", () => {\n      expectTypeOf<\n        ConstructorParameters<typeof Stagehand.LLMClient>\n      >().toEqualTypeOf<ExpectedCtorParams>();\n    });\n\n    it(\"createChatCompletion can be called with basic options\", () => {\n      expectTypeOf<\n        LLMClientInstance[\"createChatCompletion\"]\n      >().toBeCallableWith({\n        options: {\n          messages: [\n            {\n              role: \"user\",\n              content: \"Hello\",\n            },\n          ],\n        },\n        logger: () => {},\n      } satisfies ExpectedBasicOptions);\n    });\n\n    it(\"createChatCompletion can be called with response_model\", () => {\n      const mockSchema = {} as Stagehand.StagehandZodSchema;\n      expectTypeOf<\n        LLMClientInstance[\"createChatCompletion\"]\n      >().toBeCallableWith({\n        options: {\n          messages: [\n            {\n              role: \"user\",\n              content: \"Extract data\",\n            },\n          ],\n          response_model: {\n            name: \"extracted\",\n            schema: mockSchema,\n          },\n        },\n        logger: () => {},\n      } satisfies ExpectedWithResponseModel);\n    });\n\n    it(\"createChatCompletion supports generic return type\", () => {\n      type Result = { custom: string };\n      type ExpectedSignature = (\n        options: Stagehand.CreateChatCompletionOptions,\n      ) => Promise<Result>;\n\n      expectTypeOf<\n        LLMClientInstance[\"createChatCompletion\"]\n      >().toExtend<ExpectedSignature>();\n    });\n\n    it(\"has additional methods\", () => {\n      // These methods exist on LLMClient but have complex signatures from the 'ai' library\n      // We verify they exist by checking they're functions\n      expectTypeOf<LLMClientInstance[\"generateText\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"generateObject\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"streamText\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"streamObject\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"generateImage\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"embed\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"embedMany\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"transcribe\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n      expectTypeOf<LLMClientInstance[\"generateSpeech\"]>().toExtend<\n        (...args: unknown[]) => unknown\n      >();\n    });\n  });\n\n  describe(\"modelToAgentProviderMap\", () => {\n    type ExpectedModelToAgentProviderMap = Record<\n      string,\n      Stagehand.AgentProviderType\n    >;\n\n    it(\"only stores valid provider types\", () => {\n      expectTypeOf<\n        typeof Stagehand.modelToAgentProviderMap\n      >().toExtend<ExpectedModelToAgentProviderMap>();\n    });\n  });\n\n  describe(\"Response\", () => {\n    type ExpectedShape = {\n      url: () => string;\n      status: () => number;\n      statusText: () => string;\n      ok: () => boolean;\n      frame: () => unknown;\n      fromServiceWorker: () => boolean;\n      securityDetails: () => Promise<unknown>;\n      serverAddr: () => Promise<unknown>;\n      headers: () => Record<string, string>;\n      allHeaders: () => Promise<Record<string, string>>;\n      headerValue: (name: string) => Promise<string | null>;\n      headerValues: (name: string) => Promise<string[]>;\n      headersArray: () => Promise<Array<{ name: string; value: string }>>;\n      body: () => Promise<Buffer>;\n      text: () => Promise<string>;\n      json: <T = unknown>() => Promise<T>;\n      finished: () => Promise<null | Error>;\n      markFinished: (error: Error | null) => void;\n      applyExtraInfo: (info: unknown) => void;\n    };\n\n    type ResponseInstance = InstanceType<typeof Stagehand.Response>;\n\n    it(\"has correct public interface shape\", () => {\n      expectTypeOf<ResponseInstance>().toExtend<ExpectedShape>();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/public-error-types.test.ts",
    "content": "import { describe, expectTypeOf, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\nexport const publicErrorTypes = {\n  AgentAbortError: Stagehand.AgentAbortError,\n  CdpConnectionClosedError: Stagehand.CdpConnectionClosedError,\n  AgentScreenshotProviderError: Stagehand.AgentScreenshotProviderError,\n  BrowserbaseSessionNotFoundError: Stagehand.BrowserbaseSessionNotFoundError,\n  CaptchaTimeoutError: Stagehand.CaptchaTimeoutError,\n  ConnectionTimeoutError: Stagehand.ConnectionTimeoutError,\n  ContentFrameNotFoundError: Stagehand.ContentFrameNotFoundError,\n  CookieSetError: Stagehand.CookieSetError,\n  CookieValidationError: Stagehand.CookieValidationError,\n  CreateChatCompletionResponseError:\n    Stagehand.CreateChatCompletionResponseError,\n  CuaModelRequiredError: Stagehand.CuaModelRequiredError,\n  ElementNotVisibleError: Stagehand.ElementNotVisibleError,\n  ExperimentalApiConflictError: Stagehand.ExperimentalApiConflictError,\n  ExperimentalNotConfiguredError: Stagehand.ExperimentalNotConfiguredError,\n  HandlerNotInitializedError: Stagehand.HandlerNotInitializedError,\n  InvalidAISDKModelFormatError: Stagehand.InvalidAISDKModelFormatError,\n  LLMResponseError: Stagehand.LLMResponseError,\n  MCPConnectionError: Stagehand.MCPConnectionError,\n  MissingEnvironmentVariableError: Stagehand.MissingEnvironmentVariableError,\n  MissingLLMConfigurationError: Stagehand.MissingLLMConfigurationError,\n  PageNotFoundError: Stagehand.PageNotFoundError,\n  ResponseBodyError: Stagehand.ResponseBodyError,\n  ResponseParseError: Stagehand.ResponseParseError,\n  StagehandAPIError: Stagehand.StagehandAPIError,\n  StagehandAPIUnauthorizedError: Stagehand.StagehandAPIUnauthorizedError,\n  StagehandClickError: Stagehand.StagehandClickError,\n  StagehandClosedError: Stagehand.StagehandClosedError,\n  StagehandDefaultError: Stagehand.StagehandDefaultError,\n  StagehandDomProcessError: Stagehand.StagehandDomProcessError,\n  StagehandElementNotFoundError: Stagehand.StagehandElementNotFoundError,\n  StagehandEnvironmentError: Stagehand.StagehandEnvironmentError,\n  StagehandError: Stagehand.StagehandError,\n  StagehandEvalError: Stagehand.StagehandEvalError,\n  StagehandHttpError: Stagehand.StagehandHttpError,\n  StagehandIframeError: Stagehand.StagehandIframeError,\n  StagehandInitError: Stagehand.StagehandInitError,\n  StagehandInvalidArgumentError: Stagehand.StagehandInvalidArgumentError,\n  StagehandLocatorError: Stagehand.StagehandLocatorError,\n  StagehandMissingArgumentError: Stagehand.StagehandMissingArgumentError,\n  StagehandNotInitializedError: Stagehand.StagehandNotInitializedError,\n  StagehandResponseBodyError: Stagehand.StagehandResponseBodyError,\n  StagehandResponseParseError: Stagehand.StagehandResponseParseError,\n  StagehandServerError: Stagehand.StagehandServerError,\n  StagehandShadowRootMissingError: Stagehand.StagehandShadowRootMissingError,\n  StagehandShadowSegmentEmptyError: Stagehand.StagehandShadowSegmentEmptyError,\n  StagehandShadowSegmentNotFoundError:\n    Stagehand.StagehandShadowSegmentNotFoundError,\n  StreamingCallbacksInNonStreamingModeError:\n    Stagehand.StreamingCallbacksInNonStreamingModeError,\n  StagehandSnapshotError: Stagehand.StagehandSnapshotError,\n  TimeoutError: Stagehand.TimeoutError,\n  UnsupportedAISDKModelProviderError:\n    Stagehand.UnsupportedAISDKModelProviderError,\n  UnsupportedModelError: Stagehand.UnsupportedModelError,\n  UnsupportedModelProviderError: Stagehand.UnsupportedModelProviderError,\n  XPathResolutionError: Stagehand.XPathResolutionError,\n  ZodSchemaValidationError: Stagehand.ZodSchemaValidationError,\n  ActTimeoutError: Stagehand.ActTimeoutError,\n  ObserveTimeoutError: Stagehand.ObserveTimeoutError,\n  ExtractTimeoutError: Stagehand.ExtractTimeoutError,\n  UnderstudyCommandException: Stagehand.UnderstudyCommandException,\n  StagehandSetExtraHTTPHeadersError:\n    Stagehand.StagehandSetExtraHTTPHeadersError,\n} as const;\n\nconst errorTypes = Object.keys(publicErrorTypes) as Array<\n  keyof typeof publicErrorTypes\n>;\n\ndescribe(\"Stagehand public error types\", () => {\n  describe(\"errors\", () => {\n    it.each(errorTypes)(\"%s extends Error\", (errorTypeName) => {\n      const ErrorClass = Stagehand[errorTypeName];\n      type ErrorClassType = typeof ErrorClass;\n      expectTypeOf<InstanceType<ErrorClassType>>().toExtend<Error>();\n      void ErrorClass; // Mark as used to satisfy ESLint\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/public-types.test.ts",
    "content": "import { describe, expectTypeOf, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\n// Type-level manifest of all expected exported types\n// Since these types don't exist at runtime, we currently need to manually add new publicly exported types\n// to this list ourselves - it's not automatically going to catch changes like our export-surface.test.ts does.\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype ExpectedExportedTypes = {\n  // Types from model.ts\n  AvailableModel: Stagehand.AvailableModel;\n  AvailableCuaModel: Stagehand.AvailableCuaModel;\n  ModelProvider: Stagehand.ModelProvider;\n  ClientOptions: Stagehand.ClientOptions;\n  ModelConfiguration: Stagehand.ModelConfiguration;\n  AnthropicJsonSchemaObject: Stagehand.AnthropicJsonSchemaObject;\n  AISDKProvider: Stagehand.AISDKProvider;\n  AISDKCustomProvider: Stagehand.AISDKCustomProvider;\n  LLMTool: Stagehand.LLMTool;\n  // Types from methods.ts\n  ActOptions: Stagehand.ActOptions;\n  ActResult: Stagehand.ActResult;\n  ExtractResult: Stagehand.ExtractResult<Stagehand.StagehandZodSchema>;\n  Action: Stagehand.Action;\n  HistoryEntry: Stagehand.HistoryEntry;\n  ExtractOptions: Stagehand.ExtractOptions;\n  ObserveOptions: Stagehand.ObserveOptions;\n  ObserveResult: Stagehand.ObserveResult;\n  V3FunctionName: Stagehand.V3FunctionName;\n  // Types from agent.ts\n  Tool: Stagehand.Tool;\n  AgentAction: Stagehand.AgentAction;\n  AgentResult: Stagehand.AgentResult;\n  AgentExecuteOptions: Stagehand.AgentExecuteOptions;\n  AgentType: Stagehand.AgentType;\n  AgentExecutionOptions: Stagehand.AgentExecutionOptions<Stagehand.AgentExecuteOptions>;\n  AgentHandlerOptions: Stagehand.AgentHandlerOptions;\n  ActionExecutionResult: Stagehand.ActionExecutionResult;\n  ToolUseItem: Stagehand.ToolUseItem;\n  AnthropicMessage: Stagehand.AnthropicMessage;\n  AnthropicContentBlock: Stagehand.AnthropicContentBlock;\n  AnthropicTextBlock: Stagehand.AnthropicTextBlock;\n  AnthropicToolResult: Stagehand.AnthropicToolResult;\n  ResponseItem: Stagehand.ResponseItem;\n  ComputerCallItem: Stagehand.ComputerCallItem;\n  FunctionCallItem: Stagehand.FunctionCallItem;\n  ResponseInputItem: Stagehand.ResponseInputItem;\n  AgentInstance: Stagehand.AgentInstance;\n  AgentProviderType: Stagehand.AgentProviderType;\n  AgentModelConfig: Stagehand.AgentModelConfig;\n  AgentConfig: Stagehand.AgentConfig;\n  AgentToolMode: Stagehand.AgentToolMode;\n  VariableValue: Stagehand.VariableValue;\n  Variables: Stagehand.Variables;\n  AgentCallbacks: Stagehand.AgentCallbacks;\n  AgentExecuteCallbacks: Stagehand.AgentExecuteCallbacks;\n  AgentStreamCallbacks: Stagehand.AgentStreamCallbacks;\n  AgentExecuteOptionsBase: Stagehand.AgentExecuteOptionsBase;\n  AgentStreamExecuteOptions: Stagehand.AgentStreamExecuteOptions;\n  ModelMessage: Stagehand.ModelMessage;\n  // Types from agent/tools\n  AgentTools: Stagehand.AgentTools;\n  AgentToolTypesMap: Stagehand.AgentToolTypesMap;\n  AgentUITools: Stagehand.AgentUITools;\n  AgentToolCall: Stagehand.AgentToolCall;\n  AgentToolResult: Stagehand.AgentToolResult;\n  // Types from logs.ts\n  LogLevel: Stagehand.LogLevel;\n  LogLine: Stagehand.LogLine;\n  Logger: Stagehand.Logger;\n  // Types from metrics.ts\n  StagehandMetrics: Stagehand.StagehandMetrics;\n  // Types from options.ts\n  V3Env: Stagehand.V3Env;\n  LocalBrowserLaunchOptions: Stagehand.LocalBrowserLaunchOptions;\n  V3Options: Stagehand.V3Options;\n  // Types from page.ts\n  AnyPage: Stagehand.AnyPage;\n  Page: Stagehand.Page;\n  PlaywrightPage: Stagehand.PlaywrightPage;\n  PatchrightPage: Stagehand.PatchrightPage;\n  PuppeteerPage: Stagehand.PuppeteerPage;\n  ConsoleListener: Stagehand.ConsoleListener;\n  LoadState: Stagehand.LoadState;\n  // Types from LLMClient.ts\n  ChatMessage: Stagehand.ChatMessage;\n  ChatMessageContent: Stagehand.ChatMessageContent;\n  ChatMessageImageContent: Stagehand.ChatMessageImageContent;\n  ChatMessageTextContent: Stagehand.ChatMessageTextContent;\n  ChatCompletionOptions: Stagehand.ChatCompletionOptions;\n  LLMResponse: Stagehand.LLMResponse;\n  CreateChatCompletionOptions: Stagehand.CreateChatCompletionOptions;\n  LLMUsage: Stagehand.LLMUsage;\n  LLMParsedResponse: Stagehand.LLMParsedResponse<Record<string, unknown>>;\n  // Types from zodCompat.ts\n  StagehandZodSchema: Stagehand.StagehandZodSchema;\n  StagehandZodObject: Stagehand.StagehandZodObject;\n  InferStagehandSchema: Stagehand.InferStagehandSchema<Stagehand.StagehandZodSchema>;\n  JsonSchemaDocument: Stagehand.JsonSchemaDocument;\n  // Types from utils.ts\n  JsonSchema: Stagehand.JsonSchema;\n  JsonSchemaProperty: Stagehand.JsonSchemaProperty;\n  // Types from cookies.ts\n  Cookie: Stagehand.Cookie;\n  CookieParam: Stagehand.CookieParam;\n  ClearCookieOptions: Stagehand.ClearCookieOptions;\n};\n\ndescribe(\"Stagehand public API types\", () => {\n  describe(\"AnyPage\", () => {\n    type ExpectedAnyPage =\n      | Stagehand.PlaywrightPage\n      | Stagehand.PuppeteerPage\n      | Stagehand.PatchrightPage\n      | Stagehand.Page;\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.AnyPage>().toEqualTypeOf<ExpectedAnyPage>();\n    });\n  });\n\n  describe(\"ActOptions\", () => {\n    type ExpectedActOptions = {\n      model?: Stagehand.ModelConfiguration;\n      variables?: Stagehand.Variables;\n      timeout?: number;\n      page?: Stagehand.AnyPage;\n      serverCache?: boolean;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.ActOptions>().toEqualTypeOf<ExpectedActOptions>();\n    });\n  });\n\n  describe(\"ActResult\", () => {\n    type ExpectedActResult = {\n      success: boolean;\n      message: string;\n      actionDescription: string;\n      actions: Stagehand.Action[];\n      cacheStatus?: \"HIT\" | \"MISS\";\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.ActResult>().toEqualTypeOf<ExpectedActResult>();\n    });\n  });\n\n  describe(\"ExtractOptions\", () => {\n    type ExpectedExtractOptions = {\n      model?: Stagehand.ModelConfiguration;\n      timeout?: number;\n      selector?: string;\n      page?: Stagehand.AnyPage;\n      serverCache?: boolean;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.ExtractOptions>().toEqualTypeOf<ExpectedExtractOptions>();\n    });\n  });\n\n  describe(\"ObserveOptions\", () => {\n    type ExpectedObserveOptions = {\n      model?: Stagehand.ModelConfiguration;\n      timeout?: number;\n      selector?: string;\n      page?: Stagehand.AnyPage;\n      serverCache?: boolean;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.ObserveOptions>().toEqualTypeOf<ExpectedObserveOptions>();\n    });\n  });\n\n  describe(\"ObserveResult\", () => {\n    it(\"is an Action array with optional cacheStatus\", () => {\n      expectTypeOf<Stagehand.ObserveResult>().toExtend<Stagehand.Action[]>();\n      expectTypeOf<Stagehand.ObserveResult[\"cacheStatus\"]>().toEqualTypeOf<\n        \"HIT\" | \"MISS\" | undefined\n      >();\n    });\n  });\n\n  describe(\"Action\", () => {\n    type ExpectedAction = {\n      selector: string;\n      description: string;\n      method?: string;\n      arguments?: string[];\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.Action>().toEqualTypeOf<ExpectedAction>();\n    });\n  });\n\n  describe(\"AgentAction\", () => {\n    // AgentAction is a separate type from Action, not an extension\n    // It has additional fields like type, reasoning, taskCompleted, etc.\n    it(\"has type field\", () => {\n      type TestAction = { type: string } & Stagehand.AgentAction;\n      expectTypeOf<TestAction[\"type\"]>().toEqualTypeOf<string>();\n    });\n  });\n\n  describe(\"AgentExecuteOptions\", () => {\n    type ExpectedAgentExecuteOptions = {\n      instruction: string;\n      maxSteps?: number;\n      page?: Stagehand.AnyPage;\n      highlightCursor?: boolean;\n      messages?: Stagehand.ModelMessage[];\n      signal?: AbortSignal;\n      excludeTools?: string[];\n      output?: Stagehand.StagehandZodObject;\n      callbacks?: Stagehand.AgentExecuteCallbacks;\n      variables?: Stagehand.Variables;\n      toolTimeout?: number;\n      useSearch?: boolean;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.AgentExecuteOptions>().toEqualTypeOf<ExpectedAgentExecuteOptions>();\n    });\n  });\n\n  describe(\"AgentStreamExecuteOptions\", () => {\n    type ExpectedAgentStreamExecuteOptions = {\n      instruction: string;\n      maxSteps?: number;\n      page?: Stagehand.AnyPage;\n      highlightCursor?: boolean;\n      messages?: Stagehand.ModelMessage[];\n      signal?: AbortSignal;\n      excludeTools?: string[];\n      output?: Stagehand.StagehandZodObject;\n      callbacks?: Stagehand.AgentStreamCallbacks;\n      variables?: Stagehand.Variables;\n      toolTimeout?: number;\n      useSearch?: boolean;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.AgentStreamExecuteOptions>().toEqualTypeOf<ExpectedAgentStreamExecuteOptions>();\n    });\n  });\n\n  describe(\"AgentExecutionOptions\", () => {\n    type ExpectedAgentExecutionOptions<T = Stagehand.AgentExecuteOptions> = {\n      options: T;\n      logger: (message: Stagehand.LogLine) => void;\n      retries?: number;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<\n        Stagehand.AgentExecutionOptions<Stagehand.AgentExecuteOptions>\n      >().toEqualTypeOf<\n        ExpectedAgentExecutionOptions<Stagehand.AgentExecuteOptions>\n      >();\n    });\n  });\n\n  describe(\"AgentResult\", () => {\n    type ExpectedAgentResult = {\n      success: boolean;\n      message: string;\n      actions: Stagehand.AgentAction[];\n      completed: boolean;\n      metadata?: Record<string, unknown>;\n      usage?: {\n        input_tokens: number;\n        output_tokens: number;\n        reasoning_tokens?: number;\n        cached_input_tokens?: number;\n        inference_time_ms: number;\n      };\n      messages?: Stagehand.ModelMessage[];\n      output?: Record<string, unknown>;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.AgentResult>().toEqualTypeOf<ExpectedAgentResult>();\n    });\n  });\n\n  describe(\"AgentConfig\", () => {\n    type ExpectedAgentConfig = {\n      systemPrompt?: string;\n      integrations?: (unknown | string)[];\n      tools?: unknown;\n      cua?: boolean;\n      model?: string | Stagehand.AgentModelConfig<string>;\n      executionModel?: string | Stagehand.AgentModelConfig<string>;\n      stream?: boolean;\n      mode?: Stagehand.AgentToolMode;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.AgentConfig>().toExtend<ExpectedAgentConfig>();\n    });\n  });\n\n  describe(\"AgentToolMode\", () => {\n    type ExpectedAgentToolMode = \"dom\" | \"hybrid\" | \"cua\";\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.AgentToolMode>().toEqualTypeOf<ExpectedAgentToolMode>();\n    });\n  });\n\n  describe(\"HistoryEntry\", () => {\n    type ExpectedHistoryEntry = {\n      method: \"act\" | \"extract\" | \"observe\" | \"navigate\" | \"agent\";\n      parameters: unknown;\n      result: unknown;\n      timestamp: string;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.HistoryEntry>().toEqualTypeOf<ExpectedHistoryEntry>();\n    });\n  });\n\n  describe(\"Cookie\", () => {\n    type ExpectedCookie = {\n      name: string;\n      value: string;\n      domain: string;\n      path: string;\n      expires: number;\n      httpOnly: boolean;\n      secure: boolean;\n      sameSite: \"Strict\" | \"Lax\" | \"None\";\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.Cookie>().toEqualTypeOf<ExpectedCookie>();\n    });\n  });\n\n  describe(\"CookieParam\", () => {\n    type ExpectedCookieParam = {\n      name: string;\n      value: string;\n      url?: string;\n      domain?: string;\n      path?: string;\n      expires?: number;\n      httpOnly?: boolean;\n      secure?: boolean;\n      sameSite?: \"Strict\" | \"Lax\" | \"None\";\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.CookieParam>().toEqualTypeOf<ExpectedCookieParam>();\n    });\n  });\n\n  describe(\"ClearCookieOptions\", () => {\n    type ExpectedClearCookieOptions = {\n      name?: string | RegExp;\n      domain?: string | RegExp;\n      path?: string | RegExp;\n    };\n\n    it(\"matches expected type shape\", () => {\n      expectTypeOf<Stagehand.ClearCookieOptions>().toEqualTypeOf<ExpectedClearCookieOptions>();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/runtime-utils.test.ts",
    "content": "import { describe, expectTypeOf, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\ndescribe(\"Runtime Utils public API types\", () => {\n  describe(\"injectUrls\", () => {\n    type ExpectedInjectUrlsParams = [\n      unknown,\n      Array<string | number>,\n      Record<string, string>,\n    ];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.injectUrls,\n      ).parameters.branded.toEqualTypeOf<ExpectedInjectUrlsParams>();\n    });\n  });\n\n  describe(\"isRunningInBun\", () => {\n    type ExpectedIsRunningInBunParams = [];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.isRunningInBun,\n      ).parameters.branded.toEqualTypeOf<ExpectedIsRunningInBunParams>();\n    });\n  });\n\n  describe(\"loadApiKeyFromEnv\", () => {\n    type ExpectedLoadApiKeyFromEnvParams = [\n      string | undefined,\n      (logLine: Stagehand.LogLine) => void,\n    ];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.loadApiKeyFromEnv,\n      ).parameters.branded.toEqualTypeOf<ExpectedLoadApiKeyFromEnvParams>();\n    });\n  });\n\n  describe(\"providerEnvVarMap\", () => {\n    type ExpectedProviderEnvVarMap = Partial<\n      Record<string, string | Array<string>>\n    >;\n\n    it(\"maps providers to environment variable names\", () => {\n      expectTypeOf<\n        typeof Stagehand.providerEnvVarMap\n      >().toExtend<ExpectedProviderEnvVarMap>();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/schema-utils.test.ts",
    "content": "import { describe, expectTypeOf, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\ndescribe(\"Schema Utils public API types\", () => {\n  describe(\"defaultExtractSchema\", () => {\n    type ExpectedInferredType = { extraction: string };\n\n    it(\"infers to the correct type\", () => {\n      expectTypeOf<\n        Stagehand.InferStagehandSchema<typeof Stagehand.defaultExtractSchema>\n      >().toEqualTypeOf<ExpectedInferredType>();\n    });\n  });\n\n  describe(\"getZodType\", () => {\n    type ExpectedGetZodTypeParams = [Stagehand.StagehandZodSchema];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.getZodType,\n      ).parameters.branded.toEqualTypeOf<ExpectedGetZodTypeParams>();\n    });\n  });\n\n  describe(\"isZod3Schema\", () => {\n    type ExpectedIsZod3SchemaParams = [Stagehand.StagehandZodSchema];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.isZod3Schema,\n      ).parameters.branded.toEqualTypeOf<ExpectedIsZod3SchemaParams>();\n    });\n  });\n\n  describe(\"isZod4Schema\", () => {\n    type ExpectedIsZod4SchemaParams = [Stagehand.StagehandZodSchema];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.isZod4Schema,\n      ).parameters.branded.toEqualTypeOf<ExpectedIsZod4SchemaParams>();\n    });\n  });\n\n  describe(\"jsonSchemaToZod\", () => {\n    type ExpectedJsonSchemaToZodParams = [Stagehand.JsonSchema];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.jsonSchemaToZod,\n      ).parameters.branded.toEqualTypeOf<ExpectedJsonSchemaToZodParams>();\n    });\n  });\n\n  describe(\"pageTextSchema\", () => {\n    type ExpectedInferredType = { pageText: string };\n\n    it(\"infers to the correct type\", () => {\n      expectTypeOf<\n        Stagehand.InferStagehandSchema<typeof Stagehand.pageTextSchema>\n      >().toEqualTypeOf<ExpectedInferredType>();\n    });\n  });\n\n  describe(\"toGeminiSchema\", () => {\n    type ExpectedToGeminiSchemaParams = [Stagehand.StagehandZodSchema];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.toGeminiSchema,\n      ).parameters.branded.toEqualTypeOf<ExpectedToGeminiSchemaParams>();\n    });\n  });\n\n  describe(\"toJsonSchema\", () => {\n    type ExpectedToJsonSchemaParams = [Stagehand.StagehandZodSchema];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.toJsonSchema,\n      ).parameters.branded.toEqualTypeOf<ExpectedToJsonSchemaParams>();\n    });\n  });\n\n  describe(\"transformSchema\", () => {\n    type ExpectedTransformSchemaParams = [\n      Stagehand.StagehandZodSchema,\n      Array<string | number>,\n    ];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.transformSchema,\n      ).parameters.branded.toEqualTypeOf<ExpectedTransformSchemaParams>();\n    });\n  });\n\n  describe(\"trimTrailingTextNode\", () => {\n    type ExpectedTrimTrailingTextNodeParams = [string | undefined];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.trimTrailingTextNode,\n      ).parameters.branded.toEqualTypeOf<ExpectedTrimTrailingTextNodeParams>();\n    });\n  });\n\n  describe(\"validateZodSchema\", () => {\n    type ExpectedValidateZodSchemaParams = [\n      Stagehand.StagehandZodSchema,\n      unknown,\n    ];\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.validateZodSchema,\n      ).parameters.branded.toEqualTypeOf<ExpectedValidateZodSchemaParams>();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/timeout-error-types.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\n// ============================================================================\n// Public Timeout Error Types Runtime Tests\n// ============================================================================\n// These tests verify the runtime behavior of exported timeout error types,\n// complementing the type-level tests in public-error-types.test.ts\n\ndescribe(\"Public timeout error types runtime behavior\", () => {\n  describe(\"ActTimeoutError\", () => {\n    it(\"is exported and extends Error\", () => {\n      const error = new Stagehand.ActTimeoutError(1000);\n      expect(error).toBeInstanceOf(Error);\n      expect(error).toBeInstanceOf(Stagehand.ActTimeoutError);\n      expect(error.name).toBe(\"ActTimeoutError\");\n    });\n\n    it(\"contains timeout value in milliseconds in message\", () => {\n      const error = new Stagehand.ActTimeoutError(500);\n      expect(error.message).toContain(\"500ms\");\n    });\n\n    it(\"contains operation name in message\", () => {\n      const error = new Stagehand.ActTimeoutError(100);\n      expect(error.message).toContain(\"act()\");\n    });\n\n    it(\"extends TimeoutError\", () => {\n      const error = new Stagehand.ActTimeoutError(1000);\n      expect(error).toBeInstanceOf(Stagehand.TimeoutError);\n    });\n  });\n\n  describe(\"ExtractTimeoutError\", () => {\n    it(\"is exported and extends Error\", () => {\n      const error = new Stagehand.ExtractTimeoutError(1000);\n      expect(error).toBeInstanceOf(Error);\n      expect(error).toBeInstanceOf(Stagehand.ExtractTimeoutError);\n      expect(error.name).toBe(\"ExtractTimeoutError\");\n    });\n\n    it(\"contains timeout value in milliseconds in message\", () => {\n      const error = new Stagehand.ExtractTimeoutError(1000);\n      expect(error.message).toContain(\"1000ms\");\n    });\n\n    it(\"contains operation name in message\", () => {\n      const error = new Stagehand.ExtractTimeoutError(100);\n      expect(error.message).toContain(\"extract()\");\n    });\n\n    it(\"extends TimeoutError\", () => {\n      const error = new Stagehand.ExtractTimeoutError(1000);\n      expect(error).toBeInstanceOf(Stagehand.TimeoutError);\n    });\n  });\n\n  describe(\"ObserveTimeoutError\", () => {\n    it(\"is exported and extends Error\", () => {\n      const error = new Stagehand.ObserveTimeoutError(1000);\n      expect(error).toBeInstanceOf(Error);\n      expect(error).toBeInstanceOf(Stagehand.ObserveTimeoutError);\n      expect(error.name).toBe(\"ObserveTimeoutError\");\n    });\n\n    it(\"contains timeout value in milliseconds in message\", () => {\n      const error = new Stagehand.ObserveTimeoutError(1500);\n      expect(error.message).toContain(\"1500ms\");\n    });\n\n    it(\"contains operation name in message\", () => {\n      const error = new Stagehand.ObserveTimeoutError(100);\n      expect(error.message).toContain(\"observe()\");\n    });\n\n    it(\"extends TimeoutError\", () => {\n      const error = new Stagehand.ObserveTimeoutError(1000);\n      expect(error).toBeInstanceOf(Stagehand.TimeoutError);\n    });\n  });\n\n  describe(\"TimeoutError (base class)\", () => {\n    it(\"is exported and extends Error\", () => {\n      const error = new Stagehand.TimeoutError(\"custom operation\", 2000);\n      expect(error).toBeInstanceOf(Error);\n      expect(error).toBeInstanceOf(Stagehand.TimeoutError);\n    });\n\n    it(\"contains operation name and timeout in message\", () => {\n      const error = new Stagehand.TimeoutError(\"custom operation\", 2000);\n      expect(error.message).toContain(\"custom operation\");\n      expect(error.message).toContain(\"2000ms\");\n    });\n\n    it(\"extends StagehandError\", () => {\n      const error = new Stagehand.TimeoutError(\"operation\", 1000);\n      expect(error).toBeInstanceOf(Stagehand.StagehandError);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/tool-type-export.test.ts",
    "content": "import { describe, expectTypeOf, it, expect } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\nimport { type Tool } from \"ai\";\nimport { z } from \"zod\";\n\n/**\n * Test to verify tool-related exports from Stagehand.\n * Users should be able to create custom tools using the exported `tool` function\n * without needing to install the ai package directly.\n */\ndescribe(\"Tool exports from AI SDK\", () => {\n  it(\"exports Tool type that matches AI SDK Tool type\", () => {\n    expectTypeOf<Stagehand.Tool>().toEqualTypeOf<Tool>();\n  });\n\n  it(\"exports tool function\", () => {\n    expect(typeof Stagehand.tool).toBe(\"function\");\n  });\n\n  it(\"tool function can be used to define custom tools\", () => {\n    const customTool = Stagehand.tool({\n      description: \"A test tool\",\n      inputSchema: z.object({\n        input: z.string(),\n      }),\n      execute: async ({ input }) => {\n        return { result: `Processed: ${input}` };\n      },\n    });\n\n    expect(customTool).toBeDefined();\n    expect(customTool.description).toBe(\"A test tool\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/public-api/v3-core.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from \"vitest\";\nimport * as Stagehand from \"@browserbasehq/stagehand\";\n\ndescribe(\"V3 Core public API types\", () => {\n  describe(\"Stagehand\", () => {\n    type ExpectedShape = {\n      init: () => Promise<void>;\n      close: (opts?: { force?: boolean }) => Promise<void>;\n      act: (\n        input: string | Stagehand.Action,\n        options?: Stagehand.ActOptions,\n      ) => Promise<Stagehand.ActResult>;\n      extract: (...args: unknown[]) => Promise<unknown>;\n      observe: (...args: unknown[]) => Promise<Stagehand.Action[]>;\n      agent: (config?: Stagehand.AgentConfig) => {\n        execute: (\n          instructionOrOptions: string | Stagehand.AgentExecuteOptions,\n        ) => Promise<Stagehand.AgentResult>;\n      };\n      connectURL: () => string;\n      context: unknown;\n      metrics: Promise<Stagehand.StagehandMetrics>;\n      history: Promise<ReadonlyArray<Stagehand.HistoryEntry>>;\n      llmClient: Stagehand.LLMClient;\n      browserbaseSessionID: string | undefined;\n      browserbaseSessionURL: string | undefined;\n      browserbaseDebugURL: string | undefined;\n      experimental: boolean;\n      logInferenceToFile: boolean;\n      verbose: 0 | 1 | 2;\n      logger: (logLine: Stagehand.LogLine) => void;\n      isAgentReplayActive: () => boolean;\n      recordAgentReplayStep: (step: unknown) => void;\n    };\n\n    type StagehandInstance = InstanceType<typeof Stagehand.Stagehand>;\n\n    it(\"has correct public interface shape\", () => {\n      expectTypeOf<StagehandInstance>().toExtend<ExpectedShape>();\n    });\n\n    it(\"act accepts Action as first parameter\", () => {\n      const mockAction = {} as Stagehand.Action;\n      expectTypeOf<StagehandInstance[\"act\"]>().toBeCallableWith(\n        mockAction,\n        {} as Stagehand.ActOptions,\n      );\n    });\n\n    it(\"extract accepts instruction and schema\", () => {\n      const mockSchema = {} as Stagehand.StagehandZodSchema;\n      expectTypeOf<StagehandInstance[\"extract\"]>().toBeCallableWith(\n        \"instruction\",\n        mockSchema,\n        {} as Stagehand.ExtractOptions,\n      );\n    });\n\n    it(\"observe accepts instruction and options\", () => {\n      expectTypeOf<StagehandInstance[\"observe\"]>().toBeCallableWith(\n        \"instruction\",\n        {} as Stagehand.ObserveOptions,\n      );\n    });\n\n    it(\"agent execute accepts page option\", () => {\n      type AgentReturn = ReturnType<StagehandInstance[\"agent\"]>;\n      const mockPage = {} as Stagehand.AnyPage;\n      expectTypeOf<AgentReturn[\"execute\"]>().toBeCallableWith({\n        instruction: \"test\",\n        page: mockPage,\n      } satisfies Stagehand.AgentExecuteOptions);\n    });\n  });\n\n  describe(\"StagehandMetrics\", () => {\n    type ExpectedStagehandMetrics = {\n      actPromptTokens: number;\n      actCompletionTokens: number;\n      actReasoningTokens: number;\n      actCachedInputTokens: number;\n      actInferenceTimeMs: number;\n      extractPromptTokens: number;\n      extractCompletionTokens: number;\n      extractReasoningTokens: number;\n      extractCachedInputTokens: number;\n      extractInferenceTimeMs: number;\n      observePromptTokens: number;\n      observeCompletionTokens: number;\n      observeReasoningTokens: number;\n      observeCachedInputTokens: number;\n      observeInferenceTimeMs: number;\n      agentPromptTokens: number;\n      agentCompletionTokens: number;\n      agentReasoningTokens: number;\n      agentCachedInputTokens: number;\n      agentInferenceTimeMs: number;\n      totalPromptTokens: number;\n      totalCompletionTokens: number;\n      totalReasoningTokens: number;\n      totalCachedInputTokens: number;\n      totalInferenceTimeMs: number;\n    };\n\n    it(\"matches the published metrics shape\", () => {\n      expectTypeOf<Stagehand.StagehandMetrics>().toEqualTypeOf<ExpectedStagehandMetrics>();\n    });\n  });\n\n  describe(\"V3\", () => {\n    // V3 is the same class as Stagehand, just re-exported with a different name.\n    // The public interface shape is already tested in the \"Stagehand\" test above.\n    it(\"is exported\", () => {\n      expect(Stagehand.V3).toBeDefined();\n    });\n  });\n\n  describe(\"V3Evaluator\", () => {\n    type V3EvaluatorInstance = InstanceType<typeof Stagehand.V3Evaluator>;\n\n    it(\"is exported\", () => {\n      expect(Stagehand.V3Evaluator).toBeDefined();\n    });\n\n    it(\"has ask method\", () => {\n      expectTypeOf<V3EvaluatorInstance[\"ask\"]>().toExtend<\n        (options: unknown) => Promise<unknown>\n      >();\n    });\n\n    it(\"has batchAsk method\", () => {\n      expectTypeOf<V3EvaluatorInstance[\"batchAsk\"]>().toExtend<\n        (options: unknown) => Promise<unknown[]>\n      >();\n    });\n  });\n\n  describe(\"V3FunctionName\", () => {\n    const expectedFunctionNames = [\n      \"ACT\",\n      \"EXTRACT\",\n      \"OBSERVE\",\n      \"AGENT\",\n    ] as const;\n\n    it(\"matches the known function name literals\", () => {\n      expectTypeOf<Stagehand.V3FunctionName>().toExtend<\n        (typeof expectedFunctionNames)[number]\n      >();\n      void expectedFunctionNames; // Mark as used to satisfy ESLint\n    });\n  });\n\n  describe(\"connectToMCPServer\", () => {\n    type ExpectedServerConfig =\n      | string\n      | URL\n      | { command: string; args?: string[]; env?: Record<string, string> }\n      | {\n          serverUrl: string | URL;\n          clientOptions?: unknown;\n          requestOptions?: unknown;\n        };\n\n    it(\"has correct parameter types\", () => {\n      expectTypeOf(\n        Stagehand.connectToMCPServer,\n      ).parameters.branded.toEqualTypeOf<[ExpectedServerConfig]>();\n    });\n  });\n\n  describe(\"LOG_LEVEL_NAMES\", () => {\n    type ExpectedLOG_LEVEL_NAMES = Record<Stagehand.LogLevel, string>;\n\n    it(\"maps numeric levels to strings\", () => {\n      expectTypeOf<\n        typeof Stagehand.LOG_LEVEL_NAMES\n      >().toExtend<ExpectedLOG_LEVEL_NAMES>();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/safety-confirmation.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { OpenAICUAClient } from \"../../lib/v3/agent/OpenAICUAClient.js\";\nimport { GoogleCUAClient } from \"../../lib/v3/agent/GoogleCUAClient.js\";\nimport type {\n  SafetyCheck,\n  SafetyConfirmationHandler,\n} from \"../../lib/v3/types/public/agent.js\";\nimport type { LogLine } from \"../../lib/v3/types/public/logs.js\";\n\ntype LoggerMock = (message: LogLine) => void;\n\nconst openAISafetyInvoker = (\n  OpenAICUAClient.prototype as unknown as {\n    handleSafetyConfirmation: (\n      this: OpenAICUAClient,\n      pendingSafetyChecks: SafetyCheck[],\n      logger: LoggerMock,\n    ) => Promise<SafetyCheck[] | undefined>;\n  }\n).handleSafetyConfirmation;\n\nconst googleSafetyInvoker = (\n  GoogleCUAClient.prototype as unknown as {\n    handleSafetyConfirmation: (\n      this: GoogleCUAClient,\n      safetyDecision: unknown,\n      logger: LoggerMock,\n    ) => Promise<string | undefined>;\n  }\n).handleSafetyConfirmation;\n\nfunction createOpenAIClient(): OpenAICUAClient {\n  return new OpenAICUAClient(\n    \"openai\",\n    \"openai/computer-use-preview\",\n    \"test instructions\",\n    { apiKey: \"test\" },\n  );\n}\n\nfunction createGoogleClient(): GoogleCUAClient {\n  return new GoogleCUAClient(\n    \"google\",\n    \"google/gemini-2.5-computer-use-preview-10-2025\",\n    \"test instructions\",\n    { apiKey: \"test\" },\n  );\n}\n\ndescribe(\"Safety Confirmation Handler\", () => {\n  describe(\"OpenAI-style (pending_safety_checks)\", () => {\n    const mockChecks: SafetyCheck[] = [\n      {\n        id: \"check-1\",\n        code: \"malicious_instructions\",\n        message: \"Potentially harmful action detected\",\n      },\n    ];\n\n    it(\"returns checks when handler acknowledges\", async () => {\n      const client = createOpenAIClient();\n      const handler: SafetyConfirmationHandler = vi.fn(async () => ({\n        acknowledged: true,\n      }));\n      client.setSafetyConfirmationHandler(handler);\n      const logger = vi.fn<LoggerMock>();\n      const result = await openAISafetyInvoker.call(client, mockChecks, logger);\n\n      expect(handler).toHaveBeenCalledWith(mockChecks);\n      expect(result).toEqual(mockChecks);\n    });\n\n    it(\"returns undefined when handler rejects\", async () => {\n      const client = createOpenAIClient();\n      const handler: SafetyConfirmationHandler = vi.fn(async () => ({\n        acknowledged: false,\n      }));\n      client.setSafetyConfirmationHandler(handler);\n      const logger = vi.fn<LoggerMock>();\n      const result = await openAISafetyInvoker.call(client, mockChecks, logger);\n\n      expect(handler).toHaveBeenCalledWith(mockChecks);\n      expect(result).toBeUndefined();\n    });\n\n    it(\"auto-acknowledges when no handler is set\", async () => {\n      const client = createOpenAIClient();\n      const logger = vi.fn<LoggerMock>();\n      const result = await openAISafetyInvoker.call(client, mockChecks, logger);\n      expect(result).toEqual(mockChecks);\n    });\n  });\n\n  describe(\"Google-style (safety_decision)\", () => {\n    const mockDecision = {\n      decision: \"require_confirmation\",\n      explanation: \"Cookie consent dialog detected\",\n    };\n\n    it(\"returns 'true' when handler acknowledges\", async () => {\n      const client = createGoogleClient();\n      const handler: SafetyConfirmationHandler = vi.fn(async () => ({\n        acknowledged: true,\n      }));\n      client.setSafetyConfirmationHandler(handler);\n      const logger = vi.fn<LoggerMock>();\n      const result = await googleSafetyInvoker.call(\n        client,\n        mockDecision,\n        logger,\n      );\n\n      expect(handler).toHaveBeenCalledWith([\n        {\n          id: \"google-safety-decision\",\n          code: \"safety_decision\",\n          message: JSON.stringify(mockDecision, null, 2),\n        },\n      ]);\n      expect(result).toBe(\"true\");\n    });\n\n    it(\"returns undefined when handler rejects\", async () => {\n      const client = createGoogleClient();\n      const handler: SafetyConfirmationHandler = vi.fn(async () => ({\n        acknowledged: false,\n      }));\n      client.setSafetyConfirmationHandler(handler);\n      const logger = vi.fn<LoggerMock>();\n      const result = await googleSafetyInvoker.call(\n        client,\n        mockDecision,\n        logger,\n      );\n\n      expect(handler).toHaveBeenCalled();\n      expect(result).toBeUndefined();\n    });\n\n    it(\"auto-acknowledges when no handler is set\", async () => {\n      const client = createGoogleClient();\n      const logger = vi.fn<LoggerMock>();\n      const result = await googleSafetyInvoker.call(\n        client,\n        mockDecision,\n        logger,\n      );\n      expect(result).toBe(\"true\");\n    });\n\n    it(\"handles string safety decisions\", async () => {\n      const client = createGoogleClient();\n      const handler: SafetyConfirmationHandler = vi.fn(async () => ({\n        acknowledged: true,\n      }));\n      client.setSafetyConfirmationHandler(handler);\n      const logger = vi.fn<LoggerMock>();\n      const result = await googleSafetyInvoker.call(\n        client,\n        \"Simple string decision\",\n        logger,\n      );\n\n      expect(handler).toHaveBeenCalledWith([\n        {\n          id: \"google-safety-decision\",\n          code: \"safety_decision\",\n          message: \"Simple string decision\",\n        },\n      ]);\n      expect(result).toBe(\"true\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-a11y-resolvers.test.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { a11yForFrame } from \"../../lib/v3/understudy/a11y/snapshot/a11yTree.js\";\nimport type { AccessibilityTreeResult } from \"../../lib/v3/types/private/index.js\";\nimport * as focusSelectors from \"../../lib/v3/understudy/a11y/snapshot/focusSelectors.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\nimport { executionContexts } from \"../../lib/v3/understudy/executionContextRegistry.js\";\nimport { tryScopedSnapshot } from \"../../lib/v3/understudy/a11y/snapshot/capture.js\";\nimport type {\n  FrameContext,\n  A11yOptions,\n} from \"../../lib/v3/types/private/index.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\nimport * as domTree from \"../../lib/v3/understudy/a11y/snapshot/domTree.js\";\nimport * as a11yTree from \"../../lib/v3/understudy/a11y/snapshot/a11yTree.js\";\nimport * as logger from \"../../lib/v3/logger.js\";\n\nconst stringType = \"string\" as Protocol.Accessibility.AXValueType;\n\nconst baseAxNodes = (): Protocol.Accessibility.AXNode[] => [\n  {\n    nodeId: \"1\",\n    role: { type: stringType, value: \"RootWebArea\" },\n    backendDOMNodeId: 100,\n    childIds: [\"2\"],\n    ignored: false,\n  },\n  {\n    nodeId: \"2\",\n    role: { type: stringType, value: \"link\" },\n    name: { type: stringType, value: \"Docs\" },\n    backendDOMNodeId: 101,\n    parentId: \"1\",\n    childIds: [],\n    properties: [\n      {\n        name: \"url\",\n        value: { type: stringType, value: \"https://example.com\" },\n      },\n    ],\n    ignored: false,\n  },\n];\n\nconst baseHandlers = {\n  \"Accessibility.enable\": async () => ({}),\n  \"Runtime.enable\": async () => ({}),\n  \"DOM.enable\": async () => ({}),\n};\n\ndescribe(\"a11yForFrame\", () => {\n  beforeEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"returns full outline and url map when no focus selector is provided\", async () => {\n    const session = new MockCDPSession({\n      ...baseHandlers,\n      \"Accessibility.getFullAXTree\": async () => ({ nodes: baseAxNodes() }),\n    });\n\n    const opts: A11yOptions = {\n      focusSelector: undefined,\n      experimental: false,\n      tagNameMap: { \"enc-100\": \"#document\", \"enc-101\": \"a\" },\n      scrollableMap: {},\n      encode: (backend) => `enc-${backend}`,\n    };\n\n    const result = await a11yForFrame(session, undefined, opts);\n\n    expect(result.scopeApplied).toBe(false);\n    expect(result.urlMap[\"enc-101\"]).toBe(\"https://example.com\");\n    expect(result.outline).toContain(\"Docs\");\n  });\n\n  it(\"scopes the tree to the resolved focus selector target\", async () => {\n    const nodes = baseAxNodes().map((n) =>\n      n.nodeId === \"2\"\n        ? {\n            ...n,\n            childIds: [\"3\"],\n          }\n        : n,\n    );\n    nodes.push({\n      nodeId: \"3\",\n      parentId: \"2\",\n      childIds: [],\n      role: { type: stringType, value: \"StaticText\" },\n      backendDOMNodeId: 102,\n      ignored: false,\n    });\n\n    let scopedOnce = false;\n    const session = new MockCDPSession({\n      ...baseHandlers,\n      \"Accessibility.getFullAXTree\": async (params) => {\n        if (params?.frameId && !scopedOnce) {\n          scopedOnce = true;\n          throw new Error(\"does not belong to the target\");\n        }\n        return { nodes };\n      },\n      \"DOM.describeNode\": async () => ({\n        node: { backendNodeId: 101 },\n      }),\n    });\n\n    const resolveSpy = vi\n      .spyOn(focusSelectors, \"resolveObjectIdForXPath\")\n      .mockResolvedValue(\"object-1\");\n\n    const opts: A11yOptions = {\n      focusSelector: \"xpath=//a\",\n      experimental: false,\n      tagNameMap: { \"enc-101\": \"a\" },\n      scrollableMap: {},\n      encode: (backend) => `enc-${backend}`,\n    };\n\n    const result = await a11yForFrame(session, \"frame-1\", opts);\n\n    expect(result.scopeApplied).toBe(true);\n    expect(result.outline).not.toContain(\"RootWebArea\");\n    expect(resolveSpy).toHaveBeenCalled();\n    resolveSpy.mockRestore();\n  });\n\n  it(\"falls back to full tree when resolveObjectId throws\", async () => {\n    const session = new MockCDPSession({\n      ...baseHandlers,\n      \"Accessibility.getFullAXTree\": async () => ({ nodes: baseAxNodes() }),\n    });\n    vi.spyOn(focusSelectors, \"resolveObjectIdForCss\").mockRejectedValue(\n      new Error(\"fail\"),\n    );\n    const opts: A11yOptions = {\n      focusSelector: \".btn\",\n      experimental: false,\n      tagNameMap: {},\n      scrollableMap: {},\n      encode: (backend) => `enc-${backend}`,\n    };\n\n    const result = await a11yForFrame(session, \"frame-1\", opts);\n    expect(result.scopeApplied).toBe(false);\n  });\n});\n\ndescribe(\"resolveObjectIdForXPath\", () => {\n  beforeEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"evaluates in the target frame's main world when available\", async () => {\n    vi.spyOn(executionContexts, \"waitForMainWorld\").mockResolvedValue(42);\n    vi.spyOn(executionContexts, \"getMainWorld\").mockReturnValue(undefined);\n    const session = new MockCDPSession({\n      \"Runtime.evaluate\": async (params) => {\n        expect(params?.contextId).toBe(42);\n        return { result: { objectId: \"node-obj\" } };\n      },\n    });\n\n    const objectId = await focusSelectors.resolveObjectIdForXPath(\n      session,\n      \"//div\",\n      \"frame-1\",\n    );\n    expect(objectId).toBe(\"node-obj\");\n  });\n\n  it(\"returns null when evaluation throws or reports exception details\", async () => {\n    vi.spyOn(executionContexts, \"waitForMainWorld\").mockRejectedValue(\n      new Error(\"missing\"),\n    );\n    vi.spyOn(executionContexts, \"getMainWorld\").mockReturnValue(undefined);\n    const session = new MockCDPSession({\n      \"Runtime.evaluate\": async () => ({\n        result: {},\n        exceptionDetails: { exception: { description: \"bad\" } },\n      }),\n    });\n\n    const objectId = await focusSelectors.resolveObjectIdForXPath(\n      session,\n      \"//div\",\n      \"frame-2\",\n    );\n    expect(objectId).toBeNull();\n  });\n});\n\ndescribe(\"resolveObjectIdForCss\", () => {\n  beforeEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"returns primary evaluation result when available\", async () => {\n    vi.spyOn(executionContexts, \"waitForMainWorld\").mockResolvedValue(7);\n    const session = new MockCDPSession({\n      \"Runtime.evaluate\": async () => ({\n        result: { objectId: \"primary-obj\" },\n      }),\n    });\n    const objectId = await focusSelectors.resolveObjectIdForCss(\n      session,\n      \".btn\",\n      \"frame-1\",\n    );\n    expect(objectId).toBe(\"primary-obj\");\n  });\n\n  it(\"falls back to the pierce selector when the primary lookup fails\", async () => {\n    let call = 0;\n    const session = new MockCDPSession({\n      \"Runtime.evaluate\": async (params) => {\n        call++;\n        if (call === 1) {\n          expect(String(params?.expression)).toContain(\"resolveCssSelector\");\n          return { result: {} };\n        }\n        expect(String(params?.expression)).toContain(\n          \"resolveCssSelectorPierce\",\n        );\n        return { result: { objectId: \"css-obj\" } };\n      },\n    });\n\n    const objectId = await focusSelectors.resolveObjectIdForCss(\n      session,\n      \".btn\",\n      undefined,\n    );\n    expect(objectId).toBe(\"css-obj\");\n  });\n\n  it(\"returns null when both primary and fallback evaluations throw\", async () => {\n    vi.spyOn(executionContexts, \"waitForMainWorld\").mockResolvedValue(11);\n    vi.spyOn(executionContexts, \"getMainWorld\").mockReturnValue(undefined);\n    const session = new MockCDPSession({\n      \"Runtime.evaluate\": async () => ({\n        result: {},\n        exceptionDetails: { exception: { description: \"fail\" } },\n      }),\n    });\n\n    const objectId = await focusSelectors.resolveObjectIdForCss(\n      session,\n      \".missing\",\n      \"frame-1\",\n    );\n    expect(objectId).toBeNull();\n  });\n});\n\ndescribe(\"tryScopedSnapshot\", () => {\n  const ordinal = (frameId: string) => (frameId === \"frame-1\" ? 0 : 1);\n  const context: FrameContext = {\n    rootId: \"frame-1\",\n    frames: [\"frame-1\", \"frame-2\"],\n    parentByFrame: new Map([\n      [\"frame-1\", null],\n      [\"frame-2\", \"frame-1\"],\n    ]),\n  };\n\n  const makePage = (session: MockCDPSession, overrides?: Partial<Page>): Page =>\n    ({\n      mainFrameId: () => \"frame-1\",\n      asProtocolFrameTree: () => ({\n        frame: { id: \"frame-1\" as Protocol.Page.FrameId },\n        childFrames: [{ frame: { id: \"frame-2\" as Protocol.Page.FrameId } }],\n      }),\n      listAllFrameIds: () => [\"frame-1\", \"frame-2\"],\n      getSessionForFrame: () => session,\n      getOrdinal: (fid: string) => ordinal(fid),\n      ...overrides,\n    }) as unknown as Page;\n\n  beforeEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it(\"returns scoped snapshot when focus selector resolves via CSS hops\", async () => {\n    const session = new MockCDPSession({});\n    const domMapsSpy = vi\n      .spyOn(domTree, \"domMapsForSession\")\n      .mockResolvedValue({\n        tagNameMap: { \"1-10\": \"div\" },\n        xpathMap: { \"1-10\": \"/div[1]\" },\n        scrollableMap: {},\n      });\n    const a11ySpy = vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"[1-10] div\",\n      urlMap: { \"1-10\": \"https://example.com\" },\n      scopeApplied: true,\n    } as AccessibilityTreeResult);\n    vi.spyOn(focusSelectors, \"resolveCssFocusFrameAndTail\").mockResolvedValue({\n      targetFrameId: \"frame-2\",\n      tailSelector: \".btn-inner\",\n      absPrefix: \"/html/body/iframe[1]\",\n    });\n\n    const result = await tryScopedSnapshot(\n      makePage(session),\n      { focusSelector: \".btn\" },\n      context,\n      true,\n    );\n\n    expect(result).not.toBeNull();\n    expect(result?.combinedXpathMap[\"1-10\"]).toBe(\n      \"/html/body/iframe[1]/div[1]\",\n    );\n    expect(domMapsSpy).toHaveBeenCalled();\n    expect(a11ySpy).toHaveBeenCalled();\n  });\n\n  it(\"returns null and logs fallback when scope is not applied\", async () => {\n    const session = new MockCDPSession({});\n    vi.spyOn(domTree, \"domMapsForSession\").mockResolvedValue({\n      tagNameMap: { \"1-10\": \"div\" },\n      xpathMap: { \"1-10\": \"/div[1]\" },\n      scrollableMap: {},\n    });\n    vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"ignored\",\n      urlMap: {},\n      scopeApplied: false,\n    } as AccessibilityTreeResult);\n    const loggerSpy = vi.spyOn(logger, \"v3Logger\").mockImplementation(() => {});\n\n    const result = await tryScopedSnapshot(\n      makePage(session),\n      { focusSelector: \".btn\" },\n      context,\n      false,\n    );\n\n    expect(result).toBeNull();\n    expect(loggerSpy).toHaveBeenCalled();\n  });\n\n  it(\"returns null immediately when no focus selector is provided\", async () => {\n    const result = await tryScopedSnapshot(\n      makePage(new MockCDPSession({})),\n      {},\n      context,\n      true,\n    );\n    expect(result).toBeNull();\n  });\n\n  it(\"supports XPath focus resolution branch\", async () => {\n    const session = new MockCDPSession({});\n    vi.spyOn(domTree, \"domMapsForSession\").mockResolvedValue({\n      tagNameMap: { \"1-10\": \"div\" },\n      xpathMap: { \"1-10\": \"/div[1]\" },\n      scrollableMap: {},\n    });\n    vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"[1-10] div\",\n      urlMap: {},\n      scopeApplied: true,\n    } as AccessibilityTreeResult);\n    vi.spyOn(focusSelectors, \"resolveFocusFrameAndTail\").mockResolvedValue({\n      targetFrameId: \"frame-1\",\n      tailXPath: \"//div[1]\",\n      absPrefix: \"\",\n    });\n\n    const result = await tryScopedSnapshot(\n      makePage(session),\n      { focusSelector: \"xpath=//div\" },\n      context,\n      true,\n    );\n\n    expect(result).not.toBeNull();\n    expect(result?.combinedXpathMap[\"1-10\"]).toBe(\"/div[1]\");\n  });\n\n  it(\"logs and returns null when resolver throws\", async () => {\n    const session = new MockCDPSession({});\n    vi.spyOn(focusSelectors, \"resolveCssFocusFrameAndTail\").mockRejectedValue(\n      new Error(\"bad selector\"),\n    );\n    const loggerSpy = vi.spyOn(logger, \"v3Logger\").mockImplementation(() => {});\n\n    const result = await tryScopedSnapshot(\n      makePage(session),\n      { focusSelector: \".bad\" },\n      context,\n      true,\n    );\n\n    expect(result).toBeNull();\n    expect(loggerSpy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-a11y-tree-utils.test.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { describe, expect, it } from \"vitest\";\nimport type {\n  A11yNode,\n  A11yOptions,\n} from \"../../lib/v3/types/private/snapshot.js\";\nimport {\n  buildHierarchicalTree,\n  decorateRoles,\n  extractUrlFromAXNode,\n  isStructural,\n  removeRedundantStaticTextChildren,\n} from \"../../lib/v3/understudy/a11y/snapshot/a11yTree.js\";\n\nconst axString = (value: string): Protocol.Accessibility.AXValue => ({\n  type: \"string\",\n  value,\n});\n\nconst defaultOpts: A11yOptions = {\n  focusSelector: undefined,\n  experimental: false,\n  tagNameMap: {},\n  scrollableMap: {},\n  encode: (backendNodeId: number) => `enc-${backendNodeId}`,\n};\n\nconst makeAxNode = (\n  overrides: Partial<Protocol.Accessibility.AXNode> = {},\n): Protocol.Accessibility.AXNode => ({\n  nodeId: overrides.nodeId ?? String(Math.random()),\n  backendDOMNodeId:\n    overrides.backendDOMNodeId ?? Math.floor(Math.random() * 1e6),\n  role: overrides.role ?? axString(\"generic\"),\n  childIds: overrides.childIds ?? [],\n  parentId: overrides.parentId,\n  properties: overrides.properties ?? [],\n  name: overrides.name,\n  description: overrides.description,\n  value: overrides.value,\n  ignored: overrides.ignored ?? false,\n});\n\ndescribe(\"decorateRoles\", () => {\n  it(\"marks scrollable DOM nodes with tag labels and encoded ids\", () => {\n    const opts: A11yOptions = {\n      ...defaultOpts,\n      tagNameMap: {\n        \"enc-1\": \"div\",\n        \"enc-2\": \"html\",\n        \"enc-3\": \"#document\",\n        \"enc-4\": \"#svg\",\n      },\n      scrollableMap: { \"enc-1\": true, \"enc-4\": true },\n    };\n    const nodes = [\n      makeAxNode({\n        backendDOMNodeId: 1,\n        role: { type: \"string\", value: \"region\" },\n      }),\n      makeAxNode({\n        backendDOMNodeId: 2,\n        role: { type: \"string\", value: \"generic\" },\n      }),\n      makeAxNode({\n        backendDOMNodeId: 3,\n        role: { type: \"string\", value: \"generic\" },\n      }),\n      makeAxNode({\n        backendDOMNodeId: 4,\n        role: { type: \"string\", value: \"generic\" },\n      }),\n    ];\n\n    const decorated = decorateRoles(nodes, opts);\n    expect(decorated).toMatchObject([\n      { encodedId: \"enc-1\", role: \"scrollable, div\" },\n      { encodedId: \"enc-2\", role: \"scrollable, html\" },\n      { encodedId: \"enc-3\", role: \"generic\" },\n      { encodedId: \"enc-4\", role: \"scrollable, svg\" },\n    ]);\n  });\n\n  it(\"falls back when encoding fails\", () => {\n    const opts: A11yOptions = {\n      ...defaultOpts,\n      encode: () => {\n        throw new Error(\"boom\");\n      },\n    };\n    const nodes = [makeAxNode({ backendDOMNodeId: 4 })];\n    const decorated = decorateRoles(nodes, opts);\n    expect(decorated[0]?.encodedId).toBeUndefined();\n  });\n});\n\ndescribe(\"buildHierarchicalTree\", () => {\n  const opts: A11yOptions = {\n    ...defaultOpts,\n    tagNameMap: { root: \"div\", child: \"span\" },\n  };\n\n  it(\"drops structural nodes without children or names\", async () => {\n    const nodes: A11yNode[] = [\n      {\n        role: \"generic\",\n        name: \"\",\n        nodeId: \"root\",\n        encodedId: \"root\",\n        parentId: undefined,\n        childIds: [\"child\"],\n      },\n      {\n        role: \"generic\",\n        name: \"\",\n        nodeId: \"child\",\n        encodedId: \"child\",\n        parentId: \"root\",\n        childIds: [],\n      },\n    ];\n\n    const { tree } = await buildHierarchicalTree(nodes, opts);\n    expect(tree).toEqual([]);\n  });\n\n  it(\"promotes select/combobox tag names for structural nodes\", async () => {\n    const nodes: A11yNode[] = [\n      {\n        role: \"combobox\",\n        name: \"Select\",\n        nodeId: \"root\",\n        encodedId: \"root\",\n        parentId: undefined,\n        childIds: [\"child\"],\n      },\n      {\n        role: \"StaticText\",\n        name: \"Option\",\n        nodeId: \"child\",\n        encodedId: \"child\",\n        parentId: \"root\",\n        childIds: [],\n      },\n    ];\n\n    const { tree } = await buildHierarchicalTree(nodes, {\n      ...opts,\n      tagNameMap: { root: \"select\" },\n    });\n    expect(tree[0]?.role).toBe(\"select\");\n  });\n\n  it(\"drops structural parents with a single cleaned child while keeping it in place\", async () => {\n    const nodes: A11yNode[] = [\n      {\n        role: \"generic\",\n        name: \"\",\n        nodeId: \"root\",\n        encodedId: \"root\",\n        parentId: undefined,\n        childIds: [\"child\"],\n      },\n      {\n        role: \"StaticText\",\n        name: \"Ok\",\n        nodeId: \"child\",\n        encodedId: \"child\",\n        parentId: \"root\",\n        childIds: [],\n      },\n    ];\n\n    const { tree } = await buildHierarchicalTree(nodes, opts);\n    expect(tree[0]?.role).toBe(\"StaticText\");\n  });\n\n  it(\"drops structural parents entirely when all descendants are pruned\", async () => {\n    const nodes: A11yNode[] = [\n      {\n        role: \"generic\",\n        name: \"\",\n        nodeId: \"root\",\n        encodedId: \"root\",\n        parentId: undefined,\n        childIds: [\"child\"],\n      },\n      {\n        role: \"generic\",\n        name: \"\",\n        nodeId: \"child\",\n        encodedId: \"child\",\n        parentId: \"root\",\n        childIds: [],\n      },\n    ];\n\n    const { tree } = await buildHierarchicalTree(nodes, opts);\n    expect(tree).toEqual([]);\n  });\n\n  it(\"renames structural nodes to their tag names when not combobox\", async () => {\n    const nodes: A11yNode[] = [\n      {\n        role: \"generic\",\n        name: \"Container\",\n        nodeId: \"root\",\n        encodedId: \"root\",\n        parentId: undefined,\n        childIds: [\"child-a\", \"child-b\"],\n      },\n      {\n        role: \"StaticText\",\n        name: \"A\",\n        nodeId: \"child-a\",\n        encodedId: \"child-a\",\n        parentId: \"root\",\n        childIds: [],\n      },\n      {\n        role: \"StaticText\",\n        name: \"B\",\n        nodeId: \"child-b\",\n        encodedId: \"child-b\",\n        parentId: \"root\",\n        childIds: [],\n      },\n    ];\n\n    const { tree } = await buildHierarchicalTree(nodes, {\n      ...opts,\n      tagNameMap: { root: \"section\" },\n    });\n    expect(tree[0]?.role).toBe(\"section\");\n  });\n\n  it(\"skips nodes with negative node ids early\", async () => {\n    const nodes: A11yNode[] = [\n      {\n        role: \"button\",\n        name: \"Hidden\",\n        nodeId: \"-1\",\n        encodedId: \"hidden\",\n        parentId: undefined,\n        childIds: [],\n      },\n    ];\n\n    const { tree } = await buildHierarchicalTree(nodes, opts);\n    expect(tree).toEqual([]);\n  });\n});\n\ndescribe(\"isStructural\", () => {\n  it(\"marks generic/none/InlineTextBox roles as structural\", () => {\n    expect(isStructural(\"generic\")).toBe(true);\n    expect(isStructural(\"none\")).toBe(true);\n    expect(isStructural(\"InlineTextBox\")).toBe(true);\n    expect(isStructural(\"button\")).toBe(false);\n  });\n});\n\ndescribe(\"removeRedundantStaticTextChildren\", () => {\n  it(\"removes static text children whose concatenated text equals the parent name\", () => {\n    const parent: A11yNode = {\n      role: \"button\",\n      name: \"HelloWorld\",\n      nodeId: \"root\",\n    };\n    const children: A11yNode[] = [\n      { role: \"StaticText\", name: \"Hello\", nodeId: \"c1\" },\n      { role: \"StaticText\", name: \"World\", nodeId: \"c2\" },\n      { role: \"button\", name: \"Child\", nodeId: \"c3\" },\n    ];\n    const pruned = removeRedundantStaticTextChildren(parent, children);\n    expect(pruned).toEqual([{ role: \"button\", name: \"Child\", nodeId: \"c3\" }]);\n  });\n\n  it(\"keeps static text when combined text differs\", () => {\n    const parent: A11yNode = {\n      role: \"button\",\n      name: \"Hello World\",\n      nodeId: \"root\",\n    };\n    const children: A11yNode[] = [\n      { role: \"StaticText\", name: \"Hello\", nodeId: \"c1\" },\n      { role: \"StaticText\", name: \"Mars\", nodeId: \"c2\" },\n    ];\n    expect(removeRedundantStaticTextChildren(parent, children)).toEqual(\n      children,\n    );\n  });\n  it(\"returns original children when parent name is empty\", () => {\n    const parent: A11yNode = {\n      role: \"button\",\n      nodeId: \"root\",\n    };\n    const children: A11yNode[] = [\n      { role: \"StaticText\", name: \"Hello\", nodeId: \"c1\" },\n      { role: \"StaticText\", name: \"World\", nodeId: \"c2\" },\n    ];\n    expect(removeRedundantStaticTextChildren(parent, children)).toEqual(\n      children,\n    );\n  });\n});\n\ndescribe(\"extractUrlFromAXNode\", () => {\n  it(\"returns trimmed URL string from node properties\", () => {\n    const node = makeAxNode({\n      properties: [\n        { name: \"busy\", value: axString(\"bar\") },\n        { name: \"url\", value: axString(\" https://example.com \") },\n      ],\n    });\n    expect(extractUrlFromAXNode(node)).toBe(\"https://example.com\");\n  });\n\n  it(\"returns undefined when url property missing or invalid\", () => {\n    expect(\n      extractUrlFromAXNode(makeAxNode({ properties: [] })),\n    ).toBeUndefined();\n    expect(\n      extractUrlFromAXNode(\n        makeAxNode({\n          properties: [{ name: \"url\", value: { type: \"number\", value: 123 } }],\n        }),\n      ),\n    ).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-capture-orchestration.test.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { CDPSessionLike } from \"../../lib/v3/understudy/cdp.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\nimport type {\n  FrameContext,\n  SessionDomIndex,\n} from \"../../lib/v3/types/private/index.js\";\nimport * as capture from \"../../lib/v3/understudy/a11y/snapshot/capture.js\";\nimport * as a11yTree from \"../../lib/v3/understudy/a11y/snapshot/a11yTree.js\";\nimport * as domTree from \"../../lib/v3/understudy/a11y/snapshot/domTree.js\";\nimport * as focusSelectors from \"../../lib/v3/understudy/a11y/snapshot/focusSelectors.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\n\nconst makeProtocolFrame = (id: string): Protocol.Page.Frame =>\n  ({\n    id,\n    loaderId: `${id}-loader`,\n    url: \"https://example.com\",\n    securityOrigin: \"https://example.com\",\n    mimeType: \"text/html\",\n  }) as unknown as Protocol.Page.Frame;\n\nconst makeFrameTree = (\n  id: string,\n  children: Protocol.Page.FrameTree[] = [],\n): Protocol.Page.FrameTree => ({\n  frame: makeProtocolFrame(id),\n  childFrames: children,\n});\n\ntype PageStub = Pick<\n  Page,\n  | \"mainFrameId\"\n  | \"asProtocolFrameTree\"\n  | \"listAllFrameIds\"\n  | \"getSessionForFrame\"\n  | \"getOrdinal\"\n>;\n\nconst makePage = (overrides: Partial<PageStub> = {}): Page => {\n  const defaultSession = new MockCDPSession({}, \"default-session\");\n  const base: PageStub = {\n    mainFrameId: () => \"frame-1\",\n    asProtocolFrameTree: () => makeFrameTree(\"frame-1\"),\n    listAllFrameIds: () => [\"frame-1\"],\n    getSessionForFrame: () => defaultSession,\n    getOrdinal: () => 0,\n  };\n  return { ...base, ...overrides } as unknown as Page;\n};\n\nconst makeSessionIndex = (): SessionDomIndex => ({\n  rootBackend: 100,\n  absByBe: new Map([\n    [100, \"/\"],\n    [101, \"/html[1]\"],\n    [102, \"/html[1]/body[1]\"],\n    [150, \"/html[1]/body[1]/iframe[1]\"],\n    [200, \"/html[1]/body[1]/iframe[1]\"],\n    [201, \"/html[1]/body[1]/iframe[1]/div[1]\"],\n  ]),\n  tagByBe: new Map([\n    [100, \"#document\"],\n    [101, \"html\"],\n    [102, \"body\"],\n    [150, \"iframe\"],\n    [200, \"#document\"],\n    [201, \"div\"],\n  ]),\n  scrollByBe: new Map([[201, true]]),\n  docRootOf: new Map([\n    [100, 100],\n    [101, 100],\n    [102, 100],\n    [150, 100],\n    [200, 200],\n    [201, 200],\n  ]),\n  contentDocRootByIframe: new Map([[150, 200]]),\n});\n\nbeforeEach(() => {\n  vi.restoreAllMocks();\n});\n\ndescribe(\"buildFrameContext\", () => {\n  it(\"indexes parent relationships from the frame tree\", () => {\n    const frameTree = makeFrameTree(\"frame-1\", [\n      makeFrameTree(\"frame-2\", [makeFrameTree(\"frame-3\")]),\n      makeFrameTree(\"frame-4\"),\n    ]);\n    const page = makePage({\n      asProtocolFrameTree: () => frameTree,\n      listAllFrameIds: () => [\"frame-1\", \"frame-2\", \"frame-3\", \"frame-4\"],\n    });\n\n    const context = capture.buildFrameContext(page);\n\n    expect(context.rootId).toBe(\"frame-1\");\n    expect(context.frames).toEqual([\n      \"frame-1\",\n      \"frame-2\",\n      \"frame-3\",\n      \"frame-4\",\n    ]);\n    expect(context.parentByFrame.get(\"frame-1\")).toBeNull();\n    expect(context.parentByFrame.get(\"frame-2\")).toBe(\"frame-1\");\n    expect(context.parentByFrame.get(\"frame-3\")).toBe(\"frame-2\");\n    expect(context.parentByFrame.get(\"frame-4\")).toBe(\"frame-1\");\n  });\n});\n\ndescribe(\"buildSessionIndexes\", () => {\n  it(\"deduplicates frames that share the same CDP session id\", async () => {\n    const session = new MockCDPSession({}, \"session-a\");\n    const page = makePage({\n      // Every frame lookup returns the same session instance, so buildSessionIndexes\n      // should call buildSessionDomIndex only once and reuse the result.\n      getSessionForFrame: () => session,\n    });\n    const idx = makeSessionIndex();\n    const spy = vi\n      .spyOn(domTree, \"buildSessionDomIndex\")\n      .mockResolvedValue(idx);\n\n    const result = await capture.buildSessionIndexes(\n      page,\n      [\"frame-1\", \"frame-2\"],\n      true,\n    );\n\n    expect(spy).toHaveBeenCalledTimes(1); // only one DOM.getDocument per session id\n    expect(spy).toHaveBeenCalledWith(session, true);\n    expect(result.get(\"session-a\")).toBe(idx);\n  });\n\n  it(\"builds indexes for sessions without ids using the 'root' key\", async () => {\n    const sessionWithoutId: CDPSessionLike = {\n      id: undefined,\n      async send<R = unknown>(\n        _method: string,\n        _params?: Record<string, unknown>,\n      ): Promise<R> {\n        void _method;\n        void _params;\n        return {} as R;\n      },\n      on() {},\n      off() {},\n      async close() {},\n    };\n    const sessionWithId = new MockCDPSession({}, \"child-session\");\n    const page = makePage({\n      getSessionForFrame: (frameId: string) =>\n        frameId === \"frame-1\" ? sessionWithoutId : sessionWithId,\n    });\n\n    const idxA = makeSessionIndex();\n    const idxB = makeSessionIndex();\n    const spy = vi\n      .spyOn(domTree, \"buildSessionDomIndex\")\n      .mockResolvedValueOnce(idxA)\n      .mockResolvedValueOnce(idxB);\n\n    const result = await capture.buildSessionIndexes(\n      page,\n      [\"frame-1\", \"frame-2\"],\n      false,\n    );\n\n    // Verifies the helper invokes buildSessionDomIndex once for each unique session,\n    // keying anonymous sessions as \"root\" so downstream lookups remain stable.\n    expect(spy).toHaveBeenNthCalledWith(1, sessionWithoutId, false);\n    expect(spy).toHaveBeenNthCalledWith(2, sessionWithId, false);\n    expect(result.get(\"root\")).toBe(idxA);\n    expect(result.get(\"child-session\")).toBe(idxB);\n  });\n});\n\ndescribe(\"collectPerFrameMaps\", () => {\n  it(\"builds per-frame xpath/tag maps and outlines from a shared session index\", async () => {\n    const session = new MockCDPSession(\n      {\n        \"DOM.getFrameOwner\": async () => ({ backendNodeId: 150 }),\n      },\n      \"session-a\",\n    );\n    const page = makePage({\n      getSessionForFrame: () => session,\n      getOrdinal: (frameId: string) => (frameId === \"frame-1\" ? 0 : 1),\n    });\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n    const sessionIndex = makeSessionIndex();\n    const sessionToIndex = new Map([[session.id, sessionIndex]]);\n\n    vi.spyOn(a11yTree, \"a11yForFrame\").mockImplementation(\n      async (_sess, frameId) => ({\n        outline: `outline-${frameId}`,\n        urlMap: { [`url-${frameId}`]: `https://${frameId}.test` },\n        scopeApplied: false,\n      }),\n    );\n\n    const result = await capture.collectPerFrameMaps(\n      page,\n      context,\n      sessionToIndex,\n      { experimental: true },\n      true,\n      context.frames,\n    );\n\n    expect(result.perFrameOutlines).toEqual([\n      { frameId: \"frame-1\", outline: \"outline-frame-1\" },\n      { frameId: \"frame-2\", outline: \"outline-frame-2\" },\n    ]);\n    const rootMaps = result.perFrameMaps.get(\"frame-1\");\n    expect(rootMaps?.xpathMap[\"0-100\"]).toBe(\"/\");\n    expect(rootMaps?.xpathMap[\"0-101\"]).toBe(\"/html[1]\");\n    expect(rootMaps?.xpathMap[\"0-102\"]).toBe(\"/html[1]/body[1]\");\n    const childMaps = result.perFrameMaps.get(\"frame-2\");\n    expect(childMaps?.xpathMap[\"1-200\"]).toBe(\"/\");\n    expect(childMaps?.xpathMap[\"1-201\"]).toBe(\"/div[1]\");\n    expect(childMaps?.scrollableMap[\"1-201\"]).toBe(true);\n    expect(childMaps?.urlMap).toEqual({\n      \"url-frame-2\": \"https://frame-2.test\",\n    });\n    expect(session.callsFor(\"DOM.getFrameOwner\")).toHaveLength(1);\n  });\n\n  it(\"builds a missing session index on demand and memoizes it\", async () => {\n    const session = new MockCDPSession({}, \"new-session\");\n    const page = makePage({\n      getSessionForFrame: () => session,\n      getOrdinal: () => 2,\n    });\n    const context: FrameContext = {\n      rootId: \"frame-9\",\n      frames: [\"frame-9\"],\n      parentByFrame: new Map([[\"frame-9\", null]]),\n    };\n    const idx = makeSessionIndex();\n    const buildSpy = vi\n      .spyOn(domTree, \"buildSessionDomIndex\")\n      .mockResolvedValue(idx);\n    vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"outline\",\n      urlMap: {},\n      scopeApplied: false,\n    });\n\n    const sessionToIndex = new Map<string, SessionDomIndex>();\n    const result = await capture.collectPerFrameMaps(\n      page,\n      context,\n      sessionToIndex,\n      undefined,\n      false,\n      context.frames,\n    );\n\n    expect(buildSpy).toHaveBeenCalledWith(session, false);\n    expect(sessionToIndex.get(\"new-session\")).toBe(idx);\n    expect(result.perFrameMaps.get(\"frame-9\")?.xpathMap[\"2-100\"]).toBe(\"/\");\n  });\n\n  it(\"skips frames that are not listed in the frameIds argument\", async () => {\n    const session = new MockCDPSession({}, \"session-a\");\n    const page = makePage({\n      getSessionForFrame: () => session,\n      getOrdinal: (frameId: string) => (frameId === \"frame-1\" ? 0 : 1),\n    });\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n    const sessionIndex = makeSessionIndex();\n    const sessionToIndex = new Map([[session.id, sessionIndex]]);\n\n    const a11ySpy = vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"outline\",\n      urlMap: {},\n      scopeApplied: false,\n    });\n\n    const result = await capture.collectPerFrameMaps(\n      page,\n      context,\n      sessionToIndex,\n      undefined,\n      true,\n      [\"frame-1\"],\n    );\n\n    expect(a11ySpy).toHaveBeenCalledTimes(1);\n    expect(result.perFrameMaps.has(\"frame-2\")).toBe(false);\n    expect(result.perFrameOutlines.map((o) => o.frameId)).toEqual([\"frame-1\"]);\n  });\n});\n\ndescribe(\"captureHybridSnapshot\", () => {\n  it(\"returns early when the scoped snapshot path succeeds\", async () => {\n    const session = new MockCDPSession({}, \"session-a\");\n    const page = makePage({\n      getSessionForFrame: () => session,\n    });\n    const options = { focusSelector: \"/html\" };\n\n    vi.spyOn(focusSelectors, \"resolveFocusFrameAndTail\").mockResolvedValue({\n      targetFrameId: \"frame-1\",\n      tailXPath: \"\",\n      absPrefix: \"\",\n    });\n    const domMapsSpy = vi\n      .spyOn(domTree, \"domMapsForSession\")\n      .mockResolvedValue({\n        tagNameMap: { \"0-100\": \"#document\" },\n        xpathMap: { \"0-100\": \"/\" },\n        scrollableMap: {},\n      });\n    const a11ySpy = vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"scoped outline\",\n      urlMap: { \"0-100\": \"https://frame-1.test\" },\n      scopeApplied: true,\n    });\n    const buildIndexSpy = vi\n      .spyOn(domTree, \"buildSessionDomIndex\")\n      .mockImplementation(() => {\n        throw new Error(\"should not build session index when scoped\");\n      });\n\n    const result = await capture.captureHybridSnapshot(page, options);\n\n    expect(result.combinedTree).toBe(\"scoped outline\");\n    expect(result.combinedUrlMap[\"0-100\"]).toBe(\"https://frame-1.test\");\n    expect(domMapsSpy).toHaveBeenCalled();\n    expect(a11ySpy).toHaveBeenCalled();\n    expect(buildIndexSpy).not.toHaveBeenCalled();\n  });\n\n  it(\"scoped snapshot still succeeds when iframe inclusion is disabled\", async () => {\n    const session = new MockCDPSession({}, \"session-a\");\n    const page = makePage({\n      getSessionForFrame: () => session,\n    });\n    const options = { focusSelector: \"/html\", includeIframes: false };\n\n    vi.spyOn(focusSelectors, \"resolveFocusFrameAndTail\").mockResolvedValue({\n      targetFrameId: \"frame-1\",\n      tailXPath: \"\",\n      absPrefix: \"\",\n    });\n    const domMapsSpy = vi\n      .spyOn(domTree, \"domMapsForSession\")\n      .mockResolvedValue({\n        tagNameMap: { \"0-100\": \"#document\" },\n        xpathMap: { \"0-100\": \"/\" },\n        scrollableMap: {},\n      });\n    const a11ySpy = vi.spyOn(a11yTree, \"a11yForFrame\").mockResolvedValue({\n      outline: \"scoped outline\",\n      urlMap: { \"0-100\": \"https://frame-1.test\" },\n      scopeApplied: true,\n    });\n    const buildIndexSpy = vi\n      .spyOn(domTree, \"buildSessionDomIndex\")\n      .mockImplementation(() => {\n        throw new Error(\"should not build session index when scoped\");\n      });\n\n    const result = await capture.captureHybridSnapshot(page, options);\n\n    expect(result.combinedTree).toBe(\"scoped outline\");\n    expect(result.combinedUrlMap[\"0-100\"]).toBe(\"https://frame-1.test\");\n    expect(domMapsSpy).toHaveBeenCalled();\n    expect(a11ySpy).toHaveBeenCalled();\n    expect(buildIndexSpy).not.toHaveBeenCalled();\n  });\n\n  it(\"collects per-frame data and merges it when no scoped snapshot is available\", async () => {\n    const session = new MockCDPSession(\n      {\n        \"DOM.getFrameOwner\": async () => ({ backendNodeId: 150 }),\n      },\n      \"session-a\",\n    );\n    const page = makePage({\n      asProtocolFrameTree: () =>\n        makeFrameTree(\"frame-1\", [makeFrameTree(\"frame-2\")]),\n      listAllFrameIds: () => [\"frame-1\", \"frame-2\"],\n      getSessionForFrame: () => session,\n      getOrdinal: (frameId: string) => (frameId === \"frame-1\" ? 0 : 1),\n    });\n\n    const idx = makeSessionIndex();\n    vi.spyOn(domTree, \"buildSessionDomIndex\").mockResolvedValue(idx);\n    vi.spyOn(a11yTree, \"a11yForFrame\").mockImplementation(\n      async (_sess, frameId) => ({\n        outline:\n          frameId === \"frame-1\"\n            ? \"[0-150] iframe host\"\n            : \"[1-200] child subtree\",\n        urlMap: { [`url-${frameId}`]: `https://${frameId}.test` },\n        scopeApplied: false,\n      }),\n    );\n\n    const snapshot = await capture.captureHybridSnapshot(page);\n\n    expect(snapshot.combinedTree).toContain(\"[1-200] child subtree\");\n    expect(snapshot.combinedXpathMap[\"0-100\"]).toBe(\"/\");\n    expect(snapshot.combinedXpathMap[\"1-201\"]).toBe(\n      \"/html[1]/body[1]/iframe[1]/div[1]\",\n    );\n    expect(snapshot.combinedUrlMap[\"url-frame-2\"]).toBe(\"https://frame-2.test\");\n    expect(snapshot.perFrame?.map((pf) => pf.frameId)).toEqual([\n      \"frame-1\",\n      \"frame-2\",\n    ]);\n  });\n\n  it(\"omits iframe frames when includeIframes is false\", async () => {\n    const session = new MockCDPSession(\n      {\n        \"DOM.getFrameOwner\": async () => ({ backendNodeId: 150 }),\n      },\n      \"session-a\",\n    );\n    const page = makePage({\n      asProtocolFrameTree: () =>\n        makeFrameTree(\"frame-1\", [makeFrameTree(\"frame-2\")]),\n      listAllFrameIds: () => [\"frame-1\", \"frame-2\"],\n      getSessionForFrame: () => session,\n      getOrdinal: (frameId: string) => (frameId === \"frame-1\" ? 0 : 1),\n    });\n\n    const idx = makeSessionIndex();\n    vi.spyOn(domTree, \"buildSessionDomIndex\").mockResolvedValue(idx);\n    const a11ySpy = vi\n      .spyOn(a11yTree, \"a11yForFrame\")\n      .mockImplementation(async (_sess, frameId) => ({\n        outline:\n          frameId === \"frame-1\"\n            ? \"[0-150] iframe host\"\n            : \"[1-200] child subtree\",\n        urlMap: { [`url-${frameId}`]: `https://${frameId}.test` },\n        scopeApplied: false,\n      }));\n\n    const snapshot = await capture.captureHybridSnapshot(page, {\n      includeIframes: false,\n    });\n\n    expect(a11ySpy).toHaveBeenCalledTimes(1);\n    expect(session.callsFor(\"DOM.getFrameOwner\")).toHaveLength(0);\n    expect(snapshot.perFrame?.map((pf) => pf.frameId)).toEqual([\"frame-1\"]);\n    expect(snapshot.combinedXpathMap[\"1-201\"]).toBeUndefined();\n    expect(snapshot.combinedTree).not.toContain(\"[1-200] child subtree\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-cbor.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport type { Protocol } from \"devtools-protocol\";\n\nimport { captureHybridSnapshot } from \"../../lib/v3/understudy/a11y/snapshot/index.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\nimport { StagehandDomProcessError } from \"../../lib/v3/types/public/sdkErrors.js\";\nimport { CDPSessionLike } from \"../../lib/v3/understudy/cdp.js\";\n\ntype Handler = (params?: Record<string, unknown>) => Promise<unknown> | unknown;\n\nfunction createFakePage(session: CDPSessionLike): Page {\n  const frameTree: Protocol.Page.FrameTree = {\n    frame: {\n      id: \"root\" as Protocol.Page.FrameId,\n      loaderId: \"root-loader\" as Protocol.Network.LoaderId,\n      url: \"http://fake\",\n      domainAndRegistry: \"fake\",\n      securityOrigin: \"http://fake\",\n      mimeType: \"text/html\",\n      secureContextType: \"Secure\",\n      crossOriginIsolatedContextType: \"NotIsolated\",\n      gatedAPIFeatures: [],\n    },\n    childFrames: [],\n  };\n\n  return {\n    mainFrameId: () => \"root\",\n    asProtocolFrameTree: () => frameTree,\n    listAllFrameIds: () => [\"root\"],\n    getSessionForFrame: () => session,\n    getOrdinal: () => 0,\n  } as unknown as Page;\n}\n\nfunction completeDomTree(): Protocol.DOM.Node {\n  return {\n    nodeId: 1,\n    backendNodeId: 1,\n    nodeType: 9,\n    nodeName: \"#document\",\n    childNodeCount: 1,\n    children: [\n      {\n        nodeId: 2,\n        backendNodeId: 2,\n        nodeType: 1,\n        nodeName: \"HTML\",\n        childNodeCount: 1,\n        children: [\n          {\n            nodeId: 3,\n            backendNodeId: 3,\n            nodeType: 1,\n            nodeName: \"BODY\",\n            childNodeCount: 1,\n            children: [\n              {\n                nodeId: 4,\n                backendNodeId: 4,\n                nodeType: 1,\n                nodeName: \"DIV\",\n                childNodeCount: 0,\n                children: [],\n              },\n            ],\n          },\n        ],\n      },\n    ],\n  } as Protocol.DOM.Node;\n}\n\nfunction truncatedDomTree(): Protocol.DOM.Node {\n  return {\n    nodeId: 1,\n    backendNodeId: 1,\n    nodeType: 9,\n    nodeName: \"#document\",\n    childNodeCount: 1,\n    children: [\n      {\n        nodeId: 2,\n        backendNodeId: 2,\n        nodeType: 1,\n        nodeName: \"HTML\",\n        childNodeCount: 1,\n        children: [],\n      },\n    ],\n  } as Protocol.DOM.Node;\n}\n\nfunction htmlWithChildren(): Protocol.DOM.Node {\n  return {\n    nodeId: 2,\n    backendNodeId: 2,\n    nodeType: 1,\n    nodeName: \"HTML\",\n    childNodeCount: 1,\n    children: [\n      {\n        nodeId: 3,\n        backendNodeId: 3,\n        nodeType: 1,\n        nodeName: \"BODY\",\n        childNodeCount: 1,\n        children: [\n          {\n            nodeId: 4,\n            backendNodeId: 4,\n            nodeType: 1,\n            nodeName: \"DIV\",\n            childNodeCount: 0,\n            children: [],\n          },\n        ],\n      },\n    ],\n  } as Protocol.DOM.Node;\n}\n\nfunction simpleAxNodes(): Protocol.Accessibility.AXNode[] {\n  const stringType: Protocol.Accessibility.AXValueType = \"string\";\n  return [\n    {\n      nodeId: \"1\",\n      role: { type: stringType, value: \"RootWebArea\" },\n      backendDOMNodeId: 2,\n      childIds: [\"2\"],\n      ignored: false,\n    },\n    {\n      nodeId: \"2\",\n      role: { type: stringType, value: \"generic\" },\n      name: { type: stringType, value: \"Content\" },\n      backendDOMNodeId: 4,\n      parentId: \"1\",\n      childIds: [] as string[],\n      ignored: false,\n    },\n  ];\n}\n\nconst baseHandlers: Record<string, Handler> = {\n  \"DOM.enable\": async () => ({}),\n  \"Runtime.enable\": async () => ({}),\n  \"Accessibility.enable\": async () => ({}),\n  \"Accessibility.getFullAXTree\": async () => ({ nodes: simpleAxNodes() }),\n};\n\nfunction makeCborError(): Error {\n  return new Error(\"CBOR: stack limit exceeded\");\n}\n\ndescribe(\"captureHybridSnapshot CBOR fallbacks\", () => {\n  it(\"retries DOM.getDocument with reduced depths before succeeding\", async () => {\n    let domCalls = 0;\n    const session = new MockCDPSession({\n      ...baseHandlers,\n      \"DOM.getDocument\": async (params) => {\n        domCalls += 1;\n        if (domCalls === 1) throw makeCborError();\n        expect(params?.depth).toBe(256);\n        return { root: completeDomTree() };\n      },\n    });\n\n    const page = createFakePage(session);\n    const snapshot = await captureHybridSnapshot(page);\n\n    expect(snapshot.combinedTree).toContain(\"html\");\n    const depths = session\n      .callsFor(\"DOM.getDocument\")\n      .map((c) => c.params?.depth);\n    expect(depths).toEqual([-1, 256]);\n  });\n\n  it(\"throws StagehandDomProcessError after all DOM.getDocument attempts fail\", async () => {\n    const session = new MockCDPSession({\n      ...baseHandlers,\n      \"DOM.getDocument\": async () => {\n        throw makeCborError();\n      },\n    });\n\n    const page = createFakePage(session);\n    await expect(captureHybridSnapshot(page)).rejects.toThrow(\n      StagehandDomProcessError,\n    );\n  });\n\n  it(\"hydrates truncated nodes by retrying DOM.describeNode depths\", async () => {\n    let domAttempts = 0;\n    let describeAttempts = 0;\n\n    const session = new MockCDPSession({\n      ...baseHandlers,\n      \"DOM.getDocument\": async (params) => {\n        domAttempts += 1;\n        if (domAttempts === 1) throw makeCborError();\n        expect(params?.depth).toBe(256);\n        return { root: truncatedDomTree() };\n      },\n      \"DOM.describeNode\": async (params) => {\n        describeAttempts += 1;\n        if (describeAttempts === 1) throw makeCborError();\n        expect(params?.depth).toBe(64);\n        return { node: htmlWithChildren() };\n      },\n    });\n\n    const page = createFakePage(session);\n    const snapshot = await captureHybridSnapshot(page);\n\n    const describeDepths = session\n      .callsFor(\"DOM.describeNode\")\n      .map((c) => c.params?.depth);\n    expect(describeDepths).toEqual([-1, 64]);\n    expect(snapshot.combinedXpathMap[\"0-4\"]).toBe(\"/html[1]/body[1]/div[1]\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-dom-session-builders.test.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n  buildSessionDomIndex,\n  domMapsForSession,\n  getDomTreeWithFallback,\n  hydrateDomTree,\n} from \"../../lib/v3/understudy/a11y/snapshot/domTree.js\";\nimport { StagehandDomProcessError } from \"../../lib/v3/types/public/sdkErrors.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\n\nlet nextNodeId = 1;\nconst makeDomNode = (\n  overrides: Partial<Protocol.DOM.Node> = {},\n): Protocol.DOM.Node => {\n  const nodeId = overrides.nodeId ?? nextNodeId++;\n  const backendNodeId = overrides.backendNodeId ?? nextNodeId++;\n  const nodeName = overrides.nodeName ?? \"DIV\";\n  const nodeType = overrides.nodeType ?? 1;\n  const children = overrides.children ?? [];\n  return {\n    nodeId,\n    backendNodeId,\n    nodeName,\n    nodeType,\n    localName: overrides.localName ?? nodeName.toLowerCase(),\n    nodeValue: overrides.nodeValue ?? \"\",\n    childNodeCount: overrides.childNodeCount ?? children.length,\n    children,\n    shadowRoots: overrides.shadowRoots,\n    contentDocument: overrides.contentDocument,\n    isScrollable: overrides.isScrollable,\n  };\n};\n\nconst buildSampleDomTree = () => {\n  const iframeChild = makeDomNode({ nodeName: \"P\" });\n  const iframeBody = makeDomNode({\n    nodeName: \"BODY\",\n    children: [iframeChild],\n    isScrollable: true,\n  });\n  const iframeHtml = makeDomNode({ nodeName: \"HTML\", children: [iframeBody] });\n  const iframeDoc = makeDomNode({\n    nodeName: \"#document\",\n    nodeType: 9,\n    children: [iframeHtml],\n  });\n  const iframeElement = makeDomNode({\n    nodeName: \"IFRAME\",\n    contentDocument: iframeDoc,\n  });\n  const scrollDiv = makeDomNode({\n    nodeName: \"DIV\",\n    isScrollable: true,\n  });\n  const body = makeDomNode({\n    nodeName: \"BODY\",\n    children: [scrollDiv, iframeElement],\n  });\n  const html = makeDomNode({ nodeName: \"HTML\", children: [body] });\n  const root = makeDomNode({\n    nodeName: \"#document\",\n    nodeType: 9,\n    children: [html],\n  });\n  return {\n    root,\n    html,\n    body,\n    scrollDiv,\n    iframeElement,\n    iframeDoc,\n    iframeHtml,\n    iframeBody,\n    iframeChild,\n  };\n};\n\ndescribe(\"hydrateDomTree\", () => {\n  it(\"expands truncated nodes by calling DOM.describeNode\", async () => {\n    const child = makeDomNode({ nodeName: \"DIV\" });\n    const root = makeDomNode({\n      nodeName: \"HTML\",\n      childNodeCount: 1,\n      children: [],\n    });\n\n    const session = new MockCDPSession({\n      \"DOM.describeNode\": async () => ({\n        node: {\n          ...root,\n          children: [child],\n          childNodeCount: 1,\n        },\n      }),\n    });\n\n    await hydrateDomTree(session, root, true);\n    expect(root.children).toEqual([child]);\n  });\n\n  it(\"retries describeNode when CBOR errors occur before succeeding\", async () => {\n    const child = makeDomNode({ nodeName: \"DIV\" });\n    const root = makeDomNode({\n      nodeName: \"HTML\",\n      childNodeCount: 1,\n      children: [],\n    });\n\n    let attempts = 0;\n    const session = new MockCDPSession({\n      \"DOM.describeNode\": async () => {\n        attempts++;\n        if (attempts === 1) throw new Error(\"CBOR: stack limit exceeded\");\n        return { node: { ...root, children: [child], childNodeCount: 1 } };\n      },\n    });\n\n    await hydrateDomTree(session, root, true);\n    expect(attempts).toBe(2);\n    expect(root.children).toEqual([child]);\n  });\n\n  it(\"throws StagehandDomProcessError after exhausting describeNode retries\", async () => {\n    const root = makeDomNode({\n      nodeName: \"HTML\",\n      childNodeCount: 1,\n      children: [],\n    });\n    const session = new MockCDPSession({\n      \"DOM.describeNode\": async () => {\n        throw new Error(\"CBOR: stack limit exceeded\");\n      },\n    });\n\n    await expect(hydrateDomTree(session, root, true)).rejects.toBeInstanceOf(\n      StagehandDomProcessError,\n    );\n  });\n});\n\ndescribe(\"getDomTreeWithFallback\", () => {\n  it(\"retries DOM.getDocument after CBOR errors and returns the hydrated root\", async () => {\n    const root = makeDomNode({\n      nodeName: \"#document\",\n      nodeType: 9,\n      children: [],\n    });\n    const depths: number[] = [];\n    const session = new MockCDPSession({\n      \"DOM.getDocument\": async (params) => {\n        const depth = (params?.depth ?? 0) as number;\n        depths.push(depth);\n        if (depth === -1) throw new Error(\"CBOR: stack limit exceeded\");\n        return { root };\n      },\n      \"DOM.describeNode\": async () => ({ node: root }),\n    });\n\n    const result = await getDomTreeWithFallback(session, true);\n    expect(result).toBe(root);\n    expect(depths).toEqual([-1, 256]);\n  });\n\n  it(\"propagates non-CBOR DOM.getDocument errors\", async () => {\n    const session = new MockCDPSession({\n      \"DOM.getDocument\": async () => {\n        throw new Error(\"network fail\");\n      },\n    });\n    await expect(getDomTreeWithFallback(session, false)).rejects.toThrow(\n      \"network fail\",\n    );\n  });\n\n  it(\"throws StagehandDomProcessError when all depth attempts hit CBOR limits\", async () => {\n    const session = new MockCDPSession({\n      \"DOM.getDocument\": async () => {\n        throw new Error(\"CBOR: stack limit exceeded\");\n      },\n    });\n    await expect(getDomTreeWithFallback(session, false)).rejects.toBeInstanceOf(\n      StagehandDomProcessError,\n    );\n  });\n});\n\ndescribe(\"buildSessionDomIndex\", () => {\n  it(\"collects absolute paths, scrollability, and content-document metadata\", async () => {\n    const tree = buildSampleDomTree();\n    const session = new MockCDPSession({\n      \"DOM.enable\": async () => ({}),\n      \"DOM.getDocument\": async () => ({ root: tree.root }),\n      \"DOM.describeNode\": async () => ({ node: tree.root }),\n    });\n\n    const index = await buildSessionDomIndex(session, true);\n\n    expect(index.rootBackend).toBe(tree.root.backendNodeId);\n    expect(index.absByBe.get(tree.body.backendNodeId)).toBe(\"/html[1]/body[1]\");\n    expect(index.absByBe.get(tree.scrollDiv.backendNodeId)).toBe(\n      \"/html[1]/body[1]/div[1]\",\n    );\n    expect(index.scrollByBe.get(tree.scrollDiv.backendNodeId)).toBe(true);\n    expect(index.docRootOf.get(tree.iframeHtml.backendNodeId)).toBe(\n      tree.iframeDoc.backendNodeId,\n    );\n    expect(\n      index.contentDocRootByIframe.get(tree.iframeElement.backendNodeId),\n    ).toBe(tree.iframeDoc.backendNodeId);\n  });\n});\n\ndescribe(\"domMapsForSession\", () => {\n  it(\"derives frame-relative xpath/tag/scrollable maps for a frame's document root\", async () => {\n    const tree = buildSampleDomTree();\n    const session = new MockCDPSession({\n      \"DOM.enable\": async () => ({}),\n      \"DOM.getDocument\": async () => ({ root: tree.root }),\n      \"DOM.getFrameOwner\": async () => ({\n        backendNodeId: tree.iframeElement.backendNodeId,\n      }),\n      \"DOM.describeNode\": async () => ({ node: tree.root }),\n    });\n\n    const encode = (frameId: string, backendNodeId: number) =>\n      `${frameId}-${backendNodeId}`;\n    const maps = await domMapsForSession(\n      session,\n      \"frame-A\",\n      true,\n      encode,\n      true,\n    );\n\n    const iframeDocKey = `frame-A-${tree.iframeDoc.backendNodeId}`;\n    const iframeBodyKey = `frame-A-${tree.iframeBody.backendNodeId}`;\n    const iframeChildKey = `frame-A-${tree.iframeChild.backendNodeId}`;\n\n    expect(maps.tagNameMap[iframeDocKey]).toBe(\"#document\");\n    expect(maps.xpathMap[iframeDocKey]).toBe(\"/\");\n    expect(maps.xpathMap[iframeBodyKey]).toBe(\"/html[1]/body[1]\");\n    expect(maps.xpathMap[iframeChildKey]).toBe(\"/html[1]/body[1]/p[1]\");\n    expect(maps.scrollableMap[iframeBodyKey]).toBe(true);\n    expect(Object.keys(maps.tagNameMap)).not.toContain(\n      `frame-A-${tree.html.backendNodeId}`,\n    );\n  });\n\n  it(\"falls back to the root document when frame owner lookup fails\", async () => {\n    const tree = buildSampleDomTree();\n    const session = new MockCDPSession({\n      \"DOM.enable\": async () => ({}),\n      \"DOM.getDocument\": async () => ({ root: tree.root }),\n      \"DOM.getFrameOwner\": async () => {\n        throw new Error(\"owner lookup failed\");\n      },\n      \"DOM.describeNode\": async () => ({ node: tree.root }),\n    });\n\n    const encode = (frameId: string, backendNodeId: number) =>\n      `${frameId}-${backendNodeId}`;\n    const maps = await domMapsForSession(\n      session,\n      \"frame-B\",\n      false,\n      encode,\n      true,\n    );\n\n    expect(maps.xpathMap[`frame-B-${tree.html.backendNodeId}`]).toBe(\n      \"/html[1]\",\n    );\n    expect(maps.xpathMap[`frame-B-${tree.scrollDiv.backendNodeId}`]).toBe(\n      \"/html[1]/body[1]/div[1]\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-dom-tree-utils.test.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n  collectDomTraversalTargets,\n  findNodeByBackendId,\n  mergeDomNodes,\n  shouldExpandNode,\n} from \"../../lib/v3/understudy/a11y/snapshot/domTree.js\";\n\nlet nextNodeId = 1;\nconst makeNode = (\n  overrides: Partial<Protocol.DOM.Node> = {},\n): Protocol.DOM.Node => {\n  const base: Protocol.DOM.Node = {\n    nodeId: nextNodeId++,\n    backendNodeId: nextNodeId++,\n    nodeType: 1,\n    nodeName: \"DIV\",\n    localName: \"div\",\n    nodeValue: \"\",\n    childNodeCount:\n      overrides.childNodeCount ??\n      (overrides.children ? overrides.children.length : 0),\n  };\n  return { ...base, ...overrides };\n};\n\ndescribe(\"shouldExpandNode\", () => {\n  it(\"returns true when declared children exceed realized children\", () => {\n    const node = makeNode({\n      childNodeCount: 2,\n      children: [makeNode()],\n    });\n    expect(shouldExpandNode(node)).toBe(true);\n  });\n\n  it(\"returns false when all declared children are realized\", () => {\n    const child = makeNode();\n    const node = makeNode({\n      childNodeCount: 1,\n      children: [child],\n    });\n    expect(shouldExpandNode(node)).toBe(false);\n  });\n});\n\ndescribe(\"mergeDomNodes\", () => {\n  it(\"overrides structural fields with expanded node data\", () => {\n    const originalChildren = [makeNode({ nodeName: \"SPAN\" })];\n    const target = makeNode({\n      childNodeCount: 1,\n      children: originalChildren,\n      shadowRoots: [makeNode({ nodeName: \"shadow-old\" })],\n      contentDocument: makeNode({ nodeName: \"doc-old\" }),\n    });\n    const source = makeNode({\n      childNodeCount: 3,\n      children: [makeNode({ nodeName: \"DIV\" })],\n      shadowRoots: [],\n      contentDocument: makeNode({ nodeName: \"doc-new\" }),\n    });\n\n    mergeDomNodes(target, source);\n\n    expect(target.childNodeCount).toBe(3);\n    expect(target.children).toEqual(source.children);\n    expect(target.shadowRoots).toEqual([]);\n    expect(target.contentDocument?.nodeName).toBe(\"doc-new\");\n  });\n\n  it(\"preserves original structures when source omits them\", () => {\n    const child = makeNode();\n    const target = makeNode({\n      childNodeCount: 1,\n      children: [child],\n    });\n    const source = makeNode({\n      childNodeCount: 5,\n    });\n\n    mergeDomNodes(target, source);\n\n    expect(target.childNodeCount).toBe(5);\n    expect(target.children).toEqual([child]);\n  });\n});\n\ndescribe(\"collectDomTraversalTargets\", () => {\n  it(\"returns children, shadow roots, and content document in order\", () => {\n    const childA = makeNode({ nodeName: \"CHILD-A\" });\n    const childB = makeNode({ nodeName: \"CHILD-B\" });\n    const shadow = makeNode({ nodeName: \"SHADOW\" });\n    const content = makeNode({ nodeName: \"CONTENT\" });\n\n    const node = makeNode({\n      children: [childA, childB],\n      shadowRoots: [shadow],\n      contentDocument: content,\n    });\n\n    const targets = collectDomTraversalTargets(node);\n    expect(targets).toEqual([childA, childB, shadow, content]);\n  });\n});\n\ndescribe(\"findNodeByBackendId\", () => {\n  it(\"finds nodes nested within children and shadow roots\", () => {\n    const target = makeNode({ backendNodeId: 999, nodeName: \"TARGET\" });\n    const root = makeNode({\n      children: [\n        makeNode({\n          children: [makeNode(), target],\n        }),\n      ],\n      shadowRoots: [makeNode()],\n    });\n\n    expect(findNodeByBackendId(root, 999)).toBe(target);\n  });\n\n  it(\"returns undefined when no node matches the backend id\", () => {\n    const root = makeNode({\n      children: [makeNode()],\n      shadowRoots: [makeNode()],\n    });\n    expect(findNodeByBackendId(root, 123456)).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-focus-selectors-utils.test.ts",
    "content": "import type { Step } from \"../../lib/v3/types/private/snapshot.js\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n  buildXPathFromSteps,\n  IFRAME_STEP_RE,\n  listChildrenOf,\n  parseXPathToSteps,\n} from \"../../lib/v3/understudy/a11y/snapshot/focusSelectors.js\";\n\ndescribe(\"parseXPathToSteps\", () => {\n  it(\"records axis direction and normalized names\", () => {\n    const steps = parseXPathToSteps(\" //iframe[1]/div[2]//SPAN \");\n    expect(steps).toEqual([\n      { axis: \"desc\", raw: \"iframe[1]\", name: \"iframe\" },\n      { axis: \"child\", raw: \"div[2]\", name: \"div\" },\n      { axis: \"desc\", raw: \"SPAN\", name: \"span\" },\n    ]);\n  });\n\n  it(\"drops empty segments and returns [] for blank input\", () => {\n    expect(parseXPathToSteps(\"   \")).toEqual([]);\n    expect(parseXPathToSteps(\"/ \")).toEqual([]);\n  });\n});\n\ndescribe(\"buildXPathFromSteps\", () => {\n  it(\"reconstructs descendant and child hops as a string\", () => {\n    const steps: ReadonlyArray<Step> = [\n      { axis: \"child\", raw: \"iframe[1]\", name: \"iframe\" },\n      { axis: \"desc\", raw: \"div[@id='main']\", name: \"div\" },\n      { axis: \"child\", raw: \"span\", name: \"span\" },\n    ];\n    expect(buildXPathFromSteps(steps)).toBe(\"/iframe[1]//div[@id='main']/span\");\n  });\n\n  it(\"returns '/' for empty sequences\", () => {\n    expect(buildXPathFromSteps([])).toBe(\"/\");\n  });\n});\n\ndescribe(\"IFRAME_STEP_RE — frame boundary detection\", () => {\n  it(\"matches both iframe and frame with optional index\", () => {\n    expect(IFRAME_STEP_RE.test(\"iframe\")).toBe(true);\n    expect(IFRAME_STEP_RE.test(\"iframe[1]\")).toBe(true);\n    expect(IFRAME_STEP_RE.test(\"frame\")).toBe(true);\n    expect(IFRAME_STEP_RE.test(\"frame[4]\")).toBe(true);\n  });\n\n  it(\"does NOT match frameset\", () => {\n    expect(IFRAME_STEP_RE.test(\"frameset\")).toBe(false);\n    expect(IFRAME_STEP_RE.test(\"frameset[1]\")).toBe(false);\n  });\n});\n\ndescribe(\"parseXPathToSteps — frameset XPaths\", () => {\n  it(\"parses a frameset page XPath with frame[N] steps\", () => {\n    const steps = parseXPathToSteps(\n      \"/html[1]/frameset[1]/frame[4]/html[1]/body[1]/table[1]\",\n    );\n    expect(steps).toEqual([\n      { axis: \"child\", raw: \"html[1]\", name: \"html\" },\n      { axis: \"child\", raw: \"frameset[1]\", name: \"frameset\" },\n      { axis: \"child\", raw: \"frame[4]\", name: \"frame\" },\n      { axis: \"child\", raw: \"html[1]\", name: \"html\" },\n      { axis: \"child\", raw: \"body[1]\", name: \"body\" },\n      { axis: \"child\", raw: \"table[1]\", name: \"table\" },\n    ]);\n    // frame[4] step should be detected as a frame boundary\n    const frameBoundaries = steps.filter((s) => IFRAME_STEP_RE.test(s.name));\n    expect(frameBoundaries).toHaveLength(1);\n    expect(frameBoundaries[0].raw).toBe(\"frame[4]\");\n  });\n\n  it(\"detects iframe boundaries in standard iframe XPaths\", () => {\n    const steps = parseXPathToSteps(\n      \"/html[1]/body[1]/div[2]/iframe[1]/html[1]/body[1]/p[1]\",\n    );\n    const frameBoundaries = steps.filter((s) => IFRAME_STEP_RE.test(s.name));\n    expect(frameBoundaries).toHaveLength(1);\n    expect(frameBoundaries[0].raw).toBe(\"iframe[1]\");\n  });\n\n  it(\"does NOT detect frameset as a frame boundary\", () => {\n    const steps = parseXPathToSteps(\"/html[1]/frameset[1]/frame[2]\");\n    const frameBoundaries = steps.filter((s) => IFRAME_STEP_RE.test(s.name));\n    expect(frameBoundaries).toHaveLength(1);\n    // Only frame[2] matches, not frameset[1]\n    expect(frameBoundaries[0].raw).toBe(\"frame[2]\");\n  });\n});\n\ndescribe(\"listChildrenOf\", () => {\n  it(\"returns direct children whose parent matches the provided id\", () => {\n    const parentByFrame = new Map<string, string | null>([\n      [\"frame-1\", null],\n      [\"frame-2\", \"frame-1\"],\n      [\"frame-3\", \"frame-1\"],\n      [\"frame-4\", \"frame-2\"],\n    ]);\n    expect(listChildrenOf(parentByFrame, \"frame-1\")).toEqual([\n      \"frame-2\",\n      \"frame-3\",\n    ]);\n    expect(listChildrenOf(parentByFrame, \"frame-4\")).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-frame-merge.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport type {\n  FrameContext,\n  FrameDomMaps,\n} from \"../../lib/v3/types/private/index.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\nimport { MockCDPSession } from \"./helpers/mockCDPSession.js\";\nimport {\n  computeFramePrefixes,\n  mergeFramesIntoSnapshot,\n} from \"../../lib/v3/understudy/a11y/snapshot/capture.js\";\n\nconst makePage = (sessions: Record<string, MockCDPSession>): Page =>\n  ({\n    getSessionForFrame: (frameId: string) => sessions[frameId] ?? sessions.root,\n    getOrdinal: (frameId: string) =>\n      frameId === \"frame-1\" ? 0 : frameId === \"frame-2\" ? 1 : 2,\n  }) as unknown as Page;\n\ndescribe(\"computeFramePrefixes\", () => {\n  it(\"derives prefixes from parent iframe xpaths within the same session\", async () => {\n    const parentSession = new MockCDPSession({\n      \"DOM.getFrameOwner\": async () => ({ backendNodeId: 200 }),\n    });\n    const page = makePage({\n      \"frame-1\": parentSession,\n      \"frame-2\": parentSession,\n      root: parentSession,\n    });\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: { \"0-200\": \"/html[1]/body[1]/iframe[1]\" },\n        },\n      ],\n    ]);\n\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const { absPrefix, iframeHostEncByChild } = await computeFramePrefixes(\n      page,\n      context,\n      perFrameMaps,\n      context.frames,\n    );\n\n    expect(absPrefix.get(\"frame-1\")).toBe(\"\");\n    expect(absPrefix.get(\"frame-2\")).toBe(\"/html[1]/body[1]/iframe[1]\");\n    expect(iframeHostEncByChild.get(\"frame-2\")).toBe(\"0-200\");\n  });\n\n  it(\"inherits the parent prefix when frame owner lookups fail (OOPIF)\", async () => {\n    const parentSession = new MockCDPSession({\n      \"DOM.getFrameOwner\": async (params) => {\n        if (params?.frameId === \"frame-2\") return { backendNodeId: 200 };\n        if (params?.frameId === \"frame-3\") throw new Error(\"unavailable\");\n        return {};\n      },\n    });\n    const page = makePage({\n      \"frame-1\": parentSession,\n      \"frame-2\": parentSession,\n      \"frame-3\": parentSession,\n      root: parentSession,\n    });\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: { \"0-200\": \"/iframe[1]\" },\n        },\n      ],\n      [\n        \"frame-2\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: { \"1-300\": \"/div[1]/iframe[1]\" },\n        },\n      ],\n    ]);\n\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\", \"frame-3\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n        [\"frame-3\", \"frame-2\"],\n      ]),\n    };\n\n    const maps = await computeFramePrefixes(\n      page,\n      context,\n      perFrameMaps,\n      context.frames,\n    );\n\n    expect(maps.absPrefix.get(\"frame-2\")).toBe(\"/iframe[1]\");\n    expect(maps.absPrefix.get(\"frame-3\")).toBe(\"/iframe[1]\");\n  });\n\n  it(\"inherits parent prefix when iframe xpath mapping is missing\", async () => {\n    const session = new MockCDPSession({\n      \"DOM.getFrameOwner\": async () => ({ backendNodeId: 999 }),\n    });\n    const page = makePage({\n      \"frame-1\": session,\n      \"frame-2\": session,\n      root: session,\n    });\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: {},\n        },\n      ],\n    ]);\n\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const result = await computeFramePrefixes(\n      page,\n      context,\n      perFrameMaps as Map<string, FrameDomMaps>,\n      context.frames,\n    );\n    expect(result.absPrefix.get(\"frame-2\")).toBe(\"\");\n  });\n\n  it(\"does not compute prefixes for frames excluded from the scope\", async () => {\n    const session = new MockCDPSession({\n      \"DOM.getFrameOwner\": async () => ({ backendNodeId: 200 }),\n    });\n    const page = makePage({\n      \"frame-1\": session,\n      \"frame-2\": session,\n      root: session,\n    });\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: { \"0-200\": \"/iframe[1]\" },\n        },\n      ],\n    ]);\n\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const { absPrefix, iframeHostEncByChild } = await computeFramePrefixes(\n      page,\n      context,\n      perFrameMaps,\n      [\"frame-1\"],\n    );\n\n    expect(absPrefix.has(\"frame-2\")).toBe(false);\n    expect(iframeHostEncByChild.has(\"frame-2\")).toBe(false);\n  });\n});\n\ndescribe(\"mergeFramesIntoSnapshot\", () => {\n  it(\"merges root and child maps, prefixing child xpaths and injecting subtrees\", () => {\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: { \"0-10\": \"https://example.com\" },\n          xpathMap: { \"0-10\": \"/html[1]/body[1]\" },\n        },\n      ],\n      [\n        \"frame-2\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: { \"1-20\": \"https://child.com\" },\n          xpathMap: { \"1-20\": \"/div[1]/span[1]\" },\n        },\n      ],\n    ]);\n\n    const perFrameOutlines = [\n      { frameId: \"frame-1\", outline: \"[0-10] body\\n  [0-200] iframe\" },\n      { frameId: \"frame-2\", outline: \"[1-20] child\" },\n    ];\n\n    const absPrefix = new Map<string, string>([\n      [\"frame-1\", \"\"],\n      [\"frame-2\", \"/html[1]/body[1]/iframe[1]\"],\n    ]);\n    const iframeHostEncByChild = new Map<string, string>([\n      [\"frame-2\", \"0-200\"],\n    ]);\n\n    const snapshot = mergeFramesIntoSnapshot(\n      context,\n      perFrameMaps,\n      perFrameOutlines,\n      absPrefix,\n      iframeHostEncByChild,\n      context.frames,\n    );\n\n    expect(snapshot.combinedXpathMap[\"0-10\"]).toBe(\"/html[1]/body[1]\");\n    expect(snapshot.combinedXpathMap[\"1-20\"]).toBe(\n      \"/html[1]/body[1]/iframe[1]/div[1]/span[1]\",\n    );\n    expect(snapshot.combinedUrlMap[\"1-20\"]).toBe(\"https://child.com\");\n    expect(snapshot.combinedTree).toContain(\"[1-20] child\");\n  });\n\n  it(\"skips frames without maps and handles missing iframe mappings\", () => {\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: { \"0-10\": \"/html[1]\" },\n        },\n      ],\n    ]);\n\n    const perFrameOutlines = [\n      { frameId: \"frame-1\", outline: \"[0-10] html\" },\n      { frameId: \"frame-2\", outline: \"[1-20] orphan\" },\n    ];\n\n    const absPrefix = new Map<string, string>([\n      [\"frame-1\", \"\"],\n      [\"frame-2\", \"/missing\"],\n    ]);\n\n    const snapshot = mergeFramesIntoSnapshot(\n      context,\n      perFrameMaps,\n      perFrameOutlines,\n      absPrefix,\n      new Map(),\n      context.frames,\n    );\n\n    expect(snapshot.combinedXpathMap[\"1-20\"]).toBeUndefined();\n    expect(snapshot.combinedTree).toBe(\"[0-10] html\");\n  });\n\n  it(\"falls back to first outline when root frame outline is missing\", () => {\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-2\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: {},\n        },\n      ],\n    ]);\n\n    const perFrameOutlines = [\n      { frameId: \"frame-2\", outline: \"[child] frame2\" },\n    ];\n\n    const snapshot = mergeFramesIntoSnapshot(\n      context,\n      perFrameMaps,\n      perFrameOutlines,\n      new Map([[\"frame-2\", \"/iframe[1]\"]]),\n      new Map(),\n      context.frames,\n    );\n\n    expect(snapshot.combinedTree).toBe(\"[child] frame2\");\n  });\n\n  it(\"overwrites duplicate iframe host entries when multiple children map to the same parent\", () => {\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\", \"frame-3\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n        [\"frame-3\", \"frame-1\"],\n      ]),\n    };\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: {},\n          xpathMap: {},\n        },\n      ],\n    ]);\n\n    const perFrameOutlines = [\n      { frameId: \"frame-1\", outline: \"[root] frame1\\n  [0-200] iframe slot\" },\n      { frameId: \"frame-2\", outline: \"[child] frame2\" },\n      { frameId: \"frame-3\", outline: \"[child] frame3\" },\n    ];\n\n    const snapshot = mergeFramesIntoSnapshot(\n      context,\n      perFrameMaps,\n      perFrameOutlines,\n      new Map([\n        [\"frame-1\", \"\"],\n        [\"frame-2\", \"\"],\n        [\"frame-3\", \"\"],\n      ]),\n      new Map([\n        [\"frame-2\", \"0-200\"],\n        [\"frame-3\", \"0-200\"],\n      ]),\n      context.frames,\n    );\n\n    expect(snapshot.combinedTree).toContain(\"[child] frame3\");\n    expect(snapshot.combinedTree).not.toContain(\"[child] frame2\");\n  });\n\n  it(\"only merges xpath and url maps for frames included in frameIds\", () => {\n    const context: FrameContext = {\n      rootId: \"frame-1\",\n      frames: [\"frame-1\", \"frame-2\"],\n      parentByFrame: new Map([\n        [\"frame-1\", null],\n        [\"frame-2\", \"frame-1\"],\n      ]),\n    };\n\n    const perFrameMaps = new Map<string, FrameDomMaps>([\n      [\n        \"frame-1\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: { \"0-10\": \"https://root.test\" },\n          xpathMap: { \"0-10\": \"/html[1]\" },\n        },\n      ],\n      [\n        \"frame-2\",\n        {\n          tagNameMap: {},\n          scrollableMap: {},\n          urlMap: { \"1-20\": \"https://child.test\" },\n          xpathMap: { \"1-20\": \"/div[1]\" },\n        },\n      ],\n    ]);\n\n    const perFrameOutlines = [{ frameId: \"frame-1\", outline: \"[root] doc\" }];\n\n    const snapshot = mergeFramesIntoSnapshot(\n      context,\n      perFrameMaps,\n      perFrameOutlines,\n      new Map([[\"frame-1\", \"\"]]),\n      new Map(),\n      [\"frame-1\"],\n    );\n\n    expect(snapshot.combinedXpathMap[\"0-10\"]).toBe(\"/html[1]\");\n    expect(snapshot.combinedXpathMap[\"1-20\"]).toBeUndefined();\n    expect(snapshot.perFrame?.map((pf) => pf.frameId)).toEqual([\"frame-1\"]);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-tree-format-utils.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  cleanText,\n  diffCombinedTrees,\n  formatTreeLine,\n  indentBlock,\n  injectSubtrees,\n  normaliseSpaces,\n} from \"../../lib/v3/understudy/a11y/snapshot/treeFormatUtils.js\";\n\ndescribe(\"formatTreeLine\", () => {\n  it(\"includes encoded ids and indents children\", () => {\n    const outline = formatTreeLine({\n      role: \"section\",\n      name: \"Container\",\n      encodedId: \"frame-1\",\n      nodeId: \"ax-1\",\n      children: [\n        {\n          role: \"button\",\n          name: \"Submit\",\n          nodeId: \"ax-2\",\n        },\n      ],\n    });\n\n    expect(outline).toBe(\n      \"[frame-1] section: Container\\n  [ax-2] button: Submit\",\n    );\n  });\n});\n\ndescribe(\"injectSubtrees\", () => {\n  it(\"nests child outlines under iframe encoded ids\", () => {\n    const rootOutline = `[root] document\\n  [iframe-1] iframe\\n  [leaf] item`;\n    const iframeOutline = `[child-root] child\\n  [nested-frame] iframe`;\n    const nestedOutline = `[nested-leaf] nested`;\n\n    const merged = injectSubtrees(\n      rootOutline,\n      new Map([\n        [\"iframe-1\", iframeOutline],\n        [\"nested-frame\", nestedOutline],\n      ]),\n    );\n\n    expect(merged).toBe(\n      `[root] document\n  [iframe-1] iframe\n    [child-root] child\n      [nested-frame] iframe\n        [nested-leaf] nested\n  [leaf] item`,\n    );\n  });\n\n  it(\"injects child outline only once when the same id repeats\", () => {\n    const rootOutline = `[root] document\n  [iframe-1] iframe\n  [iframe-1] iframe`;\n    const iframeOutline = `[child-root] child`;\n\n    const merged = injectSubtrees(\n      rootOutline,\n      new Map([[\"iframe-1\", iframeOutline]]),\n    );\n\n    expect(merged).toBe(\n      `[root] document\n  [iframe-1] iframe\n    [child-root] child\n  [iframe-1] iframe`,\n    );\n  });\n\n  it(\"returns the original outline when no encoded ids are matched\", () => {\n    const outline = `[root] document\\n  [leaf] item`;\n    expect(injectSubtrees(outline, new Map([[\"other\", \"[x] child\"]]))).toBe(\n      outline,\n    );\n  });\n});\n\ndescribe(\"indentBlock\", () => {\n  it(\"prefixes each line with the provided indent\", () => {\n    expect(indentBlock(\"a\\nb\", \"  \")).toBe(\"  a\\n  b\");\n    expect(indentBlock(\"\", \"  \")).toBe(\"\");\n  });\n});\n\ndescribe(\"diffCombinedTrees\", () => {\n  it(\"returns newly-added lines relative to previous outline\", () => {\n    const prev = `[root] document\\n  [child] a`;\n    const next = `[root] document\\n  [child] a\\n  [child-2] b`;\n    expect(diffCombinedTrees(prev, next)).toBe(\"[child-2] b\");\n  });\n\n  it(\"normalizes indentation for added lines with stray spaces\", () => {\n    const prev = `[root] document\\n    [child] a`;\n    const next = `[root] document\\n    [child] a\\n        [child-2] b`;\n    expect(diffCombinedTrees(prev, next)).toBe(\"[child-2] b\");\n  });\n});\n\ndescribe(\"cleanText\", () => {\n  it(\"removes NBSP and private-use characters while collapsing spaces\", () => {\n    const dirty = `Hello\\u00A0\\u00A0world\\uE000 !`;\n    expect(cleanText(dirty)).toBe(\"Hello world !\");\n  });\n});\n\ndescribe(\"normaliseSpaces\", () => {\n  it(\"replaces whitespace runs with a single space\", () => {\n    expect(normaliseSpaces(\"a   b\\tc\\nd\")).toBe(\"a b c d\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/snapshot-xpath-utils.test.ts",
    "content": "import type { Protocol } from \"devtools-protocol\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n  buildChildXPathSegments,\n  joinXPath,\n  normalizeXPath,\n  prefixXPath,\n} from \"../../lib/v3/understudy/a11y/snapshot/xpathUtils.js\";\nimport { relativizeXPath } from \"../../lib/v3/understudy/a11y/snapshot/domTree.js\";\n\ndescribe(\"prefixXPath\", () => {\n  it(\"treats root prefixes as no-op\", () => {\n    expect(prefixXPath(\"/\", \"/div[1]\")).toBe(\"/div[1]\");\n    expect(prefixXPath(\"/\", \"//div[1]\")).toBe(\"//div[1]\");\n  });\n\n  it(\"handles descendant hops and blank children\", () => {\n    expect(prefixXPath(\"/html/body\", \"//slot[1]\")).toBe(\"/html/body//slot[1]\");\n    expect(prefixXPath(\"/html/body\", \"/\")).toBe(\"/html/body\");\n    expect(prefixXPath(\"/html/body/\", \"\")).toBe(\"/html/body\");\n  });\n});\n\ndescribe(\"normalizeXPath\", () => {\n  it(\"strips prefixes, trims whitespace, and enforces absolute roots\", () => {\n    expect(normalizeXPath(\"   xpath=/html/body/ \")).toBe(\"/html/body\");\n    expect(normalizeXPath(\"div/span\")).toBe(\"/div/span\");\n    expect(normalizeXPath(\"\")).toBe(\"\");\n    expect(normalizeXPath()).toBe(\"\");\n  });\n});\n\ndescribe(\"relativizeXPath\", () => {\n  it(\"returns '/' when paths match exactly\", () => {\n    expect(relativizeXPath(\"/html/body\", \"/html/body\")).toBe(\"/\");\n  });\n\n  it(\"omits duplicate prefixes and preserves descendant hops\", () => {\n    expect(relativizeXPath(\"/html/body\", \"/html/body/div[2]\")).toBe(\"/div[2]\");\n    expect(relativizeXPath(\"/html/body\", \"/html/body//shadow-root[1]\")).toBe(\n      \"//shadow-root[1]\",\n    );\n  });\n\n  it(\"falls back to absolute paths outside of the base document\", () => {\n    expect(relativizeXPath(\"/html/body\", \"/head\")).toBe(\"/head\");\n    expect(relativizeXPath(\"/\", \"/html/body\")).toBe(\"/html/body\");\n  });\n});\n\ndescribe(\"buildChildXPathSegments\", () => {\n  it(\"produces positional selectors for each node type\", () => {\n    const makeNode = (\n      nodeType: number,\n      nodeName: string,\n      override?: Partial<Protocol.DOM.Node>,\n    ): Protocol.DOM.Node => ({\n      nodeId: 1,\n      backendNodeId: 1,\n      localName: nodeName.toLowerCase(),\n      nodeValue: \"\",\n      ...override,\n      nodeType,\n      nodeName,\n    });\n\n    const nodes: Protocol.DOM.Node[] = [\n      makeNode(1, \"DIV\"),\n      makeNode(1, \"DIV\"),\n      makeNode(1, \"svg:path\"),\n      makeNode(3, \"#text\"),\n      makeNode(8, \"#comment\"),\n    ];\n\n    expect(buildChildXPathSegments(nodes)).toEqual([\n      \"div[1]\",\n      \"div[2]\",\n      \"*[name()='svg:path'][1]\",\n      \"text()[1]\",\n      \"comment()[1]\",\n    ]);\n  });\n});\n\ndescribe(\"joinXPath\", () => {\n  it(\"joins base and steps while preserving special hops\", () => {\n    expect(joinXPath(\"\", \"div[1]\")).toBe(\"/div[1]\");\n    expect(joinXPath(\"/\", \"span[1]\")).toBe(\"/span[1]\");\n    expect(joinXPath(\"/html/body\", \"//\")).toBe(\"/html/body//\");\n    expect(joinXPath(\"/html//\", \"slot[1]\")).toBe(\"/html//slot[1]\");\n    expect(joinXPath(\"/html/body\", \"\")).toBe(\"/html/body\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/timeout-handlers.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { ActHandler } from \"../../lib/v3/handlers/actHandler.js\";\nimport { ExtractHandler } from \"../../lib/v3/handlers/extractHandler.js\";\nimport { ObserveHandler } from \"../../lib/v3/handlers/observeHandler.js\";\nimport type { Page } from \"../../lib/v3/understudy/page.js\";\nimport type { ClientOptions } from \"../../lib/v3/types/public/model.js\";\nimport type { LLMClient } from \"../../lib/v3/llm/LLMClient.js\";\nimport { createTimeoutGuard } from \"../../lib/v3/handlers/handlerUtils/timeoutGuard.js\";\nimport { waitForDomNetworkQuiet } from \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\";\nimport { captureHybridSnapshot } from \"../../lib/v3/understudy/a11y/snapshot/index.js\";\nimport {\n  ActTimeoutError,\n  ExtractTimeoutError,\n  ObserveTimeoutError,\n} from \"../../lib/v3/types/public/sdkErrors.js\";\nimport {\n  act as actInference,\n  extract as extractInference,\n  observe as observeInference,\n} from \"../../lib/inference.js\";\nimport { V3FunctionName } from \"../../lib/v3/types/public/methods.js\";\n\nvi.mock(\"../../lib/v3/handlers/handlerUtils/timeoutGuard\", () => ({\n  createTimeoutGuard: vi.fn(),\n}));\n\nvi.mock(\"../../lib/v3/handlers/handlerUtils/actHandlerUtils\", () => ({\n  waitForDomNetworkQuiet: vi.fn(),\n  performUnderstudyMethod: vi.fn(),\n}));\n\nvi.mock(\"../../lib/v3/understudy/a11y/snapshot\", () => ({\n  captureHybridSnapshot: vi.fn(),\n  diffCombinedTrees: vi.fn(),\n}));\n\nvi.mock(\"../../lib/inference\", () => ({\n  act: vi.fn(),\n  extract: vi.fn(),\n  observe: vi.fn(),\n}));\n\ndescribe(\"ActHandler timeout guard\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"throws ActTimeoutError when timeout expires before snapshot\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    // Make createTimeoutGuard return a guard that throws on call #2\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        let calls = 0;\n        return vi.fn(() => {\n          calls += 1;\n          if (calls >= 2) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ActTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildActHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.act({\n        instruction: \"do something\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ActTimeoutError);\n\n    // Verify pre-timeout helper ran\n    expect(waitForDomNetworkQuietMock).toHaveBeenCalledTimes(1);\n    // Verify snapshot was NOT called (timeout fired before it)\n    expect(captureHybridSnapshotMock).not.toHaveBeenCalled();\n  });\n\n  it(\"throws ActTimeoutError when timeout expires before LLM call\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    const actInferenceMock = vi.mocked(actInference);\n\n    // Throw on call #3 (after snapshot but before LLM)\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        let calls = 0;\n        return vi.fn(() => {\n          calls += 1;\n          if (calls >= 3) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ActTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildActHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.act({\n        instruction: \"do something\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ActTimeoutError);\n\n    // Snapshot should have been called\n    expect(captureHybridSnapshotMock).toHaveBeenCalledTimes(1);\n    // LLM inference should NOT have been called\n    expect(actInferenceMock).not.toHaveBeenCalled();\n  });\n\n  it(\"throws ActTimeoutError with correct message format\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const timeoutMs = 100;\n\n    vi.mocked(createTimeoutGuard).mockImplementation((ms, errorFactory) => {\n      return vi.fn(() => {\n        throw errorFactory ? errorFactory(ms!) : new ActTimeoutError(ms!);\n      });\n    });\n\n    const handler = buildActHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    try {\n      await handler.act({\n        instruction: \"do something\",\n        page: fakePage,\n        timeout: timeoutMs,\n      });\n      throw new Error(\"Expected ActTimeoutError to be thrown\");\n    } catch (error) {\n      expect(error).toBeInstanceOf(ActTimeoutError);\n      expect((error as ActTimeoutError).message).toContain(\"act()\");\n      expect((error as ActTimeoutError).message).toContain(`${timeoutMs}ms`);\n      expect((error as ActTimeoutError).name).toBe(\"ActTimeoutError\");\n    }\n  });\n});\n\ndescribe(\"ActHandler two-step timeout\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"throws ActTimeoutError during step 2; step 2 action does not run\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const { performUnderstudyMethod } = await import(\n      \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\"\n    );\n    const performUnderstudyMethodMock = vi.mocked(performUnderstudyMethod);\n    performUnderstudyMethodMock.mockResolvedValue(undefined);\n\n    const actInferenceMock = vi.mocked(actInference);\n    // First call returns a two-step action\n    actInferenceMock.mockResolvedValueOnce({\n      element: {\n        elementId: \"1-0\",\n        description: \"click button\",\n        method: \"click\",\n        arguments: [],\n      },\n      twoStep: true,\n      prompt_tokens: 100,\n      completion_tokens: 50,\n      inference_time_ms: 500,\n    } as ReturnType<typeof actInference> extends Promise<infer T> ? T : never);\n\n    const diffCombinedTreesMock = vi.mocked(\n      (await import(\"../../lib/v3/understudy/a11y/snapshot/index.js\"))\n        .diffCombinedTrees,\n    );\n    diffCombinedTreesMock.mockReturnValue(\"diff tree\");\n\n    // Timeout fires after step 1 completes, during step 2 snapshot\n    // ensureTimeRemaining calls: 1=before wait, 2=after wait/before snap1, 3=before LLM1,\n    // 4=before action1, 5=inside takeDeterministicAction, 6=performUnderstudy,\n    // 7=before snap2 (this one should throw)\n    let callCount = 0;\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        return vi.fn(() => {\n          callCount += 1;\n          if (callCount >= 7) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ActTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildActHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.act({\n        instruction: \"click then type\",\n        page: fakePage,\n        timeout: 50,\n      }),\n    ).rejects.toThrow(ActTimeoutError);\n\n    // Step 1 action should have been executed\n    expect(performUnderstudyMethodMock).toHaveBeenCalledTimes(1);\n    // Step 2 LLM call should NOT have happened\n    expect(actInferenceMock).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe(\"ActHandler self-heal timeout\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"throws ActTimeoutError during self-heal snapshot; no retry action executes\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const { performUnderstudyMethod } = await import(\n      \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\"\n    );\n    const performUnderstudyMethodMock = vi.mocked(performUnderstudyMethod);\n    // First call fails, triggering self-heal\n    performUnderstudyMethodMock.mockRejectedValueOnce(\n      new Error(\"Element not found\"),\n    );\n\n    const actInferenceMock = vi.mocked(actInference);\n    actInferenceMock.mockResolvedValue({\n      element: {\n        elementId: \"1-0\",\n        description: \"click button\",\n        method: \"click\",\n        arguments: [],\n      },\n      twoStep: false,\n      prompt_tokens: 100,\n      completion_tokens: 50,\n      inference_time_ms: 500,\n    } as ReturnType<typeof actInference> extends Promise<infer T> ? T : never);\n\n    // Timeout during self-heal snapshot (call 7 or later)\n    let callCount = 0;\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        return vi.fn(() => {\n          callCount += 1;\n          // Timeout during self-heal snapshot call\n          if (callCount >= 7) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ActTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildActHandler({ selfHeal: true });\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.act({\n        instruction: \"click button\",\n        page: fakePage,\n        timeout: 50,\n      }),\n    ).rejects.toThrow(ActTimeoutError);\n\n    // First action attempt should have been tried\n    expect(performUnderstudyMethodMock).toHaveBeenCalledTimes(1);\n    // First LLM call should have happened\n    expect(actInferenceMock).toHaveBeenCalledTimes(1);\n    // Self-heal snapshot should have been started (call happened)\n    expect(captureHybridSnapshotMock).toHaveBeenCalled();\n  });\n\n  it(\"throws ActTimeoutError during self-heal LLM inference; no retry action executes\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const { performUnderstudyMethod } = await import(\n      \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\"\n    );\n    const performUnderstudyMethodMock = vi.mocked(performUnderstudyMethod);\n    // First call fails, triggering self-heal\n    performUnderstudyMethodMock.mockRejectedValueOnce(\n      new Error(\"Element not found\"),\n    );\n\n    const actInferenceMock = vi.mocked(actInference);\n    actInferenceMock.mockResolvedValueOnce({\n      element: {\n        elementId: \"1-0\",\n        description: \"click button\",\n        method: \"click\",\n        arguments: [],\n      },\n      twoStep: false,\n      prompt_tokens: 100,\n      completion_tokens: 50,\n      inference_time_ms: 500,\n    } as ReturnType<typeof actInference> extends Promise<infer T> ? T : never);\n\n    // Timeout during self-heal LLM inference (call 8)\n    let callCount = 0;\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        return vi.fn(() => {\n          callCount += 1;\n          // Timeout during self-heal LLM call\n          if (callCount >= 8) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ActTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildActHandler({ selfHeal: true });\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.act({\n        instruction: \"click button\",\n        page: fakePage,\n        timeout: 50,\n      }),\n    ).rejects.toThrow(ActTimeoutError);\n\n    // Self-heal snapshot was captured\n    expect(captureHybridSnapshotMock).toHaveBeenCalledTimes(2);\n    // Only one LLM inference (the retry inference was aborted by timeout)\n    expect(actInferenceMock).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe(\"ExtractHandler timeout guard\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"throws ExtractTimeoutError when timeout expires before snapshot\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    const extractInferenceMock = vi.mocked(extractInference);\n\n    // Throw immediately on first call\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        return vi.fn(() => {\n          throw errorFactory\n            ? errorFactory(timeoutMs!)\n            : new ExtractTimeoutError(timeoutMs!);\n        });\n      },\n    );\n\n    const handler = buildExtractHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.extract({\n        instruction: \"extract title\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ExtractTimeoutError);\n\n    // Snapshot should NOT have been called\n    expect(captureHybridSnapshotMock).not.toHaveBeenCalled();\n    // LLM inference should NOT have been called\n    expect(extractInferenceMock).not.toHaveBeenCalled();\n  });\n\n  it(\"throws ExtractTimeoutError when timeout expires before LLM call\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    const extractInferenceMock = vi.mocked(extractInference);\n\n    // Throw on call #2 (after snapshot but before LLM)\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        let calls = 0;\n        return vi.fn(() => {\n          calls += 1;\n          if (calls >= 2) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ExtractTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildExtractHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.extract({\n        instruction: \"extract title\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ExtractTimeoutError);\n\n    // Snapshot should have been called\n    expect(captureHybridSnapshotMock).toHaveBeenCalledTimes(1);\n    // LLM inference should NOT have been called\n    expect(extractInferenceMock).not.toHaveBeenCalled();\n  });\n\n  it(\"throws ExtractTimeoutError with correct message format\", async () => {\n    const timeoutMs = 200;\n\n    vi.mocked(createTimeoutGuard).mockImplementation((ms, errorFactory) => {\n      return vi.fn(() => {\n        throw errorFactory ? errorFactory(ms!) : new ExtractTimeoutError(ms!);\n      });\n    });\n\n    const handler = buildExtractHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    try {\n      await handler.extract({\n        instruction: \"extract title\",\n        page: fakePage,\n        timeout: timeoutMs,\n      });\n      throw new Error(\"Expected ExtractTimeoutError to be thrown\");\n    } catch (error) {\n      expect(error).toBeInstanceOf(ExtractTimeoutError);\n      expect((error as ExtractTimeoutError).message).toContain(\"extract()\");\n      expect((error as ExtractTimeoutError).message).toContain(\n        `${timeoutMs}ms`,\n      );\n      expect((error as ExtractTimeoutError).name).toBe(\"ExtractTimeoutError\");\n    }\n  });\n\n  it(\"stops LLM and post-processing when timeout expires\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: { \"1-0\": \"https://example.com\" },\n    });\n\n    const extractInferenceMock = vi.mocked(extractInference);\n\n    // Allow snapshot but timeout before LLM\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        let calls = 0;\n        return vi.fn(() => {\n          calls += 1;\n          if (calls >= 2) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ExtractTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildExtractHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.extract({\n        instruction: \"extract links\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ExtractTimeoutError);\n\n    // Post-processing (URL injection) never runs because LLM was never called\n    expect(extractInferenceMock).not.toHaveBeenCalled();\n  });\n});\n\ndescribe(\"ObserveHandler timeout guard\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"throws ObserveTimeoutError when timeout expires before snapshot\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    const observeInferenceMock = vi.mocked(observeInference);\n\n    // Throw immediately on first call\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        return vi.fn(() => {\n          throw errorFactory\n            ? errorFactory(timeoutMs!)\n            : new ObserveTimeoutError(timeoutMs!);\n        });\n      },\n    );\n\n    const handler = buildObserveHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.observe({\n        instruction: \"find buttons\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ObserveTimeoutError);\n\n    // Snapshot should NOT have been called\n    expect(captureHybridSnapshotMock).not.toHaveBeenCalled();\n    // LLM inference should NOT have been called\n    expect(observeInferenceMock).not.toHaveBeenCalled();\n  });\n\n  it(\"throws ObserveTimeoutError when timeout expires before LLM call\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    const observeInferenceMock = vi.mocked(observeInference);\n\n    // Throw on call #2 (after snapshot but before LLM)\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        let calls = 0;\n        return vi.fn(() => {\n          calls += 1;\n          if (calls >= 2) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ObserveTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildObserveHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.observe({\n        instruction: \"find buttons\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ObserveTimeoutError);\n\n    // Snapshot should have been called\n    expect(captureHybridSnapshotMock).toHaveBeenCalledTimes(1);\n    // LLM inference should NOT have been called\n    expect(observeInferenceMock).not.toHaveBeenCalled();\n  });\n\n  it(\"throws ObserveTimeoutError with correct message format\", async () => {\n    const timeoutMs = 150;\n\n    vi.mocked(createTimeoutGuard).mockImplementation((ms, errorFactory) => {\n      return vi.fn(() => {\n        throw errorFactory ? errorFactory(ms!) : new ObserveTimeoutError(ms!);\n      });\n    });\n\n    const handler = buildObserveHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    try {\n      await handler.observe({\n        instruction: \"find buttons\",\n        page: fakePage,\n        timeout: timeoutMs,\n      });\n      throw new Error(\"Expected ObserveTimeoutError to be thrown\");\n    } catch (error) {\n      expect(error).toBeInstanceOf(ObserveTimeoutError);\n      expect((error as ObserveTimeoutError).message).toContain(\"observe()\");\n      expect((error as ObserveTimeoutError).message).toContain(\n        `${timeoutMs}ms`,\n      );\n      expect((error as ObserveTimeoutError).name).toBe(\"ObserveTimeoutError\");\n    }\n  });\n\n  it(\"aborts result processing when timeout expires\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const observeInferenceMock = vi.mocked(observeInference);\n\n    // Timeout before LLM call\n    vi.mocked(createTimeoutGuard).mockImplementation(\n      (timeoutMs, errorFactory) => {\n        let calls = 0;\n        return vi.fn(() => {\n          calls += 1;\n          if (calls >= 2) {\n            throw errorFactory\n              ? errorFactory(timeoutMs!)\n              : new ObserveTimeoutError(timeoutMs!);\n          }\n        });\n      },\n    );\n\n    const handler = buildObserveHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    await expect(\n      handler.observe({\n        instruction: \"find all interactive elements\",\n        page: fakePage,\n        timeout: 5,\n      }),\n    ).rejects.toThrow(ObserveTimeoutError);\n\n    // Result mapping/processing never happens\n    expect(observeInferenceMock).not.toHaveBeenCalled();\n  });\n});\n\ndescribe(\"No-timeout success paths\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it(\"act() completes successfully without timeout and records metrics\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const { performUnderstudyMethod } = await import(\n      \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\"\n    );\n    const performUnderstudyMethodMock = vi.mocked(performUnderstudyMethod);\n    performUnderstudyMethodMock.mockResolvedValue(undefined);\n\n    const actInferenceMock = vi.mocked(actInference);\n    actInferenceMock.mockResolvedValue({\n      element: {\n        elementId: \"1-0\",\n        description: \"click button\",\n        method: \"click\",\n        arguments: [],\n      },\n      twoStep: false,\n      prompt_tokens: 100,\n      completion_tokens: 50,\n      reasoning_tokens: 10,\n      cached_input_tokens: 5,\n      inference_time_ms: 500,\n    } as ReturnType<typeof actInference> extends Promise<infer T> ? T : never);\n\n    // No timeout - guard never throws\n    vi.mocked(createTimeoutGuard).mockImplementation(() => {\n      return vi.fn(() => {\n        // No-op - never throws\n      });\n    });\n\n    const metricsCallback = vi.fn();\n    const handler = buildActHandler({ onMetrics: metricsCallback });\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    const result = await handler.act({\n      instruction: \"click button\",\n      page: fakePage,\n      // No timeout specified\n    });\n\n    expect(result.success).toBe(true);\n    expect(metricsCallback).toHaveBeenCalledWith(\n      V3FunctionName.ACT,\n      100,\n      50,\n      10,\n      5,\n      500,\n    );\n  });\n\n  it(\"extract() completes successfully without timeout and records metrics\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: {},\n      combinedUrlMap: {},\n    });\n\n    const extractInferenceMock = vi.mocked(extractInference);\n    extractInferenceMock.mockResolvedValue({\n      title: \"Test Title\",\n      metadata: { completed: true, progress: \"100%\" },\n      prompt_tokens: 200,\n      completion_tokens: 100,\n      reasoning_tokens: 20,\n      cached_input_tokens: 10,\n      inference_time_ms: 800,\n    } as ReturnType<typeof extractInference> extends Promise<infer T>\n      ? T\n      : never);\n\n    // No timeout - guard never throws\n    vi.mocked(createTimeoutGuard).mockImplementation(() => {\n      return vi.fn(() => {\n        // No-op - never throws\n      });\n    });\n\n    const metricsCallback = vi.fn();\n    const handler = buildExtractHandler({ onMetrics: metricsCallback });\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    const result = await handler.extract({\n      instruction: \"extract title\",\n      page: fakePage,\n      // No timeout specified\n    });\n\n    expect(result).toHaveProperty(\"title\", \"Test Title\");\n    expect(metricsCallback).toHaveBeenCalledWith(\n      V3FunctionName.EXTRACT,\n      200,\n      100,\n      20,\n      10,\n      800,\n    );\n  });\n\n  it(\"observe() completes successfully without timeout and records metrics\", async () => {\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const observeInferenceMock = vi.mocked(observeInference);\n    observeInferenceMock.mockResolvedValue({\n      elements: [\n        {\n          elementId: \"1-0\",\n          description: \"Submit button\",\n        },\n      ],\n      prompt_tokens: 150,\n      completion_tokens: 75,\n      reasoning_tokens: 15,\n      cached_input_tokens: 8,\n      inference_time_ms: 600,\n    } as ReturnType<typeof observeInference> extends Promise<infer T>\n      ? T\n      : never);\n\n    // No timeout - guard never throws\n    vi.mocked(createTimeoutGuard).mockImplementation(() => {\n      return vi.fn(() => {\n        // No-op - never throws\n      });\n    });\n\n    const metricsCallback = vi.fn();\n    const handler = buildObserveHandler({ onMetrics: metricsCallback });\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    const result = await handler.observe({\n      instruction: \"find buttons\",\n      page: fakePage,\n      // No timeout specified\n    });\n\n    expect(result).toHaveLength(1);\n    expect(result[0]).toHaveProperty(\"description\", \"Submit button\");\n    expect(metricsCallback).toHaveBeenCalledWith(\n      V3FunctionName.OBSERVE,\n      150,\n      75,\n      15,\n      8,\n      600,\n    );\n  });\n\n  it(\"act() with zero timeout behaves as no timeout\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const { performUnderstudyMethod } = await import(\n      \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\"\n    );\n    const performUnderstudyMethodMock = vi.mocked(performUnderstudyMethod);\n    performUnderstudyMethodMock.mockResolvedValue(undefined);\n\n    const actInferenceMock = vi.mocked(actInference);\n    actInferenceMock.mockResolvedValue({\n      element: {\n        elementId: \"1-0\",\n        description: \"click button\",\n        method: \"click\",\n        arguments: [],\n      },\n      twoStep: false,\n      prompt_tokens: 100,\n      completion_tokens: 50,\n      inference_time_ms: 500,\n    } as ReturnType<typeof actInference> extends Promise<infer T> ? T : never);\n\n    // When timeout is 0 or negative, createTimeoutGuard returns a no-op\n    vi.mocked(createTimeoutGuard).mockImplementation((timeoutMs) => {\n      if (!timeoutMs || timeoutMs <= 0) {\n        return vi.fn(() => {\n          // No-op\n        });\n      }\n      return vi.fn(() => {\n        throw new ActTimeoutError(timeoutMs);\n      });\n    });\n\n    const handler = buildActHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    const result = await handler.act({\n      instruction: \"click button\",\n      page: fakePage,\n      timeout: 0, // Zero timeout should be treated as \"no timeout\"\n    });\n\n    expect(result.success).toBe(true);\n  });\n\n  it(\"act() with negative timeout behaves as no timeout\", async () => {\n    const waitForDomNetworkQuietMock = vi.mocked(waitForDomNetworkQuiet);\n    waitForDomNetworkQuietMock.mockResolvedValue(undefined);\n\n    const captureHybridSnapshotMock = vi.mocked(captureHybridSnapshot);\n    captureHybridSnapshotMock.mockResolvedValue({\n      combinedTree: \"tree content\",\n      combinedXpathMap: { \"1-0\": \"/html/body/button\" },\n      combinedUrlMap: {},\n    });\n\n    const { performUnderstudyMethod } = await import(\n      \"../../lib/v3/handlers/handlerUtils/actHandlerUtils.js\"\n    );\n    const performUnderstudyMethodMock = vi.mocked(performUnderstudyMethod);\n    performUnderstudyMethodMock.mockResolvedValue(undefined);\n\n    const actInferenceMock = vi.mocked(actInference);\n    actInferenceMock.mockResolvedValue({\n      element: {\n        elementId: \"1-0\",\n        description: \"click button\",\n        method: \"click\",\n        arguments: [],\n      },\n      twoStep: false,\n      prompt_tokens: 100,\n      completion_tokens: 50,\n      inference_time_ms: 500,\n    } as ReturnType<typeof actInference> extends Promise<infer T> ? T : never);\n\n    vi.mocked(createTimeoutGuard).mockImplementation((timeoutMs) => {\n      if (!timeoutMs || timeoutMs <= 0) {\n        return vi.fn(() => {\n          // No-op\n        });\n      }\n      return vi.fn(() => {\n        throw new ActTimeoutError(timeoutMs);\n      });\n    });\n\n    const handler = buildActHandler();\n    const fakePage = {\n      mainFrame: vi.fn().mockReturnValue({}),\n    } as unknown as Page;\n\n    const result = await handler.act({\n      instruction: \"click button\",\n      page: fakePage,\n      timeout: -100, // Negative timeout should be treated as \"no timeout\"\n    });\n\n    expect(result.success).toBe(true);\n  });\n});\n\ninterface BuildActHandlerOptions {\n  selfHeal?: boolean;\n  onMetrics?: (\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ) => void;\n}\n\nfunction buildActHandler(options: BuildActHandlerOptions = {}): ActHandler {\n  const defaultClientOptions = {} as ClientOptions;\n  const fakeClient = {\n    type: \"openai\",\n    modelName: \"gpt-4o\",\n    clientOptions: defaultClientOptions,\n  } as LLMClient;\n  const resolveLlmClient = vi.fn().mockReturnValue(fakeClient);\n\n  return new ActHandler(\n    fakeClient,\n    \"gpt-4o\",\n    defaultClientOptions,\n    resolveLlmClient,\n    undefined,\n    false,\n    options.selfHeal ?? false,\n    options.onMetrics,\n    undefined,\n  );\n}\n\ninterface BuildExtractHandlerOptions {\n  onMetrics?: (\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ) => void;\n}\n\nfunction buildExtractHandler(\n  options: BuildExtractHandlerOptions = {},\n): ExtractHandler {\n  const defaultClientOptions = {} as ClientOptions;\n  const fakeClient = {\n    type: \"openai\",\n    modelName: \"gpt-4o\",\n    clientOptions: defaultClientOptions,\n  } as LLMClient;\n  const resolveLlmClient = vi.fn().mockReturnValue(fakeClient);\n\n  return new ExtractHandler(\n    fakeClient,\n    \"gpt-4o\",\n    defaultClientOptions,\n    resolveLlmClient,\n    undefined,\n    false,\n    false,\n    options.onMetrics,\n  );\n}\n\ninterface BuildObserveHandlerOptions {\n  onMetrics?: (\n    functionName: V3FunctionName,\n    promptTokens: number,\n    completionTokens: number,\n    reasoningTokens: number,\n    cachedInputTokens: number,\n    inferenceTimeMs: number,\n  ) => void;\n}\n\nfunction buildObserveHandler(\n  options: BuildObserveHandlerOptions = {},\n): ObserveHandler {\n  const defaultClientOptions = {} as ClientOptions;\n  const fakeClient = {\n    type: \"openai\",\n    modelName: \"gpt-4o\",\n    clientOptions: defaultClientOptions,\n  } as LLMClient;\n  const resolveLlmClient = vi.fn().mockReturnValue(fakeClient);\n\n  return new ObserveHandler(\n    fakeClient,\n    \"gpt-4o\",\n    defaultClientOptions,\n    resolveLlmClient,\n    undefined,\n    false,\n    false,\n    options.onMetrics,\n  );\n}\n"
  },
  {
    "path": "packages/core/tests/unit/understudy-command-exception.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  UnderstudyCommandException,\n  StagehandError,\n} from \"../../lib/v3/types/public/sdkErrors.js\";\n\ndescribe(\"UnderstudyCommandException\", () => {\n  it(\"extends StagehandError\", () => {\n    const err = new UnderstudyCommandException(\"test\");\n    expect(err).toBeInstanceOf(StagehandError);\n    expect(err).toBeInstanceOf(Error);\n  });\n\n  it(\"has the correct name\", () => {\n    const err = new UnderstudyCommandException(\"test\");\n    expect(err.name).toBe(\"UnderstudyCommandException\");\n  });\n\n  it(\"preserves the message\", () => {\n    const err = new UnderstudyCommandException(\"something broke\");\n    expect(err.message).toBe(\"something broke\");\n  });\n\n  it(\"stores the original error as cause when provided\", () => {\n    const original = new Error(\"root cause\");\n    const err = new UnderstudyCommandException(\"wrapper message\", original);\n\n    expect(err.cause).toBe(original);\n    expect((err.cause as Error).message).toBe(\"root cause\");\n    expect((err.cause as Error).stack).toBeDefined();\n  });\n\n  it(\"stores non-Error cause values\", () => {\n    const err = new UnderstudyCommandException(\"failed\", \"string cause\");\n    expect(err.cause).toBe(\"string cause\");\n  });\n\n  it(\"has undefined cause when none is provided\", () => {\n    const err = new UnderstudyCommandException(\"no cause\");\n    expect(err.cause).toBeUndefined();\n  });\n\n  it(\"generates its own stack trace\", () => {\n    const err = new UnderstudyCommandException(\"test\");\n    expect(err.stack).toBeDefined();\n    expect(err.stack).toContain(\"UnderstudyCommandException\");\n  });\n\n  it(\"preserves the original stack via cause for debugging\", () => {\n    function deepFunction() {\n      throw new Error(\"deep error\");\n    }\n\n    let original: Error;\n    try {\n      deepFunction();\n    } catch (e) {\n      original = e as Error;\n    }\n\n    const wrapped = new UnderstudyCommandException(original!.message, original);\n\n    // The wrapper has its own stack\n    expect(wrapped.stack).toBeDefined();\n    // The original stack is accessible via cause\n    expect((wrapped.cause as Error).stack).toContain(\"deepFunction\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/xpath-parser.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  applyPredicates,\n  parseXPathSteps,\n  type XPathPredicate,\n} from \"../../lib/v3/dom/locatorScripts/xpathParser.js\";\n\ndescribe(\"parseXPathSteps\", () => {\n  describe(\"basic tag parsing\", () => {\n    it(\"parses a simple absolute path\", () => {\n      expect(parseXPathSteps(\"/html/body/div\")).toEqual([\n        { axis: \"child\", tag: \"html\", predicates: [] },\n        { axis: \"child\", tag: \"body\", predicates: [] },\n        { axis: \"child\", tag: \"div\", predicates: [] },\n      ]);\n    });\n\n    it(\"lowercases tag names\", () => {\n      const steps = parseXPathSteps(\"/HTML/BODY\");\n      expect(steps[0].tag).toBe(\"html\");\n      expect(steps[1].tag).toBe(\"body\");\n    });\n\n    it(\"treats wildcard correctly\", () => {\n      const steps = parseXPathSteps(\"//*\");\n      expect(steps).toEqual([{ axis: \"desc\", tag: \"*\", predicates: [] }]);\n    });\n  });\n\n  describe(\"axes\", () => {\n    it(\"distinguishes child (/) from descendant (//)\", () => {\n      const steps = parseXPathSteps(\"/html//div/span\");\n      expect(steps).toEqual([\n        { axis: \"child\", tag: \"html\", predicates: [] },\n        { axis: \"desc\", tag: \"div\", predicates: [] },\n        { axis: \"child\", tag: \"span\", predicates: [] },\n      ]);\n    });\n\n    it(\"handles leading //\", () => {\n      const steps = parseXPathSteps(\"//div\");\n      expect(steps[0].axis).toBe(\"desc\");\n    });\n  });\n\n  describe(\"positional indices\", () => {\n    it(\"parses positional index\", () => {\n      const steps = parseXPathSteps(\"/div[1]/span[3]\");\n      expect(steps[0]).toMatchObject({\n        tag: \"div\",\n        predicates: [{ type: \"index\", index: 1 }],\n      });\n      expect(steps[1]).toMatchObject({\n        tag: \"span\",\n        predicates: [{ type: \"index\", index: 3 }],\n      });\n    });\n\n    it(\"clamps index to minimum 1\", () => {\n      const steps = parseXPathSteps(\"/div[0]\");\n      expect(steps[0].predicates[0]).toMatchObject({\n        type: \"index\",\n        index: 1,\n      });\n    });\n\n    it(\"keeps multiple positional predicates in order\", () => {\n      const steps = parseXPathSteps(\"//div[2][3]\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"index\", index: 2 },\n        { type: \"index\", index: 3 },\n      ]);\n    });\n  });\n\n  describe(\"attribute predicates\", () => {\n    it(\"parses single attribute predicate with single quotes\", () => {\n      const steps = parseXPathSteps(\"//img[@alt='Stagehand']\");\n      expect(steps).toEqual([\n        {\n          axis: \"desc\",\n          tag: \"img\",\n          predicates: [{ type: \"attrEquals\", name: \"alt\", value: \"Stagehand\" }],\n        },\n      ]);\n    });\n\n    it(\"parses single attribute predicate with double quotes\", () => {\n      const steps = parseXPathSteps('//img[@alt=\"Stagehand\"]');\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrEquals\", name: \"alt\", value: \"Stagehand\" },\n      ]);\n    });\n\n    it(\"parses multiple attribute predicates\", () => {\n      const steps = parseXPathSteps(\"//div[@class='foo'][@id='bar']\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrEquals\", name: \"class\", value: \"foo\" },\n        { type: \"attrEquals\", name: \"id\", value: \"bar\" },\n      ]);\n    });\n\n    it(\"parses attribute predicate combined with positional index\", () => {\n      const steps = parseXPathSteps(\"//div[@class='item'][2]\");\n      expect(steps[0]).toMatchObject({\n        tag: \"div\",\n        predicates: [\n          { type: \"attrEquals\", name: \"class\", value: \"item\" },\n          { type: \"index\", index: 2 },\n        ],\n      });\n    });\n\n    it(\"parses attribute with hyphenated name\", () => {\n      const steps = parseXPathSteps(\"//div[@data-testid='submit']\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrEquals\", name: \"data-testid\", value: \"submit\" },\n      ]);\n    });\n\n    it(\"parses attribute with empty value\", () => {\n      const steps = parseXPathSteps(\"//input[@value='']\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrEquals\", name: \"value\", value: \"\" },\n      ]);\n    });\n\n    it(\"parses attribute value containing closing bracket\", () => {\n      const steps = parseXPathSteps(\"//div[@title='array[0]']\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrEquals\", name: \"title\", value: \"array[0]\" },\n      ]);\n    });\n\n    it(\"parses attribute value containing multiple brackets\", () => {\n      const steps = parseXPathSteps(\"//div[@data-json='[1,2,3]']\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrEquals\", name: \"data-json\", value: \"[1,2,3]\" },\n      ]);\n    });\n\n    it(\"parses attribute value containing a closing bracket\", () => {\n      // The step splitter should ignore ] characters inside quotes.\n      const steps = parseXPathSteps(\"//div[@title='a]b']/span\");\n      expect(steps).toEqual([\n        {\n          axis: \"desc\",\n          tag: \"div\",\n          predicates: [{ type: \"attrEquals\", name: \"title\", value: \"a]b\" }],\n        },\n        { axis: \"child\", tag: \"span\", predicates: [] },\n      ]);\n    });\n\n    it(\"parses attribute existence predicates\", () => {\n      const steps = parseXPathSteps(\"//iframe[@data-test]\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrExists\", name: \"data-test\" },\n      ]);\n    });\n\n    it(\"parses attribute contains predicates\", () => {\n      const steps = parseXPathSteps(\"//iframe[contains(@src,'checkout')]\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrContains\", name: \"src\", value: \"checkout\" },\n      ]);\n    });\n\n    it(\"parses attribute starts-with predicates\", () => {\n      const steps = parseXPathSteps(\"//button[starts-with(@id,'save-')]\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"attrStartsWith\", name: \"id\", value: \"save-\" },\n      ]);\n    });\n  });\n\n  describe(\"text predicates\", () => {\n    it(\"parses text equality\", () => {\n      const steps = parseXPathSteps(\"//button[text()='Submit']\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"textEquals\", value: \"Submit\" },\n      ]);\n    });\n\n    it(\"parses text contains\", () => {\n      const steps = parseXPathSteps(\"//div[contains(text(),'Welcome')]\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"textContains\", value: \"Welcome\" },\n      ]);\n    });\n\n    it(\"parses normalize-space on text\", () => {\n      const steps = parseXPathSteps(\n        \"//div[normalize-space(text())='Hello world']\",\n      );\n      expect(steps[0].predicates).toEqual([\n        { type: \"textEquals\", value: \"Hello world\", normalize: true },\n      ]);\n    });\n  });\n\n  describe(\"boolean predicates\", () => {\n    it(\"parses and predicates\", () => {\n      const steps = parseXPathSteps(\"//div[@a='x' and @b='y']\");\n      expect(steps[0].predicates).toEqual([\n        {\n          type: \"and\",\n          predicates: [\n            { type: \"attrEquals\", name: \"a\", value: \"x\" },\n            { type: \"attrEquals\", name: \"b\", value: \"y\" },\n          ],\n        },\n      ]);\n    });\n\n    it(\"parses operators without surrounding whitespace\", () => {\n      const steps = parseXPathSteps(\"//div[not(@x)and@y='z']\");\n      expect(steps[0].predicates).toEqual([\n        {\n          type: \"and\",\n          predicates: [\n            { type: \"not\", predicate: { type: \"attrExists\", name: \"x\" } },\n            { type: \"attrEquals\", name: \"y\", value: \"z\" },\n          ],\n        },\n      ]);\n    });\n\n    it(\"parses or predicates\", () => {\n      const steps = parseXPathSteps(\"//div[@a='x' or @b='y']\");\n      expect(steps[0].predicates).toEqual([\n        {\n          type: \"or\",\n          predicates: [\n            { type: \"attrEquals\", name: \"a\", value: \"x\" },\n            { type: \"attrEquals\", name: \"b\", value: \"y\" },\n          ],\n        },\n      ]);\n    });\n\n    it(\"parses not predicates\", () => {\n      const steps = parseXPathSteps(\"//button[not(@disabled)]\");\n      expect(steps[0].predicates).toEqual([\n        { type: \"not\", predicate: { type: \"attrExists\", name: \"disabled\" } },\n      ]);\n    });\n\n    it(\"does not treat @and as a boolean operator\", () => {\n      const steps = parseXPathSteps(\"//div[@and='x' and @y='z']\");\n      expect(steps[0].predicates).toEqual([\n        {\n          type: \"and\",\n          predicates: [\n            { type: \"attrEquals\", name: \"and\", value: \"x\" },\n            { type: \"attrEquals\", name: \"y\", value: \"z\" },\n          ],\n        },\n      ]);\n    });\n  });\n\n  describe(\"multi-step with predicates\", () => {\n    it(\"parses complex path with mixed predicates\", () => {\n      const steps = parseXPathSteps(\n        \"/html/body//div[@class='container']/ul/li[3]\",\n      );\n      expect(steps).toEqual([\n        { axis: \"child\", tag: \"html\", predicates: [] },\n        { axis: \"child\", tag: \"body\", predicates: [] },\n        {\n          axis: \"desc\",\n          tag: \"div\",\n          predicates: [\n            { type: \"attrEquals\", name: \"class\", value: \"container\" },\n          ],\n        },\n        { axis: \"child\", tag: \"ul\", predicates: [] },\n        { axis: \"child\", tag: \"li\", predicates: [{ type: \"index\", index: 3 }] },\n      ]);\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    it(\"returns empty array for empty string\", () => {\n      expect(parseXPathSteps(\"\")).toEqual([]);\n    });\n\n    it(\"strips xpath= prefix\", () => {\n      const steps = parseXPathSteps(\"xpath=//div\");\n      expect(steps).toEqual([{ axis: \"desc\", tag: \"div\", predicates: [] }]);\n    });\n\n    it(\"strips XPATH= prefix (case-insensitive)\", () => {\n      const steps = parseXPathSteps(\"XPATH=//div\");\n      expect(steps).toEqual([{ axis: \"desc\", tag: \"div\", predicates: [] }]);\n    });\n\n    it(\"handles forward slashes inside attribute values\", () => {\n      const steps = parseXPathSteps(\"//a[@href='/api/endpoint']\");\n      expect(steps).toEqual([\n        {\n          axis: \"desc\",\n          tag: \"a\",\n          predicates: [\n            { type: \"attrEquals\", name: \"href\", value: \"/api/endpoint\" },\n          ],\n        },\n      ]);\n    });\n\n    it(\"handles URL attribute values with multiple slashes\", () => {\n      const steps = parseXPathSteps(\n        \"//a[@data-url='http://example.com/path/to/page']\",\n      );\n      expect(steps).toEqual([\n        {\n          axis: \"desc\",\n          tag: \"a\",\n          predicates: [\n            {\n              type: \"attrEquals\",\n              name: \"data-url\",\n              value: \"http://example.com/path/to/page\",\n            },\n          ],\n        },\n      ]);\n    });\n\n    it(\"handles whitespace\", () => {\n      const steps = parseXPathSteps(\"  //div  \");\n      expect(steps.length).toBe(1);\n      expect(steps[0].tag).toBe(\"div\");\n    });\n  });\n});\n\ndescribe(\"applyPredicates\", () => {\n  const makeElement = (id: string): Element => {\n    return {\n      localName: \"div\",\n      getAttribute: (name: string) => (name === \"id\" ? id : null),\n    } as unknown as Element;\n  };\n\n  it(\"applies positional predicates sequentially\", () => {\n    const elements = [\"a\", \"b\", \"c\", \"d\"].map(makeElement);\n    const predicates: XPathPredicate[] = [\n      { type: \"index\", index: 2 },\n      { type: \"index\", index: 3 },\n    ];\n    expect(applyPredicates(elements, predicates)).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/xpath-resolver.test.ts",
    "content": "import { JSDOM } from \"jsdom\";\nimport { afterAll, beforeAll, beforeEach, describe, expect, it } from \"vitest\";\nimport {\n  countXPathMatches,\n  resolveXPathAtIndex,\n} from \"../../lib/v3/dom/locatorScripts/xpathResolver.js\";\n\ntype DomGlobals = {\n  window: Window & typeof globalThis;\n  document: Document;\n  Node: typeof Node;\n  NodeFilter: typeof NodeFilter;\n  Element: typeof Element;\n  HTMLElement: typeof HTMLElement;\n  Document: typeof Document;\n  DocumentFragment: typeof DocumentFragment;\n  ShadowRoot: typeof ShadowRoot;\n  XPathResult: typeof XPathResult;\n};\n\nconst globalRef = globalThis as typeof globalThis & Partial<DomGlobals>;\nconst originalGlobals: Partial<DomGlobals> = {\n  window: globalRef.window,\n  document: globalRef.document,\n  Node: globalRef.Node,\n  NodeFilter: globalRef.NodeFilter,\n  Element: globalRef.Element,\n  HTMLElement: globalRef.HTMLElement,\n  Document: globalRef.Document,\n  DocumentFragment: globalRef.DocumentFragment,\n  ShadowRoot: globalRef.ShadowRoot,\n  XPathResult: globalRef.XPathResult,\n};\n\nlet dom: JSDOM;\n\nconst installDomGlobals = () => {\n  const win = dom.window;\n  globalRef.window = win as unknown as Window & typeof globalThis;\n  globalRef.document = win.document;\n  globalRef.Node = win.Node as unknown as typeof Node;\n  globalRef.NodeFilter = win.NodeFilter as unknown as typeof NodeFilter;\n  globalRef.Element = win.Element as unknown as typeof Element;\n  globalRef.HTMLElement = win.HTMLElement as unknown as typeof HTMLElement;\n  globalRef.Document = win.Document as unknown as typeof Document;\n  globalRef.DocumentFragment =\n    win.DocumentFragment as unknown as typeof DocumentFragment;\n  globalRef.ShadowRoot = win.ShadowRoot as unknown as typeof ShadowRoot;\n  globalRef.XPathResult = win.XPathResult as unknown as typeof XPathResult;\n};\n\nconst restoreDomGlobals = () => {\n  for (const [key, value] of Object.entries(originalGlobals)) {\n    if (value === undefined) {\n      delete (globalRef as Record<string, unknown>)[key];\n    } else {\n      (globalRef as Record<string, unknown>)[key] = value;\n    }\n  }\n};\n\ndescribe(\"xpathResolver composed traversal\", () => {\n  beforeAll(() => {\n    dom = new JSDOM(\"<!doctype html><html><body></body></html>\");\n    installDomGlobals();\n  });\n\n  afterAll(() => {\n    dom.window.close();\n    restoreDomGlobals();\n  });\n\n  beforeEach(() => {\n    document.body.innerHTML = \"\";\n  });\n\n  it(\"counts matches across light + shadow DOM without double counting\", () => {\n    document.body.innerHTML =\n      '<div id=\"light-1\"></div>' +\n      '<shadow-host id=\"host\"></shadow-host>' +\n      '<div id=\"light-2\"></div>';\n\n    const host = document.getElementById(\"host\") as HTMLElement;\n    const shadow = host.attachShadow({ mode: \"open\" });\n    shadow.innerHTML = '<div id=\"shadow-1\"></div><div id=\"shadow-2\"></div>';\n\n    expect(countXPathMatches(\"//div\")).toBe(4);\n  });\n\n  it(\"resolves nth over composed tree in document-order DFS\", () => {\n    document.body.innerHTML =\n      '<div id=\"light-1\"></div>' +\n      '<shadow-host id=\"host\"></shadow-host>' +\n      '<div id=\"light-2\"></div>';\n\n    const host = document.getElementById(\"host\") as HTMLElement;\n    const shadow = host.attachShadow({ mode: \"open\" });\n    shadow.innerHTML = '<div id=\"shadow-1\"></div><div id=\"shadow-2\"></div>';\n\n    expect(resolveXPathAtIndex(\"//div\", 0)?.id).toBe(\"light-1\");\n    expect(resolveXPathAtIndex(\"//div\", 1)?.id).toBe(\"shadow-1\");\n    expect(resolveXPathAtIndex(\"//div\", 2)?.id).toBe(\"shadow-2\");\n    expect(resolveXPathAtIndex(\"//div\", 3)?.id).toBe(\"light-2\");\n  });\n});\n"
  },
  {
    "path": "packages/core/tests/unit/zod-enum-compatibility.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport * as z3 from \"zod/v3\";\nimport { z as z4 } from \"zod\";\nimport { SupportedUnderstudyAction } from \"../../lib/v3/types/private/handlers.js\";\n\n/**\n * Tests for Zod v3/v4 compatibility with the SupportedUnderstudyAction enum.\n *\n * This test ensures that z.enum() works correctly with both Zod v3 and v4.\n * The key issue is that z.enum() in Zod v3 does NOT accept TypeScript enums directly -\n * it only accepts string literal tuples. For TypeScript enums, you need to use\n * Object.values() to convert the enum to an array first.\n *\n * In Zod v4, z.enum() was updated to accept TypeScript enums directly, but for\n * backwards compatibility, we should use Object.values() which works with both.\n *\n * See PR #1613: https://github.com/browserbase/stagehand/pull/1613\n */\ndescribe(\"SupportedUnderstudyAction enum Zod compatibility\", () => {\n  const testInput = {\n    elementId: \"1-2\",\n    method: \"click\",\n    arguments: [] as string[],\n  };\n\n  const invalidInput = {\n    elementId: \"1-2\",\n    method: \"invalidMethod\",\n    arguments: [] as string[],\n  };\n\n  it(\"Object.values(SupportedUnderstudyAction) produces correct array for z.enum()\", () => {\n    const enumValues = Object.values(\n      SupportedUnderstudyAction,\n    ) as unknown as readonly [string, ...string[]];\n\n    expect(enumValues).toContain(\"click\");\n    expect(enumValues).toContain(\"fill\");\n    expect(enumValues).toContain(\"type\");\n    expect(enumValues).toContain(\"press\");\n    expect(enumValues).toContain(\"scrollTo\");\n    expect(enumValues).toContain(\"nextChunk\");\n    expect(enumValues).toContain(\"prevChunk\");\n    expect(enumValues).toContain(\"selectOptionFromDropdown\");\n    expect(enumValues).toContain(\"hover\");\n    expect(enumValues).toContain(\"doubleClick\");\n    expect(enumValues).toContain(\"dragAndDrop\");\n    expect(enumValues.length).toBe(11);\n  });\n\n  it(\"Zod v3 z.enum() with Object.values(SupportedUnderstudyAction) works correctly\", () => {\n    const enumValues = Object.values(\n      SupportedUnderstudyAction,\n    ) as unknown as readonly [string, ...string[]];\n\n    const schema = z3.z.object({\n      elementId: z3.z.string(),\n      method: z3.z.enum(enumValues),\n      arguments: z3.z.array(z3.z.string()),\n    });\n\n    // Valid input should pass\n    const validResult = schema.safeParse(testInput);\n    expect(validResult.success).toBe(true);\n    if (validResult.success) {\n      expect(validResult.data.method).toBe(\"click\");\n    }\n\n    // Invalid input should fail\n    const invalidResult = schema.safeParse(invalidInput);\n    expect(invalidResult.success).toBe(false);\n  });\n\n  it(\"Zod v4 z.enum() with Object.values(SupportedUnderstudyAction) works correctly\", () => {\n    const enumValues = Object.values(\n      SupportedUnderstudyAction,\n    ) as unknown as readonly [string, ...string[]];\n\n    const schema = z4.object({\n      elementId: z4.string(),\n      method: z4.enum(enumValues),\n      arguments: z4.array(z4.string()),\n    });\n\n    // Valid input should pass\n    const validResult = schema.safeParse(testInput);\n    expect(validResult.success).toBe(true);\n    if (validResult.success) {\n      expect(validResult.data.method).toBe(\"click\");\n    }\n\n    // Invalid input should fail\n    const invalidResult = schema.safeParse(invalidInput);\n    expect(invalidResult.success).toBe(false);\n  });\n\n  it(\"Zod v3 z.enum() with raw TypeScript enum throws error on parse\", () => {\n    // This demonstrates the bug that PR #1613 would introduce\n    // In Zod v3, z.enum() does NOT accept TypeScript enums directly\n    // The schema creation might succeed, but parsing will fail\n\n    const schema = z3.z.object({\n      elementId: z3.z.string(),\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      method: z3.z.enum(SupportedUnderstudyAction as any),\n      arguments: z3.z.array(z3.z.string()),\n    });\n\n    // This should throw an error because the enum is not iterable\n    expect(() => schema.safeParse(testInput)).toThrow(\"object is not iterable\");\n  });\n\n  it(\"Zod v4 z.enum() with raw TypeScript enum works (but not v3 compatible)\", () => {\n    // Zod v4 allows passing TypeScript enums directly to z.enum()\n    // But this approach is NOT backwards compatible with v3\n\n    const schema = z4.object({\n      elementId: z4.string(),\n      method: z4.enum(SupportedUnderstudyAction),\n      arguments: z4.array(z4.string()),\n    });\n\n    // In v4, this works fine\n    const validResult = schema.safeParse(testInput);\n    expect(validResult.success).toBe(true);\n  });\n\n  it(\"All SupportedUnderstudyAction values are valid enum options\", () => {\n    const enumValues = Object.values(\n      SupportedUnderstudyAction,\n    ) as unknown as readonly [string, ...string[]];\n\n    // Test with both v3 and v4 schemas\n    const v3Schema = z3.z.enum(enumValues);\n    const v4Schema = z4.enum(enumValues);\n\n    for (const action of enumValues) {\n      expect(v3Schema.safeParse(action).success).toBe(true);\n      expect(v4Schema.safeParse(action).success).toBe(true);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \"../../\",\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist/esm\",\n    \"allowJs\": true,\n    \"paths\": {\n      \"@browserbasehq/stagehand\": [\"packages/core/lib/v3/index.ts\"],\n      \"@browserbasehq/stagehand/*\": [\"packages/core/lib/*\"],\n      \"*\": [\"node_modules/*\", \"packages/core/lib/types/*\"],\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\n    \"lib/**/*.ts\",\n    \"tests/**/*.ts\",\n    \"lib/v3/cli.js\"\n  ],\n  \"exclude\": [\"node_modules\", \"dist\", \"lib/v3/dom/gen*.ts\"]\n}\n"
  },
  {
    "path": "packages/core/vitest.cjs.config.mjs",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst rootDir = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@browserbasehq/stagehand\": path.join(rootDir, \"dist\", \"cjs\", \"index.js\"),\n    },\n  },\n  test: {\n    environment: \"node\",\n    include: [\"**/dist/cjs/tests/unit/**/*.test.js\"],\n  },\n});\n"
  },
  {
    "path": "packages/core/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst rootDir = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@browserbasehq/stagehand\": path.join(rootDir, \"dist\", \"esm\", \"index.js\"),\n    },\n  },\n  test: {\n    environment: \"node\",\n    include: [\"**/dist/esm/tests/unit/**/*.test.js\"],\n  },\n});\n"
  },
  {
    "path": "packages/core/vitest.esm.config.mjs",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst rootDir = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@browserbasehq/stagehand\": path.join(rootDir, \"dist\", \"esm\", \"index.js\"),\n    },\n  },\n  test: {\n    environment: \"node\",\n    include: [\"**/dist/esm/tests/unit/**/*.test.js\"],\n  },\n});\n"
  },
  {
    "path": "packages/docs/.gitignore",
    "content": "node_modules\ndownloads\n.DS_Store"
  },
  {
    "path": "packages/docs/README.md",
    "content": "# Mintlify Starter Kit\n\nClick on `Use this template` to copy the Mintlify starter kit. The starter kit contains examples including\n\n- Guide pages\n- Navigation\n- Customizations\n- API Reference pages\n- Use of popular components\n\n### Development\n\nInstall dependencies with pnpm\n\n```\npnpm install\n```\n\nRun the following command at the root of your documentation (where mint.json is)\n\n```\npnpm mintlify dev\n```\n\n### Publishing Changes\n\nInstall our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.\n\n#### Troubleshooting\n\n- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.\n- Page loads as a 404 - Make sure you are running in a folder with `mint.json`\n"
  },
  {
    "path": "packages/docs/docs.json",
    "content": "{\n  \"$schema\": \"https://mintlify.com/docs.json\",\n  \"theme\": \"willow\",\n  \"name\": \"🤘 Stagehand\",\n  \"colors\": {\n    \"primary\": \"#B88100\",\n    \"light\": \"#FFC83C\",\n    \"dark\": \"#FFC83C\"\n  },\n  \"favicon\": \"/images/favicon.svg\",\n  \"seo\": {\n    \"indexing\": \"all\",\n    \"metatags\": {\n      \"og:type\": \"website\",\n      \"og:site_name\": \"Stagehand Docs\"\n    }\n  },\n  \"openapi\": \"https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml\",\n  \"navigation\": {\n    \"versions\": [\n      {\n        \"version\": \"v3\",\n        \"dropdowns\": [\n          {\n            \"dropdown\": \"TypeScript\",\n            \"icon\": \"code\",\n            \"pages\": [\n              \"v3/first-steps/introduction\"\n            ],\n            \"groups\": [\n              {\n                \"group\": \"First Steps\",\n                \"pages\": [\n                  \"v3/first-steps/introduction\",\n                  \"v3/first-steps/quickstart\",\n                  \"v3/first-steps/installation\",\n                  \"v3/first-steps/ai-rules\"\n                ]\n              },\n              {\n                \"group\": \"The Basics\",\n                \"pages\": [\n                  \"v3/basics/agent\",\n                  \"v3/basics/act\",\n                  \"v3/basics/extract\",\n                  \"v3/basics/observe\",\n                  \"v3/basics/evals\"\n                ]\n              },\n              {\n                \"group\": \"Configuration\",\n                \"pages\": [\n                  \"v3/configuration/browser\",\n                  \"v3/configuration/observability\",\n                  \"v3/configuration/logging\",\n                  \"v3/configuration/models\"\n                ]\n              },\n              {\n                \"group\": \"Best Practices\",\n                \"pages\": [\n                  \"v3/best-practices/caching\",\n                  \"v3/best-practices/cost-optimization\",\n                  \"v3/best-practices/deterministic-agent\",\n                  \"v3/best-practices/using-multiple-tabs\",\n                  \"v3/best-practices/deployments\",\n                  \"v3/best-practices/history\",\n                  \"v3/best-practices/computer-use\",\n                  \"v3/best-practices/agent-fallbacks\",\n                  \"v3/best-practices/prompting-best-practices\",\n                  \"v3/best-practices/mcp-integrations\",\n                  \"v3/best-practices/speed-optimization\"\n                ]\n              },\n              {\n                \"group\": \"Integrations\",\n                \"pages\": [\n                  {\n                    \"group\": \"MCP Server\",\n                    \"pages\": [\n                      \"v3/integrations/mcp/introduction\",\n                      \"v3/integrations/mcp/setup\",\n                      \"v3/integrations/mcp/tools\",\n                      \"v3/integrations/mcp/configuration\"\n                    ]\n                  },\n                  {\n                    \"group\": \"CrewAI\",\n                    \"pages\": [\n                      \"v3/integrations/crew-ai/introduction\",\n                      \"v3/integrations/crew-ai/configuration\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Langchain\",\n                    \"pages\": [\n                      \"v3/integrations/langchain/introduction\",\n                      \"v3/integrations/langchain/configuration\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Next.js + Vercel\",\n                    \"pages\": [\n                      \"v3/integrations/vercel/introduction\",\n                      \"v3/integrations/vercel/configuration\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Convex\",\n                    \"pages\": [\n                      \"v3/integrations/convex/introduction\",\n                      \"v3/integrations/convex/configuration\"\n                    ]\n                  },\n                  \"v3/integrations/playwright\",\n                  \"v3/integrations/puppeteer\",\n                  \"v3/integrations/selenium\"\n                ]\n              },\n              {\n                \"group\": \"Reference\",\n                \"pages\": [\n                  \"v3/references/stagehand\",\n                  \"v3/references/agent\",\n                  \"v3/references/act\",\n                  \"v3/references/extract\",\n                  \"v3/references/observe\",\n                  \"v3/references/context\",\n                  \"v3/references/page\",\n                  \"v3/references/locator\",\n                  \"v3/references/deeplocator\",\n                  \"v3/references/response\"\n                ]\n              },\n              {\n                \"group\": \"Migration Guides\",\n                \"pages\": [\n                  \"v3/migrations/v2\",\n                  \"v3/migrations/python\"\n                ]\n              }\n            ]\n          },\n          {\n            \"dropdown\": \"Python\",\n            \"icon\": \"code\",\n            \"pages\": [\n              \"v3/sdk/python\"\n            ],\n            \"groups\": [\n              {\n                \"group\": \"SDK Reference\",\n                \"pages\": [\n                  \"v3/sdk/python\"\n                ]\n              },\n              {\n                \"group\": \"API Reference\",\n                \"openapi\": {\n                  \"source\": \"https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml\",\n                  \"directory\": \"v3/api-reference/python\"\n                },\n                \"pages\": [\n                  \"POST /v1/sessions/start\",\n                  \"POST /v1/sessions/{id}/navigate\",\n                  \"POST /v1/sessions/{id}/act\",\n                  \"POST /v1/sessions/{id}/observe\",\n                  \"POST /v1/sessions/{id}/extract\",\n                  \"POST /v1/sessions/{id}/agentExecute\",\n                  \"POST /v1/sessions/{id}/end\",\n                  \"GET /v1/sessions/{id}/replay\"\n                ]\n              }\n            ]\n          },\n          {\n            \"dropdown\": \"Java\",\n            \"icon\": \"code\",\n            \"pages\": [\n              \"v3/sdk/java\"\n            ],\n            \"groups\": [\n              {\n                \"group\": \"SDK Reference\",\n                \"pages\": [\n                  \"v3/sdk/java\"\n                ]\n              },\n              {\n                \"group\": \"API Reference\",\n                \"openapi\": {\n                  \"source\": \"https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml\",\n                  \"directory\": \"v3/api-reference/java\"\n                },\n                \"pages\": [\n                  \"POST /v1/sessions/start\",\n                  \"POST /v1/sessions/{id}/navigate\",\n                  \"POST /v1/sessions/{id}/act\",\n                  \"POST /v1/sessions/{id}/observe\",\n                  \"POST /v1/sessions/{id}/extract\",\n                  \"POST /v1/sessions/{id}/agentExecute\",\n                  \"POST /v1/sessions/{id}/end\",\n                  \"GET /v1/sessions/{id}/replay\"\n                ]\n              }\n            ]\n          },\n          {\n            \"dropdown\": \"Go\",\n            \"icon\": \"code\",\n            \"pages\": [\n              \"v3/sdk/go\"\n            ],\n            \"groups\": [\n              {\n                \"group\": \"SDK Reference\",\n                \"pages\": [\n                  \"v3/sdk/go\"\n                ]\n              },\n              {\n                \"group\": \"API Reference\",\n                \"openapi\": {\n                  \"source\": \"https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml\",\n                  \"directory\": \"v3/api-reference/go\"\n                },\n                \"pages\": [\n                  \"POST /v1/sessions/start\",\n                  \"POST /v1/sessions/{id}/navigate\",\n                  \"POST /v1/sessions/{id}/act\",\n                  \"POST /v1/sessions/{id}/observe\",\n                  \"POST /v1/sessions/{id}/extract\",\n                  \"POST /v1/sessions/{id}/agentExecute\",\n                  \"POST /v1/sessions/{id}/end\",\n                  \"GET /v1/sessions/{id}/replay\"\n                ]\n              }\n            ]\n          },\n          {\n            \"dropdown\": \"Ruby\",\n            \"icon\": \"code\",\n            \"pages\": [\n              \"v3/sdk/ruby\"\n            ],\n            \"groups\": [\n              {\n                \"group\": \"SDK Reference\",\n                \"pages\": [\n                  \"v3/sdk/ruby\"\n                ]\n              },\n              {\n                \"group\": \"API Reference\",\n                \"openapi\": {\n                  \"source\": \"https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml\",\n                  \"directory\": \"v3/api-reference/ruby\"\n                },\n                \"pages\": [\n                  \"POST /v1/sessions/start\",\n                  \"POST /v1/sessions/{id}/navigate\",\n                  \"POST /v1/sessions/{id}/act\",\n                  \"POST /v1/sessions/{id}/observe\",\n                  \"POST /v1/sessions/{id}/extract\",\n                  \"POST /v1/sessions/{id}/agentExecute\",\n                  \"POST /v1/sessions/{id}/end\",\n                  \"GET /v1/sessions/{id}/replay\"\n                ]\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"version\": \"v2\",\n        \"groups\": [\n          {\n            \"group\": \"First Steps\",\n            \"pages\": [\n              \"v2/first-steps/introduction\",\n              \"v2/first-steps/quickstart\",\n              \"v2/first-steps/installation\",\n              \"v2/first-steps/ai-rules\"\n            ]\n          },\n          {\n            \"group\": \"The Basics\",\n            \"pages\": [\n              \"v2/basics/agent\",\n              \"v2/basics/act\",\n              \"v2/basics/extract\",\n              \"v2/basics/observe\"\n            ]\n          },\n          {\n            \"group\": \"Configuration\",\n            \"pages\": [\n              \"v2/configuration/browser\",\n              \"v2/configuration/observability\",\n              \"v2/configuration/logging\",\n              \"v2/configuration/models\",\n              \"v2/configuration/evals\"\n            ]\n          },\n          {\n            \"group\": \"Best Practices\",\n            \"pages\": [\n              \"v2/best-practices/caching\",\n              \"v2/best-practices/cost-optimization\",\n              \"v2/best-practices/using-multiple-tabs\",\n              \"v2/best-practices/working-with-iframes\",\n              \"v2/best-practices/deployments\",\n              \"v2/best-practices/computer-use\",\n              \"v2/best-practices/contributing\",\n              \"v2/best-practices/playwright-interop\",\n              \"v2/best-practices/build-agent\",\n              \"v2/best-practices/agent-fallbacks\",\n              \"v2/best-practices/prompting-best-practices\",\n              \"v2/best-practices/mcp-integrations\",\n              \"v2/best-practices/speed-optimization\"\n            ]\n          },\n          {\n            \"group\": \"Integrations\",\n            \"pages\": [\n              {\n                \"group\": \"MCP Server\",\n                \"pages\": [\n                  \"v2/integrations/mcp/introduction\",\n                  \"v2/integrations/mcp/setup\",\n                  \"v2/integrations/mcp/tools\",\n                  \"v2/integrations/mcp/configuration\"\n                ]\n              },\n              {\n                \"group\": \"CrewAI\",\n                \"pages\": [\n                  \"v2/integrations/crew-ai/introduction\",\n                  \"v2/integrations/crew-ai/configuration\"\n                ]\n              },\n              {\n                \"group\": \"Langchain\",\n                \"pages\": [\n                  \"v2/integrations/langchain/introduction\",\n                  \"v2/integrations/langchain/configuration\"\n                ]\n              },\n              {\n                \"group\": \"Next.js + Vercel\",\n                \"pages\": [\n                  \"v2/integrations/vercel/introduction\",\n                  \"v2/integrations/vercel/configuration\"\n                ]\n              }\n            ]\n          },\n          {\n            \"group\": \"Reference\",\n            \"pages\": [\n              \"v2/references/stagehand\",\n              \"v2/references/act\",\n              \"v2/references/extract\",\n              \"v2/references/observe\",\n              \"v2/references/agent\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"global\": {\n      \"anchors\": [\n        {\n          \"anchor\": \"Discord\",\n          \"href\": \"https://stagehand.dev/discord\",\n          \"icon\": \"discord\"\n        },\n        {\n          \"anchor\": \"GitHub\",\n          \"href\": \"https://github.com/browserbase/stagehand\",\n          \"icon\": \"github\"\n        },\n        {\n          \"anchor\": \"Changelog\",\n          \"href\": \"https://github.com/browserbase/stagehand/releases\",\n          \"icon\": \"scroll\"\n        }\n      ]\n    }\n  },\n  \"logo\": {\n    \"light\": \"/logo/light_logo.png\",\n    \"dark\": \"/logo/dark_logo.png\",\n    \"href\": \"https://stagehand.dev\"\n  },\n  \"navbar\": {\n    \"links\": [\n      {\n        \"label\": \"Discord\",\n        \"href\": \"https://stagehand.dev/discord\"\n      },\n      {\n        \"label\": \"Support\",\n        \"href\": \"mailto:support@browserbase.com\"\n      }\n    ]\n  },\n  \"footer\": {\n    \"socials\": {\n      \"discord\": \"https://stagehand.dev/discord\",\n      \"x\": \"https://x.com/stagehanddev\",\n      \"github\": \"https://github.com/browserbase/stagehand\",\n      \"linkedin\": \"https://linkedin.com/company/browserbasehq\"\n    }\n  },\n  \"integrations\": {\n    \"posthog\": {\n      \"apiKey\": \"phc_hmwkFrlc9UVrdE1jyG8AEKoCQCSr8dScjsRpKoLBEiV\",\n      \"apiHost\": \"https://us.i.posthog.com\"\n    }\n  },\n  \"contextual\": {\n    \"options\": [\n      \"copy\",\n      \"chatgpt\",\n      \"claude\",\n      \"view\"\n    ]\n  },\n  \"redirects\": [\n    {\n      \"source\": \"/first-steps/:slug*\",\n      \"destination\": \"/v3/first-steps/:slug*\"\n    },\n    {\n      \"source\": \"/basics/:slug*\",\n      \"destination\": \"/v3/basics/:slug*\"\n    },\n    {\n      \"source\": \"/configuration/:slug*\",\n      \"destination\": \"/v3/configuration/:slug*\"\n    },\n    {\n      \"source\": \"/best-practices/:slug*\",\n      \"destination\": \"/v3/best-practices/:slug*\"\n    },\n    {\n      \"source\": \"/integrations/mcp/:slug*\",\n      \"destination\": \"/v3/integrations/mcp/:slug*\"\n    },\n    {\n      \"source\": \"/integrations/crew-ai/:slug*\",\n      \"destination\": \"/v3/integrations/crew-ai/:slug*\"\n    },\n    {\n      \"source\": \"/integrations/langchain/:slug*\",\n      \"destination\": \"/v3/integrations/langchain/:slug*\"\n    },\n    {\n      \"source\": \"/integrations/vercel/:slug*\",\n      \"destination\": \"/v3/integrations/vercel/:slug*\"\n    },\n    {\n      \"source\": \"/integrations/convex/:slug*\",\n      \"destination\": \"/v3/integrations/convex/:slug*\"\n    },\n    {\n      \"source\": \"/references/:slug*\",\n      \"destination\": \"/v3/references/:slug*\"\n    },\n    {\n      \"source\": \"/migrations/:slug*\",\n      \"destination\": \"/v3/migrations/:slug*\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/docs/language-selector.js",
    "content": "// Language switcher for Stagehand docs\n// Handles: 1) Sidebar language dropdown selection 2) Code block language syncing\n\n(function() {\n  // ============================================\n  // CONFIGURATION\n  // ============================================\n\n  const DROPDOWN_LANGUAGES = ['TypeScript', 'Python', 'Java', 'Go', 'Ruby'];\n\n  const LANGUAGE_MAP = {\n    'TypeScript': 'Javascript',\n    'Python': 'Python',\n    'Java': 'Java',\n    'Go': 'Go',\n    'Ruby': 'Ruby'\n  };\n\n  const CODE_BLOCK_LANGUAGES = ['Javascript', 'Python', 'Go', 'Java', 'Ruby', 'cURL', 'PHP'];\n\n  const SDK_PATH_MAP = {\n    'Python': 'python',\n    'Java': 'java',\n    'Go': 'go',\n    'Ruby': 'ruby'\n  };\n\n  const NAVIGATION_MAP = {\n    'TypeScript': '/v3/first-steps/introduction',\n    'Python': '/v3/sdk/python',\n    'Java': '/v3/sdk/java',\n    'Go': '/v3/sdk/go',\n    'Ruby': '/v3/sdk/ruby'\n  };\n\n  let currentSelectedLanguage = 'TypeScript';\n  let isSelecting = false;\n\n  // ============================================\n  // UTILITIES\n  // ============================================\n\n  // Run callback on next frame (immediate visual update)\n  const onNextFrame = (fn) => requestAnimationFrame(() => requestAnimationFrame(fn));\n\n  const dropdownStyle = document.createElement('style');\n  dropdownStyle.id = 'stagehand-language-style';\n  dropdownStyle.textContent = `\n    /* Hide dropdown during programmatic selection */\n    .stagehand-selecting [role=\"menu\"],\n    .stagehand-selecting [role=\"listbox\"] {\n      opacity: 0 !important;\n      pointer-events: none !important;\n      transition: none !important;\n    }\n    \n    /* Hide version switcher when non-TypeScript language is selected */\n    .stagehand-hide-version-switcher .stagehand-version-switcher {\n      display: none !important;\n    }\n    \n    /* Hide SDK reference items that don't match the selected language */\n    li[id^=\"/v3/sdk/\"].stagehand-sdk-hidden {\n      display: none !important;\n    }\n  `;\n  document.head.appendChild(dropdownStyle);\n  \n  // ============================================\n  // SDK REFERENCE FILTERING\n  // ============================================\n  \n  function updateSDKReferenceVisibility() {\n    // Get the SDK path for the current language\n    const currentSDKPath = SDK_PATH_MAP[currentSelectedLanguage];\n    \n    // Find all SDK reference items in the sidebar\n    const sdkItems = document.querySelectorAll('li[id^=\"/v3/sdk/\"]');\n    \n    sdkItems.forEach(item => {\n      const itemId = item.getAttribute('id') || '';\n      // Extract the language from the id (e.g., \"/v3/sdk/python\" -> \"python\")\n      const itemLang = itemId.split('/').pop();\n      \n      if (currentSelectedLanguage === 'TypeScript') {\n        // For TypeScript, hide all SDK references (they don't apply)\n        item.classList.add('stagehand-sdk-hidden');\n      } else if (currentSDKPath && itemLang === currentSDKPath) {\n        // Show the SDK that matches the current language\n        item.classList.remove('stagehand-sdk-hidden');\n      } else {\n        // Hide SDKs that don't match\n        item.classList.add('stagehand-sdk-hidden');\n      }\n    });\n  }\n\n  // ============================================\n  // VERSION SWITCHER VISIBILITY\n  // ============================================\n  \n  function getVersionSwitcher() {\n    // Find the version switcher button (contains \"v3\" or \"v2\" and has chevron-down)\n    const buttons = document.querySelectorAll('button');\n    for (const btn of buttons) {\n      const text = (btn.textContent || '').trim().toLowerCase();\n      // Check if it's a version button (v2, v3, etc.) with chevron icon\n      if (/^v\\d+$/.test(text) && btn.querySelector('.lucide-chevron-down')) {\n        return btn;\n      }\n    }\n    return null;\n  }\n  \n  function updateVersionSwitcherVisibility() {\n    const versionSwitcher = getVersionSwitcher();\n    \n    if (versionSwitcher) {\n      // Mark the version switcher so we can target it with CSS\n      versionSwitcher.classList.add('stagehand-version-switcher');\n      \n      // Show version switcher only for TypeScript\n      if (currentSelectedLanguage === 'TypeScript') {\n        document.body.classList.remove('stagehand-hide-version-switcher');\n      } else {\n        document.body.classList.add('stagehand-hide-version-switcher');\n      }\n    }\n  }\n  \n  // ============================================\n  // SIDEBAR DROPDOWN FUNCTIONS\n  // ============================================\n  \n  function getDropdownButton() {\n    const buttons = document.querySelectorAll('button');\n    for (const btn of buttons) {\n      const text = (btn.textContent || '').trim();\n      if (DROPDOWN_LANGUAGES.includes(text)) {\n        return btn;\n      }\n    }\n    return null;\n  }\n  \n  function getDropdownMenu() {\n    return document.querySelector('menu[role=\"menu\"], [role=\"menu\"]');\n  }\n  \n  function updateButtonText(newText) {\n    const button = getDropdownButton();\n    if (!button) return;\n    \n    const paragraph = button.querySelector('p');\n    if (paragraph) {\n      paragraph.textContent = newText;\n    }\n  }\n  \n  function updateDropdownCheckIndicator() {\n    const menu = getDropdownMenu();\n    if (!menu) return;\n    \n    const menuItems = menu.querySelectorAll('a, [role=\"menuitem\"]');\n    const checkIconsMap = new Map();\n    let anyCheckIcon = null;\n    \n    for (const item of menuItems) {\n      const text = (item.textContent || '').trim();\n      const checkIcon = item.querySelector('.lucide-check, [class*=\"lucide-check\"], svg[class*=\"check\"]');\n      \n      for (const lang of DROPDOWN_LANGUAGES) {\n        if (text.includes(lang)) {\n          checkIconsMap.set(lang, { item, checkIcon });\n          if (checkIcon) {\n            anyCheckIcon = checkIcon;\n          }\n          break;\n        }\n      }\n    }\n    \n    for (const [lang, { item, checkIcon }] of checkIconsMap) {\n      const shouldBeSelected = lang === currentSelectedLanguage;\n      \n      if (checkIcon) {\n        checkIcon.style.opacity = shouldBeSelected ? '1' : '0';\n        checkIcon.style.visibility = shouldBeSelected ? 'visible' : 'hidden';\n      } else if (shouldBeSelected && anyCheckIcon) {\n        const clonedCheck = anyCheckIcon.cloneNode(true);\n        clonedCheck.style.opacity = '1';\n        clonedCheck.style.visibility = 'visible';\n        \n        const targetSpan = item.querySelector('span:last-child') || item;\n        if (targetSpan.querySelector('.lucide-check, [class*=\"lucide-check\"]') === null) {\n          targetSpan.appendChild(clonedCheck);\n        }\n      }\n    }\n  }\n  \n  // ============================================\n  // CODE BLOCK LANGUAGE SELECTOR FUNCTIONS\n  // ============================================\n  \n  function simulateClick(element) {\n    if (!element) return;\n    const rect = element.getBoundingClientRect();\n    const x = rect.left + rect.width / 2;\n    const y = rect.top + rect.height / 2;\n    \n    ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach(eventType => {\n      const EventClass = eventType.startsWith('pointer') ? PointerEvent : MouseEvent;\n      element.dispatchEvent(new EventClass(eventType, {\n        view: window, bubbles: true, cancelable: true,\n        clientX: x, clientY: y, button: 0, buttons: 1,\n        isPrimary: true, pointerType: 'mouse'\n      }));\n    });\n  }\n  \n  function getCodeBlockLanguageDropdown() {\n    const paragraphs = document.querySelectorAll('p');\n    \n    for (const p of paragraphs) {\n      const text = (p.textContent || '').trim();\n      if (CODE_BLOCK_LANGUAGES.includes(text)) {\n        const parentDiv = p.closest('div');\n        if (parentDiv && parentDiv.querySelector('.lucide-chevrons-up-down')) {\n          return { element: parentDiv, language: text };\n        }\n      }\n    }\n    return null;\n  }\n  \n  function waitForCodeBlockMenuAndSelect(targetLanguage, attempts = 0) {\n    if (attempts > 30) {\n      document.body.classList.remove('stagehand-selecting');\n      document.body.click();\n      isSelecting = false;\n      return;\n    }\n\n    const menuItems = document.querySelectorAll('[role=\"menuitem\"], [role=\"option\"]');\n\n    if (menuItems.length === 0) {\n      requestAnimationFrame(() => waitForCodeBlockMenuAndSelect(targetLanguage, attempts + 1));\n      return;\n    }\n\n    for (const item of menuItems) {\n      const text = (item.textContent || '').trim();\n      if (text === targetLanguage) {\n        simulateClick(item);\n        onNextFrame(() => {\n          document.body.classList.remove('stagehand-selecting');\n          isSelecting = false;\n        });\n        return;\n      }\n    }\n\n    requestAnimationFrame(() => waitForCodeBlockMenuAndSelect(targetLanguage, attempts + 1));\n  }\n\n  function selectCodeBlockLanguage(targetLanguage) {\n    if (isSelecting) return;\n\n    const current = getCodeBlockLanguageDropdown();\n    if (!current) return;\n    if (current.language === targetLanguage) return;\n\n    isSelecting = true;\n    document.body.classList.add('stagehand-selecting');\n    simulateClick(current.element);\n    requestAnimationFrame(() => waitForCodeBlockMenuAndSelect(targetLanguage));\n  }\n\n  function syncCodeBlockLanguage() {\n    const codeBlockLang = LANGUAGE_MAP[currentSelectedLanguage];\n    if (codeBlockLang) {\n      selectCodeBlockLanguage(codeBlockLang);\n    }\n  }\n  \n  // ============================================\n  // EVENT HANDLERS & OBSERVERS\n  // ============================================\n  \n  function setupDropdownMenuObserver() {\n    const menuObserver = new MutationObserver(() => {\n      const menu = getDropdownMenu();\n      if (menu) {\n        updateDropdownCheckIndicator();\n        onNextFrame(updateDropdownCheckIndicator);\n      }\n    });\n\n    menuObserver.observe(document.body, {\n      subtree: true,\n      childList: true\n    });\n  }\n  \n  function setupMenuClickHandler() {\n    document.addEventListener('click', (e) => {\n      const target = e.target;\n      \n      // Check if we clicked on a sidebar dropdown menu item\n      const menuItem = target.closest('[role=\"menu\"] a, menu a');\n      if (!menuItem) return;\n      \n      const text = (menuItem.textContent || '').trim();\n      \n      // Check if it's one of our language options\n      for (const lang of DROPDOWN_LANGUAGES) {\n        if (text.includes(lang)) {\n          currentSelectedLanguage = lang;\n          \n          // Update the check indicator immediately\n          updateDropdownCheckIndicator();\n          \n          // Update version switcher visibility\n          updateVersionSwitcherVisibility();\n          \n          // Update SDK reference visibility\n          updateSDKReferenceVisibility();\n          \n          // Store in sessionStorage\n          try {\n            sessionStorage.setItem('stagehand-selected-language', lang);\n          } catch (err) {\n            // Ignore storage errors\n          }\n\n          // Navigate to the corresponding SDK page\n          const targetPath = NAVIGATION_MAP[lang];\n          const normalizedPathname = window.location.pathname.replace(/\\/$/, '');\n          if (targetPath && !normalizedPathname.endsWith(targetPath)) {\n            e.preventDefault();\n            e.stopPropagation();\n            window.location.href = targetPath;\n            return;\n          }\n\n          // Update button text after menu closes\n          onNextFrame(() => updateButtonText(lang));\n\n          // Sync the code block language selector\n          onNextFrame(syncCodeBlockLanguage);\n\n          break;\n        }\n      }\n    }, true);\n  }\n  \n  function restoreLanguageSelection() {\n    try {\n      const stored = sessionStorage.getItem('stagehand-selected-language');\n      if (stored && DROPDOWN_LANGUAGES.includes(stored)) {\n        currentSelectedLanguage = stored;\n        updateButtonText(stored);\n        updateVersionSwitcherVisibility();\n        updateSDKReferenceVisibility();\n        onNextFrame(syncCodeBlockLanguage);\n      }\n    } catch (err) {\n      // Ignore storage errors\n    }\n\n    // Always update visibility on restore\n    onNextFrame(() => {\n      updateVersionSwitcherVisibility();\n      updateSDKReferenceVisibility();\n    });\n  }\n  \n  function setupPageChangeObserver() {\n    let sdkUpdatePending = false;\n\n    const observer = new MutationObserver(() => {\n      // Check if button needs updating\n      const button = getDropdownButton();\n      if (button) {\n        const currentText = (button.textContent || '').trim();\n        if (currentText !== currentSelectedLanguage && DROPDOWN_LANGUAGES.includes(currentSelectedLanguage)) {\n          updateButtonText(currentSelectedLanguage);\n        }\n      }\n\n      // Re-check version switcher visibility (DOM might have re-rendered)\n      const versionSwitcher = getVersionSwitcher();\n      if (versionSwitcher && !versionSwitcher.classList.contains('stagehand-version-switcher')) {\n        updateVersionSwitcherVisibility();\n      }\n\n      // Check for SDK reference items that need to be hidden (debounced via rAF)\n      const sdkItems = document.querySelectorAll('li[id^=\"/v3/sdk/\"]:not(.stagehand-sdk-processed)');\n      if (sdkItems.length > 0 && !sdkUpdatePending) {\n        sdkUpdatePending = true;\n        onNextFrame(() => {\n          updateSDKReferenceVisibility();\n          document.querySelectorAll('li[id^=\"/v3/sdk/\"]').forEach(item => {\n            item.classList.add('stagehand-sdk-processed');\n          });\n          sdkUpdatePending = false;\n        });\n      }\n    });\n\n    observer.observe(document.body, {\n      subtree: true,\n      childList: true\n    });\n  }\n  \n  // Watch for code block dropdowns appearing and sync them\n  function setupCodeBlockObserver() {\n    let lastCodeBlockDropdown = null;\n\n    const observer = new MutationObserver(() => {\n      const dropdown = getCodeBlockLanguageDropdown();\n      if (dropdown && dropdown.element !== lastCodeBlockDropdown) {\n        lastCodeBlockDropdown = dropdown.element;\n\n        // New code block dropdown appeared, sync it\n        const targetLang = LANGUAGE_MAP[currentSelectedLanguage];\n        if (targetLang && dropdown.language !== targetLang) {\n          onNextFrame(() => selectCodeBlockLanguage(targetLang));\n        }\n      }\n    });\n\n    observer.observe(document.body, {\n      subtree: true,\n      childList: true\n    });\n  }\n  \n  // ============================================\n  // INITIALIZATION\n  // ============================================\n\n  function init() {\n    setupMenuClickHandler();\n    setupDropdownMenuObserver();\n    setupPageChangeObserver();\n    setupCodeBlockObserver();\n\n    restoreLanguageSelection();\n    updateVersionSwitcherVisibility();\n    updateSDKReferenceVisibility();\n  }\n\n  // Initialize on page load\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', init);\n  } else {\n    init();\n  }\n\n  // Re-run when URL changes (SPA navigation)\n  let lastUrl = location.href;\n  const urlObserver = new MutationObserver(() => {\n    if (location.href !== lastUrl) {\n      lastUrl = location.href;\n      // Remove processed class so SDK items get re-evaluated\n      document.querySelectorAll('li[id^=\"/v3/sdk/\"].stagehand-sdk-processed').forEach(item => {\n        item.classList.remove('stagehand-sdk-processed');\n      });\n      onNextFrame(() => {\n        restoreLanguageSelection();\n        syncCodeBlockLanguage();\n        updateVersionSwitcherVisibility();\n        updateSDKReferenceVisibility();\n      });\n    }\n  });\n  urlObserver.observe(document.body, { subtree: true, childList: true });\n})();\n"
  },
  {
    "path": "packages/docs/package.json",
    "content": "{\n  \"name\": \"@browserbasehq/stagehand-docs\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"type\": \"module\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"dev\": \"mintlify dev --no-open --port 3002\",\n    \"upgrade\": \"mintlify upgrade\",\n    \"sync-sdk\": \"node scripts/sync-sdk-docs.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"mintlify\": \"^4.2.47\",\n    \"zod\": \"^4.2.1\"\n  },\n  \"packageManager\": \"pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c\"\n}\n"
  },
  {
    "path": "packages/docs/scripts/runtimePaths.js",
    "content": "/**\n * Keep this file in sync with:\n * - /packages/core/lib/v3/runtimePaths.ts\n * - /packages/server-v3/scripts/runtimePaths.ts\n * - /packages/server-v4/scripts/runtimePaths.ts\n * - /packages/evals/runtimePaths.ts\n * - /packages/docs/scripts/runtimePaths.js\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst PACKAGE_SEGMENT = \"/packages/docs/\";\nconst EVAL_FRAMES = new Set([\"[eval]\", \"[eval]-wrapper\"]);\nconst INTERNAL_FRAME_NAMES = new Set([\n  \"readCallsites\",\n  \"readCallsitePath\",\n  \"resolveCallerFilePath\",\n  \"getCurrentFilePath\",\n  \"getCurrentDirPath\",\n  \"getRepoRootDir\",\n  \"isMainModule\",\n]);\n\nconst normalizePath = (value) => {\n  const input = value.startsWith(\"file://\") ? fileURLToPath(value) : value;\n  return path.resolve(input).replaceAll(\"\\\\\", \"/\");\n};\n\nconst readCallsites = () => {\n  const previousPrepare = Error.prepareStackTrace;\n  try {\n    Error.prepareStackTrace = (_, stack) => stack;\n    return new Error().stack ?? [];\n  } finally {\n    Error.prepareStackTrace = previousPrepare;\n  }\n};\n\nconst readCallsitePath = (callsite) => {\n  const rawPath =\n    callsite.getFileName?.() ?? callsite.getScriptNameOrSourceURL?.();\n  if (!rawPath) return null;\n  if (rawPath.startsWith(\"node:\")) return null;\n  if (EVAL_FRAMES.has(rawPath)) return null;\n  return normalizePath(rawPath);\n};\n\nconst isInternalCallsite = (callsite) => {\n  const functionName = callsite.getFunctionName?.();\n  if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) return true;\n\n  const methodName = callsite.getMethodName?.();\n  if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) return true;\n\n  const callsiteString = callsite.toString?.() ?? \"\";\n  for (const frameName of INTERNAL_FRAME_NAMES) {\n    if (callsiteString.includes(`${frameName} (`)) return true;\n    if (callsiteString.includes(`.${frameName} (`)) return true;\n  }\n  return false;\n};\n\nconst resolveCallerFilePath = () => {\n  const packageCandidates = [];\n  const fallbackCandidates = [];\n\n  for (const callsite of readCallsites()) {\n    const filePath = readCallsitePath(callsite);\n    if (!filePath) continue;\n    if (isInternalCallsite(callsite)) continue;\n    if (filePath.includes(PACKAGE_SEGMENT)) {\n      packageCandidates.push(filePath);\n      continue;\n    }\n    fallbackCandidates.push(filePath);\n  }\n\n  const packageCandidate = packageCandidates[0];\n  if (packageCandidate) return packageCandidate;\n\n  const fallbackCandidate = fallbackCandidates[0];\n  if (fallbackCandidate) return fallbackCandidate;\n\n  throw new Error(\"Unable to resolve caller file path.\");\n};\n\nexport const getCurrentFilePath = () => resolveCallerFilePath();\n\nexport const getCurrentDirPath = () => path.dirname(getCurrentFilePath());\n\nexport const getRepoRootDir = () => {\n  const currentFilePath = getCurrentFilePath();\n  const index = currentFilePath.lastIndexOf(PACKAGE_SEGMENT);\n  if (index === -1) {\n    throw new Error(\n      `Unable to determine repo root from ${currentFilePath} (missing ${PACKAGE_SEGMENT}).`,\n    );\n  }\n  return currentFilePath.slice(0, index);\n};\n\nexport const isMainModule = () => {\n  const entryScript = process.argv.at(1);\n  if (!entryScript) return false;\n  return normalizePath(entryScript) === getCurrentFilePath();\n};\n"
  },
  {
    "path": "packages/docs/scripts/sync-sdk-docs.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Script to sync SDK documentation from GitHub READMEs\n * \n * Usage: node scripts/sync-sdk-docs.js\n * \n * This script fetches README.md files from each language SDK repo\n * and generates MDX files for the docs.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport https from \"node:https\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nconst currentDir = getCurrentDirPath();\n\n// SDK repos configuration\nconst SDK_REPOS = {\n  java: {\n    repo: 'browserbase/stagehand-java',\n    title: 'Java SDK',\n    description: 'Official Stagehand SDK for Java',\n    outputPath: 'v3/sdk/java.mdx'\n  },\n  python: {\n    repo: 'browserbase/stagehand-python',\n    title: 'Python SDK',\n    description: 'Official Stagehand SDK for Python',\n    outputPath: 'v3/sdk/python.mdx'\n  },\n  ruby: {\n    repo: 'browserbase/stagehand-ruby',\n    title: 'Ruby SDK',\n    description: 'Official Stagehand SDK for Ruby',\n    outputPath: 'v3/sdk/ruby.mdx'\n  },\n  go: {\n    repo: 'browserbase/stagehand-go',\n    title: 'Go SDK',\n    description: 'Official Stagehand SDK for Go',\n    outputPath: 'v3/sdk/go.mdx'\n  }\n};\n\n/**\n * Fetch content from a URL\n */\nfunction fetchUrl(url) {\n  return new Promise((resolve, reject) => {\n    https.get(url, {\n      headers: {\n        'User-Agent': 'Stagehand-Docs-Sync'\n      }\n    }, (res) => {\n      // Handle redirects\n      if (res.statusCode === 301 || res.statusCode === 302) {\n        fetchUrl(res.headers.location).then(resolve).catch(reject);\n        return;\n      }\n      \n      if (res.statusCode !== 200) {\n        reject(new Error(`HTTP ${res.statusCode}: ${url}`));\n        return;\n      }\n      \n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => resolve(data));\n      res.on('error', reject);\n    }).on('error', reject);\n  });\n}\n\n/**\n * Process README content for MDX compatibility\n */\nfunction processReadmeContent(content, config) {\n  let processed = content;\n  \n  // Remove HTML comments\n  processed = processed.replace(/<!--[\\s\\S]*?-->/g, '');\n  \n  // Remove entire HTML blocks with picture/source tags (badge sections)\n  processed = processed.replace(/<div[^>]*>[\\s\\S]*?<\\/div>/gi, '');\n  processed = processed.replace(/<p[^>]*align[^>]*>[\\s\\S]*?<\\/p>/gi, '');\n  processed = processed.replace(/<picture>[\\s\\S]*?<\\/picture>/gi, '');\n  \n  // Remove standalone HTML tags\n  processed = processed.replace(/<a[^>]*>[\\s]*<img[^>]*>[\\s]*<\\/a>/gi, '');\n  processed = processed.replace(/<img[^>]*badge[^>]*>/gi, '');\n  processed = processed.replace(/<img[^>]*shields\\.io[^>]*>/gi, '');\n  processed = processed.replace(/<a[^>]*>\\s*<picture>[\\s\\S]*?<\\/picture>\\s*<\\/a>/gi, '');\n  \n  // Remove badge images in markdown format\n  processed = processed.replace(/^\\s*(\\[!\\[.*?\\]\\(.*?\\)\\]\\(.*?\\)\\s*)+/gm, '');\n  processed = processed.replace(/^\\s*!\\[.*?\\]\\(https:\\/\\/.*?badge.*?\\)\\s*/gm, '');\n  processed = processed.replace(/\\[!\\[.*?\\]\\(.*?badge.*?\\)\\]\\(.*?\\)/g, '');\n  \n  // Remove standalone anchor img tags\n  processed = processed.replace(/<a[^>]*href[^>]*><img[^>]*><\\/a>/gi, '');\n  \n  // Clean up <code> tags with backticks inside (common in Go docs)\n  processed = processed.replace(/<code>\\\\`([^`]*?)\\\\`<\\/code>/g, '`$1`');\n  processed = processed.replace(/<code>`([^`]*?)`<\\/code>/g, '`$1`');\n  processed = processed.replace(/<code>([^<]*?)<\\/code>/g, '`$1`');\n  \n  // Fix malformed links with parentheses in URL (Go docs issue)\n  processed = processed.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\(([^)]+)\\)([^)]*)\\)/g, '[$1]($2)');\n  \n  // Convert relative links to absolute GitHub links\n  const repoUrl = `https://github.com/${config.repo}`;\n  processed = processed.replace(/\\]\\((?!http)(?!#)(?!mailto)([^)]+)\\)/g, `](${repoUrl}/blob/main/$1)`);\n  \n  // Fix code block language hints for MDX\n  processed = processed.replace(/```kotlin/g, '```java');\n  \n  // Remove the first H1 if it exists (we'll add our own title)\n  processed = processed.replace(/^#\\s+.*\\n+/, '');\n  \n  // Clean up excessive newlines\n  processed = processed.replace(/\\n{4,}/g, '\\n\\n\\n');\n  \n  // Remove any remaining inline HTML img tags\n  processed = processed.replace(/<img[^>]*>/gi, '');\n  \n  // Remove any remaining <a> tags that are empty or just whitespace\n  processed = processed.replace(/<a[^>]*>\\s*<\\/a>/gi, '');\n  \n  // Clean up lines that are just whitespace\n  processed = processed.replace(/^\\s+$/gm, '');\n  \n  return processed.trim();\n}\n\n/**\n * Generate MDX frontmatter\n */\nfunction generateFrontmatter(config) {\n  return `---\ntitle: \"${config.title}\"\ndescription: \"${config.description}\"\n---\n\n<Note>\n  This documentation is automatically synced from the [${config.title} GitHub repository](https://github.com/${config.repo}).\n</Note>\n\n`;\n}\n\n/**\n * Sync a single SDK's documentation\n */\nasync function syncSdk(language, config) {\n  const rawUrl = `https://raw.githubusercontent.com/${config.repo}/main/README.md`;\n  \n  console.log(`Fetching ${language} SDK docs from ${rawUrl}...`);\n  \n  try {\n    const readme = await fetchUrl(rawUrl);\n    const processedContent = processReadmeContent(readme, config);\n    const frontmatter = generateFrontmatter(config);\n    const mdxContent = frontmatter + processedContent;\n    \n    // Ensure directory exists\n    const outputDir = path.dirname(`${currentDir}/../${config.outputPath}`);\n    if (!fs.existsSync(outputDir)) {\n      fs.mkdirSync(outputDir, { recursive: true });\n    }\n    \n    // Write MDX file\n    const outputFile = `${currentDir}/../${config.outputPath}`;\n    fs.writeFileSync(outputFile, mdxContent, 'utf8');\n    \n    console.log(`✓ ${language} SDK docs written to ${config.outputPath}`);\n    return true;\n  } catch (error) {\n    console.error(`✗ Failed to sync ${language} SDK: ${error.message}`);\n    return false;\n  }\n}\n\n/**\n * Main function\n */\nasync function main() {\n  console.log('Syncing SDK documentation from GitHub...\\n');\n  \n  const results = await Promise.all(\n    Object.entries(SDK_REPOS).map(([lang, config]) => syncSdk(lang, config))\n  );\n  \n  const successCount = results.filter(Boolean).length;\n  const totalCount = results.length;\n  \n  console.log(`\\nDone! ${successCount}/${totalCount} SDKs synced successfully.`);\n  \n  if (successCount < totalCount) {\n    process.exit(1);\n  }\n}\n\nmain().catch(error => {\n  console.error('Fatal error:', error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/docs/snippets/excalidraw.mdx",
    "content": "export const Excalidraw = ({ url, className = \"w-full\" }) => {\n\treturn (\n\t\t<>\n\t\t\t<div className=\"dark:hidden\" >\n\t\t\t\t<iframe\n\t\t\t\t\tsrc={url + `?darkMode=false`}\n\t\t\t\t\tclassName={className}\n\t\t\t\t\tallowFullScreen\n\t\t\t\t></iframe>\n\t\t\t</div>\n\n\t\t\t<div className=\"hidden dark:block\">\n\t\t\t\t<iframe\n\t\t\t\t\tsrc={url + `?darkMode=true`}\n\t\t\t\t\tclassName={className}\n\t\t\t\t\tallowFullScreen\n\t\t\t\t></iframe>\n\t\t\t</div>\n\t\t</>\n\t)\n}"
  },
  {
    "path": "packages/docs/snippets/v3-banner.mdx",
    "content": "{/* \n  V3Banner - Currently a no-op component\n  \n  This component is imported across 50+ pages in v3 docs.\n  Keeping it as a no-op rather than removing allows us to easily \n  add a new banner message in the future without editing every file.\n  \n  To add a banner, replace the null return with your JSX content.\n*/}\nexport const V3Banner = () => null;\n"
  },
  {
    "path": "packages/docs/v2/basics/act.mdx",
    "content": "---\ntitle: Act\ndescription: 'Interact with a web page'\n---\n\n## What is `act()`?\n``` typescript\npage.act(\"click on add to cart\")\n```\n`act` enables Stagehand to perform **individual** actions on a web page. Use it to build self-healing and deterministic automations that adapt to website changes. \n\n## Why use `act()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Natural Language Instructions\" icon=\"wand-magic-sparkles\" href=\"#using-act\">\n    Write automation in plain English. No selectors or complex syntax.\n  </Card>\n  <Card title=\"Precise Control\" icon=\"crosshairs\" href=\"#best-practices\">\n    Build automations step by step. Define exactly what happens at every moment.\n  </Card>\n  <Card title=\"Self-Healing\" icon=\"bandage\" href=\"#ensure-reliable-actions\">\n    Actions automatically adapt when websites change.\n  </Card>\n  <Card title=\"Caching\" icon=\"repeat\" href=\"#reduce-model-costs\">\n    Cache actions to avoid LLM calls and ensure consistent execution across runs.\n  </Card>\n</CardGroup>\n\n## Using `act()`\n\nUse `act` to perform single actions in your automation. Here's how to click a button:\n\n<CodeGroup>\n```typescript TypeScript\nawait page.goto(\"https://example-store.com\");\nawait page.act(\"click the add to cart button\");\n```\n\n```python Python\nawait page.goto(\"https://example-store.com\")\nawait page.act(\"click the add to cart button\")\n```\n</CodeGroup>\n\nWith `act`, breaking complex actions into small, single-step actions works best. If you need to orchestrate multi-step flows, use multiple `act` commands or `agent`.\n\n<Accordion title=\"Suggested actions\">\n\n| Action | Example instruction |\n|--------|---------------------|\n| Click | `click the button` |\n| Fill | `fill the field with <value>` |\n| Type | `type <text> into the search box` |\n| Press | `press <key> in the search field` |\n| Scroll | `scroll to <position>` |\n| Select from dropdown | `select <value> from the dropdown` |\n</Accordion>\n\n<Tabs>\n<Tab title=\"Do this\">\nBreak your task into single-step actions.\n\n<CodeGroup>\n```typescript TypeScript\n// Break it into single-step actions\nawait page.act(\"open the filters panel\");\nawait page.act(\"choose 4-star rating\");\nawait page.act(\"click the apply button\");\n```\n\n```python Python\n# Break it into single-step actions\nawait page.act(\"open the filters panel\")\nawait page.act(\"choose 4-star rating\")\nawait page.act(\"click the apply button\")\n```\n</CodeGroup>\n</Tab>\n\n<Tab title=\"Don't do this\">\nFor multi-step tasks, use [`agent()`](/v2/basics/agent) instead.\n\n<CodeGroup>\n```typescript TypeScript\n// Too complex - trying to do multiple things at once\nawait page.act(\"open the filters panel, choose 4-star rating, and click apply\");\n```\n\n```python Python\n# Too complex - trying to do multiple things at once\nawait page.act(\"open the filters panel, choose 4-star rating, and click apply\")\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n### Advanced Configuration\n\nFor advanced scenarios, you can configure additional options:\n\n<CodeGroup>\n```typescript TypeScript\n// Dynamic food search with advanced options\nconst foodItem = \"organic quinoa\";\n\nawait page.act({\n  action: \"Type %foodItem% in the search box and press enter\",\n  variables: {\n    foodItem: foodItem\n  },\n  modelName: \"google/gemini-2.5-pro\",\n  modelClientOptions: {\n    modelApiKey: process.env.GOOGLE_API_KEY,\n  },\n  iframes: true, // Search within iframes if needed\n  domSettleTimeoutMs: 45000, // Wait longer for dynamic content\n  timeoutMs: 60000 // Extended timeout for slow-loading forms\n});\n```\n\n```python Python\n# Dynamic food search with advanced options\nfood_item = \"organic quinoa\"\n\nawait page.act({\n  \"action\": \"Type %foodItem% in the search box and press enter\",\n  \"variables\": {\n    \"foodItem\": food_item\n  },\n  \"modelName\": \"google/gemini-2.5-pro\",\n  \"modelClientOptions\": {\n    \"modelApiKey\": os.environ.get(\"GOOGLE_API_KEY\")\n  },\n  \"iframes\": True, # Search within iframes if needed\n  \"domSettleTimeoutMs\": 45000, # Wait longer for dynamic content\n  \"timeoutMs\": 60000 # Extended timeout for slow-loading forms\n})\n```\n</CodeGroup>\n  \n<Note>\nShadow DOM support is now available! Set `experimental: true` in your Stagehand configuration to enable it. See the [configuration guide](/v2/configuration/browser) for more details.\n</Note>\n\n\n\n\n\n## Best practices\n\n### Ensure reliable actions\n\nUse `observe()` to discover candidate actions on the current page and plan reliably. It returns a list of suggested actions (with selector, description, method, and arguments). You can pass an observed action directly to `act` to execute it.\n\n<CodeGroup>\n```typescript TypeScript\nconst [action] = await page.observe(\"click the login button\");\n\nif (action) {\n  await page.act(action);\n}\n```\n\n```python Python\nresults = await page.observe(\"click the login button\")\n\nif results:\n    await page.act(results[0])\n```\n</CodeGroup>\n\n<Card title=\"Analyze pages with observe()\" icon=\"magnifying-glass\" iconType=\"sharp-solid\" href=\"/v2/basics/observe\">\n  Plan actions with `observe()` before executing with `act`.\n</Card>\n\n### Reduce model costs\n\nCache observed actions to avoid repeated LLM calls and ensure consistent execution.\n\n<CodeGroup>\n```typescript TypeScript\n// Cost-optimized actions with caching\nconst actionCache = new Map();\n\nconst getCachedAction = async (instruction: string) => {\n  if (actionCache.has(instruction)) {\n    return actionCache.get(instruction);\n  }\n  \n  const [action] = await page.observe(instruction);\n  actionCache.set(instruction, action);\n  return action;\n};\n\n// Reuse cached actions\nconst loginAction = await getCachedAction(\"click the login button\");\nawait page.act(loginAction);\n```\n\n```python Python\n# Cost-optimized actions with caching\naction_cache = {}\n\nasync def get_cached_action(instruction: str):\n    if instruction in action_cache:\n        return action_cache[instruction]\n    \n    results = await page.observe(instruction)\n    if results:\n        action = results[0]\n        action_cache[instruction] = action\n        return action\n    \n    return None\n\n# Reuse cached actions\nlogin_action = await get_cached_action(\"click the login button\")\nif login_action:\n    await page.act(login_action)\n```\n</CodeGroup>\n\n<Card title=\"Complete caching guide\" icon=\"database\" iconType=\"sharp-solid\" href=\"/v2/best-practices/caching\">\n  Learn advanced caching techniques and patterns for optimal performance.\n</Card>\n\n### Secure your automations\n\nVariables will not be shared with LLM providers. Use them for passwords, API keys, and other sensitive data.\n\n\n<Note>\nLoad sensitive data from environment variables using `.env` files. Never hardcode API keys, passwords, or other secrets directly in your code.\n</Note>\n\n<CodeGroup>\n```typescript TypeScript\nawait page.act({\n  action: \"enter %username% in the email field\",\n  variables: {\n    username: \"user@example.com\"\n  }\n});\n\nawait page.act({\n  action: \"enter %password% in the password field\",\n  variables: {\n    password: process.env.USER_PASSWORD\n  }\n});\n```\n\n```python Python\n# If using Python, set `use_api: true` in your Stagehand configuration\n\nawait page.act(\n  \"enter %username% in the email field\",\n  variables={\n      \"username\": \"user@example.com\"\n  }\n)\n\nawait page.act(\n  \"enter %password% in the password field\",\n  variables={\n      \"password\": os.environ.get(\"USER_PASSWORD\")\n  }\n)\n```\n</CodeGroup>\n\n<Warning>\nWhen handling sensitive data, set `verbose: 0` in your Stagehand configuration to prevent secrets from appearing in logs. See the [configuration guide](/v2/configuration/browser) for more details.\n</Warning>\n\n<Card title=\"User Data Best Practices\" icon=\"shield-check\" iconType=\"sharp-solid\" href=\"/v2/best-practices/user-data\">\n  Complete guide to securing your browser automations with best practices and configurations.\n</Card>\n\n## Troubleshooting\n\n<AccordionGroup>\n\n\n<Accordion title=\"Method not supported\">\n**Problem**: `act` fails with \"method not supported\" error\n\n**Solutions**:\n- Use clear and detailed instructions for what you want to accomplish\n- Review our [evals](https://stagehand.dev/evals) to find the best models for your use case\n- Use [`observe()`](/v2/basics/observe) and verify the resulting action is within a list of expected actions\n\n**Solution 1: Validate with observe**\n\n<CodeGroup>\n```typescript TypeScript\nconst prompt = \"click the submit button\";\nconst expectedMethod = \"click\";\n\ntry {\n  await page.act(prompt);\n} catch (error) {\n  if (error.message.includes(\"method not supported\")) {\n    // Observe the same prompt to get the planned action\n    const [action] = await page.observe(prompt);\n    \n    if (action && action.method === expectedMethod) {\n      await page.act(action);\n    } else {\n      throw new Error(`Unsupported method: expected \"${expectedMethod}\", got \"${action?.method}\"`);\n    }\n  } else {\n    throw error;\n  }\n}\n```\n\n```python Python\nprompt = \"click the submit button\"\nexpected_method = \"click\"\n\ntry:\n    await page.act(prompt)\nexcept Exception as error:\n    if \"method not supported\" in str(error):\n        # Observe the same prompt to get the planned action\n        results = await page.observe(prompt)\n        \n        if results and results[0].method == expected_method:\n            await page.act(results[0])\n        else:\n            method = results[0].method if results else \"unknown\"\n            raise Exception(f'Unsupported method: expected \"{expected_method}\", got \"{method}\"')\n    else:\n        raise error\n```\n</CodeGroup>\n\n**Solution 2: Retry with exponential backoff**\n\n<CodeGroup>\n```typescript TypeScript\n// Retry with exponential backoff for intermittent issues\nconst prompt = \"click the submit button\";\nconst maxRetries = 3;\n\nfor (let attempt = 0; attempt <= maxRetries; attempt++) {\n  try {\n    await page.act(prompt, { timeoutMs: 10000 + (attempt * 5000) });\n    break; // Success, exit retry loop\n  } catch (error) {\n    if (error.message.includes(\"method not supported\") && attempt < maxRetries) {\n      // Exponential backoff: wait 2^attempt seconds\n      const delay = Math.pow(2, attempt) * 1000;\n      console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);\n      await new Promise(resolve => setTimeout(resolve, delay));\n    } else {\n      throw error;\n    }\n  }\n}\n```\n\n```python Python\n# Retry with exponential backoff for intermittent issues\nimport asyncio\n\nprompt = \"click the submit button\"\nmax_retries = 3\n\nfor attempt in range(max_retries + 1):\n    try:\n        timeout = 10000 + (attempt * 5000)\n        await page.act(prompt, {\"timeoutMs\": timeout})\n        break  # Success, exit retry loop\n    except Exception as error:\n        if \"method not supported\" in str(error) and attempt < max_retries:\n            # Exponential backoff: wait 2^attempt seconds\n            delay = 2 ** attempt\n            print(f\"Retry {attempt + 1}/{max_retries} after {delay}s\")\n            await asyncio.sleep(delay)\n        else:\n            raise error\n```\n</CodeGroup>\n\n</Accordion>\n\n<Accordion title=\"Action failed or timed out\">\n**Problem**: `act` times out or fails to complete action (often due to element not found)\n\n**Solutions**:\n- Ensure page has fully loaded\n- Check if content is in iframes: [Learn more about working with iframes](/v2/best-practices/working-with-iframes)\n- Increase action timeout\n- Use `observe()` first to verify element exists\n\n<CodeGroup>\n```typescript TypeScript\n// Handle timeout and element not found issues\ntry {\n  await page.act(\"click the submit button\", { timeout: 30000 });\n} catch (error) {\n  // Check if page is fully loaded\n  await page.waitForLoadState('domcontentloaded');\n  \n  // Use observe to check element state\n  const [element] = await page.observe(\"find the submit button\");\n  \n  if (element) {\n    console.log(\"Element found, trying more specific instruction\");\n    await page.act(\"click the submit button at the bottom of the form\");\n  } else {\n    console.log(\"Element not found, trying alternative selector\");\n    await page.act(\"click the button with text 'Submit'\");\n  }\n}\n```\n\n```python Python\n# Handle timeout and element not found issues\ntry:\n    await page.act(\"click the submit button\", {\"timeout\": 30000})\nexcept Exception as error:\n    # Check if page is fully loaded\n    await page.wait_for_load_state('domcontentloaded')\n    \n    # Use observe to check element state\n    results = await page.observe(\"find the submit button\")\n    \n    if results:\n        print(\"Element found, trying more specific instruction\")\n        await page.act(\"click the submit button at the bottom of the form\")\n    else:\n        print(\"Element not found, trying alternative selector\")\n        await page.act(\"click the button with text 'Submit'\")\n```\n</CodeGroup>\n</Accordion>\n\n<Accordion title=\"Incorrect element selected\">\n**Problem**: `act` performs action on wrong element\n\n**Solutions**:\n- Be more specific in instructions: include visual cues, position, or context\n- Use `observe()` to preview which element will be selected\n- Add contextual information: \"the search button in the header\"\n- Use unique identifiers when available\n\n<CodeGroup>\n```typescript TypeScript\n// More precise element targeting\n// Instead of:\nawait page.act(\"click the button\");\n\n// Use specific context:\nawait page.act(\"click the red 'Delete' button next to the user John Smith\");\n\n// Or preview with observe first:\nconst [action] = await page.observe(\"click the submit button in the checkout form\");\nif (action.description.includes(\"checkout\")) {\n  await page.act(action);\n}\n```\n\n```python Python\n# More precise element targeting\n# Instead of:\nawait page.act(\"click the button\")\n\n# Use specific context:\nawait page.act(\"click the red 'Delete' button next to the user John Smith\")\n\n# Or preview with observe first:\nresults = await page.observe(\"click the submit button in the checkout form\")\nif results and \"checkout\" in results[0].description:\n    await page.act(results[0])\n```\n</CodeGroup>\n</Accordion>\n\n\n\n</AccordionGroup>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n  <Card title=\"Orchestrate complex workflows with Agent\" icon=\"robot\" iconType=\"sharp-solid\" href=\"/v2/basics/agent\">\n    Use `Agent` to autonomously execute multi-step tasks and complex workflows.\n  </Card>\n\n   <Card title=\"Caching actions\" icon=\"bolt\" iconType=\"sharp-solid\" href=\"/v2/best-practices/caching\">\n    Speed up repeated automations by caching actions.\n  </Card>\n\n  <Card title=\"Extract data with extract()\" icon=\"table\" iconType=\"sharp-solid\" href=\"/v2/basics/extract\">\n    Use `extract` with a data schema to pull clean, typed data from any page.\n  </Card>\n\n  <Card title=\"Working with iframes\" icon=\"frame\" iconType=\"sharp-solid\" href=\"/v2/best-practices/working-with-iframes\">\n    Learn best practices for interacting with elements inside iframes.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/basics/agent.mdx",
    "content": "---\ntitle: Agent\ndescription: 'Automate complex workflows with AI powered browser agents'\n---\n\n## What is `agent()?`\n\n``` typescript\nagent.execute(\"apply for a job at browserbase\")\n```\n`agent` turns high level tasks into **fully autonomous** browser workflows. You can customize the agent by specifying the LLM provider and model, setting custom instructions for behavior, and configuring max steps.\n\n<img src=\"/images/agent.gif\" alt=\"Agent\" />\n\n## Why use `agent()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Multi-Step Workflows\" icon=\"route\" href=\"#agent-execution-configuration\">\n    Execute complex sequences automatically.\n  </Card>\n  <Card title=\"Visual Understanding\" icon=\"eye\" href=\"/v2/best-practices/computer-use\">\n    Sees and understands web interfaces like humans do using computer vision.\n  </Card>\n</CardGroup>\n\n\n## Using `agent()`\n\nThere are two ways to create agents in Stagehand:\n\n### Computer Use Agents\n\nUse computer use agents with specialized models from OpenAI or Anthropic: \n\n<CodeGroup>\n```typescript TypeScript\nconst agent = stagehand.agent({\n  provider: \"anthropic\",\n  model: \"claude-sonnet-4-20250514\",\n  instructions: \"You are a helpful assistant that can use a web browser.\",\n  options: {\n    apiKey: process.env.ANTHROPIC_API_KEY,\n  },\n});\nawait agent.execute(\"apply for a job at Browserbase\")\n```\n\n```python Python\nagent = stagehand.agent(\n    model=\"claude-sonnet-4-20250514\",\n    instructions=\"You are a helpful assistant that can use a web browser.\",\n    options={\n        \"api_key\": os.getenv(\"ANTHROPIC_API_KEY\"),\n    },\n)\nawait agent.execute(\"apply for a job at Browserbase\")\n```\n</CodeGroup>\n\n<Callout icon=\"code\" color=\"#6ec202\" iconType=\"regular\">View or run the example template [here](https://www.browserbase.com/templates/gemini-cua)</Callout>\n\n### Use Stagehand Agent with Any LLM\n\nUse the agent without specifying a provider to utilize any model or LLM provider:\n\n<Note>Non CUA agents are currently only supported in TypeScript</Note>\n\n```typescript TypeScript\nconst agent = stagehand.agent();\nawait agent.execute(\"apply for a job at Browserbase\")\n```\n\n\n## MCP Integrations\n\nAgents can be enhanced with external tools and services through MCP (Model Context Protocol) integrations. This allows your agent to access external APIs and data sources beyond just browser interactions.\n\n<CodeGroup>\n```typescript TypeScript (Pass URL)\nconst agent = stagehand.agent({\n  provider: \"openai\",\n  model: \"computer-use-preview\",\n  integrations: [\n    `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n  ],\n  instructions: `You have access to web search through Exa. Use it to find current information before browsing.`,\n  options: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n\nawait agent.execute(\"Search for the best headphones of 2025 and go through checkout for the top recommendation\");\n```\n\n```typescript TypeScript (Create Connection)\nimport { connectToMCPServer } from \"@browserbasehq/stagehand\";\n\nconst supabaseClient = await connectToMCPServer(\n  `https://server.smithery.ai/@supabase-community/supabase-mcp/mcp?api_key=${process.env.SMITHERY_API_KEY}`\n);\n\nconst agent = stagehand.agent({\n  provider: \"openai\",\n  model: \"computer-use-preview\",\n  integrations: [supabaseClient],\n  instructions: `You can interact with Supabase databases. Use these tools to store and retrieve data.`,\n  options: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n\nawait agent.execute(\"Search for restaurants and save the first result to the database\");\n```\n</CodeGroup>\n\n<Tip>\nMCP integrations enable agents to be more powerful by combining browser automation with external APIs, databases, and services. The agent can intelligently decide when to use browser actions versus external tools.\n</Tip>\n\n<Warning>\nStagehand uses a 1288x711 viewport by default (the optimal size for Computer Use Agents). Other viewport sizes may reduce performance. If you need to modify the viewport, you can edit in the [Browser Configuration](/v2/configuration/browser).\n</Warning>\n\n\n## Available Models\n\nUse specialized computer use models (e.g., `computer-use-preview` from OpenAI or `claude-sonnet-4-20250514` from Anthropic)\n\n<Card title=\"Available Models\" icon=\"robot\" href=\"/v2/configuration/models\">\n  Check out the guide on how to use different models with Stagehand.\n</Card>\n\n## Agent Execution Configuration\n\nControl the maximum number of steps the agent can take to complete the task using the `maxSteps` parameter.\n\n<CodeGroup>\n```typescript TypeScript\n// Set maxSteps to control how many actions the agent can take\nawait agent.execute({\n  instruction: \"Sign me up for a library card\",\n  maxSteps: 15 // Agent will stop after 15 steps if task isn't complete\n});\n```\n\n```python Python\n# Set max_steps to control how many actions the agent can take\nresult = await agent.execute({\n    \"instruction\": \"Sign me up for a library card\",\n    \"max_steps\": 15  # Agent will stop after 15 steps if task isn't complete\n})\n```\n</CodeGroup>\n\nFor complex tasks, increase the `maxSteps` limit and check task success.\n\n<CodeGroup>\n```typescript TypeScript\n// Complex multi-step task requiring more actions\nconst result = await agent.execute({\n  instruction: \"Find and apply for software engineering jobs, filtering by remote work and saving 3 applications\",\n  maxSteps: 30, // Higher limit for complex workflows\n});\n\n// Check if the task completed successfully\nif (result.success === true) {\n  console.log(\"Task completed successfully!\");\n} else {\n  console.log(\"Task failed or was incomplete\");\n}\n```\n\n```python Python\n# Complex multi-step task requiring more actions\nresult = await agent.execute({\n    \"instruction\": \"Find and apply for software engineering jobs, filtering by remote work and saving 3 applications\",\n    \"max_steps\": 30  # Higher limit for complex workflows\n})\n\n# Check if the task completed successfully\nif result.success == True:\n    print(\"Task completed successfully!\")\nelse:\n    print(\"Task failed or was incomplete\")\n```\n</CodeGroup>\n\n## Best Practices\n\nFollowing these best practices will improve your agent's success rate, reduce execution time, and minimize unexpected errors during task completion.\n\n### Start on the Right Page\nNavigate to your target page before executing tasks:\n\n<Tabs>\n<Tab title=\"Do this\">\n<CodeGroup>\n```typescript TypeScript\nawait page.goto('https://github.com/browserbase/stagehand');\nawait agent.execute('Get me the latest PR on the stagehand repo');\n```\n\n```python Python\nawait page.goto(\"https://github.com/browserbase/stagehand\")\nresult = await agent.execute(\"Get me the latest PR on the stagehand repo\")\n```\n</CodeGroup>\n</Tab>\n\n<Tab title=\"Don't do this\">\n<CodeGroup>\n```typescript TypeScript\nawait agent.execute('Go to GitHub and find the latest PR on browserbase/stagehand');\n```\n\n```python Python\nresult = await agent.execute(\"Go to GitHub and find the latest PR on browserbase/stagehand\")\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n\n### Be Specific\nProvide detailed instructions for better results:\n\n<Tabs>\n<Tab title=\"Do this\">\n<CodeGroup>\n```typescript TypeScript\nawait agent.execute(\"Find Italian restaurants in Brooklyn that are open after 10pm and have outdoor seating\");\n```\n\n```python Python\nresult = await agent.execute(\"Find Italian restaurants in Brooklyn that are open after 10pm and have outdoor seating\")\n```\n</CodeGroup>\n</Tab>\n\n<Tab title=\"Don't do this\">\n<CodeGroup>\n```typescript TypeScript\nawait agent.execute(\"Find a restaurant\");\n```\n\n```python Python\nresult = await agent.execute(\"Find a restaurant\")\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n## Troubleshooting\n\n<AccordionGroup>\n\n\n<Accordion title=\"Agent is stopping before completing the task\">\n**Problem**: Agent stops before finishing the requested task\n\n**Solutions**:\n- Check if the agent is hitting the maxSteps limit (default is 20)\n- Increase maxSteps for complex tasks: `maxSteps: 30` or higher\n- Break very complex tasks into smaller sequential executions\n\n```typescript\n// Increase maxSteps for complex tasks\nawait agent.execute({\n  instruction: \"Complete the multi-page registration form with all required information\",\n  maxSteps: 40 // Increased limit for complex task\n});\n\n// Or break into smaller tasks with success checking\nconst firstResult = await agent.execute({\n  instruction: \"Fill out page 1 of the registration form\", \n  maxSteps: 15\n});\n\n// Only proceed if the first task was successful\nif (firstResult.success === true) {\n  await agent.execute({\n    instruction: \"Navigate to page 2 and complete remaining fields\",\n    maxSteps: 15\n  });\n} else {\n  console.log(\"First task failed, stopping execution\");\n}\n```\n</Accordion>\n\n<Accordion title=\"Agent is failing to click the proper elements\">\n**Problem**: Agent clicks on wrong elements or fails to interact with the correct UI components\n\n**Solutions**:\n- Ensure proper viewport size: Stagehand uses `1288x711` by default (optimal for Computer Use models)\n- Avoid changing viewport dimensions as other sizes may reduce performance\n</Accordion>\n\n\n</AccordionGroup>\n\n\n## Next steps\n\n<CardGroup cols={2}>\n<Card title=\"Act\" icon=\"play\" href=\"/v2/basics/act\">\n  Execute actions efficiently using observe results\n</Card>\n\n<Card title=\"Extract\" icon=\"download\" href=\"/v2/basics/extract\">\n  Extract structured data from observed elements\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/basics/extract.mdx",
    "content": "---\ntitle: Extract\ndescription: Extract structured data from a webpage\n---\n\n## What is `extract()`?\n\n```typescript\npage.extract(\"extract the name of the repository\");\n```\n\n`extract` grabs structured data from a webpage. You can define your schema with [zod](https://github.com/colinhacks/zod) (TypeScript) or [pydantic](https://github.com/pydantic/pydantic) (Python). If you do not want to define a schema, you can also call `extract` with just a [natural language prompt](#prompt-only-extraction), or call `extract` [with no parameters](#extract-with-no-parameters).\n\n## Why use `extract()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Structured\" icon=\"brackets-curly\" href=\"#list-of-objects-extraction\">\n    Turn messy webpage data into clean objects that follow a schema.\n  </Card>\n  <Card title=\"Resilient\" icon=\"dumbbell\" href=\"#extract-with-context\">\n    Build resilient extractions that don't break when the website changes\n  </Card>\n</CardGroup>\n\n<Note>\nFor TypeScript, the extract schemas are defined using zod schemas.\n\nFor Python, the extract schemas are defined using pydantic models.\n</Note>\n\n## Using `extract()`\n\n### Single object Extraction\n\nHere is how an `extract` call might look for a single object:\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod/v3';\n\nconst item = await page.extract({\n  instruction: \"extract the price of the item\",\n  schema: z.object({\n    price: z.number(),\n  }),\n});\n```\n\n```python Python\nfrom pydantic import BaseModel\n\nclass Extraction(BaseModel):\n    price: float\n\nitem = await page.extract(\n    \"extract the price of the item\", \n    schema=Extraction\n)\n```\n</CodeGroup>\n\nYour output schema will look like:\n```Example\n{ price: number }\n```\n\n### List of objects Extraction\n\nHere is how an `extract` call might look for a list of objects.\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod/v3';\n\nconst apartments = await page.extract({\n  instruction:\n    \"Extract ALL the apartment listings and their details, including address, price, and square feet.\",\n  schema: z.object({\n    list_of_apartments: z.array(\n      z.object({\n        address: z.string(),\n        price: z.string(),\n        square_feet: z.string(),\n      }),\n    ),\n  })\n})\n\nconsole.log(\"the apartment list is: \", apartments);\n```\n\n```python Python\nfrom pydantic import BaseModel\n\nclass Apartment(BaseModel):\n    address: str\n    price: str\n    square_feet: str\n\nclass Apartments(BaseModel):\n    list_of_apartments: list[Apartment]\n\napartments = await page.extract(\n    \"Extract ALL the apartment listings and their details as a list, including address, price, and square feet for each apartment\",\n    schema=Apartments\n)\n\nprint(\"the apartment list is: \", apartments)\n```\n</CodeGroup>\n\nYour output schema will look like:\n```Example\nlist_of_apartments: [\n    {\n      address: \"street address here\",\n      price: \"$1234.00\",\n      square_feet: \"700\"\n    },\n    {\n        address: \"another address here\",\n        price: \"1010.00\",\n        square_feet: \"500\"\n    },\n    ...\n]\n```\n\n### Prompt-only Extraction\n\nYou can call `extract` with just a natural language prompt:\n\n<CodeGroup>\n```typescript TypeScript\nconst result = await page.extract(\"extract the name of the repository\");\n```\n\n```python Python\nresult = await page.extract(\"extract the name of the repository\")\n```\n</CodeGroup>\n\nWhen you call `extract` with just a prompt, your output schema will look like:\n```Example\n{ extraction: string }\n```\n\n### Extract with no parameters\n\nHere is how you can call `extract` with no parameters.\n\n<CodeGroup>\n```typescript TypeScript\nconst pageText = await page.extract();\n```\n\n```python Python\npageText = await page.extract()\n```\n</CodeGroup>\n\nOutput schema:\n```Example\n{ pageText: string }\n```\n\nCalling `extract` with no parameters will return hierarchical tree representation of the root DOM. This will not be passed through an LLM. It will look something like this:\n\n```\nAccessibility Tree:\n[0-2] RootWebArea: What is Stagehand? - 🤘 Stagehand\n  [0-37] scrollable\n    [0-118] body\n      [0-241] scrollable\n        [0-242] div\n          [0-244] link: 🤘 Stagehand home page light logo\n            [0-245] span\n              [0-246] StaticText: 🤘 Stagehand\n              [0-247] StaticText: home page\n```\n\n## Best practices\n\n\n### Extract with Context\n\nYou can provide additional context to your schema to help the model extract the data more accurately.\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod/v3';\n\nconst apartments = await page.extract({\n instruction:\n   \"Extract ALL the apartment listings and their details, including address, price, and square feet.\",\n schema: z.object({\n   list_of_apartments: z.array(\n     z.object({\n       address: z.string().describe(\"the address of the apartment\"),\n       price: z.string().describe(\"the price of the apartment\"),\n       square_feet: z.string().describe(\"the square footage of the apartment\"),\n     }),\n   ),\n })\n})\n```\n\n```python Python\nfrom pydantic import BaseModel, Field\n\nclass Apartment(BaseModel):\n    address: str = Field(..., description=\"the address of the apartment\")\n    price: str = Field(..., description=\"the price of the apartment\")\n    square_feet: str = Field(..., description=\"the square footage of the apartment\")\n\nclass Apartments(BaseModel):\n    list_of_apartments: list[Apartment]\n\napartments = await page.extract(\n    \"Extract ALL the apartment listings and their details as a list. For each apartment, include: the address of the apartment, the price of the apartment, and the square footage of the apartment\",\n    schema=Apartments\n)\n```\n</CodeGroup>\n\n### Link Extraction\n<Note>\nTo extract links or URLs, in the TypeScript version of Stagehand, you'll need to define the relevant field as `z.string().url()`.\nIn Python, you'll need to define it as `HttpUrl`.\n</Note>\n\nHere is how an `extract` call might look for extracting a link or URL. This also works for image links.\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod/v3';\n\nconst extraction = await page.extract({\n  instruction: \"extract the link to the 'contact us' page\",\n  schema: z.object({\n    link: z.string().url(), // note the usage of z.string().url() here\n  }),\n});\n\nconsole.log(\"the link to the contact us page is: \", extraction.link);\n```\n\n```python Python\nfrom pydantic import BaseModel, HttpUrl\n\nclass Extraction(BaseModel):\n    link: HttpUrl # note the usage of HttpUrl here\n\nextraction = await page.extract(\n    \"extract the link to the 'contact us' page\", \n    schema=Extraction\n)\n\nprint(\"the link to the contact us page is: \", extraction.link)\n```\n</CodeGroup>\n\n<Tip>\nInside Stagehand, extracting links works by asking the LLM to select an ID. Stagehand looks up that ID in a mapping of IDs -> URLs. When logging the LLM trace, you should expect to see IDs. The actual URLs will be included in the final `ExtractResult`.\n</Tip>\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Empty or partial results\">\n**Problem**: `extract()` returns empty or incomplete data\n\n**Solutions**:\n- **Check your instruction clarity**: Make sure your instruction is specific and describes exactly what data you want to extract\n- **Verify the data exists**: Use `page.observe()` first to confirm the data is present on the page\n- **Wait for dynamic content**: If the page loads content dynamically, use `page.act(\"wait for the content to load\")` before extracting\n\n**Solution: Wait for content before extracting**\n<CodeGroup>\n```typescript TypeScript\n// Wait for content before extracting\nawait page.act(\"wait for the product listings to load\");\nconst products = await page.extract({\n  instruction: \"extract all product names and prices\",\n  schema: z.object({\n    products: z.array(z.object({\n      name: z.string(),\n      price: z.string()\n    }))\n  })\n});\n```\n\n```python Python\n# Wait for content before extracting\nawait page.act(\"wait for the product listings to load\")\nproducts = await page.extract(\n    \"extract all product names and prices\",\n    schema=ProductList\n)\n```\n</CodeGroup>\n</Accordion>\n\n<Accordion title=\"Schema validation errors\">\n**Problem**: Getting schema validation errors or type mismatches\n\n**Solutions**:\n- **Use optional fields**: Make fields optional with `z.optional()` (TypeScript) or `Optional[type]` (Python) if the data might not always be present\n- **Use flexible types**: Consider using `z.string()` instead of `z.number()` for prices that might include currency symbols\n- **Add descriptions**: Use `.describe()` (TypeScript) or `Field(description=\"...\")` (Python) to help the model understand field requirements\n\n**Solution: More flexible schema**\n<CodeGroup>\n```typescript TypeScript\nconst schema = z.object({\n  price: z.string().describe(\"price including currency symbol, e.g., '$19.99'\"),\n  availability: z.string().optional().describe(\"stock status if available\"),\n  rating: z.number().optional()\n});\n```\n\n```python Python\nclass FlexibleProduct(BaseModel):\n    price: str = Field(description=\"price including currency symbol, e.g., '$19.99'\")\n    availability: Optional[str] = Field(default=None, description=\"stock status if available\")\n    rating: Optional[float] = None\n```\n</CodeGroup>\n</Accordion>\n\n<Accordion title=\"Inconsistent results\">\n**Problem**: Extraction results vary between runs\n\n**Solutions**:\n- **Be more specific in instructions**: Instead of \"extract prices\", use \"extract the numerical price value for each item\"\n- **Use context in schema descriptions**: Add field descriptions to guide the model\n- **Combine with observe**: Use `page.observe()` to understand the page structure first\n\n**Solution: Validate with observe first**\n<CodeGroup>\n```typescript TypeScript\n// First observe to understand the page structure\nconst elements = await page.observe(\"find all product listings\");\nconsole.log(\"Found elements:\", elements.map(e => e.description));\n\n// Then extract with specific targeting\nconst products = await page.extract({\n  instruction: \"extract name and price from each product listing shown on the page\",\n  schema: z.object({\n    products: z.array(z.object({\n      name: z.string().describe(\"the product title or name\"),\n      price: z.string().describe(\"the price as displayed, including currency\")\n    }))\n  })\n});\n```\n\n```python Python\n# First observe to understand the page structure\nelements = await page.observe(\"find all product listings\")\nprint(\"Found elements:\", [e.description for e in elements])\n\n# Then extract with specific targeting\nproducts = await page.extract(\n    \"extract name and price from each product listing shown on the page\",\n    schema=ProductSchema\n)\n```\n</CodeGroup>\n</Accordion>\n\n<Accordion title=\"Performance issues\">\n**Problem**: Extraction is slow or timing out\n\n**Solutions**:\n- **Reduce scope**: Extract smaller chunks of data in multiple calls rather than everything at once\n- **Use targeted instructions**: Be specific about which part of the page to focus on\n- **Consider pagination**: For large datasets, extract one page at a time\n- **Increase timeout**: Use `timeoutMs` parameter for complex extractions\n\n**Solution: Break down large extractions**\n<CodeGroup>\n```typescript TypeScript\n// Instead of extracting everything at once\nconst allData = [];\nconst pageNumbers = [1, 2, 3, 4, 5];\n\nfor (const pageNum of pageNumbers) {\n  await page.act(`navigate to page ${pageNum}`);\n  \n  const pageData = await page.extract({\n    instruction: \"extract product data from the current page only\",\n    schema: ProductPageSchema,\n    timeoutMs: 60000 // 60 second timeout\n  });\n  \n  allData.push(...pageData.products);\n}\n```\n\n```python Python\n# Instead of extracting everything at once\nall_data = []\npage_numbers = [1, 2, 3, 4, 5]\n\nfor page_num in page_numbers:\n    await page.act(f\"navigate to page {page_num}\")\n    \n    page_data = await page.extract(\n        \"extract product data from the current page only\",\n        schema=ProductPageSchema,\n        timeout_ms=60000  # 60 second timeout\n    )\n    \n    all_data.extend(page_data.products)\n```\n</CodeGroup>\n</Accordion>\n</AccordionGroup>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n  <Card title=\"Act\" icon=\"play\" href=\"/v2/basics/act\">\n    Execute actions efficiently using observe results\n  </Card>\n\n  <Card title=\"Observe\" icon=\"magnifying-glass\" href=\"/v2/basics/observe\">\n    Analyze pages with observe()\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/basics/observe.mdx",
    "content": "---\ntitle: Observe\nsidebarTitle: Observe\ndescription: 'Find suggested actions for your workflows'\n---\n\n## What is `observe()`?\n``` typescript\npage.observe(\"Find the login button\")\n```\n\n`observe` allows you to turn any page into a checklist of reliable, executable actions. It discovers key elements, ranks likely next steps, and returns structured actions (selector, method, args) you can run instantly with `act` or use to precisely target `extract` so workflows are faster, cheaper, and more resilient.\n\n## Why use `observe()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Explore\" icon=\"compass\" href=\"/v2/basics/observe#observe-with-act\">\n    When you're unsure what's on a page or need to discover available actions\n  </Card>\n  <Card title=\"Plan\" icon=\"map\" href=\"/v2/basics/observe#plan-ahead\">\n    When building complex workflows, plan ahead all the actions you'll need to take\n  </Card>\n  <Card title=\"Cache\" icon=\"database\" href=\"/v2/best-practices/caching\">\n    When you want to remember actions for the future and avoid LLM calls\n  </Card>\n    <Card title=\"Validate\" icon=\"check\" href=\"/v2/basics/observe#observe-with-act\">\n    Before performing critical actions to ensure elements exist\n  </Card>\n</CardGroup>\n\n## Using `observe()`\n\nCalling `observe` supercharges other Stagehand methods. Use it to plan workflows, speed up `act`, and precisely target `extract`. Using `observe` helps you explore what's possible on a page by giving you a list of suggested actions.\n\n<CodeGroup>\n```typescript TypeScript\n// Plan & validate\nconst buttons = await page.observe(\"Find the log in / sign up buttons\");\n```\n```python Python\n# Plan & validate\nbuttons = await page.observe(\"Find the log in / sign up buttons\")\n```\n</CodeGroup>\n\nThis will return a list of suggestions with the following structure\n```json\n{\n  \"selector\": \"xpath=/html/body/header/div/button[1]\",\n  \"description\": \"Log in button in the top right corner\",\n  \"method\": \"click\",\n  \"arguments\": []\n}\n```\n\n### Observe with Act\n\nYou can **validate** the action (method, selector, arguments...) and then pass it to `act` to **avoid extra LLM inference**.\n\n<Note>\n**Performance Tip**: Acting on multiple `observe` suggestions will minimize the number of LLM calls for multi-step actions and speed up your workflow 2-3x.\n</Note>\n\n<CodeGroup>\n```typescript TypeScript\nawait page.act(buttons[0]); // No LLM!\n```\n```python Python\nawait page.act(buttons[0]) # No LLM!\n```\n</CodeGroup>\n\n#### Plan ahead\n\nYou can use multiple suggestions from `observe` to preview a batch of actions. For example, when filling a form you could ask `observe` to find all the fields and then pass them in to `act`. **Call the LLM once, act multiple times**.\n\n<CodeGroup>\n```typescript TypeScript\nconst fields = await page.observe(\"Find all the fields in the form\");\nfor (const field of fields) {\n  await page.act(field); // No LLM!\n}\n```\n```python Python\nfields = await page.observe(\"Find all the fields in the form\")\nfor field in fields:\n  await page.act(field) # No LLM!\n```\n</CodeGroup>\n\n### Observe and Extract\n\nUsing `observe` to focus `extract` on a specific section of the page (like a table, a form, a list...) minimizes the context needed for an extraction. \n<Tip>\n**Savings Tip**: Pass the selector to `extract` to reduce LLM token usage by 10x for verbose websites!\n</Tip>\n\n<CodeGroup>\n```typescript TypeScript\n// Use observe to validate elements before extraction\nconst [ table ] = await page.observe(\"Find the data table\");\n\nconst { data } = await page.extract({\n  instruction: \"Extract data from the table\",\n  schema: z.object({\n    data: z.string()\n  }),\n  selector: table.selector // Reduce context scope needed for extraction\n});\n```\n```python Python\n# Use observe to validate elements before extraction\n[ table ] = await page.observe(\"Find the data table\")\n\nextraction = await page.extract(\n  \"Extract data from the table\",\n  schema=Data, # Pydantic schema\n  selector=table.selector # Reduce context scope needed for extraction\n)\n```\n</CodeGroup>\n\n## Best Practices\n\n### Choose the right commands\n\n<Tabs>\n<Tab title=\"Do this\">\n\n- Use `observe` when a yes/no answer will gate an action (e.g., \"Find the Submit button\"), then conditionally `act`.\n- Use `extract` for information-only questions (e.g., \"What’s the page title?\", \"How many results are listed?\").\n\n</Tab>\n\n<Tab title=\"Don't do this\">\n\n- Don’t call `extract` to locate elements you plan to click next.\n- Don’t call `observe` to answer info-only questions that won’t lead to an action.\n\n</Tab>\n</Tabs>\n- **Discover and plan with `observe`**: Use `observe(\"Find…\")` to map actionable elements and preview next steps.\n- **Scope `extract` with selectors from `observe`**: First `observe(\"Find the data table\")`, then pass `selector` to `extract` to reduce tokens and boost accuracy.\n\n### Conserve LLM tokens\n\nOptimize performance by directly passing `ObserveResult` to `act` (e.g., `await page.act(results[0])`) to save LLM tokens. Batch operations by using `observe` once to find elements, then act on each. Cache and reuse stable `observe` results for familiar pages, using self-healing if layouts change.\n\n<Card title=\"Build your own cache\" icon=\"database\" href=\"/v2/best-practices/caching\">\n  Check out the guide on how to build your own action cache\n</Card>\n\n### Improve Accuracy\n\nBe precise with instructions, e.g., \"Find the primary CTA in the hero\" for better results. For iframes, set `iframes: true` and wait for `networkidle`. Use `observe` selectors in `extract` to limit context.\n\n<Card title=\"Prompting Best Practices\" icon=\"robot\" href=\"/v2/best-practices/prompting-best-practices\">\n  Check out the guide on how to improve the accuracy of your results\n</Card>\n\n### Action Validation\n\nBefore performing critical actions, validate the suggestion's `method`, `selector`, and `arguments` to prevent misclicks. If a direct `act` fails, use `observe` with the same prompt to verify the method, then proceed with the suggested action.\n\n<CodeGroup>\n```typescript TypeScript\nconst prompt = \"click the submit button\";\nconst expectedMethod = \"click\";\n\ntry {\n  await page.act(prompt);\n} catch (error) {\n  if (error.message.includes(\"method not supported\")) {\n    // Observe the same prompt to get the planned action\n    const [action] = await page.observe(prompt);\n    \n    if (action && action.method === expectedMethod) {\n      await page.act(action);\n    } else {\n      throw new Error(`Unsupported method: expected \"${expectedMethod}\", got \"${action?.method}\"`);\n    }\n  } else {\n    throw error;\n  }\n}\n```\n\n```python Python\nprompt = \"click the submit button\"\nexpected_method = \"click\"\n\ntry:\n    await page.act(prompt)\nexcept Exception as error:\n    if \"method not supported\" in str(error):\n        # Observe the same prompt to get the planned action\n        results = await page.observe(prompt)\n        \n        if results and results[0].method == expected_method:\n            await page.act(results[0])\n        else:\n            method = results[0].method if results else \"unknown\"\n            raise Exception(f'Unsupported method: expected \"{expected_method}\", got \"{method}\"')\n    else:\n        raise error\n```\n</CodeGroup>\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"No elements found\">\n**Problem**: `observe` returns empty array\n\n**Solutions**:\n- Make sure the element exists on the page\n- Use explicit instructions to find the element\n- Ensure page has fully loaded\n- Look at the [debugging logs](/v2/configuration/logging), if the element is there then the LLM might be hallucinating/not catching it. \n</Accordion>\n\n<Accordion title=\"Inaccurate element descriptions\">\n**Problem**: Descriptions don't match actual elements\n\n**Solutions**:\n- Use more capable models: check [evals](https://stagehand.dev/evals) for the best models for your use case\n- Provide more specific instructions\n- Log inference to file (see [debugging logs](/v2/configuration/logging#llm-inference-logging)) to get an LLM trace\n\n</Accordion>\n<Accordion title=\"Wrong method identified\">\n**Problem**: The method identified is not valid\n\n**Solutions**:\n- Check the [supported actions](/v2/basics/act)\n- Provide more specific instructions\n- Validate the method, if invalid override with one of the supported ones\n\n</Accordion>\n</AccordionGroup>\n\n\n## Next Steps\n\n<CardGroup cols={2}>\n<Card title=\"Act Overview\" icon=\"play\" href=\"/v2/basics/act\">\nExecute actions efficiently using `observe` results\n</Card>\n\n<Card title=\"Extract Data\" icon=\"download\" href=\"/v2/basics/extract\">  \nExtract structured data from observed elements\n</Card>\n\n<Card title=\"Observability\" icon=\"chart-line\" href=\"/v2/configuration/observability\">\nMonitor and debug observation performance  \n</Card>\n\n<Card title=\"Best Practices\" icon=\"star\" href=\"/v2/best-practices/prompting-best-practices\">\nAdvanced patterns and optimization techniques\n</Card>\n</CardGroup>\n\n\n\n\n\n"
  },
  {
    "path": "packages/docs/v2/best-practices/agent-fallbacks.mdx",
    "content": "---\ntitle: Agent Fallbacks\ndescription: \"A failsafe when unexpected page changes add extra steps\"\n---\n\n## When to use\n\nUse an agent fallback as a failsafe when a one step action unexpectedly becomes a multi-step flow.\n\n## How it works\n\n1. [`act()`](/v2/basics/act) is attempted for the direct action\n2. If it fails, [`agent()`](/v2/basics/agent) figures out the new path\n3. Agent completes all needed steps (open menu → click button)\n\n### Example scenario\n\n**Before**: Sign in button was in the header  \n**After**: Sign in now requires: Click account menu → Click \"Sign in\" option\n\nA single `act(\"click sign in\")` can't handle this change. The agent fallback can discover and execute both steps.\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\ntry {\n  await page.act(\"click the 'Sign In' button\");\n} catch (err) {\n  console.log(\"Agent fallback triggered\");\n\n  const agent = stagehand.agent({\n    provider: \"anthropic\",\n    model: \"claude-sonnet-4-20250514\",\n    instructions: \"You are a helpful assistant that can use a web browser.\",\n  });\n\n  const result = await agent.execute({\n    instruction: \"Find and click Sign In button\",\n    maxSteps: 10,\n  });\n\n  console.log(result.success ? \"Agent fallback success\" : \"Agent fallback failed\");\n\n  if (!result.success) throw err;\n}\n```\n\n```python Python\nfrom stagehand import Stagehand\n\ntry:\n    await page.act(\"click the 'Sign In' button\")\nexcept Exception as err:\n    print(\"Agent fallback triggered\")\n\n    agent = stagehand.agent({\n        \"provider\": \"anthropic\",\n        \"model\": \"claude-sonnet-4-20250514\",\n        \"instructions\": \"Complete the action, handling any new steps required.\",\n    })\n\n    result = await agent.execute({\n        \"instruction\": \"Find and click Sign In button\",\n        \"max_steps\": 10,\n    })\n\n    print(\"Agent fallback success\" if result.success else \"Agent fallback failed\")\n\n    if not result.success:\n        raise err\n```\n</CodeGroup>\n\n"
  },
  {
    "path": "packages/docs/v2/best-practices/build-agent.mdx",
    "content": "---\ntitle: 'Build a web browsing agent'\ndescription: 'Build an AI agent that can autonomously control a browser with Stagehand'\n---\nimport { Excalidraw } from '/snippets/excalidraw.mdx';\n\nStagehand gives AI agents powerful tools to control a browser completely autonomously. Watch below as a Stagehand agent autonomously navigates to a URL, takes actions on the page, and extracts structured data to answer a question.\nThere's quite a few ways to build an agent with Stagehand. Let's look at a few of them.\n\n![Agent](/media/stagehand-agent.gif)\n\n## Stagehand MCP\n\nThe above example is a Claude agent that uses Stagehand to control a browser. At this time of writing, [multimodal tool calling](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#multi-modal-tool-results) is only supported in Claude 3.5/3.7 Sonnet. \nThis means Claude is intelligent enough to know when to request a browser screenshot, and it can then use that screenshot to make decisions about what actions to take next.\n\n<CardGroup>\n<Card title=\"Browserbase MCP\" href=\"https://github.com/browserbase/mcp-server-browserbase/\" icon=\"hand-horns\">\nControl a browser with Browserbase MCP powered by Stagehand\n</Card>\n</CardGroup>\n\nWhat's really interesting about this is that the agent is able to reason about the browser state and take actions separate from one another! \nClaude is able to reason about the browser state, while Stagehand is able to take actions on the page with GPT-4o-mini or a computer use model.\nStagehand is even smart enough to know when to use GPT-4o-mini and when to use a computer use model, i.e. on iframe detection.\n\n<Excalidraw className=\"w-full aspect-video\" url=\"https://link.excalidraw.com/readonly/GWQWmWUBqMBEAamlWsIM?darkMode=true\" />\n\nWe've found great success from having Claude as the \"Trajectory\" agent calling Stagehand tools when it sees fit! \nWhile MCP is really nascent, we're excited to see where it goes.\n\n## Stagehand + Computer Use Models\n\nStagehand lets you leverage powerful computer use APIs from OpenAI and Anthropic with just one line of code. \n\n<CodeGroup>\n```typescript TypeScript\nawait page.goto(\"https://github.com/browserbase/stagehand\");\n\n// Create a Computer Use agent with just one line of code!\nconst agent = stagehand.agent({\n\tprovider: \"openai\",\n\tmodel: \"computer-use-preview\"\n});\n\n// Use the agent to execute a task\nconst result = await agent.execute(\"Extract the top contributor's username\");\nconsole.log(result);\n```\n```python Python\nawait page.goto(\"https://github.com/browserbase/stagehand-python\")\n\n# Create a Computer Use agent with just one line of code!\nagent = stagehand.agent(\n    model=\"computer-use-preview\"\n)\n\n# Use the agent to execute a task\nresult = await agent.execute(\"Extract the top contributor's username\")\nprint(result)\n```\n</CodeGroup>\n\n<CardGroup>\n<Card title=\"Stagehand + Computer Use Docs\" href=\"/best-practices/computer-use\" icon=\"scroll\">\nCheck out our docs page for instructions on how to use computer use models with Stagehand.\n</Card>\n<Card title=\"CUA Browser Demo\" href=\"https://cua.browserbase.com/\" icon=\"brain-circuit\">\nCheck out a live demo of a Browserbase browser controlled by OpenAI's Computer Using Agent (CUA) model.\n</Card>\n</CardGroup>\n\n## Sequential Tool Calling (Open Operator)\n\nIn January 2025, Browserbase released [Open Operator](https://operator.browserbase.com). \nOpen Operator is able to reason about the browser state and take actions accordingly to accomplish larger tasks like \"order me a pizza\".\nIt works by calling Stagehand tools in sequence:\n\n1. If there's no URL, go to a default URL.\n1. Examine the browser state. Ask an LLM to reason about what to do next.\n1. Use `page.act()` to execute the LLM-suggested action.\n1. Repeat\n\n<Excalidraw className=\"w-full\" url=\"https://link.excalidraw.com/readonly/dKh5sB1gEM1EjVqRCGKn\" />\n\nIncorporating `stagehand.agent` into your browser automation is as easy as adding a single line of code:\n\n<Note>\nPython currently supports `stagehand.agent` with Computer Use Agent (CUA) models. The default implementation is coming soon. \n</Note>\n\n<CodeGroup>\n```typescript TypeScript\nawait stagehand.page.goto(\"https://github.com/browserbase/stagehand\");\n\n// Open Operator will use the default LLM from Stagehand config\nconst operator = stagehand.agent();\nconst { message, actions } = await operator.execute(\n\t\"Extract the top contributor's username\"\n);\n\nconsole.log(message);\n```\n</CodeGroup>\n\n### Replay the agent's actions\n\nYou can replay the agent's actions exactly the same way you would with a regular Stagehand agent. You can even automatically cache the actions to avoid unnecessary LLM calls on a repeated run.\n\nLet's use the `replay` function below to save the actions to a Stagehand script file, which will reproduce the same actions the agent did, with cached actions built in.\n\n<Accordion title=\"utils.ts\">\n```typescript\nimport { AgentAction, AgentResult } from \"@browserbasehq/stagehand\";\nimport { exec } from \"child_process\";\nimport fs from \"fs/promises\";\n\nexport async function replay(result: AgentResult) {\n  const history = result.actions;\n  const replay = history\n    .map((action: AgentAction) => {\n      switch (action.type) {\n        case \"act\":\n          if (!action.playwrightArguments) {\n            throw new Error(\"No playwright arguments provided\");\n          }\n          return `await page.act(${JSON.stringify(\n            action.playwrightArguments\n          )})`;\n        case \"extract\":\n          return `await page.extract(\"${action.parameters}\")`;\n        case \"goto\":\n          return `await page.goto(\"${action.parameters}\")`;\n        case \"wait\":\n          return `await page.waitForTimeout(${parseInt(\n            action.parameters as string\n          )})`;\n        case \"navback\":\n          return `await page.goBack()`;\n        case \"refresh\":\n          return `await page.reload()`;\n        case \"close\":\n          return `await stagehand.close()`;\n        default:\n          return `await stagehand.oops()`;\n      }\n    })\n    .join(\"\\n\");\n\n  console.log(\"Replay:\");\n  const boilerplate = `\nimport { Page, BrowserContext, Stagehand } from \"@browserbasehq/stagehand\";\n\nexport async function main(stagehand: Stagehand) {\n    const page = stagehand.page\n\t${replay}\n}\n  `;\n  await fs.writeFile(\"replay.ts\", boilerplate);\n\n  // Format the replay file with prettier\n  await new Promise((resolve, reject) => {\n    exec(\n      \"npx prettier --write replay.ts\",\n      (error: any, stdout: any, stderr: any) => {\n        if (error) {\n          console.error(`Error formatting replay.ts: ${error}`);\n          reject(error);\n          return;\n        }\n        resolve(stdout);\n      }\n    );\n  });\n}\n```\n</Accordion>\n\nHere's the replay output of an instruction like `\"Get me the stock price of NVDA\"`:\n\n```typescript {14-22} replay.ts\nimport { Page, BrowserContext, Stagehand } from \"@browserbasehq/stagehand\";\n\nexport async function main({\n  page,\n  context,\n  stagehand,\n}: {\n  page: Page; // Playwright Page with act, extract, and observe methods\n  context: BrowserContext; // Playwright BrowserContext\n  stagehand: Stagehand; // Stagehand instance\n}) {\n  await page.goto(\"https://www.google.com\");\n\n  // Replay will default to Playwright first to avoid unnecessary LLM calls!\n  // If the Playwright action fails, Stagehand AI will take over and self-heal\n  await page.act({\n    description: \"The search combobox where users can type their queries.\",\n    method: \"fill\",\n    arguments: [\"NVDA stock price\"],\n    selector:\n      \"xpath=/html/body[1]/div[1]/div[3]/form[1]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]\",\n  });\n  await page.extract(\n    \"the displayed NVDA stock price in the search suggestions\",\n  );\n  await stagehand.close();\n}\n```"
  },
  {
    "path": "packages/docs/v2/best-practices/caching.mdx",
    "content": "---\ntitle: Caching Actions\ndescription: You can cache actions in Stagehand to avoid redundant LLM calls.\n---\n\nCaching actions in Stagehand is useful for actions that are expensive to run, or when the underlying DOM structure is not expected to change.\n\n## Using `observe` to preview an action\n`observe` lets you preview an action before taking it. If you are satisfied with the action preview, you can run it in `page.act` with no further LLM calls.\n\n<CodeGroup>\n```typescript TypeScript\nconst [actionPreview] = await page.observe(\"Click the quickstart link\");\n\n/** actionPreview is a JSON-ified version of a Playwright action:\n{\n\tdescription: \"The quickstart link\",\n\tmethod: \"click\",\n\tselector: \"/html/body/div[1]/div[1]/a\",\n\targuments: [],\n}\n**/\n\n// NO LLM INFERENCE when calling act on the preview\nawait page.act(actionPreview)\n```\n\n```python Python\nactions = await page.observe(\"Click the quickstart link\")\naction_preview = actions[0]\n\n# action_preview is a dictionary version of a Playwright action:\n# {\n#\t\"description\": \"The quickstart link\",\n#\t\"method\": \"click\",\n#\t\"selector\": \"/html/body/div[1]/div[1]/a\",\n#\t\"arguments\": [],\n# }\n\n# NO LLM INFERENCE when calling act on the preview\nawait page.act(action_preview)\n```\n</CodeGroup>\n\n## Simple caching\n\nLet's use a simple file-based cache for this example. We'll write a getter and a setter functions that can read and write to a JSON file:\n\n<CodeGroup>\n```typescript TypeScript\n// Get the cached value (undefined if it doesn't exist)\nasync function getCache(key: string): Promise<ObserveResult | undefined> {\n  try {\n    const cache = await readFile(\"cache.json\");\n    const parsed = JSON.parse(cache);\n    return parsed[key];\n  } catch {\n    return undefined;\n  }\n}\n\n// Set the cache value\nasync function setCache(key: string, value: ObserveResult): Promise<void> {\n  const cache = await readFile(\"cache.json\");\n  const parsed = JSON.parse(cache);\n  parsed[key] = value;\n  await writeFile(\"cache.json\", JSON.stringify(parsed));\n}\n```\n\n```python Python\n# Get the cached value (None if it doesn't exist)\nasync def get_cache(key: str) -> Optional[Dict[str, Any]]:\n    try:\n        async with aiofiles.open(\"cache.json\", 'r') as f:\n            cache_content = await f.read()\n            parsed = json.loads(cache_content)\n            return parsed.get(key)\n    except (FileNotFoundError, json.JSONDecodeError):\n        return None\n\n# Set the cache value\nasync def set_cache(key: str, value: Dict[str, Any]) -> None:\n    try:\n        async with aiofiles.open(\"cache.json\", 'r') as f:\n            cache_content = await f.read()\n            parsed = json.loads(cache_content)\n    except (FileNotFoundError, json.JSONDecodeError):\n        parsed = {}\n    \n    parsed[key] = value\n    \n    async with aiofiles.open(\"cache.json\", 'w') as f:\n        await f.write(json.dumps(parsed))\n```\n</CodeGroup>\n\n### Act with cache\nLet's write a function that will check the cache, get the action, and run it. If the action fails, we'll attempt to \"self-heal\", i.e. retry it with `page.act` directly.\n\n<CodeGroup>\n```typescript TypeScript\n// Check the cache, get the action, and run it\n// If selfHeal is true, we'll attempt to self-heal if the action fails\nasync function actWithCache(page: Page, key: string, prompt: string, selfHeal = false) {\n\ttry {\n\t\tconst cacheExists = await getCache(key);\n\n\t\tlet action: ObserveResult;\n\t\tif (cacheExists) {\n\t\t// Get the cached action\n\t\taction = await getCache(prompt);\n\t\t} else {\n\t\t// Get the observe result (the action)\n\t\t[action] = await page.observe(prompt);\n\n\t\t// Cache the action\n\t\tawait setCache(prompt, action);\n\t\t}\n\n\t\t// Run the action (no LLM inference)\n\t\tawait page.act(action);\n\t} catch (e) {\n\t\tconsole.error(e);\n\t\t// in selfHeal mode, we'll retry the action\n\t\tif (selfHeal) {\n\t\t\tconsole.log(\"Attempting to self-heal...\");\n\t\t\tawait page.act(prompt);\n\t\t}\n\t\telse {\n\t\t\tthrow e;\n\t\t}\n\t}\n}\n```\n\n```python Python\n# Check the cache, get the action, and run it\n# If self_heal is true, we'll attempt to self-heal if the action fails\nasync def act_with_cache(page, key: str, prompt: str, self_heal: bool = False):\n    try:\n        cache_exists = await get_cache(key)\n\n        if cache_exists:\n            # Get the cached action\n            action = await get_cache(prompt)\n        else:\n            # Get the observe result (the action)\n            actions = await page.observe(prompt)\n            action = actions[0]\n\n            # Cache the action\n            await set_cache(prompt, action)\n\n        # Run the action (no LLM inference)\n        await page.act(action)\n    except Exception as e:\n        print(f\"Error: {e}\")\n        # in self_heal mode, we'll retry the action\n        if self_heal:\n            print(\"Attempting to self-heal...\")\n            await page.act(prompt)\n        else:\n            raise e\n```\n</CodeGroup>\n\nYou can now use `actWithCache` to run an action with caching:\n\n<CodeGroup>\n```typescript TypeScript\nconst prompt = \"Click the quickstart link\";\nconst key = prompt; // Simple cache key\n// Attempt cached action or self-heal\nawait actWithCache(page, key, prompt);\n```\n\n```python Python\nprompt = \"Click the quickstart link\"\nkey = prompt  # Simple cache key\n# Attempt cached action or self-heal\nawait act_with_cache(page, key, prompt)\n```\n</CodeGroup>\n\n## Advanced caching\n\nThe above example is simple, but you may want to cache actions based on the page contents. Also, if you have duplicate prompts, you should use a more unique key.\n\nWe want to leave caching logic up to you, but give you all the tools you need to implement your own caching strategy.\n\nYou can directly access the DOM and accessibility tree from Playwright's page object. Here's an example of how to access the page content:\n\n<CodeGroup>\n```typescript TypeScript\n// Get the page content\nconst pageContent = await page.content();\n```\n\n```python Python\n# Get the page content\npage_content = await page.content()\n```\n</CodeGroup>\n\nYou may also want to use the accessibility tree, the DOM, or any other information to create a more unique key. You can do this as you please, with very similar logic to the above example."
  },
  {
    "path": "packages/docs/v2/best-practices/computer-use.mdx",
    "content": "---\ntitle: Computer Use Agents\ndescription: Incorporate Computer Use APIs from Anthropic and OpenAI with one line of code in Stagehand.\n---\n\n## What is a Computer Use Agent?\n\n<iframe\n  width=\"100%\"\n  height=\"400\"\n  src=\"https://www.youtube.com/embed/ODaHJzOyVCQ\"\n  title=\"YouTube video player\"\n  frameborder=\"0\"\n  allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n  allowfullscreen\n></iframe>\nYou might've heard of [Claude Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) or [OpenAI's Computer Using Agent](https://openai.com/index/computer-using-agent/).\n\nThese are powerful tools that can convert natural language into actions on the computer. However, you'd otherwise need to write your own code to convert these actions into Playwright commands.\n\nStagehand not only handles the execution of Computer Use outputs, but also lets you hot-swap between OpenAI and Anthropic models with one line of code.\n\n## How to use a Computer Use Agent in Stagehand\n\nStagehand lets you use Computer Use Agents with one line of code:\n\n<Note>\n**IMPORTANT! Configure your browser dimensions**\n\nComputer Use Agents will often return XY-coordinates to click on the screen, so you'll need to configure your browser dimensions.\n\nIf not specified, the default browser dimensions are 1024x768. You can also configure the browser dimensions in the `browserbaseSessionCreateParams` or `localBrowserLaunchOptions` options.\n</Note>\n\n\n### Configuring browser dimensions\n\nBrowser configuration differs by environment:\n\n<Tabs>\n<Tab title=\"BROWSERBASE\">\n<CodeGroup>  \n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n\tenv: \"BROWSERBASE\",\n  \tapiKey: process.env.BROWSERBASE_API_KEY /* API key for authentication */,\n    projectId: process.env.BROWSERBASE_PROJECT_ID /* Project identifier */,\n  \n    browserbaseSessionCreateParams: {\n      projectId: process.env.BROWSERBASE_PROJECT_ID!,\n      browserSettings: {\n\t\tblockAds: true,\n        viewport: {\n          width: 1024,\n          height: 768,\n        },\n      },\n  \t},\n});\n\nawait stagehand.init();\n```\n```python Python\nimport os\nfrom stagehand import Stagehand, StagehandConfig\n\nstagehand = Stagehand(StagehandConfig(\n    env=\"BROWSERBASE\",\n    api_key=os.getenv(\"BROWSERBASE_API_KEY\"),  # API key for authentication\n    project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),  # Project identifier\n    \n    browserbase_session_create_params={\n        \"projectId\": os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n        \"browserSettings\": {\n            \"blockAds\": True,\n            \"viewport\": {\n                \"width\": 1024,\n                \"height\": 768,\n            },\n        },\n    },\n))\n\nawait stagehand.init()\n```\n</CodeGroup>\n</Tab>\n<Tab title=\"LOCAL\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    headless: false,\n    viewport: {\n      width: 1024,\n      height: 768,\n    },\n  }\n});\n\nawait stagehand.init();\n```\n```python Python\nfrom stagehand import Stagehand, StagehandConfig\n\nstagehand = Stagehand(StagehandConfig(\n    env=\"LOCAL\",\n    local_browser_launch_options={\n        \"headless\": False,\n        \"viewport\": {\n            \"width\": 1024,\n            \"height\": 768,\n        },\n    }\n))\n\nawait stagehand.init()\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n### Direct your Computer Use Agent\n\nCall `execute` on the agent to assign a task to the agent.\n\n<CodeGroup>\n```typescript TypeScript\n// Navigate to a website\nawait stagehand.page.goto(\"https://www.google.com\");\n\nconst agent = stagehand.agent({\n\t// You can use either OpenAI or Anthropic\n\tprovider: \"anthropic\",\n\t// The model to use (computer-use-preview for OpenAI)\n\tmodel: \"claude-sonnet-4-20250514\",\n\n\t// Customize the system prompt\n\tinstructions: `You are a helpful assistant that can use a web browser.\n\tDo not ask follow up questions, the user will trust your judgement.`,\n\n\t// Customize the API key\n\toptions: {\n\t\tapiKey: process.env.ANTHROPIC_API_KEY,\n\t},\n});\n\n// Execute the agent\nawait agent.execute(\"Apply for a library card at the San Francisco Public Library\");\n```\n\n```python Python\nimport os\n\n# Navigate to a website\nawait stagehand.page.goto(\"https://www.google.com\")\n\nagent = stagehand.agent({\n    # The model to use\n    model=\"computer-use-preview\",\n\n    # Customize the system prompt\n    instructions=\"You are a helpful assistant that can use a web browser. Do not ask follow up questions, the user will trust your judgement.\",\n\n    # Customize the API key\n    options={\n        \"apiKey\": os.getenv(\"ANTHROPIC_API_KEY\"),\n    },\n})\n\n# Execute the agent\nawait agent.execute(\"Apply for a library card at the San Francisco Public Library\")\n```\n</CodeGroup>\n\nYou can also define the maximum number of steps the agent can take with:\n\n<CodeGroup>\n```typescript TypeScript\nawait agent.execute({\n\tinstructions: \"Apply for a library card at the San Francisco Public Library\",\n\tmaxSteps: 10,\n});\n```\n\n```python Python\nawait agent.execute(\n\t\"Apply for a library card at the San Francisco Public Library\",\n\tmax_steps=10,\n)\n```\n</CodeGroup> \n\n<Callout icon=\"code\" color=\"#6ec202\" iconType=\"regular\">View or run the example templates [here](https://www.browserbase.com/templates?category=Computer+Use+Agents)</Callout>\n"
  },
  {
    "path": "packages/docs/v2/best-practices/contributing.mdx",
    "content": "---\ntitle: 'Contribute to Stagehand'\ndescription: 'Best practices for making a meaningful contribution to Stagehand'\n---\n\n# Codeowners and Subject-Matter Experts\n\nAny contribution must be explicitly approved by a codeowner. Officially, Stagehand codeowners are as follows:\n\n- [**Paul Klein**](https://github.com/pkiv)\n- [**Miguel Gonzalez**](https://github.com/miguelg719)\n- [**Sean McGuire**](https://github.com/seanmcguire12)\n- [**Anirudh Kamath**](https://github.com/kamath)\n- [**Sameel Arif**](https://github.com/sameelarif)\n- [**Filip Michalsky**](https://github.com/filip-michalsky)\n\nSpecial thanks to [Jeremy Press](https://github.com/jeremypress), [Navid Pour](https://github.com/navidkpr), and [all the contributors](https://github.com/browserbase/stagehand/graphs/contributors) for your help in making Stagehand the best browser automation framework.\n\n***Please do not hesitate to reach out to anyone listed here in the [public Discord server](https://stagehand.dev/discord)***\n\n## General Workflow\n\nGet listed as [one of our beloved contributors](https://github.com/browserbase/stagehand/graphs/contributors)!\n\n1. **Discuss your proposed contribution before starting.** Not doing this runs you the risk of entirely discarding something you put considerable time and effort into. You can DM Miguel on [Discord](https://stagehand.dev/discord) for a 1on1 call.\n2. **Open a Pull Request.** Create a fork of this repository, and follow [GitHub’s instructions to create a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). This allows our team to review your contribution and leave comments. \n3. **Wait for Review**. We'll do our best to get to your contribution as soon as possible. If it's been 2-3 days and you have yet to receive any comments, DM Miguel on [Discord](https://stagehand.dev/discord)\n4. **Merge into `evals` branch.** We don’t let external contributors [run our CI via GitHub Actions](https://github.com/browserbase/stagehand/blob/main/.github/workflows/ci.yml) to prevent spam and misuse. If your contribution passes an initial screen, we’ll run our evals on it\n    1. By default, all PRs run the following tests that you can also run from the repo source:\n        1. Lint (`npm run lint`) - Runs `prettier` and `eslint`. If this fails, you can most likely run `npm run format` to fix some simple linting errors.\n        2. Build (`npm run build`) - Lints and builds TS → JS in `dist/`\n        3. End-to-End (`npm run e2e`) - These are deterministic end-to-end Playwright tests to ensure the integrity of basic Playwright functionality of [`stagehand.page`](http://stagehand.page) and `stagehand.context` as well as compatibility with the Browserbase API\n        4. Combination (`npm run evals category combination`) - This runs AI-based end-to-end tests using combinations of `act`, `extract`, and `observe` \n    2. If you’re changing anything about `act`, `extract`, or `observe` itself, we might also run specific act/extract/observe evals to ensure existing functionality doesn’t significantly drop.\n    \n    ![CI](/images/CI.png)\n    \n5. **Cleanup and merge to main**. Once it’s in `evals`, unfortunately the original contributor can’t make any further changes. The internal Stagehand team will be responsible for cleaning up the code and bringing it into main. \n\n## Contribution Guidelines\n\n1. **Use draft PRs.** If your PR is a work in progress, please convert it to a draft (see below) while you’re working on it, and mark it for review/add reviewers when you’re ready. This helps us prevent clutter in the review queue.\n    \n    ![Draft PR](/images/pr_draft.png)\n    \n2. **Provide a reproducible test plan.** Include an eval (preferred) or example. We can’t merge your PR if we can’t run anything that specifically highlights your contribution. \n    1. Write a script in [`evals/tasks`](https://github.com/browserbase/stagehand/tree/v2/evals/tasks) as `someTask.ts`\n    2. Add your script to [`evals.config.json`](https://github.com/browserbase/stagehand/blob/v2/evals/evals.config.json) with default category `combination` (*or act/extract/observe if you’re* *only* *testing* *act/extract/observe*).\n3. **Add a changeset.** Run `npx changeset` in TS or `uvx changeset` in Python to add a changeset that will directly reflect in the `CHANGELOG` in the upcoming release.\n    1. `patch` - no net new functionality to an end-user\n    2. `minor` - some net new functionality to an end-user (new function parameter, new exposed type, etc.)\n    3. `major` - you shouldn’t be committing a major change\n"
  },
  {
    "path": "packages/docs/v2/best-practices/cost-optimization.mdx",
    "content": "---\ntitle: Cost Optimization  \nsidebarTitle: Cost Optimization\ndescription: Minimize costs while maintaining automation performance\n---\n\nCost optimization in Stagehand involves balancing LLM inference costs and browser infrastructure costs. This guide provides practical strategies to reduce your automation expenses.\n\n## Quick Wins\n\nStart with these simple optimizations that can reduce costs:\n\n### 1. Use the Right Model for the Job\n\nWe don't recommend using larger, more premium models for simple tasks. See our [evaluation results](https://stagehand.dev/evals) for model performance and cost comparisons across different task types.\n\n<CardGroup cols={2}>\n<Card title=\"Model Selection Guide\" icon=\"brain\" href=\"/v2/configuration/models\">\n  Choose the right LLM for your budget and accuracy requirements\n</Card>\n<Card title=\"Evaluation Results\" icon=\"chart-line\" href=\"https://www.stagehand.dev/evals\">\n  See how different models perform on different tasks\n</Card>\n</CardGroup>\n\n### 2. Implement Smart Caching\n\nCache successful actions to avoid repeated LLM calls. Learn the basics in our [Caching Guide](/v2/best-practices/caching):\n\n<CodeGroup>\n```typescript TypeScript\n// Cache successful actions\nconst [action] = await page.observe(\"Click the sign in button\");\nawait setCache(\"sign_in_button\", action);\n\n// Reuse cached action (no LLM cost)\nconst cachedAction = await getCache(\"sign_in_button\");\nif (cachedAction) {\n  await page.act(cachedAction);\n} else {\n  await page.act(action);\n}\n```\n```python Python\n# Cache successful actions\nactions = await page.observe(\"Click the sign in button\")\naction = actions[0]\nawait set_cache(\"sign_in_button\", action)\n\n# Reuse cached action (no LLM cost)\ncached_action = await get_cache(\"sign_in_button\")\nif cached_action:\n    await page.act(cached_action)\nelse:\n    await page.act(action)\n```\n</CodeGroup>\n\n<CardGroup cols={1}>\n<Card title=\"Caching Guide\" icon=\"database\" href=\"/v2/best-practices/caching\">\n  Reduce costs with smart action caching and observe patterns\n</Card>\n</CardGroup>\n\n### 3. Optimize Browser Sessions\n\nReuse sessions when possible and set appropriate timeouts. See [Browser Configuration](/v2/configuration/browser) for details:\n\n<CodeGroup>\n```typescript TypeScript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionCreateParams: {\n    timeout: 1800, // 30 minutes instead of default 1 hour\n    keepAlive: true, // Keep session alive between tasks\n  }\n});\n```\n```python Python\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n    browserbase_session_create_params={\n        \"timeout\": 1800,  # 30 minutes instead of default 1 hour\n        \"keep_alive\": True,  # Keep session alive between tasks\n    }\n)\n```\n</CodeGroup>\n\n<CardGroup cols={1}>\n<Card title=\"Browserbase Cost Optimization\" icon=\"window-maximize\" href=\"https://docs.browserbase.com/guides/cost-optimization\">\n  Optimize Browserbase infrastructure costs and session management\n</Card>\n</CardGroup>\n\n## Advanced Strategies\n\n### Intelligent Model Switching\n\nAutomatically fall back to cheaper models for simple tasks:\n\n<CodeGroup>\n```typescript TypeScript\n// Use models from least to most expensive based on task complexity\n// See stagehand.dev/evals for performance comparisons\nasync function smartAct(page: Page, prompt: string) {\n  const models = [\"cheaper-model\", \"premium-model\"];\n  \n  for (const model of models) {\n    try {\n      const stagehand = new Stagehand({ modelName: model });\n      await stagehand.init();\n      const [action] = await stagehand.page.observe(prompt);\n      await stagehand.page.act(action);\n      return;\n    } catch (error) {\n      console.log(`Falling back to ${model}...`);\n    }\n  }\n}\n```\n```python Python\n# Use models from least to most expensive based on task complexity\n# See stagehand.dev/evals for performance comparisons\nasync def smart_act(page, prompt: str):\n    models = [\"cheaper-model\", \"premium-model\"]\n    \n    for model in models:\n        try:\n            stagehand = Stagehand(model_name=model)\n            await stagehand.init()\n            actions = await stagehand.page.observe(prompt)\n            action = actions[0]\n            await stagehand.page.act(action)\n            return\n        except Exception:\n            print(f\"Falling back to {model}...\")\n```\n</CodeGroup>\n\n### Session Pooling\n\nReuse browser sessions across multiple tasks:\n\n<CodeGroup>\n```typescript TypeScript\nclass SessionManager {\n  private sessions = new Map<string, Stagehand>();\n  \n  async getSession(taskType: string): Promise<Stagehand> {\n    if (this.sessions.has(taskType)) {\n      return this.sessions.get(taskType)!;\n    }\n    \n    const stagehand = new Stagehand({ env: \"BROWSERBASE\" });\n    await stagehand.init();\n    this.sessions.set(taskType, stagehand);\n    return stagehand;\n  }\n}\n```\n```python Python\nclass SessionManager:\n    def __init__(self):\n        self.sessions = {}\n    \n    async def get_session(self, task_type: str):\n        if task_type in self.sessions:\n            return self.sessions[task_type]\n        \n        stagehand = Stagehand(env=\"BROWSERBASE\")\n        await stagehand.init()\n        self.sessions[task_type] = stagehand\n        return stagehand\n```\n</CodeGroup>\n\n## Cost Monitoring\n\nTrack your spending to identify optimization opportunities. See our [Observability Guide](/configuration/observability) for detailed metrics:\n\n<CodeGroup>\n```typescript TypeScript\n// Monitor token usage\nconst metrics = stagehand.metrics;\nconsole.log(`Total tokens: ${metrics.totalPromptTokens + metrics.totalCompletionTokens}`);\nconsole.log(`Estimated cost: $${(metrics.totalPromptTokens + metrics.totalCompletionTokens) * 0.00001}`);\n```\n```python Python\n# Monitor token usage\nmetrics = stagehand.metrics\ntotal_tokens = metrics['total_prompt_tokens'] + metrics['total_completion_tokens']\nprint(f\"Total tokens: {total_tokens}\")\nprint(f\"Estimated cost: ${total_tokens * 0.00001:.4f}\")\n```\n</CodeGroup>\n\n<CardGroup cols={1}>\n<Card title=\"Observability & Metrics\" icon=\"chart-line\" href=\"/v2/configuration/observability\">\n  Monitor usage patterns and track costs in real-time\n</Card>\n</CardGroup>\n\n## Budget Controls\n\nSet spending limits to prevent unexpected costs:\n\n<CodeGroup>\n```typescript TypeScript\nclass BudgetGuard {\n  private dailySpend = 0;\n  private maxDailyBudget: number;\n  \n  constructor(maxDailyBudget: number = 25) {\n    this.maxDailyBudget = maxDailyBudget;\n  }\n  \n  checkBudget(estimatedCost: number): void {\n    if (this.dailySpend + estimatedCost > this.maxDailyBudget) {\n      throw new Error(`Daily budget exceeded: $${this.maxDailyBudget}`);\n    }\n    this.dailySpend += estimatedCost;\n  }\n}\n```\n```python Python\nclass BudgetGuard:\n    def __init__(self, max_daily_budget: float = 25.0):\n        self.daily_spend = 0\n        self.max_daily_budget = max_daily_budget\n    \n    def check_budget(self, estimated_cost: float) -> None:\n        if self.daily_spend + estimated_cost > self.max_daily_budget:\n            raise Exception(f\"Daily budget exceeded: ${self.max_daily_budget}\")\n        self.daily_spend += estimated_cost\n```\n</CodeGroup>\n\n\n## Related Resources\n\n<CardGroup cols={2}>\n<Card title=\"Model Selection Guide\" icon=\"brain\" href=\"/v2/configuration/models\">\n  Choose the right LLM for your budget and accuracy requirements\n</Card>\n\n<Card title=\"Caching Strategies\" icon=\"database\" href=\"/v2/best-practices/caching\">\n  Reduce costs with smart action caching and observe patterns\n</Card>\n\n<Card title=\"Observability & Metrics\" icon=\"chart-line\" href=\"/v2/configuration/observability\">\n  Monitor usage patterns and track costs in real-time\n</Card>\n\n<Card title=\"Browser Configuration\" icon=\"window-maximize\" href=\"/v2/configuration/browser\">\n  Optimize Browserbase infrastructure costs and session management\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/best-practices/deployments.mdx",
    "content": "---\ntitle: 'Deploying Stagehand'\ndescription: 'Deploy your AI agents and automations to the cloud'\n---\n\n<Tip>\n**🌟 Preview: Browser Functions** - Deploy your web automation code directly on Browserbase with browser functions. Scale your `act()` automations in the cloud with zero infrastructure setup. Reach out to hello@browserbase.com to get beta access.\n</Tip>\n\n## Deploy on Vercel\n\nSecurely run Stagehand on Browserbase inside a Vercel Function. This guide shows a minimal, production-safe HTTP endpoint you can call directly or on a schedule.\n\n### 1. Install Vercel CLI\n\nTo download and install Vercel CLI, run one of the following commands:\n\n<CodeGroup>\n```bash pnpm\npnpm i -g vercel\n```\n```bash yarn\nyarn global add vercel\n```\n```bash npm\nnpm i -g vercel\n```\n```bash bun\nbun add -g vercel\n```\n</CodeGroup>\n\n### 2. Project layout\n\n```text\nyour-project/\n  api/\n    run.ts\n  package.json\n  tsconfig.json\n  vercel.json\n```\n\nCreate the structure with:\n\n```bash\nmkdir -p api\ntouch api/run.ts package.json vercel.json tsconfig.json\n```\n\n### 3. `api/run.ts` (Node.js runtime)\n\n```typescript\n// api/run.ts\nimport type { VercelRequest, VercelResponse } from \"@vercel/node\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod/v3\";\n\nexport default async function handler(req: VercelRequest, res: VercelResponse): Promise<void> {\n  try {\n    const stagehand = new Stagehand({\n      env: \"BROWSERBASE\",\n      apiKey: process.env.BROWSERBASE_API_KEY!,\n      projectId: process.env.BROWSERBASE_PROJECT_ID!,\n      disablePino: true,\n      modelName: \"google/gemini-2.5-flash\",\n      modelClientOptions: {\n        apiKey: process.env.GOOGLE_API_KEY!,\n      },\n      // optional session params\n      browserbaseSessionCreateParams: {\n        projectId: process.env.BROWSERBASE_PROJECT_ID!,\n        region: \"us-west-2\",\n        browserSettings: {\n          blockAds: true,\n        },\n      },\n    });\n\n    await stagehand.init();\n    const page = stagehand.page;\n\n    await page.goto(\"https://www.stagehand.dev/\");\n    await page.act(\"click the evals button\");\n\n    const { extraction } = await page.extract(\"extract the fastest model\");\n    const data = { model: extraction ?? \"\" };\n\n    await stagehand.close();\n\n    res.status(200).json({ ok: true, data: data.model });\n  } catch (err: unknown) {\n    const msg = err instanceof Error ? err.message : String(err);\n    res.status(500).json({ ok: false, error: msg });\n  }\n}\n```\n\n### 4. `package.json`\n\n```json\n{\n    \"name\": \"bb-stagehand-on-vercel\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"engines\": { \"node\": \">=18\" },\n    \"dependencies\": {\n      \"@browserbasehq/stagehand\": \"^2.4.3\",\n      \"zod\": \"^3.25.0\"\n    },\n    \"devDependencies\": {\n      \"typescript\": \"^5.6.0\",\n      \"@types/node\": \"^20.12.12\",\n      \"@vercel/node\": \"^3.2.20\"\n    }\n}\n```\n\n### 5. `tsconfig.json`\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"node\",\n    \"outDir\": \".vercel/output/functions\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"api/**/*.ts\"]\n}\n```\n\n### 6. `vercel.json`\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"functions\": {\n    \"api/run.ts\": {\n      \"maxDuration\": 60\n    }\n  }\n}\n```\n\nSee Vercel's [configuring functions](https://vercel.com/docs/functions/configuring-functions) docs for more details.\n\n### 7. Link your project\n\nLink your local folder to a Vercel project before configuring environment variables:\n\n```bash\n# authenticate if needed\nvercel login\n\n# link the current directory to a Vercel project (interactive)\nvercel link\n```\n\n### 8. Environment variables\n\nDo not commit `.env` in production. Add variables via Vercel CLI:\n\n```bash\nvercel env add BROWSERBASE_API_KEY\nvercel env add BROWSERBASE_PROJECT_ID\n# (and your model key if needed)\nvercel env add GOOGLE_API_KEY\n```\n\nSee also: [Browser Environment](/configuration/environment) for details on required variables.\n\n### 9. Test locally\n\nReplicate the Vercel environment locally to exercise your Function before deploying. Run from the project root.\n\n```bash\n# ensure dependencies are installed\nnpm install\n\n# start the local Vercel dev server\nvercel dev --listen 5005\n```\n\n### 10. Deploy\n\n```bash\nvercel\nvercel --prod\n```\n\n### Execute the function\n\n#### Configure Protection Bypass for Automation\n\nBefore invoking the production URL, create a Protection Bypass for Automation:\n\n1. Generate a 32-character secret (you can use `openssl rand -hex 16`)\n2. Go to your project in Vercel\n3. Navigate to Settings → Deployment Protection\n4. Add the secret to \"Protection Bypass for Automation\"\n\nThen invoke the function with the bypass header:\n\n```bash\ncurl -X POST \\\n  -H \"x-vercel-protection-bypass: <your-32-character-secret>\" \\\n  https://<your-deployment>/api/run\n```\n\n### Optional: Cron on Vercel\n\nHit the same endpoint on a schedule by extending `vercel.json`:\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"functions\": {\n    \"api/run.ts\": {\n      \"maxDuration\": 60\n    }\n  }\n  },\n  \"crons\": [\n    { \"path\": \"/api/run\", \"schedule\": \"0 * * * *\" }\n  ]\n}\n```\n\n### Features\n- **No local browsers needed** with `env: \"BROWSERBASE\"`. [Browserbase](https://www.browserbase.com/) provides the browsers.\n- **Fast functionality**: Offload browser work to Browserbase and return JSON promptly.\n- **Long-running tasks**: Raise `maxDuration` and/or consider Edge runtime limits depending on plan.\n\n"
  },
  {
    "path": "packages/docs/v2/best-practices/mcp-integrations.mdx",
    "content": "---\ntitle: \"MCP Integrations\"\ndescription: \"Using Model Context Protocol (MCP) integrations to enhance agent capabilities\"\n---\n\n## What are MCP Integrations?\n\nMCP (Model Context Protocol) integrations allow you to connect your Stagehand agents to external tools, APIs, and services. This enables agents to perform actions beyond browser automation, such as web search, database operations, and API calls.\n\n<Info>\nMCP integrations make your agents more powerful by combining browser automation with external capabilities. The agent can intelligently decide when to use browser actions versus external tools.\n</Info>\n\n## Connection Options\n\nThere are two options for connecting to MCP servers:\n\n1. **Pass a URL directly** - The simplest approach for quick setup\n2. **Create a connection first** - Gives you more control over the connection\n\n<Note>\nMCP client support is currently only available in TypeScript.\n</Note>\n\n## Passing a URL\n\nThe simplest way to add MCP integrations is by providing server URLs directly in the agent configuration:\n\n```typescript\nconst agent = stagehand.agent({\n  provider: \"openai\",\n  model: \"computer-use-preview\",\n  integrations: [\n    `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n  ],\n  instructions: `You have access to web search through Exa. Use it to find current information before browsing.`,\n  options: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n\nawait agent.execute(\"Search for the best headphones of 2025 and go through checkout for the top recommendation\");\n```\n\n## Creating a Connection First\n\nAlternatively, you can establish MCP connections first and then pass the client objects:\n\n```typescript\nimport { connectToMCPServer } from \"@browserbasehq/stagehand\";\n\n// Connect to MCP server\nconst supabaseClient = await connectToMCPServer(\n  `https://server.smithery.ai/@supabase-community/supabase-mcp/mcp?api_key=${process.env.SMITHERY_API_KEY}`\n);\n\n// You can also pass the config to start a local MCP server\nconst notionClient = await connectToMCPServer({\n  command: \"npx\",\n  args: [\"-y\", \"@notionhq/notion-mcp-server\"],\n  env: {\n    NOTION_TOKEN: process.env.NOTION_TOKEN,\n  },\n});\n\n// Use the connected client\nconst agent = stagehand.agent({\n  provider: \"openai\", \n  model: \"computer-use-preview\",\n  integrations: [supabaseClient, notionClient],\n  instructions: `You can interact with Supabase databases and Notion. Use these tools to store and retrieve data.`,\n  options: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n\nawait agent.execute(\"Search for restaurants in New Brunswick, NJ and save the first result to the database\");\n```\n\n\n\n## Multiple Integrations\n\nYou can combine multiple MCP integrations in a single agent:\n\n```typescript\nconst databaseClient = await connectToMCPServer(/* database config */);\n\nconst agent = stagehand.agent({\n  integrations: [\n    `https://search-service.example.com/mcp?apiKey=${process.env.SEARCH_API_KEY}`,\n    databaseClient\n  ],\n  instructions: `You have access to external tools for search and data storage. Use these tools strategically to complete tasks efficiently.`\n});\n```\n\n## Best Practices\n\n### Choose the Right Connection Approach\n<Tabs>\n<Tab title=\"Passing a URL\">\n**When to use:**\n- Simple setup requirements\n- Standard API configurations\n- Getting started quickly\n\n**Benefits:**\n- Minimal code required\n- Automatic connection handling\n- Easy to configure\n</Tab>\n\n<Tab title=\"Creating a Connection First\">\n**When to use:**\n- Custom connection options\n- Connection reuse across agents\n- Advanced error handling\n\n**Benefits:**\n- Full control over connections\n- Better error handling\n- Connection pooling capabilities\n</Tab>\n</Tabs>\n\n### Environment Variables\n\nAlways use environment variables for API keys and sensitive information:\n\n```bash\n# .env file\nSEARCH_API_KEY=your_search_service_key\nMCP_SERVICE_API_KEY=your_mcp_service_key\nOPENAI_API_KEY=your_openai_key\nDATABASE_URL=your_database_url\nDATABASE_API_KEY=your_database_key\n```\n\n### Instructions Best Practices\n\nProvide clear instructions about available tools:\n\n<Tabs>\n<Tab title=\"Good Instructions\">\n```typescript\ninstructions: `You have access to:\n1. Web search tools - Use to find current information\n2. Database tools - Use to store/retrieve data\n3. Browser automation - Use for web interactions\n\nAlways search for current information before making decisions.\nStore important data for later reference.`\n```\n</Tab>\n\n<Tab title=\"Poor Instructions\">\n```typescript\ninstructions: \"You can search and save data.\"\n```\n</Tab>\n</Tabs>\n\n### Error Handling\n\nImplement proper error handling for MCP connections:\n\n```typescript\ntry {\n  const client = await connectToMCPServer(serverUrl);\n  \n  const agent = stagehand.agent({\n    integrations: [client],\n    // ... other config\n  });\n  \n  const result = await agent.execute(instruction);\n} catch (error) {\n  console.error(\"MCP integration failed:\", error);\n  // Handle fallback behavior\n}\n```\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Connection timeouts\">\n**Problem:** MCP server connections timing out\n\n**Solutions:**\n- Verify server URLs are correct and accessible\n- Check network connectivity\n- Ensure API keys are valid and have proper permissions\n- Try connecting to servers individually to isolate issues\n</Accordion>\n\n<Accordion title=\"Tool not being used\">\n**Problem:** Agent not using available MCP tools\n\n**Solutions:**\n- Make instructions more specific about when to use tools\n- Ensure API keys are properly configured\n- Check that the MCP server supports the expected tools\n- Verify tool descriptions are clear and actionable\n</Accordion>\n\n<Accordion title=\"Authentication errors\">\n**Problem:** API key or authentication failures\n\n**Solutions:**\n- Verify all required environment variables are set\n- Check API key validity and permissions  \n- Ensure URLs include necessary authentication parameters\n- Test MCP connections independently before using in agents\n</Accordion>\n</AccordionGroup>\n\n## Examples\n\n### Web Search + Browser Automation\n```typescript\nconst agent = stagehand.agent({\n  integrations: [`https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`],\n  instructions: `First search for current information, then use the browser to complete tasks based on what you find.`\n});\n\nawait agent.execute(\"Find the best laptop deals for 2025 and navigate to purchase the top recommendation\");\n```\n\n### Data Extraction + Storage\n```typescript\nconst supabaseClient = await connectToMCPServer(/* config */);\n\nconst agent = stagehand.agent({\n  integrations: [supabaseClient],\n  instructions: `Extract data from websites and store it using available database tools.`\n});\n\nawait agent.execute(\"Extract all restaurant information from this directory and save it to the database\");\n```\n\n### Multi-tool Workflow\n```typescript\nconst agent = stagehand.agent({\n  integrations: [\n    `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n    supabaseClient\n  ],\n  instructions: `Use all available tools strategically: search for current info, browse websites, and store important data.`\n});\n\nawait agent.execute(\"Research competitor pricing, compare with our site, and store the analysis\");\n```\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Agent Basics\" icon=\"robot\" href=\"/v2/basics/agent\">\n  Learn the fundamentals of Stagehand agents\n</Card>\n\n<Card title=\"MCP Server Setup\" icon=\"server\" href=\"/v2/integrations/mcp/setup\">  \n  Set up your own MCP server\n</Card>\n\n<Card title=\"Custom Tools\" icon=\"wrench\" href=\"/v2/integrations/mcp/tools\">\n  Create custom MCP tools\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/best-practices/playwright-interop.mdx",
    "content": "---\ntitle: 'Playwright Interoperability'\ndescription: 'How Stagehand interacts with Playwright'\n---\n\nStagehand is built on top of [Playwright](https://playwright.dev/), so you can use Playwright methods directly through the Stagehand instance.\n\n## `page` and `context`\n\n`stagehand.page` and `stagehand.context` are instances of Playwright's `Page` and `BrowserContext` respectively. Use these methods to interact with the Playwright instance that Stagehand is using.\n\n<CodeGroup>\n```TypeScript TypeScript\nconst page = stagehand.page;\n// Base Playwright methods work\nawait page.goto(\"https://github.com/browserbase/stagehand\");\n\n// Stagehand overrides Playwright objects\nawait page.act(\"click on the contributors\")\n```\n\n```python Python\npage = stagehand.page\n# Base Playwright methods work\nawait page.goto(\"https://github.com/browserbase/stagehand\")\n\n# Stagehand overrides Playwright objects\nawait page.act(\"click on the contributors\")\n```\n</CodeGroup>\n\n## Stagehand v. Playwright\nBelow is an example of how to extract a list of companies from the AI Grant website using both Stagehand and Playwright.\n\n<img src=\"/images/stagehand-playwright.png\" alt=\"Stagehand v. Playwright\" />\n\nThe above example with Stagehand can be easily reused to extract data from other websites, whereas the Playwright example would need to be rewritten for each new website."
  },
  {
    "path": "packages/docs/v2/best-practices/prompting-best-practices.mdx",
    "content": "---\ntitle: Prompting Best Practices\ndescription: \"Write effective prompts for reliable Stagehand automation\"\n---\n\nGood prompts make Stagehand reliable. Bad prompts cause failures. Here's how to write prompts that work consistently.\n\n## Act Method\n\nUse `act()` for single actions on web pages. Each action should be focused and clear.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Single, specific actions\nawait page.act(\"click the 'Add to Cart' button\");\nawait page.act(\"type 'user@example.com' into the email field\");\n\n// Bad - Multiple actions combined\nawait page.act(\"fill out the form and submit it\");\nawait page.act(\"login with credentials and navigate to dashboard\");\n```\n\n```python Python\n# Good - Single, specific actions\nawait page.act(\"click the 'Add to Cart' button\")\nawait page.act(\"type 'user@example.com' into the email field\")\n\n# Bad - Multiple actions combined\nawait page.act(\"fill out the form and submit it\")\nawait page.act(\"login with credentials and navigate to dashboard\")\n```\n</CodeGroup>\n\n### Use Element Types, Not Colors\n\nDescribe elements by their type and function rather than visual attributes like color.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Element types and descriptive text\nawait page.act(\"click the 'Sign In' button\");\nawait page.act(\"type into the email input field\");\n\n// Bad - Color-based descriptions\nawait page.act(\"click the blue button\");\nawait page.act(\"type into the white input\");\n```\n\n```python Python\n# Good - Element types and descriptive text\nawait page.act(\"click the 'Sign In' button\")\nawait page.act(\"type into the email input field\")\n\n# Bad - Color-based descriptions\nawait page.act(\"click the blue button\")\nawait page.act(\"type into the white input\")\n```\n</CodeGroup>\n\n### Use Descriptive Language\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Clear element identification\nawait page.act(\"click the 'Next' button at the bottom of the form\");\nawait page.act(\"type into the search bar at the top of the page\");\n\n// Bad - Vague descriptions\nawait page.act(\"click next\");\nawait page.act(\"type into search\");\n```\n\n```python Python\n# Good - Clear element identification\nawait page.act(\"click the 'Next' button at the bottom of the form\")\nawait page.act(\"type into the search bar at the top of the page\")\n\n# Bad - Vague descriptions\nawait page.act(\"click next\")\nawait page.act(\"type into search\")\n```\n</CodeGroup>\n\n### Choose the Right Action Verbs\n\n- **Click** for buttons, links, checkboxes\n- **Type** for text inputs\n- **Select** for dropdowns\n- **Check/uncheck** for checkboxes\n- **Upload** for file inputs\n\n<CodeGroup>\n```typescript TypeScript\n// Good\nawait page.act(\"click the submit button\");\nawait page.act(\"select 'Option 1' from dropdown\");\n\n// Bad\nawait page.act(\"click submit\");\nawait page.act(\"choose option 1\");\n```\n\n```python Python\n# Good\nawait page.act(\"click the submit button\")\nawait page.act(\"select 'Option 1' from dropdown\")\n\n# Bad\nawait page.act(\"click submit\")\nawait page.act(\"choose option 1\")\n```\n</CodeGroup>\n\n### Protect Sensitive Data\n\nVariables keep sensitive information out of prompts and logs.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Secure approach\nawait page.act({\n  action: \"enter %username% in the email field\",\n  variables: {\n    username: \"user@example.com\"\n  }\n});\n\nawait page.act({\n  action: \"enter %password% in the password field\", \n  variables: {\n    password: process.env.USER_PASSWORD\n  }\n});\n\n// Bad - Insecure approach\nawait page.act(\"type 'mySecretPassword123' into the password field\");\n```\n\n```python Python\nimport os\n\n# Good - Secure approach\nawait page.act(\n  \"enter %username% in the email field\",\n  variables={\n    \"username\": \"user@example.com\"\n  }\n)\n\nawait page.act(\n  \"enter %password% in the password field\",\n  variables={\n    \"password\": os.environ.get(\"USER_PASSWORD\")\n  }\n)\n\n# Bad - Insecure approach\nawait page.act(\"type 'mySecretPassword123' into the password field\")\n```\n</CodeGroup>\n\n<Warning>\nSet `verbose: 0` in your Stagehand config to prevent secrets from appearing in logs.\n</Warning>\n\n## Extract Method\n\nUse `extract()` to pull structured data from pages. Define clear schemas and provide context.\n\n### Schema Best Practices\n\nUse descriptive field names, correct types, and detailed descriptions. Field descriptions provide context that helps the agent understand exactly what to extract.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Descriptive names, correct types, and helpful descriptions\nconst productData = await page.extract({\n  instruction: \"Extract product information\",\n  schema: z.object({\n    productTitle: z.string().describe(\"The main product name displayed on the page\"),\n    priceInDollars: z.number().describe(\"Current selling price as a number, without currency symbol\"),\n    isInStock: z.boolean().describe(\"Whether the product is available for purchase\")\n  })\n});\n\n// Bad - Generic names, wrong types, no descriptions\nconst data = await page.extract({\n  instruction: \"Get product details\", \n  schema: z.object({\n    name: z.string(), // Too generic, no context\n    price: z.string(), // Should be number\n    stock: z.string() // Should be boolean, no context\n  })\n});\n```\n\n```python Python\nfrom pydantic import BaseModel, Field\n\n# Good - Descriptive names, correct types, and helpful descriptions\nclass ProductData(BaseModel):\n    productTitle: str = Field(description=\"The main product name displayed on the page\")\n    priceInDollars: float = Field(description=\"Current selling price as a number, without currency symbol\")\n    isInStock: bool = Field(description=\"Whether the product is available for purchase\")\n\nproductData = await page.extract(\n  \"Extract product information\",\n  schema=ProductData\n)\n\n# Bad - Generic names, wrong types, no descriptions\nclass Data(BaseModel):\n    name: str      # Too generic, no context\n    price: str     # Should be float, no context\n    stock: str     # Should be bool, no context\n\ndata = await page.extract(\n  \"Get product details\",\n  schema=Data\n)\n```\n</CodeGroup>\n\n### Handle Arrays Correctly\n\nAlways wrap schemas in objects for reliable extraction.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Array wrapped in object\nconst listings = await page.extract({\n  instruction: \"Extract all apartment listings\",\n  schema: z.object({\n    apartments: z.array(z.object({\n      address: z.string(),\n      rent: z.number()\n    }))\n  })\n});\n\n// Bad - Bare array\nconst listings = await page.extract({\n  instruction: \"Extract apartment listings\",\n  schema: z.array(z.string()) // Don't do this\n});\n```\n\n```python Python\nfrom pydantic import BaseModel\nfrom typing import List\n\n# Good - Array wrapped in object\nclass Apartment(BaseModel):\n    address: str\n    rent: float\n\nclass Listings(BaseModel):\n    apartments: List[Apartment]\n\nlistings = await page.extract(\n  \"Extract all apartment listings\",\n  schema=Listings\n)\n\n# Bad - Bare array (not supported)\n# Don't do this - arrays must be wrapped in objects\n```\n</CodeGroup>\n\n### Use Proper URL Types\n\nSpecify URL types to tell Stagehand to extract URLs. Without proper URL types, Stagehand won't extract URLs.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Tells Stagehand to extract URLs\nconst links = await page.extract({\n  instruction: \"Extract navigation links\",\n  schema: z.object({\n    links: z.array(z.object({\n      text: z.string(),\n      url: z.string().url() // Required for URL extraction\n    }))\n  })\n});\n```\n\n```python Python\nfrom pydantic import BaseModel, HttpUrl\nfrom typing import List\n\n# Good - Tells Stagehand to extract URLs\nclass Link(BaseModel):\n    text: str\n    url: HttpUrl  # Required for URL extraction\n\nclass Links(BaseModel):\n    links: List[Link]\n\nlinks = await page.extract(\n  \"Extract navigation links\",\n  schema=Links\n)\n```\n</CodeGroup>\n\n## Observe Method\n\nUse `observe()` to discover actionable elements before acting on them.\n\n### Check Elements First\n\nVerify elements exist before taking action to avoid errors.\n\n<CodeGroup>\n```typescript TypeScript\n// Check for elements first\nconst loginButtons = await page.observe(\"Find the login button\");\n\nif (loginButtons.length > 0) {\n  await page.act(loginButtons[0]);\n} else {\n  console.log(\"No login button found\");\n}\n```\n\n```python Python\n# Check for elements first\nlogin_buttons = await page.observe(\"Find the login button\")\n\nif len(login_buttons) > 0:\n    await page.act(login_buttons[0])\nelse:\n    print(\"No login button found\")\n```\n</CodeGroup>\n\n### Be Specific About Element Types\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Specific element types\nconst submitButtons = await page.observe(\"Find submit button in the form\");\nconst dropdowns = await page.observe(\"Find the state dropdown menu\");\n\n// Bad - Too vague\nconst elements = await page.observe(\"Find submit stuff\");\nconst things = await page.observe(\"Find state selection\");\n```\n\n```python Python\n# Good - Specific element types\nsubmit_buttons = await page.observe(\"Find submit button in the form\")\ndropdowns = await page.observe(\"Find the state dropdown menu\")\n\n# Bad - Too vague\nelements = await page.observe(\"Find submit\")\nthings = await page.observe(\"Find state selection\")\n```\n</CodeGroup>\n\n## Agent Method\n\nUse `agent()` for complex, multi-step workflows. Provide detailed instructions and set appropriate limits.\n\n### Navigate First\n\nDon't include navigation in agent tasks. Handle it separately.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Navigate first\nawait page.goto('https://amazon.com');\nawait agent.execute('Search for wireless headphones under $100 and add the best rated one to cart');\n\n// Bad - Navigation in task\nawait agent.execute('Go to Amazon, search for headphones, and add one to cart');\n```\n\n```python Python\n# Good - Navigate first\nawait page.goto('https://amazon.com')\nawait agent.execute('Search for wireless headphones under $100 and add the best rated one to cart')\n\n# Bad - Navigation in task\nawait agent.execute('Go to Amazon, search for headphones, and add one to cart')\n```\n</CodeGroup>\n\n### Be Highly Specific\n\nDetailed instructions lead to better results.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Detailed instructions\nawait agent.execute({\n  instruction: \"Find Italian restaurants in Brooklyn that are open after 10pm, have outdoor seating, and are rated 4+ stars. Save the top 3 results.\",\n  maxSteps: 25\n});\n\n// Bad - Vague instructions\nawait agent.execute(\"Find some good restaurants\");\n```\n\n```python Python\n# Good - Detailed instructions\nawait agent.execute(\n  instruction=\"Find Italian restaurants in Brooklyn that are open after 10pm, have outdoor seating, and are rated 4+ stars. Save the top 3 results.\",\n  max_steps=25\n)\n\n# Bad - Vague instructions\nawait agent.execute(\"Find some good restaurants\")\n```\n</CodeGroup>\n\n### Set Appropriate Step Limits\n\nMatch step limits to task complexity.\n\n<CodeGroup>\n```typescript TypeScript\n// Simple task - fewer steps\nawait agent.execute({\n  instruction: \"Subscribe to the newsletter with email 'user@example.com'\",\n  maxSteps: 10\n});\n\n// Complex task - more steps  \nawait agent.execute({\n  instruction: \"Research and compare 5 project management tools with pricing and features\",\n  maxSteps: 50\n});\n```\n\n```python Python\n# Simple task - fewer steps\nawait agent.execute(\n  instruction=\"Subscribe to the newsletter with email 'user@example.com'\",\n  max_steps=10\n)\n\n# Complex task - more steps\nawait agent.execute(\n  instruction=\"Research and compare 5 project management tools with pricing and features\",\n  max_steps=50\n)\n```\n</CodeGroup>\n\n### Include Success Criteria\n\nTell the agent how to know when it's done.\n\n<CodeGroup>\n```typescript TypeScript\n// Good - Clear success criteria\nawait agent.execute({\n  instruction: \"Add 3 smartphone cases to cart and confirm the cart shows exactly 3 items with total price\",\n  maxSteps: 20\n});\n\n// Bad - No validation\nawait agent.execute(\"Add some items to cart\");\n```\n\n```python Python\n# Good - Clear success criteria\nawait agent.execute(\n  instruction=\"Add 3 smartphone cases to cart and confirm the cart shows exactly 3 items with total price\",\n  max_steps=20\n)\n\n# Bad - No validation\nawait agent.execute(\"Add some items to cart\")\n```\n</CodeGroup>\n\n## Common Mistakes to Avoid\n\n- **Combining multiple actions** - Keep each `act()` call to one action\n- **Using vague descriptions** - Be specific about which elements to interact with  \n- **Exposing sensitive data** - Always use variables for credentials\n- **Skipping validation** - Check results before proceeding\n\n## Testing Your Prompts\n\n1. **Start simple** - Test basic functionality first\n2. **Add complexity gradually** - Build up to complex workflows\n3. **Monitor results** - Use logging to understand what's happening\n4. **Iterate based on failures** - Refine prompts when they don't work\nRemember: Good prompting is iterative. When in doubt, be more specific rather than less."
  },
  {
    "path": "packages/docs/v2/best-practices/speed-optimization.mdx",
    "content": "---\ntitle: Speed Optimization\nsidebarTitle: Speed Optimization\ndescription: Optimize Stagehand performance for faster automation and reduced latency\n---\n\nStagehand performance depends on several factors: DOM processing speed, LLM inference time, browser operations, and network latency. This guide provides proven strategies to maximize automation speed.\n\n## Quick Performance Wins\n\n### 1. Plan Ahead with Observe\n\n\nUse a single `observe()` call to plan multiple actions, then execute them efficiently:\n\n<CodeGroup>\n```typescript TypeScript\n// Instead of sequential operations with multiple LLM calls\nawait page.act(\"Fill name field\");        // LLM call #1\nawait page.act(\"Fill email field\");       // LLM call #2\nawait page.act(\"Select country dropdown\"); // LLM call #3\n\n// Use single observe to plan all form fields - one LLM call\nconst formFields = await page.observe(\"Find all form fields to fill\");\n\n// Execute all actions without LLM inference\nfor (const field of formFields) {\n  await page.act(field); // No LLM calls!\n}\n```\n```python Python\nimport asyncio\n\n# Instead of sequential operations with multiple LLM calls\nawait page.act(\"Fill name field\")        # LLM call #1\nawait page.act(\"Fill email field\")       # LLM call #2  \nawait page.act(\"Select country dropdown\") # LLM call #3\n\n# Use single observe to plan all form fields - one LLM call\nform_fields = await page.observe(\"Find all form fields to fill\")\n\n# Execute all actions without LLM inference\nfor field in form_fields:\n    await page.act(field) # No LLM calls!\n\n```\n</CodeGroup>\n\n<Note>\n**Performance Tip**: Acting on `observe` results avoids LLM inference entirely. This approach is 2-3x faster than direct `act()` calls and is the recommended pattern for multi-step workflows.\n</Note>\n\n<Card title=\"Caching Guide\" icon=\"database\" href=\"/v2/best-practices/caching\">\n  Learn advanced caching patterns and cache invalidation strategies\n</Card>\n\n### 2. Optimize DOM Processing\n\nReduce DOM complexity before Stagehand processes the page:\n\n<CodeGroup>\n```typescript TypeScript\n// Remove heavy elements that slow down processing\nawait page.evaluate(() => {\n  // Remove video elements\n  document.querySelectorAll('video, iframe').forEach(el => el.remove());\n  \n  // Hide complex animations\n  document.querySelectorAll('[style*=\"animation\"]').forEach(el => {\n    (el as HTMLElement).style.animation = 'none';\n  });\n});\n\n// Then perform Stagehand operations\nawait page.act(\"Click the submit button\");\n```\n```python Python\n# Remove heavy elements that slow down processing\nawait page.evaluate(\"\"\"\n() => {\n  // Remove video elements\n  document.querySelectorAll('video, iframe').forEach(el => el.remove());\n  \n  // Hide complex animations\n  document.querySelectorAll('[style*=\"animation\"]').forEach(el => {\n    el.style.animation = 'none';\n  });\n}\n\"\"\")\n\n# Then perform Stagehand operations\nawait page.act(\"Click the submit button\")\n```\n</CodeGroup>\n\n### 3. Set Appropriate Timeouts\n\nUse shorter timeouts for simple operations and longer ones for complex page loads:\n\n<CodeGroup>\n```typescript TypeScript\n// Simple actions - reduce action timeout\nawait page.act({ \n  instruction: \"Click the login button\",\n  actTimeout: 5000  // Default is 30000ms, reduce for simple clicks\n});\n\n// Complex page loads - optimize navigation\nawait page.goto(\"https://heavy-spa.com\", {\n  waitUntil: \"domcontentloaded\", // Don't wait for all resources\n  timeout: 15000 // Shorter than default 30s\n});\n```\n```python Python\n# Simple actions - reduce action timeout\nawait page.act(\"Click button\", act_timeout=5000)\n\n\n# Complex page loads - optimize navigation\nawait page.goto(\"https://heavy-spa.com\", \n    wait_until=\"domcontentloaded\",\n    timeout=15000\n)\n```\n</CodeGroup>\n\n## Advanced Performance Strategies\n\n\n### Smart Model Selection\n\nUse faster models for simple tasks, premium models only when needed:\n\n<CodeGroup>\n```typescript TypeScript\nclass SpeedOptimizedStagehand {\n  private fastModel: Stagehand;\n  private premiumModel: Stagehand;\n\n  async smartAct(page: Page, prompt: string, complexity: 'simple' | 'complex') {\n    const model = complexity === 'simple' ? this.fastModel : this.premiumModel;\n    return await model.page.act(prompt);\n  }\n}\n\n// Use fast model for simple clicks/forms\nawait stagehand.smartAct(page, \"Click submit\", 'simple');\n\n// Use premium model for complex reasoning\nawait stagehand.smartAct(page, \"Find the cheapest flight option\", 'complex');\n```\n```python Python\nclass SpeedOptimizedStagehand:\n    def __init__(self):\n        self.fast_model = Stagehand(model_name=\"fast-model\")\n        self.premium_model = Stagehand(model_name=\"premium-model\")\n    \n    async def smart_act(self, page, prompt: str, complexity: str):\n        model = self.fast_model if complexity == 'simple' else self.premium_model\n        return await model.page.act(prompt)\n\n# Use fast model for simple clicks/forms\nawait stagehand.smart_act(page, \"Click submit\", 'simple')\n\n# Use premium model for complex reasoning  \nawait stagehand.smart_act(page, \"Find the cheapest flight option\", 'complex')\n```\n</CodeGroup>\n\n<Card title=\"Model Configuration\" icon=\"brain\" href=\"/v2/configuration/models\">\n  Compare model performance and costs\n</Card>\n\n### Page Load Optimization\n\nSkip unnecessary resources during page loads:\n\n<CodeGroup>\n```typescript TypeScript\n// Block heavy resources globally\nawait context.route('**/*', (route) => {\n  const resourceType = route.request().resourceType();\n  if (['image', 'font', 'media'].includes(resourceType)) {\n    route.abort();\n  } else {\n    route.continue();\n  }\n});\n\n// Use faster navigation\nawait page.goto(url, { \n  waitUntil: 'domcontentloaded',  // Don't wait for images/fonts\n  timeout: 10000 \n});\n```\n```python Python\n# Block heavy resources globally\nasync def handle_route(route):\n    resource_type = route.request.resource_type\n    if resource_type in ['image', 'font', 'media']:\n        await route.abort()\n    else:\n        await route.continue_()\n\nawait context.route('**/*', handle_route)\n\n# Use faster navigation\nawait page.goto(url, \n    wait_until='domcontentloaded',  # Don't wait for images/fonts\n    timeout=10000\n)\n```\n</CodeGroup>\n<Card title=\"Cost Optimization\" icon=\"dollar-sign\" href=\"/v2/best-practices/cost-optimization\">\n  Balance speed with cost considerations\n</Card>\n\n## Performance Monitoring and Benchmarking\n\nTrack performance metrics and measure optimization impact:\n\n### Performance Tracking\n\n<CodeGroup>\n```typescript TypeScript\nclass PerformanceTracker {\n  private speedMetrics: Map<string, number[]> = new Map();\n\n  async timedAct(page: Page, prompt: string): Promise<ActResult> {\n    const start = Date.now();\n    const result = await page.act(prompt);\n    const duration = Date.now() - start;\n    \n    if (!this.speedMetrics.has(prompt)) {\n      this.speedMetrics.set(prompt, []);\n    }\n    this.speedMetrics.get(prompt)!.push(duration);\n    \n    console.log(`Action \"${prompt}\" took ${duration}ms`);\n    return result;\n  }\n\n  getAverageTime(prompt: string): number {\n    const times = this.speedMetrics.get(prompt) || [];\n    return times.reduce((a, b) => a + b, 0) / times.length;\n  }\n}\n```\n```python Python\nimport time\nfrom collections import defaultdict\n\nclass PerformanceTracker:\n    def __init__(self):\n        self.speed_metrics = defaultdict(list)\n    \n    async def timed_act(self, page, prompt: str):\n        start = time.time()\n        result = await page.act(prompt)\n        duration = (time.time() - start) * 1000  # Convert to ms\n        \n        self.speed_metrics[prompt].append(duration)\n        print(f'Action \"{prompt}\" took {duration:.0f}ms')\n        return result\n    \n    def get_average_time(self, prompt: str) -> float:\n        times = self.speed_metrics[prompt]\n        return sum(times) / len(times) if times else 0\n```\n</CodeGroup>\n\nExample Output:\n```\nAction \"Fill form\" took 1000ms\nAction \"Click submit\" took 2000ms\nAction \"Confirm submission\" took 5000ms\n```\n\n### Before vs After Benchmarking\n\n<CodeGroup>\n```typescript TypeScript\n// Before optimization\nconsole.time(\"workflow\");\nawait page.act(\"Fill form\");\nawait page.act(\"Click submit\");\nawait page.act(\"Confirm submission\");\nconsole.timeEnd(\"workflow\"); // 8000ms\n\n// After optimization with observe planning\nconsole.time(\"workflow-optimized\");\nconst workflowActions = await page.observe(\"Find form, submit, and confirm elements\");\n\n// Execute actions sequentially to avoid conflicts\nfor (const action of workflowActions) {\n  await page.act(action);\n}\nconsole.timeEnd(\"workflow-optimized\"); // 500ms\n```\n```python Python\nimport time\n\n# Before optimization  \nstart = time.time()\nawait page.act(\"Fill form\")\nawait page.act(\"Click submit\") \nawait page.act(\"Confirm submission\")\nprint(f\"Workflow took {(time.time() - start) * 1000:.0f}ms\")  # 8000ms\n\n# After optimization with observe planning\nstart = time.time()\nworkflow_actions = await page.observe(\"Find form, submit, and confirm elements\")\n\n# Execute actions sequentially to avoid conflicts\nfor action in workflow_actions:\n    await page.act(action)\nprint(f\"Optimized workflow took {(time.time() - start) * 1000:.0f}ms\")  # 500ms\n```\n</CodeGroup>\n\nExample Output:\n```\nWorkflow took 8000ms\nOptimized workflow took 500ms\n```\n\n<CardGroup cols={1}>\n<Card title=\"Observability & Metrics\" icon=\"chart-line\" href=\"/v2/configuration/observability\">\n  Set up comprehensive performance monitoring\n</Card>\n</CardGroup>\n\n\n## Related Resources\n\n<CardGroup cols={2}>\n<Card title=\"Caching Strategies\" icon=\"database\" href=\"/v2/best-practices/caching\">\n  Advanced caching patterns for maximum performance\n</Card>\n\n<Card title=\"Cost Optimization\" icon=\"dollar-sign\" href=\"/v2/best-practices/cost-optimization\">\n  Balance speed improvements with cost considerations\n</Card>\n\n<Card title=\"Browser Configuration\" icon=\"window-maximize\" href=\"/v2/configuration/browser\">\n  Optimize Browserbase settings for speed\n</Card>\n\n<Card title=\"Model Selection\" icon=\"brain\" href=\"/v2/configuration/models\">\n  Choose the right model for speed vs accuracy\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/best-practices/usecase-observe.mdx",
    "content": "---\nsidebarTitle: Use Cases\n---\n\n## Real-World Use Cases\n\n### E-commerce Product Discovery\n\n```typescript\n// Discover product interaction elements\nconst productActions = await page.observe({\n  instruction: \"Find add to cart buttons, size selectors, and product images\"\n});\n\n// Categorize actions by type\nconst cartButtons = productActions.filter(a => \n  a.description.toLowerCase().includes('cart')\n);\nconst sizeOptions = productActions.filter(a => \n  a.description.toLowerCase().includes('size')\n);\n\n// Execute purchase workflow\nif (sizeOptions.length > 0) {\n  await page.act(sizeOptions[0]); // Select size first\n}\nif (cartButtons.length > 0) {\n  await page.act(cartButtons[0]); // Then add to cart\n}\n```\n\n### Form Handling & Validation\n\n```typescript\n// Analyze form structure before filling\nconst formElements = await page.observe({\n  instruction: \"Find form fields, validation messages, and submit buttons\"\n});\n\n// Check for required fields\nconst requiredFields = formElements.filter(e => \n  e.description.includes('required') || e.description.includes('*')\n);\n\nconsole.log(`Found ${requiredFields.length} required fields to complete`);\n\n// Fill form systematically\nfor (const field of requiredFields) {\n  await page.act(field);\n  // Add appropriate input based on field type\n}\n```\n\n### Dynamic Content & SPA Navigation\n\n```typescript\n// Wait for and discover dynamically loaded content\nawait page.waitForLoadState('networkidle');\n\nconst dynamicElements = await page.observe({\n  instruction: \"Find newly loaded content, infinite scroll triggers, or loading indicators\",\n  domSettleTimeoutMs: 15000 // Wait longer for dynamic content\n});\n\n// Handle infinite scroll\nconst scrollTriggers = dynamicElements.filter(e => \n  e.description.toLowerCase().includes('load more') ||\n  e.description.toLowerCase().includes('scroll')\n);\n\nif (scrollTriggers.length > 0) {\n  await page.act(scrollTriggers[0]);\n  // Recursively observe new content\n  const newContent = await page.observe(\"Find additional items\");\n}\n```\n\n### Multi-Step Workflow Planning\n\n```typescript\n// Plan entire checkout flow upfront\nasync function planCheckoutWorkflow() {\n  // Step 1: Cart page analysis\n  await page.goto('/cart');\n  const cartActions = await page.observe(\"Find checkout and cart modification options\");\n  \n  // Step 2: Checkout page analysis  \n  const checkoutButton = cartActions.find(a => a.description.includes('checkout'));\n  if (checkoutButton) await page.act(checkoutButton);\n  \n  const checkoutActions = await page.observe(\"Find payment forms and shipping options\");\n  \n  // Step 3: Plan execution order\n  const shippingFields = checkoutActions.filter(a => a.description.includes('shipping'));\n  const paymentFields = checkoutActions.filter(a => a.description.includes('payment'));\n  const submitButton = checkoutActions.find(a => a.description.includes('complete order'));\n  \n  return { shippingFields, paymentFields, submitButton };\n}\n\n// Execute planned workflow\nconst workflow = await planCheckoutWorkflow();\n// Fill shipping → payment → submit\n```\n"
  },
  {
    "path": "packages/docs/v2/best-practices/user-data.mdx",
    "content": "---\ntitle: User Data Directory\nsidebarTitle: User Data\ndescription: Persist browser data between sessions\n---\n\n### User Data Directory\n\nPersist browser data between sessions using a custom user data directory:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// For Browserbase sessions\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionCreateParams: {\n    userDataDir: \"/path/to/user/data/directory\",\n  },\n});\n\n// For Local sessions\nconst localStagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    userDataDir: \"./browser-data\",\n  },\n});\n\nawait stagehand.init();\nconsole.log(\"Session ID:\", stagehand.sessionId);\n```\n```python Python\nfrom stagehand import Stagehand\n\n# For Browserbase sessions\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n    browserbase_session_create_params={\n        \"user_data_dir\": \"/path/to/user/data/directory\",\n    },\n)\n\n# For Local sessions\nlocal_stagehand = Stagehand(\n    env=\"LOCAL\",\n    local_browser_launch_options={\n        \"user_data_dir\": \"./browser-data\",\n    },\n)\n\nawait stagehand.init()\nprint(f\"Session ID: {stagehand.session_id}\")\n```\n</CodeGroup>"
  },
  {
    "path": "packages/docs/v2/best-practices/using-multiple-tabs.mdx",
    "content": "---\ntitle: 'Using Multiple Tabs'\ndescription: 'Act on multiple tabs with Stagehand'\n---\n\nMany modern web applications open new tabs when users click certain buttons or links. Without proper multitab support, automation scripts break when expected content appears in a new tab rather than the current one. Stagehand's multitab capabilities ensure your automations work seamlessly across multitab workflows.\n\n## The Stagehand Page\n\nStagehand automatically adapts to multitab workflows. The `stagehand.page` object always points to the most recently opened or active tab, ensuring your automations continue working even when new tabs are created.\n\nThis means you can continue using familiar patterns:\n\n<CodeGroup>\n```typescript TypeScript\nconst page = stagehand.page;\nawait page.goto(\"https://example.com\");\nawait page.act(\"click the button that opens a new tab\");\n// page now automatically points to the new tab\nawait page.extract(\"get data from new tab\");\n```\n\n```python Python\npage = stagehand.page\nawait page.goto(\"https://example.com\")\nawait page.act(\"click the button that opens a new tab\")\n# page now automatically points to the new tab\nawait page.extract(\"get data from new tab\")\n```\n</CodeGroup>\n\n<Warning>\n**Important**: [Stagehand Agent](/v2/basics/agent) will always operate on the `stagehand.page`. If you need an agent to work across specific tabs, you'll need to manage page switching manually.\n</Warning>\n\n## Manual Page Management\n\nFor more control or multitab workflows, you can manage multiple tabs explicitly:\n\n<CodeGroup>\n```typescript TypeScript\n// Create a second page\nawait stagehand.context.newPage();\nconst pages = stagehand.context.pages();\n\nconst githubPage = pages[0];\nconst pythonPage = pages[1];\n\n// Navigate each page to different repositories\nawait githubPage.goto(\"https://github.com/browserbase/stagehand\");\nawait pythonPage.goto(\"https://github.com/browserbase/stagehand-python\");\n\n// Extract data from both pages simultaneously\nconst [stagehandStars, stagehandPythonStars] = await Promise.all([\n  githubPage.extract(\"extract the repository stars\"),\n  pythonPage.extract(\"extract the repository stars\")\n]);\n\nconsole.log(`Stagehand stars: ${stagehandStars}`);\nconsole.log(`Stagehand-Python stars: ${stagehandPythonStars}`);\n```\n\n```python Python\n# Create a second page\nawait stagehand.context.new_page()\npages = stagehand.context.pages()\n\ngithub_page = pages[0]\npython_page = pages[1]\n\n# Navigate each page to different repositories  \nawait github_page.goto(\"https://github.com/browserbase/stagehand\")\nawait python_page.goto(\"https://github.com/browserbase/stagehand-python\")\n\n# Extract data from both pages\nstagehand_stars = await github_page.extract(\"extract the repository stars\")\nstagehand_python_stars = await python_page.extract(\"extract the repository stars\")\n\nprint(f\"Stagehand stars: {stagehand_stars}\")\nprint(f\"Stagehand-Python stars: {stagehand_python_stars}\")\n```\n</CodeGroup>\n\n## Handling Tab Events\n\nYou can also listen for tab events to control what happens when new tabs are opened:\n\n<CodeGroup>\n```typescript TypeScript\nconst page = stagehand.page;\nawait page.goto(\"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/\");\n\n// close the new tab after it's opened\npage.on(\"popup\", async () => {\n  const newPage = stagehand.context.pages()[1];\n  await newPage.close();\n});\n\nawait page.act(\"click the button to open the other page\");\n\nconst page_number = await page.extract(\"extract the page number\");\nconsole.log(`You're on page ${page_number}`);\n```\n\n```python Python\npage = stagehand.page\nawait page.goto(\"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/\")\n\n# Close the new tab after it's opened\nasync def handle_popup():\n    new_page = stagehand.context.pages()[1]\n    await new_page.close()\n\npage.on(\"popup\", handle_popup)\n\nawait page.act(\"click the button to open the other page\")\n\npage_number = await page.extract(\"extract the page number\")\nprint(f\"You're on page {page_number}\")\n```\n</CodeGroup>\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Orchestrate complex workflows with Agent\" icon=\"robot\" iconType=\"sharp-solid\" href=\"/v2/basics/agent\">\n    Use `Agent` to autonomously execute multi-step tasks and complex workflows.\n  </Card>\n\n  <Card title=\"Working with iframes\" icon=\"frame\" iconType=\"sharp-solid\" href=\"/v2/best-practices/working-with-iframes\">\n    Learn best practices for interacting with elements inside iframes.\n  </Card>\n\n  <Card title=\"Browser Configuration\" icon=\"browser\" iconType=\"sharp-solid\" href=\"/v2/configuration/browser\">\n    Manage browser contexts and sessions for complex automation scenarios.\n  </Card>\n\n  <Card title=\"Logging & Debugging\" icon=\"bug\" iconType=\"sharp-solid\" href=\"/v2/configuration/logging\">\n    Handle errors gracefully and debug automation issues effectively.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/best-practices/working-with-iframes.mdx",
    "content": "---\ntitle: Working with iframes\n---\n\n### What is an iframe?\n\nIframes embed other pages within your current page. Sites use them for consent banners, payment widgets, chat bubbles, and third-party content.\nElements inside iframes exist in a separate context than the main page.\n\n### Enable iframe support\n\nSet `iframes: true` in your `act()`, `observe()`, and `extract()` commands.\n\n<CodeGroup>\n```typescript TypeScript\n// Act within iframes\nawait page.act({ action: \"click the accept cookies button\", iframes: true });\n\n// Observe within iframes\nconst results = await page.observe({\n  instruction: \"Find the primary action button\",\n  iframes: true,\n});\n\n// Extract from iframes\nconst data = await page.extract({\n  instruction: \"Extract the product price from the payment widget\",\n  schema: z.object({\n    price: z.string(),\n  }),\n  iframes: true,\n});\n```\n\n```python Python\n# Act within iframes\nawait page.act(\n    \"click the accept cookies button\",\n    iframes=True\n)\n\n# Observe within iframes\nresults = await page.observe({\n    \"instruction\": \"Find the primary action button\",\n    \"iframes\": True,\n})\n\n# Extract from iframes\ndata = await page.extract({\n    \"instruction\": \"Extract the product price from the payment widget\",\n    \"schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"price\": {\"type\": \"string\"}\n        }\n    },\n    \"iframes\": True,\n})\n```\n</CodeGroup>\n\n### Tips\n\n- Iframes can increase processing time. For best performance, use the iframe option only when necessary.\n- When you are unsure whether an element will be in an iframe, you can verify the presence of iframes in Stagehand logs.\n- If an element intermittently fails to be found, it may be inside a lazy‑loaded iframe. Add small waits between steps or re‑run your action.\n\n<Note>\nYou can enable experimental features (like Shadow DOM support) via your Stagehand configuration. See the [configuration guide](/v2/configuration/browser).\n</Note>\n\n## Next steps\n\n<CardGroup cols={2}>\n  <Card title=\"Analyze pages with observe()\" icon=\"magnifying-glass\" iconType=\"sharp-solid\" href=\"/v2/basics/observe\">\n    Use `observe()` to plan precise, single-step actions before executing them.\n  </Card>\n\n  <Card title=\"Extract data with extract()\" icon=\"table\" iconType=\"sharp-solid\" href=\"/v2/basics/extract\">\n    Use `extract()` with a data schema to pull clean, typed data from any page.\n  </Card>\n\n  <Card title=\"Caching actions\" icon=\"bolt\" iconType=\"sharp-solid\" href=\"/v2/best-practices/caching\">\n    Speed up repeated automations by caching actions.\n  </Card>\n\n  <Card title=\"Act fundamentals\" icon=\"arrow-pointer\" iconType=\"sharp-solid\" href=\"/v2/basics/act\">\n    Learn how to perform single-step actions reliably with `act()`.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/configuration/browser.mdx",
    "content": "---\ntitle: Browser\nsidebarTitle: Browser\ndescription: Configure Stagehand on Browserbase or locally\n---\n\nStagehand supports two primary environments:\n\n- **Browserbase** - Cloud-managed browser infrastructure optimized for production web automation at scale\n- **Local** - Run browsers directly on your machine for development and debugging\n\n## Browserbase Environment\n\nBrowserbase provides managed cloud browser infrastructure optimized for web automation at scale. It offers advanced features like stealth mode, proxy support, and persistent contexts.\n\n<Card icon=\"cloud\" title=\"Browserbase\" href=\"https://docs.browserbase.com\" description=\"Explore the features and benefits of using Browserbase for scalable web automation.\">\n  Discover the power of cloud-managed browser infrastructure with Browserbase.\n</Card>\n\n### Environment Variables\n\nBefore getting started, set up the required environment variables:\n\n<CodeGroup>\n```bash .env\nBROWSERBASE_API_KEY=your_api_key_here\nBROWSERBASE_PROJECT_ID=your_project_id_here\n```\n</CodeGroup>\n\n<Tip>\nGet your API key and Project ID from the [Browserbase Dashboard](https://browserbase.com/overview)\n</Tip>\n\n### Using Stagehand with Browserbase\n\n#### Basic Setup\n\nThe simplest way to get started is with default settings:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n});\n\nawait stagehand.init();\n```\n```python Python\nimport os\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n)\n\nawait stagehand.init()\n```\n</CodeGroup>\n\n#### Advanced Configuration\n\nConfigure browser settings, proxy support, and other session parameters:\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  // Optional: API Key and Project ID will be pulled directly from your environment\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  browserbaseSessionCreateParams: {\n    proxies: true,\n    region: \"us-west-2\",\n    browserSettings: {\n      viewport: { width: 1920, height: 1080 },\n      blockAds: true,\n    },\n  },\n});\n\nawait stagehand.init();\nconsole.log(\"Session ID:\", stagehand.sessionId);\n```\n```python Python\nimport os\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n    # Optional: API Key and Project ID will be pulled directly from your environment\n    api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n    project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n    browserbase_session_create_params={\n        \"proxies\": True,\n        \"region\": \"us-west-2\",\n        \"browser_settings\": {\n            \"viewport\": {\"width\": 1920, \"height\": 1080},\n            \"block_ads\": True,\n        },\n    },\n)\n```\n</CodeGroup>\n\n<Accordion title=\"Advanced Browserbase Configuration Example\">\n    <CodeGroup>\n    ```typescript TypeScript\n    const stagehand = new Stagehand({\n      env: \"BROWSERBASE\",\n      apiKey: process.env.BROWSERBASE_API_KEY,\n      projectId: process.env.BROWSERBASE_PROJECT_ID,\n      browserbaseSessionCreateParams: {\n        projectId: process.env.BROWSERBASE_PROJECT_ID!,\n        proxies: true,\n        region: \"us-west-2\",\n        timeout: 3600, // 1 hour session timeout\n        keepAlive: true, // Available on Startup plan\n        browserSettings: {\n          advancedStealth: false, // this is a Scale Plan feature - reach out to support@browserbase.com to enable\n          blockAds: true,\n          solveCaptchas: true,\n          recordSession: false,\n          viewport: {\n            width: 1920,\n            height: 1080,\n          },\n          fingerprint: {\n            browsers: [\"chrome\", \"edge\"],\n            devices: [\"desktop\"],\n            operatingSystems: [\"windows\", \"macos\"],\n            locales: [\"en-US\", \"en-GB\"],\n            httpVersion: 2,\n          },\n        },\n        userMetadata: {\n          userId: \"automation-user-123\",\n          environment: \"production\",\n        },\n      },\n    });\n    ```\n    ```python Python\n    stagehand = Stagehand(\n        env=\"BROWSERBASE\",\n        api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n        project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n        browserbase_session_create_params={\n            \"project_id\": os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n            \"proxies\": True,\n            \"region\": \"us-west-2\",\n            \"timeout\": 3600,  # 1 hour session timeout\n            \"keep_alive\": True,  # Available on Startup plan\n            \"browser_settings\": {\n                \"advanced_stealth\": False,  # this is a Scale Plan feature - reach out to support@browserbase.com to enable\n                \"block_ads\": True,\n                \"solve_captchas\": True,\n                \"record_session\": False,\n                \"viewport\": {\n                    \"width\": 1920,\n                    \"height\": 1080,\n                },\n                \"fingerprint\": {\n                    \"browsers\": [\"chrome\", \"edge\"],\n                    \"devices\": [\"desktop\"],\n                    \"operating_systems\": [\"windows\", \"macos\"],\n                    \"locales\": [\"en-US\", \"en-GB\"],\n                    \"http_version\": 2,\n                },\n            },\n            \"user_metadata\": {\n                \"user_id\": \"automation-user-123\",\n                \"environment\": \"production\",\n            },\n        },\n    )\n    ```\n</CodeGroup>\n</Accordion>\n\n#### Initialization Result\nAfter calling `stagehand.init()`, the method returns configuration information about the initialized session:\n\n<CodeGroup>\n```typescript TypeScript\nconst result = await stagehand.init();\nconsole.log(result);\n```\n```python Python\nresult = await stagehand.init()\nprint(result)\n```\n</CodeGroup>\n\nThe returned object contains:\n```Example\n{\n  debugUrl: 'https://www.browserbase.com/devtools/inspector.html?wss=connect.browserbase.com/debug/f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0/devtools/page/5474B0E0510C5B6E629BEB06E799CD70?debug=true',\n  sessionUrl: 'https://www.browserbase.com/sessions/f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0',\n  sessionId: 'f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0'\n}\n```\n\n<AccordionGroup>\n<Accordion title=\"debugUrl\">\n**Open the Browserbase [session live view](https://docs.browserbase.com/features/session-live-view)** to include a human-in-the-loop.\n</Accordion>\n\n<Accordion title=\"sessionUrl\">\n**Open the [session replay](https://docs.browserbase.com/features/session-replay)** to see the full session recording. \n</Accordion>\n\n<Accordion title=\"sessionId\">\n**Unique identifier** for the [Browserbase session](https://docs.browserbase.com/introduction/what-is-browserbase). This is used to identify the session in the Browserbase dashboard and to connect to the session.\n</Accordion>\n</AccordionGroup>\n\n### Alternative: Browserbase SDK\n\nIf you prefer to manage sessions directly, you can use the Browserbase SDK:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\nconst bb = new Browserbase({ \n  apiKey: process.env.BROWSERBASE_API_KEY! \n});\n\nconst session = await bb.sessions.create({\n  projectId: process.env.BROWSERBASE_PROJECT_ID!,\n  // Add configuration options here\n});\n```\n```python Python\nfrom browserbase import Browserbase\n\nbb = Browserbase(api_key=os.environ[\"BROWSERBASE_API_KEY\"])\n\nsession = bb.sessions.create(\n    project_id=os.environ[\"BROWSERBASE_PROJECT_ID\"],\n    # Add configuration options here\n)\n```\n</CodeGroup>\n\n#### Connecting to an Existing Session\n\nConnect to a previously created Browserbase session using its session ID:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionID: \"existing-session-uuid-here\",\n});\n\nawait stagehand.init();\nconsole.log(\"Resumed Session ID:\", stagehand.sessionId);\n```\n```python Python\nimport os\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n    browserbase_session_id=\"existing-session-uuid-here\",\n)\n\nawait stagehand.init()\nprint(f\"Resumed Session ID: {stagehand.session_id}\")\n```\n</CodeGroup>\n\n## Local Environment\n\nThe local environment runs browsers directly on your machine, providing full control over browser instances and configurations. Ideal for development, debugging, and scenarios requiring custom browser setups.\n\n### Environment Comparison\n\n| Feature | Browserbase | Local |\n| --- | --- | --- |\n| **Scalability** | High (cloud-managed) | Limited (local resources) |\n| **Stealth Features** | Advanced fingerprinting | Basic stealth |\n| **Proxy Support** | Built-in residential proxies | Manual configuration |\n| **Session Persistence** | Cloud context storage | File-based user data |\n| **Geographic Distribution** | Multi-region deployment | Single machine |\n| **Debugging** | Session recordings & logs | Direct DevTools access |\n| **Setup Complexity** | Environment variables only | Browser installation required |\n| **Cost** | Usage-based pricing | Infrastructure & maintenance |\n| **Best For** | Production, scale, compliance | Development, debugging |\n\n### Basic Local Setup\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\"\n});\n  \nawait stagehand.init();\nconsole.log(\"Session ID:\", stagehand.sessionId);\n```\n```python Python\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"LOCAL\"\n)\n\nawait stagehand.init()\nprint(f\"Session ID: {stagehand.session_id}\")\n```\n</CodeGroup>\n\n### Advanced Local Configuration\n\nCustomize browser launch options for local development:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    headless: false, // Show browser window\n    devtools: true, // Open developer tools\n    viewport: { width: 1280, height: 720 },\n    executablePath: '/opt/google/chrome/chrome', // Custom Chrome path\n    args: [\n      '--no-sandbox',\n      '--disable-setuid-sandbox',\n      '--disable-web-security',\n      '--allow-running-insecure-content',\n    ],\n    env: {\n      NODE_ENV: \"development\",\n      DEBUG: \"true\",\n    },\n  },\n});\n\nawait stagehand.init();\n```\n```python Python\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"LOCAL\",\n    headless=False,  # Show browser window\n    local_browser_launch_options={\n        \"devtools\": True,  # Open developer tools\n        \"viewport\": {\"width\": 1280, \"height\": 720},\n        \"executable_path\": \"/opt/google/chrome/chrome\",  # Custom Chrome path\n        \"args\": [\n            \"--no-sandbox\",\n            \"--disable-setuid-sandbox\",\n            \"--disable-web-security\",\n            \"--allow-running-insecure-content\",\n        ],\n        \"env\": {\n            \"NODE_ENV\": \"development\",\n            \"DEBUG\": \"true\",\n        },\n    },\n)\n\nawait stagehand.init()\n```\n</CodeGroup>\n\n### Connecting to your local browser\n\nConnect to your existing local Chrome/Chromium browser instead of launching a new one. This lets you automate your normal browser with all your existing tabs, extensions and settings.\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n\tenv: \"LOCAL\",\n\tlocalBrowserLaunchOptions: {\n\t\tcdpUrl: 'http://localhost:9222'\n\t}\n});\n\nawait stagehand.init();\n```\n```python Python\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"LOCAL\",\n    local_browser_launch_options={\n      \"cdp_url\": \"http://localhost:9222\"\n    }\n)\n\nawait stagehand.init()\n```\n</CodeGroup>\n\n## Troubleshooting\n\n### Common Issues\n\n<AccordionGroup>\n<Accordion title=\"Browserbase Authentication Errors\">\n- Verify your `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` are set correctly\n- Check that your API key has the necessary permissions\n- Ensure your Browserbase account has sufficient credits\n</Accordion>\n\n<Accordion title=\"Local Browser Launch Failures\">\n- Install Chrome or Chromium on your system\n- Set the correct `executablePath` for your Chrome installation\n- Check that required dependencies are installed (Linux: `libnss3-dev libatk-bridge2.0-dev libgtk-3-dev libxss1 libasound2`)\n</Accordion>\n\n<Accordion title=\"Session Timeout Issues\">\n- Increase session timeout in `browserbaseSessionCreateParams.timeout`\n- Use `keepAlive: true` for long-running sessions\n- Monitor session usage to avoid unexpected terminations\n</Accordion>\n</AccordionGroup>"
  },
  {
    "path": "packages/docs/v2/configuration/evals.mdx",
    "content": "---\ntitle: Evaluations & Metrics\nsidebarTitle: Evaluations\ndescription: Monitor performance, optimize costs, and evaluate LLM effectiveness\n---\n\nEvaluations help you understand how well your automation performs, which models work best for your use cases, and how to optimize for cost and reliability. This guide covers both monitoring your own workflows and running comprehensive evaluations.\n\n## Why Evaluations Matter\n\n- **Performance Optimization**: Identify which models and settings work best for your specific automation tasks\n- **Cost Control**: Track token usage and inference time to optimize spending\n- **Reliability**: Measure success rates and identify failure patterns\n- **Model Selection**: Compare different LLMs on real-world tasks to make informed decisions\n\n<Card\n  title=\"Live Model Comparisons\"\n  icon=\"scale-balanced\"\n  href=\"https://www.stagehand.dev/evals\"\n>\n  View real-time performance comparisons across different LLMs on the [Stagehand Evals Dashboard](https://www.stagehand.dev/evals)\n</Card>\n\n## Comprehensive Evaluations\n\nEvaluations help you systematically test and improve your automation workflows. Stagehand provides both built-in evaluations and tools to create your own.\n\nWe have 2 types of evals:\n1. **Deterministic Evals** - These include unit tests, integration tests, and E2E tests that can be run without any LLM inference.\n2. **LLM-based Evals** - These are evals that test the underlying functionality of Stagehand's AI primitives.\n\n\n### Evals CLI\n![Evals CLI](/media/evals-cli.png)\n\n<Tip>\nTo run evals, you'll need to clone the [Stagehand repo](https://github.com/browserbase/stagehand) and set up the CLI.\n\nWe recommend using [Braintrust](https://www.braintrust.dev/docs/) to help visualize evals results and metrics.\n</Tip>\n\nThe Stagehand CLI provides a powerful interface for running evaluations. You can run specific evals, categories, or external benchmarks with customizable settings.\n\nEvals are grouped into:\n1. **Act Evals** - These are evals that test the functionality of the `act` method.\n2. **Extract Evals** - These are evals that test the functionality of the `extract` method.\n3. **Observe Evals** - These are evals that test the functionality of the `observe` method.\n4. **Combination Evals** - These are evals that test the functionality of the `act`, `extract`, and `observe` methods together.\n5. **Experimental Evals** - These are experimental custom evals that test the functionality of the stagehand primitives.\n6. **Agent Evals** - These are evals that test the functionality of `agent`.\n7. **(NEW) External Benchmarks** - Run external benchmarks like WebBench, GAIA, WebVoyager, OnlineMind2Web, and OSWorld.\n\n#### Installation\n\n<Steps> \n<Step title=\"Install Dependencies\">\n```bash\n# From the stagehand root directory\npnpm install\n```\n</Step>\n\n<Step title=\"Build the CLI\">\n```bash\npnpm run build:cli\n```\n</Step>\n\n<Step title=\"Verify Installation\">\n```bash\nevals help\n```\n</Step>\n</Steps>\n\n#### CLI Commands and Options\n\n##### Basic Commands\n\n```bash\n# Run all evals\nevals run all\n\n# Run specific category\nevals run act\nevals run extract\nevals run observe\nevals run agent\n\n# Run specific eval\nevals run extract/extract_text\n\n# List available evals\nevals list\nevals list --detailed\n\n# Configure defaults\nevals config\nevals config set env browserbase\nevals config set trials 5\n```\n\n##### Command Options\n\n- **`-e, --env`**: Environment (`local` or `browserbase`)\n- **`-t, --trials`**: Number of trials per eval (default: 3)\n- **`-c, --concurrency`**: Max parallel sessions (default: 10)\n- **`-m, --model`**: Model override\n- **`-p, --provider`**: Provider override\n- **`--api`**: Use Stagehand API instead of SDK\n\n##### Running External Benchmarks\n\nThe CLI supports several industry-standard benchmarks:\n\n```bash\n# WebBench with filters\nevals run benchmark:webbench -l 10 -f difficulty=easy -f category=READ\n\n# GAIA benchmark\nevals run b:gaia -s 100 -l 25 -f level=1\n\n# WebVoyager\nevals run b:webvoyager -l 50\n\n# OnlineMind2Web\nevals run b:onlineMind2Web\n\n# OSWorld\nevals run b:osworld -f source=Mind2Web\n```\n\n#### Configuration Files\n\nYou can view the specific evals in [`evals/tasks`](https://github.com/browserbase/stagehand/tree/v2/evals/tasks). Each eval is grouped into eval categories based on [`evals/evals.config.json`](https://github.com/browserbase/stagehand/blob/main/evals/evals.config.json).\n\n\n#### Viewing eval results\n![Eval results](/images/evals.png)\n\nEval results are viewable on Braintrust. You can view the results of a specific eval by going to the Braintrust URL specified in the terminal when you run `npm run evals`.\n\nBy default, each eval will run five times per model. The \"Exact Match\" column shows the percentage of times the eval was correct. The \"Error Rate\" column shows the percentage of times the eval errored out.\n\nYou can use the Braintrust UI to filter by model/eval and aggregate results across all evals.\n\n### Deterministic Evals\n\nTo run deterministic evals, you can run `npm run e2e` from within the Stagehand repo. This will test the functionality of Playwright within Stagehand to make sure it's working as expected.\n\nThese tests are in [`evals/deterministic`](https://github.com/browserbase/stagehand/tree/v2/evals/deterministic) and test on both Browserbase browsers and local headless Chromium browsers.\n\n## Creating Custom Evaluations\n\n### Step-by-Step Guide\n\n<Steps>\n<Step title=\"Create Evaluation File\">\nCreate a new file in `evals/tasks/your-eval.ts`:\n\n```typescript\nimport { EvalTask } from '../types';\n\nexport const customEvalTask: EvalTask = {\n  name: 'custom_task_name',\n  description: 'Test specific automation workflow',\n  \n  // Test setup\n  setup: async ({ page }) => {\n    await page.goto('https://example.com');\n  },\n  \n  // The actual test\n  task: async ({ stagehand, page }) => {\n    // Your automation logic\n    await page.act({ action: 'click the login button' });\n    const result = await page.extract({ \n      instruction: 'Get the user name',\n      schema: { username: 'string' }\n    });\n    return result;\n  },\n  \n  // Validation\n  validate: (result, expected) => {\n    return result.username === expected.username;\n  },\n  \n  // Test cases\n  testCases: [\n    {\n      input: { /* test input */ },\n      expected: { username: 'john_doe' }\n    }\n  ],\n  \n  // Evaluation criteria\n  scoring: {\n    exactMatch: true,\n    timeout: 30000,\n    retries: 2\n  }\n};\n```\n</Step>\n\n<Step title=\"Add to Configuration\">\nUpdate `evals/evals.config.json`:\n\n```json\n{\n  \"categories\": {\n    \"custom\": [\"custom_task_name\"],\n    \"existing_category\": [\"custom_task_name\"]\n  }\n}\n```\n</Step>\n\n<Step title=\"Run Your Evaluation\">\n```bash\n# Test your custom evaluation\nevals run custom_task_name\n\n# Run the entire custom category\nevals run custom\n\n# Run with specific settings\nevals run custom_task_name -e browserbase -t 5 -m gpt-4o\n```\n</Step>\n</Steps>\n\n\n## Best Practices for Custom Evals\n\n<AccordionGroup>\n<Accordion title=\"Test Design Principles\">\n- **Atomic**: Each test should validate one specific capability\n- **Deterministic**: Tests should produce consistent results\n- **Realistic**: Use real-world scenarios and websites\n- **Measurable**: Define clear success/failure criteria\n</Accordion>\n\n<Accordion title=\"Performance Optimization\">\n- **Parallel Execution**: Design tests to run independently\n- **Resource Management**: Clean up after each test\n- **Timeout Handling**: Set appropriate timeouts for operations\n- **Error Recovery**: Handle failures gracefully\n</Accordion>\n\n<Accordion title=\"Data Quality\">\n- **Ground Truth**: Establish reliable expected outcomes\n- **Edge Cases**: Test boundary conditions and error scenarios\n- **Statistical Significance**: Run multiple iterations for reliability\n- **Version Control**: Track changes to test cases over time\n</Accordion>\n</AccordionGroup>\n\n### Troubleshooting Evaluations\n<AccordionGroup>\n<Accordion title=\"Evaluation Timeouts\">\n**Symptoms**: Tests fail with timeout errors\n\n**Solutions**:\n- Increase timeout in `taskConfig.ts`\n- Use faster models (Gemini 2.5 Flash, GPT-4o Mini)\n- Optimize test scenarios to be less complex\n- Check network connectivity to LLM providers\n</Accordion>\n\n<Accordion title=\"Inconsistent Results\">\n**Symptoms**: Same test passes/fails randomly\n\n**Solutions**:\n- Set temperature to 0 for deterministic outputs\n- Increase repetitions for statistical significance\n- Use more capable models for complex tasks\n- Check for dynamic website content affecting tests\n</Accordion>\n\n<Accordion title=\"High Evaluation Costs\">\n**Symptoms**: Token usage exceeding budget\n\n**Solutions**:\n- Use cost-effective models (Gemini 2.0 Flash, GPT-4o Mini)\n- Reduce repetitions for initial testing\n- Focus on specific evaluation categories\n- Use local browser environment to reduce Browserbase costs\n</Accordion>\n\n<Accordion title=\"Braintrust Integration Issues\">\n**Symptoms**: Results not uploading to dashboard\n\n**Solutions**:\n- Check Braintrust API key configuration\n- Verify internet connectivity\n- Update Braintrust SDK to latest version\n- Check project permissions in Braintrust dashboard\n</Accordion>\n</AccordionGroup>"
  },
  {
    "path": "packages/docs/v2/configuration/logging.mdx",
    "content": "---\ntitle: Logging & Debugging\nsidebarTitle: Logging\ndescription: Set up logging, debugging, and error tracking for Stagehand workflows\n---\n\nStagehand provides comprehensive logging capabilities to help you debug automation workflows, track execution, and diagnose issues. Configure logging levels, structured output, and debugging tools for both development and production environments.\n\n## Logging Configuration\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\", // or \"LOCAL\"\n  verbose: 1, // 0 = errors only, 1 = info, 2 = debug\n});\n```\n\n```python Python\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",  # or \"LOCAL\"\n    verbose=1,  # 0 = errors only, 1 = info, 2 = debug\n)\n```\n</CodeGroup>\n\n### Verbose Levels\n\n- **Level 0**: Errors only - minimal output for production\n- **Level 1**: Info - includes successful operations and important events\n- **Level 2**: Debug - comprehensive logging including internal operations\n\n## Structured Logging\n\n### Log Line Format\n\nEach log entry contains structured information:\n\n<CodeGroup>\n```typescript TypeScript\ninterface LogLine {\n  category: 'browser' | 'action' | 'llm' | 'error' | 'stagehand' | 'cache';\n  message: string;\n  level: 0 | 1 | 2; // error | info | debug\n  timestamp: string;\n  auxiliary?: {\n    executionTime?: { value: string; unit: string };\n    sessionId?: string;\n    url?: string;\n    [key: string]: any;\n  };\n}\n```\n\n```python Python\n# Log line structure in Python\n{\n  \"category\": \"browser\" | \"action\" | \"llm\" | \"error\" | \"stagehand\" | \"cache\",\n  \"message\": str,\n  \"level\": 0 | 1 | 2,  # error | info | debug\n  \"timestamp\": str,\n  \"auxiliary\": {\n    \"execution_time\": {\"value\": str, \"unit\": str},\n    \"session_id\": str,\n    \"url\": str,\n    # ... other context data\n  }\n}\n```\n</CodeGroup>\n\n### Custom Logger\n\n<CodeGroup>\n```typescript TypeScript\nclass AdvancedLogger {\n  private logFile?: string;\n  \n  constructor(logFile?: string) {\n    this.logFile = logFile;\n  }\n  \n  log = (logLine: any) => {\n    const timestamp = new Date().toISOString();\n    const colors = {\n      browser: '\\x1b[34m', // blue\n      action: '\\x1b[32m',  // green\n      llm: '\\x1b[35m',     // magenta\n      error: '\\x1b[31m',   // red\n      stagehand: '\\x1b[36m', // cyan\n      cache: '\\x1b[33m',   // yellow\n    };\n    \n    const color = colors[logLine.category] || '\\x1b[0m';\n    const reset = '\\x1b[0m';\n    \n    // Console output with colors\n    console.log(`${color}[${logLine.category}]${reset} ${logLine.message}`);\n    \n    // Log execution time if available\n    if (logLine.auxiliary?.executionTime) {\n      console.log(` ${logLine.auxiliary.executionTime.value}${logLine.auxiliary.executionTime.unit}`);\n    }\n    \n    // Log additional context\n    if (logLine.auxiliary && Object.keys(logLine.auxiliary).length > 0) {\n      console.log('  Context:', JSON.stringify(logLine.auxiliary, null, 2));\n    }\n    \n    // File logging (optional)\n    if (this.logFile) {\n      const logEntry = {\n        timestamp,\n        ...logLine\n      };\n      require('fs').appendFileSync(this.logFile, JSON.stringify(logEntry) + '\\n');\n    }\n  }\n}\n\n// Usage\nconst logger = new AdvancedLogger('./automation.log');\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  verbose: 2,\n  logger: logger.log\n});\n```\n\n```python Python\nimport json\nimport os\nfrom datetime import datetime\nfrom typing import Dict, Any, Optional\n\nclass AdvancedLogger:\n    def __init__(self, log_file: Optional[str] = None):\n        self.log_file = log_file\n    \n    def log(self, log_line: Dict[str, Any]):\n        timestamp = datetime.now().isoformat()\n        colors = {\n            'browser': '\\033[34m',   # blue\n            'action': '\\033[32m',    # green\n            'llm': '\\033[35m',       # magenta\n            'error': '\\033[31m',     # red\n            'stagehand': '\\033[36m', # cyan\n            'cache': '\\033[33m',     # yellow\n        }\n        \n        color = colors.get(log_line.get('category', ''), '\\033[0m')\n        reset = '\\033[0m'\n        \n        # Console output with colors\n        print(f\"{color}[{log_line.get('category')}]{reset} {log_line.get('message')}\")\n        \n        # Log execution time if available\n        if log_line.get('auxiliary', {}).get('execution_time'):\n            exec_time = log_line['auxiliary']['execution_time']\n            print(f\"{exec_time['value']}{exec_time['unit']}\")\n        \n        # Log additional context\n        auxiliary = log_line.get('auxiliary', {})\n        if auxiliary and len(auxiliary) > 0:\n            print('  Context:', json.dumps(auxiliary, indent=2))\n        \n        # File logging (optional)\n        if self.log_file:\n            log_entry = {\n                'timestamp': timestamp,\n                **log_line\n            }\n            with open(self.log_file, 'a') as f:\n                f.write(json.dumps(log_entry) + '\\n')\n\n# Usage\nlogger = AdvancedLogger('./automation.log')\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n    verbose=2,\n    logger=logger.log\n)\n```\n</CodeGroup>\n\n## Detailed Logging Features\n\n### LLM Inference Logging\n\nEnable detailed logging of all LLM interactions:\n\n<CodeGroup>\n```typescript TypeScript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  logInferenceToFile: true,  // Creates inference_summary/ directory\n  verbose: 2\n});\n```\n\n```python Python\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n    log_inference_to_file=True,  # Creates inference_summary/ directory\n    verbose=2\n)\n```\n</CodeGroup>\n\nThe `inference_summary/` directory structure:\n```\ninference_summary/                   \n├── act_summary/            \n│   ├── 20240329_080446068.json    \n│   ├── 20240329_080447019.json   \n│   └── act_summary.json          \n├── extract_summary/               \n│   ├── 20240329_081205123.json    \n│   └── extract_summary.json       \n└── observe_summary/                \n    ├── 20240329_081634891.json    \n    └── observe_summary.json       \n```\n\n## Log Analysis & Debugging\n\n### Common Log Patterns\n<Tabs>\n  <Tab title=\"Successful Action\">\n    ```json\n    {\n      \"category\": \"action\", \n      \"message\": \"act completed successfully\",\n      \"level\": 1,\n      \"auxiliary\": {\n        \"executionTime\": {\"value\": \"1250\", \"unit\": \"ms\"},\n        \"url\": \"https://example.com\",\n        \"sessionId\": \"session-123\"\n      }\n    }\n    ```\n  </Tab>\n  <Tab title=\"LLM Inference\">\n    ```json\n    {\n      \"category\": \"llm\",\n      \"message\": \"inference completed\", \n      \"level\": 1,\n      \"auxiliary\": {\n        \"model\": \"gpt-4o\",\n        \"tokens\": {\"prompt\": 3451, \"completion\": 45},\n        \"executionTime\": {\"value\": \"951\", \"unit\": \"ms\"}\n      }\n    }\n    ```\n  </Tab>\n  <Tab title=\"Error Example\">\n    ```json\n    {\n      \"category\": \"action\",\n      \"message\": \"action failed: element not found\",\n      \"level\": 0, \n      \"auxiliary\": {\n        \"selector\": \"button[data-testid='submit']\",\n        \"url\": \"https://example.com/form\",\n        \"sessionId\": \"session-123\"\n      }\n    }\n    ```\n  </Tab>\n</Tabs>\n\n## Best Practices\n\n<AccordionGroup>\n<Accordion title=\"Development Environment\">\n- Use `verbose: 2` with visual debugging\n- Enable browser DevTools for element inspection\n- Use `logInferenceToFile: true` to capture LLM decisions\n- Implement structured logging early\n</Accordion>\n\n<Accordion title=\"Production Environment\">\n- Use `verbose: 1` to balance visibility with performance\n- Implement error tracking and alerting\n- Use structured JSON logging\n- Monitor session success rates and execution times\n</Accordion>\n\n<Accordion title=\"Security & Compliance\">\n- Never log credentials or sensitive data\n- Implement log retention policies\n- Secure log files and dashboards\n</Accordion>\n</AccordionGroup>"
  },
  {
    "path": "packages/docs/v2/configuration/models.mdx",
    "content": "---\ntitle: Models\nsidebarTitle: Models\ndescription: Enhance Stagehand with LLMs for optimal performance, cost, and reliability\n---\n\nStagehand uses Large Language Models (LLMs) to understand web pages, plan actions, and interact with complex interfaces. The choice of LLM significantly impacts your automation's accuracy, speed, and cost.\n\n<Card title=\"Model Evaluation\" href=\"https://www.stagehand.dev/evals\" icon=\"paper-plane\">\nFind more details about how to choose the right model on our Model Evaluation page.\n</Card>\n\n## Why LLM Choice Matters\n\n- **Accuracy**: Better models provide more reliable element detection and action planning\n- **Speed**: Faster models reduce automation latency\n- **Cost**: Different providers offer varying pricing structures\n- **Reliability**: Structured output support ensures consistent automation behavior\n\n<Tip>\nFind more details about how to choose the right model on our [Model Evaluation](https://www.stagehand.dev/evals) page.\n</Tip>\n\n<Warning>\nSmall models on **Ollama** struggle with consistent structured outputs. While technically supported, we don't recommend them for production Stagehand workflows.\n</Warning>\n\n## Environment Variables Setup\n\nSet up your API keys before configuring Stagehand:\n\n<CodeGroup>\n```bash .env\n# Choose one or more providers\nOPENAI_API_KEY=your_openai_key_here\nANTHROPIC_API_KEY=your_anthropic_key_here\nGOOGLE_API_KEY=your_google_key_here\nGROQ_API_KEY=your_groq_key_here\n```\n</CodeGroup>\n\n## Supported Providers\n\nStagehand supports major LLM providers with structured output capabilities:\n\n### Production-Ready Providers\n\n| Provider | Best Models | Strengths | Use Case |\n|----------|-------------|-----------|----------|\n| **OpenAI** | `gpt-4.1`, `gpt-4.1-mini` | High accuracy, reliable | Production, complex sites |\n| **Anthropic** | `claude-sonnet-4-6` | Excellent reasoning | Complex automation tasks |\n| **Google** | `gemini-2.5-flash`, `gemini-2.5-pro` | Fast, cost-effective | High-volume automation |\n\n### Additional Providers\n\n<Expandable title=\"More Providers\">\n- **Groq** - `llama-3.3-70b-versatile` (Good for speed critical applications)\n- **xAI** - `grok-beta` (Good for complex reasoning)\n- **Azure** - Enterprise OpenAI deployment\n- **Cerebras** - High-speed inference\n- **TogetherAI** - Open-source models\n- **Mistral** - `mixtral-8x7b-32768` (European option)\n- **DeepSeek** - Cost-effective alternative\n- **Perplexity** - Real-time web data\n- **Ollama** - Local deployment (limited accuracy)\n- **Run any model included in AI SDK** - Find supported models in the [Vercel AI SDK](https://sdk.vercel.ai/providers/ai-sdk-providers) (Follow the guide \n     [here](#vercel-ai-sdk) to get started.)\n</Expandable>\n\n## Basic Configuration\n\n### Model Name Format\n\nStagehand uses the format `provider/model-name` for model specification.\n\n**Examples:**\n- OpenAI: `openai/gpt-4.1`\n- Anthropic: `anthropic/claude-sonnet-4-6`\n- Google: `google/gemini-2.5-flash` (Recommended)\n\n### Quick Start Examples\n\n<Tabs>\n<Tab title=\"Google (Recommended)\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  modelName: \"google/gemini-2.5-flash\",\n  modelClientOptions: {\n    apiKey: process.env.GOOGLE_API_KEY,\n  },\n});\n```\n```python Python\nimport os\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    model_name=\"google/gemini-2.5-flash\",\n    model_api_key=os.getenv(\"GOOGLE_API_KEY\")\n)\n```\n</CodeGroup>\n</Tab>\n<Tab title=\"OpenAI\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  modelName: \"openai/gpt-4.1\",\n  modelClientOptions: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n```\n```python Python\nimport os\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    model_name=\"openai/gpt-4.1\",\n    model_api_key=os.getenv(\"OPENAI_API_KEY\")\n)\n```\n</CodeGroup>\n</Tab>\n\n<Tab title=\"Anthropic\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  modelName: \"anthropic/claude-sonnet-4-6\",\n  modelClientOptions: {\n    apiKey: process.env.ANTHROPIC_API_KEY,\n  },\n});\n```\n```python Python\nimport os\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    model_name=\"anthropic/claude-sonnet-4-6\",\n    model_api_key=os.getenv(\"ANTHROPIC_API_KEY\")\n)\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n## Custom LLM Integration\n\n<Note>\nCustom LLMs are currently only supported in TypeScript.\n</Note>\n\nIntegrate any LLM with Stagehand using custom clients. The only requirement is **structured output support** for consistent automation behavior.\n\n### Vercel AI SDK\nThe [Vercel AI SDK](https://sdk.vercel.ai/providers/ai-sdk-providers) is a popular library for interacting with LLMs. You can use any of the providers supported by the Vercel AI SDK to create a client for your model, **as long as they support structured outputs**.\n\nVercel AI SDK supports providers for OpenAI, Anthropic, and Google, along with support for **Amazon Bedrock** and **Azure OpenAI**.\n\nTo get started, you'll need to install the `ai` package and the provider you want to use. For example, to use Amazon Bedrock, you'll need to install the `@ai-sdk/amazon-bedrock` package.\n\nYou'll also need to use the [Vercel AI SDK external client](https://github.com/browserbase/stagehand/blob/v2/examples/external_clients/aisdk.ts) as a template to create a client for your model.\n\n<Tabs>\n\t<Tab title=\"npm\">\n\t```bash\n\tnpm install ai @ai-sdk/amazon-bedrock\n\t```\n\t</Tab>\n\n\t<Tab title=\"pnpm\">\n\t```bash\n\tpnpm install ai @ai-sdk/amazon-bedrock\n\t```\n\t</Tab>\n\n\t<Tab title=\"yarn\">\n\t```bash\n\tyarn add ai @ai-sdk/amazon-bedrock\n\t```\n\t</Tab>\n</Tabs>\n\nTo get started, you can use the [Vercel AI SDK external client](https://github.com/browserbase/stagehand/blob/84f810b4631291307a32a47addad7e26e9c1deb3/examples/external_clients/aisdk.ts) as a template to create a client for your model.\n\n```ts\n// Install/import the provider you want to use.\n// For example, to use OpenAI, import `openai` from @ai-sdk/openai\nimport { bedrock } from \"@ai-sdk/amazon-bedrock\";\nimport { AISdkClient } from \"./external_clients/aisdk\";\n\nconst stagehand = new Stagehand({\n  llmClient: new AISdkClient({\n\tmodel: bedrock(\"anthropic.claude-sonnet-4-6-v1:0\"),\n  }),\n});\n```\n\n## Troubleshooting\n\n### Common Issues\n\n<AccordionGroup>\n<Accordion title=\"Model doesn't support structured outputs\">\n**Error**: `Model does not support structured outputs`\n\n**Solution**:\nUse models that support function calling/structured outputs. The minimum requirements are:\n\n- Model must support JSON/structured outputs\n- Model must have strong reasoning capabilities\n- Model must be able to handle complex instructions\n\nFor each provider, use their latest models that meet these requirements. Some examples:\n\n- **OpenAI**: GPT-4 series or newer\n- **Anthropic**: Claude 3 series or newer \n- **Google**: Gemini 2 series or newer\n- **Other providers**: Latest models with structured output support\n\n**Note**: Avoid base language models without structured output capabilities or fine-tuning for instruction following. When in doubt, check our [Model Evaluation](https://www.stagehand.dev/evals) page for up-to-date recommendations.\n</Accordion>\n\n<Accordion title=\"Authentication errors\">\n**Error**: `Invalid API key` or `Unauthorized`\n\n**Solution**:\n- Verify your environment variables are set correctly\n- Check API key permissions and quotas\n- Ensure you're using the correct API key for the provider\n- For Anthropic, make sure you have access to the Claude API\n</Accordion>\n\n<Accordion title=\"Inconsistent automation results\">\n**Symptoms**: Actions work sometimes but fail other times\n\n**Causes & Solutions**:\n- **Weak models**: Use more capable models - check our [Model Evaluation](https://www.stagehand.dev/evals) page for current recommendations\n- **High temperature**: Set temperature to 0 for deterministic outputs\n- **Complex pages**: Switch to models with higher accuracy scores on our [Model Evaluation](https://www.stagehand.dev/evals) page\n- **Rate limits**: Implement retry logic with exponential backoff\n- **Context limits**: Reduce page complexity or use models with larger context windows\n- **Prompt clarity**: Ensure your automation instructions are clear and specific\n</Accordion>\n\n<Accordion title=\"Slow performance\">\n**Issue**: Automation takes too long to respond\n\n**Solutions**:\n- **Use fast models**: Choose models optimized for speed\n  - Any model with < 1s response time\n  - Models with \"fast\" or \"flash\" variants\n- **Optimize settings**: \n  - Use `verbose: 0` to minimize token usage\n  - Set temperature to 0 for fastest processing\n  - Keep max tokens as low as possible\n- **Consider local deployment**: Local models can provide lowest latency\n- **Batch operations**: Group multiple actions when possible\n</Accordion>\n\n<Accordion title=\"High costs\">\n**Issue**: LLM usage costs are too high\n\n**Cost Optimization Strategies**:\n1. **Switch to cost-effective models**: \n   - Check our [Model Evaluation](https://www.stagehand.dev/evals) page for current cost-performance benchmarks\n   - Choose models with lower cost per token that still meet accuracy requirements\n   - Consider models optimized for speed to reduce total runtime costs\n2. **Optimize token usage**: \n   - Set `verbose: 0` to reduce logging overhead\n   - Use concise prompts and limit response length\n3. **Smart model selection**: Start with cheaper models, fallback to premium ones only when needed\n4. **Cache responses**: Implement LLM response caching for repeated automation patterns\n5. **Monitor usage**: Set up billing alerts and track costs per automation run\n6. **Batch processing**: Process multiple similar tasks together\n</Accordion>\n</AccordionGroup>\n\n### Next Steps\n<CardGroup cols={2}>\n<Card title=\"Choose Models\" href=\"https://www.stagehand.dev/evals\" icon=\"robot\">\n  See our Model Evaluation page\n</Card>\n\n<Card title=\"Test Models\" href=\"/v2/configuration/evals\" icon=\"flask-vial\">\n  Evaluate performance on your specific use cases in our Model Evaluation guide\n</Card>\n\n<Card title=\"Track Costs\" href=\"/v2/configuration/observability\" icon=\"chart-line\">\n  Monitor token usage and set alerts using our Observability tools\n</Card>\n\n<Card title=\"Cache Results\" href=\"/v2/best-practices/caching\" icon=\"database\">\n  Store successful patterns using our Caching Guide\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/configuration/observability.mdx",
    "content": "---\ntitle: Observability\nsidebarTitle: Observability\ndescription: Track Stagehand automation with session visibility and analytics\n---\n\nStagehand provides powerful observability features to help you monitor, track performance, and analyze your browser automation workflows. Focus on session monitoring, resource usage, and operational insights for both Browserbase and local environments.\n\n## Browserbase Session Monitoring\n\nWhen running on Browserbase, you gain access to comprehensive cloud-based monitoring and session management through the Browserbase API and dashboard.\n\n<div style={{ textAlign: \"center\" }}>\n  <img src=\"/media/observability.gif\" alt=\"Browserbase Session Observability\" width=\"400\" />\n</div>\n\n### Live Session Visibility\n\nBrowserbase provides real-time visibility into your automation sessions:\n\n**Session Dashboard Features**\n- Real-time browser screen recording and replay\n- Network request monitoring with detailed timing\n- JavaScript console logs and error tracking\n- CPU and memory usage metrics\n- Session status and duration tracking\n\n**Session Management & API Access**\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\nconst browserbase = new Browserbase({\n  apiKey: process.env.BROWSERBASE_API_KEY,\n});\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\"\n});\n\nawait stagehand.init();\n\nconst sessionInfo = await browserbase.sessions.retrieve(stagehand.sessionId);\n\nconsole.log(\"Session status:\", sessionInfo.status);\nconsole.log(\"Session region:\", sessionInfo.region);\nconsole.log(\"CPU usage:\", sessionInfo.avgCpuUsage);\nconsole.log(\"Memory usage:\", sessionInfo.memoryUsage);\nconsole.log(\"Proxy bytes:\", sessionInfo.proxyBytes);\n```\n\n```python Python\nimport os\nfrom stagehand import Stagehand\nfrom browserbase import Browserbase\n\nbrowserbase = Browserbase(\n  api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n)\n\nstagehand = Stagehand(\n    env=\"BROWSERBASE\",\n)\n\nawait stagehand.init()\n\nsession_info = browserbase.sessions.retrieve(stagehand.session_id)\n\nprint(f\"Session status: {session_info['status']}\")\nprint(f\"Session region: {session_info['region']}\")\nprint(f\"CPU usage: {session_info['avgCpuUsage']}\")\nprint(f\"Memory usage: {session_info['memoryUsage']}\")\nprint(f\"Proxy bytes: {session_info['proxyBytes']}\")\n```\n</CodeGroup>\n\n### Session Analytics & Insights\n\n<CardGroup>\n  <Card title=\"Real-Time Monitoring\" icon=\"chart-line\">\n    Monitor live session status, resource usage, and geographic distribution. Scale and manage concurrent sessions with real-time insights.\n  </Card>\n\n  <Card title=\"Session Recordings\" icon=\"video\">\n    Review complete session recordings with frame-by-frame playback. Analyze network requests and debug browser interactions visually.\n  </Card>\n\n  <Card title=\"API Management\" icon=\"code\">\n    Programmatically access session data, automate lifecycle management, and integrate with monitoring systems through our API.\n  </Card>\n\n  <Card title=\"Usage Monitoring\" icon=\"chart-bar\">\n    Track resource consumption, session duration, and API usage. Get detailed breakdowns of costs and utilization across your automation.\n  </Card>\n</CardGroup>\n\n### Session Monitoring & Filtering\n\nQuery and monitor sessions by status and metadata:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\nconst browserbase = new Browserbase({\n  apiKey: process.env.BROWSERBASE_API_KEY,\n});\n\n// List sessions with filtering\nasync function getFilteredSessions() {\n  const sessions = await browserbase.sessions.list({\n    status: 'RUNNING'\n  });\n  \n  return sessions.map(session => ({\n    id: session.id,\n    status: session.status, // RUNNING, COMPLETED, ERROR, TIMED_OUT\n    startedAt: session.startedAt,\n    endedAt: session.endedAt,\n    region: session.region,\n    avgCpuUsage: session.avgCpuUsage,\n    memoryUsage: session.memoryUsage,\n    proxyBytes: session.proxyBytes,\n    userMetadata: session.userMetadata\n  }));\n}\n\n// Query sessions by metadata\nasync function querySessionsByMetadata(query: string) {\n  const sessions = await browserbase.sessions.list({\n    q: query\n  });\n  \n  return sessions;\n}\n```\n\n```python Python\nimport os\nfrom browserbase import Browserbase\n\nbrowserbase = Browserbase(\n    api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n)\n\ndef get_filtered_sessions():\n    sessions = browserbase.sessions.list(status=\"RUNNING\")\n    \n    return [{\n        'id': session['id'],\n        'status': session['status'],  # RUNNING, COMPLETED, ERROR, TIMED_OUT\n        'started_at': session['startedAt'],\n        'ended_at': session['endedAt'],\n        'region': session['region'],\n        'avg_cpu_usage': session['avgCpuUsage'],\n        'memory_usage': session['memoryUsage'],\n        'proxy_bytes': session['proxyBytes'],\n        'user_metadata': session['userMetadata']\n    } for session in sessions]\n\ndef query_sessions_by_metadata(query):\n    sessions = browserbase.sessions.list(q=query)\n    \n    return sessions\n```\n</CodeGroup>\n\n## Local Environment Monitoring\n\nFor local development, Stagehand provides performance monitoring and resource tracking capabilities directly on your machine.\n\n### Performance Tracking\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  verbose: 1, // Monitor performance without debug noise\n});\n\n// Track local automation metrics\nconst startTime = Date.now();\nconst initialMetrics = stagehand.metrics;\n\n// ... perform automation tasks\n\nconst finalMetrics = stagehand.metrics;\nconst executionTime = Date.now() - startTime;\n\nconsole.log('Local Performance Summary:', {\n  executionTime: `${executionTime}ms`,\n  totalTokens: finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens,\n  averageResponseTime: finalMetrics.totalInferenceTimeMs / 3, // Assuming 3 operations\n  tokensPerSecond: (finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens) / (executionTime / 1000)\n});\n```\n\n```python Python\nfrom stagehand import Stagehand\nimport time\n\nstagehand = Stagehand(\n    env=\"LOCAL\",\n    verbose=1,  # Monitor performance without debug noise\n)\n\n# Track local automation metrics\nstart_time = time.time()\ninitial_metrics = stagehand.metrics\n\n# ... perform automation tasks\n\nfinal_metrics = stagehand.metrics\nexecution_time = (time.time() - start_time) * 1000  # Convert to ms\n\nprint('Local Performance Summary:', {\n    'execution_time': f\"{execution_time:.0f}ms\",\n    'total_tokens': final_metrics['total_prompt_tokens'] + final_metrics['total_completion_tokens'],\n    'average_response_time': final_metrics['total_inference_time_ms'] / 3,  # Assuming 3 operations\n    'tokens_per_second': (final_metrics['total_prompt_tokens'] + final_metrics['total_completion_tokens']) / (execution_time / 1000)\n})\n```\n</CodeGroup>\n\n## Resource Usage Monitoring\n\nWhen running locally, monitor system resource usage and browser performance:\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport * as os from 'os';\nimport { performance } from 'perf_hooks';\n\nclass LocalResourceMonitor {\n  private cpuUsage: number[] = [];\n  private memoryUsage: number[] = [];\n  \n  startMonitoring() {\n    const interval = setInterval(() => {\n      // Track system resources\n      const memUsage = process.memoryUsage();\n      this.memoryUsage.push(memUsage.heapUsed / 1024 / 1024); // MB\n      \n      // Track CPU (simplified)\n      const loadAvg = os.loadavg()[0];\n      this.cpuUsage.push(loadAvg);\n    }, 1000);\n    \n    return interval;\n  }\n  \n  getResourceSummary() {\n    return {\n      avgMemoryUsage: this.memoryUsage.reduce((a, b) => a + b, 0) / this.memoryUsage.length,\n      peakMemoryUsage: Math.max(...this.memoryUsage),\n      avgCpuLoad: this.cpuUsage.reduce((a, b) => a + b, 0) / this.cpuUsage.length,\n      totalDataPoints: this.cpuUsage.length\n    };\n  }\n}\n\nconst monitor = new LocalResourceMonitor();\nconst interval = monitor.startMonitoring();\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\n\n// ... run automation\n\nclearInterval(interval);\nconsole.log('Resource Usage:', monitor.getResourceSummary());\n```\n\n```python Python\nimport psutil\nimport time\nfrom typing import List\nfrom stagehand import Stagehand\n\nclass LocalResourceMonitor:\n    def __init__(self):\n        self.cpu_usage: List[float] = []\n        self.memory_usage: List[float] = []\n        self.monitoring = False\n    \n    def start_monitoring(self):\n        self.monitoring = True\n        import threading\n        \n        def monitor_resources():\n            while self.monitoring:\n                # Track CPU and memory usage\n                cpu_percent = psutil.cpu_percent(interval=1)\n                memory_info = psutil.virtual_memory()\n                \n                self.cpu_usage.append(cpu_percent)\n                self.memory_usage.append(memory_info.percent)\n                \n                time.sleep(1)\n        \n        thread = threading.Thread(target=monitor_resources)\n        thread.daemon = True\n        thread.start()\n        return thread\n    \n    def stop_monitoring(self):\n        self.monitoring = False\n    \n    def get_resource_summary(self):\n        if not self.cpu_usage or not self.memory_usage:\n            return {'error': 'No monitoring data collected'}\n        \n        return {\n            'avg_cpu_usage': sum(self.cpu_usage) / len(self.cpu_usage),\n            'peak_cpu_usage': max(self.cpu_usage),\n            'avg_memory_usage': sum(self.memory_usage) / len(self.memory_usage),\n            'peak_memory_usage': max(self.memory_usage),\n            'total_data_points': len(self.cpu_usage)\n        }\n\nmonitor = LocalResourceMonitor()\nmonitor.start_monitoring()\n\nstagehand = Stagehand(env=\"LOCAL\")\n\n# ... run automation\n\nmonitor.stop_monitoring()\nprint('Resource Usage:', monitor.get_resource_summary())\n```\n</CodeGroup>\n\n\n  <Card title=\"LLM Usage\" icon=\"chart-line\" href=\"/v2/configuration/evals\">\n    Monitor token usage, costs, and speed. Set up automated alerting for critical failures. Implement cost tracking across different environments. Use session analytics to optimize automation workflows.\n  </Card>\n\n\n## Real-Time Metrics & Monitoring\n\n### Basic Usage Tracking\n\nMonitor your automation's resource usage in real-time with `stagehand.metrics`:\n\n<CodeGroup>\n```typescript TypeScript\n// Get current metrics\nconsole.log(stagehand.metrics);\n\n// Monitor during automation\nconst startTime = Date.now();\nconst initialMetrics = stagehand.metrics;\n\n// ... perform automation tasks\n\nconst finalMetrics = stagehand.metrics;\nconst executionTime = Date.now() - startTime;\n\nconsole.log('Automation Summary:', {\n  totalTokens: finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens,\n  totalCost: calculateCost(finalMetrics),\n  executionTime,\n  efficiency: (finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens) / executionTime\n});\n```\n\n```python Python\n# Get current metrics\nprint(stagehand.metrics)\n\n# Monitor during automation\nimport time\nstart_time = time.time()\ninitial_metrics = stagehand.metrics\n\n# ... perform automation tasks\n\nfinal_metrics = stagehand.metrics\nexecution_time = (time.time() - start_time) * 1000  # Convert to ms\n\nprint('Automation Summary:', {\n    'total_tokens': final_metrics['total_prompt_tokens'] + final_metrics['total_completion_tokens'],\n    'total_cost': calculate_cost(final_metrics),\n    'execution_time': execution_time,\n    'efficiency': (final_metrics['total_prompt_tokens'] + final_metrics['total_completion_tokens']) / execution_time\n})\n```\n</CodeGroup>\n\n### Understanding Metrics Data\n\nThe metrics object provides detailed breakdown by Stagehand operation:\n\n<CodeGroup>\n```typescript TypeScript\n{\n  actPromptTokens: 4011,\n  actCompletionTokens: 51,\n  actInferenceTimeMs: 1688,\n\n  extractPromptTokens: 4200,\n  extractCompletionTokens: 243,\n  extractInferenceTimeMs: 4297,\n\n  observePromptTokens: 347,\n  observeCompletionTokens: 43,\n  observeInferenceTimeMs: 903,\n\n  totalPromptTokens: 8558,\n  totalCompletionTokens: 337,\n  totalInferenceTimeMs: 6888\n}\n```\n\n```python Python\n{\n  \"act_prompt_tokens\": 4011,\n  \"act_completion_tokens\": 51,\n  \"act_inference_time_ms\": 1688,\n\n  \"extract_prompt_tokens\": 4200,\n  \"extract_completion_tokens\": 243,\n  \"extract_inference_time_ms\": 4297,\n\n  \"observe_prompt_tokens\": 347,\n  \"observe_completion_tokens\": 43,\n  \"observe_inference_time_ms\": 903,\n\n  \"total_prompt_tokens\": 8558,\n  \"total_completion_tokens\": 337,\n  \"total_inference_time_ms\": 6888\n}\n```\n</CodeGroup>\n\n### Log Inference to File\n\nYou can also log inference to a file by setting `logInferenceToFile` to `true`. This will create a directory called `inference_summary` in your project's root directory.\n<CodeGroup>\n```typescript TypeScript\nconst stagehand = new Stagehand({\n  logInferenceToFile: true,    \n});\n```\n\n```python Python\nstagehand = Stagehand(\n    log_inference_to_file=True,             \n)\n```\n</CodeGroup>\nThe `inference_summary` directory provides granular analysis data:\n```\ninference_summary/\n├── act_summary/\n│   ├── {timestamp}.json\n│   ├── {timestamp}.json\n│   └── ...\n│   └── act_summary.json\n├── extract_summary/\n│   ├── {timestamp}.json\n│   ├── {timestamp}.json\n│   └── ...\n│   └── extract_summary.json\n├── observe_summary/\n│   ├── {timestamp}.json\n│   ├── {timestamp}.json\n│   └── ...\n│   └── observe_summary.json\n```\n\n### Log File Structure\n\nEach operation creates detailed logs for analysis:\n```typescript\n{\n  \"act_summary\": [\n    {\n      \"act_inference_type\": \"act\",\n      \"timestamp\": \"20250329_080446068\",\n      \"LLM_input_file\": \"20250329_080446068_act_call.txt\",\n      \"LLM_output_file\": \"20250329_080447019_act_response.txt\",\n      \"prompt_tokens\": 3451,\n      \"completion_tokens\": 45,\n      \"inference_time_ms\": 951\n    },\n    ...\n  ],\n}\n```\n\n\n## Best Practices\n\n<AccordionGroup>\n<Accordion title=\"Production Monitoring\">\n- Track session success rates and failure patterns\n- Monitor resource usage and scaling requirements\n- Set up automated alerting for critical failures\n- Implement cost tracking across different environments\n- Use session analytics to optimize automation workflows\n</Accordion>\n\n<Accordion title=\"Performance Optimization\">\n- Compare Browserbase vs local execution times\n- Monitor token usage and inference costs across models\n- Track geographic performance differences\n- Identify bottlenecks in automation workflows\n- Optimize for cost-effectiveness and speed\n</Accordion>\n\n<Accordion title=\"Operational Insights\">\n- Track session distribution across regions\n- Monitor concurrent session limits and scaling\n- Analyze failure patterns and common error scenarios\n- Use session recordings for root cause analysis\n- Implement custom metadata for workflow categorization\n</Accordion>\n\n<Accordion title=\"Integration & Alerting\">\n- Integrate session APIs with monitoring dashboards\n- Set up automated notifications for session failures  \n- Track SLA compliance and performance benchmarks\n- Monitor resource costs and usage patterns\n- Use analytics data for capacity planning and optimization\n</Accordion>\n</AccordionGroup>\n\nFor detailed logging and debugging capabilities, see [Logging](/v2/configuration/logging)."
  },
  {
    "path": "packages/docs/v2/first-steps/ai-rules.mdx",
    "content": "---\ntitle: AI Rules\ndescription: Using AI to write Stagehand code faster, and better.\n---\n\nYou're likely using AI to write code, and there's a **right and wrong way to do it.** This page is a collection of rules, configs, and copy‑paste snippets to allow your AI agents/assistants to write performant, Stagehand code as fast as possible. \n\n## Quickstart\n\n<CardGroup cols={2}>\n  <Card title=\"Add MCP servers\" icon=\"screwdriver-wrench\">\n    Configure Browserbase (Stagehand), Context7, DeepWiki, and Stagehand Docs in your MCP client. \n  </Card>\n  <Card title=\"Pin editor rules\" icon=\"memo\">\n    Drop in `cursorrules` and `claude.md` so AI agents/assistants always emit Stagehand patterns. \n  </Card>\n</CardGroup>\n\n## Using MCP Servers\n\nMCP (Model Context Protocol) servers act as intermediaries that connect AI systems to external data sources and tools. These servers enable your coding assistant to access real-time information, execute tasks, and retrieve structured data to enhance code generation accuracy.\n\nThe following **MCP servers** provide specialized access to Stagehand documentation and related resources:\n\n<Accordion title=\"Context7 by Upstash\" icon=\"database\">\nProvides semantic search across documentation and codebase context. Context7 enables AI assistants to find relevant code patterns, examples, and implementation details from your project history. It maintains contextual understanding of your development workflow and can surface related solutions from previous work.\n\n**Installation:**\n```json\n{\n  \"mcpServers\": {\n    \"context7\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@upstash/context7-mcp\"]\n    }\n  }\n}\n```\n</Accordion>\n\n<Accordion title=\"DeepWiki by Cognition\" icon=\"book-open\">\nOffers deep indexing of GitHub repositories and documentation. DeepWiki allows AI agents to understand project architecture, API references, and best practices from the entire Stagehand ecosystem. It provides comprehensive knowledge about repository structure, code relationships, and development patterns.\n\n**Installation:**\n```json\n{\n  \"mcpServers\": {\n    \"deepwiki\": {\n      \"url\": \"https://mcp.deepwiki.com/mcp\"\n    }\n  }\n}\n```\n</Accordion>\n\n<Accordion title=\"Stagehand Docs by Mintlify\" icon=\"mintbit\">\nDirect access to official Stagehand documentation. This MCP server provides AI assistants with up-to-date API references, configuration options, and usage examples for accurate code generation. Mintlify auto-generates this server from the official docs, ensuring your AI assistant always has the latest information.\n\n**Usage:**\n```json\n{\n  \"mcpServers\": {\n    \"stagehand-docs\": {\n      \"url\": \"https://docs.stagehand.dev/mcp\"\n    }\n  }\n}\n```\n</Accordion>\n\n**How MCP Servers Enhance Your Development:**\n- **Real-time Documentation Access**: AI assistants can query the latest Stagehand docs, examples, and best practices\n- **Context-Aware Code Generation**: Servers provide relevant code patterns and configurations based on your specific use case\n- **Reduced Integration Overhead**: Standardized protocol eliminates the need for custom integrations with each documentation source\n- **Enhanced Accuracy**: AI agents receive structured, up-to-date information rather than relying on potentially outdated training data\n\n\n<Tip>\n**Prompting tip:** \nExplicitly ask your coding agent/assistant to use these MCP servers to fetch relevant information from the docs so they have better context and know how to write proper Stagehand code. \n\nie. **\"Use the stagehand-docs MCP to fetch the act/observe guidelines, then generate code that follows them. Prefer cached observe results.\"**\n</Tip>\n\n\n## Editor rule files (copy‑paste)\n\nDrop these in `.cursorrules`, `windsurfrules`, `claude.md`, or any agent rule framework:\n\n<Accordion title=\"TypeScript\">\n\n``````md\n# Stagehand Project\n\nThis is a project that uses [Stagehand](https://github.com/browserbase/stagehand), which amplifies Playwright with AI-powered `act`, `extract`, and `observe` methods added to the Page class.\n\n`Stagehand` is a class that provides configuration and browser automation capabilities with:\n- `stagehand.page`: A StagehandPage object (extends Playwright Page)\n- `stagehand.context`: A StagehandContext object (extends Playwright BrowserContext)\n- `stagehand.agent()`: Create AI-powered agents for autonomous multi-step workflows\n- `stagehand.init()`: Initialize the browser session\n- `stagehand.close()`: Clean up resources\n\n`Page` extends Playwright's Page class with AI-powered methods:\n- `act()`: Perform actions on web elements using natural language\n- `extract()`: Extract structured data from pages using schemas\n- `observe()`: Plan actions and get selectors before executing\n\n`Agent` provides autonomous Computer Use Agent capabilities:\n- `execute()`: Perform complex multi-step tasks using natural language instructions\n\n`Context` extends Playwright's BrowserContext class for browser session management.\n\nUse the following rules to write code for this project.\n\n- To plan an instruction like \"click the sign in button\", use Stagehand `observe` to get the action to execute.\n\n```typescript\nconst results = await page.observe(\"Click the sign in button\");\n```\n\nYou can also pass in the following params:\n\n```typescript\nawait page.observe({\n  instruction: \"the instruction to execute\",\n  returnAction: true \n});\n```\n\n- The result of `observe` is an array of `ObserveResult` objects that can directly be used as params for `act` like this:\n  ```typescript\n  const results = await page.observe({\n    instruction: \"the instruction to execute\",\n    returnAction: true, // return the action to execute\n  });\n\n  await page.act(results[0]);\n  ```\n  \n- When writing code that needs to extract data from the page, use Stagehand `extract`. Explicitly pass the following params by default:\n\n```typescript\nconst { someValue } = await page.extract({\n  instruction: \"the instruction to execute\",\n  schema: z.object({\n    someValue: z.string(),\n  }), // The schema to extract\n});\n```\n\n## Initialize\n\n```typescript\nimport { Stagehand, Page, BrowserContext } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\"\n});\n\nawait stagehand.init();\n\nconst page = stagehand.page; // Playwright Page with act, extract, and observe methods\n\nconst context = stagehand.context; // Playwright BrowserContext\n```\n### Configuration Options\n```typescript\nconst StagehandConfig = {\n  env: \"BROWSERBASE\" | \"LOCAL\", // Environment to run in\n  apiKey: process.env.BROWSERBASE_API_KEY, // Browserbase API key\n  projectId: process.env.BROWSERBASE_PROJECT_ID, // Browserbase project ID\n  debugDom: true, // Enable DOM debugging features\n  headless: false, // Run browser in headless mode\n  domSettleTimeoutMs: 30_000, // Timeout for DOM to settle\n  enableCaching: true, // Enable action caching\n  modelName: \"gpt-4o\", // AI model to use\n  modelClientOptions: {\n    apiKey: process.env.OPENAI_API_KEY, // OpenAI API key\n  },\n};\n```\n## Act\n\nYou can act directly with string instructions:\n\n```typescript\nawait page.act(\"Click the sign in button\");\n```\n\nUse variables for dynamic form filling:\n\n```typescript\nawait page.act({\n  action: `Enter the following information:\n    Name: %name%\n    Email: %email%\n    Phone: %phone%`,\n  variables: {\n    name: \"John Doe\",\n    email: \"john@example.com\", \n    phone: \"+1-555-0123\"\n  }\n});\n```\n\n**Best Practices:**\n- Cache the results of `observe` to avoid unexpected DOM changes\n- Keep actions atomic and specific (e.g., \"Click the sign in button\" not \"Sign in to the website\")\n- Use variable substitution for dynamic data entry\n\nAct `action` should be as atomic and specific as possible, i.e. \"Click the sign in button\" or \"Type 'hello' into the search input\".\nAVOID actions that are more than one step, i.e. \"Order me pizza\" or \"Send an email to Paul asking him to call me\".\n\n## Extract\n\n### Simple String Extraction\n\n```typescript\nconst signInButtonText = await page.extract(\"extract the sign in button text\");\n```\n\n### Structured Extraction with Schema (Recommended)\n\nAlways use Zod schemas for structured data extraction:\n\n```typescript\nimport { z } from \"zod/v3\";\n\nconst data = await page.extract({\n  instruction: \"extract the sign in button text\",\n  schema: z.object({\n    text: z.string(),\n  }),\n});\n```\n\n### Array Extraction\n\nTo extract multiple items, wrap the array in a single object:\n\n```typescript\nconst data = await page.extract({\n  instruction: \"extract the text inside all buttons\",\n  schema: z.object({\n    buttons: z.array(z.string()),\n  })\n});\n```\n\n### Complex Object Extraction\n\nFor more complex data structures:\n\n```typescript\nconst productData = await page.extract({\n  instruction: \"extract product information from this page\",\n  schema: z.object({\n    title: z.string(),\n    price: z.number(),\n    description: z.string(),\n    features: z.array(z.string()),\n    availability: z.boolean(),\n  }),\n});\n```\n\n### Schema Validation\n\n```typescript\nimport { validateZodSchema } from \"./utils.js\";\nimport { z } from \"zod/v3\";\n\nconst schema = z.object({ name: z.string() });\nconst isValid = validateZodSchema(schema, { name: \"John\" }); // true\n```\n\n## Agent System\n\nStagehand provides an Agent System for autonomous web browsing using Computer Use Agents (CUA). Agents execute multi-step workflows using natural language instructions.\n\n### Creating Agents\n\n```typescript\n// Basic agent (default)\nconst agent = stagehand.agent();\n\n// OpenAI agent\nconst agent = stagehand.agent({\n  provider: \"openai\",\n  model: \"computer-use-preview\",\n  instructions: \"You are a helpful assistant that can use a web browser.\",\n  options: { \n    apiKey: process.env.OPENAI_API_KEY \n  }\n});\n\n// Anthropic agent\nconst agent = stagehand.agent({\n  provider: \"anthropic\", \n  model: \"claude-sonnet-4-20250514\",\n  instructions: \"You are a helpful assistant that can use a web browser.\",\n  options: { \n    apiKey: process.env.ANTHROPIC_API_KEY \n  }\n});\n```\n### Agent Execution\n```typescript\n// Simple task\nconst result = await agent.execute(\"Extract the title from this webpage\");\n\n// Complex multi-step task\nconst result = await agent.execute({\n  instruction: \"Apply for the first engineer position with mock data\",\n  maxSteps: 20,\n  autoScreenshot: true\n});\n```\n\n### Best Practices\n- Be specific with instructions: `\"Fill out the contact form with name 'John Doe' and submit it\"`\n- Break down complex tasks into smaller steps\n- Use error handling with try/catch blocks\n- Combine agents for navigation with traditional methods for precise data extraction\n\n```typescript\n// Good: Specific instructions\nawait agent.execute(\"Navigate to products page and filter by 'Electronics'\");\n\n// Avoid: Vague instructions  \nawait agent.execute(\"Do some stuff on this page\");\n```\n\n## Project Structure Best Practices\n\n- Store configurations in `stagehand.config.ts`\n- Use environment variables for API keys (see `.env.example`)\n- Implement main automation logic in functions that accept `{ page, context, stagehand }`\n- Use TypeScript with proper imports from `@browserbasehq/stagehand`\n``````\n\n</Accordion>\n\n<Accordion title=\"Python\">\n\n``````md\n# Stagehand Python Project\n\nThis is a project that uses [Stagehand Python](https://github.com/browserbase/stagehand-python), which provides AI-powered browser automation with `act`, `extract`, and `observe` methods.\n\n`Stagehand` is a class that provides configuration and browser automation capabilities with:\n- `stagehand.page`: A StagehandPage object (extends Playwright Page)\n- `stagehand.context`: A StagehandContext object (extends Playwright BrowserContext)\n- `stagehand.agent()`: Create AI-powered agents for autonomous multi-step workflows\n- `stagehand.init()`: Initialize the browser session\n- `stagehand.close()`: Clean up resources\n\n`Page` extends Playwright's Page class with AI-powered methods:\n- `act()`: Perform actions on web elements using natural language\n- `extract()`: Extract structured data from pages using schemas\n- `observe()`: Plan actions and get selectors before executing\n\n`Agent` provides autonomous Computer Use Agent capabilities:\n- `execute()`: Perform complex multi-step tasks using natural language instructions\n\nUse the following rules to write code for this project.\n\n- To plan an instruction like \"click the sign in button\", use Stagehand `observe` to get the action to execute.\n\n```python\nresults = await page.observe(\"Click the sign in button\")\n```\n\nYou can also pass in the following params:\n\n```python\nawait page.observe(\n    instruction=\"the instruction to execute\",\n    draw_overlay=True  # Show visual overlay on observed elements\n)\n```\n\n- The result of `observe` is a list of `ObserveResult` objects that can directly be used as params for `act` like this:\n  ```python\n  results = await page.observe(\"Click the sign in button\")\n  await page.act(results[0])\n  ```\n- When writing code that needs to extract data from the page, use Stagehand `extract`. Use Pydantic models for schemas:\n\n```python\nfrom pydantic import BaseModel\n\nclass ExtractedData(BaseModel):\n    some_value: str\n\nresult = await page.extract(\n    instruction=\"the instruction to execute\",\n    schema=ExtractedData\n)\n```\n\n## Initialize\n\n```python\nfrom stagehand import Stagehand, StagehandConfig\nimport asyncio\nimport os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nasync def main():\n    config = StagehandConfig(\n        env=\"BROWSERBASE\",  # or \"LOCAL\"\n        api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n        project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n        model_name=\"google/gemini-2.5-flash-preview-05-20\",\n        model_api_key=os.getenv(\"MODEL_API_KEY\"),\n    )\n    \n    # Recommended: Use as async context manager\n    async with Stagehand(config) as stagehand:\n        page = stagehand.page\n        # Your automation code here\n        \n    # Alternative: Manual initialization\n    stagehand = Stagehand(config)\n    await stagehand.init()\n    page = stagehand.page\n    # Your automation code here\n    await stagehand.close()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Configuration Options\n\nKey configuration options in `StagehandConfig`:\n\n```python\nconfig = StagehandConfig(\n    env=\"BROWSERBASE\",  # or \"LOCAL\"\n    api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n    project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n    model_name=\"google/gemini-2.5-flash-preview-05-20\",\n    model_api_key=os.getenv(\"MODEL_API_KEY\"),\n    verbose=1,  # 0=minimal, 1=medium, 2=detailed\n    dom_settle_timeout_ms=30000,\n    self_heal=True,  # Enable self-healing functionality\n)\n```\n\n## Act\n\nYou can act directly with string instructions:\n\n```python\nawait page.act(\"Click the sign in button\")\n```\n\nUse variables for dynamic form filling:\n\n```python\nawait page.act(\n    \"Enter the following information: Name: John Doe, Email: john@example.com\"\n)\n```\n\n**Best Practices:**\n- Cache the results of `observe` to avoid unexpected DOM changes\n- Keep actions atomic and specific (e.g., \"Click the sign in button\" not \"Sign in to the website\")\n- Use specific, descriptive instructions\n\nAct `action` should be as atomic and specific as possible, i.e. \"Click the sign in button\" or \"Type 'hello' into the search input\".\nAVOID actions that are more than one step, i.e. \"Order me pizza\" or \"Send an email to Paul asking him to call me\".\n\n## Extract\n\n### Simple String Extraction\n```python\nsign_in_button_text = await page.extract(\"extract the sign in button text\")\n```\n\n### Structured Extraction with Schema (Recommended)\nAlways use Pydantic models for structured data extraction:\n\n```python\nfrom pydantic import BaseModel, Field\nfrom typing import List\n\nclass ButtonData(BaseModel):\n    text: str = Field(..., description=\"Button text content\")\n\ndata = await page.extract(\n    instruction=\"extract the sign in button text\",\n    schema=ButtonData\n)\n```\n\n### Array Extraction\nFor arrays, use List types:\n\n```python\nfrom pydantic import BaseModel, Field\nfrom typing import List\n\nclass ButtonsData(BaseModel):\n    buttons: List[str] = Field(..., description=\"List of button texts\")\n\ndata = await page.extract(\n    instruction=\"extract the text inside all buttons\",\n    schema=ButtonsData\n)\n```\n\n### Complex Object Extraction\nFor more complex data structures:\n\n```python\nfrom pydantic import BaseModel, Field\nfrom typing import List\n\nclass Company(BaseModel):\n    name: str = Field(..., description=\"Company name\")\n    description: str = Field(..., description=\"Brief company description\")\n\nclass Companies(BaseModel):\n    companies: List[Company] = Field(..., description=\"List of companies\")\n\ncompanies_data = await page.extract(\n    \"Extract names and descriptions of 5 companies\",\n    schema=Companies\n)\n```\n\n## Agent System\n\nStagehand provides an Agent System for autonomous web browsing using Computer Use Agents (CUA).\n\n### Creating Agents\n\n```python\n# Basic agent (uses default model)\nagent = stagehand.agent()\n\n# OpenAI agent\nagent = stagehand.agent(\n    model=\"computer-use-preview\",\n    instructions=\"You are a helpful web navigation assistant.\",\n    options={\"apiKey\": os.getenv(\"OPENAI_API_KEY\")}\n)\n\n# Anthropic agent\nagent = stagehand.agent(\n    model=\"claude-sonnet-4-20250514\",\n    instructions=\"You are a helpful web navigation assistant.\",\n    options={\"apiKey\": os.getenv(\"ANTHROPIC_API_KEY\")}\n)\n```\n\n### Agent Execution\n\n```python\n# Simple task\nresult = await agent.execute(\"Play a game of 2048\")\n\n# Complex multi-step task with options\nresult = await agent.execute(\n    instruction=\"Apply for the first engineer position with mock data\",\n    max_steps=20,\n    auto_screenshot=True,\n    wait_between_actions=1000  # milliseconds\n)\n```\n\n**Best Practices:**\n- Be specific with instructions: `\"Fill out the contact form with name 'John Doe' and submit it\"`\n- Break down complex tasks into smaller steps\n- Use error handling with try/except blocks\n- Combine agents for navigation with traditional methods for precise data extraction\n\n```python\n# Good: Specific instructions\nawait agent.execute(\"Navigate to products page and filter by 'Electronics'\")\n\n# Avoid: Vague instructions\nawait agent.execute(\"Do some stuff on this page\")\n```\n\n## Project Structure Best Practices\n\n- Store configurations in environment variables or config files\n- Use async/await patterns consistently\n- Implement main automation logic in async functions\n- Use async context managers for resource management\n- Use type hints and Pydantic models for data validation\n- Handle exceptions appropriately with try/except blocks\n``````\n\n</Accordion>\n\n## Security notes\n\n- Do not embed secrets in docs or rule files; use env vars in MCP configs.\n- Avoid broad actions that may trigger unintended navigation; prefer `observe` first.\n\n## Resources/references\n\n- Context7 MCP (Upstash)\n  - https://github.com/upstash/context7\n- DeepWiki MCP\n  - https://mcp.deepwiki.com/\n- Stagehand Docs MCP (Mintlify)\n  - https://docs.stagehand.dev/mcp\n"
  },
  {
    "path": "packages/docs/v2/first-steps/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Integrate Stagehand into an existing project.\n---\n\nInstall Stagehand in your current app with the TypeScript or Python SDK.\n\n<Tip>\nFor TypeScript/Node.js: We highly recommend using the Node.js runtime environment to run Stagehand scripts, as opposed to newer alternatives like Deno or Bun. \n\n**Bun does not support Stagehand** since it doesn't support [Playwright](https://github.com/search?q=repo:oven-sh/bun+playwright&type=issues).\n\nFor Python: We require Python 3.9+ and recommend using [uv](https://docs.astral.sh/uv/) to manage your virtual environment.\n</Tip>\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n### Install dependencies\n\n<CodeGroup>\n```bash npm\nnpm install @browserbasehq/stagehand playwright zod\n```\n\n```bash pnpm\npnpm add @browserbasehq/stagehand playwright zod\n```\n\n```bash yarn\nyarn add @browserbasehq/stagehand playwright zod\n```\n</CodeGroup>\n\n<Tip>\nIf you plan to run locally, install browsers once: `npx playwright install`.\nFor cloud browser sessions, skip this.\n</Tip>\n\n### Configure environment\n\nSet environment variables (or a `.env` via your framework):\n\n<CodeGroup>\n```bash Bash\nOPENAI_API_KEY=your_api_key\nBROWSERBASE_API_KEY=your_api_key\nBROWSERBASE_PROJECT_ID=your_project_id\n```\n</CodeGroup>\n\n### Use in your codebase\n\nAdd Stagehand where you need browser automation.\n\n<CodeGroup>\n```typescript TypeScript\nimport \"dotenv/config\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod/v3\";\n\nasync function main() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\"\n  });\n\n  await stagehand.init();\n  const page = stagehand.page;\n\n  await page.goto(\"https://example.com\");\n  \n  // Act on the page\n  await page.act(\"Click the sign in button\");\n  \n  // Extract structured data\n  const { title } = await page.extract({\n    instruction: \"extract the page title\",\n    schema: z.object({\n      title: z.string(),\n    }),\n  });\n\n  console.log(title);\n  await stagehand.close();\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n```\n</CodeGroup>\n\n</Tab>\n\n<Tab title=\"Python\">\n\n### Add dependencies\n\n<CodeGroup>\n\n```bash uv\nuv add stagehand\n```\n\n```bash pip\npip install stagehand\n```\n\n</CodeGroup>\n\n### Configure environment\n\nSet environment variables (or a `.env` via your framework):\n\n<CodeGroup>\n```bash Bash\nMODEL_API_KEY=your_api_key\nBROWSERBASE_API_KEY=your_api_key\nBROWSERBASE_PROJECT_ID=your_project_id\n```\n</CodeGroup>\n\n### Use in your codebase\n\n<CodeGroup>\n```python Python\nimport os\nimport asyncio\nfrom stagehand import Stagehand\n\nasync def main():\n    stagehand = Stagehand(\n        env=\"BROWSERBASE\",\n        model_api_key=os.getenv(\"MODEL_API_KEY\")\n    )\n    await stagehand.init()\n    page = stagehand.page\n    \n    await page.goto(\"https://example.com\")\n    \n    # Act on the page\n    await page.act(\"Click the sign in button\")\n    \n    # Extract structured data\n    result = await page.extract({\n        \"instruction\": \"extract the page title\",\n        \"schema\": {\n            \"title\": {\n                \"type\": \"string\"\n            }\n        }\n    })\n    \n    print(result[\"title\"])\n    await stagehand.close()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n</CodeGroup>\n\n</Tab>\n\n</Tabs>\n\n## Next steps\n\n<CardGroup cols={2}>\n  <Card \n    title=\"Configuration\"\n    icon=\"gear\"\n    href=\"/v2/configuration/browser\"\n  >\n    Environment, Browserbase vs Local, logging, timeouts, LLM customization\n  </Card>\n  <Card \n    title=\"Act\"\n    icon=\"arrow-pointer\"\n    href=\"/v2/basics/act\"\n  >\n    Perform precise actions with natural language\n  </Card>\n  <Card \n    title=\"Extract\"\n    icon=\"download\"\n    href=\"/v2/basics/extract\"\n  >\n    Typed data extraction with Zod schemas\n  </Card>\n  <Card \n    title=\"Observe\"\n    icon=\"eye\"\n    href=\"/v2/basics/observe\"\n  >\n    Discover elements and suggested actions\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/first-steps/introduction.mdx",
    "content": "---\ntitle: Introducing Stagehand\nsidebarTitle: Introduction\ndescription: Developers use Stagehand to reliably automate the web.\n---\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## The Problem with Browser Automation\n\nTraditional frameworks like Playwright and Puppeteer force you to write brittle scripts that break with every UI change. Web agents promise to solve this with AI, but leave you at the mercy of unpredictable behavior.\n\n**You're stuck between two bad options:**\n- **Too brittle**: Traditional selectors break when websites change\n- **Too agentic**: AI agents are unpredictable and impossible to debug\n\n## Enter Stagehand\n\nStagehand gives you the best of both worlds through four powerful primitives that let you choose exactly how much AI to use:\n\n<CardGroup cols={2}>\n  <Card title=\"Act\" icon=\"play\" href=\"/v2/basics/act\">\n    Execute actions using natural language\n  </Card>\n  <Card title=\"Extract\" icon=\"database\" href=\"/v2/basics/extract\">\n    Pull structured data with schemas\n  </Card>\n  <Card title=\"Observe\" icon=\"eye\" href=\"/v2/basics/observe\">\n    Discover available actions on any page\n  </Card>\n  <Card title=\"Agent\" icon=\"robot\" href=\"/v2/basics/agent\">\n    Automate entire workflows autonomously\n  </Card>\n</CardGroup>\n\n<CodeGroup>\n```typescript TypeScript\n// Act - Execute natural language actions\nawait page.act(\"click the login button\");\n\n// Extract - Pull structured data\nconst { price } = await page.extract({\n  schema: z.object({ price: z.number() })\n});\n\n// Observe - Discover available actions\nconst actions = await page.observe(\"find submit buttons\");\n\n// Agent - Automate entire workflows\nconst agent = stagehand.agent({\n    provider: \"anthropic\",\n    model: \"claude-sonnet-4-20250514\",\n    options: {\n      apiKey: process.env.ANTHROPIC_API_KEY,\n    },\n})\nawait agent.execute(\"apply for this job\");\n```\n```python Python\n# Act - Execute natural language actions\nawait page.act(\"click the login button\")\n\n# Extract - Pull structured data\nresult = await page.extract(\n  schema={\"price\": float}\n)\n\n# Observe - Discover available actions\nactions = await page.observe(\"find submit buttons\")\n\n# Agent - Automate entire workflows\nawait agent.execute(\"apply for this job\")\n```\n</CodeGroup>\n\n\n## Why Developers Choose Stagehand\n\n- **Precise Control**: Mix AI-powered actions with deterministic code. You decide exactly how much AI to use.\n\n- **Actually Repeatable**: Save and replay actions exactly. No more \"it worked on my machine\" with browser automations.\n\n- **Maintainable at Scale**: One script can automate multiple websites. When sites change, your automations adapt.\n\n- **Composable Tools**: Choose your level of automation with Act, Extract, Observe, and Agent.\n\n## Built for Modern Development\nStagehand is designed for developers building production browser automations and AI agents that need reliable web access.\n\n<AccordionGroup>\n  <Accordion title=\"Full Playwright Compatibility\">\n    Use any Playwright API alongside Stagehand. You're never locked into our abstractions.\n  </Accordion>\n  <Accordion title=\"TypeScript & Python SDKs\">\n    First-class support for both ecosystems with type safety and IDE autocomplete.\n  </Accordion>\n  <Accordion title=\"Works Everywhere\">\n    Compatible with all Chromium-based browsers: Chrome, Edge, Arc, Brave, and more.\n  </Accordion>\n  <Accordion title=\"Built by Browserbase\">\n    Created and maintained by the team behind enterprise browser infrastructure.\n  </Accordion>\n</AccordionGroup>\n\n## Get Started in 60 Seconds\n<Info>\n  **Pro tip**: For best results, we recommend using Stagehand with [Browserbase](https://www.browserbase.com) for reliable cloud browser infrastructure.\n</Info>\n<CardGroup cols={2}>\n  <Card\n    title=\"Quickstart\"\n    icon=\"rocket\"\n    href=\"/v2/first-steps/quickstart\"\n  >\n    Build your first automation in under a minute\n  </Card>\n  <Card\n    title=\"Try Director\"\n    icon=\"wand-magic-sparkles\"\n    href=\"https://www.director.ai\"\n  >\n    Generate Stagehand scripts with AI\n  </Card>\n  <Card\n    title=\"View Templates\"\n    icon=\"code\"\n    href=\"https://www.browserbase.com/templates\"\n  >\n    See real-world automation examples\n  </Card>\n  <Card\n    title=\"Join Discord\"\n    icon=\"discord\"\n    href=\"https://stagehand.dev/discord\"\n  >\n    Get help from the community\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/first-steps/quickstart.mdx",
    "content": "---\ntitle: Quickstart\ndescription: 'Stagehand allows you to build web automations with natural language and code.'\n---\n\nIf this is your **first time using Stagehand**, you should try [Director](https://director.ai) first. It's an agent that allows you to build Stagehand workflows using natural language. You can also try Stagehand using our [MCP server](/v2/integrations/mcp/introduction).\n\nOtherwise, the quickest way to start with Stagehand is with our CLI. It scaffolds a ready‑to‑run Stagehand app with sensible defaults, and an example script.\n\n<Note>\nThis quickstart is for **TypeScript**. For **Python**, see the [installation guide](/v2/first-steps/installation).\n</Note>\n\n## 1) Create a sample project\n\n<CodeGroup>\n```bash Bash\nnpx create-browser-app\n```\n</CodeGroup>\n\n## 2) Run it\n\nFollow the CLI prompts to enter the project directory and add your API keys. Then run the example script.\n\n<CodeGroup>\n```bash Bash\ncd my-stagehand-app # Enter the project directory\ncp .env.example .env  # Add your API keys\nnpm start # Run the example script\n```\n</CodeGroup>\n\n## 3) Use Stagehand (act, extract, observe)\n\nThe scaffold includes an index.ts file that contains the example script. Here's what it looks like:\n\n<CodeGroup>\n```typescript TypeScript\nimport \"dotenv/config\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nasync function main() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\"\n  });\n\n  await stagehand.init();\n\n  console.log(`Stagehand Session Started`);\n  console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`);\n\n  const page = stagehand.page;\n\n  await page.goto(\"https://stagehand.dev\");\n\n  const extractResult = await page.extract(\"Extract the value proposition from the page.\");\n  console.log(`Extract result:\\n`, extractResult);\n\n  const actResult = await page.act(\"Click the 'Evals' button.\");\n  console.log(`Act result:\\n`, actResult);\n\n  const observeResult = await page.observe(\"What can I click on this page?\");\n  console.log(`Observe result:\\n`, observeResult);\n\n  const agent = await stagehand.agent({\n    instructions: \"You're a helpful assistant that can control a web browser.\",\n  });\n\n  const agentResult = await agent.execute(\"What is the most accurate model to use in Stagehand?\");\n  console.log(`Agent result:\\n`, agentResult);\n\n  await stagehand.close();\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n\n```\n</CodeGroup>\n\n<Tip>\nTo use, set provider keys in `.env` (e.g., `OPENAI_API_KEY`). For cloud browsers, add `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID`.\n</Tip>\n\n## Next steps\n\nLearn about the Stagehand primitives: act, extract, observe, and agent.\n\n<CardGroup cols={2}>\n  <Card \n    title=\"Act\" \n    icon=\"arrow-pointer\" \n    href=\"/v2/basics/act\"\n  >\n    Perform actions on web pages with natural language\n  </Card>\n  \n  <Card \n    title=\"Extract\" \n    icon=\"download\" \n    href=\"/v2/basics/extract\"\n  >\n    Get structured data with Zod schemas\n  </Card>\n  \n  <Card \n    title=\"Observe\" \n    icon=\"eye\" \n    href=\"/v2/basics/observe\"\n  >\n    Discover available elements and actions\n  </Card>\n  \n  <Card \n    title=\"Agent\" \n    icon=\"robot\" \n    href=\"/v2/basics/agent\"\n  >\n    Autonomous multi-step browser workflows\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/integrations/crew-ai/configuration.mdx",
    "content": "---\ntitle: \"Use CrewAI to Automate Browser Tasks\"\nsidebarTitle: Configuration\ndescription: \"Create intelligent agents that can interact with websites and automate browser tasks using natural language instructions\"\n---\n\nThis guide walks you through setting up CrewAI with Browserbase to create agents that can perform web automation tasks using natural language instructions.\n\n## Step 1: Install Dependencies\n\nInstall the required packages for CrewAI and Stagehand integration:\n\n```bash\npip install stagehand-py crewai crewai-tools\n```\n\n## Step 2: Configure Environment Variables\n\nYou'll need API keys from three services:\n\n1. **Browserbase API Key and Project ID**: Get these from your [Browserbase dashboard](https://www.browserbase.com/)\n2. **LLM API Key**: Get an API key from [OpenAI](https://platform.openai.com/api-keys) or [Anthropic](https://console.anthropic.com/)\n\nStore your API keys securely as environment variables:\n\n```bash\nBROWSERBASE_API_KEY=\"your-browserbase-api-key\"\nBROWSERBASE_PROJECT_ID=\"your-browserbase-project-id\"\nOPENAI_API_KEY=\"your-openai-api-key\"\nANTHROPIC_API_KEY=\"your-anthropic-api-key\"\n```\n\n## Step 3: Create Your First Agent\n\nCreate a Python script with a basic CrewAI agent:\n\n```python\nimport os\nfrom crewai import Agent, Task, Crew\nfrom crewai_tools import StagehandTool\nfrom stagehand.schemas import AvailableModel\n\n# Get API keys from environment\nbrowserbase_api_key = os.environ.get(\"BROWSERBASE_API_KEY\")\nbrowserbase_project_id = os.environ.get(\"BROWSERBASE_PROJECT_ID\")\nmodel_api_key = os.environ.get(\"OPENAI_API_KEY\")  # or ANTHROPIC_API_KEY\n\n# Initialize the StagehandTool\nstagehand_tool = StagehandTool(\n    api_key=browserbase_api_key,\n    project_id=browserbase_project_id,\n    model_api_key=model_api_key,\n    model_name=AvailableModel.GPT_4O,  # or AvailableModel.CLAUDE_3_7_SONNET_LATEST\n)\n\n# Create an agent with the tool\nresearcher = Agent(\n    role=\"Web Researcher\",\n    goal=\"Find and summarize information from websites\",\n    backstory=\"I'm an expert at finding information online.\",\n    verbose=True,\n    tools=[stagehand_tool],\n)\n```\n\n## Step 4: Create and Run a Task\n\nDefine a task for your agent and execute it:\n\n```python\n# Create a task that uses the tool\nresearch_task = Task(\n    description=\"Go to https://www.example.com and tell me what you see on the homepage.\",\n    agent=researcher,\n)\n\n# Run the crew\ncrew = Crew(\n    agents=[researcher],\n    tasks=[research_task],\n    verbose=True,\n)\n\ntry:\n    result = crew.kickoff()\n    print(result)\nfinally:\n    # Clean up resources\n    stagehand_tool.close()\n```\n\n## Step 5: Run Your Script\n\nExecute your Python script:\n\n```bash\npython your_crew_script.py\n```\n\n## Advanced Configuration\n\nCustomize the StagehandTool behavior with additional parameters:\n\n```python\nstagehand_tool = StagehandTool(\n    api_key=browserbase_api_key,\n    project_id=browserbase_project_id, \n    model_api_key=model_api_key,\n    model_name=AvailableModel.CLAUDE_3_7_SONNET_LATEST,\n    dom_settle_timeout_ms=5000,  # Wait longer for DOM to settle\n    headless=True,  # Run browser in headless mode\n    self_heal=True,  # Attempt to recover from errors\n    wait_for_captcha_solves=True,  # Wait for CAPTCHA solving\n    verbose=1,  # Control logging verbosity (0-3)\n)\n```\n\n## Example Tasks\n\n<Tabs>\n  <Tab title=\"Form Submission\" value=\"form-submission\" label=\"Python\">\n    ```python\n    form_task = Task(\n        description=\"\"\"\n        Submit a contact form:\n        1. Go to https://example.com/contact\n        2. Fill out the form with name 'John Doe', email 'john@example.com'\n        3. Submit and confirm success\n        \"\"\",\n        agent=researcher,\n    )\n    ```\n  </Tab>\n  <Tab title=\"Data Extraction\" value=\"data-extraction\" label=\"Python\">\n    ```python\n    extraction_task = Task(\n        description=\"\"\"\n        Extract product information:\n        1. Go to the products page\n        2. Extract all product names, prices, and descriptions\n        3. Format as structured data\n        \"\"\",\n        agent=researcher,\n    )\n    ```\n  </Tab>\n  <Tab title=\"Multi-step Navigation\" value=\"multi-step-navigation\" label=\"Python\">\n    ```python\n    navigation_task = Task(\n        description=\"\"\"\n        Navigate and analyze:\n        1. Start at homepage\n        2. Navigate to products section  \n        3. Filter by 'Electronics' category\n        4. Find and extract details of highest-rated product\n        \"\"\",\n        agent=researcher,\n    )\n    ```\n  </Tab>\n</Tabs>\n\n<CardGroup cols={2}>\n  <Card title=\"CrewAI Documentation\" icon=\"book\" href=\"https://docs.crewai.com/\">\n    Dive into the CrewAI documentation to learn more about its capabilities and integrations.\n  </Card>\n  <Card title=\"Browserbase Documentation\" icon=\"book\" href=\"https://docs.browserbase.com/\">\n    Access the Browserbase documentation for comprehensive guides and resources.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/integrations/crew-ai/introduction.mdx",
    "content": "---\ntitle: \"CrewAI Introduction\"\nsidebarTitle: Introduction\ndescription: \"Automate browser tasks using natural language instructions with CrewAI\"\n---\n\n## Overview\n\nThis guide shows you how to use CrewAI with Browserbase to create intelligent agents that can automate web interactions. By the end of this guide, you'll know how to:\n\n- Set up CrewAI with the StagehandTool\n- Create agents that can interact with websites\n- Automate browser tasks using natural language instructions\n- Extract structured data from web pages\n\n## When You'd Use This\n\nThe CrewAI integration is perfect for scenarios where you need intelligent web automation:\n\n- **Research automation**: Have agents research information across multiple websites\n- **Data collection**: Extract structured data from e-commerce sites, job boards, or news sites\n- **Form automation**: Automatically fill out and submit forms based on specific criteria\n- **Multi-step workflows**: Execute complex browser workflows that require decision-making\n\nThe StagehandTool wraps the Stagehand Python SDK to provide CrewAI agents with the ability to control a real web browser and interact with websites using three core primitives:\n\n1. **Act**: Perform actions like clicking, typing, or navigating\n2. **Extract**: Extract structured data from web pages\n3. **Observe**: Identify and analyze elements on the page\n\n<CardGroup cols={1}>\n<Card title=\"CrewAI Configuration\" icon=\"gear\" href=\"/integrations/crew-ai/configuration\">\n  Learn how to configure and use the StagehandTool with CrewAI agents for web automation tasks\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/integrations/langchain/configuration.mdx",
    "content": "---\ntitle: \"LangChain JS Configuration\"\nsidebarTitle: Configuration\ndescription: \"Set up Stagehand with LangChain JS to create intelligent web automation agents\"\n---\n\nThis guide walks you through integrating Stagehand with LangChain JS to build powerful web automation workflows using natural language instructions.\n\n## Step 1: Install Dependencies\n\nInstall the required packages for LangChain JS and Stagehand integration:\n\n```bash\nnpm install @langchain/langgraph @langchain/community @langchain/core @browserbasehq/stagehand\n```\n\n## Step 2: Configure Environment Variables\n\nFor remote browser automation, set up your Browserbase credentials:\n\n```bash\nBROWSERBASE_API_KEY=\"your-browserbase-api-key\"\nBROWSERBASE_PROJECT_ID=\"your-browserbase-project-id\"\n```\n\n## Step 3: Create a Stagehand Instance\n\nInitialize Stagehand with your preferred configuration:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// For local development\nconst stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 2,\n    enableCaching: false,\n});\n\n// For production with Browserbase\nconst stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    enableCaching: true,\n});\n```\n\n## Step 4: Generate the StagehandToolkit\n\nCreate the toolkit that provides LangChain-compatible tools:\n\n```typescript\nimport { StagehandToolkit } from '@langchain/community/agents/toolkits/stagehand';\n\nconst stagehandToolkit = await StagehandToolkit.fromStagehand(stagehand);\n```\n\n## Step 5: Use Individual Tools\n\nThe toolkit provides four specialized tools for web automation:\n\n### Available Tools\n\n- **stagehand_navigate**: Navigate to specific URLs\n- **stagehand_act**: Perform browser actions (clicking, typing, etc.)\n- **stagehand_extract**: Extract structured data using schemas  \n- **stagehand_observe**: Analyze page elements and possible actions\n\n### Basic Tool Usage\n\n```typescript\nimport { z } from \"zod\";\n\n// Navigate to a website\nconst navigateTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_navigate\"\n);\nawait navigateTool.invoke(\"https://www.google.com\");\n\n// Perform an action\nconst actionTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_act\"\n);\nawait actionTool.invoke('Search for \"OpenAI\"');\n\n// Observe the page\nconst observeTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_observe\"\n);\nconst result = await observeTool.invoke(\n    \"What actions can be performed on the current page?\"\n);\nconsole.log(JSON.parse(result));\n\n// Extract structured data\nconst extractTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_extract\"\n);\nconst extractResult = await extractTool.invoke({\n    instruction: \"Extract the main heading and description\",\n    schema: z.object({\n        heading: z.string(),\n        description: z.string(),\n    }),\n});\nconsole.log(extractResult);\n```\n\n## Step 6: Build LangGraph Agents\n\nIntegrate with LangGraph for complex automation workflows:\n\n```typescript\nimport { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n\n// Create an LLM\nconst llm = new ChatOpenAI({\n    model: \"gpt-4\",\n    temperature: 0,\n});\n\n// Create an agent with Stagehand tools\nconst agent = createReactAgent({\n    llm,\n    tools: stagehandToolkit.tools,\n});\n\n// Execute a complex workflow\nconst result = await agent.invoke({\n    messages: [\n        {\n            role: \"user\", \n            content: \"Go to example.com, find the contact form, and extract all the form fields\"\n        }\n    ]\n});\n```\n\n## Advanced Configuration\n\n### Custom Stagehand Configuration\n\n```typescript\nconst stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 2,\n    enableCaching: true,\n    headless: true,\n    domSettleTimeoutMs: 5000,\n});\n```\n\n### Error Handling\n\n```typescript\ntry {\n    const result = await agent.invoke({\n        messages: [{ role: \"user\", content: \"Navigate to invalid-url.com\" }]\n    });\n} catch (error) {\n    console.error(\"Automation failed:\", error);\n} finally {\n    // Clean up resources\n    await stagehand.close();\n}\n```\n\n## Example Workflows\n\n<Tabs>\n  <Tab title=\"Data Extraction\" value=\"data-extraction\" label=\"TypeScript\">\n    ```typescript\n    const extractionAgent = createReactAgent({\n        llm,\n        tools: stagehandToolkit.tools,\n    });\n\n    const result = await extractionAgent.invoke({\n        messages: [{\n            role: \"user\",\n            content: `\n                Go to news-website.com and extract:\n                1. All article headlines\n                2. Publication dates  \n                3. Author names\n                Format as structured JSON\n            `\n        }]\n    });\n    ```\n  </Tab>\n  <Tab title=\"Form Automation\" value=\"form-automation\" label=\"TypeScript\">\n    ```typescript\n    const formAgent = createReactAgent({\n        llm,\n        tools: stagehandToolkit.tools,\n    });\n\n    const result = await formAgent.invoke({\n        messages: [{\n            role: \"user\", \n            content: `\n                Navigate to contact-form.com and:\n                1. Fill out the contact form with:\n                   - Name: John Doe\n                   - Email: john@example.com\n                   - Message: Inquiry about services\n                2. Submit the form\n                3. Confirm submission success\n            `\n        }]\n    });\n    ```\n  </Tab>\n  <Tab title=\"Multi-site Research\" value=\"multi-site-research\" label=\"TypeScript\">\n    ```typescript\n    const researchAgent = createReactAgent({\n        llm,\n        tools: stagehandToolkit.tools,\n    });\n\n    const result = await researchAgent.invoke({\n        messages: [{\n            role: \"user\",\n            content: `\n                Research product pricing by:\n                1. Visit competitor1.com and extract pricing info\n                2. Visit competitor2.com and extract pricing info  \n                3. Compare features and prices\n                4. Provide summary analysis\n            `\n        }]\n    });\n    ```\n  </Tab>\n</Tabs>\n\n<CardGroup cols={1}>\n  <Card title=\"LangChain JS Documentation\" icon=\"book\" href=\"https://js.langchain.com/docs/integrations/tools/stagehand/\">\n    Official LangChain JS documentation for the Stagehand integration\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/integrations/langchain/introduction.mdx",
    "content": "---\ntitle: \"Langchain JS Introduction\"\nsidebarTitle: Introduction\ndescription: \"Integrate Stagehand with Langchain JS for intelligent web automation\"\n---\n\n## Overview\n\nThis guide shows you how to use Stagehand with Langchain JS to create intelligent agents that can automate web interactions. By the end of this guide, you'll know how to:\n\n- Set up the StagehandToolkit with Langchain JS\n- Create agents that can navigate and interact with websites\n- Extract structured data using natural language instructions\n- Build complex automation workflows with LangGraph\n\n## When You'd Use This\n\nThe Langchain JS integration is perfect for scenarios where you need intelligent web automation with advanced reasoning:\n\n- **AI-driven research**: Create agents that can research information across multiple websites and synthesize findings\n- **Dynamic form filling**: Automatically fill out complex forms based on contextual requirements\n- **Data extraction workflows**: Extract and transform data from multiple sources with intelligent navigation\n- **Multi-step web processes**: Execute complex browser workflows that require decision-making and adaptation\n\n<CardGroup cols={1}>\n<Card title=\"Langchain JS Configuration\" icon=\"gear\" href=\"/integrations/langchain/configuration\">\n  Learn how to set up and configure the StagehandToolkit with Langchain JS agents\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/integrations/mcp/configuration.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server Configuration\"\nsidebarTitle: \"Configuration\"\ndescription: \"Configure your browser automation with command-line flags, environment variables, and advanced options\"\n---\n\n## Configuration Overview\n\nThe Browserbase MCP server supports extensive configuration options through command-line flags and environment variables. Configure browser behavior, proxy settings, stealth modes, model selection, and more to customize your browser automation workflows.\n\n<Note>\nCommand-line flags are only available when running the server locally (`npx @browserbasehq/mcp-server-browserbase` with flags or local development setup).\n</Note>\n\n## Environment Variables\n\nConfigure the essential Browserbase credentials and optional debugging settings:\n\n<CardGroup cols={2}>\n<Card title=\"BROWSERBASE_API_KEY\" icon=\"key\">\nYour Browserbase API key for authentication\n</Card>\n\n<Card title=\"BROWSERBASE_PROJECT_ID\" icon=\"key\">\nYour Browserbase project ID\n</Card>\n\n</CardGroup>\n\n## Command-Line Flags\n\n### Available Flags\n\n| Flag | Description |\n|------|-------------|\n| `--proxies` | Enable Browserbase proxies for the session |\n| `--advancedStealth` | Enable Browserbase Advanced Stealth (Scale Plan only) |\n| `--keepAlive` | Enable Browserbase Keep Alive Session |\n| `--contextId <contextId>` | Specify a Browserbase Context ID to use |\n| `--persist [boolean]` | Whether to persist the Browserbase context (default: true) |\n| `--port <port>` | Port to listen on for HTTP/SHTTP transport |\n| `--host <host>` | Host to bind server to (default: localhost, use 0.0.0.0 for all interfaces) |\n| `--cookies [json]` | JSON array of cookies to inject into the browser |\n| `--browserWidth <width>` | Browser viewport width (default: 1024) |\n| `--browserHeight <height>` | Browser viewport height (default: 768) |\n| `--modelName <model>` | The model to use for Stagehand (default: google/gemini-2.5-flash-lite) |\n| `--modelApiKey <key>` | API key for the custom model provider (required when using custom models) |\n| `--experimental` | Enable experimental features (default: false) |\n\n## Configuration Examples\n\n### Basic Configuration\n\n<Tabs>\n<Tab title=\"Remote URL (SHTTP)\">\n\n\n<CodeGroup>\n```json Direct SHTTP\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"your-smithery-url.com\"\n    }\n  }\n}\n```\n</CodeGroup>\n\nWhen using our remote hosted server, we provide the LLM costs for Gemini, the [best performing model](https://www.stagehand.dev/evals) in [Stagehand](https://www.stagehand.dev).\n</Tab>\n\n<Tab title=\"NPM Package\">\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Local STDIO\">\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/mcp-server-browserbase/cli.js\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Local SHTTP\">\n```bash\n# Start server\nnode cli.js --port 8931\n```\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"http://localhost:8931/mcp\",\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n### Advanced Features\n\n<Tabs>\n<Tab title=\"Proxies\">\nEnable Browserbase proxies for IP rotation and geo-location testing.\n\n<Panel>\n[Learn more about Browserbase Proxies](https://docs.browserbase.com/features/proxies)\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\", \"--proxies\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Advanced Stealth\">\nEnable advanced anti-detection features for enhanced stealth browsing.\n\n<Panel>\n[Learn more about Advanced Stealth](https://docs.browserbase.com/features/stealth-mode#advanced-stealth-mode)\n\n**Note:** Advanced Stealth is only available for Scale Plan users.\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\", \"--advancedStealth\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Contexts\">\nUse persistent browser contexts to maintain authentication and state across sessions.\n\n<Panel>\n[Learn more about Browserbase Contexts](https://docs.browserbase.com/features/contexts)\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\", \"--contextId\", \"your_context_id\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n### Browser Customization\n\n<Tabs>\n<Tab title=\"Viewport Sizing\">\nCustomize browser window dimensions. Default is 1024x768. Recommended aspect ratios: 16:9.\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--browserWidth\", \"1920\",\n        \"--browserHeight\", \"1080\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n\n**Common Resolutions:**\n- Desktop: 1920x1080, 1280x720, 1024x768\n- Mobile: 375x667 (iPhone), 360x640 (Android)\n- Tablet: 768x1024 (iPad)\n</Tab>\n\n<Tab title=\"Cookie Injection\">\nInject session cookies for authentication. Useful when persistent contexts don't handle session cookies.\n\n<Panel>\nCookies must be in [Playwright Cookie format](https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies).\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--cookies\",\n        \"[{\\\"name\\\": \\\"session\\\", \\\"value\\\": \\\"abc123\\\", \\\"domain\\\": \\\".example.com\\\", \\\"path\\\": \\\"/\\\", \\\"httpOnly\\\": true, \\\"secure\\\": true}]\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n## Model Configuration\n\nConfigure AI models for enhanced browser automation. Stagehand defaults to Google's Gemini 2.5 Flash Lite but supports multiple providers.\n\n<Warning>\nWhen using any custom model (non-default), you must provide your own API key for that model provider using the `--modelApiKey` flag.\n</Warning>\n\n<Tabs>\n<Tab title=\"Available Models\">\n**Google Gemini** (Default)\n- `google/gemini-2.5-flash-lite` (default)\n- `google/gemini-1.5-pro`\n- `google/gemini-1.5-flash`\n\n**OpenAI**\n- `openai/gpt-4o`\n- `openai/gpt-4o-mini`\n- `openai/o1-mini`\n- `openai/o1-preview`\n- `openai/o3-mini`\n\n**Anthropic Claude**\n- `anthropic/claude-sonnet-4-6`\n- `anthropic/claude-sonnet-4-5-20250929`\n\n[View full list of supported models](https://docs.stagehand.dev/examples/custom_llms#supported-llms)\n</Tab>\n\n<Tab title=\"Configuration Examples\">\n<CodeGroup>\n```json OpenAI GPT-4o\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--modelName\", \"openai/gpt-4o\",\n        \"--modelApiKey\", \"your_openai_api_key\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\"\n      }\n    }\n  }\n}\n```\n\n```json Claude Sonnet\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--modelName\", \"anthropic/claude-sonnet-4-6\",\n        \"--modelApiKey\", \"your_anthropic_api_key\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\"\n      }\n    }\n  }\n}\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n## Development Configuration\n\n<Tabs>\n<Tab title=\"Debug Mode\">\nEnable detailed logging for troubleshooting and development.\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\",\n        \"DEBUG\": \"true\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Custom Host/Port\">\nConfigure custom host and port for SHTTP transport.\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--host\", \"0.0.0.0\",\n        \"--port\", \"8080\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n## Best Practices\n\n<Accordion title=\"Performance - How can I optimize browser automation performance?\">\n- Use appropriate viewport sizes for your use case\n- Enable proxies only when needed for geo-location\n- Choose efficient models (Gemini Flash for speed, GPT-4o for accuracy)\n- Reuse contexts for authentication persistence\n</Accordion>\n\n<Accordion title=\"Security - What security measures should I implement?\">\n- Store API keys securely in environment variables\n- Use Advanced Stealth for sensitive operations\n- Implement proper session management\n- Rotate cookies and contexts regularly\n</Accordion>\n\n<Accordion title=\"Development - What are the recommended development practices?\">\n- Enable debug mode during development\n- Use context persistence for faster iteration\n- Test with different viewport sizes\n- Monitor session usage and quotas\n</Accordion>\n\n<Accordion title=\"Production - How should I configure for production environments?\">\n- Use NPM installation for reliability\n- Configure appropriate timeouts\n- Implement error handling and retries\n- Monitor performance and resource usage\n</Accordion>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Browserbase Documentation\" icon=\"globe\" href=\"https://docs.browserbase.com\">\nComplete platform documentation\n</Card>\n\n<Card title=\"Stagehand Docs\" icon=\"robot\" href=\"https://docs.stagehand.dev/\">\nAI-powered browser automation\n</Card>\n\n<Card title=\"Support\" icon=\"headset\" href=\"mailto:support@browserbase.com\">\nGet help from our team\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/integrations/mcp/introduction.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server\"\nsidebarTitle: \"Introduction\"\ndescription: \"AI-powered browser automation through Model Context Protocol integration with Stagehand\"\n---\n\n## Overview\n\nThe Browserbase MCP Server brings powerful browser automation capabilities to MCP clients through the Model Context Protocol (MCP). Built on top of [Stagehand](https://docs.stagehand.dev/), this integration provides AI-powered web automation using natural language commands.\n\n<Info>\n  The hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)\n  endpoint is served on Browserbase infrastructure.\n  You can also run the MCP server locally with STDIO, but we recommend the\n  hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)\n  endpoint for most users.\n</Info>\n\n## Key Features\n\n<CardGroup cols={2}>\n<Card title=\"Natural Language Automation\" icon=\"wand-magic-sparkles\">\nControl browsers using plain English commands like \"click the login button\" or \"fill out the contact form\"\n</Card>\n\n<Card title=\"Web Interaction\" icon=\"browser\">\n  Navigate, click, and fill forms with ease\n</Card>\n\n<Card title=\"Data Extraction\" icon=\"download\">\n  Extract structured data from any website automatically\n</Card>\n\n<Card title=\"Session Lifecycle\" icon=\"route\">\n  Create, reuse, and close browser sessions with explicit MCP tools\n</Card>\n\n</CardGroup>\n\n## Core Benefits\n\n<Tabs>\n<Tab title=\"Ease of Use\">\n<CardGroup cols={2}>\n<Card title=\"Intuitive Commands\" icon=\"wand-magic-sparkles\">\nNo need to learn complex selectors or automation syntax. Simply describe what you want to do in natural language.\n</Card>\n\n<Card title=\"Quick Setup\" icon=\"rocket\">\n  Get started in minutes with either hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) or local STDIO.\n</Card>\n\n<Card title=\"Smart Automation\" icon=\"brain\">\nStagehand's AI understands web page context and can adapt to different layouts and designs.\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Powerful Capabilities\">\n<CardGroup cols={2}>\n<Card title=\"Full Browser Control\" icon=\"browser\">\nNavigate, click, type, scroll, and interact with any web element.\n</Card>\n\n<Card title=\"Data Intelligence\" icon=\"chart-line\">\n  Extract structured information from complex web pages automatically.\n</Card>\n\n<Card title=\"Session Persistence\" icon=\"cookie-bite\">\nMaintain authentication states and cookies across multiple interactions.\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Enterprise Ready\">\n<CardGroup cols={2}>\n<Card title=\"Reliable Infrastructure\" icon=\"server\">\nHosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) runs on Browserbase infrastructure for consistent performance.\n</Card>\n\n<Card title=\"Scalable Architecture\" icon=\"arrows-up-to-line\">\n  Handle multiple concurrent sessions and high-volume automation tasks.\n</Card>\n\n<Card title=\"Security Features\" icon=\"shield-check\">\n  Stealth mode, proxy support, and advanced anti-detection capabilities.\n</Card>\n\n<Card title=\"Comprehensive Logging\" icon=\"file-lines\">\nDetailed session recordings and debugging information.\n</Card>\n</CardGroup>\n</Tab>\n</Tabs>\n\n## Use Cases\n\n<Tabs>\n<Tab title=\"Web Scraping & Data Collection\">\n<CardGroup cols={2}>\n<Card title=\"E-commerce Monitoring\" icon=\"store\">\nTrack product prices, availability, and competitor information\n</Card>\n\n<Card title=\"Market Research\" icon=\"chart-bar\">\n  Gather data from multiple sources for analysis and reporting\n</Card>\n\n<Card title=\"Content Aggregation\" icon=\"newspaper\">\n  Collect articles, posts, and media from various websites\n</Card>\n\n<Card title=\"Lead Generation\" icon=\"users\">\nExtract contact information and business data from directories\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Testing\">\n<CardGroup cols={2}>\n<Card title=\"Automated Testing\" icon=\"flask\">\nCreate comprehensive test suites for web applications\n</Card>\n\n<Card title=\"Cross-Browser Validation\" icon=\"browsers\">\n  Test functionality across different browser environments\n</Card>\n\n<Card title=\"User Journey Testing\" icon=\"route\">\n  Simulate real user interactions and workflows\n</Card>\n\n<Card title=\"Performance Monitoring\" icon=\"gauge\">\nTrack page load times and user experience metrics\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Workflow Automation\">\n<CardGroup cols={2}>\n<Card title=\"Form Automation\" icon=\"file-contract\">\nAutomatically fill and submit complex web forms\n</Card>\n\n<Card title=\"Report Generation\" icon=\"chart-line\">\n  Extract data and generate automated reports\n</Card>\n\n<Card title=\"Social Media Management\" icon=\"share-nodes\">\n  Schedule posts and monitor engagement across platforms\n</Card>\n\n<Card title=\"Administrative Tasks\" icon=\"clipboard-check\">\nAutomate repetitive web-based business processes\n</Card>\n</CardGroup>\n</Tab>\n</Tabs>\n\n## Getting Started\n\n<Steps>\n<Step title=\"Install the MCP Server\">\nChoose hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) (recommended) or local STDIO based on your needs.\n</Step>\n\n<Step title=\"Configure Authentication\">\n  Set up your Browserbase API credentials in MCP configuration. Get API keys\n  from the [Browserbase Dashboard](https://www.browserbase.com/overview).\n</Step>\n\n<Step title=\"Start Automating\">\nBegin using natural language commands to control browsers through your MCP client.\n</Step>\n</Steps>\n\n<Tip>\n  Ready to get started? Check out the [Setup Guide](/v2/integrations/mcp/setup).\n</Tip>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Setup Guide\" icon=\"rocket\" href=\"/v2/integrations/mcp/setup\">\nGet started with installation and configuration\n</Card>\n\n<Card title=\"MCP Docs\" icon=\"book\" href=\"https://modelcontextprotocol.io/introduction\">\nLearn more about the MCP protocol\n</Card>\n\n<Card title=\"Browserbase Docs\" icon=\"globe\" href=\"https://docs.browserbase.com\">\nExplore Browserbase features and capabilities\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/integrations/mcp/setup.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server Setup\"\nsidebarTitle: \"Setup\"\ndescription: \"Add the Browserbase MCP Server to your MCP client\"\n---\n\n## Quick Installation\n\n<Card title=\"Install with Cursor\" icon=\"arrow-pointer\" href=\"cursor://anysphere.cursor-deeplink/mcp/install?name=browserbase&config=eyJ1cmwiOiJodHRwczovL21jcC5icm93c2VyYmFzZS5jb20vbWNwP2Jyb3dzZXJiYXNlQXBpS2V5PVlPVVJfQlJPV1NFUkJBU0VfQVBJX0tFWSJ9\">\n  One-click installation directly in Cursor\n</Card>\n\nYou can also add Browserbase MCP to Claude Code with a single command:\n\n```bash\nclaude mcp add --transport http browserbase \"https://mcp.browserbase.com/mcp?browserbaseApiKey=YOUR_BROWSERBASE_API_KEY\"\n```\n\nWe support both local STDIO and hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) (SHTTP). We recommend hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) for most users.\n\n## Endpoint\n\nHosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) endpoint (served on Browserbase infrastructure):\n\n```text\nhttps://mcp.browserbase.com/mcp\n```\n\n## Prerequisites\n\n<Steps>\n<Step title=\"Get your Browserbase credentials\">\nGet your Browserbase API key from the [Browserbase Dashboard](https://www.browserbase.com/overview).\n\n<Frame>\n<img src=\"/images/quickstart/api-key.png\" alt=\"Browserbase API Key settings\" />\n</Frame>\n\nThen copy your API Key directly from the input.\n</Step>\n</Steps>\n\n## Query Parameters (Hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http))\n\n### Required for tool calls\n\n<CardGroup cols={1}>\n<Card title=\"browserbaseApiKey\" icon=\"key\">\nBrowserbase API key.\n</Card>\n</CardGroup>\n\n### Optional\n\n| Query Param       | Type           | Behavior                                   |\n| ----------------- | -------------- | ------------------------------------------ |\n| `modelName`       | string         | Defaults to `google/gemini-2.5-flash-lite` |\n| `modelApiKey`     | string         | Required when `modelName` is non-default   |\n| `keepAlive`       | boolean string | `\"true\"` or `\"false\"`                      |\n| `proxies`         | boolean string | `\"true\"` or `\"false\"`                      |\n| `advancedStealth` | boolean string | `\"true\"` or `\"false\"`                      |\n\n<Warning>\n  Boolean query values must be exact strings: `\"true\"` or `\"false\"`.\n</Warning>\n\n## Available Tools\n\n<Accordion title=\"navigate\">\nNavigate to any URL in the browser\n\n<ParamField path=\"url\" type=\"string\" required>\n  The URL to navigate to\n</ParamField>\n</Accordion>\n\n<Accordion title=\"act\">\nPerform an action on the web page using natural language\n\n<ParamField path=\"action\" type=\"string\" required>\n  The action to perform (e.g., \"click the login button\", \"fill form field\")\n</ParamField>\n</Accordion>\n\n<Accordion title=\"observe\">\nObserve and find actionable elements on the page.\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  Specific instruction for observation (e.g., \"find the login button\", \"locate search form\")\n</ParamField>\n</Accordion>\n\n<Accordion title=\"extract\">\nExtract data from the current page.\n\n<ParamField path=\"instruction\" type=\"string\">\nOptional extraction instruction.\n</ParamField>\n</Accordion>\n\n<Accordion title=\"start\">\nCreate or reuse a Browserbase session and set it as active for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n\n<ResponseField name=\"sessionId\" type=\"string\">\nBrowserbase session ID.\n</ResponseField>\n</Accordion>\n\n<Accordion title=\"end\">\nClose the active Browserbase session for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n</Accordion>\n\n## Local Command-Line Flags\n\n<Note>\nCommand-line flags are only available when running the server locally (`npx @browserbasehq/mcp-server-browserbase` with flags or local development setup).\n</Note>\n\n| Flag | Description |\n|------|-------------|\n| `--proxies` | Enable Browserbase proxies for the session |\n| `--advancedStealth` | Enable Browserbase Advanced Stealth (Scale Plan only) |\n| `--keepAlive` | Enable Browserbase Keep Alive Session |\n| `--contextId <contextId>` | Specify a Browserbase Context ID to use |\n| `--persist [boolean]` | Whether to persist the Browserbase context (default: true) |\n| `--port <port>` | Port to listen on for HTTP or [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport |\n| `--host <host>` | Host to bind server to (default: localhost, use 0.0.0.0 for all interfaces) |\n| `--browserWidth <width>` | Browser viewport width (default: 1024) |\n| `--browserHeight <height>` | Browser viewport height (default: 768) |\n| `--modelName <model>` | The model to use for Stagehand (default: google/gemini-2.5-flash-lite) |\n| `--modelApiKey <key>` | API key for the custom model provider (required when using custom models) |\n| `--experimental` | Enable experimental features (default: false) |\n\n## Installation Methods\n\n<Tabs>\n<Tab title=\"Hosted (recommended)\">\n\nUse your MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"https://mcp.browserbase.com/mcp?browserbaseApiKey=YOUR_BROWSERBASE_API_KEY\"\n    }\n  }\n}\n```\n\nFor custom models, include `modelName` and `modelApiKey`:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"https://mcp.browserbase.com/mcp?browserbaseApiKey=YOUR_BROWSERBASE_API_KEY&modelName=openai/gpt-4.1&modelApiKey=YOUR_MODEL_API_KEY\"\n    }\n  }\n}\n```\n\n</Tab>\n\n<Tab title=\"NPM Package (STDIO)\">\nThe easiest way to get started locally is using our NPM package.\n\n<Note>\nIf you would like to use a different model, you have to pass the model name and keys in the args. More info in the [Local Command-Line Flags](#local-command-line-flags) section.\n</Note>\n\n<Steps>\n<Step title=\"Add to MCP Config\">\nGo into your MCP Config JSON and add the Browserbase Server:\n\n<CodeGroup>\n```json Claude Desktop\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</CodeGroup>\n</Step>\n\n<Step title=\"Restart your MCP client\">\n<Check>\nThat's it! Reload your MCP client and you will be able to use Browserbase.\n</Check>\n</Step>\n</Steps>\n\n</Tab>\n\n<Tab title=\"Local Development\">\nFor local development or customization, you can run the server locally.\n\n<Steps>\n<Step title=\"Clone and build\">\n```bash\n# Clone the Repo\ngit clone https://github.com/browserbase/mcp-server-browserbase.git\ncd mcp-server-browserbase\n\n# Install the dependencies and build the project\nnpm install && npm run build\n```\n</Step>\n\n<Step title=\"Choose your transport method\">\nYou can run locally using either STDIO or [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).\n\n<Tabs>\n<Tab title=\"STDIO\">\nAdd the following to your MCP Config JSON file:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/mcp-server-browserbase/cli.js\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Self-hosted Streamable HTTP\">\nFirst, run the server:\n\n```bash\nnode cli.js --port 8931\n```\n\nThen add this to your MCP Config JSON file:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"http://localhost:8931/mcp\",\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n</Step>\n\n<Step title=\"Restart your client\">\n<Check>\nReload your MCP client and you should be good to go!\n</Check>\n</Step>\n</Steps>\n</Tab>\n</Tabs>\n\n## Verify Installation\n\n<Steps>\n<Step title=\"Restart your MCP client\">\nRestart/refresh your MCP client app and verify tools are available.\n</Step>\n\n<Step title=\"Test the integration\">\nGet started using our MCP Server by asking your MCP client to navigate to any page and see your Browserbase Browser in action on the [dashboard](https://www.browserbase.com/sessions).\n\n<Tip>\nTry: \"Navigate to example.com and extract the main heading\"\n</Tip>\n</Step>\n</Steps>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Model Context Protocol (MCP) Docs\" icon=\"book\" href=\"https://modelcontextprotocol.io/introduction\">\nLearn more about the MCP protocol\n</Card>\n\n<Card title=\"Browserbase Documentation\" icon=\"globe\" href=\"https://docs.browserbase.com\">\nExplore Browserbase features and capabilities\n</Card>\n\n<Card title=\"Support\" icon=\"headset\" href=\"mailto:support@browserbase.com\">\nGet help from our support team\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/integrations/mcp/tools.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server Tools\"\nsidebarTitle: \"Tools\"\ndescription: \"This guide covers the specialized tools available in the Browserbase MCP server for browser automation and interaction.\"\n---\n\n## Overview\n\nThe Browserbase MCP server provides tools for browser automation and session management through a transport-scoped active session.\n\n## Core Browser Automation Tools\n\nThese are the primary tools for modern web automation using natural language commands.\n\n<Accordion title=\"navigate\">\nNavigate to any URL in the browser\n\n<ParamField path=\"url\" type=\"string\" required>\n  The URL to navigate to\n</ParamField>\n</Accordion>\n\n<Accordion title=\"act\">\nPerform an action on the web page using natural language\n\n<ParamField path=\"action\" type=\"string\" required>\n  The action to perform (e.g., \"click the login button\", \"fill form field\")\n</ParamField>\n\n</Accordion>\n\n<Accordion title=\"observe\">\nObserve and find actionable elements on the page\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  Specific instruction for observation (e.g., \"find the login button\", \"locate search form\")\n</ParamField>\n</Accordion>\n\n<Accordion title=\"extract\">\nExtract data from the current page.\n\n<ParamField path=\"instruction\" type=\"string\">\n  Optional extraction instruction.\n</ParamField>\n</Accordion>\n\n## Session Management\n\n<Accordion title=\"start\">\nCreate or reuse a Browserbase session and set it as active for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n\n<ResponseField name=\"sessionId\" type=\"string\">\n  Browserbase session ID.\n</ResponseField>\n</Accordion>\n\n<Accordion title=\"end\">\nClose the active Browserbase session for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n\n</Accordion>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Model Context Protocol (MCP) Docs\" icon=\"book\" href=\"https://modelcontextprotocol.io/introduction\">\nLearn more about the MCP protocol\n</Card>\n\n<Card title=\"Stagehand Documentation\" icon=\"robot\" href=\"https://docs.stagehand.dev/\">\nExplore Stagehand's AI-powered browser automation\n</Card>\n\n<Card title=\"Support\" icon=\"headset\" href=\"mailto:support@browserbase.com\">\nGet help from our support team\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v2/integrations/vercel/configuration.mdx",
    "content": "---\ntitle: Use Stagehand in Next.js\nsidebarTitle: Configuration\ndescription: Next.js is a popular framework for developing web-based applications in production. It powers Stagehand apps like [Director](https://director.ai), [Brainrot](https://brainrot.run) and [Open Operator](https://operator.browserbase.com).\n---\n\n<Card\n  title=\"Check out the Stagehand Next.js Quickstart\"\n  icon=\"github\"\n  href=\"https://github.com/browserbase/stagehand-nextjs-quickstart\"\n>\n  Clone our [GitHub repo](https://github.com/browserbase/stagehand-nextjs-quickstart) to get started with Stagehand in Next.js.\n</Card>\n\n## Add Stagehand to an existing Next.js project\nIf you'd like to add Stagehand to an existing Next.js project, you can do so by installing the dependencies:\n<Tabs>\n\t<Tab title=\"npm\">\n\t```bash\n\tnpm install @browserbasehq/stagehand @browserbasehq/sdk playwright zod\n\t```\n\t</Tab>\n\n\t<Tab title=\"pnpm\">\n\t```bash\n\tpnpm add @browserbasehq/stagehand @browserbasehq/sdk playwright zod\n\t```\n\t</Tab>\n\n\t<Tab title=\"yarn\">\n\t```bash\n\tyarn add @browserbasehq/stagehand @browserbasehq/sdk playwright zod\n\t```\n\t</Tab>\n</Tabs>\n\n### Write a server action\nNext, let's define our `main` function as a server action in `app/stagehand/main.ts`. This file will have the following three functions:\n\n1. **`main`: Run the main Stagehand script**\n2. **`runStagehand`: Initialize and run the `main` function**\n3. **`startBBSSession`: Start a Browserbase session**\n\n```ts app/stagehand/main.ts\n// 🤘 Welcome to Stagehand!\n// This file is from the [Stagehand docs](https://docs.stagehand.dev/sections/examples/nextjs).\n\n\"use server\";\n\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod/v3\";\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\n/**\n * Run the main Stagehand script\n */\nasync function main(stagehand: Stagehand) {\n  // You can use the `page` instance to write any Playwright code\n  // For more info: https://playwright.dev/docs/pom\n  const page = stagehand.page;\n\n  // In this example, we'll get the title of the Stagehand quickstart page\n  await page.goto(\"https://docs.stagehand.dev/\");\n  await page.act(\"click the quickstart link\");\n  const { title } = await page.extract({\n    instruction: \"extract the main heading of the page\",\n    schema: z.object({\n      title: z.string(),\n    }),\n  });\n\n  return title;\n}\n\n/**\n * Initialize and run the main() function\n */\nexport async function runStagehand(sessionId?: string) {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    apiKey: process.env.BROWSERBASE_API_KEY,\n    projectId: process.env.BROWSERBASE_PROJECT_ID,\n    verbose: 1,\n    logger: console.log,\n    browserbaseSessionID: sessionId,\n    disablePino: true,\n  });\n  await stagehand.init();\n  await main(stagehand);\n  await stagehand.close();\n}\n\n/**\n * Start a Browserbase session\n */\nexport async function startBBSSession() {\n  const browserbase = new Browserbase();\n  const session = await browserbase.sessions.create({\n    projectId: process.env.BROWSERBASE_PROJECT_ID!,\n  });\n  const debugUrl = await browserbase.sessions.debug(session.id);\n  return {\n    sessionId: session.id,\n    debugUrl: debugUrl.debuggerFullscreenUrl,\n  };\n}\n```\n\n### Create a client component\nNext, let's create a client component that will start a Browserbase session and run the `main` function with the server actions we just defined. We'll first create a Browserbase session and embed the session in an iframe before running the `main` function.\n\n```tsx app/components/stagehandEmbed.tsx\n\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { runStagehand, startBBSSession } from \"@/app/stagehand/main\";\n\nexport function StagehandEmbed() {\n  const [sessionId, setSessionId] = useState<string | null>(null);\n  const [debugUrl, setDebugUrl] = useState<string | null>(null);\n\n  const startSession = useCallback(async () => {\n    const { sessionId, debugUrl } = await startBBSSession();\n    setSessionId(sessionId);\n    setDebugUrl(debugUrl);\n    await runStagehand(sessionId);\n  }, []);\n\n  return (\n    <div>\n      {!sessionId && <button onClick={startSession}>Start Session</button>}\n      {sessionId && debugUrl && (\n        <iframe src={debugUrl} className=\"w-full h-full\" />\n      )}\n    </div>\n  );\n}\n```\n\n### Use the `StagehandEmbed` component\nNow, we can use the `StagehandEmbed` component in our app.\n\n```tsx app/page.tsx\nimport { StagehandEmbed } from \"@/app/components/stagehandEmbed\";\n\nexport default function Home() {\n\treturn (\n\t\t<main>\n\t\t\t<StagehandEmbed />\n\t\t</main>\n\t)\n}\n```\n\n## References\n\n<CardGroup cols={2}>\n  <Card title=\"Deploy Template\" icon=\"rocket\" href=\"https://vercel.com/templates/ai/stagehand-next-js-quickstart\">\n    One‑click deploy the Stagehand Next.js template on Vercel\n  </Card>\n  \n  <Card title=\"Source Code\" icon=\"github\" href=\"https://github.com/browserbase/stagehand-nextjs-quickstart\">\n    Browse the complete template repository on GitHub\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v2/integrations/vercel/introduction.mdx",
    "content": "---\ntitle: \"Next.js + Vercel\"\nsidebarTitle: \"Introduction\"\ndescription: \"Build and deploy a Stagehand‑powered Next.js app to Vercel\"\n---\n\n## Overview\n\nThe Stagehand + Next.js Quickstart is a production‑ready template that pairs Stagehand's AI browser automation with a modern Next.js app, deployable in one click on Vercel.\n\n<CardGroup cols={3}>\n  <Card title=\"Deploy Template\" icon=\"rocket\" href=\"https://vercel.com/templates/ai/stagehand-next-js-quickstart\">\n    One‑click deploy to Vercel with environment setup\n  </Card>\n\n  <Card title=\"Live Demo\" icon=\"globe\" href=\"https://stagehand-nextjs-quickstart.vercel.app\">\n    See the deployed template in action\n  </Card>\n\n  <Card title=\"Source Code\" icon=\"github\" href=\"https://github.com/browserbase/stagehand-nextjs-quickstart\">\n    Browse the repository on GitHub\n  </Card>\n</CardGroup>\n\n## What you get\n\n<CardGroup cols={2}>\n  <Card title=\"App Router project\" icon=\"browser\">\n    Next.js App Router scaffold with Tailwind styling\n  </Card>\n  <Card title=\"Server‑safe automation\" icon=\"shield-check\">\n    Uses Browserbase for cloud browsers (works on Vercel functions)\n  </Card>\n  <Card title=\"Prewired config\" icon=\"gear\">\n    `stagehand.config.ts` with model + provider switching\n  </Card>\n  <Card title=\"Automation ready\" icon=\"robot\">\n    Example usage of Stagehand primitives\n  </Card>\n</CardGroup>\n\n## Requirements\n\n- **Node 18+** locally\n- **Model key**: OpenAI or Anthropic (or plug a custom client)\n- **Browserbase keys**: `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` for cloud browsers\n\n<Tip>\nLocal Playwright browsers are not available on Vercel. Set Stagehand to Browserbase when deploying.\n</Tip>\n\n## Links\n\n<CardGroup cols={2}>\n  <Card title=\"Quickstart\" icon=\"rocket\" href=\"/integrations/vercel/quickstart\">\n    Run locally and deploy to Vercel in minutes\n  </Card>\n  <Card title=\"TypeScript Quickstart\" icon=\"code\" href=\"/first-steps/quickstart\">\n    More templates, including Vercel deployment examples\n  </Card>\n</CardGroup>\n\n"
  },
  {
    "path": "packages/docs/v2/references/act.mdx",
    "content": "---\ntitle: act()\ndescription: 'Complete API reference for the act() method'\nicon: 'arrow-pointer'\n---\n\n<CardGroup cols={1}>\n<Card title=\"Act\" icon=\"arrow-pointer\" href=\"/v2/basics/act\">\n  See how to use act() to perform browser actions\n</Card>\n</CardGroup>\n\n### Method Signatures\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// String instruction\nawait page.act(instruction: string): Promise<ActResult>\n\n// With ActOptions\nawait page.act(options: ActOptions): Promise<ActResult>\n\n// Execute observed action\nawait page.act(observeResult: ObserveResult): Promise<ActResult>\n```\n\n**ActOptions Interface:**\n```typescript\ninterface ActOptions {\n  action: string;\n  modelName?: AvailableModel;\n  modelClientOptions?: ClientOptions;\n  variables?: Record<string, string>;\n  domSettleTimeoutMs?: number;\n  timeoutMs?: number;\n  iframes?: boolean;\n}\n```\n\n</Tab>\n<Tab title=\"Python\">\n\n```python\n# String instruction\nawait page.act(instruction: str) -> ActResult\n\n# With parameters\nawait page.act(\n    instruction: str,\n    variables: Dict[str, str] = None,\n    dom_settle_timeout_ms: int = None,\n    timeout_ms: int = None,\n    model_name: AvailableModel = None,\n    model_client_options: Dict = None,\n    iframes: bool = None,\n) -> ActResult\n\n# Execute observed action\nawait page.act(observe_result: ObserveResult) -> ActResult\n```\n\n</Tab>\n</Tabs>\n\n### Parameters\n\n<ParamField path=\"action\" type=\"string\" required>\n  Natural language description of the action to perform.\n</ParamField>\n\n<ParamField path=\"variables\" type=\"Record<string, string>\" optional>\n  Key-value pairs for variable substitution using `%variable%` syntax. Prevents sensitive data from appearing in logs.\n</ParamField>\n\n<ParamField path=\"modelName\" type=\"AvailableModel\" optional>\n  Override the default LLM model for this action.\n</ParamField>\n\n<ParamField path=\"modelClientOptions\" type=\"ClientOptions\" optional>\n  Model-specific configuration options.\n  \n  **Options:** `temperature`, `maxTokens`, `apiKey`\n</ParamField>\n\n<ParamField path=\"domSettleTimeoutMs\" type=\"number\" optional>\n  Maximum time to wait for DOM to stabilize before attempting action.\n  \n  **Default:** `30000`\n</ParamField>\n\n<ParamField path=\"timeoutMs\" type=\"number\" optional>\n  Maximum time to wait for the action to complete.\n</ParamField>\n\n<ParamField path=\"iframes\" type=\"boolean\" optional>\n  Set to `true` if target element is within an iframe.\n  \n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"observeResult\" type=\"ObserveResult\" optional>\n  Previously observed action to execute directly (enables self-healing).\n</ParamField>\n\n### Returns `Promise<ActResult>`\n\n<ParamField path=\"success\" type=\"boolean\" required>\n  Whether the action was completed successfully.\n</ParamField>\n\n<ParamField path=\"message\" type=\"string\" required>\n  Details about the action's execution.\n</ParamField>\n\n<ParamField path=\"action\" type=\"string\" required>\n  The action that was performed.\n</ParamField>\n\n```Example Response\n{\n  success: true,\n  message: 'Action [scrollTo] performed successfully on selector: /html[1]',\n  action: 'Scrollable area of the page where user can navigate to the pricing section or other parts of the page'\n}\n```\n\n### Error Types\n\n- **TimeoutError** - Action exceeded timeout limits\n- **ElementNotFoundError** - Target element could not be located  \n- **ActionFailedError** - Action could not be completed\n- **StagehandError** - General Stagehand-specific errors"
  },
  {
    "path": "packages/docs/v2/references/agent.mdx",
    "content": "---\ntitle: agent()\ndescription: 'Complete API reference for the agent() method'\nicon: 'robot'\n---\n\n<CardGroup cols={1}>\n<Card title=\"Agent\" icon=\"robot\" href=\"/v2/basics/agent\">\n  See how to use agent() to create autonomous AI agents for multi-step browser workflows\n</Card>\n</CardGroup>\n\n### Agent Creation\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// Create agent instance\nconst agent = stagehand.agent(config: AgentConfig): AgentInstance\n```\n\n**AgentConfig Interface:**\n```typescript\ninterface AgentConfig {\n  provider?: AgentProviderType;  // \"openai\" | \"anthropic\"\n  model?: string;\n  instructions?: string;\n  options?: Record<string, unknown>;\n  integrations?: (Client | string)[];\n  tools?: ToolSet;\n}\n```\n\n**AgentInstance Interface:**\n```typescript\ninterface AgentInstance {\n  execute: (instructionOrOptions: string | AgentExecuteOptions) => Promise<AgentResult>;\n}\n```\n\n</Tab>\n<Tab title=\"Python\">\n\n```python\n# Create agent instance\nagent = stagehand.agent(\n    model: str,\n    instructions: str = None,\n    options: Dict[str, Any] = None\n)\n```\n\n</Tab>\n</Tabs>\n\n### Agent Configuration\n\n<ParamField path=\"provider\" type=\"AgentProviderType\" optional>\n  AI provider for agent functionality.\n  \n  **Options:** `\"anthropic\"`, `\"openai\"`\n</ParamField>\n\n<ParamField path=\"model\" type=\"string\" optional>\n  Specific model for agent execution.\n  \n  **Anthropic:** `\"claude-sonnet-4-6\"`, `\"claude-sonnet-4-5-20250929\"`\n  \n  **OpenAI:** `\"computer-use-preview\"`, `\"gpt-4o\"`\n</ParamField>\n\n<ParamField path=\"instructions\" type=\"string\" optional>\n  System instructions defining agent behavior.\n</ParamField>\n\n<ParamField path=\"options\" type=\"Record<string, unknown>\" optional>\n  Provider-specific configuration.\n  \n  **Common:** `apiKey`, `baseURL`, `organization`\n</ParamField>\n\n<ParamField path=\"integrations\" type=\"(Client | string)[]\" optional>\n  MCP (Model Context Protocol) integrations for external tools and services.\n  \n  **Array of:** MCP server URLs (strings) or connected Client objects\n</ParamField>\n\n<ParamField path=\"tools\" type=\"ToolSet\" optional>\n  Custom tool definitions to extend agent capabilities.\n  \n  **Format:** Object with tool name keys and tool definition values\n</ParamField>\n\n### Execute Method\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// String instruction\nawait agent.execute(instruction: string): Promise<AgentResult>\n\n// With options\nawait agent.execute(options: AgentExecuteOptions): Promise<AgentResult>\n```\n\n**AgentExecuteOptions Interface:**\n```typescript\ninterface AgentExecuteOptions {\n  instruction: string;\n  maxSteps?: number;\n  autoScreenshot?: boolean;\n  waitBetweenActions?: number;\n  context?: string;\n}\n```\n\n</Tab>\n<Tab title=\"Python\">\n\n```python\n# String instruction\nawait agent.execute(instruction: str) -> AgentResult\n\n# With options dictionary\nawait agent.execute({\n    \"instruction\": str,\n    \"max_steps\": int = 20,\n    \"auto_screenshot\": bool = True,\n    \"wait_between_actions\": int = 0,\n    \"context\": str = None\n}) -> AgentResult\n```\n\n</Tab>\n</Tabs>\n\n### Execute Parameters\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  High-level task description in natural language.\n</ParamField>\n\n<ParamField path=\"maxSteps\" type=\"number\" optional>\n  Maximum number of actions the agent can take.\n  \n  **Default:** `20`\n</ParamField>\n\n<ParamField path=\"autoScreenshot\" type=\"boolean\" optional>\n  Whether to take screenshots before each action.\n  \n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"waitBetweenActions\" type=\"number\" optional>\n  Delay in milliseconds between actions.\n  \n  **Default:** `0`\n</ParamField>\n\n<ParamField path=\"context\" type=\"string\" optional>\n  Additional context or constraints for the agent.\n</ParamField>\n\n### Response\n\n**Returns:** `Promise<AgentResult>`\n\n**AgentResult Interface:**\n```typescript\ninterface AgentResult {\n  success: boolean;\n  message: string;\n  actions: AgentAction[];\n  completed: boolean;\n  metadata?: Record<string, unknown>;\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n    reasoning_tokens?: number;\n    cached_input_tokens?: number;\n    inference_time_ms: number;\n  };\n}\n```\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n<ParamField path=\"success\" type=\"boolean\">\n  Whether the task was completed successfully.\n</ParamField>\n\n<ParamField path=\"message\" type=\"string\">\n  Description of the execution result and status.\n</ParamField>\n\n<ParamField path=\"actions\" type=\"AgentAction[]\">\n  Array of individual actions taken during execution.\n</ParamField>\n\n<ParamField path=\"completed\" type=\"boolean\">\n  Whether the agent believes the task is fully complete.\n</ParamField>\n\n<ParamField path=\"metadata\" type=\"Record<string, unknown>\" optional>\n  Additional execution metadata and debugging information.\n</ParamField>\n\n<ParamField path=\"usage\" type=\"object\" optional>\n  Token usage and performance metrics.\n</ParamField>\n\n</Tab>\n<Tab title=\"Python\">\n\n<ParamField path=\"success\" type=\"boolean\">\n  Whether the task was completed successfully.\n</ParamField>\n\n<ParamField path=\"message\" type=\"string\">\n  Description of the execution result and status.\n</ParamField>\n\n<ParamField path=\"actions\" type=\"AgentAction[]\">\n  Array of individual actions taken during execution.\n</ParamField>\n\n<ParamField path=\"completed\" type=\"boolean\">\n  Whether the agent believes the task is fully complete.\n</ParamField>\n\n</Tab>\n</Tabs>\n\n### Example Response\n```json\n{\n  \"success\": true,\n  \"message\": \"Task completed successfully\",\n  \"actions\": [\n    {\n      \"action\": \"click\",\n      \"selector\": \"button.primary\",\n      \"text\": \"Submit\"\n    }\n  ],\n  \"completed\": true,\n  \"metadata\": {\n    \"execution_time\": 1000\n  },\n  \"usage\": {\n    \"input_tokens\": 100,\n    \"output_tokens\": 50,\n    \"reasoning_tokens\": 12,\n    \"cached_input_tokens\": 0,\n    \"inference_time_ms\": 100\n  }\n}\n```"
  },
  {
    "path": "packages/docs/v2/references/extract.mdx",
    "content": "---\ntitle: extract()\ndescription: 'Complete API reference for the extract() method'\nicon: 'ufo-beam'\n---\n\n<CardGroup cols={1}>\n<Card title=\"Extract\" icon=\"ufo-beam\" href=\"/v2/basics/extract\">\n  See how to use extract() to extract structured data from web pages\n</Card>\n</CardGroup>\n\n### Method Signatures\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// With schema and options\nawait page.extract<T extends z.AnyZodObject>(options: ExtractOptions<T>): Promise<ExtractResult<T>>\n\n// String instruction only\nawait page.extract(instruction: string): Promise<{ extraction: string }>\n\n// No parameters (raw page content)\nawait page.extract(): Promise<{ pageText: string }>\n```\n\n**ExtractOptions Interface:**\n```typescript\ninterface ExtractOptions<T extends z.AnyZodObject> {\n  instruction?: string;\n  schema?: T;\n  modelName?: AvailableModel;\n  modelClientOptions?: ClientOptions;\n  domSettleTimeoutMs?: number;\n  selector?: string;\n  iframes?: boolean;\n}\n\ntype ExtractResult<T> = z.infer<T>;\n```\n\n</Tab>\n<Tab title=\"Python\">\n\n```python\n# With schema and parameters\nawait page.extract(\n    instruction: str = None,\n    schema: BaseModel = None,\n    selector: str = None,\n    iframes: bool = None,\n    model_name: AvailableModel = None,\n    model_client_options: Dict = None,\n    dom_settle_timeout_ms: int = None\n) -> ExtractResult\n\n# String instruction only\nawait page.extract(instruction: str) -> Dict[str, str]\n\n# No parameters (raw page content)\nawait page.extract() -> Dict[str, str]\n```\n\n</Tab>\n</Tabs>\n\n### Parameters\n\n<ParamField path=\"instruction\" type=\"string\" optional>\n  Natural language description of what data to extract.\n</ParamField>\n\n<ParamField path=\"schema\" type=\"z.ZodSchema | BaseModel\" optional>\n  Type schema defining the structure of data to extract. Ensures type safety and validation.\n</ParamField>\n\n<ParamField path=\"selector\" type=\"string\" optional>\n  XPath selector to limit extraction scope. Reduces token usage and improves accuracy.\n</ParamField>\n\n<ParamField path=\"iframes\" type=\"boolean\" optional>\n  Set to `true` if content exists within iframes.\n  \n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"modelName\" type=\"AvailableModel\" optional>\n  Override the default LLM model for this extraction.\n</ParamField>\n\n<ParamField path=\"modelClientOptions\" type=\"ClientOptions\" optional>\n  Model-specific configuration options.\n</ParamField>\n\n<ParamField path=\"domSettleTimeoutMs\" type=\"number\" optional>\n  Maximum time to wait for DOM to stabilize.\n  \n  **Default:** `30000`\n</ParamField>\n\n### Response Types\n\n<Tabs>\n<Tab title=\"With Schema\">\n**Returns:** `Promise<ExtractResult<T>>` where T matches your schema\n\nThe returned object will be strictly typed according to your schema definition.\n</Tab>\n\n<Tab title=\"String Only\">\n**Returns:** `Promise<{ extraction: string }>`\n\nSimple string extraction without schema validation.\n</Tab>\n\n<Tab title=\"No Parameters\">\n**Returns:** `Promise<{ pageText: string }>`\n\nRaw accessibility tree representation of page content.\n</Tab>\n</Tabs>\n\n### Code Examples\n\n<Tabs>\n<Tab title=\"Single Object\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod';\n\n// Schema definition\nconst ProductSchema = z.object({\n  name: z.string(),\n  price: z.number(),\n  inStock: z.boolean()\n});\n\n// Extraction\nconst product = await page.extract({\n  instruction: \"extract product details\",\n  schema: ProductSchema\n});\n```\n\n```python Python\nfrom pydantic import BaseModel\n\n# Schema definition\nclass Product(BaseModel):\n    name: str\n    price: float\n    in_stock: bool\n\n# Extraction\nproduct = await page.extract(\n    instruction=\"extract product details\",\n    schema=Product\n)\n```\n</CodeGroup>\n\n#### Example Response\n```json\n{\n  \"name\": \"Product Name\",\n  \"price\": 100,\n  \"inStock\": true\n}\n```\n\n</Tab>\n<Tab title=\"Arrays\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod';\n\n// Schema definition\nconst ApartmentListingsSchema = z.object({\n  apartments: z.array(z.object({\n    address: z.string(),\n    price: z.string(),\n    bedrooms: z.number()\n  }))\n});\n\n// Extraction\nconst listings = await page.extract({\n  instruction: \"extract all apartment listings\", \n  schema: ApartmentListingsSchema\n});\n```\n\n```python Python\nfrom pydantic import BaseModel\nfrom typing import List\n\n# Schema definition\nclass Apartment(BaseModel):\n    address: str\n    price: str\n    bedrooms: int\n\nclass ApartmentListings(BaseModel):\n    apartments: List[Apartment]\n\n# Extraction\nlistings = await page.extract(\n    instruction=\"extract all apartment listings\",\n    schema=ApartmentListings\n)\n```\n</CodeGroup>\n\n#### Example Response\n```json\n{\n  \"apartments\": [\n    {\n      \"address\": \"123 Main St\",\n      \"price\": \"$100,000\",\n      \"bedrooms\": 3\n    },\n    {\n      \"address\": \"456 Elm St\",\n      \"price\": \"$150,000\",\n      \"bedrooms\": 2\n    }\n  ]\n}\n```\n\n</Tab>\n<Tab title=\"URLs\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod';\n\n// Schema definition\nconst NavigationSchema = z.object({\n  links: z.array(z.object({\n    text: z.string(),\n    url: z.string().url()  // URL validation\n  }))\n});\n\n// Extraction\nconst links = await page.extract({\n  instruction: \"extract navigation links\",\n  schema: NavigationSchema\n});\n```\n\n```python Python\nfrom pydantic import BaseModel, HttpUrl\nfrom typing import List\n\n# Schema definition\nclass NavLink(BaseModel):\n    text: str\n    url: HttpUrl  # URL validation\n\nclass Navigation(BaseModel):\n    links: List[NavLink]\n\n# Extraction\nlinks = await page.extract(\n    instruction=\"extract navigation links\", \n    schema=Navigation\n)\n```\n</CodeGroup>\n\n#### Example Response\n```json\n{\n  \"links\": [\n    {\n      \"text\": \"Home\",\n      \"url\": \"https://example.com\"\n    }\n  ]\n}\n```\n\n</Tab>\n<Tab title=\"Scoped\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod';\n\nconst ProductSchema = z.object({\n  name: z.string(),\n  price: z.number(),\n  description: z.string()\n});\n\n// Extract from specific page section\nconst data = await page.extract({\n  instruction: \"extract product info from this section\",\n  selector: \"xpath=/html/body/div/div\",\n  schema: ProductSchema\n});\n```\n\n```python Python\nfrom pydantic import BaseModel\n\nclass Product(BaseModel):\n    name: str\n    price: float\n    description: str\n\n# Extract from specific page section\ndata = await page.extract(\n    instruction=\"extract product info from this section\",\n    selector=\"xpath=/html/body/div/div\",\n    schema=Product\n)\n```\n</CodeGroup>\n\n#### Example Response\n```json\n{\n  \"name\": \"Product Name\",\n  \"price\": 100,\n  \"description\": \"Product description\"\n}\n```\n\n</Tab>\n<Tab title=\"Schema-less\">\n\n<CodeGroup>\n```typescript TypeScript\n// String only extraction\nconst title = await page.extract(\"get the page title\");\n// Returns: { extraction: \"Page Title\" }\n\n// Raw page content\nconst content = await page.extract();\n// Returns: { pageText: \"Accessibility Tree: ...\" }\n```\n\n```python Python\n# String only extraction\ntitle = await page.extract(\"get the page title\")\n# Returns: {\"extraction\": \"Page Title\"}\n\n# Raw page content\ncontent = await page.extract()\n# Returns: {\"pageText\": \"Accessibility Tree: ...\"}\n```\n</CodeGroup>\n\n#### Example Response\n```json\n{\n  \"extraction\": \"Page Title\"\n}\n```\n\n</Tab>\n<Tab title=\"Advanced\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { z } from 'zod';\n\n// Schema with descriptions and validation\nconst ProductSchema = z.object({\n  price: z.number().describe(\"Product price in USD\"),\n  rating: z.number().min(0).max(5).describe(\"Customer rating out of 5\"),\n  available: z.boolean().describe(\"Whether product is in stock\"),\n  tags: z.array(z.string()).optional()\n});\n\n// Nested schema\nconst EcommerceSchema = z.object({\n  product: z.object({\n    name: z.string(),\n    price: z.object({\n      current: z.number(),\n      original: z.number().optional()\n    })\n  }),\n  reviews: z.array(z.object({\n    rating: z.number(),\n    comment: z.string()\n  }))\n});\n```\n\n```python Python\nfrom pydantic import BaseModel, Field\nfrom typing import Optional, List\n\n# Schema with descriptions and validation\nclass Product(BaseModel):\n    price: float = Field(description=\"Product price in USD\")\n    rating: float = Field(ge=0, le=5, description=\"Customer rating out of 5\")\n    available: bool = Field(description=\"Whether product is in stock\")\n    tags: Optional[List[str]] = None\n\n# Nested schema\nclass Price(BaseModel):\n    current: float\n    original: Optional[float] = None\n\nclass Review(BaseModel):\n    rating: int\n    comment: str\n\nclass ProductDetails(BaseModel):\n    name: str\n    price: Price\n\nclass EcommerceData(BaseModel):\n    product: ProductDetails\n    reviews: List[Review]\n```\n</CodeGroup>\n\n#### Example Response\n```json\n{\n  \"product\": {\n    \"name\": \"Product Name\",\n    \"price\": {\n      \"current\": 100,\n      \"original\": 120\n    }\n  },\n  \"reviews\": [\n    {\n      \"rating\": 4,\n      \"comment\": \"Great product!\"\n    }\n  ]\n}\n```\n\n</Tab>\n</Tabs>"
  },
  {
    "path": "packages/docs/v2/references/observe.mdx",
    "content": "---\ntitle: observe()\ndescription: 'Complete API reference for the observe() method'\nicon: 'magnifying-glass'\n---\n\n<CardGroup cols={1}>\n<Card title=\"Observe\" icon=\"magnifying-glass\" href=\"/v2/basics/observe\">\n  See how to use observe() to discover actionable elements and analyze web page structure\n</Card>\n</CardGroup>\n\n### Method Signatures\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// With ObserveOptions\nawait page.observe(options: ObserveOptions): Promise<ObserveResult[]>\n\n// String instruction shorthand\nawait page.observe(instruction: string): Promise<ObserveResult[]>\n```\n\n**ObserveOptions Interface:**\n```typescript\ninterface ObserveOptions {\n  instruction?: string;\n  modelName?: AvailableModel;\n  modelClientOptions?: ClientOptions;\n  domSettleTimeoutMs?: number;\n  drawOverlay?: boolean;\n  iframes?: boolean;\n}\n```\n\n</Tab>\n<Tab title=\"Python\">\n\n```python\n# With parameters\nawait page.observe(\n    instruction: str,\n    dom_settle_timeout_ms: int = None,\n    iframes: bool = None,\n    model_name: AvailableModel = None,\n    model_client_options: Dict = None\n) -> List[ObserveResult]\n```\n\n</Tab>\n</Tabs>\n\n### Parameters\n\n<ParamField path=\"instruction\" type=\"string\" optional>\n  Natural language description of elements or actions to discover.\n</ParamField>\n\n<ParamField path=\"modelName\" type=\"AvailableModel\" optional>\n  Override the default LLM model for this observation.\n</ParamField>\n\n<ParamField path=\"modelClientOptions\" type=\"ClientOptions\" optional>\n  Model-specific configuration options.\n</ParamField>\n\n<ParamField path=\"domSettleTimeoutMs\" type=\"number\" optional>\n  Maximum time to wait for DOM to stabilize before analysis.\n  \n  **Default:** `30000`\n</ParamField>\n\n<ParamField path=\"drawOverlay\" type=\"boolean\" optional>\n  Whether to draw visual overlays on discovered elements (debugging).\n  \n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"iframes\" type=\"boolean\" optional>\n  Set to `true` to search within iframes.\n  \n  **Default:** `false`\n</ParamField>\n\n### Response\n\n**Returns:** `Promise<ObserveResult[]>`\n\nArray of discovered actionable elements, ordered by relevance.\n\n**ObserveResult Interface:**\n```typescript\ninterface ObserveResult {\n  selector: string;        // XPath selector to locate element\n  description: string;     // Human-readable description\n  method?: string;         // Suggested action method\n  arguments?: string[];    // Additional action parameters\n}\n```\n\n<ParamField path=\"selector\" type=\"string\">\n  XPath selector that precisely locates the element.\n</ParamField>\n\n<ParamField path=\"description\" type=\"string\">\n  Human-readable description of the element and its purpose.\n</ParamField>\n\n<ParamField path=\"method\" type=\"string\" optional>\n  Suggested interaction method: `\"click\"`, `\"fill\"`, `\"selectOptionFromDropdown\"`, `\"nextChunk\"`, `\"scrollTo\"`, `\"prevChunk\"`.\n</ParamField>\n\n<ParamField path=\"arguments\" type=\"string[]\" optional>\n  Additional parameters for the suggested action.\n</ParamField>\n\n\n### Code Examples\n\n<CodeGroup>\n```typescript TypeScript\n// Basic element discovery\nconst buttons = await page.observe(\"find all clickable buttons\");\nconst formFields = await page.observe(\"locate form input fields\");\n\n// Advanced configuration\nconst elements = await page.observe({\n  instruction: \"find important call-to-action buttons\",\n  modelName: \"gpt-4.1-mini\",\n  domSettleTimeoutMs: 45000,\n  drawOverlay: true\n});\n\n// Working with results\nconst [loginButton] = await page.observe(\"find the login button\");\nif (loginButton) {\n  console.log(\"Found:\", loginButton.description);\n  console.log(\"Selector:\", loginButton.selector);\n  await page.act(loginButton); // Execute the action\n}\n\n// Filter results\nconst submitButtons = await page.observe(\"find all submit buttons\");\nconst primarySubmit = submitButtons.find(btn => \n  btn.description.toLowerCase().includes('primary')\n);\n\n// Iframe search\nconst iframeElements = await page.observe({\n  instruction: \"find form fields inside the iframe\",\n  iframes: true\n});\n```\n\n```python Python\n# Basic element discovery\nbuttons = await page.observe(\"find all clickable buttons\")\nform_fields = await page.observe(\"locate the form fields\")\n\n# Advanced configuration  \nelements = await page.observe(\n    instruction=\"find important call-to-action buttons\",\n    model_name=\"gpt-4.1-mini\",\n    dom_settle_timeout_ms=45000\n)\n\n# Working with results\nlogin_buttons = await page.observe(\"find the login button\")\nif login_buttons:\n    button = login_buttons[0]\n    print(\"Found:\", button.description)\n    print(\"Selector:\", button.selector)\n    await page.act(button)  # Execute the action\n\n# Filter results\nsubmit_buttons = await page.observe(\"find all submit buttons\")\nprimary_submit = next((\n    btn for btn in submit_buttons \n    if 'primary' in btn.description.lower()\n), None)\n\n# Iframe search\niframe_elements = await page.observe(\n    instruction=\"find the form fields inside the iframe\",\n    iframes=True\n)\n```\n</CodeGroup>\n\n### Integration Patterns\n\n<CodeGroup>\n```typescript TypeScript\n// Observe → Act workflow\nconst actions = await page.observe(\"find checkout elements\");\nfor (const action of actions) {\n  await page.act(action);\n  await page.waitForTimeout(1000);\n}\n\n// Observe → Extract workflow\nconst tables = await page.observe(\"find data tables\");\nif (tables.length > 0) {\n  const data = await page.extract({\n    instruction: \"extract the table data\",\n    selector: tables[0].selector,\n    schema: DataSchema\n  });\n}\n\n// Element validation\nconst requiredElements = await page.observe(\"find the login form\");\nif (requiredElements.length === 0) {\n  throw new Error(\"Login form not found\");\n}\n```\n\n```python Python\n# Observe → Act workflow  \nactions = await page.observe(\"find checkout elements\")\nfor action in actions:\n    await page.act(action)\n    await page.wait_for_timeout(1000)\n\n# Observe → Extract workflow\ntables = await page.observe(\"find data tables\")\nif tables:\n    data = await page.extract(\n        instruction=\"extract the table data\",\n        selector=tables[0].selector,\n        schema=DataSchema\n    )\n\n# Element validation\nrequired_elements = await page.observe(\"find login form\")\nif not required_elements:\n    raise Exception(\"Login form not found\")\n```\n</CodeGroup>"
  },
  {
    "path": "packages/docs/v2/references/stagehand.mdx",
    "content": "---\ntitle: 'Stagehand'\ndescription: 'Complete API reference for initializing Stagehand'\nicon: 'wand-magic-sparkles'\n---\n\n### Constructor Signature\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  // ... other options\n});\nawait stagehand.init();\n```\n\n**ConstructorParams Interface:**\n```typescript\ninterface ConstructorParams {\n  env: \"BROWSERBASE\" | \"LOCAL\";\n  apiKey?: string;\n  projectId?: string;\n  verbose?: 0 | 1 | 2;\n  llmProvider?: LLMProvider;\n  logger?: (message: LogLine) => void | Promise<void>;\n  domSettleTimeoutMs?: number;\n  browserbaseSessionCreateParams?: Omit<Browserbase.Sessions.SessionCreateParams, \"projectId\"> & { projectId?: string };\n  enableCaching?: boolean;\n  browserbaseSessionID?: string;\n  modelName?: AvailableModel;\n  llmClient?: LLMClient;\n  modelClientOptions?: ClientOptions;\n  systemPrompt?: string;\n  useAPI?: boolean;\n  waitForCaptchaSolves?: boolean;\n  localBrowserLaunchOptions?: LocalBrowserLaunchOptions;\n  logInferenceToFile?: boolean;\n  selfHeal?: boolean;\n  disablePino?: boolean;\n  experimental?: boolean;\n}\n```\n\n</Tab>\n<Tab title=\"Python\">\n\n```python\nfrom stagehand import Stagehand\n\nstagehand = Stagehand(\n    env: Literal[\"BROWSERBASE\", \"LOCAL\"] = \"BROWSERBASE\",\n    api_key: str = None,\n    project_id: str = None,\n    api_url: str = None,\n    model_name: str = None,\n    model_api_key: str = None,\n    model_client_options: Dict[str, Any] = None,\n    verbose: int = 1,\n    logger: Callable = None,\n    use_rich_logging: bool = True,\n    dom_settle_timeout_ms: int = 3000,\n    browserbase_session_create_params: Dict = None,\n    browserbase_session_id: str = None,\n    enable_caching: bool = False,\n    self_heal: bool = True,\n    wait_for_captcha_solves: bool = False,\n    system_prompt: str = None,\n    local_browser_launch_options: Dict[str, Any] = None,\n    use_api: bool = True,\n    experimental: bool = False,\n)\nawait stagehand.init()\n```\n\n</Tab>\n</Tabs>\n\n<CardGroup cols={1}>\n<Card title=\"Model Configuration\" icon=\"brain\" href=\"/configuration/models\">\n  Learn how to configure different LLM models for Stagehand\n</Card>\n</CardGroup>\n\n### Parameters\n\n#### Required Parameters\n\n<ParamField path=\"env\" type=\"'BROWSERBASE' | 'LOCAL'\" required>\n  The environment to use for Stagehand.\n\n  - `BROWSERBASE` - Run browser on Browserbase cloud infrastructure\n  - `LOCAL` - Run browser locally on your machine\n\n  **Default:** `\"BROWSERBASE\"` (Python only)\n</ParamField>\n\n#### Browserbase Configuration\n\n<ParamField path=\"apiKey\" type=\"string\" optional>\n  Your Browserbase API key. Required when `env` is `BROWSERBASE`.\n</ParamField>\n\n<ParamField path=\"projectId\" type=\"string\" optional>\n  Your Browserbase project ID. Required when `env` is `BROWSERBASE`.\n</ParamField>\n\n<ParamField path=\"browserbaseSessionID\" type=\"string\" optional>\n  The ID of an existing Browserbase session to resume. Useful for continuing previous browser sessions.\n</ParamField>\n\n<ParamField path=\"browserbaseSessionCreateParams\" type=\"object\" optional>\n  Parameters to use when creating a Browserbase session. See [Browserbase API documentation](https://docs.browserbase.com/reference/api/create-a-session) for available options.\n</ParamField>\n\n<ParamField path=\"waitForCaptchaSolves\" type=\"boolean\" optional>\n  Wait for captchas to be solved after navigation when using Browserbase environment.\n\n  **Default:** `false`\n</ParamField>\n\n#### Local Browser Configuration\n\n<ParamField path=\"localBrowserLaunchOptions\" type=\"LocalBrowserLaunchOptions\" optional>\n  Configuration options for launching a local browser. Only used when `env` is `LOCAL`.\n\n  See the [full interface definition](https://github.com/browserbase/stagehand/blob/v2/types/stagehand.ts#L174) for all available options.\n</ParamField>\n\n#### LLM Configuration\n\n<CardGroup cols={1}>\n<Card title=\"Model Configuration\" icon=\"brain\" href=\"/configuration/models\">\n  Learn how to configure different LLM models for Stagehand\n</Card>\n</CardGroup>\n\n<ParamField path=\"modelName\" type=\"AvailableModel\" optional>\n  The LLM model to use for Stagehand operations.\n\n  **Examples:** `gpt-4o`, `gpt-4o-mini`, `claude-sonnet-4-6`\n\n  **Python Default:** `\"gpt-4o\"`\n</ParamField>\n\n<ParamField path=\"modelApiKey\" type=\"string\" optional>\n  API key for the LLM model provider. **Python only.**\n\n  In TypeScript, use `modelClientOptions.apiKey` instead.\n</ParamField>\n\n<ParamField path=\"llmProvider\" type=\"LLMProvider\" optional>\n  The LLM provider to use for Stagehand. Custom provider implementation. **TypeScript only.**\n</ParamField>\n\n<ParamField path=\"llmClient\" type=\"LLMClient\" optional>\n  Custom LLM client instance to use for Stagehand operations. **TypeScript only.**\n</ParamField>\n\n<ParamField path=\"modelClientOptions\" type=\"ClientOptions\" optional>\n  LLM client configuration options. Useful for parameterizing LLM API keys and other settings.\n\n  **Common options:** `apiKey` (TypeScript), `api_base`, `temperature`, `maxTokens`\n</ParamField>\n\n<ParamField path=\"enableCaching\" type=\"boolean\" optional>\n  Enable caching of LLM responses to reduce API calls and costs.\n\n  **TypeScript Default:** `true`\n  **Python Default:** `false`\n</ParamField>\n\n#### Logging and Debugging\n\n<Note>\n**Security tip:** Use `verbose: 0` when your automation handles secrets to prevent sensitive data from appearing in logs.\n</Note>\n\n<ParamField path=\"verbose\" type=\"0 | 1 | 2\" optional>\n  The verbosity level of the Stagehand logger.\n\n  - `0` - Minimal (ERROR only)\n  - `1` - Medium (INFO level)\n  - `2` - Detailed (DEBUG level)\n\n  **Default:** `1`\n</ParamField>\n\n<ParamField path=\"logger\" type=\"(message: LogLine) => void | Promise<void>\" optional>\n  Custom logger function to handle log messages from Stagehand.\n</ParamField>\n\n<ParamField path=\"useRichLogging\" type=\"boolean\" optional>\n  Whether to use Rich for colorized logging output. **Python only.**\n\n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"disablePino\" type=\"boolean\" optional>\n  Disable Pino logger. Helpful for Next.js or test environments where Pino causes issues. **TypeScript only.**\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"logInferenceToFile\" type=\"boolean\" optional>\n  Log LLM inference details to a file for debugging purposes. **TypeScript only.**\n\n  **Default:** `false`\n</ParamField>\n\n#### Performance and Behavior\n\n<ParamField path=\"domSettleTimeoutMs\" type=\"number\" optional>\n  Default timeout to wait for the DOM to settle before performing operations.\n\n  **TypeScript Default:** `10000` (10 seconds)\n  **Python Default:** `3000` (3 seconds)\n</ParamField>\n\n<ParamField path=\"selfHeal\" type=\"boolean\" optional>\n  Enable self-healing capabilities to automatically recover from failures.\n\n  **Python Default:** `true`\n</ParamField>\n\n<ParamField path=\"systemPrompt\" type=\"string\" optional>\n  Customize the Stagehand system prompt used for LLM interactions.\n</ParamField>\n\n<ParamField path=\"useAPI\" type=\"boolean\" optional>\n  Offload Stagehand method calls to the Stagehand API.\n\n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"experimental\" type=\"boolean\" optional>\n  Enable the latest experimental features. Use with caution in production.\n\n  **Default:** `false`\n</ParamField>\n\n### Returns `InitResult`\n\nAfter calling `stagehand.init()`, you receive an `InitResult` object:\n\n<ParamField path=\"debugUrl\" type=\"string\" required>\n  URL for debugging the browser session (e.g., Chrome DevTools).\n</ParamField>\n\n<ParamField path=\"sessionUrl\" type=\"string\" required>\n  URL of the browser session (especially useful with Browserbase).\n</ParamField>\n\n<ParamField path=\"sessionId\" type=\"string\" required>\n  Unique identifier for the browser session.\n</ParamField>"
  },
  {
    "path": "packages/docs/v3/basics/act.mdx",
    "content": "---\ntitle: Act\ndescription: 'Interact with a web page'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## What is `act()`?\n``` typescript\nawait stagehand.act(\"click on add to cart\")\n```\n`act` enables Stagehand to perform **individual** actions on a web page. Use it to build self-healing and deterministic automations that adapt to website changes. \n\n## Why use `act()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Natural Language Instructions\" icon=\"wand-magic-sparkles\" href=\"#using-act\">\n    Write automation in plain English. No selectors or complex syntax.\n  </Card>\n  <Card title=\"Precise Control\" icon=\"crosshairs\" href=\"#best-practices\">\n    Build automations step by step. Define exactly what happens at every moment.\n  </Card>\n  <Card title=\"Self-Healing\" icon=\"bandage\" href=\"#ensure-reliable-actions\">\n    Actions automatically adapt when websites change.\n  </Card>\n  <Card title=\"Caching\" icon=\"repeat\" href=\"#reduce-model-costs\">\n    Cache actions to avoid LLM calls and ensure consistent execution across runs.\n  </Card>\n</CardGroup>\n\n\n## Using `act()`\n\nUse `act` to perform single actions in your automation. Here's how to click a button:\n\n```typescript\nawait page.goto(\"https://example-store.com\");\nawait stagehand.act(\"click the add to cart button\");\n```\n\n<Note>\n**iFrame and Shadow DOM Support** Stagehand automatically handles iFrame traversal and shadow DOM elements without requiring additional configuration.\n</Note>\n\nWith `act`, breaking complex actions into small, single-step actions works best. If you need to orchestrate multi-step flows, use multiple `act` commands or `agent`.\n\n<Accordion title=\"Suggested actions\">\n\n| Action | Example instruction |\n|--------|---------------------|\n| Click | `click the button` |\n| Fill | `fill the field with <value>` |\n| Type | `type <text> into the search box` |\n| Press | `press <key> in the search field` |\n| Scroll | `scroll to <position>` |\n| Select from dropdown | `select <value> from the dropdown` |\n</Accordion>\n\n### Return value of `act()`?\nWhen you use `act()`, Stagehand will return a `Promise<ActResult>` with the following structure:\n``` typescript\n{\n  success: true,\n  message: 'Action [click] performed successfully on selector: xpath=/html[1]/body[1]/div[1]/span[1] → Action [click] performed successfully on selector: xpath=/html[1]/body[1]/div[2]/div[1]/section[1]/div[1]/div[1]/div[25]',\n  actionDescription: 'Favorite Colour',\n  actions: [\n    {\n      selector: 'xpath=/html[1]/body[1]/div[1]/span[1]',\n      description: 'Favorite Colour',\n      method: 'click',\n      arguments: []\n    },\n    {\n      selector: 'xpath=/html[1]/body[1]/div[2]/div[1]/section[1]/div[1]/div[1]/div[25]',\n      description: 'Peach',\n      method: 'click',\n      arguments: []\n    }\n  ]\n}\n```\n\n\n<Tabs>\n<Tab title=\"Do this\" icon=\"check\">\nBreak your task into single-step actions.\n\n```typescript\n// Break it into single-step actions\nawait stagehand.act(\"open the filters panel\");\nawait stagehand.act(\"choose 4-star rating\");\nawait stagehand.act(\"click the apply button\");\n```\n</Tab>\n\n<Tab title=\"Don't do this\" icon=\"xmark\">\nFor multi-step tasks, use [`agent()`](/basics/agent) instead.\n\n```typescript\n// Too complex - trying to do multiple things at once\nawait stagehand.act(\"open the filters panel, choose 4-star rating, and click apply\");\n```\n</Tab>\n</Tabs>\n\n## Advanced Configuration\n\nYou can pass additional options to configure the model, timeout, variables, and target page:\n\n```typescript\n// Custom model configuration\nawait stagehand.act(\"choose 'Peach' from the favorite color dropdown\", {\n  model: {\n    modelName: \"google/gemini-2.5-flash\",\n    apiKey: process.env.GEMINI_API_KEY\n  },\n  timeout: 10000\n});\n```\n\n### Server-side Caching\n\n<Note>\n`serverCache` only works when running with `env: \"BROWSERBASE\"`. It has no effect in local environments.\n</Note>\n\nWhen running on Browserbase, Stagehand automatically caches `act()` results server-side. Repeated calls with the same inputs return instantly without consuming LLM tokens. Caching is enabled by default and can be controlled globally on the constructor or overridden per call:\n\n```typescript\n// Disable server-side caching for the entire instance\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  serverCache: false,\n});\n\n// Or disable it for a single call\nawait stagehand.act(\"click the login button\", { serverCache: false });\n\n// Check whether a result was served from cache\nconst result = await stagehand.act(\"click the login button\");\nconsole.log(result.cacheStatus); // \"HIT\", \"MISS\", or undefined\n```\n\n### Using with Custom Pages\n\nYou can use `act()` with pages from other browser automation libraries like Puppeteer, Playwright, or Patchright by passing the `page` option:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport puppeteer from \"puppeteer-core\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n});\nawait stagehand.init();\n\n// Connect with Puppeteer\nconst browser = await puppeteer.connect({\n  browserWSEndpoint: stagehand.connectURL(),\n  defaultViewport: null,\n});\n\nconst pages = await browser.pages();\nconst customPage = pages[0];\n\nawait customPage.goto(\"https://www.example.com/blog\");\n\n// Use act with the custom Puppeteer page\nawait stagehand.act(\"click the next page button\", {\n  page: customPage\n});\n```\n\nThis works with:\n- **Puppeteer**: Pass Puppeteer Page objects\n- **Playwright**: Pass Playwright Page objects\n- **Patchright**: Pass Patchright Page objects\n- **Stagehand Page**: Use `stagehand.context.pages()[0]` or `context.activePage()` (default)\n\n<Card title=\"Complete API Reference\" icon=\"book\" href=\"/v3/references/act\">\n  See the full `act()` reference for detailed parameter documentation, return values, and advanced examples.\n</Card>\n\n\n\n\n\n## Best practices\n\n### Ensure reliable actions\n\nUse `observe()` to discover candidate actions on the current page and plan reliably. It returns a list of suggested actions (with selector, description, method, and arguments). You can pass an observed action directly to `act` to execute it.\n\n```typescript\nconst [action] = await stagehand.observe(\"click the login button\");\n\nif (action) {\n  await stagehand.act(action);\n}\n```\n\n<Card title=\"Analyze pages with observe()\" icon=\"magnifying-glass\" iconType=\"sharp-solid\" href=\"/v3/basics/observe\">\n  Plan actions with `observe()` before executing with `act`.\n</Card>\n\n### Reduce model costs\n\nEnable automatic action caching by specifying a `cacheDir` when initializing Stagehand. The first time an action runs, it's cached. Subsequent runs reuse the cached action without LLM calls.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Enable caching by specifying a cache directory\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"act-cache\" // Actions are automatically cached here\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// First run - makes LLM call and caches the action\nawait stagehand.act(\"click the login button\");\n```\n\n<Note>\nCaching persists across script executions. The first time you run your script, actions are cached to your local filesystem. On subsequent runs, cached actions are reused automatically, significantly reducing costs and improving performance.\n</Note>\n\n<Card title=\"Complete caching guide\" icon=\"database\" iconType=\"sharp-solid\" href=\"/v3/best-practices/caching\">\n  Learn advanced caching techniques and patterns for optimal performance.\n</Card>\n\n### Secure your automations\n\nVariables are **not shared with LLM providers**. Use them for passwords, API keys, and other sensitive data.\n\n<Note>\nLoad sensitive data from environment variables using `.env` files. Never hardcode API keys, passwords, or other secrets directly in your code.\n</Note>\n\n```typescript\n// Variables use %variableName% syntax in the instruction\nawait stagehand.act(\"type %username% into the email field\", {\n  variables: { username: \"user@example.com\" }\n});\n\nawait stagehand.act(\"type %password% into the password field\", {\n  variables: { password: process.env.USER_PASSWORD }\n});\n\nawait stagehand.act(\"click the login button\");\n```\n\n<Warning>\nWhen handling sensitive data, set `verbose: 0` in your Stagehand configuration to prevent secrets from appearing in logs. See the [configuration guide](/configuration/browser) for more details.\n</Warning>\n\n<Card title=\"User Data Best Practices\" icon=\"shield-check\" iconType=\"sharp-solid\" href=\"/v3/best-practices/user-data\">\n  Complete guide to securing your browser automations with best practices and configurations.\n</Card>\n\n## Troubleshooting\n\n<AccordionGroup>\n\n\n<Accordion title=\"Method not supported\">\n**Problem**: `act` fails with \"method not supported\" error\n\n**Solutions**:\n- Use clear and detailed instructions for what you want to accomplish\n- Review our [evals](https://stagehand.dev/evals) to find the best models for your use case\n- Use [`observe()`](/basics/observe) and verify the resulting action is within a list of expected actions\n\n**Solution 1: Validate with observe**\n\n```typescript\nconst prompt = \"click the submit button\";\nconst expectedMethod = \"click\";\n\ntry {\n  await stagehand.act(prompt);\n} catch (error) {\n  if (error.message.includes(\"method not supported\")) {\n    // Observe the same prompt to get the planned action\n    const [action] = await stagehand.observe(prompt);\n\n    if (action && action.method === expectedMethod) {\n      await stagehand.act(action);\n    } else {\n      throw new Error(`Unsupported method: expected \"${expectedMethod}\", got \"${action?.method}\"`);\n    }\n  } else {\n    throw error;\n  }\n}\n```\n\n**Solution 2: Retry with exponential backoff**\n\n```typescript\n// Retry with exponential backoff for intermittent issues\nconst prompt = \"click the submit button\";\nconst maxRetries = 3;\n\nfor (let attempt = 0; attempt <= maxRetries; attempt++) {\n  try {\n    await stagehand.act(prompt, { timeout: 10000 + (attempt * 5000) });\n    break; // Success, exit retry loop\n  } catch (error) {\n    if (error.message.includes(\"method not supported\") && attempt < maxRetries) {\n      // Exponential backoff: wait 2^attempt seconds\n      const delay = Math.pow(2, attempt) * 1000;\n      console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);\n      await new Promise(resolve => setTimeout(resolve, delay));\n    } else {\n      throw error;\n    }\n  }\n}\n```\n\n</Accordion>\n\n<Accordion title=\"Action failed or timed out\">\n**Problem**: `act` times out or fails to complete action (often due to element not found)\n\n**Solutions**:\n- Ensure page has fully loaded\n- Check if content is in iframes: [Learn more about working with iframes](/best-practices/working-with-iframes)\n- Increase action timeout\n- Use `observe()` first to verify element exists\n\n```typescript\n// Handle timeout and element not found issues\ntry {\n  await stagehand.act(\"click the submit button\", { timeout: 30000 });\n} catch (error) {\n  // Check if page is fully loaded\n  await page.waitForLoadState('domcontentloaded');\n\n  // Use observe to check element state\n  const [element] = await stagehand.observe(\"find the submit button\");\n\n  if (element) {\n    console.log(\"Element found, trying more specific instruction\");\n    await stagehand.act(\"click the submit button at the bottom of the form\");\n  } else {\n    console.log(\"Element not found, trying alternative selector\");\n    await stagehand.act(\"click the button with text 'Submit'\");\n  }\n}\n```\n</Accordion>\n\n<Accordion title=\"Incorrect element selected\">\n**Problem**: `act` performs action on wrong element\n\n**Solutions**:\n- Be more specific in instructions: include visual cues, position, or context\n- Use `observe()` to preview which element will be selected\n- Add contextual information: \"the search button in the header\"\n- Use unique identifiers when available\n\n```typescript\n// More precise element targeting\n// Instead of:\nawait stagehand.act(\"click the button\");\n\n// Use specific context:\nawait stagehand.act(\"click the red 'Delete' button next to the user John Smith\");\n\n// Or preview with observe first:\nconst [action] = await stagehand.observe(\"click the submit button in the checkout form\");\nif (action.description.includes(\"checkout\")) {\n  await stagehand.act(action);\n}\n```\n</Accordion>\n\n\n\n</AccordionGroup>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n  <Card title=\"Orchestrate complex workflows with Agent\" icon=\"robot\" iconType=\"sharp-solid\" href=\"/v3/basics/agent\">\n    Use `Agent` to autonomously execute multi-step tasks and complex workflows.\n  </Card>\n\n   <Card title=\"Caching actions\" icon=\"bolt\" iconType=\"sharp-solid\" href=\"/v3/best-practices/caching\">\n    Speed up repeated automations by caching actions.\n  </Card>\n\n  <Card title=\"Extract data with extract()\" icon=\"table\" iconType=\"sharp-solid\" href=\"/v3/basics/extract\">\n    Use `extract` with a data schema to pull clean, typed data from any page.\n  </Card>\n\n  <Card title=\"Preview actions with observe()\" icon=\"magnifying-glass\" iconType=\"sharp-solid\" href=\"/v3/basics/observe\">\n    Preview actions with `observe()` before executing them.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/basics/agent.mdx",
    "content": "---\ntitle: Agent\ndescription: 'Automate complex workflows with AI powered browser agents'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## What is `agent()?`\n\n``` typescript\nawait agent.execute(\"apply for a job at browserbase\")\n```\n`agent` turns high level tasks into **fully autonomous** browser workflows. You can customize the agent by specifying the LLM provider and model, setting custom instructions for behavior, and configuring max steps.\n\n<img src=\"/images/agent.gif\" alt=\"Agent\" />\n\n## Why use `agent()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Multi-Step Workflows\" icon=\"route\" href=\"#agent-execution-configuration\">\n    Execute complex sequences automatically.\n  </Card>\n  <Card title=\"Visual Understanding\" icon=\"eye\" href=\"/v3/best-practices/computer-use\">\n    Sees and understands web interfaces like humans do using computer vision.\n  </Card>\n</CardGroup>\n\n## Using `agent()`\n\nThere are three ways to create agents in Stagehand:\n1. Use a Computer Use Agent (CUA mode)\n2. Use Agent with any LLM (DOM mode)\n3. Use Agent with vision and DOM (Hybrid mode)\n\n### Feature Availability\n\nSome advanced features are only available with certain agent modes:\n\n| Feature                  | CUA | DOM | Hybrid |\n|:-------------------------|:---:|:---:|:------:|\n| Basic execution          | ✅  | ✅  | ✅     |\n| Custom tools             | ✅  | ✅  | ✅     |\n| MCP integrations         | ✅  | ✅  | ✅     |\n| System prompt            | ✅  | ✅  | ✅     |\n| Variables                | ❌  | ✅  | ✅     |\n| Streaming                | ❌  | ✅  | ✅     |\n| Callbacks                | ❌  | ✅  | ✅     |\n| Abort signal             | ❌  | ✅  | ✅     |\n| Message continuation     | ❌  | ✅  | ✅     |\n| Exclude tools            | ❌  | ✅  | ✅     |\n| Structured output        | ❌  | ✅  | ✅     |\n| DOM-based actions        | ❌  | ✅  | ✅     |\n| Coordinate-based actions | ✅  | ❌  | ✅     |\n| Visual cursor highlight  | ✅  | ❌  | ✅     |\n\n### Computer Use Agents\n\nYou can use specialized computer use models from Google, OpenAI, Anthropic, or Microsoft as shown below, with `mode` set to `\"cua\"`. To compare the performance of different computer use models, you can visit our [evals page](https://www.stagehand.dev/agent-evals).\n\n<Warning>\n**Deprecation Notice:** The `cua: true` option is deprecated and will be removed in a future version. Use `mode: \"cua\"` instead.\n</Warning>\n\n<CodeGroup>\n```typescript Google\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n        apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n    },\n    systemPrompt: \"You are a helpful assistant...\",\n});\n\nawait agent.execute({\n    instruction: \"Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.\",\n    maxSteps: 20,\n    highlightCursor: true\n})\n```\n\n```typescript OpenAI\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"openai/computer-use-preview\",\n        apiKey: process.env.OPENAI_API_KEY\n    },\n    systemPrompt: \"You are a helpful assistant...\",\n});\n\nawait agent.execute({\n    instruction: \"Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.\",\n    maxSteps: 20,\n    highlightCursor: true\n})\n```\n```typescript Anthropic\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"anthropic/claude-sonnet-4-20250514\",\n        apiKey: process.env.ANTHROPIC_API_KEY\n    },\n    systemPrompt: \"You are a helpful assistant...\",\n});\n\nawait agent.execute({\n    instruction: \"Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.\",\n    maxSteps: 20,\n    highlightCursor: true\n})\n```\n</CodeGroup>\n\n<Callout icon=\"code\" color=\"#6ec202\" iconType=\"regular\">View or run the example template [here](https://www.browserbase.com/templates/gemini-cua)</Callout>\n\n### Use Stagehand Agent with Any LLM\n\nUse the agent without specifying a provider to utilize any model or LLM provider:\n\n<Note>Non CUA agents are currently only supported in TypeScript</Note>\n\n```typescript TypeScript\nconst agent = stagehand.agent();\nawait agent.execute(\"apply for a job at Browserbase\")\n```\n<Card title=\"Available Agent Models\" icon=\"robot\" href=\"/v3/configuration/models#agent-models-with-cua-support\">\n  Check out the guide on how to use different models with Stagehand Agent.\n</Card>\n\n### Hybrid Mode\n\nBoth DOM and CUA modes have their strengths and weaknesses. Hybrid mode combines them, giving the agent access to both coordinate-based and DOM-based tools to better account for where each may fall short.\n\n<Warning>\n**Model Requirements:** Hybrid mode requires models that can reliably perform coordinate-based actions from screenshots. The following models are recommended:\n- **Google:** `google/gemini-3-flash-preview`\n- **Anthropic:** `anthropic/claude-sonnet-4-20250514`, `anthropic/claude-sonnet-4-5-20250929`, `anthropic/claude-haiku-4-5-20251001`\n\nOther models may not reliably produce accurate coordinates for clicking and typing.\n\n</Warning>\n\n<Note>Hybrid mode requires `experimental: true` in your Stagehand constructor.</Note>\n\n<CodeGroup>\n```typescript Hybrid Mode with Google\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  experimental: true, // Required for hybrid mode\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  mode: \"hybrid\",\n  model: \"google/gemini-3-flash-preview\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\nawait agent.execute({\n  instruction: \"Click the sign up button and fill out the registration form\",\n  maxSteps: 20,\n});\n```\n\n```typescript Hybrid Mode with Anthropic\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  experimental: true, // Required for hybrid mode\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  mode: \"hybrid\",\n  model: \"anthropic/claude-haiku-4-5-20251001\",\n  systemPrompt: \"You are a helpful assistant that interacts with web pages visually.\",\n});\n\nawait agent.execute({\n  instruction: \"Navigate the page and interact with the form elements\",\n  maxSteps: 15,\n  highlightCursor: true, // Enabled by default in hybrid mode\n});\n```\n</CodeGroup>\n\n### Return value of `agent()`?\n\nWhen you use `agent()`, Stagehand will return a `Promise<AgentResult>` with the following structure:\n\n```typescript\n{\n  success: true,\n  message: \"The first name and email fields have been filled successfully with 'John' and 'john@example.com'.\",\n  actions: [\n    {\n      type: 'ariaTree',\n      reasoning: undefined,\n      taskCompleted: true,\n      pageUrl: 'https://example.com',\n      timestamp: 1761598722055\n    },\n    {\n      type: 'act',\n      reasoning: undefined,\n      taskCompleted: true,\n      action: 'type \"John\" into the First Name textbox',\n      playwrightArguments: {...},\n      pageUrl: 'https://example.com',\n      timestamp: 1761598731643\n    },\n    {\n      type: 'close',\n      reasoning: \"The first name and email fields have been filled successfully.\",\n      taskCompleted: true,\n      taskComplete: true,\n      pageUrl: 'https://example.com',\n      timestamp: 1761598732861\n    }\n  ],\n  completed: true,\n  // Only populated when `output` schema is provided (DOM/Hybrid modes only)\n  output: {\n    price: \"$199\",\n    airline: \"Delta\"\n  },\n  usage: {\n    input_tokens: 2040,\n    output_tokens: 28,\n    reasoning_tokens: 12,\n    cached_input_tokens: 0,\n    inference_time_ms: 14079\n  }\n}\n```\n\n## Customizing Agent Tools\n\nStagehand agents come with built-in tools for browser automation, but you can customize the toolset by adding your own custom tools or excluding built-in ones.\n\n### Adding Custom Tools\n\nCustom tools enhance agents with additional capabilities for more granular control and better performance. Unlike MCP integrations, custom tools are defined inline and execute directly within your application.\n\n<Note>Custom tools provide a cleaner, more performant alternative to MCP integrations when you need specific functionality.</Note>\n\n#### Defining Custom Tools\n\nUse the `tool` helper exported from `@browserbasehq/stagehand` to define custom tools:\n\n<CodeGroup>\n```typescript Basic Tool\nimport { tool } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\nconst agent = stagehand.agent({\n  model: \"openai/gpt-5\",\n  tools: {\n    getWeather: tool({\n      description: 'Get the current weather in a location',\n      inputSchema: z.object({\n        location: z.string().describe('The location to get weather for'),\n      }),\n      execute: async ({ location }) => {\n        // Your custom logic here\n        const weather = await fetchWeatherAPI(location);\n        return {\n          location,\n          temperature: weather.temp,\n          conditions: weather.conditions,\n        };\n      },\n    }),\n  },\n  systemPrompt: 'You are a helpful assistant with access to weather data.',\n});\n\nawait agent.execute(\"What's the weather in San Francisco and should I bring an umbrella?\");\n```\n\n```typescript Multiple Tools\nimport { tool } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: \"anthropic/claude-sonnet-4-20250514\",\n  tools: {\n    searchDatabase: tool({\n      description: 'Search for records in the database',\n      inputSchema: z.object({\n        query: z.string().describe('The search query'),\n        limit: z.number().optional().describe('Max results to return'),\n      }),\n      execute: async ({ query, limit = 10 }) => {\n        const results = await db.search(query, limit);\n        return { results };\n      },\n    }),\n\n    calculatePrice: tool({\n      description: 'Calculate the total price with tax',\n      inputSchema: z.object({\n        amount: z.number().describe('The base amount'),\n        taxRate: z.number().describe('Tax rate as decimal (e.g., 0.08 for 8%)'),\n      }),\n      execute: async ({ amount, taxRate }) => {\n        const total = amount * (1 + taxRate);\n        return { total: total.toFixed(2) };\n      },\n    }),\n  },\n});\n\nawait agent.execute(\"Find products under $50 and calculate the total with 8% tax\");\n```\n\n```typescript Tool with API Integration\nimport { tool } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.0-flash\",\n  tools: {\n    sendEmail: tool({\n      description: 'Send an email via SendGrid',\n      inputSchema: z.object({\n        to: z.string().email().describe('Recipient email address'),\n        subject: z.string().describe('Email subject'),\n        body: z.string().describe('Email body content'),\n      }),\n      execute: async ({ to, subject, body }) => {\n        const response = await fetch('https://api.sendgrid.com/v3/mail/send', {\n          method: 'POST',\n          headers: {\n            'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify({\n            personalizations: [{ to: [{ email: to }] }],\n            from: { email: 'noreply@example.com' },\n            subject,\n            content: [{ type: 'text/plain', value: body }],\n          }),\n        });\n\n        return {\n          sent: response.ok,\n          messageId: response.headers.get('X-Message-Id'),\n        };\n      },\n    }),\n  },\n});\n\nawait agent.execute(\"Fill out the contact form and send me a confirmation email at user@example.com\");\n```\n</CodeGroup>\n\n#### Custom Tools vs MCP Integrations\n\n| Custom Tools                           | MCP Integrations                        |\n|----------------------------------------|-----------------------------------------|\n| Defined inline with your code          | Connect to external services            |\n| Direct function execution              | Standard protocol                       |\n| Better performance & optimized context | Reusable across applications            |\n| Type-safe with TypeScript              | Access to pre-built integrations        |\n| Granular control                       | Network-based communication             |\n\n<Tip>\nUse custom tools when you need specific functionality within your application. Use MCP integrations when connecting to external services or when you need standardized cross-application tools.\n</Tip>\n\n### Excluding Built-in Tools\n\nPrevent the agent from using specific built-in tools during execution. This is useful when you want to restrict the agent's capabilities or avoid certain behaviors.\n\n<Note>**Non-CUA agents only.** Requires `experimental: true`. Not available when `cua: true`.</Note>\n\n#### Basic Usage\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for excludeTools\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\n// Exclude screenshot and extract tools\nconst result = await agent.execute({\n  instruction: \"Navigate through the website and click the submit button\",\n  maxSteps: 15,\n  excludeTools: [\"screenshot\", \"extract\"],\n});\n```\n\n#### Available Tools by Mode\n\nThe tools you can exclude depend on the agent mode:\n\n<Tabs>\n<Tab title=\"DOM Mode (default)\">\n\n| Tool | Description |\n|------|-------------|\n| `act` | Perform semantic actions (click, type, etc.) |\n| `fillForm` | Fill form fields using DOM selectors |\n| `ariaTree` | Get accessibility tree of the page |\n| `extract` | Extract structured data from page |\n| `goto` | Navigate to a URL |\n| `scroll` | Scroll using semantic directions (up/down/left/right) |\n| `keys` | Press keyboard keys |\n| `navback` | Navigate back in history |\n| `screenshot` | Take a screenshot |\n| `think` | Agent reasoning/planning step |\n| `wait` | Wait for time or condition |\n| `search` | Web search (requires `useSearch: true` and `BROWSERBASE_API_KEY`) |\n\n</Tab>\n\n<Tab title=\"Hybrid Mode\">\n\n| Tool | Description |\n|------|-------------|\n| `click` | Click at specific coordinates |\n| `type` | Type text at coordinates |\n| `dragAndDrop` | Drag from one point to another |\n| `clickAndHold` | Click and hold at coordinates |\n| `fillFormVision` | Fill forms using vision/coordinates |\n| `act` | Perform semantic actions |\n| `ariaTree` | Get accessibility tree |\n| `extract` | Extract data from page |\n| `goto` | Navigate to URL |\n| `scroll` | Scroll using coordinates |\n| `keys` | Press keyboard keys |\n| `navback` | Navigate back |\n| `screenshot` | Take screenshot |\n| `think` | Agent reasoning step |\n| `wait` | Wait for time/condition |\n| `search` | Web search (requires `useSearch: true` and `BROWSERBASE_API_KEY`) |\n\n</Tab>\n</Tabs>\n\n#### Use Cases\n\n```typescript\n// Prevent the agent from taking screenshots during execution\nconst result = await agent.execute({\n  instruction: \"Fill out the contact form\",\n  excludeTools: [\"screenshot\"],\n});\n\n// Prevent the agent from extracting data\nconst result = await agent.execute({\n  instruction: \"Click through the signup flow\",\n  excludeTools: [\"extract\"],\n});\n\n// Disable web search capability\nconst result = await agent.execute({\n  instruction: \"Find information on the current page\",\n  excludeTools: [\"search\"],\n});\n```\n\n## Web Search\n\nEnable the `search` tool by setting `useSearch: true` in `agent.execute()`. This gives the agent the ability to perform web searches using the Browserbase Search API, which is useful when the agent needs to find URLs or gather information before navigating.\n\n<Note>Requires a valid Browserbase API key. Set `BROWSERBASE_API_KEY` in your environment, or pass `apiKey` in the Stagehand constructor.</Note>\n\n```typescript\nconst result = await agent.execute({\n  instruction: \"Find the latest pricing for Browserbase\",\n  useSearch: true,\n  maxSteps: 20,\n});\n```\n\n## Variables\n\nUse variables to pass sensitive data (like passwords, API keys, or personal information) to the agent without exposing the actual values to the LLM. The agent sees only variable names and descriptions, while the actual values are substituted at runtime.\n\n<Note>**Non-CUA agents only.** Variables require `experimental: true` and are not available with Computer Use Agents.</Note>\n\n### Basic Usage\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for variables\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com/login\");\n\nconst result = await agent.execute({\n  instruction: \"Log into the website using my credentials\",\n  maxSteps: 10,\n  variables: {\n    username: {\n      value: \"john@example.com\",\n      description: \"The user's email address for login\"\n    },\n    password: {\n      value: process.env.USER_PASSWORD,\n      description: \"The user's password for login\"\n    }\n  }\n});\n```\n\nVariables use the same type as `act()`. You can pass simple values or rich objects with descriptions:\n\n```typescript\n// Simple values (same format as act)\nvariables: {\n  username: \"john@example.com\",\n  password: \"secret123\",\n}\n\n// Rich values with descriptions (helps the agent understand context)\nvariables: {\n  username: { value: \"john@example.com\", description: \"The login email\" },\n  password: { value: \"secret123\", description: \"The login password\" },\n}\n```\n\n### How Variables Work\n\n1. **LLM receives descriptions only**: The agent sees variable names and descriptions in its system prompt, but never the actual values\n2. **Placeholder syntax**: The LLM uses `%variableName%` syntax when it needs to use a variable (e.g., \"type %password% into the password field\")\n3. **Runtime substitution**: Actual values are substituted just before the action executes\n4. **Secure logging**: Variable values are never logged or returned in tool outputs\n\n### Supported Tools\n\nVariables work with the following agent tools:\n\n<Tabs>\n<Tab title=\"DOM Mode\">\n\n| Tool | Usage |\n|------|-------|\n| `act` | Use `%variableName%` in the action description |\n| `fillForm` | Use `%variableName%` in field values |\n\n</Tab>\n\n<Tab title=\"Hybrid Mode\">\n\n| Tool | Usage |\n|------|-------|\n| `type` | Use `%variableName%` in the text to type |\n| `fillFormVision` | Use `%variableName%` in field values |\n| `act` | Use `%variableName%` in the action description |\n\n</Tab>\n</Tabs>\n\n### Cache Optimization\n\nVariables are cache-friendly by design:\n- Cache keys use only variable names, not values\n- Changing variable values (e.g., different passwords) won't invalidate cached executions\n- This enables efficient replay of the same workflow with different credentials\n\n### Best Practices\n\n<Tabs>\n<Tab title=\"Do this\">\n```typescript\n// Use variables for sensitive data\nvariables: {\n  apiKey: {\n    value: process.env.API_KEY,\n    description: \"API key for authentication\"\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Don't do this\">\n```typescript\n// Don't hardcode sensitive values in instructions\ninstruction: \"Log in with password 'secret123'\"\n```\n</Tab>\n</Tabs>\n\n<Tip>\nUse descriptive names and descriptions for variables. The LLM relies on the description to understand when and how to use each variable.\n</Tip>\n\n## MCP Integrations\n\nAgents can be enhanced with external tools and services through MCP (Model Context Protocol) integrations. This allows your agent to access external APIs and data sources beyond just browser interactions.\n\n<CodeGroup>\n```typescript Pass URL\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"openai/computer-use-preview\",\n        apiKey: process.env.OPENAI_API_KEY\n    },\n    integrations: [\n      `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n    ],\n    systemPrompt: `You have access to web search through Exa. Use it to find current information before browsing.`\n});\n\nawait agent.execute(\"Search for the best headphones of 2025 and go through checkout for the top recommendation\");\n```\n\n```typescript Create Connection\nimport { connectToMCPServer } from \"@browserbasehq/stagehand\";\n\nconst supabaseClient = await connectToMCPServer(\n  `https://server.smithery.ai/@supabase-community/supabase-mcp/mcp?api_key=${process.env.SMITHERY_API_KEY}`\n);\n\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"openai/computer-use-preview\",\n        apiKey: process.env.OPENAI_API_KEY\n    },\n    integrations: [supabaseClient],\n    systemPrompt: `You can interact with Supabase databases. Use these tools to store and retrieve data.`\n});\n\nawait agent.execute(\"Search for restaurants and save the first result to the database\");\n```\n</CodeGroup>\n\n<Tip>\nMCP integrations enable agents to be more powerful by combining browser automation with external APIs, databases, and services. The agent can intelligently decide when to use browser actions versus external tools.\n</Tip>\n\n## Streaming\n\nEnable streaming mode to receive incremental responses from the agent. This is useful for building real-time UIs that show the agent's reasoning as it progresses.\n\n<Warning>\n**Non-CUA agents only.** Streaming, callbacks, abort signals, and message continuation are only available when using the standard agent (without `mode: \"cua\"`). These features are not supported with Computer Use Agents.\n</Warning>\n\n<Note>These are experimental features. Set `experimental: true` in your Stagehand constructor to enable them.</Note>\n\n### Enabling Streaming Mode\n\nSet `stream: true` in the agent configuration to enable streaming:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for streaming\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n  stream: true, // Enable streaming mode\n});\n\nconst streamResult = await agent.execute({\n  instruction: \"Search for headphones on Amazon\",\n  maxSteps: 20,\n});\n\n// Stream the text output incrementally\nfor await (const delta of streamResult.textStream) {\n  process.stdout.write(delta);\n}\n\n// Get the final result after streaming completes\nconst finalResult = await streamResult.result;\nconsole.log(\"Completed:\", finalResult.completed);\n```\n\n### Stream Properties\n\nWhen streaming is enabled, `execute()` returns an `AgentStreamResult` with:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `textStream` | `AsyncIterable<string>` | Incremental text output from the agent |\n| `fullStream` | `AsyncIterable<StreamPart>` | All stream events including tool calls and messages |\n| `result` | `Promise<AgentResult>` | Final result after streaming completes |\n\n```typescript\n// Stream everything (tool calls, messages, etc.)\nfor await (const event of streamResult.fullStream) {\n  console.log(event);\n}\n```\n\n## Callbacks\n\nCallbacks let you hook into the agent's execution lifecycle to monitor progress, log events, or modify behavior.\n\n<Note>**Non-CUA agents only.** Callbacks require `experimental: true` and are not available with Computer Use Agents.</Note>\n\n### Available Callbacks\n\n<Tabs>\n<Tab title=\"Non-Streaming\">\n\nWhen `stream: false` (default), these callbacks are available:\n\n| Callback | Description |\n|----------|-------------|\n| `prepareStep` | Called before each LLM step to modify settings |\n| `onStepFinish` | Called when each step completes |\n\n```typescript\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nawait agent.execute({\n  instruction: \"Fill out the contact form\",\n  maxSteps: 10,\n  callbacks: {\n    prepareStep: async (stepContext) => {\n      console.log(`Starting step ${stepContext.stepNumber}`);\n      return stepContext; // Return modified or original context\n    },\n    onStepFinish: async (event) => {\n      console.log(`Step finished: ${event.finishReason}`);\n      if (event.toolCalls) {\n        for (const tc of event.toolCalls) {\n          console.log(`Tool called: ${tc.toolName}`);\n        }\n      }\n    },\n  },\n});\n```\n\n</Tab>\n\n<Tab title=\"Streaming\">\n\nWhen `stream: true`, additional callbacks are available:\n\n| Callback | Description |\n|----------|-------------|\n| `prepareStep` | Called before each LLM step to modify settings |\n| `onStepFinish` | Called when each step completes |\n| `onChunk` | Called for each stream chunk |\n| `onFinish` | Called when streaming completes |\n| `onError` | Called when an error occurs |\n| `onAbort` | Called when the stream is aborted |\n\n```typescript\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n  stream: true,\n});\n\nconst streamResult = await agent.execute({\n  instruction: \"Search for products\",\n  maxSteps: 15,\n  callbacks: {\n    onChunk: async (chunk) => {\n      // Called for each incremental chunk\n      console.log(\"Chunk received:\", chunk);\n    },\n    onStepFinish: async (event) => {\n      console.log(`Step completed: ${event.finishReason}`);\n    },\n    onFinish: (event) => {\n      console.log(\"Stream finished!\");\n      console.log(\"Total steps:\", event.steps.length);\n    },\n    onError: ({ error }) => {\n      console.error(\"Stream error:\", error);\n    },\n    onAbort: (event) => {\n      console.log(\"Stream aborted after\", event.steps.length, \"steps\");\n    },\n  },\n});\n\n// Don't forget to consume the stream\nfor await (const delta of streamResult.textStream) {\n  process.stdout.write(delta);\n}\n\nawait streamResult.result;\n```\n\n</Tab>\n</Tabs>\n\n<Warning>\nStreaming-only callbacks (`onChunk`, `onFinish`, `onError`, `onAbort`) will throw an error if used without `stream: true`. If you need these callbacks, enable streaming in your agent configuration.\n</Warning>\n\n## Abort Signal\n\nCancel agent execution at any time using an `AbortSignal`. This is useful for implementing timeouts or allowing users to stop long-running tasks.\n\n<Note>**Non-CUA agents only.** Abort signals require `experimental: true` and are not available with Computer Use Agents.</Note>\n\n### Basic Usage\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for abort signal\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst controller = new AbortController();\n\n// Set a 30 second timeout\nsetTimeout(() => controller.abort(), 30000);\n\ntry {\n  const result = await agent.execute({\n    instruction: \"Complete a complex multi-step task\",\n    maxSteps: 50,\n    signal: controller.signal,\n  });\n} catch (error) {\n  if (error.name === \"AgentAbortError\") {\n    console.log(\"Task was cancelled\");\n  }\n}\n```\n\n### Abort with Streaming\n\nAbort signals also work with streaming mode:\n\n```typescript\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n  stream: true,\n});\n\nconst controller = new AbortController();\n\nconst streamResult = await agent.execute({\n  instruction: \"Describe every element on the page\",\n  maxSteps: 50,\n  signal: controller.signal,\n  callbacks: {\n    onAbort: (event) => {\n      console.log(`Aborted after ${event.steps.length} steps`);\n    },\n  },\n});\n\n// Abort after receiving 10 chunks\nlet chunkCount = 0;\nfor await (const delta of streamResult.textStream) {\n  process.stdout.write(delta);\n  chunkCount++;\n  if (chunkCount >= 10) {\n    controller.abort();\n    break;\n  }\n}\n\n// The result promise will reject with AgentAbortError\ntry {\n  await streamResult.result;\n} catch (error) {\n  console.log(\"Stream was aborted:\", error.message);\n}\n```\n\n### Custom Abort Reasons\n\nYou can pass a reason when aborting:\n\n```typescript\ncontroller.abort(\"User cancelled the operation\");\n\n// The error message will include your reason\n// Error: \"User cancelled the operation\"\n```\n\n## Message Continuation\n\nContinue a conversation across multiple agent executions by passing the `messages` from a previous result. This is useful for multi-turn interactions or breaking complex tasks into steps while maintaining context.\n\n<Note>**Non-CUA agents only.** Message continuation requires `experimental: true` and is not available with Computer Use Agents.</Note>\n\n### Basic Continuation\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for message continuation\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com/products\");\n\n// First execution: search for products\nconst firstResult = await agent.execute({\n  instruction: \"Search for wireless headphones and note the top 3 results\",\n  maxSteps: 10,\n});\n\nconsole.log(\"First task:\", firstResult.message);\n\n// Continue with the same context: ask follow-up\nconst secondResult = await agent.execute({\n  instruction: \"Now filter by price under $100 and tell me which of those 3 are still available\",\n  maxSteps: 10,\n  messages: firstResult.messages, // Pass previous conversation\n});\n\nconsole.log(\"Follow-up:\", secondResult.message);\n\n// Continue further: take action based on conversation history\nconst thirdResult = await agent.execute({\n  instruction: \"Add the cheapest one to the cart\",\n  maxSteps: 10,\n  messages: secondResult.messages, // Chain the conversation\n});\n\nconsole.log(\"Final action:\", thirdResult.message);\n```\n\n## Structured Output\n\nDefine a Zod schema to receive structured data when the agent completes its task. This is useful when you need specific information extracted from the agent's execution, such as prices, dates, or other structured data.\n\n<Note>**Non-CUA agents only.** Structured output requires `experimental: true` and is not available with Computer Use Agents.</Note>\n\n<Tip>Use `.describe()` on schema fields to help the agent understand what data to extract.</Tip>\n\n<CodeGroup>\n```typescript Basic Usage\nimport { z } from \"zod\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for structured output\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://www.google.com/flights\");\n\nconst result = await agent.execute({\n  instruction: \"Find the cheapest flight from NYC to LA for next week\",\n  maxSteps: 20,\n  output: z.object({\n    price: z.string().describe(\"The price of the flight\"),\n    airline: z.string().describe(\"The airline name\"),\n    departureTime: z.string().describe(\"Departure time\"),\n    arrivalTime: z.string().describe(\"Arrival time\"),\n  }),\n});\n\n// Access the structured output\nconsole.log(result.output);\n// { price: \"$199\", airline: \"Delta\", departureTime: \"8:00 AM\", arrivalTime: \"11:30 AM\" }\n```\n\n```typescript Complex Schema\nconst result = await agent.execute({\n  instruction: \"Extract all items from the shopping cart\",\n  output: z.object({\n    items: z.array(z.object({\n      name: z.string().describe(\"Product name\"),\n      quantity: z.number().describe(\"Quantity in cart\"),\n      unitPrice: z.string().describe(\"Price per item\"),\n      totalPrice: z.string().describe(\"Total price for this item\"),\n    })).describe(\"List of items in the cart\"),\n    subtotal: z.string().describe(\"Cart subtotal before tax\"),\n    tax: z.string().optional().describe(\"Tax amount if shown\"),\n    total: z.string().describe(\"Final total\"),\n  }),\n});\n\nconsole.log(`Found ${result.output?.items.length} items in cart`);\nconsole.log(`Total: ${result.output?.total}`);\n```\n\n```typescript With Streaming\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n  stream: true,\n});\n\nconst streamResult = await agent.execute({\n  instruction: \"Find the top 3 search results\",\n  output: z.object({\n    results: z.array(z.object({\n      title: z.string().describe(\"The title of the search result\"),\n      url: z.string().url().describe(\"The URL of the search result\"),\n      snippet: z.string().describe(\"A brief description or snippet\"),\n    })).max(3).describe(\"Top 3 search results\"),\n  }),\n});\n\n// Stream the text output\nfor await (const delta of streamResult.textStream) {\n  process.stdout.write(delta);\n}\n\n// Get the structured output from the final result\nconst finalResult = await streamResult.result;\nconsole.log(finalResult.output?.results);\n```\n</CodeGroup>\n\n## Agent Execution Configuration\n\n<Warning>\nStagehand uses a 1288x711 viewport by default. Other viewport sizes may reduce performance. If you need to modify the viewport, you can edit in the [Browser Configuration](/v3/configuration/browser).\n</Warning>\n\nControl the maximum number of steps the agent can take to complete the task using the `maxSteps` parameter.\n\n<CodeGroup>\n```typescript TypeScript\n// Set maxSteps to control how many actions the agent can take\nawait agent.execute({\n  instruction: \"Sign me up for a library card\",\n  maxSteps: 15 // Agent will stop after 15 steps if task isn't complete\n});\n```\n\nFor complex tasks, increase the `maxSteps` limit and check task success.\n\n```typescript\n// Complex multi-step task requiring more actions\nconst result = await agent.execute({\n  instruction: \"Find and apply for software engineering jobs, filtering by remote work and saving 3 applications\",\n  maxSteps: 30, // Higher limit for complex workflows\n});\n\n// Check if the task completed successfully\nif (result.success === true) {\n  console.log(\"Task completed successfully!\");\n} else {\n  console.log(\"Task failed or was incomplete\");\n}\n```\n</CodeGroup>\n\n## Best Practices\n\nFollowing these best practices will improve your agent's success rate, reduce execution time, and minimize unexpected errors during task completion.\n\n### Start on the Right Page\nNavigate to your target page before executing tasks:\n\n<Tabs>\n<Tab title=\"Do this\">\n```typescript\nawait page.goto('https://github.com/browserbase/stagehand');\nawait agent.execute('Get me the latest PR on the stagehand repo');\n```\n</Tab>\n\n<Tab title=\"Don't do this\">\n```typescript\nawait agent.execute('Go to GitHub and find the latest PR on browserbase/stagehand');\n```\n</Tab>\n</Tabs>\n\n\n### Be Specific\nProvide detailed instructions for better results:\n\n<Tabs>\n<Tab title=\"Do this\">\n```typescript\nawait agent.execute(\"Find Italian restaurants in Brooklyn that are open after 10pm and have outdoor seating\");\n```\n</Tab>\n\n<Tab title=\"Don't do this\">\n```typescript\nawait agent.execute(\"Find a restaurant\");\n```\n</Tab>\n</Tabs>\n\n## Troubleshooting\n\n<AccordionGroup>\n\n\n<Accordion title=\"Agent is stopping before completing the task\">\n**Problem**: Agent stops before finishing the requested task\n\n**Solutions**:\n- Check if the agent is hitting the maxSteps limit (default is 20)\n- Increase maxSteps for complex tasks: `maxSteps: 30` or higher\n- Break very complex tasks into smaller sequential executions\n\n```typescript\n// Increase maxSteps for complex tasks\nawait agent.execute({\n  instruction: \"Complete the multi-page registration form with all required information\",\n  maxSteps: 40 // Increased limit for complex task\n});\n\n// Or break into smaller tasks with success checking\nconst firstResult = await agent.execute({\n  instruction: \"Fill out page 1 of the registration form\", \n  maxSteps: 15\n});\n\n// Only proceed if the first task was successful\nif (firstResult.success === true) {\n  await agent.execute({\n    instruction: \"Navigate to page 2 and complete remaining fields\",\n    maxSteps: 15\n  });\n} else {\n  console.log(\"First task failed, stopping execution\");\n}\n```\n</Accordion>\n\n<Accordion title=\"Agent is failing to click the proper elements\">\n**Problem**: Agent clicks on wrong elements or fails to interact with the correct UI components\n\n**Solutions**:\n- Ensure proper viewport size: Stagehand uses `1288x711` by default (optimal for Computer Use models)\n- Avoid changing viewport dimensions as other sizes may reduce performance\n</Accordion>\n\n\n</AccordionGroup>\n\n\n## Next steps\n\n<CardGroup cols={2}>\n<Card title=\"Act\" icon=\"play\" href=\"/v3/basics/act\">\n  Execute actions efficiently using observe results\n</Card>\n\n<Card title=\"Extract\" icon=\"download\" href=\"/v3/basics/extract\">\n  Extract structured data from observed elements\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/basics/evals.mdx",
    "content": "---\ntitle: Evaluations & Metrics\nsidebarTitle: Evals\ndescription: Monitor performance, optimize costs, and evaluate LLM effectiveness\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nEvaluations help you understand how well your automation performs, which models work best for your use cases, and how to optimize for cost and reliability. This guide covers both monitoring your own workflows and running comprehensive evaluations.\n\n## Why Evaluations Matter\n\n- **Performance Optimization**: Identify which models and settings work best for your specific automation tasks\n- **Cost Control**: Track token usage and inference time to optimize spending\n- **Reliability**: Measure success rates and identify failure patterns\n- **Model Selection**: Compare different LLMs on real-world tasks to make informed decisions\n\n<Card\n  title=\"Live Model Comparisons\"\n  icon=\"scale-balanced\"\n  href=\"https://www.stagehand.dev/evals\"\n>\n  View real-time performance comparisons across different LLMs on the [Stagehand Evals Dashboard](https://www.stagehand.dev/evals)\n</Card>\n\n## Comprehensive Evaluations\n\nEvaluations help you systematically test and improve your automation workflows. Stagehand provides both built-in evaluations and tools to create your own.\n\n### Evals CLI\n![Evals CLI](/media/evals-cli.png)\n\n<Tip>\nTo run evals, you'll need to clone the [Stagehand repo](https://github.com/browserbase/stagehand) and set up the CLI.\n\nWe recommend using [Braintrust](https://www.braintrust.dev/docs/) to help visualize evals results and metrics.\n</Tip>\n\nThe Stagehand CLI provides a powerful interface for running evaluations. You can run specific evals, categories, or external benchmarks with customizable settings.\n\nEvals are grouped into:\n1. **Act Evals** - These are evals that test the functionality of the `act` method.\n2. **Extract Evals** - These are evals that test the functionality of the `extract` method.\n3. **Observe Evals** - These are evals that test the functionality of the `observe` method.\n4. **Combination Evals** - These are evals that test the functionality of the `act`, `extract`, and `observe` methods together.\n5. **Experimental Evals** - These are experimental custom evals that test the functionality of the stagehand primitives.\n6. **Agent Evals** - These are evals that test the functionality of `agent`.\n7. **(NEW) External Benchmarks** - Run external benchmarks like WebBench, GAIA, WebVoyager, OnlineMind2Web, and OSWorld.\n\n#### Installation\n\n<Steps> \n<Step title=\"Install Dependencies\">\n```bash\n# From the stagehand root directory\npnpm install\n```\n</Step>\n\n<Step title=\"Build the CLI\">\n```bash\npnpm run build:cli\n```\n</Step>\n\n<Step title=\"Verify Installation\">\n```bash\nevals help\n```\n</Step>\n</Steps>\n\n#### CLI Commands and Options\n\n##### Basic Commands\n\n```bash\n# Run all evals\nevals run all\n\n# Run specific category\nevals run act\nevals run extract\nevals run observe\nevals run agent\n\n# Run specific eval\nevals run extract/extract_text\n\n# List available evals\nevals list\nevals list --detailed\n\n# Configure defaults\nevals config\nevals config set env browserbase\nevals config set trials 5\n```\n\n##### Command Options\n\n- **`-e, --env`**: Environment (`local` or `browserbase`)\n- **`-t, --trials`**: Number of trials per eval (default: 3)\n- **`-c, --concurrency`**: Max parallel sessions (default: 10)\n- **`-m, --model`**: Model override\n- **`-p, --provider`**: Provider override\n- **`--api`**: Use Stagehand API instead of SDK\n\n##### Running External Benchmarks\n\nThe CLI supports several industry-standard benchmarks:\n\n```bash\n# WebBench with filters\nevals run benchmark:webbench -l 10 -f difficulty=easy -f category=READ\n\n# GAIA benchmark\nevals run b:gaia -s 100 -l 25 -f level=1\n\n# WebVoyager\nevals run b:webvoyager -l 50\n\n# OnlineMind2Web\nevals run b:onlineMind2Web\n\n# OSWorld\nevals run b:osworld -f source=Mind2Web\n```\n\n#### Configuration Files\n\nYou can view the specific evals in [`evals/tasks`](https://github.com/browserbase/stagehand/tree/main/packages/evals/tasks). Each eval is grouped into eval categories based on [`evals/evals.config.json`](https://github.com/browserbase/stagehand/blob/main/evals/evals.config.json).\n\n\n#### Viewing eval results\n![Eval results](/images/evals.png)\n\nEval results are viewable on Braintrust. You can view the results of a specific eval by going to the Braintrust URL specified in the terminal when you run `npm run evals`.\n\nBy default, each eval will run five times per model. The \"Exact Match\" column shows the percentage of times the eval was correct. The \"Error Rate\" column shows the percentage of times the eval errored out.\n\nYou can use the Braintrust UI to filter by model/eval and aggregate results across all evals.\n\n## Creating Custom Evaluations\n\n### Step-by-Step Guide\n\n<Steps>\n<Step title=\"Create Evaluation File\">\nCreate a new file in `evals/tasks/your-eval.ts`:\n\n```typescript\nimport { EvalTask } from '../types';\n\nexport const customEvalTask: EvalTask = {\n  name: 'custom_task_name',\n  description: 'Test specific automation workflow',\n  \n  // Test setup\n  setup: async ({ page }) => {\n    await page.goto('https://example.com');\n  },\n  \n  // The actual test\n  task: async ({ stagehand, page }) => {\n    // Your automation logic\n    await stagehand.act({ action: 'click the login button' });\n    const result = await stagehand.extract({ \n      instruction: 'Get the user name',\n      schema: { username: 'string' }\n    });\n    return result;\n  },\n  \n  // Validation\n  validate: (result, expected) => {\n    return result.username === expected.username;\n  },\n  \n  // Test cases\n  testCases: [\n    {\n      input: { /* test input */ },\n      expected: { username: 'john_doe' }\n    }\n  ],\n  \n  // Evaluation criteria\n  scoring: {\n    exactMatch: true,\n    timeout: 30000,\n    retries: 2\n  }\n};\n```\n</Step>\n\n<Step title=\"Add to Configuration\">\nUpdate `evals/evals.config.json`:\n\n```json\n{\n  \"categories\": {\n    \"custom\": [\"custom_task_name\"],\n    \"existing_category\": [\"custom_task_name\"]\n  }\n}\n```\n</Step>\n\n<Step title=\"Run Your Evaluation\">\n```bash\n# Test your custom evaluation\nevals run custom_task_name\n\n# Run the entire custom category\nevals run custom\n\n# Run with specific settings\nevals run custom_task_name -e browserbase -t 5 -m gpt-4o\n```\n</Step>\n</Steps>\n\n\n## Best Practices for Custom Evals\n\n<AccordionGroup>\n<Accordion title=\"Test Design Principles\">\n- **Atomic**: Each test should validate one specific capability\n- **Deterministic**: Tests should produce consistent, measurable results\n- **Realistic**: Use real-world scenarios and websites\n- **Measurable**: Define clear success/failure criteria\n</Accordion>\n\n<Accordion title=\"Performance Optimization\">\n- **Parallel Execution**: Design tests to run independently\n- **Resource Management**: Clean up after each test\n- **Timeout Handling**: Set appropriate timeouts for operations\n- **Error Recovery**: Handle failures gracefully\n</Accordion>\n\n<Accordion title=\"Data Quality\">\n- **Ground Truth**: Establish reliable expected outcomes\n- **Edge Cases**: Test boundary conditions and error scenarios\n- **Statistical Significance**: Run multiple iterations for reliability\n- **Version Control**: Track changes to test cases over time\n</Accordion>\n</AccordionGroup>\n\n### Troubleshooting Evaluations\n<AccordionGroup>\n<Accordion title=\"Evaluation Timeouts\">\n**Symptoms**: Tests fail with timeout errors\n\n**Solutions**:\n- Increase timeout in `taskConfig.ts`\n- Use faster models (Gemini 2.5 Flash, Claude Haiku 4.5)\n- Optimize test scenarios to be less complex\n- Check network connectivity to LLM providers\n</Accordion>\n\n<Accordion title=\"Inconsistent Results\">\n**Symptoms**: Same test passes/fails randomly\n\n**Solutions**:\n- Set temperature to 0 for deterministic outputs\n- Increase repetitions for statistical significance\n- Use more capable models for complex tasks\n- Check for dynamic website content affecting tests\n</Accordion>\n\n<Accordion title=\"High Evaluation Costs\">\n**Symptoms**: Token usage exceeding budget\n\n**Solutions**:\n- Use cost-effective models (Gemini 2.5 Flash, Claude Haiku 4.5)\n- Reduce repetitions for initial testing\n- Focus on specific evaluation categories\n- Use local browser environment to reduce Browserbase costs\n</Accordion>\n\n<Accordion title=\"Braintrust Integration Issues\">\n**Symptoms**: Results not uploading to dashboard\n\n**Solutions**:\n- Check Braintrust API key configuration\n- Verify internet connectivity\n- Update Braintrust SDK to latest version\n- Check project permissions in Braintrust dashboard\n</Accordion>\n</AccordionGroup>"
  },
  {
    "path": "packages/docs/v3/basics/extract.mdx",
    "content": "---\ntitle: Extract\ndescription: Extract structured data from a webpage\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## What is `extract()`?\n\n```typescript\nawait stagehand.extract(\"extract the name of the repository\");\n```\n\n`extract()` grabs structured data from a webpage. You can define your schema with [Zod](https://github.com/colinhacks/zod) (TypeScript) or JSON. If you don't want to define a schema, you can also call `extract` with just a [natural language prompt](#instruction-only), or call `extract` [with no parameters](#no-parameters).\n\n## Why use `extract()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Structured\" icon=\"brackets-curly\" href=\"#basic-schema\">\n    Turn messy webpage data into clean objects that follow a schema.\n  </Card>\n  <Card title=\"Resilient\" icon=\"dumbbell\" href=\"#extract-with-context\">\n    Build resilient extractions that don't break when the website changes\n  </Card>\n</CardGroup>\n\n## Return value\n\nWhen you use `extract()`, Stagehand will return a `Promise<ExtractResult>` with the following structure:\n<Tabs>\n<Tab title=\"Basic Schema\">\n\nWhen extracting with a schema, the return type is inferred from your Zod schema:\n\n```typescript\nconst result = await stagehand.extract(\n  \"extract product details\",\n  z.object({\n    name: z.string(),\n    price: z.number(),\n    inStock: z.boolean()\n  })\n);\n```\n\n**Example result:**\n```typescript\n{\n  name: \"Wireless Mouse\",\n  price: 29.99,\n  inStock: true\n}\n```\n\n</Tab>\n<Tab title=\"Array\">\n\nWhen extracting an array, you get an array of objects:\n\n```typescript\nconst apartments = await stagehand.extract(\n  \"extract all apartment listings\",\n  z.array(\n    z.object({\n      address: z.string(),\n      price: z.string(),\n      sqft: z.number()\n    })\n  )\n);\n```\n\n**Example result:**\n```typescript\n[\n  {\n    address: \"123 Main St\",\n    price: \"$1,200/mo\",\n    sqft: 750\n  },\n  {\n    address: \"456 Oak Ave\",\n    price: \"$1,500/mo\",\n    sqft: 900\n  }\n]\n```\n\n</Tab>\n<Tab title=\"Primitive\">\n\nWhen extracting a single primitive value:\n\n```typescript\nconst price = await stagehand.extract(\n  \"extract the price\",\n  z.number()\n);\n```\n\n**Example result:**\n```typescript\n19.99\n```\n\nYou can also extract strings, booleans, etc.:\n\n```typescript\nconst url = await stagehand.extract(\n  \"extract the contact page link\",\n  z.string().url()\n);\n```\n\n</Tab>\n<Tab title=\"Instruction Only\">\n\nWhen calling with just an instruction (no schema):\n\n```typescript\nconst result = await stagehand.extract(\"extract the repository name\");\n```\n\n**Example result:**\n```typescript\n{\n  extraction: \"stagehand\"\n}\n```\n\n</Tab>\n<Tab title=\"No Parameters\">\n\nWhen calling with no parameters:\n\n```typescript\nconst result = await stagehand.extract();\n```\n\n**Example result:**\n```typescript\n{\n  pageText: \"Accessibility Tree:\\n[0-2] RootWebArea: Page Title\\n  [0-37] scrollable\\n    [0-118] body\\n      ...\"\n}\n```\n\nThis returns the accessibility tree representation of the page without LLM processing.\n\n</Tab>\n</Tabs>\n\n## Advanced Configuration\n\nYou can pass additional options to configure the model, timeout, and selector scope:\n```typescript\nconst result = await stagehand.extract(\"extract the repository name\", {\n  model: \"anthropic/claude-sonnet-4-5\",\n  timeout: 30000,\n  selector: \"//header\" // Focus on specific area\n});\n```\n\n### Server-side Caching\n\n<Note>\n`serverCache` only works when running with `env: \"BROWSERBASE\"`. It has no effect in local environments.\n</Note>\n\nWhen running on Browserbase, Stagehand automatically caches `extract()` results server-side. Repeated calls with the same inputs return instantly without consuming LLM tokens. Caching is enabled by default and can be controlled globally on the constructor or overridden per call:\n\n```typescript\n// Disable server-side caching for the entire instance\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  serverCache: false,\n});\n\n// Or disable it for a single call\nconst data = await stagehand.extract(\"extract the repository name\", { serverCache: false });\n\n// Check whether a result was served from cache\nconst result = await stagehand.extract(\"extract the page title\");\nconsole.log(result.cacheStatus); // \"HIT\" or \"MISS\"\n```\n\n### Targeted Extract\n\nPass a selector to `extract` to target a specific element on the page.\n<Tip>\nThis helps reduce the context passed to the LLM, optimizing token usage/speed and improving accuracy.\n</Tip>\n```typescript\nconst tableData = await stagehand.extract(\n  \"Extract the values of the third row\",\n  z.object({\n    values: z.array(z.string())\n  }),\n  {\n    // xPath or CSS selector\n    selector: \"xpath=/html/body/div/table/\" \n  }\n);\n```\n\n\n## Best practices\n\n\n### Extract with Context\n\nYou can provide additional context to your schema to help the model extract the data more accurately.\n\n```typescript\nconst apartments = await stagehand.extract(\n  \"Extract ALL the apartment listings and their details, including address, price, and square feet.\",\n  z.array(\n    z.object({\n      address: z.string().describe(\"the address of the apartment\"),\n      price: z.string().describe(\"the price of the apartment\"),\n      square_feet: z.string().describe(\"the square footage of the apartment\"),\n    })\n  )\n);\n```\n\n### Link Extraction\n<Note>\nTo extract links or URLs, define the relevant field as `z.string().url()`.\n</Note>\n\nHere is how an `extract` call might look for extracting a link or URL. This also works for image links.\n\n```typescript\nconst contactLink = await stagehand.extract(\n  \"extract the link to the 'contact us' page\",\n  z.string().url() // note the usage of z.string().url() for URL validation\n);\n\nconsole.log(\"the link to the contact us page is: \", contactLink);\n```\n\n<Tip>\nInside Stagehand, extracting links works by asking the LLM to select an ID. Stagehand looks up that ID in a mapping of IDs -> URLs. When logging the LLM trace, you should expect to see IDs. The actual URLs will be included in the final `ExtractResult`.\n</Tip>\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Empty or partial results\">\n**Problem**: `extract()` returns empty or incomplete data\n\n**Solutions**:\n- **Check your instruction clarity**: Make sure your instruction is specific and describes exactly what data you want to extract\n- **Verify the data exists**: Use `stagehand.observe()` first to confirm the data is present on the page\n- **Wait for dynamic content**: If the page loads content dynamically, use `stagehand.act(\"wait for the content to load\")` before extracting\n\n**Solution: Wait for content before extracting**\n```typescript\n// Wait for content before extracting\nawait stagehand.act(\"wait for the product listings to load\");\nconst products = await stagehand.extract(\n  \"extract all product names and prices\",\n  z.array(z.object({\n    name: z.string(),\n    price: z.string()\n  }))\n);\n```\n</Accordion>\n\n<Accordion title=\"Schema validation errors\">\n**Problem**: Getting schema validation errors or type mismatches\n\n**Solutions**:\n- **Use optional fields**: Make fields optional with `z.optional()` if the data might not always be present\n- **Use flexible types**: Consider using `z.string()` instead of `z.number()` for prices that might include currency symbols\n- **Add descriptions**: Use `.describe()` to help the model understand field requirements\n\n**Solution: More flexible schema**\n```typescript\nconst schema = z.object({\n  price: z.string().describe(\"price including currency symbol, e.g., '$19.99'\"),\n  availability: z.string().optional().describe(\"stock status if available\"),\n  rating: z.number().optional()\n});\n```\n</Accordion>\n\n<Accordion title=\"Inconsistent results\">\n**Problem**: Extraction results vary between runs\n\n**Solutions**:\n- **Be more specific in instructions**: Instead of \"extract prices\", use \"extract the numerical price value for each item\"\n- **Use context in schema descriptions**: Add field descriptions to guide the model\n- **Combine with observe**: Use `stagehand.observe()` to understand the page structure first\n\n**Solution: Validate with observe first**\n```typescript\n// First observe to understand the page structure\nconst elements = await stagehand.observe(\"find all product listings\");\nconsole.log(\"Found elements:\", elements.map(e => e.description));\n\n// Then extract with specific targeting\nconst products = await stagehand.extract(\n  \"extract name and price from each product listing shown on the page\",\n  z.array(z.object({\n    name: z.string().describe(\"the product title or name\"),\n    price: z.string().describe(\"the price as displayed, including currency\")\n  }))\n);\n```\n</Accordion>\n\n<Accordion title=\"Performance issues\">\n**Problem**: Extraction is slow or timing out\n\n**Solutions**:\n- **Reduce scope**: Extract smaller chunks of data in multiple calls rather than everything at once\n- **Use targeted instructions**: Be specific about which part of the page to focus on\n- **Consider pagination**: For large datasets, extract one page at a time\n- **Increase timeout**: Use `timeoutMs` parameter for complex extractions\n\n**Solution: Break down large extractions**\n```typescript\n// Instead of extracting everything at once\nconst allData = [];\nconst pageNumbers = [1, 2, 3, 4, 5];\n\nfor (const pageNum of pageNumbers) {\n  await stagehand.act(`navigate to page ${pageNum}`);\n\n  const pageData = await stagehand.extract(\n    \"extract product data from the current page only\",\n    z.array(z.object({\n      name: z.string(),\n      price: z.number()\n    })),\n    { timeout: 60000 } // 60 second timeout\n  );\n\n  allData.push(...pageData);\n}\n```\n</Accordion>\n</AccordionGroup>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n  <Card title=\"Act\" icon=\"play\" href=\"/v3/basics/act\">\n    Execute actions efficiently\n  </Card>\n\n  <Card title=\"Observe\" icon=\"magnifying-glass\" href=\"/v3/basics/observe\">\n    Analyze pages and preview actions\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/basics/observe.mdx",
    "content": "---\ntitle: Observe\nsidebarTitle: Observe\ndescription: 'Discover and plan executable actions on any web page'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## What is `observe()`?\n\n```typescript\nawait stagehand.observe(\"find the login button\");\n```\n\n`observe()` discovers actionable elements on a page and returns structured actions you can execute or validate before acting. Use it to explore pages, plan multi-step workflows, cache actions, and validate elements before acting.\n\n## Why use `observe()`?\n\n<CardGroup cols={2}>\n  <Card title=\"Explore\" icon=\"compass\" href=\"#using-observe\">\n    Discover what's possible on a page—find buttons, forms, links, and interactive elements\n  </Card>\n  <Card title=\"Plan\" icon=\"map\" href=\"#plan-then-execute\">\n    Map out multi-step workflows by discovering all required actions upfront\n  </Card>\n  <Card title=\"Cache\" icon=\"database\" href=\"/v3/best-practices/caching\">\n    Store discovered actions to skip LLM calls and speed up repeated workflows\n  </Card>\n  <Card title=\"Validate\" icon=\"check\" href=\"#validate-before-acting\">\n    Verify elements exist and check their properties before performing critical actions\n  </Card>\n</CardGroup>\n\n## Using `observe()`\n\nUse `observe()` to discover actionable elements on a page. Here's how to find a button:\n\n```typescript\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\nconst actions = await stagehand.observe(\"find the learn more button\");\n```\n\n<Note>\n**iFrame and Shadow DOM Support** Stagehand automatically handles iFrame traversal and shadow DOM elements without requiring additional configuration.\n</Note>\n\n<Accordion title=\"Common use cases\">\n\n| Use Case | Example instruction |\n|----------|---------------------|\n| Find buttons | `find the submit button` |\n| Locate forms | `find all input fields in the form` |\n| Discover links | `find navigation links` |\n| Identify tables | `find the pricing table` |\n| Map workflows | `find all checkout steps` |\n| Validate elements | `find the delete account button` |\n\n</Accordion>\n\n\n### Return value of `observe()`?\nWhen you use `observe()`, Stagehand will return a `Promise<Action[]>` with the following structure:\n``` typescript\n[\n  {\n    description: 'Learn more button',\n    method: 'click',\n    arguments: [],\n    selector: 'xpath=/html[1]/body[1]/shadow-demo[1]//div[1]/button[1]'\n  }\n]\n```\n\n<Tabs>\n<Tab title=\"Do this\">\nUse specific, descriptive instructions.\n\n```typescript\n// Clear and specific\nawait stagehand.observe(\"find the primary call-to-action button in the hero section\");\nawait stagehand.observe(\"find all input fields in the checkout form\");\nawait stagehand.observe(\"find the delete account button in settings\");\n```\n</Tab>\n\n<Tab title=\"Don't do this\">\nAvoid vague or data-oriented queries.\n\n```typescript\n// Too vague\nawait stagehand.observe(\"find buttons\");\n\n// Use extract() for data instead\nawait stagehand.observe(\"what is the page title?\");\n```\n</Tab>\n</Tabs>\n\n## Advanced Configuration\n\nYou can pass additional options to configure the model, timeout, and selector scope:\n\n```typescript\n// Custom model configuration\nconst actions = await stagehand.observe(\"find navigation links\", {\n  model: \"openai/gpt-4o\",\n  timeout: 30000,\n  selector: \"//header\" // Focus on specific area\n});\n```\n\n### Server-side Caching\n\n<Note>\n`serverCache` only works when running with `env: \"BROWSERBASE\"`. It has no effect in local environments.\n</Note>\n\nWhen running on Browserbase, Stagehand automatically caches `observe()` results server-side. Repeated calls with the same inputs return instantly without consuming LLM tokens. Caching is enabled by default and can be controlled globally on the constructor or overridden per call:\n\n```typescript\n// Disable server-side caching for the entire instance\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  serverCache: false,\n});\n\n// Or disable it for a single call\nconst actions = await stagehand.observe(\"find the login button\", { serverCache: false });\n```\n\n<Note>\n  `observe()` does not currently expose a `cacheStatus` field. To check whether an `observe()` call was served from cache, use the [Browserbase session replay dashboard](https://docs.browserbase.com/features/observability#stagehand) or inspect the session logs.\n</Note>\n\n### Using with Custom Pages\n\nYou can use `observe()` with pages from other browser automation libraries like Puppeteer, Playwright, or Patchright by passing the `page` option:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport puppeteer from \"puppeteer-core\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n});\nawait stagehand.init();\n\n// Connect with Puppeteer\nconst browser = await puppeteer.connect({\n  browserWSEndpoint: stagehand.connectURL(),\n  defaultViewport: null,\n});\n\nconst pages = await browser.pages();\nconst customPage = pages[0];\n\nawait customPage.goto(\"https://www.example.com/products\");\n\n// Use observe with the custom Puppeteer page\nconst actions = await stagehand.observe(\"find all product cards\", {\n  page: customPage\n});\n```\n\nThis works with:\n- **Puppeteer**: Pass Puppeteer Page objects\n- **Playwright**: Pass Playwright Page objects\n- **Patchright**: Pass Patchright Page objects\n- **Stagehand Context Pages**: Access pages via `stagehand.context.pages()` (default)\n\n<Card title=\"Complete API Reference\" icon=\"book\" href=\"/v3/references/observe\">\n  See the full `observe()` reference for detailed parameter documentation, return values, and advanced examples.\n</Card>\n\n## Best practices\n\n### Plan then execute\n\nDiscover all actions once, then execute without additional LLM calls. This approach is 2-3x faster than separate `act()` calls.\n\n```typescript\nconst formFields = await stagehand.observe(\"find all form input fields\");\n\nfor (const field of formFields) {\n  await stagehand.act(field); // No LLM call\n}\n```\n\n<Card title=\"Analyze pages with observe()\" icon=\"magnifying-glass\" href=\"/v3/references/observe\">\n  Complete guide to planning actions with `observe()`.\n</Card>\n\n### Scope extractions\n\nUse `observe()` to narrow extraction scope and reduce token usage by up to 10x.\n\n```typescript\nconst [table] = await stagehand.observe(\"find the pricing table\");\n\nconst pricing = await stagehand.extract({\n  instruction: \"extract all pricing tiers\",\n  schema: PricingSchema,\n  selector: table.selector\n});\n```\n\n<Card title=\"Extract structured data\" icon=\"table\" href=\"/v3/basics/extract\">\n  Learn how to use `observe()` with `extract()` for precise data extraction.\n</Card>\n\n### Validate before acting\n\nCheck elements exist and verify their properties before performing critical operations.\n\n```typescript\nconst [deleteButton] = await stagehand.observe(\"find the delete account button\");\n\nif (deleteButton?.method === \"click\") {\n  await stagehand.act(deleteButton);\n} else {\n  throw new Error(\"Delete button not found\");\n}\n```\n\n<Card title=\"Execute actions with act()\" icon=\"play\" href=\"/v3/basics/act\">\n  Learn how to execute observed actions reliably.\n</Card>\n\n### Cache observed actions\n\nStore and reuse observed actions to eliminate redundant LLM calls. Build a simple cache:\n\n```typescript\nconst actionCache = new Map<string, Action[]>();\n\nasync function cachedObserve(instruction: string) {\n  if (actionCache.has(instruction)) {\n    return actionCache.get(instruction)!;\n  }\n\n  const actions = await stagehand.observe(instruction);\n  actionCache.set(instruction, actions);\n  return actions;\n}\n```\n\n<Card title=\"Complete caching guide\" icon=\"database\" href=\"/v3/best-practices/caching\">\n  Learn advanced caching techniques and patterns for optimal performance.\n</Card>\n\n## Troubleshooting\n\n<AccordionGroup>\n\n<Accordion title=\"No elements found\">\n**Problem**: `observe()` returns empty array\n\n**Solutions**:\n- Verify the element exists on the page\n- Use more specific instructions (e.g., \"find the blue submit button\" instead of \"find button\")\n- Ensure page has fully loaded before calling `observe()`\n- Enable verbose logging in Stagehand configuration to inspect detection behavior\n\n```typescript\n// Check page state before observing\nconst page = stagehand.context.pages()[0];\nawait page.waitForLoadState('domcontentloaded');\n\nconst actions = await stagehand.observe(\"find the submit button\");\n\nif (actions.length === 0) {\n  console.log(\"No elements found, trying alternative instruction\");\n  const altActions = await stagehand.observe(\"find the button at the bottom of the form\");\n}\n```\n</Accordion>\n\n<Accordion title=\"Inaccurate results\">\n**Problem**: Descriptions or selectors don't match actual elements\n\n**Solutions**:\n- Use more capable models—check [model evals](https://stagehand.dev/evals) for recommendations\n- Provide more context in your instruction (e.g., \"find the submit button in the checkout form\")\n- Enable verbose logging and `logInferenceToFile` in Stagehand configuration to inspect LLM reasoning\n\n```typescript\n// More specific instructions improve accuracy\n// Instead of:\nawait stagehand.observe(\"find the button\");\n\n// Use context:\nawait stagehand.observe(\"find the red 'Delete' button in the user settings panel\");\n```\n</Accordion>\n\n<Accordion title=\"Wrong method suggested\">\n**Problem**: The `method` field has an unexpected value\n\n**Solutions**:\n- Validate the method before using it: `if (action.method === \"click\") { ... }`\n- Check [supported actions](/v3/basics/act) for valid method names\n- Override with a specific method when needed: `await stagehand.act({ ...action, method: \"click\" })`\n\n```typescript\nconst [action] = await stagehand.observe(\"find the submit button\");\n\n// Validate method before acting\nconst validMethods = [\"click\", \"fill\", \"type\", \"press\"];\nif (action && validMethods.includes(action.method || \"\")) {\n  await stagehand.act(action);\n} else {\n  console.warn(`Unexpected method: ${action?.method}`);\n}\n```\n</Accordion>\n\n</AccordionGroup>\n\n## Next steps\n\n<CardGroup cols={2}>\n  <Card title=\"Execute actions with act()\" icon=\"play\" href=\"/v3/basics/act\">\n    Use `act()` to execute discovered actions reliably.\n  </Card>\n\n  <Card title=\"Extract structured data\" icon=\"table\" href=\"/v3/basics/extract\">\n    Combine `observe()` with `extract()` for precise data extraction.\n  </Card>\n\n  <Card title=\"Caching actions\" icon=\"bolt\" href=\"/v3/best-practices/caching\">\n    Build action caches to eliminate redundant LLM calls.\n  </Card>\n\n  <Card title=\"Complete API Reference\" icon=\"book\" href=\"/v3/references/observe\">\n    Full `observe()` reference with detailed parameter documentation.\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/best-practices/agent-fallbacks.mdx",
    "content": "---\ntitle: Agent Fallbacks\ndescription: \"A failsafe when unexpected page changes add extra steps\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## When to use\n\nUse an agent fallback as a failsafe when a one step action unexpectedly becomes a multi-step flow.\n\n## How it works\n\n1. [`act()`](/v3/basics/act) is attempted for the direct action\n2. If it fails, [`agent()`](/v3/basics/agent) figures out the new path\n3. Agent completes all needed steps (open menu → click button)\n\n### Example scenario\n\n**Before**: Sign in button was in the header  \n**After**: Sign in now requires: Click account menu → Click \"Sign in\" option\n\nA single `act(\"click sign in\")` can't handle this change. The agent fallback can discover and execute both steps.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\ntry {\n  await stagehand.act(\"click the 'Sign In' button\");\n} catch (err) {\n  console.log(\"Agent fallback triggered\");\n\n  const agent = stagehand.agent({\n    model: \"anthropic/claude-sonnet-4-20250514\",\n    systemPrompt: \"You are a helpful assistant that can use a web browser.\",\n  });\n\n  const result = await agent.execute({\n    instruction: \"Find and click Sign In button\",\n    maxSteps: 10,\n  });\n\n  console.log(result.success ? \"Agent fallback success\" : \"Agent fallback failed\");\n\n  if (!result.success) throw err;\n}\n```\n\nSee all available agent models on the [models page](/v3/configuration/models#agent-models-with-cua-support).\n"
  },
  {
    "path": "packages/docs/v3/best-practices/caching.mdx",
    "content": "---\ntitle: Caching Actions\ndescription: Cache actions automatically to reduce costs and improve performance\n---\n\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\nStagehand supports two caching strategies to reduce LLM costs and speed up your automations: **Browserbase Cache** and **Local Cache**. They serve different use cases and can be used independently or together.\n\n---\n\n## Browserbase Cache\n\nBrowserbase Cache is a managed, server-side caching layer built into the Stagehand API. When you run Stagehand with `env: \"BROWSERBASE\"`, every `act()` call is automatically cached on Browserbase's servers. Repeated calls with the same inputs return instantly without consuming any LLM tokens. You don't need to configure anything to start benefiting.\n\nThe cache key is generated from the instruction, page content, and options you pass. On a cache hit, the response is returned directly from the server with no LLM inference and no token cost. You can inspect cache behavior via the `cacheStatus` field returned by `act()`. Check out the [Browserbase blog](https://www.browserbase.com/blog/stagehand-caching) for more details on how it works under the hood.\n\n### Disabling on the Constructor\n\nPass `serverCache: false` to disable caching for all requests made by that instance:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  serverCache: false,\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\n// Cache is disabled, always hits the LLM\nawait stagehand.act(\"click the login button\");\n```\n\n### Disabling per Call\n\nOverride the instance setting for a single call by passing `serverCache: false` in the options:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" }); // caching on by default\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\n// This call skips the cache\nawait stagehand.act(\"click the login button\", { serverCache: false });\n\n// This call uses the cache as normal\nawait stagehand.act(\"submit the form\");\n```\n\n### Inspecting Cache Status\n\n`act()` returns a `cacheStatus` field you can use to verify whether a result was served from cache:\n\n```typescript\nconst actResult = await stagehand.act(\"click the login button\");\nconsole.log(actResult.cacheStatus); // \"HIT\" or \"MISS\"\n```\n\n### Limitations\n\n- The page URL factors in to the cache key. If the action is being made on a page with a dynamic URL, caching may not work as expected. We do filter out certain query parameters like referral trackers and analytics, but we don't catch everything just yet.\n- If the page content or structure changes, the action won't get a cache `HIT` and the LLM will be called. The subsequent actions will attempt to hit the resulting cache entry.\n\n---\n\n## Local Cache\n\nLocal Cache writes action results to your filesystem so they persist across script runs. It works in both `LOCAL` and `BROWSERBASE` environments. When you specify a `cacheDir`, Stagehand saves every action and agent step to a local file on first run, then replays those cached actions on subsequent runs with no LLM calls, no token cost, and no network round-trip to Browserbase.\n\nThis is especially useful for:\n\n- **CI/CD pipelines** - commit your cache directory to version control for consistent, deterministic runs across environments\n- **Local development** - iterate on automations without burning tokens on repeated runs\n- **Cross-machine sharing** - cache files are portable and can be shared across machines\n\n### Caching with `act()`\n\nCache actions from `act()` by specifying a cache directory in your Stagehand constructor.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"act-cache\", // Specify a cache directory\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-same-proc-scroll/\");\n\n// First run: uses LLM inference and caches\n// Subsequent runs: reuses cached action\nawait stagehand.act(\"scroll to the bottom of the iframe\");\n\n// Variables work with caching too\nawait stagehand.act(\"fill the username field with %username%\", {\n  variables: {\n    username: \"fakeUsername\",\n  },\n});\n```\n\n### Caching with `agent()`\n\nCache agent actions (including Computer Use Agent actions) the same way. Just specify a `cacheDir`. The cache key is automatically generated based on the instruction, start URL, agent execution options, and agent configuration. Subsequent runs with the same parameters will reuse cached actions.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"agent-cache\", // Specify a cache directory\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://browserbase.github.io/stagehand-eval-sites/sites/drag-drop/\");\n\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n  },\n  systemPrompt: \"You are a helpful assistant that can use a web browser.\",\n});\n\nawait page.goto(\"https://play2048.co/\");\n\n// First run: uses LLM inference and caches\n// Subsequent runs: reuses cached actions\nconst result = await agent.execute({\n  instruction: \"play a game of 2048\",\n  maxSteps: 20,\n});\n\nconsole.log(JSON.stringify(result, null, 2));\n```\n\n### Cache Directory Organization\n\nYou can organize your caches by using different directory names for different workflows:\n\n```typescript\n// Separate caches for different parts of your automation\nconst loginStagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/login-flow\"\n});\n\nconst checkoutStagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/checkout-flow\"\n});\n\nconst dataExtractionStagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/data-extraction\"\n});\n```\n\n### Best Practices\n\n<AccordionGroup>\n\n<Accordion title=\"Use descriptive cache directories\">\nOrganize caches by workflow or feature for easier management:\n\n```typescript\n// Good: descriptive cache names\ncacheDir: \"cache/login-actions\"\ncacheDir: \"cache/search-actions\"\ncacheDir: \"cache/form-submissions\"\n\n// Avoid: generic cache names\ncacheDir: \"cache\"\ncacheDir: \"my-cache\"\n```\n</Accordion>\n\n<Accordion title=\"Clear cache when DOM changes\">\nIf the website structure changes significantly, clear your cache directory to force fresh inference:\n\n```bash\nrm -rf cache/login-actions\n```\n\nOr programmatically:\n\n```typescript\nimport { rmSync } from 'fs';\n\n// Clear cache before running if needed\nif (shouldClearCache) {\n  rmSync('cache/login-actions', { recursive: true, force: true });\n}\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/login-actions\"\n});\n```\n</Accordion>\n\n<Accordion title=\"Commit cache for CI/CD\">\nConsider committing your cache directory to version control for consistent behavior across environments:\n\n```gitignore\n# .gitignore\n# Don't ignore cache directories\n!cache/\n```\n\nThis ensures your CI/CD pipelines use the same cached actions without needing to run inference on first execution.\n</Accordion>\n\n</AccordionGroup>\n"
  },
  {
    "path": "packages/docs/v3/best-practices/computer-use.mdx",
    "content": "---\ntitle: Computer Use Agents\ndescription: Incorporate Computer Use APIs from Google, Anthropic, OpenAI, and Microsoft with one line of code in Stagehand.\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## What is a Computer Use Agent?\n\n<iframe\n  width=\"100%\"\n  height=\"400\"\n  src=\"https://www.youtube.com/embed/ODaHJzOyVCQ\"\n  title=\"YouTube video player\"\n  frameborder=\"0\"\n  allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n  allowfullscreen\n></iframe>\nYou might've heard of [Gemini Computer Use](https://blog.google/technology/google-deepmind/gemini-computer-use-model/), [Claude Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use), or [OpenAI's Computer Using Agent](https://openai.com/index/computer-using-agent/).\n\nThese are powerful tools that can convert natural language into actions on the computer. However, you'd otherwise need to write your own code to convert these actions into Playwright commands.\n\nStagehand not only handles the execution of Computer Use outputs, but also lets you hot-swap between Google, OpenAI, Anthropic, and Microsoft models with one line of code. You can find more information on the performance of different computer use models by visiting our [evals page](https://www.stagehand.dev/agent-evals).\n\n## How to use a Computer Use Agent in Stagehand\n\nStagehand lets you use Computer Use Agents with one line of code:\n\n<Warning>\n**Deprecation Notice:** The `cua: true` option is deprecated and will be removed in a future version. Use `mode: \"cua\"` instead.\n</Warning>\n\n<Note>\n**IMPORTANT! Configure your browser dimensions**\n\nComputer Use Agents will often return XY-coordinates to click on the screen, so you'll need to configure your browser dimensions.\n\nIf not specified, the default browser dimensions are 1288 x 711. You can also configure the browser dimensions in the `browserbaseSessionCreateParams` or `localBrowserLaunchOptions` options.\n</Note>\n\n\n### Configuring browser dimensions\n\nBrowser configuration differs by environment:\n\n<Tabs>\n<Tab title=\"BROWSERBASE\">\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n\tenv: \"BROWSERBASE\",\n    model: \"google/gemini-2.5-flash\",\n  \n    browserbaseSessionCreateParams: {\n      projectId: process.env.BROWSERBASE_PROJECT_ID!,\n      browserSettings: {\n\t\tblockAds: true,\n        viewport: {\n          width: 1288,\n          height: 711,\n        },\n      },\n  \t},\n});\n\nawait stagehand.init();\n```\n</Tab>\n<Tab title=\"LOCAL\">\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    headless: false,\n    viewport: {\n      width: 1288,\n      height: 711,\n    },\n  }\n});\n\nawait stagehand.init();\n```\n</Tab>\n</Tabs>\n\n### Direct your Computer Use Agent\n\nCall `execute` on the agent to assign a task to the agent.\n\n<CodeGroup>\n```typescript Google\nawait page.goto(\"https://www.google.com/\");\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n        apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n    },\n    systemPrompt: \"You are a helpful assistant...\",\n});\n\nawait agent.execute({\n    instruction: \"Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.\",\n    maxSteps: 20,\n    highlightCursor: true\n})\n```\n\n```typescript OpenAI\nawait page.goto(\"https://www.google.com/\");\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"openai/computer-use-preview\",\n        apiKey: process.env.OPENAI_API_KEY\n    },\n    systemPrompt: \"You are a helpful assistant...\",\n});\n\nawait agent.execute({\n    instruction: \"Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.\",\n    maxSteps: 20,\n    highlightCursor: true\n})\n```\n```typescript Anthropic\nawait page.goto(\"https://www.google.com/\");\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: {\n        modelName: \"anthropic/claude-sonnet-4-20250514\",\n        apiKey: process.env.ANTHROPIC_API_KEY\n    },\n    systemPrompt: \"You are a helpful assistant...\",\n});\n\nawait agent.execute({\n    instruction: \"Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.\",\n    maxSteps: 20,\n    highlightCursor: true\n})\n```\n</CodeGroup>\n\nYou can define the maximum number of steps the agent can take with `maxSteps`:\n\n```typescript\nawait agent.execute({\n\tinstructions: \"Apply for a library card at the San Francisco Public Library\",\n\tmaxSteps: 10,\n});\n``` \n\n### Select Your Computer Use Model\n\nStagehand supports computer use models from Google, Anthropic, OpenAI, and Microsoft. You can find all supported models on the [models page](/v3/configuration/models#agent-models-with-cua-support).\n\n<Tabs>\n<Tab title=\"Google\">\n```typescript\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    // GOOGLE_GENERATIVE_AI_API_KEY is auto-loaded - set in your .env\n});\n```\n</Tab>\n<Tab title=\"Anthropic\">\n```typescript\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: \"anthropic/claude-sonnet-4-20250514\",\n    // ANTHROPIC_API_KEY is auto-loaded - set in your .env\n});\n```\n</Tab>\n<Tab title=\"OpenAI\">\n```typescript\nconst agent = stagehand.agent({\n    mode: \"cua\",\n    model: \"openai/computer-use-preview\",\n    // OPENAI_API_KEY is auto-loaded - set in your .env\n});\n```\n</Tab>\n</Tabs>\n\n<Callout icon=\"code\" color=\"#6ec202\" iconType=\"regular\">View or run the example templates [here](https://www.browserbase.com/templates?category=Computer+Use+Agents)</Callout>\n"
  },
  {
    "path": "packages/docs/v3/best-practices/cost-optimization.mdx",
    "content": "---\ntitle: Cost Optimization  \nsidebarTitle: Cost Optimization\ndescription: Minimize costs while maintaining automation performance\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nCost optimization in Stagehand involves balancing LLM inference costs and browser infrastructure costs. This guide provides practical strategies to reduce your automation expenses.\n\n## Quick Wins\n\nStart with these simple optimizations that can reduce costs:\n\n### 1. Use the Right Model for the Job\n\nWe don't recommend using larger, more premium models for simple tasks. See our [evaluation results](https://stagehand.dev/evals) for model performance and cost comparisons across different task types.\n\n<CardGroup cols={2}>\n<Card title=\"Model Selection Guide\" icon=\"brain\" href=\"/configuration/models\">\n  Choose the right LLM for your budget and accuracy requirements\n</Card>\n<Card title=\"Evaluation Results\" icon=\"chart-line\" href=\"https://www.stagehand.dev/evals\">\n  See how different models perform on different tasks\n</Card>\n</CardGroup>\n\n### 2. Implement Caching\n\nEnable automatic action caching to eliminate redundant LLM calls. Simply specify a `cacheDir` when initializing Stagehand:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"action-cache\", // Enable automatic caching\n});\n\nawait stagehand.init();\n\n// First run: uses LLM inference and caches\n// Subsequent runs: reuses cached action (no LLM cost)\nawait stagehand.act(\"Click the sign in button\");\n```\n\n<CardGroup cols={1}>\n<Card title=\"Caching Guide\" icon=\"database\" href=\"/best-practices/caching\">\n  Learn how to organize caches and manage cache directories\n</Card>\n</CardGroup>\n\n### 3. Optimize Browser Sessions\n\nReuse sessions when possible and set appropriate timeouts. See [Browser Configuration](/configuration/browser) for details:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionCreateParams: {\n    timeout: 1800, // 30 minutes instead of default 1 hour\n    keepAlive: true, // Keep session alive between tasks\n  }\n});\n```\n\n<CardGroup cols={1}>\n<Card title=\"Browserbase Cost Optimization\" icon=\"window-maximize\" href=\"https://docs.browserbase.com/guides/cost-optimization\">\n  Optimize Browserbase infrastructure costs and session management\n</Card>\n</CardGroup>\n\n## Advanced Strategies\n\n### Intelligent Model Switching\n\nAutomatically fall back to cheaper models for simple tasks:\n\n```typescript\n// Use models from least to most expensive based on task complexity\n// See stagehand.dev/evals for performance comparisons\nasync function smartAct(prompt: string) {\n  const models = [\"google/gemini-2.5-flash\", \"openai/gpt-4o\"];\n\n  for (const model of models) {\n    try {\n      const stagehand = new Stagehand({\n        env: \"LOCAL\",\n        model: model\n      });\n      await stagehand.init();\n      const [action] = await stagehand.observe(prompt);\n      await stagehand.act(action);\n      await stagehand.close();\n      return;\n    } catch (error) {\n      console.log(`Falling back to ${model}...`);\n      await stagehand.close();\n    }\n  }\n}\n```\n\n### Session Pooling\n\nReuse browser sessions across multiple tasks:\n\n```typescript\nclass SessionManager {\n  private sessions = new Map<string, Stagehand>();\n  \n  async getSession(taskType: string): Promise<Stagehand> {\n    if (this.sessions.has(taskType)) {\n      return this.sessions.get(taskType)!;\n    }\n    \n    const stagehand = new Stagehand({ env: \"BROWSERBASE\" });\n    await stagehand.init();\n    this.sessions.set(taskType, stagehand);\n    return stagehand;\n  }\n}\n```\n\n## Cost Monitoring\n\nTrack your spending to identify optimization opportunities. See our [Observability Guide](/configuration/observability) for detailed metrics:\n\n```typescript\n// Monitor token usage\nconst metrics = await stagehand.metrics;\nconsole.log(`Total tokens: ${metrics.totalPromptTokens + metrics.totalCompletionTokens}`);\nconsole.log(`Estimated cost: $${(metrics.totalPromptTokens + metrics.totalCompletionTokens) * 0.00001}`);\n```\n\n<CardGroup cols={1}>\n<Card title=\"Observability & Metrics\" icon=\"chart-line\" href=\"/configuration/observability\">\n  Monitor usage patterns and track costs in real-time\n</Card>\n</CardGroup>\n\n## Budget Controls\n\nSet spending limits to prevent unexpected costs:\n\n```typescript\nclass BudgetGuard {\n  private dailySpend = 0;\n  private maxDailyBudget: number;\n  \n  constructor(maxDailyBudget: number = 25) {\n    this.maxDailyBudget = maxDailyBudget;\n  }\n  \n  checkBudget(estimatedCost: number): void {\n    if (this.dailySpend + estimatedCost > this.maxDailyBudget) {\n      throw new Error(`Daily budget exceeded: $${this.maxDailyBudget}`);\n    }\n    this.dailySpend += estimatedCost;\n  }\n}\n```\n\n\n## Related Resources\n\n<CardGroup cols={2}>\n<Card title=\"Model Selection Guide\" icon=\"brain\" href=\"/configuration/models\">\n  Choose the right LLM for your budget and accuracy requirements\n</Card>\n\n<Card title=\"Caching Strategies\" icon=\"database\" href=\"/best-practices/caching\">\n  Reduce costs with smart action caching and observe patterns\n</Card>\n\n<Card title=\"Observability & Metrics\" icon=\"chart-line\" href=\"/configuration/observability\">\n  Monitor usage patterns and track costs in real-time\n</Card>\n\n<Card title=\"Browser Configuration\" icon=\"window-maximize\" href=\"/configuration/browser\">\n  Optimize Browserbase infrastructure costs and session management\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/best-practices/deployments.mdx",
    "content": "---\ntitle: 'Deploying Stagehand'\ndescription: 'Deploy your AI agents and automations to the cloud'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<Tip>\n**🌟 Preview: Browser Functions** - Deploy your web automation code directly on Browserbase with browser functions. Scale your `act()` automations in the cloud with zero infrastructure setup. Reach out to hello@browserbase.com to get beta access.\n</Tip>\n\n## Deploy on Vercel\n\nSecurely run Stagehand on Browserbase inside a Vercel Function. This guide shows a minimal, production-safe HTTP endpoint you can call directly or on a schedule.\n\n### 1. Install Vercel CLI\n\nTo download and install Vercel CLI, run one of the following commands:\n\n<CodeGroup>\n```bash pnpm\npnpm i -g vercel\n```\n```bash yarn\nyarn global add vercel\n```\n```bash npm\nnpm i -g vercel\n```\n```bash bun\nbun add -g vercel\n```\n</CodeGroup>\n\n### 2. Project layout\n\n```text\nyour-project/\n  api/\n    run.ts\n  package.json\n  tsconfig.json\n  vercel.json\n```\n\nCreate the structure with:\n\n```bash\nmkdir -p api\ntouch api/run.ts package.json vercel.json tsconfig.json\n```\n\n### 3. `api/run.ts` (Node.js runtime)\n\n```typescript\n// api/run.ts\nimport type { VercelRequest, VercelResponse } from \"@vercel/node\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\nexport default async function handler(req: VercelRequest, res: VercelResponse): Promise<void> {\n  try {\n    const stagehand = new Stagehand({\n      env: \"BROWSERBASE\",\n      apiKey: process.env.BROWSERBASE_API_KEY!,\n      projectId: process.env.BROWSERBASE_PROJECT_ID!,\n      disablePino: true,\n      model: {\n        modelName: \"google/gemini-2.5-flash\",\n        apiKey: process.env.GOOGLE_API_KEY!,\n      },\n      // optional session params\n      browserbaseSessionCreateParams: {\n        projectId: process.env.BROWSERBASE_PROJECT_ID!,\n        region: \"us-west-2\",\n        browserSettings: {\n          blockAds: true,\n        },\n      },\n    });\n\n    await stagehand.init();\n    const page = stagehand.context.pages()[0];\n\n    await page.goto(\"https://www.stagehand.dev/\");\n    await stagehand.act(\"click the evals button\");\n\n    const fastestModel = await stagehand.extract(\"extract the fastest model\", z.string());\n\n    await stagehand.close();\n\n    res.status(200).json({ ok: true, data: fastestModel });\n  } catch (err: unknown) {\n    const msg = err instanceof Error ? err.message : String(err);\n    res.status(500).json({ ok: false, error: msg });\n  }\n}\n```\n\n### 4. `package.json`\n\n```json\n{\n    \"name\": \"bb-stagehand-on-vercel\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"engines\": { \"node\": \">=18\" },\n    \"dependencies\": {\n      \"@browserbasehq/stagehand\": \"^3.0.0\"\n    },\n    \"devDependencies\": {\n      \"@types/node\": \"^20.12.12\",\n      \"@vercel/node\": \"^3.2.20\",\n      \"typescript\": \"^5.2.2\"\n    }\n}\n```\n\n### 5. `tsconfig.json`\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"node\",\n    \"outDir\": \".vercel/output/functions\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"api/**/*.ts\"]\n}\n```\n\n### 6. `vercel.json`\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"functions\": {\n    \"api/run.ts\": {\n      \"maxDuration\": 60\n    }\n  }\n}\n```\n\nSee Vercel's [configuring functions](https://vercel.com/docs/functions/configuring-functions) docs for more details.\n\n### 7. Link your project\n\nLink your local folder to a Vercel project before configuring environment variables:\n\n```bash\n# authenticate if needed\nvercel login\n\n# link the current directory to a Vercel project (interactive)\nvercel link\n```\n\n### 8. Environment variables\n\nDo not commit `.env` in production. Add variables via Vercel CLI:\n\n```bash\nvercel env add BROWSERBASE_API_KEY\nvercel env add BROWSERBASE_PROJECT_ID\n# (and your model key if needed)\nvercel env add GOOGLE_API_KEY\n```\n\nSee also: [Browser Environment](/configuration/environment) for details on required variables.\n\n### 9. Test locally\n\nReplicate the Vercel environment locally to exercise your Function before deploying. Run from the project root.\n\n```bash\n# ensure dependencies are installed\nnpm install\n\n# start the local Vercel dev server\nvercel dev --listen 5005\n```\n\n### 10. Deploy\n\n```bash\nvercel\nvercel --prod\n```\n\n### Execute the function\n\n#### Configure Protection Bypass for Automation\n\nBefore invoking the production URL, create a Protection Bypass for Automation:\n\n1. Generate a 32-character secret (you can use `openssl rand -hex 16`)\n2. Go to your project in Vercel\n3. Navigate to Settings → Deployment Protection\n4. Add the secret to \"Protection Bypass for Automation\"\n\nThen invoke the function with the bypass header:\n\n```bash\ncurl -X POST \\\n  -H \"x-vercel-protection-bypass: <your-32-character-secret>\" \\\n  https://<your-deployment>/api/run\n```\n\n### Optional: Cron on Vercel\n\nHit the same endpoint on a schedule by extending `vercel.json`:\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"functions\": {\n    \"api/run.ts\": {\n      \"maxDuration\": 60\n    }\n  }\n  },\n  \"crons\": [\n    { \"path\": \"/api/run\", \"schedule\": \"0 * * * *\" }\n  ]\n}\n```\n\n### Features\n- **No local browsers needed** with `env: \"BROWSERBASE\"`. [Browserbase](https://www.browserbase.com/) provides the browsers.\n- **Fast functionality**: Offload browser work to Browserbase and return JSON promptly.\n- **Long-running tasks**: Raise `maxDuration` and/or consider Edge runtime limits depending on plan.\n\n"
  },
  {
    "path": "packages/docs/v3/best-practices/deterministic-agent.mdx",
    "content": "---\ntitle: Deterministic Agent Scripts\nsidebarTitle: Deterministic Agent\ndescription: Use auto-caching to convert agent workflows into fast, deterministic scripts\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nAgent workflows are powerful for exploring and automating complex tasks, but they can be slow and non-deterministic. This guide shows you how to use Stagehand's built-in auto-caching to convert agent-discovered workflows into fast, deterministic scripts that run 10-100x faster.\n\n## Why Use Auto-Caching with Agent?\n\n<CardGroup cols={2}>\n  <Card title=\"Speed\" icon=\"bolt\">\n    Cached agent workflows run 10-100x faster by skipping LLM inference on subsequent runs\n  </Card>\n  <Card title=\"Cost\" icon=\"dollar-sign\">\n    Eliminate repeated LLM calls—first run uses inference, subsequent runs use cache\n  </Card>\n  <Card title=\"Reliability\" icon=\"shield-check\">\n    Cached actions are deterministic and more predictable than fresh agent exploration\n  </Card>\n  <Card title=\"Simplicity\" icon=\"wand-magic-sparkles\">\n    Works automatically—just specify `cacheDir` and Stagehand handles everything\n  </Card>\n</CardGroup>\n\n## How Auto-Caching Works\n\nWhen you specify a `cacheDir`:\n\n1. **First run**: Agent explores and executes workflow using LLM inference\n2. **Actions cached**: All actions are automatically saved to local cache\n3. **Subsequent runs**: Same workflow reuses cached actions (no LLM calls)\n4. **Performance**: 10-100x faster execution, zero LLM tokens\n\nThe cache key is automatically generated based on:\n- Agent instruction\n- Start URL\n- Agent execution options\n- Agent configuration\n\n## Basic Auto-Caching with Agent\n\nSimply add `cacheDir` when initializing Stagehand:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Enable auto-caching\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"agent-cache\" // Automatic caching enabled\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n  },\n  systemPrompt: \"You are a helpful assistant that can use a web browser.\",\n});\n\n// First run: Uses LLM inference (~20-30 seconds, ~50,000 tokens)\n// Subsequent runs: Uses cached actions (~2-3 seconds, 0 tokens)\nconst result = await agent.execute({\n  instruction: \"Find the login form, fill in username 'demo' and password 'test123', then click submit\",\n  maxSteps: 10\n});\n\nconsole.log(\"Completed:\", result.success);\nconsole.log(\"Actions taken:\", result.actions.length);\n\nawait stagehand.close();\n```\n\nThat's it! The second time you run this script, it will reuse the cached agent actions automatically.\n\n## Organizing Caches by Workflow\n\nUse descriptive cache directories for different workflows:\n\n```typescript\n// Login workflow\nconst loginStagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/login-workflow\"\n});\n\n// Checkout workflow\nconst checkoutStagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/checkout-workflow\"\n});\n\n// Data extraction workflow\nconst extractStagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/extraction-workflow\"\n});\n```\n\n## Complete Example: First vs Subsequent Runs\n\n### First Run (Exploration Mode)\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/github-search\" // Enable caching\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://github.com\");\n\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n  },\n  systemPrompt: \"You are a helpful assistant that can use a web browser.\",\n});\n\nconsole.log(\"First run: Exploring with agent...\");\nconst startTime = Date.now();\n\nconst result = await agent.execute({\n  instruction: \"Search for 'stagehand' and click the first repository result\",\n  maxSteps: 10\n});\n\nconst duration = Date.now() - startTime;\nconsole.log(`First run completed in ${duration}ms`);\nconsole.log(`Actions: ${result.actions.length}`);\nconsole.log(`Status: ${result.success}`);\n\nawait stagehand.close();\n\n// Output (example):\n// First run completed in 25000ms\n// Actions: 8\n// Status: true\n```\n\n### Subsequent Runs (Cached Mode)\n\nRun the **exact same script** again:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/github-search\" // Same cache directory\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://github.com\");\n\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n  },\n  systemPrompt: \"You are a helpful assistant that can use a web browser.\",\n});\n\nconsole.log(\"Subsequent run: Using cached actions...\");\nconst startTime = Date.now();\n\nconst result = await agent.execute({\n  instruction: \"Search for 'stagehand' and click the first repository result\",\n  maxSteps: 10\n});\n\nconst duration = Date.now() - startTime;\nconsole.log(`Subsequent run completed in ${duration}ms`);\nconsole.log(`Actions: ${result.actions.length}`);\nconsole.log(`Status: ${result.success}`);\n\nawait stagehand.close();\n\n// Output (example):\n// Subsequent run completed in 2500ms  ← 10x faster!\n// Actions: 8\n// Status: true\n```\n\n## Using History for Analysis\n\nWhile caching handles execution automatically, you can still use `stagehand.history` to analyze what happened:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport fs from \"fs/promises\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/workflow\"\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY\n  },\n  systemPrompt: \"You are a helpful assistant that can use a web browser.\",\n});\n\nawait agent.execute({\n  instruction: \"Complete the login process\",\n  maxSteps: 10\n});\n\n// Analyze what the agent did\nconst history = await stagehand.history;\n\nconsole.log(`\\nWorkflow Analysis:`);\nconsole.log(`Total operations: ${history.length}`);\n\nconst agentOps = history.filter(e => e.method === 'agent');\nconst actOps = history.filter(e => e.method === 'act');\nconst navOps = history.filter(e => e.method === 'navigate');\n\nconsole.log(`- Agent executions: ${agentOps.length}`);\nconsole.log(`- Act operations: ${actOps.length}`);\nconsole.log(`- Navigate operations: ${navOps.length}`);\n\n// Save for documentation\nawait fs.writeFile(\n  'workflow-analysis.json',\n  JSON.stringify(history, null, 2)\n);\n\nawait stagehand.close();\n```\n\n## Cache Management\n\n### Clear Cache When Site Changes\n\nIf the website structure changes, clear the cache to force fresh exploration:\n\n```typescript\nimport { rmSync } from 'fs';\n\n// Clear specific workflow cache\nrmSync('cache/login-workflow', { recursive: true, force: true });\n\n// Then run with fresh exploration\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/login-workflow\" // Will rebuild cache\n});\n```\n\n### Programmatic Cache Control\n\n```typescript\nimport { rmSync, existsSync } from 'fs';\n\nfunction clearCacheIfNeeded(cacheDir: string, maxAge: number = 7 * 24 * 60 * 60 * 1000) {\n  if (!existsSync(cacheDir)) {\n    return; // No cache to clear\n  }\n\n  const stats = statSync(cacheDir);\n  const age = Date.now() - stats.mtimeMs;\n\n  if (age > maxAge) {\n    console.log(`Cache older than ${maxAge}ms, clearing...`);\n    rmSync(cacheDir, { recursive: true, force: true });\n  }\n}\n\n// Clear cache if older than 7 days\nclearCacheIfNeeded('cache/workflow');\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/workflow\"\n});\n```\n\n## Advanced Patterns\n \n### Fallback to Fresh Exploration\n\nCombine caching with fallback for resilience:\n\n```typescript\nasync function executeWithFallback() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    cacheDir: \"cache/workflow\",\n    selfHeal: true // Enable self-healing\n  });\n\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n\n  await page.goto(\"https://example.com\");\n\n  const agent = stagehand.agent({\n    model: \"anthropic/claude-sonnet-4-20250514\"\n  });\n\n  try {\n    // Try with cache\n    const result = await agent.execute({\n      instruction: \"Complete the checkout process\",\n      maxSteps: 15\n    });\n\n    console.log(\"Execution successful:\", result.success);\n  } catch (error) {\n    console.error(\"Cached workflow failed:\", error);\n\n    // Clear cache and retry with fresh exploration\n    rmSync('cache/workflow', { recursive: true, force: true });\n\n    console.log(\"Retrying with fresh exploration...\");\n    const retryResult = await agent.execute({\n      instruction: \"Complete the checkout process\",\n      maxSteps: 15\n    });\n\n    console.log(\"Retry successful:\", retryResult.success);\n  }\n\n  await stagehand.close();\n}\n```\n\n### Version Control for Caches\n\nCommit cache directories to ensure consistent behavior across environments:\n\n```gitignore\n# .gitignore\n\n# Commit cache directories for deterministic CI/CD\n!cache/\n!cache/**/*.json\n```\n\n```typescript\n// CI/CD pipeline will use pre-generated cache\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/production-workflow\" // Committed to repo\n});\n```\n\n## Best Practices\n\n<AccordionGroup>\n\n<Accordion title=\"Use Descriptive Cache Names\">\nOrganize caches by workflow or feature:\n\n```typescript\n// Good: descriptive cache names\ncacheDir: \"cache/user-registration\"\ncacheDir: \"cache/product-search\"\ncacheDir: \"cache/checkout-flow\"\n\n// Avoid: generic names\ncacheDir: \"cache\"\ncacheDir: \"my-cache\"\n```\n</Accordion>\n\n<Accordion title=\"Cache Invalidation Strategy\">\nImplement a strategy for refreshing caches:\n\n```typescript\n// Option 1: Time-based invalidation\nif (isCacheOlderThan('cache/workflow', 7)) {\n  clearCache('cache/workflow');\n}\n\n// Option 2: Version-based invalidation\nconst CACHE_VERSION = 'v2';\nconst cacheDir = `cache/workflow-${CACHE_VERSION}`;\n\n// Option 3: Manual invalidation flag\nif (process.env.CLEAR_CACHE === 'true') {\n  clearCache('cache/workflow');\n}\n```\n</Accordion>\n\n<Accordion title=\"Test in Staging First\">\nAlways test cached workflows in staging before production:\n\n```typescript\nconst env = process.env.NODE_ENV === 'production' ? 'production' : 'staging';\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: `cache/${env}-workflow`\n});\n```\n</Accordion>\n\n<Accordion title=\"Monitor Cache Hit Rates\">\nTrack cache usage for optimization:\n\n```typescript\nconst cacheHit = existsSync('cache/workflow') &&\n                statSync('cache/workflow').mtimeMs < Date.now();\n\nif (cacheHit) {\n  console.log(\"Cache hit - using cached workflow\");\n} else {\n  console.log(\"Cache miss - exploring with agent\");\n}\n\n// Log metrics\nmetrics.recordCacheHit(cacheHit);\n```\n</Accordion>\n\n</AccordionGroup>\n\n## Performance Comparison\n\n**Without Caching (Every Run):**\n```typescript\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\n// No cacheDir specified\n\nconst result = await agent.execute({\n  instruction: \"Complete workflow\",\n  maxSteps: 10\n});\n\n// Every run: ~20-30 seconds, ~50,000 tokens\n```\n\n**With Auto-Caching (First Run):**\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/workflow\"\n});\n\nconst result = await agent.execute({\n  instruction: \"Complete workflow\",\n  maxSteps: 10\n});\n\n// First run: ~20-30 seconds, ~50,000 tokens (cached for next time)\n```\n\n**With Auto-Caching (Subsequent Runs):**\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/workflow\" // Reuses cache\n});\n\nconst result = await agent.execute({\n  instruction: \"Complete workflow\",\n  maxSteps: 10\n});\n\n// Subsequent runs: ~2-3 seconds, 0 tokens ← 10-100x faster!\n```\n\n<Note>\nCached agent workflows run **10-100x faster** and consume **zero LLM tokens** on subsequent runs. The first run pays the exploration cost, every run after is nearly instant.\n</Note>\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Cache not being used\">\n**Problem**: Workflow still slow on subsequent runs\n\n**Solutions**:\n- Verify `cacheDir` path is correct and consistent across runs\n- Ensure instruction, URL, and agent config are identical\n- Check file permissions on cache directory\n- Look for cache hit/miss logs in verbose mode\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/workflow\",\n  verbose: 2 // Enable debug logs\n});\n```\n</Accordion>\n\n<Accordion title=\"Cached workflow fails\">\n**Problem**: Cached actions fail on subsequent runs\n\n**Solutions**:\n- Website may have changed—clear cache to re-explore\n- Enable self-healing to adapt to minor changes\n- Implement fallback logic to retry with fresh exploration\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  cacheDir: \"cache/workflow\",\n  selfHeal: true // Adapt to changes\n});\n```\n</Accordion>\n\n<Accordion title=\"Too many cache directories\">\n**Problem**: Cache directories growing uncontrolled\n\n**Solutions**:\n- Use version prefixes for cache directories\n- Implement automatic cleanup of old caches\n- Share cache directories for similar workflows\n\n```typescript\n// Versioned caches\nconst CACHE_VERSION = '2024-01';\nconst cacheDir = `cache/workflow-${CACHE_VERSION}`;\n\n// Cleanup old versions\nrmSync('cache/workflow-2023-12', { recursive: true, force: true });\n```\n</Accordion>\n</AccordionGroup>\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Agent Guide\" icon=\"robot\" href=\"/v3/basics/agent\">\n    Learn more about agent capabilities and configuration\n  </Card>\n\n  <Card title=\"Caching Guide\" icon=\"database\" href=\"/v3/best-practices/caching\">\n    Complete guide to auto-caching with act() and agent()\n  </Card>\n\n  <Card title=\"Observability\" icon=\"chart-line\" href=\"/v3/configuration/observability\">\n    Monitor and track history and metrics\n  </Card>\n\n  <Card title=\"Speed Optimization\" icon=\"bolt\" href=\"/v3/best-practices/speed-optimization\">\n    Additional techniques for faster automation\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/best-practices/history.mdx",
    "content": "---\ntitle: History Tracking\nsidebarTitle: History Tracking\ndescription: Track and analyze Stagehand operations with the history API\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nThe history API captures every Stagehand operation for debugging, auditing, and workflow analysis.\n\n## Basic Usage\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\nawait stagehand.act(\"click login button\");\n\n// Get complete history\nconst history = await stagehand.history;\n\nconsole.log(`Total operations: ${history.length}`);\nhistory.forEach((entry, i) => {\n  console.log(`${i + 1}. ${entry.method} at ${entry.timestamp}`);\n});\n\nawait stagehand.close();\n```\n\n## History Entry Structure\n\n```typescript\ninterface HistoryEntry {\n  method: \"act\" | \"extract\" | \"observe\" | \"navigate\" | \"agent\";\n  parameters: unknown;  // Input parameters\n  result: unknown;      // Output/result\n  timestamp: string;    // ISO 8601 timestamp\n}\n```\n\n## Common Use Cases\n\n### Debugging Failures\n\n```typescript\ntry {\n  await stagehand.act(\"click login button\");\n} catch (error) {\n  const history = await stagehand.history;\n\n  history.forEach((entry, i) => {\n    const status = entry.result && 'error' in entry.result ? \"FAILED\" : \"SUCCESS\";\n    console.log(`${i + 1}. ${status} - ${entry.method}`);\n  });\n}\n```\n\n### Analyzing Timing\n\n```typescript\nconst history = await stagehand.history;\n\nconst timings = history.map((entry, i) => {\n  if (i === 0) return null;\n  const duration = new Date(entry.timestamp).getTime() -\n                   new Date(history[i - 1].timestamp).getTime();\n  return { operation: entry.method, duration };\n}).filter(Boolean);\n\nconsole.log(\"Slowest operations:\",\n  timings.sort((a, b) => b.duration - a.duration).slice(0, 3)\n);\n```\n\n### Operation Statistics\n\n```typescript\nconst history = await stagehand.history;\n\nconst stats = history.reduce((acc, entry) => {\n  acc[entry.method] = (acc[entry.method] || 0) + 1;\n  return acc;\n}, {} as Record<string, number>);\n\nconsole.log(\"Operations:\", stats);\n// { act: 5, extract: 2, observe: 3, navigate: 1 }\n```\n\n### Saving History\n\n```typescript\nimport fs from \"fs/promises\";\n\nconst history = await stagehand.history;\nconst metrics = await stagehand.metrics;\n\nawait fs.writeFile(\n  `workflow-report.json`,\n  JSON.stringify({\n    history,\n    totalOps: history.length,\n    totalTokens: metrics.totalPromptTokens + metrics.totalCompletionTokens\n  }, null, 2)\n);\n```\n\n## Filtering by Operation Type\n\n```typescript\nconst history = await stagehand.history;\n\nconst actions = history.filter(e => e.method === 'act');\nconst extractions = history.filter(e => e.method === 'extract');\nconst agentOps = history.filter(e => e.method === 'agent');\n\nconsole.log(`Actions: ${actions.length}`);\nconsole.log(`Extractions: ${extractions.length}`);\nconsole.log(`Agent executions: ${agentOps.length}`);\n```\n\n## Combining with Metrics\n\n```typescript\nconst history = await stagehand.history;\nconst metrics = await stagehand.metrics;\n\nconst report = {\n  totalOps: history.length,\n  successful: history.filter(e => !e.result || !('error' in e.result)).length,\n  failed: history.filter(e => e.result && 'error' in e.result).length,\n  totalTokens: metrics.totalPromptTokens + metrics.totalCompletionTokens,\n  avgTimePerOp: `${(metrics.totalInferenceTimeMs / history.length).toFixed(0)}ms`\n};\n\nconsole.log(report);\n```\n\n<Card title=\"Observability Guide\" icon=\"chart-line\" href=\"/configuration/observability\">\n  Learn more about metrics, logging, and monitoring\n</Card>\n\n## What's Tracked?\n\nOnly Stagehand methods are tracked in history:\n\n```typescript\n// Tracked\nawait stagehand.act(\"click button\");              // ✓\nawait stagehand.extract({ instruction: \"...\" }); // ✓\nawait stagehand.observe(\"find elements\");         // ✓\nawait page.goto(\"https://example.com\");      // ✓\n\n// Not tracked\nawait page.locator(\"button\").click();        // ✗ Native Playwright\nawait page.click(\"button\");                  // ✗ Native Playwright\n```\n\n## Best Practices\n\n- **Save history for critical workflows** - Maintain audit trails for production\n- **Inspect history when debugging** - Check the last operations to identify failures\n- **Analyze timing periodically** - Find slow operations and optimize\n- **Combine with metrics** - Get complete visibility into performance and cost\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Deterministic Agent\" icon=\"robot\" href=\"/best-practices/deterministic-agent\">\n    Build fast, cached agent workflows\n  </Card>\n\n  <Card title=\"Observability\" icon=\"chart-line\" href=\"/configuration/observability\">\n    Combine history with metrics\n  </Card>\n\n  <Card title=\"Caching\" icon=\"database\" href=\"/best-practices/caching\">\n    Speed up workflows with caching\n  </Card>\n\n  <Card title=\"Logging\" icon=\"file-lines\" href=\"/configuration/logging\">\n    Configure detailed execution traces\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/best-practices/mcp-integrations.mdx",
    "content": "---\ntitle: \"MCP Integrations\"\ndescription: \"Using Model Context Protocol (MCP) integrations to enhance agent capabilities\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## What are MCP Integrations?\n\nMCP (Model Context Protocol) integrations allow you to connect your Stagehand agents to external tools, APIs, and services. This enables agents to perform actions beyond browser automation, such as web search, database operations, and API calls.\n\n<Info>\nMCP integrations make your agents more powerful by combining browser automation with external capabilities. The agent can intelligently decide when to use browser actions versus external tools.\n</Info>\n\n## Connection Options\n\nThere are two options for connecting to MCP servers:\n\n1. **Pass a URL directly** - The simplest approach for quick setup\n2. **Create a connection first** - Gives you more control over the connection\n\n<Note>\nMCP client support is currently only available in TypeScript.\n</Note>\n\n## Passing a URL\n\nThe simplest way to add MCP integrations is by providing server URLs directly in the agent configuration:\n\n```typescript\nconst agent = stagehand.agent({\n  provider: \"openai\",\n  model: \"computer-use-preview\",\n  integrations: [\n    `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n  ],\n  systemPrompt: `You have access to web search through Exa. Use it to find current information before browsing.`,\n  options: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n\nawait agent.execute(\"Search for the best headphones of 2025 and go through checkout for the top recommendation\");\n```\n\n## Creating a Connection First\n\nAlternatively, you can establish MCP connections first and then pass the client objects:\n\n```typescript\nimport { connectToMCPServer } from \"@browserbasehq/stagehand\";\n\n// Connect to MCP server\nconst supabaseClient = await connectToMCPServer(\n  `https://server.smithery.ai/@supabase-community/supabase-mcp/mcp?api_key=${process.env.SMITHERY_API_KEY}`\n);\n\n// You can also pass the config to start a local MCP server\nconst notionClient = await connectToMCPServer({\n  command: \"npx\",\n  args: [\"-y\", \"@notionhq/notion-mcp-server\"],\n  env: {\n    NOTION_TOKEN: process.env.NOTION_TOKEN,\n  },\n});\n\n// Use the connected clients (example with Supabase + Notion)\nconst agent = stagehand.agent({\n  provider: \"openai\", \n  model: \"computer-use-preview\",\n  integrations: [supabaseClient, notionClient],\n  systemPrompt: `You can interact with Supabase databases and Notion. Use these tools to store and retrieve data.`,\n  options: {\n    apiKey: process.env.OPENAI_API_KEY,\n  },\n});\n\nawait agent.execute(\"Search for restaurants in New Brunswick, NJ and save the first result to the database\");\n```\n\n## Authenticated MCP Servers\n\nSome MCP servers require authentication via HTTP request headers. You can pass request headers through `requestOptions`:\n\n```typescript\nconst authenticatedClient = await connectToMCPServer({\n  serverUrl: \"https://mcp-server.example.com/mcp\",\n  requestOptions: {\n    requestInit: {\n      headers: {\n        Authorization: `Bearer ${process.env.MCP_SERVER_API_KEY}`,\n      },\n    },\n  },\n});\n```\n\n\n\n## Multiple Integrations\n\nYou can combine multiple MCP integrations in a single agent:\n\n```typescript\nconst databaseClient = await connectToMCPServer(/* database config */);\n\nconst agent = stagehand.agent({\n  integrations: [\n    `https://search-service.example.com/mcp?apiKey=${process.env.SEARCH_API_KEY}`,\n    databaseClient\n  ],\n  systemPrompt: `You have access to external tools for search and data storage. Use these tools strategically to complete tasks efficiently.`\n});\n```\n\n## Best Practices\n\n### Choose the Right Connection Approach\n<Tabs>\n<Tab title=\"Passing a URL\">\n**When to use:**\n- Simple setup requirements\n- Standard API configurations\n- Getting started quickly\n\n**Benefits:**\n- Minimal code required\n- Automatic connection handling\n- Easy to configure\n</Tab>\n\n<Tab title=\"Creating a Connection First\">\n**When to use:**\n- Custom connection options\n- Connection reuse across agents\n- Advanced error handling\n\n**Benefits:**\n- Full control over connections\n- Better error handling\n- Connection pooling capabilities\n</Tab>\n</Tabs>\n\n### Environment Variables\n\nAlways use environment variables for API keys and sensitive information:\n\n```bash\n# .env file\nSEARCH_API_KEY=your_search_service_key\nMCP_SERVICE_API_KEY=your_mcp_service_key\nOPENAI_API_KEY=your_openai_key\nDATABASE_URL=your_database_url\nDATABASE_API_KEY=your_database_key\n```\n\n### Instructions Best Practices\n\nProvide clear instructions about available tools:\n\n<Tabs>\n<Tab title=\"Good Instructions\">\n```typescript\nsystemPrompt: `You have access to:\n1. Web search tools - Use to find current information\n2. Database tools - Use to store/retrieve data\n3. Browser automation - Use for web interactions\n\nAlways search for current information before making decisions.\nStore important data for later reference.`\n```\n</Tab>\n\n<Tab title=\"Poor Instructions\">\n```typescript\nsystemPrompt: \"You can search and save data.\"\n```\n</Tab>\n</Tabs>\n\n### Error Handling\n\nImplement proper error handling for MCP connections:\n\n```typescript\ntry {\n  const client = await connectToMCPServer(serverUrl);\n  \n  const agent = stagehand.agent({\n    integrations: [client],\n    // ... other config\n  });\n  \n  const result = await agent.execute(instruction);\n} catch (error) {\n  console.error(\"MCP integration failed:\", error);\n  // Handle fallback behavior\n}\n```\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Connection timeouts\">\n**Problem:** MCP server connections timing out\n\n**Solutions:**\n- Verify server URLs are correct and accessible\n- Check network connectivity\n- Ensure API keys are valid and have proper permissions\n- Try connecting to servers individually to isolate issues\n</Accordion>\n\n<Accordion title=\"Tool not being used\">\n**Problem:** Agent not using available MCP tools\n\n**Solutions:**\n- Make instructions more specific about when to use tools\n- Ensure API keys are properly configured\n- Check that the MCP server supports the expected tools\n- Verify tool descriptions are clear and actionable\n</Accordion>\n\n<Accordion title=\"Authentication errors\">\n**Problem:** API key or authentication failures\n\n**Solutions:**\n- Verify all required environment variables are set\n- Check API key validity and permissions  \n- Ensure URLs include necessary authentication parameters\n- Test MCP connections independently before using in agents\n</Accordion>\n</AccordionGroup>\n\n## Examples\n\n### Web Search + Browser Automation\n```typescript\nconst agent = stagehand.agent({\n  integrations: [`https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`],\n  systemPrompt: `First search for current information, then use the browser to complete tasks based on what you find.`\n});\n\nawait agent.execute(\"Find the best laptop deals for 2025 and navigate to purchase the top recommendation\");\n```\n\n### Data Extraction + Storage\n```typescript\nconst supabaseClient = await connectToMCPServer(/* config */);\n\nconst agent = stagehand.agent({\n  integrations: [supabaseClient],\n  systemPrompt: `Extract data from websites and store it using available database tools.`\n});\n\nawait agent.execute(\"Extract all restaurant information from this directory and save it to the database\");\n```\n\n### Multi-tool Workflow\n```typescript\nconst agent = stagehand.agent({\n  integrations: [\n    `https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`,\n    supabaseClient\n  ],\n  systemPrompt: `Use all available tools strategically: search for current info, browse websites, and store important data.`\n});\n\nawait agent.execute(\"Research competitor pricing, compare with our site, and store the analysis\");\n```\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Agent Basics\" icon=\"robot\" href=\"/basics/agent\">\n  Learn the fundamentals of Stagehand agents\n</Card>\n\n<Card title=\"MCP Server Setup\" icon=\"server\" href=\"/v3/integrations/mcp/setup\">  \n  Set up your own MCP server\n</Card>\n\n<Card title=\"Custom Tools\" icon=\"wrench\" href=\"/v3/integrations/mcp/tools\">\n  Create custom MCP tools\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/best-practices/prompting-best-practices.mdx",
    "content": "---\ntitle: Prompting Best Practices\ndescription: \"Write effective prompts for reliable Stagehand automation\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nGood prompts make Stagehand reliable. Bad prompts cause failures. Here's how to write prompts that work consistently.\n\n## Act Method\n\nUse `act()` for single actions on web pages. Each action should be focused and clear.\n\n```typescript\n// Good - Single, specific actions\nawait stagehand.act(\"click the 'Add to Cart' button\");\nawait stagehand.act(\"type 'user@example.com' into the email field\");\n\n// Bad - Multiple actions combined\nawait stagehand.act(\"fill out the form and submit it\");\nawait stagehand.act(\"login with credentials and navigate to dashboard\");\n```\n\n### Use Element Types, Not Colors\n\nDescribe elements by their type and function rather than visual attributes like color.\n\n```typescript\n// Good - Element types and descriptive text\nawait stagehand.act(\"click the 'Sign In' button\");\nawait stagehand.act(\"type into the email input field\");\n\n// Bad - Color-based descriptions\nawait stagehand.act(\"click the blue button\");\nawait stagehand.act(\"type into the white input\");\n```\n\n### Use Descriptive Language\n\n```typescript\n// Good - Clear element identification\nawait stagehand.act(\"click the 'Next' button at the bottom of the form\");\nawait stagehand.act(\"type into the search bar at the top of the page\");\n\n// Bad - Vague descriptions\nawait stagehand.act(\"click next\");\nawait stagehand.act(\"type into search\");\n```\n\n### Choose the Right Action Verbs\n\n- **Click** for buttons, links, checkboxes\n- **Type** for text inputs\n- **Select** for dropdowns\n- **Check/uncheck** for checkboxes\n- **Upload** for file inputs\n\n```typescript\n// Good\nawait stagehand.act(\"click the submit button\");\nawait stagehand.act(\"select 'Option 1' from dropdown\");\n\n// Bad\nawait stagehand.act(\"click submit\");\nawait stagehand.act(\"choose option 1\");\n```\n\n### Protect Sensitive Data\n\nVariables keep sensitive information out of prompts and logs.\n\n```typescript\n// Use variables for sensitive data\nawait stagehand.act(\"type %username% into the email field\", {\n  variables: { username: \"user@example.com\" }\n});\n\nawait stagehand.act(\"type %password% into the password field\", {\n  variables: { password: process.env.USER_PASSWORD }\n});\n```\n\n<Warning>\nSet `verbose: 0` in your Stagehand config to prevent secrets from appearing in logs.\n</Warning>\n\n## Extract Method\n\nUse `extract()` to pull structured data from pages. Define clear schemas and provide context.\n\n### Schema Best Practices\n\nUse descriptive field names, correct types, and detailed descriptions. Field descriptions provide context that helps the model understand exactly what to extract.\n\n```typescript\n// Good - Descriptive names, correct types, and helpful descriptions\nconst productData = await stagehand.extract(\n  \"Extract product information\",\n  z.object({\n    productTitle: z.string().describe(\"The main product name displayed on the page\"),\n    priceInDollars: z.number().describe(\"Current selling price as a number, without currency symbol\"),\n    isInStock: z.boolean().describe(\"Whether the product is available for purchase\")\n  })\n);\n\n// Bad - Generic names, wrong types, no descriptions\nconst data = await stagehand.extract(\n  \"Get product details\",\n  z.object({\n    name: z.string(), // Too generic, no context\n    price: z.string(), // Should be number\n    stock: z.string() // Should be boolean, no context\n  })\n);\n```\n\n### Use Proper URL Types\n\nSpecify URL types with `z.string().url()` to tell Stagehand to extract URLs.\n\n```typescript\n// Good - Tells Stagehand to extract URLs\nconst links = await stagehand.extract(\n  \"Extract navigation links\",\n  z.array(z.object({\n    text: z.string(),\n    url: z.string().url() // Required for URL extraction\n  }))\n);\n\n// Single URL extraction\nconst contactUrl = await stagehand.extract(\n  \"extract the contact page URL\",\n  z.string().url()\n);\n```\n\n## Observe Method\n\nUse `observe()` to discover actionable elements before acting on them.\n\n### Check Elements First\n\nVerify elements exist before taking action to avoid errors.\n\n```typescript\n// Check for elements first\nconst loginButtons = await stagehand.observe(\"Find the login button\");\n\nif (loginButtons.length > 0) {\n  await stagehand.act(loginButtons[0]);\n} else {\n  console.log(\"No login button found\");\n}\n```\n\n### Be Specific About Element Types\n\n```typescript\n// Good - Specific element types\nconst submitButtons = await stagehand.observe(\"Find submit button in the form\");\nconst dropdowns = await stagehand.observe(\"Find the state dropdown menu\");\n\n// Bad - Too vague\nconst elements = await stagehand.observe(\"Find submit stuff\");\nconst things = await stagehand.observe(\"Find state selection\");\n```\n\n## Agent Method\n\nUse `agent()` for complex, multi-step workflows. Provide detailed instructions and set appropriate limits.\n\n### Navigate First\n\nDon't include navigation in agent tasks. Handle it separately.\n\n```typescript\n// Good - Navigate first\nawait page.goto('https://amazon.com');\nawait agent.execute('Search for wireless headphones under $100 and add the best rated one to cart');\n\n// Bad - Navigation in task\nawait agent.execute('Go to Amazon, search for headphones, and add one to cart');\n```\n\n### Be Highly Specific\n\nDetailed instructions lead to better results.\n\n```typescript\n// Good - Detailed instructions\nawait agent.execute({\n  instruction: \"Find Italian restaurants in Brooklyn that are open after 10pm, have outdoor seating, and are rated 4+ stars. Save the top 3 results.\",\n  maxSteps: 25\n});\n\n// Bad - Vague instructions\nawait agent.execute(\"Find some good restaurants\");\n```\n\n### Set Appropriate Step Limits\n\nMatch step limits to task complexity.\n\n```typescript\n// Simple task - fewer steps\nawait agent.execute({\n  instruction: \"Subscribe to the newsletter with email 'user@example.com'\",\n  maxSteps: 10\n});\n\n// Complex task - more steps  \nawait agent.execute({\n  instruction: \"Research and compare 5 project management tools with pricing and features\",\n  maxSteps: 50\n});\n```\n\n### Include Success Criteria\n\nTell the agent how to know when it's done.\n\n```typescript\n// Good - Clear success criteria\nawait agent.execute({\n  instruction: \"Add 3 smartphone cases to cart and confirm the cart shows exactly 3 items with total price\",\n  maxSteps: 20\n});\n\n// Bad - No validation\nawait agent.execute(\"Add some items to cart\");\n```\n\n## Common Mistakes to Avoid\n\n- **Combining multiple actions** - Keep each `act()` call to one action\n- **Using vague descriptions** - Be specific about which elements to interact with  \n- **Exposing sensitive data** - Always use variables for credentials\n- **Skipping validation** - Check results before proceeding\n\n## Testing Your Prompts\n\n1. **Start simple** - Test basic functionality first\n2. **Add complexity gradually** - Build up to complex workflows\n3. **Monitor results** - Use logging to understand what's happening\n4. **Iterate based on failures** - Refine prompts when they don't work\nRemember: Good prompting is iterative. When in doubt, be more specific rather than less."
  },
  {
    "path": "packages/docs/v3/best-practices/speed-optimization.mdx",
    "content": "---\ntitle: Speed Optimization\nsidebarTitle: Speed Optimization\ndescription: Optimize Stagehand performance for faster automation and reduced latency\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nStagehand performance depends on several factors: DOM processing speed, LLM inference time, browser operations, and network latency. This guide provides proven strategies to maximize automation speed.\n\n## Quick Performance Wins\n\n### 1. Plan Ahead with Observe\n\n\nUse a single `observe()` call to plan multiple actions, then execute them efficiently:\n\n```typescript\n// Instead of sequential operations with multiple LLM calls\nawait stagehand.act(\"Fill name field\");        // LLM call #1\nawait stagehand.act(\"Fill email field\");       // LLM call #2\nawait stagehand.act(\"Select country dropdown\"); // LLM call #3\n\n// Use single observe to plan all form fields - one LLM call\nconst formFields = await stagehand.observe(\"Find all form fields to fill\");\n\n// Execute all actions without LLM inference\nfor (const field of formFields) {\n  await stagehand.act(field); // No LLM calls!\n}\n```\n\n<Note>\n**Performance Tip**: Acting on `observe` results avoids LLM inference entirely. This approach is 2-3x faster than direct `act()` calls and is the recommended pattern for multi-step workflows.\n</Note>\n\n<Card title=\"Caching Guide\" icon=\"database\" href=\"/best-practices/caching\">\n  Learn advanced caching patterns and cache invalidation strategies\n</Card>\n\n### 2. Optimize DOM Processing\n\nReduce DOM complexity before Stagehand processes the page:\n\n```typescript\n// Remove heavy elements that slow down processing\nawait page.evaluate(() => {\n  // Remove video elements\n  document.querySelectorAll('video, iframe').forEach(el => el.remove());\n  \n  // Hide complex animations\n  document.querySelectorAll('[style*=\"animation\"]').forEach(el => {\n    (el as HTMLElement).style.animation = 'none';\n  });\n});\n\n// Then perform Stagehand operations\nawait stagehand.act(\"Click the submit button\");\n```\n\n### 3. Set Appropriate Timeouts\n\nUse shorter timeouts for simple operations and longer ones for complex page loads:\n\n```typescript\n// Simple actions - reduce action timeout\nawait stagehand.act(\"Click the login button\", {\n  timeout: 5000  // Default is 30000ms, reduce for simple clicks\n});\n\n// Complex page loads - optimize navigation\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://heavy-spa.com\", {\n  waitUntil: \"domcontentloaded\", // Don't wait for all resources\n  timeout: 15000 // Shorter than default 30s\n});\n```\n\n## Performance Monitoring and Benchmarking\n\nTrack performance metrics and measure optimization impact:\n\n### Performance Tracking\n\n```typescript\nclass PerformanceTracker {\n  private speedMetrics: Map<string, number[]> = new Map();\n\n  async timedAct(page: Page, prompt: string): Promise<ActResult> {\n    const start = Date.now();\n    const result = await stagehand.act(prompt);\n    const duration = Date.now() - start;\n    \n    if (!this.speedMetrics.has(prompt)) {\n      this.speedMetrics.set(prompt, []);\n    }\n    this.speedMetrics.get(prompt)!.push(duration);\n    \n    console.log(`Action \"${prompt}\" took ${duration}ms`);\n    return result;\n  }\n\n  getAverageTime(prompt: string): number {\n    const times = this.speedMetrics.get(prompt) || [];\n    return times.reduce((a, b) => a + b, 0) / times.length;\n  }\n}\n```\n\nExample Output:\n```\nAction \"Fill form\" took 1000ms\nAction \"Click submit\" took 2000ms\nAction \"Confirm submission\" took 5000ms\n```\n\n### Before vs After Benchmarking\n\n```typescript\n// Before optimization\nconsole.time(\"workflow\");\nawait stagehand.act(\"Fill form\");\nawait stagehand.act(\"Click submit\");\nawait stagehand.act(\"Confirm submission\");\nconsole.timeEnd(\"workflow\"); // 8000ms\n\n// After optimization with observe planning\nconsole.time(\"workflow-optimized\");\nconst workflowActions = await stagehand.observe(\"Find form, submit, and confirm elements\");\n\n// Execute actions sequentially to avoid conflicts\nfor (const action of workflowActions) {\n  await stagehand.act(action);\n}\nconsole.timeEnd(\"workflow-optimized\"); // 500ms\n```\n\nExample Output:\n```\nWorkflow took 8000ms\nOptimized workflow took 500ms\n```\n\n<CardGroup cols={1}>\n<Card title=\"Observability & Metrics\" icon=\"chart-line\" href=\"/configuration/observability\">\n  Set up comprehensive performance monitoring\n</Card>\n</CardGroup>\n\n\n## Related Resources\n\n<CardGroup cols={2}>\n<Card title=\"Caching Strategies\" icon=\"database\" href=\"/best-practices/caching\">\n  Advanced caching patterns for maximum performance\n</Card>\n\n<Card title=\"Cost Optimization\" icon=\"dollar-sign\" href=\"/best-practices/cost-optimization\">\n  Balance speed improvements with cost considerations\n</Card>\n\n<Card title=\"Browser Configuration\" icon=\"window-maximize\" href=\"/configuration/browser\">\n  Optimize Browserbase settings for speed\n</Card>\n\n<Card title=\"Model Selection\" icon=\"brain\" href=\"/configuration/models\">\n  Choose the right model for speed vs accuracy\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/best-practices/usecase-observe.mdx",
    "content": "---\nsidebarTitle: Use Cases\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Real-World Use Cases\n\n### E-commerce Product Discovery\n\n```typescript\n// Discover product interaction elements\nconst productActions = await stagehand.observe({\n  instruction: \"Find add to cart buttons, size selectors, and product images\"\n});\n\n// Categorize actions by type\nconst cartButtons = productActions.filter(a => \n  a.description.toLowerCase().includes('cart')\n);\nconst sizeOptions = productActions.filter(a => \n  a.description.toLowerCase().includes('size')\n);\n\n// Execute purchase workflow\nif (sizeOptions.length > 0) {\n  await stagehand.act(sizeOptions[0]); // Select size first\n}\nif (cartButtons.length > 0) {\n  await stagehand.act(cartButtons[0]); // Then add to cart\n}\n```\n\n### Form Handling & Validation\n\n```typescript\n// Analyze form structure before filling\nconst formElements = await stagehand.observe({\n  instruction: \"Find form fields, validation messages, and submit buttons\"\n});\n\n// Check for required fields\nconst requiredFields = formElements.filter(e => \n  e.description.includes('required') || e.description.includes('*')\n);\n\nconsole.log(`Found ${requiredFields.length} required fields to complete`);\n\n// Fill form systematically\nfor (const field of requiredFields) {\n  await stagehand.act(field);\n  // Add appropriate input based on field type\n}\n```\n\n### Dynamic Content & SPA Navigation\n\n```typescript\n// Wait for and discover dynamically loaded content\nawait page.waitForLoadState('networkidle');\n\nconst dynamicElements = await stagehand.observe({\n  instruction: \"Find newly loaded content, infinite scroll triggers, or loading indicators\",\n  domSettleTimeoutMs: 15000 // Wait longer for dynamic content\n});\n\n// Handle infinite scroll\nconst scrollTriggers = dynamicElements.filter(e => \n  e.description.toLowerCase().includes('load more') ||\n  e.description.toLowerCase().includes('scroll')\n);\n\nif (scrollTriggers.length > 0) {\n  await stagehand.act(scrollTriggers[0]);\n  // Recursively observe new content\n  const newContent = await stagehand.observe(\"Find additional items\");\n}\n```\n\n### Multi-Step Workflow Planning\n\n```typescript\n// Plan entire checkout flow upfront\nasync function planCheckoutWorkflow() {\n  // Step 1: Cart page analysis\n  await page.goto('/cart');\n  const cartActions = await stagehand.observe(\"Find checkout and cart modification options\");\n  \n  // Step 2: Checkout page analysis  \n  const checkoutButton = cartActions.find(a => a.description.includes('checkout'));\n  if (checkoutButton) await stagehand.act(checkoutButton);\n  \n  const checkoutActions = await stagehand.observe(\"Find payment forms and shipping options\");\n  \n  // Step 3: Plan execution order\n  const shippingFields = checkoutActions.filter(a => a.description.includes('shipping'));\n  const paymentFields = checkoutActions.filter(a => a.description.includes('payment'));\n  const submitButton = checkoutActions.find(a => a.description.includes('complete order'));\n  \n  return { shippingFields, paymentFields, submitButton };\n}\n\n// Execute planned workflow\nconst workflow = await planCheckoutWorkflow();\n// Fill shipping → payment → submit\n```\n"
  },
  {
    "path": "packages/docs/v3/best-practices/user-data.mdx",
    "content": "---\ntitle: User Data Directory\nsidebarTitle: User Data\ndescription: Persist browser data between sessions\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n### User Data Directory\n\nPersist browser data between sessions.\n\n#### Local Sessions\n\nFor local sessions, use the `userDataDir` option:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    userDataDir: \"./browser-data\",\n  },\n});\n\nawait stagehand.init();\n```\n\n#### Browserbase Sessions\n\nFor Browserbase sessions, use [contexts](https://docs.browserbase.com/features/contexts) to persist browser data:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionCreateParams: {\n    browserSettings: {\n      context: {\n        id: \"my-context-id\",\n        persist: true,\n      },\n    },\n  },\n});\n\nawait stagehand.init();\nconsole.log(\"Session ID:\", stagehand.sessionId);\n```"
  },
  {
    "path": "packages/docs/v3/best-practices/using-multiple-tabs.mdx",
    "content": "---\ntitle: 'Using Multiple Tabs'\ndescription: 'Act on multiple tabs with Stagehand'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nMany modern web applications open new tabs when users click certain buttons or links. Without proper multitab support, automation scripts break when expected content appears in a new tab rather than the current one. Stagehand's multitab capabilities ensure your automations work seamlessly across multitab workflows.\n\n## The Stagehand Page\n\nStagehand automatically adapts to multitab workflows. The active page (accessed via `context.activePage()`) always points to the most recently opened or active tab, ensuring your automations continue working even when new tabs are created.\n\nThis means you can continue using familiar patterns:\n\n```typescript\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\nawait stagehand.act(\"click the button that opens a new tab\");\n// page now automatically points to the new tab\nawait stagehand.extract(\"get data from new tab\");\n```\n\n<Warning>\n**Important**: [Stagehand Agent](/v3/basics/agent) will always operate on the active page. If you need an agent to work across specific tabs, you'll need to manage page switching manually.\n</Warning>\n\n## Manual Page Management\n\nFor more control or multitab workflows, you can manage multiple tabs explicitly:\n\n```typescript\n// Create a second page\nawait stagehand.context.newPage();\nconst pages = stagehand.context.pages();\n\nconst githubPage = pages[0];\nconst pythonPage = pages[1];\n\n// Navigate each page to different repositories\nawait githubPage.goto(\"https://github.com/browserbase/stagehand\");\nawait pythonPage.goto(\"https://github.com/browserbase/stagehand-python\");\n\n// Extract data from both pages simultaneously\nconst [stagehandStars, stagehandPythonStars] = await Promise.all([\n  stagehand.extract(\"extract the repository stars\", { page: githubPage }),\n  stagehand.extract(\"extract the repository stars\", { page: pythonPage })\n]);\n\nconsole.log(`Stagehand stars: ${stagehandStars}`);\nconsole.log(`Stagehand-Python stars: ${stagehandPythonStars}`);\n```\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Orchestrate complex workflows with Agent\" icon=\"robot\" iconType=\"sharp-solid\" href=\"/v3/basics/agent\">\n    Use `Agent` to autonomously execute multi-step tasks and complex workflows.\n  </Card>\n\n  <Card title=\"Working with iframes\" icon=\"frame\" iconType=\"sharp-solid\" href=\"/v3/best-practices/working-with-iframes\">\n    Learn best practices for interacting with elements inside iframes.\n  </Card>\n\n  <Card title=\"Browser Configuration\" icon=\"browser\" iconType=\"sharp-solid\" href=\"/v3/configuration/browser\">\n    Manage browser contexts and sessions for complex automation scenarios.\n  </Card>\n\n  <Card title=\"Logging & Debugging\" icon=\"bug\" iconType=\"sharp-solid\" href=\"/v3/configuration/logging\">\n    Handle errors gracefully and debug automation issues effectively.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/configuration/browser.mdx",
    "content": "---\ntitle: Browser\nsidebarTitle: Browser\ndescription: Configure Stagehand on Browserbase or locally\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nStagehand supports two primary environments:\n\n- **Browserbase** - Cloud-managed browser infrastructure optimized for production web automation at scale\n- **Local** - Run browsers directly on your machine for development and debugging\n\n## Browserbase Environment\n\nBrowserbase provides managed cloud browser infrastructure optimized for web automation at scale. It offers advanced features like stealth mode, proxy support, and persistent contexts.\n\n<Card icon=\"cloud\" title=\"Browserbase\" href=\"https://docs.browserbase.com\" description=\"Explore the features and benefits of using Browserbase for scalable web automation.\">\n  Discover the power of cloud-managed browser infrastructure with Browserbase.\n</Card>\n\n### Multi-Region Support\n\nStagehand API is available in multiple regions to optimize latency and support data residency requirements. The SDK automatically routes requests to the correct regional API endpoint based on your browser session's region.\n\n| Region | API Endpoint |\n| --- | --- |\n| **us-west-2** (Default) | https://api.stagehand.browserbase.com |\n| **us-east-1** | https://api.use1.stagehand.browserbase.com |\n| **eu-central-1** | https://api.euc1.stagehand.browserbase.com |\n| **ap-southeast-1** | https://api.apse1.stagehand.browserbase.com |\n\nConfigure your browser session region in `browserbaseSessionCreateParams`:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionCreateParams: {\n    region: \"eu-central-1\", // Browser runs in Frankfurt\n  },\n});\n\nawait stagehand.init();\n```\n\n<Warning>\nThe API endpoint must match your browser session region. If there's a mismatch, you'll receive an error:\n`Session is in region 'X' but this API instance serves 'Y'. Please route your request to the X Stagehand API endpoint.`\n</Warning>\n\n### Disabling Stagehand API\n\nIf you want to use Stagehand purely as a local library without routing through the Stagehand API, you can disable API mode:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  disableAPI: true, // Disable Stagehand API - runs locally with Browserbase\n});\n\nawait stagehand.init();\n```\n\n<Tip>\nDisabling the API is useful when you want to manage browser sessions directly while still using Stagehand's automation features locally.\n</Tip>\n\n### Environment Variables\n\nBefore getting started, set up the required environment variables:\n\n<CodeGroup>\n```bash .env\nBROWSERBASE_API_KEY=your_api_key_here\nBROWSERBASE_PROJECT_ID=your_project_id_here\n```\n</CodeGroup>\n\n<Tip>\nGet your API key and Project ID from the [Browserbase Dashboard](https://browserbase.com/overview)\n</Tip>\n\n### Using Stagehand with Browserbase\n\n#### Basic Setup\n\nThe simplest way to get started is with default settings:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n});\n\nawait stagehand.init();\n```\n\n#### Advanced Configuration\n\nConfigure browser settings, proxy support, and other session parameters:\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  // Optional: API Key and Project ID will be pulled directly from your environment\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  browserbaseSessionCreateParams: {\n    proxies: true,\n    region: \"us-west-2\",\n    browserSettings: {\n      viewport: { width: 1920, height: 1080 },\n      blockAds: true,\n    },\n  },\n});\n\nawait stagehand.init();\nconsole.log(\"Session ID:\", stagehand.sessionId);\n```\n\n<Accordion title=\"Advanced Browserbase Configuration Example\">\n    ```typescript\nconst stagehand = new Stagehand({\n      env: \"BROWSERBASE\",\n      apiKey: process.env.BROWSERBASE_API_KEY,\n      projectId: process.env.BROWSERBASE_PROJECT_ID,\n      browserbaseSessionCreateParams: {\n        projectId: process.env.BROWSERBASE_PROJECT_ID!,\n        proxies: true,\n        region: \"us-west-2\",\n        timeout: 3600, // 1 hour session timeout\n        keepAlive: true, // Available on Startup plan\n        browserSettings: {\n          advancedStealth: false, // this is a Scale Plan feature - reach out to support@browserbase.com to enable\n          blockAds: true,\n          solveCaptchas: true,\n          recordSession: false,\n          viewport: {\n            width: 1920,\n            height: 1080,\n          },\n        },\n        userMetadata: {\n          userId: \"automation-user-123\",\n          environment: \"production\",\n        },\n      },\n    });\n    ```\n</Accordion>\n\n### Alternative: Browserbase SDK\n\nIf you prefer to manage sessions directly, you can use the Browserbase SDK:\n\n```typescript\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\nconst bb = new Browserbase({ \n  apiKey: process.env.BROWSERBASE_API_KEY! \n});\n\nconst session = await bb.sessions.create({\n  projectId: process.env.BROWSERBASE_PROJECT_ID!,\n  // Add configuration options here\n});\n```\n\n#### Connecting to an Existing Session\n\nConnect to a previously created Browserbase session using its session ID:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionID: \"existing-session-uuid-here\",\n});\n\nawait stagehand.init();\nconsole.log(\"Resumed Session ID:\", stagehand.sessionId);\n```\n\n## Local Environment\n\nThe local environment runs browsers directly on your machine, providing full control over browser instances and configurations. Ideal for development, debugging, and scenarios requiring custom browser setups.\n\n### Environment Comparison\n\n| Feature | Browserbase | Local |\n| --- | --- | --- |\n| **Scalability** | High (cloud-managed) | Limited (local resources) |\n| **Stealth Features** | Advanced fingerprinting | Basic stealth |\n| **Proxy Support** | Built-in residential proxies | Manual configuration |\n| **Session Persistence** | Cloud context storage | File-based user data |\n| **Geographic Distribution** | Multi-region deployment | Single machine |\n| **Debugging** | Session recordings & logs | Direct DevTools access |\n| **Setup Complexity** | Environment variables only | Browser installation required |\n| **Cost** | Usage-based pricing | Infrastructure & maintenance |\n| **Best For** | Production, scale, compliance | Development, debugging |\n\n### Basic Local Setup\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\"\n});\n  \nawait stagehand.init();\nconsole.log(\"Session ID:\", stagehand.sessionId);\n```\n\n### Advanced Local Configuration\n\nCustomize browser launch options for local development:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    headless: false, // Show browser window\n    devtools: true, // Open developer tools\n    viewport: { width: 1280, height: 720 },\n    executablePath: '/opt/google/chrome/chrome', // Custom Chrome path\n    port: 9222, // Fixed CDP debugging port\n    args: [\n      '--no-sandbox',\n      '--disable-setuid-sandbox',\n      '--disable-web-security',\n      '--allow-running-insecure-content',\n    ],\n    userDataDir: './chrome-user-data', // Persist browser data\n    preserveUserDataDir: true, // Keep data after closing\n    chromiumSandbox: false, // Disable sandbox (adds --no-sandbox)\n    ignoreHTTPSErrors: true, // Ignore certificate errors\n    locale: 'en-US', // Set browser language\n    deviceScaleFactor: 1.0, // Display scaling\n    proxy: {\n      server: 'http://proxy.example.com:8080',\n      username: 'user',\n      password: 'pass'\n    },\n    downloadsPath: './downloads', // Download directory\n    acceptDownloads: true, // Allow downloads\n    connectTimeoutMs: 30000, // Connection timeout\n  },\n});\n\nawait stagehand.init();\n```\n\n## Advanced Configuration\n\n### Keep Alive\n\nThe `keepAlive` option controls whether the browser remains running after `stagehand.close()` is called or when the parent process exits unexpectedly (e.g., crash, `SIGTERM`, `SIGINT`).\n\nBy default, Stagehand terminates the browser and cleans up all resources when it shuts down. Setting `keepAlive: true` keeps the browser running independently so you can reconnect to it later.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  keepAlive: true,\n});\n\nawait stagehand.init();\n\n// The browser session continues running after close()\nawait stagehand.close();\n\n// Later, reconnect to the same session\nconst stagehand2 = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionID: stagehand.browserbaseSessionID,\n});\nawait stagehand2.init();\n```\n\n#### Behavior by Environment\n\n| Behavior | `keepAlive: true` | `keepAlive: false` (default) |\n| --- | --- | --- |\n| **Browserbase** | Session stays active after `close()` | Session is terminated via API |\n| **Local** | Chrome process continues running | Chrome process is killed and temp profile is removed |\n| **On crash/signal** | Browser is left running | Browser is automatically cleaned up |\n\n#### Local Environment\n\nWhen running locally with `keepAlive: true`, the Chrome process is detached from the Node.js event loop, allowing your script to exit while the browser stays open. This is useful for debugging or for handing off a browser session to another process.\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  keepAlive: true,\n  localBrowserLaunchOptions: {\n    headless: false,\n  },\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\n// Browser window stays open after the script exits\nawait stagehand.close();\n```\n\n#### Browserbase Environment\n\nOn Browserbase, `keepAlive: true` keeps the cloud session active so you can reconnect later using `browserbaseSessionID`. This is useful for long-running workflows that span multiple script executions.\n\n<Note>\nThe top-level `keepAlive` option overrides `browserbaseSessionCreateParams.keepAlive` when both are provided.\n</Note>\n\n### Fixed CDP Debugging Port\n\nSpecify a fixed Chrome DevTools Protocol (CDP) debugging port instead of using a randomly assigned one.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  localBrowserLaunchOptions: {\n    port: 9222,\n  },\n});\n\nawait stagehand.init();\n```\n\n<Tip>\nIf no `port` is specified, a random port will be assigned.\n</Tip>\n\n### DOM Settle Timeout\n\nConfigure how long Stagehand waits for the DOM to stabilize before taking actions.\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  domSettleTimeout: 3000 // Wait up to 3 seconds for DOM to settle\n});\n```\n\n#### What is DOM Settling?\n\nDOM settling ensures that:\n- **Animations complete** before interacting with elements\n- **Lazy-loaded content** has time to appear\n- **JavaScript updates** finish before actions are taken\n- **Dynamic content** is fully rendered\n\n#### When to Adjust\n\nIncrease `domSettleTimeout` for pages with:\n- Heavy animations or transitions\n- Lazy-loading or infinite scroll\n- Dynamic JavaScript frameworks (React, Vue, Angular)\n- Complex single-page applications\n\n```typescript\n// For fast, static pages\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  domSettleTimeout: 500 // Minimal wait\n});\n\n// For dynamic, animated pages\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  domSettleTimeout: 5000 // Longer wait for stability\n});\n```\n\n<Warning>\nSetting `domSettleTimeout` too low may cause actions to fail on elements that aren't ready. Setting it too high increases execution time unnecessarily.\n</Warning>\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Browserbase Authentication Errors\">\n- Verify your `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` are set correctly\n- Check that your API key has the necessary permissions\n- Ensure your Browserbase account has sufficient credits\n</Accordion>\n\n<Accordion title=\"Local Browser Launch Failures\">\n- Install Chrome or Chromium on your system\n- Set the correct `executablePath` for your Chrome installation\n- Check that required dependencies are installed (Linux: `libnss3-dev libatk-bridge2.0-dev libgtk-3-dev libxss1 libasound2`)\n</Accordion>\n\n<Accordion title=\"Session Timeout Issues\">\n- Increase session timeout in `browserbaseSessionCreateParams.timeout`\n- Use `keepAlive: true` for long-running sessions\n- Monitor session usage to avoid unexpected terminations\n</Accordion>\n</AccordionGroup>"
  },
  {
    "path": "packages/docs/v3/configuration/logging.mdx",
    "content": "---\ntitle: Logging\nsidebarTitle: Logging\ndescription: Set up logging, debugging, and error tracking for Stagehand workflows\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nStagehand provides comprehensive logging capabilities to help you debug automation workflows, track execution, and diagnose issues. Configure logging levels, structured output, and debugging tools for both development and production environments.\n\n## Quick Start\n\nChoose your logging setup based on your environment:\n\n<CodeGroup>\n```typescript Development\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  verbose: 2,  // Full debug output\n  // restOfYourConfiguration...\n});\n```\n\n```typescript Production\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  verbose: 1,  // Standard logging - less noise\n  disablePino: true,  // Disable default console logging - no console spam\n  // logger: yourProductionLogger,  // Send to observability platform like Sentry or DataDog\n  // restOfYourConfiguration...\n});\n```\n\n```typescript Testing\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  verbose: 1,\n  // Pino automatically disabled in test environments - no worker thread issues\n  // logger: yourTestLogger,  // Send to test logging framework like Jest\n  // restOfYourConfiguration...\n});\n```\n</CodeGroup>\n\n---\n\n## Operational Logging\n\nReal-time event logging during automation execution.\n\n### Verbosity Level\n\nControl how much detail you see in logs:\n\n<Tabs>\n<Tab title=\"Level 2: Debug\">\n**Use for:** Development, debugging specific issues\n\n```typescript\nconst stagehand = new Stagehand({\n  verbose: 2,  // Maximum detail\n  // restOfYourConfiguration...\n});\n```\n\n<Accordion title=\"Example Output\">\n\n```\n[12:34:56] DEBUG: Capturing DOM snapshot\n[12:34:57] DEBUG: DOM contains 847 elements\n[12:34:58] DEBUG: LLM inference started\n[12:34:59] DEBUG: LLM response: {\"selector\": \"#btn-submit\", \"method\": \"click\"}\n[12:35:00] INFO: act completed successfully\n```\n\n</Accordion>\n</Tab>\n\n<Tab title=\"Level 1: Info (Default)\">\n**Use for:** Standard operations, staging, production\n\n```typescript\nconst stagehand = new Stagehand({\n  verbose: 1,  // Default level\n  // restOfYourConfiguration...\n});\n```\n\n\n<Accordion title=\"Example Output\">\n\n```\n[12:34:56] INFO: act started\n[12:35:00] INFO: act completed successfully\n[12:35:01] INFO: extract started\n[12:35:03] INFO: extract completed\n```\n\n</Accordion>\n</Tab>\n\n<Tab title=\"Level 0: Errors Only\">\n**Use for:** Production with external monitoring, minimal noise\n\n```typescript\nconst stagehand = new Stagehand({\n  verbose: 0,  // Errors only\n  // restOfYourConfiguration...\n});\n```\n\n<Accordion title=\"Example Output\">\n\n```\n[12:35:05] ERROR: act failed: element not found\n[12:35:10] ERROR: navigation timeout exceeded\n```\n\n</Accordion>\n</Tab>\n</Tabs>\n\n---\n\n### Log Destinations\n\nLogs can be sent to different destinations, including your console and external observability platforms:\n\n<Tabs>\n<Tab title=\"Pino (Default)\">\nFast, structured, colorized JSON logger with console output.\n\n**When to use:** Development, staging, or production without external observability; can manage multiple Stagehand instances\n\n```typescript\n// Enabled by default - Pino handles console output automatically\nconst stagehand = new Stagehand({\n  verbose: 1,\n  // restOfYourConfiguration...\n});\n```\n\n<Accordion title=\"Auto-disabled when\">\n- `process.env.NODE_ENV === \"test\"`\n- `process.env.JEST_WORKER_ID !== undefined` (Jest tests)\n- `process.env.PLAYWRIGHT_TEST_BASE_DIR !== undefined` (Playwright tests)\n- `process.env.CI === \"true\"` (CI/CD environments)\n\n**Why auto-disable?** Pino uses worker threads for pretty-printing, which can cause issues in test runners.\n</Accordion>\n</Tab>\n\n<Tab title=\"Console Fallback\">\nSimple console.log/error output.\n\n**When to use:** Automatically activated in tests, or when `disablePino: true` without setting an external logger\n\n```typescript\nconst stagehand = new Stagehand({\n  verbose: 1,\n  disablePino: true, // Set to true automatically when a test is detected\n  // restOfYourConfiguration...\n});\n```\n\n<Accordion title=\"Auto-disabled when\">\n- `process.env.NODE_ENV === \"test\"`\n- `process.env.JEST_WORKER_ID !== undefined` (Jest tests)\n- `process.env.PLAYWRIGHT_TEST_BASE_DIR !== undefined` (Playwright tests)\n- `process.env.CI === \"true\"` (CI/CD environments)\n\n**Why auto-disable?** Pino uses worker threads for pretty-printing, which can cause issues in test runners.\n</Accordion>\n</Tab>\n<Tab title=\"Custom Logger\">\nYour custom logging function to receive all logs. Works independently of Pino - receives logs regardless of Pino setting.\n\n**When to use:** Development, debugging, or when you don't need querying\ncapabilities.\n\n<Steps>\n\n<Step title=\"Create a simple logger\">\n```typescript\n// Simple logger without parsing (for basic console output)\nconst simpleLogger = (logLine: LogLine) => {\n  console.log(`[${logLine.level}] ${logLine.message}`);\n\n  // Optional: log raw auxiliary data\n  if (logLine.auxiliary) {\n    console.log('  Context:', logLine.auxiliary);\n  }\n};\n```\n</Step>\n\n<Step title=\"Pass the logger in your Stagehand instance\">\nThen pass the logger in your Stagehand instance:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  verbose: 1,\n  logger: simpleLogger,\n  disablePino: true,  // Avoid duplicate processing\n  // restOfYourConfiguration...\n})\n```\n\n</Step>\n</Steps>\n</Tab>\n\n<Tab title=\"External Logger (Production)\">\nYour custom logging function to receive all logs. Works independently of Pino - receives logs regardless of Pino setting.\n\n**When to use:** Production with DataDog, Sentry, CloudWatch, or custom observability platforms for centralized monitoring and enable error alerting. Here's examples using Sentry and DataDog:\n\n<Steps>\n\n<Step title=\"Create a production logger\">\n\n<Tabs>\n\n<Tab title=\"Sentry\">\n\n```typescript\nimport * as Sentry from \"@sentry/node\";\n\nconst productionLogger = (logLine: LogLine) => {\n  // Send errors to Sentry\n  if (logLine.level === 0) {\n    Sentry.captureMessage(logLine.message, {\n      level: 'error',\n      extra: aux,\n    });\n  }\n}\n\n// Helper to parse auxiliary data to be flat, numeric, and filterable\nfunction parseAuxiliary(aux?: LogLine['auxiliary']): Record<string, any> {\n  if (!aux) return {};\n  const parsed: Record<string, any> = {};\n  for (const [key, entry] of Object.entries(aux)) {\n    parsed[key] = entry.type === 'object'\n      ? JSON.parse(entry.value)\n      : entry.value;\n  }\n  return parsed;\n}\n```\n\n</Tab>\n<Tab title=\"DataDog\">\n\n```typescript\nimport { datadogLogs } from \"@datadog/browser-logs\";\n\nconst productionLogger = (logLine: LogLine) => {\n  // Send all logs to DataDog\n  datadogLogs.logger.log(logLine.message, {\n    status: logLine.level === 0 ? 'error' : 'info',\n    service: 'stagehand-automation',\n    category: logLine.category,\n    ...aux,\n  });\n}\n\n// Helper to parse auxiliary data to be flat, numeric, and filterable\nfunction parseAuxiliary(aux?: LogLine['auxiliary']): Record<string, any> {\n  if (!aux) return {};\n  const parsed: Record<string, any> = {};\n  for (const [key, entry] of Object.entries(aux)) {\n    parsed[key] = entry.type === 'object'\n      ? JSON.parse(entry.value)\n      : entry.value;\n  }\n  return parsed;\n}\n```\n</Tab>\n</Tabs>\n\n</Step>\n\n<Step title=\"Pass the logger in your Stagehand instance\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  verbose: 1,\n  logger: productionLogger,\n  disablePino: true,  // Avoid duplicate processing\n  // restOfYourConfiguration...\n})\n```\n\n</Step>\n</Steps>\n\n</Tab>\n</Tabs>\n\n---\n\n## File-Based Session Logging\n\nEnable detailed file-based logging for all Stagehand operations by setting a config directory. This creates comprehensive logs for `agent.execute`, `act`, `observe`, `extract`, CDP events, and LLM requests/responses.\n\n### Setup\n\nAdd to your shell configuration (`~/.zshrc`, `~/.bashrc`, etc.):\n\n```bash\nexport BROWSERBASE_CONFIG_DIR=~/.config/browserbase\n```\n\nThen reload your shell or run `source ~/.zshrc`.\n\n### Usage\n\nRun your Stagehand script as normal:\n\n```bash\ntsx run_some_script_that_imports_stagehand.ts\n```\n\nLogs are written to `~/.config/browserbase/sessions/<session-id>/` with a `latest` symlink pointing to the most recent session.\n\n### Viewing Logs\n\n<Tabs>\n<Tab title=\"Real-time Monitoring\">\nFollow all logs as they happen:\n\n```bash\ntail -f ~/.config/browserbase/sessions/latest/*.log\n```\n\nOr watch specific log types:\n\n```bash\n# LLM requests and responses only\ntail -f ~/.config/browserbase/sessions/latest/llm_events.log\n\n# CDP (Chrome DevTools Protocol) events only\ntail -f ~/.config/browserbase/sessions/latest/cdp_events.log\n```\n</Tab>\n\n<Tab title=\"Chronological Review\">\nView unified output sorted by timestamp:\n\n```bash\ncat ~/.config/browserbase/sessions/latest/*.log | sort\n```\n</Tab>\n\n<Tab title=\"Historical Sessions\">\nBrowse previous session logs:\n\n```bash\nls ~/.config/browserbase/sessions/\n# Output: 2025-01-06_14-30-45_abc123  2025-01-06_15-45-12_def456  latest\n\ncat ~/.config/browserbase/sessions/2025-01-06_14-30-45_abc123/*.log | sort\n```\n</Tab>\n</Tabs>\n\n### Log Files\n\nEach session directory contains:\n\n| File | Contents |\n|------|----------|\n| `llm_events.log` | LLM requests and responses for act, extract, observe, and agent operations |\n| `cdp_events.log` | Chrome DevTools Protocol calls and events |\n| `stagehand.log` | General Stagehand operations and state changes |\n\n<Note>\nThis is especially useful for debugging agent workflows where you need to trace the full sequence of LLM decisions, browser actions, and CDP interactions.\n</Note>\n\n---\n\n## LLM Inference Debugging\n\n<Warning>\n**Development only** - Creates large files and contains page content. Do not use in production.\n</Warning>\n\nSave complete LLM request/response dumps to disk for offline analysis. See exactly what DOM was sent to the LLM and why it chose the wrong element.\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  verbose: 2,\n  logInferenceToFile: true,  // Writes files to ./inference_summary/\n});\n```\n\nCreates timestamped files for each LLM call:\n\n```\n./inference_summary/\n├── act_summary/\n│   ├── act_summary.json                      # Aggregate metrics\n│   ├── 20250127_123456_act_call.txt          # LLM request\n│   ├── 20250127_123456_act_response.txt      # LLM response\n│   ├── 20250127_123501_act_call.txt\n│   └── 20250127_123501_act_response.txt\n├── extract_summary/\n│   ├── extract_summary.json\n│   ├── 20250127_123510_extract_call.txt\n│   ├── 20250127_123510_extract_response.txt\n│   ├── 20250127_123511_metadata_call.txt\n│   └── 20250127_123511_metadata_response.txt\n└── observe_summary/\n    ├── observe_summary.json\n    └── ...\n```\n\n**File Types:**\n\n<AccordionGroup>\n<Accordion title=\"Call File\">\nContains the complete LLM request:\n\n```json\n{\n  \"modelCall\": \"act\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are a browser automation assistant. You have access to these actions:\\n- click\\n- type\\n- scroll\\n...\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"Click the sign in button\\n\\nDOM:\\n<html>\\n  <body>\\n    <button id=\\\"btn-1\\\">Sign In</button>\\n    <button id=\\\"btn-2\\\">Sign Up</button>\\n  </body>\\n</html>\"\n    }\n  ]\n}\n```\n</Accordion>\n\n<Accordion title=\"Response File\">\nContains the LLM output:\n\n```json\n{\n  \"modelResponse\": \"act\",\n  \"rawResponse\": {\n    \"selector\": \"#btn-1\",\n    \"method\": \"click\",\n    \"reasoning\": \"Found sign in button with ID btn-1\"\n  }\n}\n```\n</Accordion>\n\n<Accordion title=\"Summary File\">\nAggregates all calls with metrics:\n\n```json\n{\n  \"act_summary\": [\n    {\n      \"act_inference_type\": \"act\",\n      \"timestamp\": \"20250127_123456\",\n      \"LLM_input_file\": \"20250127_123456_act_call.txt\",\n      \"LLM_output_file\": \"20250127_123456_act_response.txt\",\n      \"prompt_tokens\": 3451,\n      \"completion_tokens\": 45,\n      \"inference_time_ms\": 951\n    },\n    {\n      \"act_inference_type\": \"act\",\n      \"timestamp\": \"20250127_123501\",\n      \"LLM_input_file\": \"20250127_123501_act_call.txt\",\n      \"LLM_output_file\": \"20250127_123501_act_response.txt\",\n      \"prompt_tokens\": 2890,\n      \"completion_tokens\": 38,\n      \"inference_time_ms\": 823\n    }\n  ]\n}\n```\n</Accordion>\n</AccordionGroup>\n\n---\n\n## Reference\n\n### Logging Configuration\n\nAll logging options are passed to the Stagehand constructor:\n\n```typescript\nconst stagehand = new Stagehand({\n  // ... your other configurations (env, model, etc.)\n\n  // Logging options:\n  verbose?: 0 | 1 | 2;                   // Log level (default: 1)\n  logger?: (line: LogLine) => void;      // External logger function\n  disablePino?: boolean;                 // Disable Pino backend (default: false)\n  logInferenceToFile?: boolean;          // Save LLM requests to disk (default: false)\n});\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `verbose` | `1` | Log level: `0` = errors only, `1` = info, `2` = debug |\n| `logger` | `undefined` | Custom logger function for external platforms |\n| `disablePino` | `false` | Disable Pino (auto `true` in tests) |\n| `logInferenceToFile` | `false` | Save LLM requests to disk (default: false) |\n\n### Log Structure\n\nEach log entry follows a structured format:\n\n```typescript\ninterface LogLine {\n  message: string;              // \"act completed successfully\"\n  level?: 0 | 1 | 2;            // error | info | debug\n  category?: string;            // \"action\", \"llm\", \"browser\", \"cache\"\n  timestamp?: string;           // ISO 8601 timestamp\n  auxiliary?: {                 // Additional structured metadata\n    [key: string]: {\n      value: string;             // Serialized value\n      type: \"object\" | \"string\" | \"integer\" | \"float\" | \"boolean\";\n    };\n  };\n}\n```\n\n<Accordion title=\"Log Examples\">\n\n<Tabs>\n<Tab title=\"Successful Action\">\n```json\n{\n  \"category\": \"action\",\n  \"message\": \"act completed successfully\",\n  \"level\": 1,\n  \"timestamp\": \"2025-01-27T12:35:00.123Z\",\n  \"auxiliary\": {\n    \"selector\": {\n      \"value\": \"#btn-submit\",\n      \"type\": \"string\"\n    },\n    \"executionTime\": {\n      \"value\": \"1250\",\n      \"type\": \"integer\"\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"LLM Inference\">\n```json\n{\n  \"category\": \"llm\",\n  \"message\": \"inference completed\",\n  \"level\": 1,\n  \"timestamp\": \"2025-01-27T12:34:58.456Z\",\n  \"auxiliary\": {\n    \"model\": {\n      \"value\": \"gpt-4o\",\n      \"type\": \"string\"\n    },\n    \"promptTokens\": {\n      \"value\": \"3451\",\n      \"type\": \"integer\"\n    },\n    \"completionTokens\": {\n      \"value\": \"45\",\n      \"type\": \"integer\"\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Error\">\n```json\n{\n  \"category\": \"action\",\n  \"message\": \"action failed: element not found\",\n  \"level\": 0,\n  \"timestamp\": \"2025-01-27T12:35:05.789Z\",\n  \"auxiliary\": {\n    \"selector\": {\n      \"value\": \"#missing-btn\",\n      \"type\": \"string\"\n    },\n    \"url\": {\n      \"value\": \"https://example.com/form\",\n      \"type\": \"string\"\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n</Accordion>\n\n---\n\n## Next Steps\n\nNow that you have logging configured, explore additional debugging and monitoring tools in [the Observability guide](/v3/configuration/observability):\n\n<CardGroup cols={2}>\n<Card title=\"History API\" icon=\"clock-rotate-left\" href=\"/v3/best-practices/history\">\nTrack all LLM operations (act, extract, observe, agent) with parameters, results, and timestamps. Perfect for debugging sequences and replaying workflows.\n</Card>\n\n<Card title=\"Metrics API\" icon=\"chart-line\" href=\"/v3/configuration/observability#real-time-metrics-%26-monitoring\">\nMonitor token usage and performance in real-time. Track costs per operation, identify expensive calls, and optimize resource usage.\n</Card>\n\n<Card title=\"LLM Inference Debugging\" icon=\"microscope\" href=\"/v3/configuration/logging#llm-inference-debugging\">\nSave complete LLM request/response dumps to disk. See exactly what DOM was sent to the LLM and why it made specific decisions.\n</Card>\n\n<Card title=\"Browserbase Session Monitoring\" icon=\"video\" href=\"/v3/configuration/observability#browserbase-session-monitoring\">\nWatch your automation visually with session recordings, network monitoring, and real-time browser inspection (Browserbase only).\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/configuration/models.mdx",
    "content": "---\ntitle: Models\nsidebarTitle: Models\ndescription: Use any LLM model with Stagehand for optimal performance\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nUnderstand web pages, plan actions, and interact with complex interfaces with Google, OpenAI, Anthropic, xAI, DeepSeek, Perplexity, Azure, Ollama, the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway), or any other LLM model from [the Vercel AI SDK](https://sdk.vercel.ai/providers).\n\n---\n\n## Configuration Setup\n\n### Quick Start\n\n<Tip>\n  Set your API key in `.env` and Stagehand handles the rest. No explicit\n  configuration needed!\n</Tip>\n\nGet started with Google Gemini (recommended for speed and cost):\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"google/gemini-2.5-flash\"\n  // API key auto-loads from GOOGLE_GENERATIVE_AI_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n\n\n---\n\n### First Class Models\n\nUse any model from the following supported providers.\n\n<Tabs>\n<Tab title=\"Google\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"google/gemini-2.5-flash\"\n  // API key auto-loads from GOOGLE_GENERATIVE_AI_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n[View all supported Google models →](https://ai.google.dev/gemini-api/docs/models)\n</Tab>\n\n<Tab title=\"Google Vertex\">\n\n<Warning>\nGoogle Vertex requires `experimental: true` in the Stagehand constructor.\n</Warning>\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  experimental: true, // required for Vertex\n  model: {\n    modelName: \"vertex/gemini-3-flash-preview\",\n    project: \"your-gcp-project-id\",\n    location: \"us-central1\",\n    googleAuthOptions: {\n      credentials: {\n        client_email: \"your-sa@project.iam.gserviceaccount.com\",\n        private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY,\n      },\n    },\n  },\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n\nThe `model` object accepts:\n- `modelName` — The Vertex model, prefixed with `vertex/` (e.g. `vertex/gemini-3-flash-preview`)\n- `project` — Your GCP project ID\n- `location` — Your Vertex AI region (e.g. `us-central1`)\n- `googleAuthOptions.credentials` — Service account credentials with `client_email` and `private_key`\n\n[View all supported Vertex AI models →](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models)\n</Tab>\n\n<Tab title=\"Anthropic\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"anthropic/claude-haiku-4-5\"\n  // API key auto-loads from ANTHROPIC_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n[View all supported Anthropic models →](https://docs.anthropic.com/en/docs/models-overview)\n</Tab>\n\n<Tab title=\"OpenAI\">\n\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"openai/gpt-5\"\n  // API key auto-loads from OPENAI_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n[View all supported OpenAI models →](https://platform.openai.com/docs/models)\n</Tab>\n<Tab title=\"Azure\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"azure/gpt-5\"\n  // API key auto-loads from AZURE_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n[View all supported Azure models →](https://ai.azure.com/catalog)\n</Tab>\n\n<Tab title=\"Cerebras\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"cerebras/llama-4-scout\"\n  // API key auto-loads from CEREBRAS_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n[View all supported Cerebras models →](https://inference-docs.cerebras.ai/models/overview)\n</Tab>\n\n<Tab title=\"DeepSeek\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"deepseek/deepseek-chat\"\n  // API key auto-loads from DEEPSEEK_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n[View all supported DeepSeek models →](https://api-docs.deepseek.com/quick_start/pricing)\n</Tab>\n\n<Tab title=\"Groq\">\n\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"groq/llama-3.1-8b-instant\"\n  // API key auto-loads from GROQ_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n[View all supported Groq models →](https://console.groq.com/docs/models)\n</Tab>\n\n<Tab title=\"Mistral\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"mistral/codestral-2508\"\n  // API key auto-loads from MISTRAL_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n[View all supported Mistral models →](https://docs.mistral.ai/getting-started/models)\n</Tab>\n\n<Tab title=\"Ollama\">\n\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"ollama/llama3.2\"\n  // No API key required\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n[View all supported Ollama models →](https://ollama.com/library)\n</Tab>\n\n<Tab title=\"Perplexity\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"perplexity/sonar-reasoning\"\n  // API key auto-loads from PERPLEXITY_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n[View all supported Perplexity models →](https://docs.perplexity.ai/getting-started/models)\n</Tab>\n<Tab title=\"TogetherAI\">\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"togetherai/Qwen/Qwen3-235B-A22B-Instruct-2507-tput\"\n  // API key auto-loads from TOGETHER_AI_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n```\n\n</CodeGroup>\n[View all supported TogetherAI models →](https://www.together.ai/models)\n\n</Tab>\n<Tab title=\"xAI\">\n\n<CodeGroup>\n```typescript TypeScript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"xai/grok-4-fast-reasoning\"\n  // API key auto-loads from XAI_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n\n```\n</CodeGroup>\n\n[View all xAI models →](https://docs.x.ai/docs/models)\n</Tab>\n\n</Tabs>\n\n---\n\n### Custom Models\n\nAmazon Bedrock, Cohere, all [first class models](/v3/configuration/models#first-class-models), and any model from [the Vercel AI SDK](https://sdk.vercel.ai/providers) is supported.\n\nUse this configuration for custom endpoints and custom retry or caching logic.\n\nWe'll use Amazon Bedrock and Google as examples below.\n\n<AccordionGroup>\n<Accordion title=\"Amazon Bedrock\">\n\n<Steps>\n<Step title=\"Install dependencies\">\nInstall the Vercel AI SDK for your provider.\n\n<Tabs>\n<Tab title=\"npm\">\n```bash\nnpm install @ai-sdk/amazon-bedrock\n```\n\n</Tab>\n<Tab title=\"pnpm\">\n```bash\npnpm add @ai-sdk/amazon-bedrock\n```\n</Tab>\n<Tab title=\"yarn\">\n```bash\nyarn add @ai-sdk/amazon-bedrock\n```\n</Tab>\n<Tab title=\"bun\">\n```bash\nbun add @ai-sdk/amazon-bedrock\n```\n</Tab>\n</Tabs>\n</Step>\n\n<Step title=\"Import, create provider, and create client\">\n```typescript\nimport { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';\nimport { AISdkClient } from '@browserbasehq/stagehand';\n\nconst bedrockProvider = createAmazonBedrock({\n  region: 'us-east-1',\n  accessKeyId: 'xxxxxxxxx',\n  secretAccessKey: 'xxxxxxxxx',\n  sessionToken: 'xxxxxxxxx',\n});\n\nconst bedrockClient = new AISdkClient({\n  model: bedrockProvider(\"amazon/nova-pro-latest\"),\n});\n\n```\n</Step>\n\n<Step title=\"Pass client to Stagehand\">\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  llmClient: bedrockClient\n});\n\nawait stagehand.init();\n```\n\n</Step>\n</Steps>\n</Accordion>\n<Accordion title=\"Google\">\n\n<Steps>\n<Step title=\"Install dependencies\">\nInstall the Vercel AI SDK for your provider.\n\n<Tabs>\n<Tab title=\"npm\">\n```bash\nnpm install @ai-sdk/google\n```\n</Tab>\n<Tab title=\"pnpm\">\n```bash\npnpm add @ai-sdk/google\n```\n</Tab>\n<Tab title=\"yarn\">\n```bash\nyarn add @ai-sdk/google\n```\n</Tab>\n<Tab title=\"bun\">\n```bash\nbun add @ai-sdk/google\n```\n</Tab>\n</Tabs>\n</Step>\n\n<Step title=\"Import, create provider, and create client\">\n```typescript\nimport { createGoogle } from '@ai-sdk/google';\nimport { AISdkClient } from '@browserbasehq/stagehand';\n\nconst googleProvider = createGoogle({\n  apiKey: process.env.GEMINI_API_KEY,\n});\n\nconst googleClient = new AISdkClient({\n  model: googleProvider(\"google/gemini-2.5-flash\"),\n});\n\n```\n</Step>\n\n<Step title=\"Pass client to Stagehand\">\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  llmClient: googleClient\n});\n\nawait stagehand.init();\n```\n\n</Step>\n</Steps>\n</Accordion>\n<Accordion title=\"All Providers\">\n\nTo implement a custom model, follow the steps for the provider you are using. See the Amazon Bedrock and Google examples above. All supported providers and models are in [the Vercel AI SDK](https://sdk.vercel.ai/providers).\n\n<Steps>\n<Step title=\"Install dependencies\">\nInstall the Vercel AI SDK for your provider.\n</Step>\n<Step title=\"Import, create provider, and create client\">\n```typescript\nimport { createProvider } from '@ai-sdk/provider';\nimport { AISdkClient } from '@browserbasehq/stagehand';\n\nconst provider = createProvider({\n  apiKey: 'xxxxxxxxx',\n});\n\nconst providerClient = new AISdkClient({\n  model: provider(\"model/name\"),\n});\n\n```\n</Step>\n<Step title=\"Pass client to Stagehand\">\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  llmClient: providerClient\n});\n\nawait stagehand.init();\n```\n\n</Step>\n</Steps>\n</Accordion>\n</AccordionGroup>\n\n\n---\n\n## Choose a Model\n\nDifferent models excel at different tasks. Consider speed, accuracy, and cost for your use case.\n\n<Card title=\"Model Selection Guide\" href=\"https://www.stagehand.dev/evals\" icon=\"scale-balanced\">\n  Find detailed model comparisons and recommendations on our Model Evaluation page.\n</Card>\n\n**Quick Recommendations**\n\n| Use Case                  | Recommended Model                    | Why                            |\n| ------------------------- | ------------------------------------ | ------------------------------ |\n| **Production** | `google/gemini-2.5-flash`            | Fast, accurate, cost-effective |\n| **Intelligence**     | `google/gemini-3-pro-preview` | Best accuracy on hard tasks    |\n| **Speed**        | `google/gemini-2.5-flash`                 | Fastest response times         |\n| **Cost**     | `google/gemini-2.5-flash`            | Best value per token           |\n| **Local/offline**         | `ollama/qwen3`                    | No API costs, full control     |\n\n\n---\n\n## Advanced Options\n\n### Agent Models (with CUA Support)\n\n**Default**\n\nThe Stagehand agent by default uses the same model passed to Stagehand. All models ([first class](/v3/configuration/models#first-class-models) and [custom](/v3/configuration/models#custom-models)) are supported. Here's an example with Gemini:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"google/gemini-2.5-flash\",\n  // GOOGLE_GENERATIVE_AI_API_KEY is auto-loaded from .env\n  // ... other stagehand options\n});\n\n// Agent will use google/gemini-2.5-flash\nconst agent = stagehand.agent();\n```\n\n**Override (with CUA support)**\n\nHowever, the stagehand agent also accepts a `model` parameter, which accepts any [first class](/v3/configuration/models#first-class-models) model, including [computer use agents (CUA)](/v3/configuration/models#agent-models-with-cua-support). This is useful when you'd like the agent to use a different model than the one passed to Stagehand.\n\n<Tip>\n  To use a CUA model, you must pass the `mode: \"cua\"` parameter to the `agent()` method. If a non-CUA model is used, whether specified in Stagehand or overridden in the `agent()` method, an error will be thrown.\n</Tip>\n\n<Warning>\n**Deprecation Notice:** The `cua: true` option is deprecated and will be removed in a future version. Use `mode: \"cua\"` instead.\n</Warning>\n\n<Tabs>\n<Tab title=\"Google CUA\">\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: \"google/gemini-2.5-computer-use-preview-10-2025\",\n  // GOOGLE_GENERATIVE_AI_API_KEY is auto-loaded from .env\n  // ... other agent options\n});\n```\n</Tab>\n<Tab title=\"Anthropic CUA\">\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: \"anthropic/claude-sonnet-4-6\",\n  // ANTHROPIC_API_KEY is auto-loaded from .env\n  // ... other agent options\n});\n```\n</Tab>\n<Tab title=\"OpenAI CUA\">\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: \"openai/computer-use-preview\",\n  // OPENAI_API_KEY is auto-loaded from .env\n  // ... other agent options\n});\n```\n</Tab>\n<Tab title=\"Example First Class Model\">\nAll [first class models](/v3/configuration/models#first-class-models) are supported. Here's an example with Gemini:\n\n```typescript\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.5-pro\",\n  // GOOGLE_GENERATIVE_AI_API_KEY is auto-loaded from .env\n  // ... other agent options\n});\n```\n</Tab>\n</Tabs>\n\n<Accordion title=\"All Supported CUA Models\">\n| Provider | Model |\n| -------- | ----- |\n| Anthropic | `anthropic/claude-haiku-4-5-20251001` |\n| Anthropic | `anthropic/claude-sonnet-4-6` |\n| Anthropic | `anthropic/claude-sonnet-4-5-20250929` |\n| Anthropic | `anthropic/claude-opus-4-5-20251101` |\n| Anthropic | `anthropic/claude-opus-4-6` |\n| Google   | `google/gemini-2.5-computer-use-preview-10-2025` |\n| Google   | `google/gemini-3-flash-preview` |\n| Google   | `google/gemini-3-pro-preview` |\n| Microsoft | `microsoft/fara-7b` |\n| OpenAI   | `openai/computer-use-preview` |\n| OpenAI   | `openai/computer-use-preview-2025-03-11` |\n</Accordion>\n\n<Note>\n  For overriding the agent API key, using a corporate proxy, adding provider-specific options, or other advanced use cases, the agent model can also take the form of an object. To learn more, see the [Agent Reference](/v3/references/agent).\n</Note>\n---\n\n### Custom Endpoints\n\nIf you need Azure OpenAI deployments or enterprise deployments.\n\n<Tabs>\n<Tab title=\"OpenAI\">\n\nFor OpenAI, you can pass configuration directly without using `llmClient` using the `model` parameter:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: {\n    modelName: \"openai/gpt-5\",\n    apiKey: process.env.OPENAI_API_KEY,\n    baseURL: \"https://custom-openai-endpoint.com/v1\"\n  }\n});\n```\n\n</Tab>\n\n<Tab title=\"Anthropic\">\n\nFor Anthropic, you can pass configuration directly without using `llmClient` using the `model` parameter:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: {\n    modelName: \"anthropic/claude-haiku-4-5\",\n    apiKey: process.env.ANTHROPIC_API_KEY,\n    baseURL: \"https://custom-anthropic-endpoint.com\",\n  },\n});\n```\n\n  </Tab>\n<Tab title=\"All Other Providers\">\nFor all other providers, use `llmClient`. Here's an example with Hugging Face:\n\n```typescript\n// pnpm add @ai-sdk/huggingface\n\nimport { createHuggingFace } from \"@ai-sdk/huggingface\";\nimport { AISdkClient } from \"@browserbasehq/stagehand\";\n\nconst huggingFaceProvider = createHuggingFace({\n  apiKey: process.env.HUGGINGFACE_API_KEY,\n  baseURL: \"https://custom-huggingface-endpoint.com\",\n});\n\nconst huggingFaceClient = new AISdkClient({\n  model: huggingFaceProvider(\"meta-llama/Llama-3.1-8B-Instruct\"),\n});\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  llmClient: huggingFaceClient,\n});\n```\n\n</Tab>\n</Tabs>\n\n---\n\n### AI Gateway\n\nThe [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) lets you access models from multiple providers (OpenAI, Anthropic, Google, and more) through a single API key and interface. No extra provider SDKs or per-provider API keys needed.\n\n<Tip>\n  The AI Gateway is built into the `ai` package that Stagehand already uses -- no additional dependencies required.\n</Tip>\n\n**Key benefits:**\n- Access models from all major providers with a single `AI_GATEWAY_API_KEY`\n- Automatic provider fallback and dynamic routing based on uptime and latency\n- Usage tracking and observability through the Vercel dashboard\n- Bring Your Own Key (BYOK) support for existing provider credentials\n\n<Tabs>\n<Tab title=\"Simple\">\n\nUse the `gateway/` prefix followed by the provider and model name:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"gateway/openai/gpt-5\"\n  // API key auto-loads from AI_GATEWAY_API_KEY - set in your .env\n});\n\nawait stagehand.init();\n```\n\nWorks with any model available on the gateway:\n\n```typescript\n// Anthropic via gateway\nmodel: \"gateway/anthropic/claude-sonnet-4.5\"\n\n// Google via gateway\nmodel: \"gateway/google/gemini-3-flash-preview\"\n```\n\n</Tab>\n<Tab title=\"Custom Config\">\n\nPass the API key and optional base URL explicitly using the model object format:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: {\n    modelName: \"gateway/openai/gpt-5\",\n    apiKey: process.env.AI_GATEWAY_API_KEY,\n    baseURL: \"https://ai-gateway.vercel.sh/v3/ai\" // optional custom endpoint\n  }\n});\n\nawait stagehand.init();\n```\n\n</Tab>\n</Tabs>\n\n[View all available AI Gateway models →](https://vercel.com/docs/ai-gateway/models-and-providers)\n\n---\n\n### Extending the AI SDK Client\n\nFor advanced use cases like custom retries or caching logic, you can extend the `AISdkClient`:\n\n```typescript\nimport { LLMClient } from \"@browserbasehq/stagehand\";\n\nclass CustomRetryClient extends LLMClient {\n  async createChatCompletion(options) {\n    let retries = 3;\n    while (retries > 0) {\n      try {\n        return await super.createChatCompletion(options);\n      } catch (error) {\n        retries--;\n        if (retries === 0) throw error;\n        await new Promise((r) => setTimeout(r, 1000 * (4 - retries)));\n      }\n    }\n  }\n}\n```\n\n<Tip>\n  Need custom caching? Consider using built-in [caching\n  feature](/v3/best-practices/caching).\n</Tip>\n\n---\n\n### Legacy Model Format\n\n<Tip>\n**Recommendation:** Use `provider/model` format. Example:\n- `model: \"openai/gpt-4o\"` (recommended)\n- `model: \"gpt-4o\"` (legacy)\n\n</Tip>\n\nThe following models work without the `provider/` prefix in the model parameter as part of legacy support:\n\n<AccordionGroup title=\"Legacy Model Format\">\n<Accordion title=\"Google\">\n\n- `gemini-2.5-flash-preview-04-17`\n- `gemini-2.5-pro-preview-03-25`\n- `gemini-2.0-flash`\n- `gemini-2.0-flash-lite`\n- `gemini-1.5-flash`\n- `gemini-1.5-flash-8b`\n- `gemini-1.5-pro`\n\n</Accordion>\n<Accordion title=\"Anthropic\">\n\n- `claude-sonnet-4-6`\n- `claude-sonnet-4-5-20250929`\n- `claude-haiku-4-5-20251001`\n\n</Accordion>\n<Accordion title=\"OpenAI\">\n- `gpt-4o`\n- `gpt-4o-mini`\n- `o1`\n- `o1-mini`\n- `o3`\n- `o3-mini`\n- `gpt-4.1`\n- `gpt-4.1-mini`\n- `gpt-4.1-nano`\n- `o4-mini`\n- `gpt-4.5-preview`\n- `gpt-4o-2024-08-06`\n- `o1-preview`\n\n</Accordion>\n<Accordion title=\"Cerebras\">\n\n- `cerebras-llama-3.3-70b`\n- `cerebras-llama-3.1-8b`\n\n</Accordion>\n<Accordion title=\"Groq\">\n\n- `groq-llama-3.3-70b-versatile`\n- `groq-llama-3.3-70b-specdec`\n- `moonshotai/kimi-k2-instruct`\n\n</Accordion>\n</AccordionGroup>\n\n---\n\n## Troubleshooting\n\n<AccordionGroup>\n<Accordion title=\"Error: API key not found\">\n**Error:** `API key not found`\n\n**Solutions:**\n\n- Check `.env` file has the correct variable name for the provider you are using\n- Ensure environment variables are loaded (use `dotenv`)\n- Restart your application after updating `.env` file\n\n| Provider   | Environment Variable           |\n| ---------- | ------------------------------ |\n| Google     | `GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY` |\n| Vertex | Service account credentials (see [setup](#first-class-models)) |\n| Anthropic  | `ANTHROPIC_API_KEY`            |\n| OpenAI     | `OPENAI_API_KEY`               |\n| Azure      | `AZURE_API_KEY`                |\n| Cerebras   | `CEREBRAS_API_KEY`             |\n| DeepSeek   | `DEEPSEEK_API_KEY`             |\n| Groq       | `GROQ_API_KEY`                 |\n| Mistral    | `MISTRAL_API_KEY`              |\n| Ollama     | None (local)                   |\n| Perplexity | `PERPLEXITY_API_KEY`           |\n| TogetherAI | `TOGETHER_AI_API_KEY`          |\n| xAI        | `XAI_API_KEY`                  |\n| AI Gateway | `AI_GATEWAY_API_KEY`           |\n\n</Accordion>\n\n<Accordion title=\"Error: Model not supported\">\n**Error:** `Unsupported model`\n\n**Solutions:**\n\n- Use the `provider/model` format: `openai/gpt-5`\n- Verify the model name exists in the provider's documentation\n- Check model name is spelled correctly\n- Ensure your Model API key can access the model\n</Accordion>\n\n<Accordion title=\"Model doesn't support structured outputs\">\n**Error:** `Model does not support structured outputs`\n\n**Solutions:**\n\n- Check our [Model Evaluation page](https://www.stagehand.dev/evals) for recommended models\n</Accordion>\n\n<Accordion title=\"High costs or slow performance\">\n**Symptoms:** Automation is expensive or slow\n\n**Solutions:**\n\n- Switch to cost-effective models (check [evals](https://www.stagehand.dev/evals) for comparisons)\n- Use faster models for simple tasks, powerful ones for complex tasks\n- Implement [caching](/v3/best-practices/caching) for repeated patterns\n</Accordion>\n<Accordion title=\"Python SDK or custom models\">\nPython is now supported in Stagehand v3! The Python SDK uses a BYOB (Bring Your Own Browser) architecture.\n\n**Solutions:**\n\n- See the [Python SDK documentation](/v3/sdk/python) for installation and usage\n- Check the [Python migration guide](/v3/migrations/python) if upgrading from v2\n</Accordion>\n</AccordionGroup>\n\n### Need Help? Contact Support\n\nCan't find a solution? Have a question? Reach out to our support team:\n\n<Card\n  title=\"Contact Support\"\n  icon=\"envelope\"\n  href=\"mailto:support@browserbase.com\"\n>\n  Email us at support@browserbase.com\n</Card>\n\n---\n\n## Next Steps\n\n<CardGroup cols={2}>\n<Card title=\"Prompting Guide\" href=\"/v3/best-practices/prompting-best-practices\" icon=\"brain\">\n  Learn how to prompt LLMs for optimal results\n</Card>\n<Card title=\"Run Evals\" href=\"/v3/basics/evals\" icon=\"flask-vial\">\n  Test which models work best for your specific use case\n</Card>\n\n<Card title=\"Caching Guide\" href=\"/v3/best-practices/caching\" icon=\"database\">\n  Cache responses to reduce costs and improve speed\n</Card>\n<Card\n  title=\"Optimize Costs\"\n  href=\"/v3/best-practices/cost-optimization\"\n  icon=\"dollar-sign\"\n>\n  Reduce LLM spending with caching and smart model selection\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/configuration/observability.mdx",
    "content": "---\ntitle: Observability\nsidebarTitle: Observability\ndescription: Track Stagehand automation with session visibility and analytics\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nStagehand provides powerful observability features to help you monitor, track performance, and analyze your browser automation workflows. Focus on session monitoring, resource usage, and operational insights for both Browserbase and local environments.\n\n## Browserbase Session Monitoring\n\nWhen running on Browserbase, you gain access to comprehensive cloud-based monitoring and session management through the Browserbase API and dashboard.\n\n<div style={{ textAlign: \"center\" }}>\n  <img src=\"/media/observability.gif\" alt=\"Browserbase Session Observability\" width=\"400\" />\n</div>\n\n### Live Session Visibility\n\nBrowserbase provides real-time visibility into your automation sessions:\n\n**Session Dashboard Features**\n- Real-time browser screen recording and replay\n- Network request monitoring with detailed timing\n- JavaScript console logs and error tracking\n- CPU and memory usage metrics\n- Session status and duration tracking\n\n**Session Management & API Access**\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\nconst browserbase = new Browserbase({\n  apiKey: process.env.BROWSERBASE_API_KEY,\n});\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\"\n});\n\nawait stagehand.init();\n\nconst sessionInfo = await browserbase.sessions.retrieve(stagehand.sessionId);\n\nconsole.log(\"Session status:\", sessionInfo.status);\nconsole.log(\"Session region:\", sessionInfo.region);\nconsole.log(\"CPU usage:\", sessionInfo.avgCpuUsage);\nconsole.log(\"Memory usage:\", sessionInfo.memoryUsage);\nconsole.log(\"Proxy bytes:\", sessionInfo.proxyBytes);\n```\n\n### Session Analytics & Insights\n\n<CardGroup>\n  <Card title=\"Real-Time Monitoring\" icon=\"chart-line\">\n    Monitor live session status, resource usage, and geographic distribution. Scale and manage concurrent sessions with real-time insights.\n  </Card>\n\n  <Card title=\"Session Recordings\" icon=\"video\">\n    Review complete session recordings with frame-by-frame playback. Analyze network requests and debug browser interactions visually.\n  </Card>\n\n  <Card title=\"API Management\" icon=\"code\">\n    Programmatically access session data, automate lifecycle management, and integrate with monitoring systems through our API.\n  </Card>\n\n  <Card title=\"Usage Monitoring\" icon=\"chart-bar\">\n    Track resource consumption, session duration, and API usage. Get detailed breakdowns of costs and utilization across your automation.\n  </Card>\n</CardGroup>\n\n### Session Monitoring & Filtering\n\nQuery and monitor sessions by status and metadata:\n\n```typescript\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\nconst browserbase = new Browserbase({\n  apiKey: process.env.BROWSERBASE_API_KEY,\n});\n\n// List sessions with filtering\nasync function getFilteredSessions() {\n  const sessions = await browserbase.sessions.list({\n    status: 'RUNNING'\n  });\n  \n  return sessions.map(session => ({\n    id: session.id,\n    status: session.status, // RUNNING, COMPLETED, ERROR, TIMED_OUT\n    startedAt: session.startedAt,\n    endedAt: session.endedAt,\n    region: session.region,\n    avgCpuUsage: session.avgCpuUsage,\n    memoryUsage: session.memoryUsage,\n    proxyBytes: session.proxyBytes,\n    userMetadata: session.userMetadata\n  }));\n}\n\n// Query sessions by metadata\nasync function querySessionsByMetadata(query: string) {\n  const sessions = await browserbase.sessions.list({\n    q: query\n  });\n  \n  return sessions;\n}\n```\n\n## Local Environment Monitoring\n\nFor local development, Stagehand provides performance monitoring and resource tracking capabilities directly on your machine.\n\n### Performance Tracking\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  verbose: 1, // Monitor performance without debug noise\n});\n\nawait stagehand.init();\n\n// Track local automation metrics\nconst startTime = Date.now();\nconst initialMetrics = await stagehand.metrics;\n\n// ... perform automation tasks\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\nawait stagehand.act(\"click button\");\nawait stagehand.extract({ instruction: \"get data\", schema: DataSchema });\n\nconst finalMetrics = await stagehand.metrics;\nconst executionTime = Date.now() - startTime;\n\nconsole.log('Local Performance Summary:', {\n  executionTime: `${executionTime}ms`,\n  totalTokens: finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens,\n  totalInferenceTime: `${finalMetrics.totalInferenceTimeMs}ms`,\n  tokensPerSecond: ((finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens) / (executionTime / 1000)).toFixed(2)\n});\n```\n\n## Resource Usage Monitoring\n\nWhen running locally, monitor system resource usage and browser performance:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport * as os from 'os';\nimport { performance } from 'perf_hooks';\n\nclass LocalResourceMonitor {\n  private cpuUsage: number[] = [];\n  private memoryUsage: number[] = [];\n  \n  startMonitoring() {\n    const interval = setInterval(() => {\n      // Track system resources\n      const memUsage = process.memoryUsage();\n      this.memoryUsage.push(memUsage.heapUsed / 1024 / 1024); // MB\n      \n      // Track CPU (simplified)\n      const loadAvg = os.loadavg()[0];\n      this.cpuUsage.push(loadAvg);\n    }, 1000);\n    \n    return interval;\n  }\n  \n  getResourceSummary() {\n    return {\n      avgMemoryUsage: this.memoryUsage.reduce((a, b) => a + b, 0) / this.memoryUsage.length,\n      peakMemoryUsage: Math.max(...this.memoryUsage),\n      avgCpuLoad: this.cpuUsage.reduce((a, b) => a + b, 0) / this.cpuUsage.length,\n      totalDataPoints: this.cpuUsage.length\n    };\n  }\n}\n\nconst monitor = new LocalResourceMonitor();\nconst interval = monitor.startMonitoring();\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\n\n// ... run automation\n\nclearInterval(interval);\nconsole.log('Resource Usage:', monitor.getResourceSummary());\n```\n\n\n  <Card title=\"LLM Usage\" icon=\"chart-line\" href=\"/v3/basics/evals\">\n    Monitor token usage, costs, and speed. Set up automated alerting for critical failures. Implement cost tracking across different environments. Use session analytics to optimize automation workflows.\n  </Card>\n\n\n## Real-Time Metrics & Monitoring\n\n### Basic Usage Tracking\n\nMonitor your automation's resource usage in real-time with `stagehand.metrics`:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\n\n// Metrics are async in V3\nconst metrics = await stagehand.metrics;\nconsole.log(metrics);\n\n// Monitor during automation\nconst startTime = Date.now();\nconst initialMetrics = await stagehand.metrics;\n\n// ... perform automation tasks\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\nawait stagehand.act(\"click the login button\");\nconst data = await stagehand.extract({\n  instruction: \"extract user info\",\n  schema: UserSchema\n});\n\nconst finalMetrics = await stagehand.metrics;\nconst executionTime = Date.now() - startTime;\n\nconsole.log('Automation Summary:', {\n  totalTokens: finalMetrics.totalPromptTokens + finalMetrics.totalCompletionTokens,\n  executionTime: `${executionTime}ms`,\n  avgInferenceTime: `${finalMetrics.totalInferenceTimeMs / 3}ms`,\n});\n```\n\n### Understanding Metrics Data\n\nThe metrics object provides detailed breakdown by Stagehand operation:\n\n```typescript\ninterface StagehandMetrics {\n  // Act operation metrics\n  actPromptTokens: number;\n  actCompletionTokens: number;\n  actReasoningTokens: number;\n  actCachedInputTokens: number;\n  actInferenceTimeMs: number;\n\n  // Extract operation metrics\n  extractPromptTokens: number;\n  extractCompletionTokens: number;\n  extractReasoningTokens: number;\n  extractCachedInputTokens: number;\n  extractInferenceTimeMs: number;\n\n  // Observe operation metrics\n  observePromptTokens: number;\n  observeCompletionTokens: number;\n  observeReasoningTokens: number;\n  observeCachedInputTokens: number;\n  observeInferenceTimeMs: number;\n\n  // Agent operation metrics\n  agentPromptTokens: number;\n  agentCompletionTokens: number;\n  agentReasoningTokens: number;\n  agentCachedInputTokens: number;\n  agentInferenceTimeMs: number;\n\n  // Cumulative totals\n  totalPromptTokens: number;\n  totalCompletionTokens: number;\n  totalReasoningTokens: number;\n  totalCachedInputTokens: number;\n  totalInferenceTimeMs: number;\n}\n```\n\n**Example metrics output:**\n\n```typescript\nconst metrics = await stagehand.metrics;\nconsole.log(metrics);\n\n// {\n//   actPromptTokens: 4011,\n//   actCompletionTokens: 51,\n//   actReasoningTokens: 12,\n//   actCachedInputTokens: 0,\n//   actInferenceTimeMs: 1688,\n//   extractPromptTokens: 4200,\n//   extractCompletionTokens: 243,\n//   extractReasoningTokens: 18,\n//   extractCachedInputTokens: 0,\n//   extractInferenceTimeMs: 4297,\n//   observePromptTokens: 347,\n//   observeCompletionTokens: 43,\n//   observeReasoningTokens: 5,\n//   observeCachedInputTokens: 0,\n//   observeInferenceTimeMs: 903,\n//   agentPromptTokens: 0,\n//   agentCompletionTokens: 0,\n//   agentReasoningTokens: 0,\n//   agentCachedInputTokens: 0,\n//   agentInferenceTimeMs: 0,\n//   totalPromptTokens: 8558,\n//   totalCompletionTokens: 337,\n//   totalReasoningTokens: 35,\n//   totalCachedInputTokens: 0,\n//   totalInferenceTimeMs: 6888\n// }\n```\n\n## Best Practices\n\n<AccordionGroup>\n<Accordion title=\"Production Monitoring\">\n- Track session success rates and failure patterns\n- Monitor resource usage and scaling requirements\n- Set up automated alerting for critical failures\n- Implement cost tracking across different environments\n- Use session analytics to optimize automation workflows\n</Accordion>\n\n<Accordion title=\"Performance Optimization\">\n- Compare Browserbase vs local execution times\n- Monitor token usage and inference costs across models\n- Track geographic performance differences\n- Identify bottlenecks in automation workflows\n- Optimize for cost-effectiveness and speed\n</Accordion>\n\n<Accordion title=\"Operational Insights\">\n- Track session distribution across regions\n- Monitor concurrent session limits and scaling\n- Analyze failure patterns and common error scenarios\n- Use session recordings for root cause analysis\n- Implement custom metadata for workflow categorization\n</Accordion>\n\n<Accordion title=\"Integration & Alerting\">\n- Integrate session APIs with monitoring dashboards\n- Set up automated notifications for session failures  \n- Track SLA compliance and performance benchmarks\n- Monitor resource costs and usage patterns\n- Use analytics data for capacity planning and optimization\n</Accordion>\n</AccordionGroup>\n\n## Next Steps\n\n<CardGroup cols={2}>\n<Card title=\"History Tracking\" icon=\"clock-rotate-left\" href=\"/v3/best-practices/history\">\n  Track all LLM operations with parameters, results, and timestamps for debugging.\n</Card>\n<Card title=\"Logging\" icon=\"file-lines\" href=\"/v3/configuration/logging\">\n  Configure logging levels, custom loggers, and file-based session logging.\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/first-steps/ai-rules.mdx",
    "content": "---\ntitle: AI Rules\ndescription: Using AI to write Stagehand code faster, and better.\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nYou're likely using AI to write code, and there's a **right and wrong way to do it.** This page is a collection of rules, configs, and copy‑paste snippets to allow your AI agents/assistants to write performant, Stagehand code as fast as possible. \n\n## Quickstart\n\n<CardGroup cols={2}>\n  <Card title=\"Add MCP servers\" icon=\"screwdriver-wrench\">\n    Configure Browserbase (Stagehand), Context7, DeepWiki, and Stagehand Docs in your MCP client. \n  </Card>\n  <Card title=\"Pin editor rules\" icon=\"memo\">\n    Drop in `cursorrules` and `claude.md` so AI agents/assistants always emit Stagehand patterns. \n  </Card>\n</CardGroup>\n\n## Using MCP Servers\n\nMCP (Model Context Protocol) servers act as intermediaries that connect AI systems to external data sources and tools. These servers enable your coding assistant to access real-time information, execute tasks, and retrieve structured data to enhance code generation accuracy.\n\nThe following **MCP servers** provide specialized access to Stagehand documentation and related resources:\n\n<Accordion title=\"Context7 by Upstash\" icon=\"database\">\nProvides semantic search across documentation and codebase context. Context7 enables AI assistants to find relevant code patterns, examples, and implementation details from your project history. It maintains contextual understanding of your development workflow and can surface related solutions from previous work.\n\n**Installation:**\n```json\n{\n  \"mcpServers\": {\n    \"context7\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@upstash/context7-mcp\"]\n    }\n  }\n}\n```\n</Accordion>\n\n<Accordion title=\"DeepWiki by Cognition\" icon=\"book-open\">\nOffers deep indexing of GitHub repositories and documentation. DeepWiki allows AI agents to understand project architecture, API references, and best practices from the entire Stagehand ecosystem. It provides comprehensive knowledge about repository structure, code relationships, and development patterns.\n\n**Installation:**\n```json\n{\n  \"mcpServers\": {\n    \"deepwiki\": {\n      \"url\": \"https://mcp.deepwiki.com/mcp\"\n    }\n  }\n}\n```\n</Accordion>\n\n<Accordion title=\"Stagehand Docs by Mintlify\" icon=\"mintbit\">\nDirect access to official Stagehand documentation. This MCP server provides AI assistants with up-to-date API references, configuration options, and usage examples for accurate code generation. Mintlify auto-generates this server from the official docs, ensuring your AI assistant always has the latest information.\n\n**Usage:**\n```json\n{\n  \"mcpServers\": {\n    \"stagehand-docs\": {\n      \"url\": \"https://docs.stagehand.dev/mcp\"\n    }\n  }\n}\n```\n</Accordion>\n\n**How MCP Servers Enhance Your Development:**\n- **Real-time Documentation Access**: AI assistants can query the latest Stagehand docs, examples, and best practices\n- **Context-Aware Code Generation**: Servers provide relevant code patterns and configurations based on your specific use case\n- **Reduced Integration Overhead**: Standardized protocol eliminates the need for custom integrations with each documentation source\n- **Enhanced Accuracy**: AI agents receive structured, up-to-date information rather than relying on potentially outdated training data\n\n<Tip>\n**Prompting tip:** \nExplicitly ask your coding agent/assistant to use these MCP servers to fetch relevant information from the docs so they have better context and know how to write proper Stagehand code. \n\nie. **\"Use the stagehand-docs MCP to fetch the act/observe guidelines, then generate code that follows them. Prefer cached observe results.\"**\n</Tip>\n\n## Editor rule files (copy‑paste)\n\nDrop these in `.cursorrules`, `windsurfrules`, `claude.md`, or any agent rule framework:\n\n<Accordion title=\"TypeScript\">\n\n``````md\n# Stagehand Project\n\nThis is a project that uses Stagehand V3, a browser automation framework with AI-powered `act`, `extract`, `observe`, and `agent` methods.\n\nThe main class can be imported as `Stagehand` from `@browserbasehq/stagehand`.\n\n**Key Classes:**\n\n- `Stagehand`: Main orchestrator class providing `act`, `extract`, `observe`, and `agent` methods\n- `context`: A `V3Context` object that manages browser contexts and pages\n- `page`: Individual page objects accessed via `stagehand.context.pages()[i]` or created with `stagehand.context.newPage()`\n\n## Initialize\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\", // or \"BROWSERBASE\"\n  verbose: 2, // 0, 1, or 2\n  model: \"openai/gpt-4.1-mini\", // or any supported model\n});\n\nawait stagehand.init();\n\n// Access the browser context and pages\nconst page = stagehand.context.pages()[0];\nconst context = stagehand.context;\n\n// Create new pages if needed\nconst page2 = await stagehand.context.newPage();\n```\n\n## Act\n\nActions are called on the `stagehand` instance (not the page). Use atomic, specific instructions:\n\n```typescript\n// Act on the current active page\nawait stagehand.act(\"click the sign in button\");\n\n// Act on a specific page (when you need to target a page that isn't currently active)\nawait stagehand.act(\"click the sign in button\", { page: page2 });\n```\n\n**Important:** Act instructions should be atomic and specific:\n\n- ✅ Good: \"Click the sign in button\" or \"Type 'hello' into the search input\"\n- ❌ Bad: \"Order me pizza\" or \"Type in the search bar and hit enter\" (multi-step)\n\n### Observe + Act Pattern (Recommended)\n\nCache the results of `observe` to avoid unexpected DOM changes:\n\n```typescript\nconst instruction = \"Click the sign in button\";\n\n// Get candidate actions\nconst actions = await stagehand.observe(instruction);\n\n// Execute the first action\nawait stagehand.act(actions[0]);\n```\n\nTo target a specific page:\n\n```typescript\nconst actions = await stagehand.observe(\"select blue as the favorite color\", {\n  page: page2,\n});\nawait stagehand.act(actions[0], { page: page2 });\n```\n\n## Extract\n\nExtract data from pages using natural language instructions. The `extract` method is called on the `stagehand` instance.\n\n### Basic Extraction (with schema)\n\n```typescript\nimport { z } from \"zod\";\n\n// Extract with explicit schema\nconst data = await stagehand.extract(\n  \"extract all apartment listings with prices and addresses\",\n  z.object({\n    listings: z.array(\n      z.object({\n        price: z.string(),\n        address: z.string(),\n      }),\n    ),\n  }),\n);\n\nconsole.log(data.listings);\n```\n\n### Simple Extraction (without schema)\n\n```typescript\n// Extract returns a default object with 'extraction' field\nconst result = await stagehand.extract(\"extract the sign in button text\");\n\nconsole.log(result);\n// Output: { extraction: \"Sign in\" }\n\n// Or destructure directly\nconst { extraction } = await stagehand.extract(\n  \"extract the sign in button text\",\n);\nconsole.log(extraction); // \"Sign in\"\n```\n\n### Targeted Extraction\n\nExtract data from a specific element using a selector:\n\n```typescript\nconst reason = await stagehand.extract(\n  \"extract the reason why script injection fails\",\n  z.string(),\n  { selector: \"/html/body/div[2]/div[3]/iframe/html/body/p[2]\" },\n);\n```\n\n### URL Extraction\n\nWhen extracting links or URLs, use `z.string().url()`:\n\n```typescript\nconst { links } = await stagehand.extract(\n  \"extract all navigation links\",\n  z.object({\n    links: z.array(z.string().url()),\n  }),\n);\n```\n\n### Extracting from a Specific Page\n\n```typescript\n// Extract from a specific page (when you need to target a page that isn't currently active)\nconst data = await stagehand.extract(\n  \"extract the placeholder text on the name field\",\n  { page: page2 },\n);\n```\n\n## Observe\n\nPlan actions before executing them. Returns an array of candidate actions:\n\n```typescript\n// Get candidate actions on the current active page\nconst [action] = await stagehand.observe(\"Click the sign in button\");\n\n// Execute the action\nawait stagehand.act(action);\n```\n\nObserving on a specific page:\n\n```typescript\n// Target a specific page (when you need to target a page that isn't currently active)\nconst actions = await stagehand.observe(\"find the next page button\", {\n  page: page2,\n});\nawait stagehand.act(actions[0], { page: page2 });\n```\n\n## Agent\n\nUse the `agent` method to autonomously execute complex, multi-step tasks.\n\n### Basic Agent Usage\n\n```typescript\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://www.google.com\");\n\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.0-flash\",\n  executionModel: \"google/gemini-2.0-flash\",\n});\n\nconst result = await agent.execute({\n  instruction: \"Search for the stock price of NVDA\",\n  maxSteps: 20,\n});\n\nconsole.log(result.message);\n```\n\n### Computer Use Agent (CUA)\n\nFor more advanced scenarios using computer-use models:\n\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\", // Enable Computer Use Agent mode\n  model: \"anthropic/claude-sonnet-4-20250514\",\n  // or \"google/gemini-2.5-computer-use-preview-10-2025\"\n  systemPrompt: `You are a helpful assistant that can use a web browser.\n    Do not ask follow up questions, the user will trust your judgement.`,\n});\n\nawait agent.execute({\n  instruction: \"Apply for a library card at the San Francisco Public Library\",\n  maxSteps: 30,\n});\n```\n\n### Agent with Custom Model Configuration\n\n```typescript\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: {\n    modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GEMINI_API_KEY,\n  },\n  systemPrompt: `You are a helpful assistant.`,\n});\n```\n\n### Agent with Integrations (MCP/External Tools)\n\n```typescript\nconst agent = stagehand.agent({\n  integrations: [`https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`],\n  systemPrompt: `You have access to the Exa search tool.`,\n});\n```\n\n## Advanced Features\n\n### DeepLocator (XPath Targeting)\n\nTarget specific elements across shadow DOM and iframes:\n\n```typescript\nawait page\n  .deepLocator(\"/html/body/div[2]/div[3]/iframe/html/body/p\")\n  .highlight({\n    durationMs: 5000,\n    contentColor: { r: 255, g: 0, b: 0 },\n  });\n```\n\n### Multi-Page Workflows\n\n```typescript\nconst page1 = stagehand.context.pages()[0];\nawait page1.goto(\"https://example.com\");\n\nconst page2 = await stagehand.context.newPage();\nawait page2.goto(\"https://example2.com\");\n\n// Act/extract/observe operate on the current active page by default\n// Pass { page } option to target a specific page\nawait stagehand.act(\"click button\", { page: page1 });\nawait stagehand.extract(\"get title\", { page: page2 });\n```\n``````\n\n</Accordion>\n\n<Accordion title=\"Python\">\n\n``````md\n# Stagehand Python Project\n\nThis is a project that uses [Stagehand Python](https://github.com/browserbase/stagehand-python), which provides AI-powered browser automation with `act`, `extract`, and `observe` methods.\n\n`Stagehand` is a class that provides configuration and browser automation capabilities with:\n- Pages accessed via `stagehand.context.pages()` or `stagehand.context.activePage()`\n- `stagehand.context`: A StagehandContext object (extends Playwright BrowserContext)\n- `stagehand.agent()`: Create AI-powered agents for autonomous multi-step workflows\n- `stagehand.init()`: Initialize the browser session\n- `stagehand.close()`: Clean up resources\n\n`Page` extends Playwright's Page class with AI-powered methods:\n- `act()`: Perform actions on web elements using natural language\n- `extract()`: Extract structured data from pages using schemas\n- `observe()`: Plan actions and get selectors before executing\n\n`Agent` provides autonomous Computer Use Agent capabilities:\n- `execute()`: Perform complex multi-step tasks using natural language instructions\n\nUse the following rules to write code for this project.\n\n- To plan an instruction like \"click the sign in button\", use Stagehand `observe` to get the action to execute.\n\nYou can also pass in the following params:\n\n- The result of `observe` is a list of `ObserveResult` objects that can directly be used as params for `act` like this:\n  \n- When writing code that needs to extract data from the page, use Stagehand `extract`. Use Pydantic models for schemas:\n\n## Initialize\n\n### Configuration Options\n\nKey configuration options in `StagehandConfig`:\n\n## Act\n\nYou can act directly with string instructions:\n\nUse variables for dynamic form filling:\n\n**Best Practices:**\n- Cache the results of `observe` to avoid unexpected DOM changes\n- Keep actions atomic and specific (e.g., \"Click the sign in button\" not \"Sign in to the website\")\n- Use specific, descriptive instructions\n\nAct `action` should be as atomic and specific as possible, i.e. \"Click the sign in button\" or \"Type 'hello' into the search input\".\nAVOID actions that are more than one step, i.e. \"Order me pizza\" or \"Send an email to Paul asking him to call me\".\n\n## Extract\n\n### Simple String Extraction\n\n### Structured Extraction with Schema (Recommended)\nAlways use Pydantic models for structured data extraction:\n\n### Array Extraction\nFor arrays, use List types:\n\n### Complex Object Extraction\nFor more complex data structures:\n\n## Agent System\n\nStagehand provides an Agent System for autonomous web browsing using Computer Use Agents (CUA).\n\n### Creating Agents\n\n### Agent Execution\n\n**Best Practices:**\n- Be specific with instructions: `\"Fill out the contact form with name 'John Doe' and submit it\"`\n- Break down complex tasks into smaller steps\n- Use error handling with try/except blocks\n- Combine agents for navigation with traditional methods for precise data extraction\n\n## Project Structure Best Practices\n\n- Store configurations in environment variables or config files\n- Use async/await patterns consistently\n- Implement main automation logic in async functions\n- Use async context managers for resource management\n- Use type hints and Pydantic models for data validation\n- Handle exceptions appropriately with try/except blocks\n``````\n\n</Accordion>\n\n## Security notes\n\n- Do not embed secrets in docs or rule files; use env vars in MCP configs.\n- Avoid broad actions that may trigger unintended navigation; prefer `observe` first.\n\n## Resources/references\n\n- Context7 MCP (Upstash)\n  - https://github.com/upstash/context7\n- DeepWiki MCP\n  - https://mcp.deepwiki.com/\n- Stagehand Docs MCP (Mintlify)\n  - https://docs.stagehand.dev/mcp\n"
  },
  {
    "path": "packages/docs/v3/first-steps/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Integrate Stagehand into an existing project.\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nInstall Stagehand in your current app with the TypeScript SDK.\n\n<Tip>\nWe recommend using the Node.js runtime environment to run Stagehand scripts.\n\n**Bun is now supported** as long as you do not integrate Stagehand with Playwright. Playwright is not compatible with Bun.\n</Tip>\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n### Install dependencies\n\n<CodeGroup>\n```bash npm\nnpm install @browserbasehq/stagehand\n```\n\n```bash pnpm\npnpm add @browserbasehq/stagehand\n```\n\n```bash yarn\nyarn add @browserbasehq/stagehand\n```\n```bash bun icon=\"sparkles\"\nbun add @browserbasehq/stagehand\n```\n</CodeGroup>\n\n<Tip>\nIf you plan to run locally, you need to have [Chrome](https://www.google.com/chrome/) installed on your machine. For cloud browser sessions, skip this.\n</Tip>\n\n### Configure environment\n\nSet environment variables (or a `.env` via your framework):\n\n<CodeGroup>\n```bash Bash\nOPENAI_API_KEY=your_api_key\nBROWSERBASE_API_KEY=your_api_key\nBROWSERBASE_PROJECT_ID=your_project_id\n```\n</CodeGroup>\n\n<Note>\nStagehand does not auto-load `.env` files.\n\nIf you use a `.env` file, install and initialize `dotenv` in your own app code:\n\n```bash\nnpm install dotenv\n```\n\n```typescript\nimport dotenv from \"dotenv\";\ndotenv.config({ path: \".env\" });\n```\n</Note>\n\n### Use in your codebase\n\nAdd Stagehand where you need browser automation.\n\n```typescript\nimport dotenv from \"dotenv\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\ndotenv.config({ path: \".env\" });  // if needed\n\nasync function main() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\"\n  });\n\n  await stagehand.init();\n  const page = stagehand.context.pages()[0];\n\n  await page.goto(\"https://example.com\");\n\n  // Act on the page\n  await stagehand.act(\"Click the learn more button\");\n\n  // Extract structured data\n  const description = await stagehand.extract(\"extract the description\", z.string());\n\n  console.log(description);\n  await stagehand.close();\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n```\n\n</Tab>\n\n<Tab title=\"Other Languages\">\n\n<Note>\nFor Python and other language SDKs, use the **language selector** in the top left corner of the sidebar to view the SDK documentation for your language.\n</Note>\n\n</Tab>\n\n</Tabs>\n\n## Next steps\n\n<CardGroup cols={2}>\n  <Card \n    title=\"Configuration\"\n    icon=\"gear\"\n    href=\"/v3/configuration/browser\"\n  >\n    Environment, Browserbase vs Local, logging, timeouts, LLM customization\n  </Card>\n  <Card \n    title=\"Act\"\n    icon=\"arrow-pointer\"\n    href=\"/v3/basics/act\"\n  >\n    Perform precise actions with natural language\n  </Card>\n  <Card \n    title=\"Extract\"\n    icon=\"download\"\n    href=\"/v3/basics/extract\"\n  >\n    Typed data extraction with Zod schemas\n  </Card>\n  <Card \n    title=\"Observe\"\n    icon=\"eye\"\n    href=\"/v3/basics/observe\"\n  >\n    Discover elements and suggested actions\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/first-steps/introduction.mdx",
    "content": "---\ntitle: Introducing Stagehand\nsidebarTitle: Introduction\ndescription: Developers use Stagehand to reliably automate the web.\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## The Problem with Browser Automation\n\nTraditional frameworks like Playwright and Puppeteer force you to write brittle scripts that break with every UI change. Web agents promise to solve this with AI, but leave you at the mercy of unpredictable behavior.\n\n**You're stuck between two bad options:**\n- **Too brittle**: Traditional selectors break when websites change\n- **Too agentic**: AI agents are unpredictable and impossible to debug\n\n## Enter Stagehand\n\nStagehand gives you the best of both worlds through four powerful primitives that let you choose exactly how much AI to use:\n\n<CardGroup cols={2}>\n  <Card title=\"Act\" icon=\"play\" href=\"/v3/basics/act\">\n    Execute actions using natural language\n  </Card>\n  <Card title=\"Extract\" icon=\"database\" href=\"/v3/basics/extract\">\n    Pull structured data with schemas\n  </Card>\n  <Card title=\"Observe\" icon=\"eye\" href=\"/v3/basics/observe\">\n    Discover available actions on any page\n  </Card>\n  <Card title=\"Agent\" icon=\"robot\" href=\"/v3/basics/agent\">\n    Automate entire workflows autonomously\n  </Card>\n</CardGroup>\n\n```typescript\n// Act - Execute natural language actions\nawait stagehand.act(\"click the login button\");\n\n// Extract - Pull structured data\nconst price = await stagehand.extract(\n  \"extract the price\",\n  z.number()\n);\n\n// Observe - Discover available actions\nconst actions = await stagehand.observe(\"find submit buttons\");\n\n// Agent - Automate entire workflows\nconst agent = stagehand.agent({\n  mode: \"cua\",\n  model: \"google/gemini-2.5-computer-use-preview-10-2025\",\n});\nawait agent.execute(\"apply for this job\");\n```\n\n\n## Why Developers Choose Stagehand\n\n- **Precise Control**: Mix AI-powered actions with deterministic code. You decide exactly how much AI to use.\n\n- **Actually Repeatable**: Save and replay actions exactly. No more \"it worked on my machine\" with browser automations.\n\n- **Maintainable at Scale**: One script can automate multiple websites. When sites change, your automations adapt.\n\n- **Composable Tools**: Choose your level of automation with Act, Extract, Observe, and Agent.\n\n## Built for Modern Development\nStagehand is designed for developers building production browser automations and AI agents that need reliable web access.\n\n<AccordionGroup>\n  <Accordion title=\"Works Everywhere\">\n    Compatible with all Chromium-based browsers: Chrome, Edge, Arc, Brave, and more.\n  </Accordion>\n  <Accordion title=\"Built by Browserbase\">\n    Created and maintained by the team behind enterprise browser infrastructure.\n  </Accordion>\n</AccordionGroup>\n\n## Get Started in 60 Seconds\n<Info>\n  **Pro tip**: For best results, we recommend using Stagehand with [Browserbase](https://www.browserbase.com) for reliable cloud browser infrastructure.\n</Info>\n<CardGroup cols={2}>\n  <Card\n    title=\"Quickstart\"\n    icon=\"rocket\"\n    href=\"/v3/first-steps/quickstart\"\n  >\n    Build your first automation in under a minute\n  </Card>\n  <Card\n    title=\"Try Director\"\n    icon=\"wand-magic-sparkles\"\n    href=\"https://www.director.ai\"\n  >\n    Generate Stagehand scripts with AI\n  </Card>\n  <Card\n    title=\"View Templates\"\n    icon=\"code\"\n    href=\"https://www.browserbase.com/templates\"\n  >\n    See real-world automation examples\n  </Card>\n  <Card\n    title=\"Join Discord\"\n    icon=\"discord\"\n    href=\"https://stagehand.dev/discord\"\n  >\n    Get help from the community\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/first-steps/quickstart.mdx",
    "content": "---\ntitle: Quickstart\ndescription: 'Stagehand allows you to build web automations with natural language and code.'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nIf this is your **first time using Stagehand**, you should try [Director](https://director.ai) first. It's an agent that allows you to build Stagehand workflows using natural language. You can also try Stagehand using our [MCP server](/v3/integrations/mcp/introduction).\n\nOtherwise, the quickest way to start with Stagehand is with our CLI. It scaffolds a ready‑to‑run Stagehand app with sensible defaults, and an example script.\n\n<Note>\nThis quickstart is for **TypeScript**. For other languages, change the language selector in the top left corner.\n</Note>\n\n## 1) Create a sample project\n\n<CodeGroup>\n```bash Bash\nnpx create-browser-app\n```\n</CodeGroup>\n\n## 2) Run it\n\nFollow the CLI prompts to enter the project directory and add your API keys. Then run the example script.\n\n<CodeGroup>\n```bash Bash\ncd my-stagehand-app # Enter the project directory\ncp .env.example .env  # Add your API keys\nnpm start # Run the example script\n```\n</CodeGroup>\n\n## 3) Use Stagehand (act, extract, observe)\n\nThe scaffold includes an index.ts file that contains the example script. Here's what it looks like:\n\n<CodeGroup>\n```typescript TypeScript\nimport \"dotenv/config\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nasync function main() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\"\n  });\n\n  await stagehand.init();\n\n  console.log(`Stagehand Session Started`);\n  console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`);\n\n  const page = stagehand.context.pages()[0];\n\n  await page.goto(\"https://stagehand.dev\");\n\n  const extractResult = await stagehand.extract(\"Extract the value proposition from the page.\");\n  console.log(`Extract result:\\n`, extractResult);\n\n  await stagehand.act(\"Click the 'Evals' button.\");\n\n  const observeResult = await stagehand.observe(\"What can I click on this page?\");\n  console.log(`Observe result:\\n`, observeResult);\n\n  const agent = stagehand.agent({\n    mode: \"cua\",\n    model: \"google/gemini-2.5-computer-use-preview-10-2025\",\n    systemPrompt: \"You're a helpful assistant that can control a web browser.\",\n  });\n\n  const agentResult = await agent.execute(\"What is the most accurate model to use in Stagehand?\");\n  console.log(`Agent result:\\n`, agentResult);\n\n  await stagehand.close();\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n\n```\n</CodeGroup>\n\n<Tip>\nTo use, set provider keys in `.env` (e.g., `OPENAI_API_KEY`). For cloud browsers, add `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID`.\n</Tip>\n\n## Next steps\n\nLearn about the Stagehand primitives: act, extract, observe, and agent.\n\n<CardGroup cols={2}>\n  <Card \n    title=\"Act\" \n    icon=\"arrow-pointer\" \n    href=\"/v3/basics/act\"\n  >\n    Perform actions on web pages with natural language\n  </Card>\n  \n  <Card \n    title=\"Extract\" \n    icon=\"download\" \n    href=\"/v3/basics/extract\"\n  >\n    Get structured data with Zod schemas\n  </Card>\n  \n  <Card \n    title=\"Observe\" \n    icon=\"eye\" \n    href=\"/v3/basics/observe\"\n  >\n    Discover available elements and actions\n  </Card>\n  \n  <Card \n    title=\"Agent\" \n    icon=\"robot\" \n    href=\"/v3/basics/agent\"\n  >\n    Autonomous multi-step browser workflows\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/convex/configuration.mdx",
    "content": "---\ntitle: \"Use Stagehand in Convex\"\nsidebarTitle: Configuration\ndescription: \"Set up AI-powered browser automation in your Convex application\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<Card\n  title=\"Check out the convex-stagehand repo\"\n  icon=\"github\"\n  href=\"https://github.com/browserbase/convex-stagehand\"\n>\n  Clone the [GitHub repo](https://github.com/browserbase/convex-stagehand) to get started with Stagehand in Convex.\n</Card>\n\n## Installation\n\nInstall the convex-stagehand component and Zod for schema validation:\n\n```bash\nnpm install @browserbasehq/convex-stagehand zod\n```\n\n## Configuration\n\nAdd the Stagehand component to your `convex/convex.config.ts`:\n\n```typescript convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport stagehand from \"@browserbasehq/convex-stagehand/convex.config\";\n\nconst app = defineApp();\napp.use(stagehand, { name: \"stagehand\" });\nexport default app;\n```\n\n## Environment Variables\n\nSet the following environment variables in your [Convex Dashboard](https://dashboard.convex.dev):\n\n| Variable | Description |\n|----------|-------------|\n| `BROWSERBASE_API_KEY` | Your Browserbase API key |\n| `BROWSERBASE_PROJECT_ID` | Your Browserbase project ID |\n| `MODEL_API_KEY` | API key for your LLM provider (OpenAI, Anthropic, etc.) |\n\n## Basic Usage\n\n### Initialize the Client\n\nCreate a Stagehand instance in your Convex action:\n\n```typescript convex/actions.ts\n\"use node\";\n\nimport { Stagehand } from \"@browserbasehq/convex-stagehand\";\nimport { components } from \"./_generated/api\";\nimport { action } from \"./_generated/server\";\nimport { z } from \"zod\";\n\nconst stagehand = new Stagehand(components.stagehand, {\n  browserbaseApiKey: process.env.BROWSERBASE_API_KEY!,\n  browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID!,\n  modelApiKey: process.env.MODEL_API_KEY!,\n});\n```\n\n### Extract Data\n\nExtract structured data from a web page using natural language instructions and Zod schemas:\n\n```typescript\nexport const extractProducts = action({\n  handler: async (ctx) => {\n    const data = await stagehand.extract(ctx, {\n      url: \"https://example.com/products\",\n      instruction: \"Extract all product names and prices\",\n      schema: z.object({\n        products: z.array(z.object({\n          name: z.string(),\n          price: z.string(),\n        }))\n      })\n    });\n\n    return data.products;\n  }\n});\n```\n\n### Perform Actions\n\nExecute browser interactions using plain English:\n\n```typescript\nexport const loginToSite = action({\n  handler: async (ctx) => {\n    const result = await stagehand.act(ctx, {\n      url: \"https://example.com/login\",\n      action: \"Click the login button and wait for the page to load\"\n    });\n\n    return result;\n  }\n});\n```\n\n### Observe Elements\n\nIdentify interactive elements on a page:\n\n```typescript\nexport const findNavLinks = action({\n  handler: async (ctx) => {\n    const actions = await stagehand.observe(ctx, {\n      url: \"https://example.com\",\n      instruction: \"Find all clickable navigation links\"\n    });\n\n    return actions;\n  }\n});\n```\n\n### Run Autonomous Tasks\n\nUse the agent API for complex multi-step workflows:\n\n```typescript\nexport const searchAndExtract = action({\n  handler: async (ctx) => {\n    const result = await stagehand.agent(ctx, {\n      url: \"https://google.com\",\n      instruction: \"Search for 'convex database' and extract the top 3 results\",\n      options: { maxSteps: 10 }\n    });\n\n    return result;\n  }\n});\n```\n\n## Session Management\n\nFor workflows that span multiple operations, you can reuse browser sessions:\n\n```typescript\nexport const multiStepWorkflow = action({\n  handler: async (ctx) => {\n    // Start a session\n    const session = await stagehand.startSession(ctx, {\n      url: \"https://example.com\",\n      options: { timeout: 30000, waitUntil: \"networkidle\" }\n    });\n\n    // Perform multiple operations with the same session\n    await stagehand.act(ctx, {\n      sessionId: session.sessionId,\n      action: \"Click the login button\"\n    });\n\n    const data = await stagehand.extract(ctx, {\n      sessionId: session.sessionId,\n      instruction: \"Extract the user profile information\",\n      schema: z.object({\n        name: z.string(),\n        email: z.string(),\n      })\n    });\n\n    // End the session\n    await stagehand.endSession(ctx, { sessionId: session.sessionId });\n\n    return data;\n  }\n});\n```\n\nSession persistence allows you to preserve authentication state and cookies between operations.\n\n## Model Configuration\n\nThe default model is `openai/gpt-4o`. You can configure alternative providers:\n\n```typescript\nconst stagehand = new Stagehand(components.stagehand, {\n  browserbaseApiKey: process.env.BROWSERBASE_API_KEY!,\n  browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID!,\n  modelApiKey: process.env.ANTHROPIC_API_KEY!,\n  modelName: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n```\n\n## Requirements\n\n- Convex 1.29.3 or later\n- A [Browserbase](https://browserbase.com) account with API credentials\n- An API key from a supported LLM provider (OpenAI, Anthropic, etc.)\n\n## References\n\n<CardGroup cols={2}>\n  <Card title=\"Source Code\" icon=\"github\" href=\"https://github.com/browserbase/convex-stagehand\">\n    Browse the complete repository on GitHub\n  </Card>\n  <Card title=\"Convex Docs\" icon=\"book\" href=\"https://docs.convex.dev\">\n    Learn more about Convex\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/convex/introduction.mdx",
    "content": "---\ntitle: \"Convex\"\nsidebarTitle: Introduction\ndescription: \"AI-powered browser automation for Convex applications\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nThis guide shows you how to use Stagehand with Convex to create AI-powered browser automation within your Convex applications. By the end of this guide, you'll know how to:\n\n- Set up the convex-stagehand component in your Convex app\n- Extract structured data from web pages using natural language\n- Execute browser actions via plain English instructions\n- Build autonomous multi-step workflows with the agent API\n\n## When You'd Use This\n\nThe Convex integration is perfect for scenarios where you need browser automation in serverless Convex functions:\n\n- **Data extraction pipelines**: Extract structured data from websites and store it directly in your Convex database\n- **Automated workflows**: Build background jobs that interact with web pages on behalf of users\n- **Form automation**: Automatically fill out and submit forms based on data from your Convex app\n- **Multi-step web processes**: Execute complex browser workflows that require decision-making and adaptation\n\nThe integration wraps the Stagehand REST API to provide Convex actions with the ability to control cloud browsers via Browserbase:\n\n1. **Act**: Perform actions like clicking, typing, or navigating using natural language\n2. **Extract**: Extract structured data from web pages with Zod schemas\n3. **Observe**: Identify and analyze interactive elements on the page\n4. **Agent**: Run autonomous multi-step tasks with AI decision-making\n\n<CardGroup cols={2}>\n  <Card title=\"Source Code\" icon=\"github\" href=\"https://github.com/browserbase/convex-stagehand\">\n    Browse the repository on GitHub\n  </Card>\n  <Card title=\"Convex Configuration\" icon=\"gear\" href=\"/v3/integrations/convex/configuration\">\n    Learn how to set up and configure convex-stagehand\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/crew-ai/configuration.mdx",
    "content": "---\ntitle: \"Use CrewAI to Automate Browser Tasks\"\nsidebarTitle: Configuration\ndescription: \"Create intelligent agents that can interact with websites and automate browser tasks using natural language instructions\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nThis guide walks you through setting up CrewAI with Browserbase to create agents that can perform web automation tasks using natural language instructions.\n\n## Step 1: Install Dependencies\n\nInstall the required packages for CrewAI and Stagehand integration:\n\n```bash\npip install stagehand crewai crewai-tools\n```\n\n## Step 2: Configure Environment Variables\n\nYou'll need API keys from three services:\n\n1. **Browserbase API Key and Project ID**: Get these from your [Browserbase dashboard](https://www.browserbase.com/)\n2. **LLM API Key**: Get an API key from [OpenAI](https://platform.openai.com/api-keys) or [Anthropic](https://console.anthropic.com/)\n\nStore your API keys securely as environment variables:\n\n```bash\nBROWSERBASE_API_KEY=\"your-browserbase-api-key\"\nBROWSERBASE_PROJECT_ID=\"your-browserbase-project-id\"\nOPENAI_API_KEY=\"your-openai-api-key\"\nANTHROPIC_API_KEY=\"your-anthropic-api-key\"\n```\n\n## Step 3: Create Your First Agent\n\nCreate a Python script with a basic CrewAI agent:\n\n```python\nimport os\nfrom crewai import Agent, Task, Crew\nfrom crewai_tools import StagehandTool\nfrom stagehand.schemas import AvailableModel\n\n# Get API keys from environment\nbrowserbase_api_key = os.environ.get(\"BROWSERBASE_API_KEY\")\nbrowserbase_project_id = os.environ.get(\"BROWSERBASE_PROJECT_ID\")\nmodel_api_key = os.environ.get(\"OPENAI_API_KEY\")  # or ANTHROPIC_API_KEY\n\n# Initialize the StagehandTool\nstagehand_tool = StagehandTool(\n    api_key=browserbase_api_key,\n    project_id=browserbase_project_id,\n    model_api_key=model_api_key,\n    model_name=AvailableModel.GPT_4O,  # or AvailableModel.CLAUDE_3_7_SONNET_LATEST\n)\n\n# Create an agent with the tool\nresearcher = Agent(\n    role=\"Web Researcher\",\n    goal=\"Find and summarize information from websites\",\n    backstory=\"I'm an expert at finding information online.\",\n    verbose=True,\n    tools=[stagehand_tool],\n)\n```\n\n## Step 4: Create and Run a Task\n\nDefine a task for your agent and execute it:\n\n```python\n# Create a task that uses the tool\nresearch_task = Task(\n    description=\"Go to https://www.example.com and tell me what you see on the homepage.\",\n    agent=researcher,\n)\n\n# Run the crew\ncrew = Crew(\n    agents=[researcher],\n    tasks=[research_task],\n    verbose=True,\n)\n\ntry:\n    result = crew.kickoff()\n    print(result)\nfinally:\n    # Clean up resources\n    stagehand_tool.close()\n```\n\n## Step 5: Run Your Script\n\nExecute your Python script:\n\n```bash\npython your_crew_script.py\n```\n\n## Advanced Configuration\n\nCustomize the StagehandTool behavior with additional parameters:\n\n```python\nstagehand_tool = StagehandTool(\n    api_key=browserbase_api_key,\n    project_id=browserbase_project_id,\n    model_api_key=model_api_key,\n    model_name=AvailableModel.CLAUDE_3_7_SONNET_LATEST,\n    dom_settle_timeout_ms=5000,  # Wait longer for DOM to settle\n    headless=True,  # Run browser in headless mode\n    self_heal=True,  # Attempt to recover from errors\n    wait_for_captcha_solves=True,  # Wait for CAPTCHA solving\n    verbose=1,  # Control logging verbosity (0-3)\n)\n```\n\n## Example Tasks\n\n<Tabs>\n  <Tab title=\"Form Submission\" value=\"form-submission\" label=\"Python\">\n    ```python\n    form_task = Task(\n        description=\"\"\"\n        Submit a contact form:\n        1. Go to https://example.com/contact\n        2. Fill out the form with name 'John Doe', email 'john@example.com'\n        3. Submit and confirm success\n        \"\"\",\n        agent=researcher,\n    )\n    ```\n  </Tab>\n  <Tab title=\"Data Extraction\" value=\"data-extraction\" label=\"Python\">\n    ```python\n    extraction_task = Task(\n        description=\"\"\"\n        Extract product information:\n        1. Go to the products page\n        2. Extract all product names, prices, and descriptions\n        3. Format as structured data\n        \"\"\",\n        agent=researcher,\n    )\n    ```\n  </Tab>\n  <Tab title=\"Multi-step Navigation\" value=\"multi-step-navigation\" label=\"Python\">\n    ```python\n    navigation_task = Task(\n        description=\"\"\"\n        Navigate and analyze:\n        1. Start at homepage\n        2. Navigate to products section\n        3. Filter by 'Electronics' category\n        4. Find and extract details of highest-rated product\n        \"\"\",\n        agent=researcher,\n    )\n    ```\n  </Tab>\n</Tabs>\n\n<CardGroup cols={2}>\n  <Card title=\"CrewAI Documentation\" icon=\"book\" href=\"https://docs.crewai.com/\">\n    Dive into the CrewAI documentation to learn more about its capabilities and integrations.\n  </Card>\n  <Card title=\"Browserbase Documentation\" icon=\"book\" href=\"https://docs.browserbase.com/\">\n    Access the Browserbase documentation for comprehensive guides and resources.\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/integrations/crew-ai/introduction.mdx",
    "content": "---\ntitle: \"CrewAI Introduction\"\nsidebarTitle: Introduction\ndescription: \"Automate browser tasks using natural language instructions with CrewAI\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nThis guide shows you how to use CrewAI with Browserbase to create intelligent agents that can automate web interactions. By the end of this guide, you'll know how to:\n\n- Set up CrewAI with the StagehandTool\n- Create agents that can interact with websites\n- Automate browser tasks using natural language instructions\n- Extract structured data from web pages\n\n## When You'd Use This\n\nThe CrewAI integration is perfect for scenarios where you need intelligent web automation:\n\n- **Research automation**: Have agents research information across multiple websites\n- **Data collection**: Extract structured data from e-commerce sites, job boards, or news sites\n- **Form automation**: Automatically fill out and submit forms based on specific criteria\n- **Multi-step workflows**: Execute complex browser workflows that require decision-making\n\nThe StagehandTool wraps the Stagehand Python SDK to provide CrewAI agents with the ability to control a real web browser and interact with websites using three core primitives:\n\n1. **Act**: Perform actions like clicking, typing, or navigating\n2. **Extract**: Extract structured data from web pages\n3. **Observe**: Identify and analyze elements on the page\n\n<CardGroup cols={1}>\n<Card title=\"CrewAI Configuration\" icon=\"gear\" href=\"/v3/integrations/crew-ai/configuration\">\n  Learn how to configure and use the StagehandTool with CrewAI agents for web automation tasks\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/integrations/langchain/configuration.mdx",
    "content": "---\ntitle: \"LangChain JS Configuration\"\nsidebarTitle: Configuration\ndescription: \"Set up Stagehand with LangChain JS to create intelligent web automation agents\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\nThis guide walks you through integrating Stagehand with LangChain JS to build powerful web automation workflows using natural language instructions.\n\n## Step 1: Install Dependencies\n\nInstall the required packages for LangChain JS and Stagehand integration:\n\n```bash\nnpm install @langchain/langgraph @langchain/community @langchain/core @browserbasehq/stagehand\n```\n\n## Step 2: Configure Environment Variables\n\nFor remote browser automation, set up your Browserbase credentials:\n\n```bash\nBROWSERBASE_API_KEY=\"your-browserbase-api-key\"\nBROWSERBASE_PROJECT_ID=\"your-browserbase-project-id\"\n```\n\n## Step 3: Create a Stagehand Instance\n\nInitialize Stagehand with your preferred configuration:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// For local development\nconst stagehand = new Stagehand({\n    env: \"LOCAL\",\n    verbose: 2,\n    enableCaching: false,\n});\n\n// For production with Browserbase\nconst stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 1,\n    enableCaching: true,\n});\n```\n\n## Step 4: Generate the StagehandToolkit\n\nCreate the toolkit that provides LangChain-compatible tools:\n\n```typescript\nimport { StagehandToolkit } from '@langchain/community/agents/toolkits/stagehand';\n\nconst stagehandToolkit = await StagehandToolkit.fromStagehand(stagehand);\n```\n\n## Step 5: Use Individual Tools\n\nThe toolkit provides four specialized tools for web automation:\n\n### Available Tools\n\n- **stagehand_navigate**: Navigate to specific URLs\n- **stagehand_act**: Perform browser actions (clicking, typing, etc.)\n- **stagehand_extract**: Extract structured data using schemas  \n- **stagehand_observe**: Analyze page elements and possible actions\n\n### Basic Tool Usage\n\n```typescript\nimport { z } from \"zod\";\n\n// Navigate to a website\nconst navigateTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_navigate\"\n);\nawait navigateTool.invoke(\"https://www.google.com\");\n\n// Perform an action\nconst actionTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_act\"\n);\nawait actionTool.invoke('Search for \"OpenAI\"');\n\n// Observe the page\nconst observeTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_observe\"\n);\nconst result = await observeTool.invoke(\n    \"What actions can be performed on the current page?\"\n);\nconsole.log(JSON.parse(result));\n\n// Extract structured data\nconst extractTool = stagehandToolkit.tools.find(\n    (t) => t.name === \"stagehand_extract\"\n);\nconst extractResult = await extractTool.invoke({\n    instruction: \"Extract the main heading and description\",\n    schema: z.object({\n        heading: z.string(),\n        description: z.string(),\n    }),\n});\nconsole.log(extractResult);\n```\n\n## Step 6: Build LangGraph Agents\n\nIntegrate with LangGraph for complex automation workflows:\n\n```typescript\nimport { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n\n// Create an LLM\nconst llm = new ChatOpenAI({\n    model: \"gpt-4\",\n    temperature: 0,\n});\n\n// Create an agent with Stagehand tools\nconst agent = createReactAgent({\n    llm,\n    tools: stagehandToolkit.tools,\n});\n\n// Execute a complex workflow\nconst result = await agent.invoke({\n    messages: [\n        {\n            role: \"user\", \n            content: \"Go to example.com, find the contact form, and extract all the form fields\"\n        }\n    ]\n});\n```\n\n## Advanced Configuration\n\n### Custom Stagehand Configuration\n\n```typescript\nconst stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    verbose: 2,\n    enableCaching: true,\n    headless: true,\n    domSettleTimeoutMs: 5000,\n});\n```\n\n### Error Handling\n\n```typescript\ntry {\n    const result = await agent.invoke({\n        messages: [{ role: \"user\", content: \"Navigate to invalid-url.com\" }]\n    });\n} catch (error) {\n    console.error(\"Automation failed:\", error);\n} finally {\n    // Clean up resources\n    await stagehand.close();\n}\n```\n\n## Example Workflows\n\n<Tabs>\n  <Tab title=\"Data Extraction\" value=\"data-extraction\" label=\"TypeScript\">\n    ```typescript\n    const extractionAgent = createReactAgent({\n        llm,\n        tools: stagehandToolkit.tools,\n    });\n\n    const result = await extractionAgent.invoke({\n        messages: [{\n            role: \"user\",\n            content: `\n                Go to news-website.com and extract:\n                1. All article headlines\n                2. Publication dates  \n                3. Author names\n                Format as structured JSON\n            `\n        }]\n    });\n    ```\n  </Tab>\n  <Tab title=\"Form Automation\" value=\"form-automation\" label=\"TypeScript\">\n    ```typescript\n    const formAgent = createReactAgent({\n        llm,\n        tools: stagehandToolkit.tools,\n    });\n\n    const result = await formAgent.invoke({\n        messages: [{\n            role: \"user\", \n            content: `\n                Navigate to contact-form.com and:\n                1. Fill out the contact form with:\n                   - Name: John Doe\n                   - Email: john@example.com\n                   - Message: Inquiry about services\n                2. Submit the form\n                3. Confirm submission success\n            `\n        }]\n    });\n    ```\n  </Tab>\n  <Tab title=\"Multi-site Research\" value=\"multi-site-research\" label=\"TypeScript\">\n    ```typescript\n    const researchAgent = createReactAgent({\n        llm,\n        tools: stagehandToolkit.tools,\n    });\n\n    const result = await researchAgent.invoke({\n        messages: [{\n            role: \"user\",\n            content: `\n                Research product pricing by:\n                1. Visit competitor1.com and extract pricing info\n                2. Visit competitor2.com and extract pricing info  \n                3. Compare features and prices\n                4. Provide summary analysis\n            `\n        }]\n    });\n    ```\n  </Tab>\n</Tabs>\n\n<CardGroup cols={1}>\n  <Card title=\"LangChain JS Documentation\" icon=\"book\" href=\"https://js.langchain.com/docs/integrations/tools/stagehand/\">\n    Official LangChain JS documentation for the Stagehand integration\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/integrations/langchain/introduction.mdx",
    "content": "---\ntitle: \"Langchain JS Introduction\"\nsidebarTitle: Introduction\ndescription: \"Integrate Stagehand with Langchain JS for intelligent web automation\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nThis guide shows you how to use Stagehand with Langchain JS to create intelligent agents that can automate web interactions. By the end of this guide, you'll know how to:\n\n- Set up the StagehandToolkit with Langchain JS\n- Create agents that can navigate and interact with websites\n- Extract structured data using natural language instructions\n- Build complex automation workflows with LangGraph\n\n## When You'd Use This\n\nThe Langchain JS integration is perfect for scenarios where you need intelligent web automation with advanced reasoning:\n\n- **AI-driven research**: Create agents that can research information across multiple websites and synthesize findings\n- **Dynamic form filling**: Automatically fill out complex forms based on contextual requirements\n- **Data extraction workflows**: Extract and transform data from multiple sources with intelligent navigation\n- **Multi-step web processes**: Execute complex browser workflows that require decision-making and adaptation\n\n<CardGroup cols={1}>\n<Card title=\"Langchain JS Configuration\" icon=\"gear\" href=\"/v3/integrations/langchain/configuration\">\n  Learn how to set up and configure the StagehandToolkit with Langchain JS agents\n</Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/integrations/mcp/configuration.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server Configuration\"\nsidebarTitle: \"Configuration\"\ndescription: \"Configure your browser automation with command-line flags, environment variables, and advanced options\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Configuration Overview\n\nThe Browserbase MCP server supports extensive configuration options through command-line flags and environment variables. Configure browser behavior, proxy settings, stealth modes, model selection, and more to customize your browser automation workflows.\n\n<Note>\nCommand-line flags are only available when running the server locally (`npx @browserbasehq/mcp-server-browserbase` with flags or local development setup).\n</Note>\n\n## Environment Variables\n\nConfigure the essential Browserbase credentials and optional debugging settings:\n\n<CardGroup cols={2}>\n<Card title=\"BROWSERBASE_API_KEY\" icon=\"key\">\nYour Browserbase API key for authentication\n</Card>\n\n<Card title=\"BROWSERBASE_PROJECT_ID\" icon=\"key\">\nYour Browserbase project ID\n</Card>\n\n</CardGroup>\n\n## Command-Line Flags\n\n### Available Flags\n\n| Flag | Description |\n|------|-------------|\n| `--proxies` | Enable Browserbase proxies for the session |\n| `--advancedStealth` | Enable Browserbase Advanced Stealth (Scale Plan only) |\n| `--keepAlive` | Enable Browserbase Keep Alive Session |\n| `--contextId <contextId>` | Specify a Browserbase Context ID to use |\n| `--persist [boolean]` | Whether to persist the Browserbase context (default: true) |\n| `--port <port>` | Port to listen on for HTTP/SHTTP transport |\n| `--host <host>` | Host to bind server to (default: localhost, use 0.0.0.0 for all interfaces) |\n| `--browserWidth <width>` | Browser viewport width (default: 1024) |\n| `--browserHeight <height>` | Browser viewport height (default: 768) |\n| `--modelName <model>` | The model to use for Stagehand (default: google/gemini-2.5-flash-lite) |\n| `--modelApiKey <key>` | API key for the custom model provider (required when using custom models) |\n| `--experimental` | Enable experimental features (default: false) |\n\n## Configuration Examples\n\n### Basic Configuration\n\n<Tabs>\n<Tab title=\"Remote URL (SHTTP)\">\n\n\n<CodeGroup>\n```json Direct SHTTP\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"your-smithery-url.com\"\n    }\n  }\n}\n```\n</CodeGroup>\n\nWhen using our remote hosted server, we provide the LLM costs for Gemini, the [best performing model](https://www.stagehand.dev/evals) in [Stagehand](https://www.stagehand.dev).\n</Tab>\n\n<Tab title=\"NPM Package\">\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Local STDIO\">\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/mcp-server-browserbase/cli.js\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Local SHTTP\">\n```bash\n# Start server\nnode cli.js --port 8931\n```\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"http://localhost:8931/mcp\",\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n### Advanced Features\n\n<Tabs>\n<Tab title=\"Proxies\">\nEnable Browserbase proxies for IP rotation and geo-location testing.\n\n<Panel>\n[Learn more about Browserbase Proxies](https://docs.browserbase.com/features/proxies)\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\", \"--proxies\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Advanced Stealth\">\nEnable advanced anti-detection features for enhanced stealth browsing.\n\n<Panel>\n[Learn more about Advanced Stealth](https://docs.browserbase.com/features/stealth-mode#advanced-stealth-mode)\n\n**Note:** Advanced Stealth is only available for Scale Plan users.\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\", \"--advancedStealth\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Contexts\">\nUse persistent browser contexts to maintain authentication and state across sessions.\n\n<Panel>\n[Learn more about Browserbase Contexts](https://docs.browserbase.com/features/contexts)\n</Panel>\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\", \"--contextId\", \"your_context_id\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n### Browser Customization\n\n<Tabs>\n<Tab title=\"Viewport Sizing\">\nCustomize browser window dimensions. Default is 1288x711. Recommended aspect ratios: 16:9.\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--browserWidth\", \"1920\",\n        \"--browserHeight\", \"1080\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n\n**Common Resolutions:**\n- Desktop: 1920x1080, 1280x720, 1024x768\n- Mobile: 375x667 (iPhone), 360x640 (Android)\n- Tablet: 768x1024 (iPad)\n</Tab>\n\n</Tabs>\n\n## Model Configuration\n\nConfigure AI models for enhanced browser automation. Stagehand defaults to Google's Gemini 2.5 Flash Lite but supports multiple providers.\n\n<Warning>\nWhen using any custom model (non-default), you must provide your own API key for that model provider using the `--modelApiKey` flag.\n</Warning>\n\n<Tabs>\n<Tab title=\"Available Models\">\n**Google Gemini** (Default)\n- `google/gemini-2.5-flash-lite` (default)\n- `google/gemini-2.5-pro`\n- `google/gemini-2.5-flash`\n\n**OpenAI**\n- `gpt-5-2025-08-07`\n- `gpt-4.1-2025-04-14`\n- `gpt-4o`\n- `gpt-4o-mini`\n\n**Anthropic Claude**\n- `claude-sonnet-4-5`\n- `claude-haiku-4-5`\n\n[View full list of supported models](https://docs.stagehand.dev/v3/configuration/models#models)\n</Tab>\n\n<Tab title=\"Configuration Examples\">\n<CodeGroup>\n```json OpenAI GPT-4o\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--modelName\", \"gpt-4o\",\n        \"--modelApiKey\", \"your_openai_api_key\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\"\n      }\n    }\n  }\n}\n```\n\n```json Claude Sonnet\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--modelName\", \"claude-sonnet-4-6\",\n        \"--modelApiKey\", \"your_anthropic_api_key\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\"\n      }\n    }\n  }\n}\n```\n</CodeGroup>\n</Tab>\n</Tabs>\n\n## Development Configuration\n\n<Tabs>\n<Tab title=\"Custom Host/Port\">\nConfigure custom host and port for SHTTP transport.\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@browserbasehq/mcp-server-browserbase\",\n        \"--host\", \"0.0.0.0\",\n        \"--port\", \"8080\"\n      ],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"BROWSERBASE_PROJECT_ID\": \"your_project_id\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n\n## Best Practices\n\n<Accordion title=\"Performance - How can I optimize browser automation performance?\">\n- Use appropriate viewport sizes for your use case\n- Enable proxies only when needed for geo-location\n- Choose efficient models (Gemini Flash for speed, GPT-4o for accuracy)\n- Reuse contexts for authentication persistence\n</Accordion>\n\n<Accordion title=\"Security - What security measures should I implement?\">\n- Store API keys securely in environment variables\n- Use Advanced Stealth for sensitive operations\n- Implement proper session management\n- Rotate cookies and contexts regularly\n</Accordion>\n\n<Accordion title=\"Development - What are the recommended development practices?\">\n- Enable debug mode during development\n- Use context persistence for faster iteration\n- Test with different viewport sizes\n- Monitor session usage and quotas\n</Accordion>\n\n<Accordion title=\"Production - How should I configure for production environments?\">\n- Use NPM installation for reliability\n- Configure appropriate timeouts\n- Implement error handling and retries\n- Monitor performance and resource usage\n</Accordion>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Browserbase Documentation\" icon=\"globe\" href=\"https://docs.browserbase.com\">\nComplete platform documentation\n</Card>\n\n<Card title=\"Stagehand Docs\" icon=\"robot\" href=\"https://docs.stagehand.dev/\">\nAI-powered browser automation\n</Card>\n\n<Card title=\"Support\" icon=\"headset\" href=\"mailto:support@browserbase.com\">\nGet help from our team\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/mcp/introduction.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server\"\nsidebarTitle: \"Introduction\"\ndescription: \"AI-powered browser automation through Model Context Protocol integration with Stagehand\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n## Overview\n\nThe Browserbase MCP Server brings powerful browser automation capabilities to MCP clients through the Model Context Protocol (MCP). Built on top of [Stagehand](https://docs.stagehand.dev/), this integration provides AI-powered web automation using natural language commands.\n\n<Info>\n  The hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)\n  endpoint is served on Browserbase infrastructure.\n  You can also run the MCP server locally with STDIO, but we recommend the\n  hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)\n  endpoint for most users.\n</Info>\n\n## Key Features\n\n<CardGroup cols={2}>\n<Card title=\"Natural Language Automation\" icon=\"wand-magic-sparkles\">\nControl browsers using plain English commands like \"click the login button\" or \"fill out the contact form\"\n</Card>\n\n<Card title=\"Web Interaction\" icon=\"browser\">\n  Navigate, click, and fill forms with ease\n</Card>\n\n<Card title=\"Data Extraction\" icon=\"download\">\n  Extract structured data from any website automatically\n</Card>\n\n<Card title=\"Session Lifecycle\" icon=\"route\">\n  Create, reuse, and close browser sessions with explicit MCP tools\n</Card>\n\n</CardGroup>\n\n## Core Benefits\n\n<Tabs>\n<Tab title=\"Ease of Use\">\n<CardGroup cols={2}>\n<Card title=\"Intuitive Commands\" icon=\"wand-magic-sparkles\">\nNo need to learn complex selectors or automation syntax. Simply describe what you want to do in natural language.\n</Card>\n\n<Card title=\"Quick Setup\" icon=\"rocket\">\n  Get started in minutes with either hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) or local STDIO.\n</Card>\n\n<Card title=\"Smart Automation\" icon=\"brain\">\nStagehand's AI understands web page context and can adapt to different layouts and designs.\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Powerful Capabilities\">\n<CardGroup cols={2}>\n<Card title=\"Full Browser Control\" icon=\"browser\">\nNavigate, click, type, scroll, and interact with any web element.\n</Card>\n\n<Card title=\"Data Intelligence\" icon=\"chart-line\">\n  Extract structured information from complex web pages automatically.\n</Card>\n\n<Card title=\"Session Persistence\" icon=\"cookie-bite\">\nMaintain authentication states and cookies across multiple interactions.\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Enterprise Ready\">\n<CardGroup cols={2}>\n<Card title=\"Reliable Infrastructure\" icon=\"server\">\nHosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) runs on Browserbase infrastructure for consistent performance.\n</Card>\n\n<Card title=\"Scalable Architecture\" icon=\"arrows-up-to-line\">\n  Handle multiple concurrent sessions and high-volume automation tasks.\n</Card>\n\n<Card title=\"Security Features\" icon=\"shield-check\">\n  Stealth mode, proxy support, and advanced anti-detection capabilities.\n</Card>\n\n<Card title=\"Comprehensive Logging\" icon=\"file-lines\">\nDetailed session recordings and debugging information.\n</Card>\n</CardGroup>\n</Tab>\n</Tabs>\n\n## Use Cases\n\n<Tabs>\n<Tab title=\"Web Scraping & Data Collection\">\n<CardGroup cols={2}>\n<Card title=\"E-commerce Monitoring\" icon=\"store\">\nTrack product prices, availability, and competitor information\n</Card>\n\n<Card title=\"Market Research\" icon=\"chart-bar\">\n  Gather data from multiple sources for analysis and reporting\n</Card>\n\n<Card title=\"Content Aggregation\" icon=\"newspaper\">\n  Collect articles, posts, and media from various websites\n</Card>\n\n<Card title=\"Lead Generation\" icon=\"users\">\nExtract contact information and business data from directories\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Testing\">\n<CardGroup cols={2}>\n<Card title=\"Automated Testing\" icon=\"flask\">\nCreate comprehensive test suites for web applications\n</Card>\n\n<Card title=\"Cross-Browser Validation\" icon=\"browsers\">\n  Test functionality across different browser environments\n</Card>\n\n<Card title=\"User Journey Testing\" icon=\"route\">\n  Simulate real user interactions and workflows\n</Card>\n\n<Card title=\"Performance Monitoring\" icon=\"gauge\">\nTrack page load times and user experience metrics\n</Card>\n</CardGroup>\n</Tab>\n\n<Tab title=\"Workflow Automation\">\n<CardGroup cols={2}>\n<Card title=\"Form Automation\" icon=\"file-contract\">\nAutomatically fill and submit complex web forms\n</Card>\n\n<Card title=\"Report Generation\" icon=\"chart-line\">\n  Extract data and generate automated reports\n</Card>\n\n<Card title=\"Social Media Management\" icon=\"share-nodes\">\n  Schedule posts and monitor engagement across platforms\n</Card>\n\n<Card title=\"Administrative Tasks\" icon=\"clipboard-check\">\nAutomate repetitive web-based business processes\n</Card>\n</CardGroup>\n</Tab>\n</Tabs>\n\n## Getting Started\n\n<Steps>\n<Step title=\"Install the MCP Server\">\nChoose hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) (recommended) or local STDIO based on your needs.\n</Step>\n\n<Step title=\"Configure Authentication\">\n  Set up your Browserbase API credentials in MCP configuration. Get API keys\n  from the [Browserbase Dashboard](https://www.browserbase.com/overview).\n</Step>\n\n<Step title=\"Start Automating\">\nBegin using natural language commands to control browsers through your MCP client.\n</Step>\n</Steps>\n\n<Tip>\n  Ready to get started? Check out the [Setup Guide](/v3/integrations/mcp/setup).\n</Tip>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Setup Guide\" icon=\"rocket\" href=\"/v3/integrations/mcp/setup\">\nGet started with installation and configuration\n</Card>\n\n<Card title=\"MCP Docs\" icon=\"book\" href=\"https://modelcontextprotocol.io/introduction\">\nLearn more about the MCP protocol\n</Card>\n\n<Card title=\"Browserbase Docs\" icon=\"globe\" href=\"https://docs.browserbase.com\">\nExplore Browserbase features and capabilities\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/mcp/setup.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server Setup\"\nsidebarTitle: \"Setup\"\ndescription: \"Add the Browserbase MCP Server to your MCP client\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n## Quick Installation\n\n<Card title=\"Install with Cursor\" icon=\"arrow-pointer\" href=\"cursor://anysphere.cursor-deeplink/mcp/install?name=browserbase&config=eyJ1cmwiOiJodHRwczovL21jcC5icm93c2VyYmFzZS5jb20vbWNwP2Jyb3dzZXJiYXNlQXBpS2V5PVlPVVJfQlJPV1NFUkJBU0VfQVBJX0tFWSJ9\">\n  One-click installation directly in Cursor\n</Card>\n\nYou can also add Browserbase MCP to Claude Code with a single command:\n\n```bash\nclaude mcp add --transport http browserbase \"https://mcp.browserbase.com/mcp?browserbaseApiKey=YOUR_BROWSERBASE_API_KEY\"\n```\n\nWe support both local STDIO and hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) (SHTTP). We recommend hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) for most users.\n\n## Endpoint\n\nHosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) endpoint (served on Browserbase infrastructure):\n\n```text\nhttps://mcp.browserbase.com/mcp\n```\n\n## Prerequisites\n\n<Steps>\n<Step title=\"Get your Browserbase credentials\">\nGet your Browserbase API key from the [Browserbase Dashboard](https://www.browserbase.com/overview).\n\n<Frame>\n<img src=\"/images/quickstart/api-key.png\" alt=\"Browserbase API Key settings\" />\n</Frame>\n\nThen copy your API Key directly from the input.\n</Step>\n</Steps>\n\n## Query Parameters (Hosted [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http))\n\n### Required for tool calls\n\n<CardGroup cols={1}>\n<Card title=\"browserbaseApiKey\" icon=\"key\">\nBrowserbase API key.\n</Card>\n</CardGroup>\n\n### Optional\n\n| Query Param       | Type           | Behavior                                   |\n| ----------------- | -------------- | ------------------------------------------ |\n| `modelName`       | string         | Defaults to `google/gemini-2.5-flash-lite` |\n| `modelApiKey`     | string         | Required when `modelName` is non-default   |\n| `keepAlive`       | boolean string | `\"true\"` or `\"false\"`                      |\n| `proxies`         | boolean string | `\"true\"` or `\"false\"`                      |\n| `advancedStealth` | boolean string | `\"true\"` or `\"false\"`                      |\n\n<Warning>\n  Boolean query values must be exact strings: `\"true\"` or `\"false\"`.\n</Warning>\n\n## Available Tools\n\n<Accordion title=\"navigate\">\nNavigate to any URL in the browser\n\n<ParamField path=\"url\" type=\"string\" required>\n  The URL to navigate to\n</ParamField>\n</Accordion>\n\n<Accordion title=\"act\">\nPerform an action on the web page using natural language\n\n<ParamField path=\"action\" type=\"string\" required>\n  The action to perform (e.g., \"click the login button\", \"fill form field\")\n</ParamField>\n</Accordion>\n\n<Accordion title=\"observe\">\nObserve and find actionable elements on the page.\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  Specific instruction for observation (e.g., \"find the login button\", \"locate search form\")\n</ParamField>\n</Accordion>\n\n<Accordion title=\"extract\">\nExtract data from the current page.\n\n<ParamField path=\"instruction\" type=\"string\">\nOptional extraction instruction.\n</ParamField>\n</Accordion>\n\n<Accordion title=\"start\">\nCreate or reuse a Browserbase session and set it as active for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n\n<ResponseField name=\"sessionId\" type=\"string\">\nBrowserbase session ID.\n</ResponseField>\n</Accordion>\n\n<Accordion title=\"end\">\nClose the active Browserbase session for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n</Accordion>\n\n## Local Command-Line Flags\n\n<Note>\nCommand-line flags are only available when running the server locally (`npx @browserbasehq/mcp-server-browserbase` with flags or local development setup).\n</Note>\n\n| Flag | Description |\n|------|-------------|\n| `--proxies` | Enable Browserbase proxies for the session |\n| `--advancedStealth` | Enable Browserbase Advanced Stealth (Scale Plan only) |\n| `--keepAlive` | Enable Browserbase Keep Alive Session |\n| `--contextId <contextId>` | Specify a Browserbase Context ID to use |\n| `--persist [boolean]` | Whether to persist the Browserbase context (default: true) |\n| `--port <port>` | Port to listen on for HTTP or [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport |\n| `--host <host>` | Host to bind server to (default: localhost, use 0.0.0.0 for all interfaces) |\n| `--browserWidth <width>` | Browser viewport width (default: 1024) |\n| `--browserHeight <height>` | Browser viewport height (default: 768) |\n| `--modelName <model>` | The model to use for Stagehand (default: google/gemini-2.5-flash-lite) |\n| `--modelApiKey <key>` | API key for the custom model provider (required when using custom models) |\n| `--experimental` | Enable experimental features (default: false) |\n\n## Installation Methods\n\n<Tabs>\n<Tab title=\"Hosted (recommended)\">\n\nUse your MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"https://mcp.browserbase.com/mcp?browserbaseApiKey=YOUR_BROWSERBASE_API_KEY\"\n    }\n  }\n}\n```\n\nFor custom models, include `modelName` and `modelApiKey`:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"https://mcp.browserbase.com/mcp?browserbaseApiKey=YOUR_BROWSERBASE_API_KEY&modelName=openai/gpt-4.1&modelApiKey=YOUR_MODEL_API_KEY\"\n    }\n  }\n}\n```\n\n</Tab>\n\n<Tab title=\"NPM Package (STDIO)\">\nThe easiest way to get started locally is using our NPM package.\n\n<Note>\nIf you would like to use a different model, you have to pass the model name and keys in the args. More info in the [Local Command-Line Flags](#local-command-line-flags) section.\n</Note>\n\n<Steps>\n<Step title=\"Add to MCP Config\">\nGo into your MCP Config JSON and add the Browserbase Server:\n\n<CodeGroup>\n```json Claude Desktop\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"npx\",\n      \"args\": [\"@browserbasehq/mcp-server-browserbase\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</CodeGroup>\n</Step>\n\n<Step title=\"Restart your MCP client\">\n<Check>\nThat's it! Reload your MCP client and you will be able to use Browserbase.\n</Check>\n</Step>\n</Steps>\n\n</Tab>\n\n<Tab title=\"Local Development\">\nFor local development or customization, you can run the server locally.\n\n<Steps>\n<Step title=\"Clone and build\">\n```bash\n# Clone the Repo\ngit clone https://github.com/browserbase/mcp-server-browserbase.git\ncd mcp-server-browserbase\n\n# Install the dependencies and build the project\nnpm install && npm run build\n```\n</Step>\n\n<Step title=\"Choose your transport method\">\nYou can run locally using either STDIO or [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).\n\n<Tabs>\n<Tab title=\"STDIO\">\nAdd the following to your MCP Config JSON file:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/mcp-server-browserbase/cli.js\"],\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n\n<Tab title=\"Self-hosted Streamable HTTP\">\nFirst, run the server:\n\n```bash\nnode cli.js --port 8931\n```\n\nThen add this to your MCP Config JSON file:\n\n```json\n{\n  \"mcpServers\": {\n    \"browserbase\": {\n      \"url\": \"http://localhost:8931/mcp\",\n      \"env\": {\n        \"BROWSERBASE_API_KEY\": \"your_api_key\",\n        \"GEMINI_API_KEY\": \"your_gemini_api_key\"\n      }\n    }\n  }\n}\n```\n</Tab>\n</Tabs>\n</Step>\n\n<Step title=\"Restart your client\">\n<Check>\nReload your MCP client and you should be good to go!\n</Check>\n</Step>\n</Steps>\n</Tab>\n</Tabs>\n\n## Verify Installation\n\n<Steps>\n<Step title=\"Restart your MCP client\">\nRestart/refresh your MCP client app and verify tools are available.\n</Step>\n\n<Step title=\"Test the integration\">\nGet started using our MCP Server by asking your MCP client to navigate to any page and see your Browserbase Browser in action on the [dashboard](https://www.browserbase.com/sessions).\n\n<Tip>\nTry: \"Navigate to example.com and extract the main heading\"\n</Tip>\n</Step>\n</Steps>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Model Context Protocol (MCP) Docs\" icon=\"book\" href=\"https://modelcontextprotocol.io/introduction\">\nLearn more about the MCP protocol\n</Card>\n\n<Card title=\"Browserbase Documentation\" icon=\"globe\" href=\"https://docs.browserbase.com\">\nExplore Browserbase features and capabilities\n</Card>\n\n<Card title=\"Support\" icon=\"headset\" href=\"mailto:support@browserbase.com\">\nGet help from our support team\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/mcp/tools.mdx",
    "content": "---\ntitle: \"Browserbase MCP Server Tools\"\nsidebarTitle: \"Tools\"\ndescription: \"This guide covers the specialized tools available in the Browserbase MCP server for browser automation and interaction.\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nThe Browserbase MCP server provides tools for browser automation and session management through a transport-scoped active session.\n\n## Core Browser Automation Tools\n\nThese are the primary tools for modern web automation using natural language commands.\n\n<Accordion title=\"navigate\">\nNavigate to any URL in the browser\n\n<ParamField path=\"url\" type=\"string\" required>\n  The URL to navigate to\n</ParamField>\n</Accordion>\n\n<Accordion title=\"act\">\nPerform an action on the web page using natural language\n\n<ParamField path=\"action\" type=\"string\" required>\n  The action to perform (e.g., \"click the login button\", \"fill form field\")\n</ParamField>\n\n</Accordion>\n\n<Accordion title=\"observe\">\nObserve and find actionable elements on the page.\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  Specific instruction for observation (e.g., \"find the login button\", \"locate search form\")\n</ParamField>\n</Accordion>\n\n<Accordion title=\"extract\">\nExtract data from the current page.\n\n<ParamField path=\"instruction\" type=\"string\">\n  Optional extraction instruction.\n</ParamField>\n</Accordion>\n\n## Session Management\n\n<Accordion title=\"start\">\nCreate or reuse a Browserbase session and set it as active for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n\n<ResponseField name=\"sessionId\" type=\"string\">\n  Browserbase session ID.\n</ResponseField>\n</Accordion>\n\n<Accordion title=\"end\">\nClose the active Browserbase session for the current MCP transport session.\n\n<Info>No input parameters required.</Info>\n</Accordion>\n\n## Further Reading\n\n<CardGroup cols={3}>\n<Card title=\"Model Context Protocol (MCP) Docs\" icon=\"book\" href=\"https://modelcontextprotocol.io/introduction\">\nLearn more about the MCP protocol\n</Card>\n\n<Card title=\"Stagehand Documentation\" icon=\"robot\" href=\"https://docs.stagehand.dev/\">\nExplore Stagehand's AI-powered browser automation\n</Card>\n\n<Card title=\"Support\" icon=\"headset\" href=\"mailto:support@browserbase.com\">\nGet help from our support team\n</Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/playwright.mdx",
    "content": "---\ntitle: Playwright\ndescription: Use Stagehand with Playwright for browser automation\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nStagehand v3 can work seamlessly with Playwright, allowing you to use Playwright's `Page` objects directly with Stagehand's AI-powered methods like `act()`, `extract()`, and `observe()`.\n\n## Installation\n\nFirst, install both Stagehand and Playwright:\n\n```bash\nnpm install @browserbasehq/stagehand playwright-core\n```\n\n## Quickstart\n\n### Basic Setup\n\nConnect Playwright to Stagehand's browser instance using Chrome DevTools Protocol (CDP):\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { chromium } from \"playwright-core\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\", // or \"LOCAL\"\n  model: \"openai/gpt-5\",\n});\n\nawait stagehand.init();\n\n// Connect Playwright to Stagehand's browser\nconst browser = await chromium.connectOverCDP({\n  wsEndpoint: stagehand.connectURL(),\n});\n\nconst pwContext = browser.contexts()[0];\nconst pwPage = pwContext.pages()[0];\n```\n\n### Using Playwright Pages with Stagehand\n\nOnce connected, you can use Playwright's `Page` objects with Stagehand's AI-powered methods:\n\n```typescript\n// Navigate using Playwright\nawait pwPage.goto(\"https://example.com\");\n\n// Use Stagehand's AI methods with the Playwright page\nawait stagehand.act(\"click the login button\", { page: pwPage });\n\nconst data = await stagehand.extract(\n  \"extract the article title\",\n  z.object({ title: z.string() }),\n  { page: pwPage }\n);\n```\n\n## Multi-Page Example\n\nStagehand works great with multiple Playwright pages:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { chromium } from \"playwright-core\";\nimport { z } from \"zod\";\n\n// Initialize Stagehand\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"openai/gpt-5\",\n});\n\nawait stagehand.init();\n\n// Connect Playwright\nconst browser = await chromium.connectOverCDP({\n  wsEndpoint: stagehand.connectURL(),\n});\n\nconst pwContext = browser.contexts()[0];\nconst pwPage1 = pwContext.pages()[0];\n\n// Create a second page\nconst pwPage2 = await pwContext.newPage();\n\n// Navigate both pages\nawait pwPage1.goto(\"https://docs.stagehand.dev/first-steps/introduction\");\nawait pwPage2.goto(\"https://docs.stagehand.dev/configuration/observability\");\n\n// Extract data from both pages concurrently\nconst [page1Data, page2Data] = await Promise.all([\n  stagehand.extract(\n    \"extract the names of the four stagehand primitives\",\n    z.array(z.string()),\n    { page: pwPage1 }\n  ),\n  stagehand.extract(\n    \"extract the list of session dashboard features\",\n    z.array(z.string()),\n    { page: pwPage2 }\n  ),\n]);\n\nconsole.log(\"Page 1 primitives:\", page1Data);\nconsole.log(\"Page 2 features:\", page2Data);\n```\n\n## Complete Example\n\nHere's a full working example:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { chromium } from \"playwright-core\";\nimport { z } from \"zod\";\n\nasync function main() {\n  // Initialize Stagehand\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    model: \"openai/gpt-5\",\n    verbose: 1,\n  });\n\n  await stagehand.init();\n  console.log(\"Stagehand initialized\");\n\n  // Connect Playwright to Stagehand's browser\n  const browser = await chromium.connectOverCDP({\n    wsEndpoint: stagehand.connectURL(),\n  });\n\n  const pwContext = browser.contexts()[0];\n  const pwPage = pwContext.pages()[0];\n\n  // Navigate and interact\n  await pwPage.goto(\"https://example.com\");\n\n  // Use Stagehand's AI methods\n  const actions = await stagehand.observe(\"find the main heading\", {\n    page: pwPage,\n  });\n\n  console.log(\"Found actions:\", actions);\n\n  // Extract data\n  const heading = await stagehand.extract(\n    \"extract the main heading text\",\n    z.object({ heading: z.string() }),\n    { page: pwPage }\n  );\n\n  console.log(\"Heading:\", heading);\n\n  // Cleanup\n  await stagehand.close();\n}\n\nmain();\n```\n\n## Key Points\n\n- **Connect via CDP**: Use `chromium.connectOverCDP()` with `stagehand.connectURL()` as the WebSocket endpoint\n- **Pass the page**: Always pass the Playwright `page` object to Stagehand methods using the `{ page }` option\n- **Multi-page support**: Create multiple pages with `pwContext.newPage()` and pass them to Stagehand methods\n- **Concurrent operations**: Use `Promise.all()` to run multiple Stagehand operations in parallel across different pages\n\n## Environment Variables\n\nWhen using Browserbase, set your credentials:\n\n```bash\nBROWSERBASE_API_KEY=your_api_key\nBROWSERBASE_PROJECT_ID=your_project_id\n```\n\nFor OpenAI (or other providers):\n\n```bash\nOPENAI_API_KEY=your_api_key\n```\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Agent\" icon=\"robot\" href=\"/v3/references/agent\">\n    Automate entire workflows\n  </Card>\n  <Card title=\"Act\" icon=\"play\" href=\"/v3/references/act\">\n    Execute actions on web pages\n  </Card>\n  <Card title=\"Extract\" icon=\"ufo-beam\" href=\"/v3/references/extract\">\n    Extract structured data from pages\n  </Card>\n  <Card title=\"Observe\" icon=\"eye\" href=\"/v3/references/observe\">\n    Observe and find elements on pages\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/puppeteer.mdx",
    "content": "---\ntitle: Puppeteer\ndescription: Use Stagehand with Puppeteer for browser automation\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nStagehand v3 can work seamlessly with Puppeteer, allowing you to use Puppeteer's `Page` objects directly with Stagehand's AI-powered methods like `act()`, `extract()`, and `observe()`.\n\n## Installation\n\nFirst, install both Stagehand and Puppeteer:\n\n```bash\nnpm install @browserbasehq/stagehand puppeteer-core\n```\n\n## Quickstart\n\n### Basic Setup\n\nConnect Puppeteer to Stagehand's browser instance:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport puppeteer from \"puppeteer-core\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\", // or \"BROWSERBASE\"\n  model: \"openai/gpt-5\",\n});\n\nawait stagehand.init();\n\n// Connect Puppeteer to Stagehand's browser\nconst browser = await puppeteer.connect({\n  browserWSEndpoint: stagehand.connectURL(),\n  defaultViewport: null,\n});\n\nconst pages = await browser.pages();\nconst ppPage = pages[0];\n```\n\n### Using Puppeteer Pages with Stagehand\n\nOnce connected, you can use Puppeteer's `Page` objects with Stagehand's AI-powered methods:\n\n```typescript\n// Navigate using Puppeteer\nawait ppPage.goto(\"https://example.com\");\n\n// Use Stagehand's AI methods with the Puppeteer page\nawait stagehand.act(\"click the sign in button\", { page: ppPage });\n\nconst data = await stagehand.extract(\n  \"extract the page title\",\n  z.object({ title: z.string() }),\n  { page: ppPage }\n);\n```\n\n## Advanced: Multi-Page Usage\n\nCreate and manage multiple Puppeteer pages with Stagehand:\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport puppeteer from \"puppeteer-core\";\nimport { z } from \"zod\";\n\nasync function multiPageExample() {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    model: \"openai/gpt-5\",\n  });\n\n  await stagehand.init();\n\n  // Connect Puppeteer\n  const browser = await puppeteer.connect({\n    browserWSEndpoint: stagehand.connectURL(),\n    defaultViewport: null,\n  });\n\n  // Get the first page\n  const pages = await browser.pages();\n  const ppPage1 = pages[0];\n\n  // Create a second page\n  const ppPage2 = await browser.newPage();\n\n  // Navigate both pages\n  await ppPage1.goto(\"https://example.com\");\n  await ppPage2.goto(\"https://another-site.com\");\n\n  // Use Stagehand on different pages\n  await stagehand.act(\"click the button\", { page: ppPage1 });\n\n  const data = await stagehand.extract(\n    \"extract the title\",\n    z.object({ title: z.string() }),\n    { page: ppPage2 }\n  );\n\n  console.log(\"Extracted from page 2:\", data);\n\n  await stagehand.close();\n}\n```\n\n## Observe + Act Pattern\n\nThe recommended pattern for reliable automation:\n\n```typescript\n// Step 1: Observe to find candidate actions\nconst actions = await stagehand.observe(\n  \"find the submit button\",\n  { page: ppPage }\n);\n\n// Step 2: Execute the first action\nif (actions.length > 0) {\n  await stagehand.act(actions[0], { page: ppPage });\n}\n```\n\nThis pattern helps avoid DOM changes between observation and action execution.\n\n## Key Points\n\n- **Connect via WebSocket**: Use `puppeteer.connect()` with `stagehand.connectURL()` as the `browserWSEndpoint`\n- **Pass the page**: Always pass the Puppeteer `page` object to Stagehand methods using the `{ page }` option\n- **Disable viewport**: Set `defaultViewport: null` to use Stagehand's viewport settings\n- **Multi-page support**: Create multiple pages with `browser.newPage()` and pass them to Stagehand methods\n\n## Environment Variables\n\nWhen using Browserbase, set your credentials:\n\n```bash\nBROWSERBASE_API_KEY=your_api_key\nBROWSERBASE_PROJECT_ID=your_project_id\n```\n\nFor OpenAI (or other providers):\n\n```bash\nOPENAI_API_KEY=your_api_key\n```\n\n## Comparison: Stagehand Native vs Puppeteer\n\n| Feature | Stagehand Native | With Puppeteer |\n|---------|------------------|----------------|\n| **Setup** | Simple - use `stagehand.context.pages()` | Requires `puppeteer.connect()` |\n| **Page Access** | `stagehand.context.pages()[0]` | `await browser.pages()` |\n| **AI Methods** | `stagehand.act(\"click\")` | `stagehand.act(\"click\", { page: ppPage })` |\n| **Best For** | Pure Stagehand workflows | Existing Puppeteer codebases |\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Agent\" icon=\"robot\" href=\"/v3/references/agent\">\n    Automate entire workflows\n  </Card>\n  <Card title=\"Act\" icon=\"play\" href=\"/v3/references/act\">\n    Execute actions on web pages\n  </Card>\n  <Card title=\"Extract\" icon=\"ufo-beam\" href=\"/v3/references/extract\">\n    Extract structured data from pages\n  </Card>\n  <Card title=\"Observe\" icon=\"eye\" href=\"/v3/references/observe\">\n    Observe and find elements on pages\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/selenium.mdx",
    "content": "---\ntitle: Selenium\ndescription: Use Stagehand with Selenium to operate the same browser in tandem\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nStagehand v3 can work alongside Selenium WebDriver, allowing both tools to operate on the same browser session simultaneously. This enables you to combine Stagehand's AI-powered automation with Selenium's precise element interactions.\n\n<Warning>\n**Browserbase Only**: This integration requires Browserbase. It does not work with `env: \"LOCAL\"` because Selenium needs a remote WebDriver endpoint.\n</Warning>\n\n## Installation\n\nInstall Stagehand, Selenium, and the Browserbase SDK:\n\n```bash\nnpm install @browserbasehq/stagehand selenium-webdriver @browserbasehq/sdk\n```\n\n## Quickstart\n\n### Create Shared Session\n\nUse the Browserbase SDK to create a session that both tools can connect to:\n\n```typescript\nimport http from \"http\";\nimport { Builder, Key } from \"selenium-webdriver\";\nimport Browserbase from \"@browserbasehq/sdk\";\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst bb = new Browserbase({\n  apiKey: process.env.BROWSERBASE_API_KEY,\n});\n\n// Create shared session\nconst session = await bb.sessions.create({\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n});\n\nconsole.log(\"Session created:\", session.id);\n```\n\n### Connect Stagehand\n\nInitialize Stagehand with the session ID:\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  browserbaseSessionID: session.id,\n  model: \"openai/gpt-5\",\n  verbose: 2,\n});\n\nawait stagehand.init();\n```\n\n### Connect Selenium\n\nUse a custom HTTP agent with the session's signing key:\n\n```typescript\n// Create custom HTTP agent with signing key\nconst customHttpAgent = new http.Agent({});\n(customHttpAgent as any).addRequest = (req: any, options: any) => {\n  req.setHeader(\"x-bb-signing-key\", session.signingKey);\n  (http.Agent.prototype as any).addRequest.call(customHttpAgent, req, options);\n};\n\n// Connect Selenium WebDriver\nconst driver = new Builder()\n  .forBrowser(\"chrome\")\n  .usingHttpAgent(customHttpAgent)\n  .usingServer(session.seleniumRemoteUrl)\n  .build();\n```\n\n### Use Both Tools Together\n\nNow both Stagehand and Selenium operate on the same browser:\n\n```typescript\n// Navigate with Stagehand\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://www.google.com\");\n\n// Extract page content with Stagehand AI\nconst pageContent = await stagehand.extract();\nconsole.log(\"Page content:\", pageContent);\n\n// Use Selenium for precise element interaction\nconst searchBox = await driver.findElement({ name: \"q\" });\nawait searchBox.sendKeys(\"Browserbase automation\");\nawait searchBox.sendKeys(Key.RETURN);\n\n// Wait for results\nawait driver.sleep(2000);\n\nconsole.log(\"Search completed!\");\n```\n\n## Key Points\n\n- **Shared Session**: Both tools connect to the same Browserbase session\n- **Signing Key**: Selenium requires the session's `signingKey` in HTTP headers\n- **Remote URL**: Use `session.seleniumRemoteUrl` for Selenium's server endpoint\n- **Concurrent Usage**: Both tools can operate on the browser simultaneously\n- **Cleanup**: Close both Stagehand (`await stagehand.close()`) and Selenium (`await driver.quit()`)\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Agent\" icon=\"robot\" href=\"/v3/references/agent\">\n    Automate entire workflows\n  </Card>\n  <Card title=\"Act\" icon=\"play\" href=\"/v3/references/act\">\n    Execute actions on web pages\n  </Card>\n  <Card title=\"Extract\" icon=\"ufo-beam\" href=\"/v3/references/extract\">\n    Extract structured data from pages\n  </Card>\n  <Card title=\"Observe\" icon=\"eye\" href=\"/v3/references/observe\">\n    Observe and find elements on pages\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/integrations/vercel/configuration.mdx",
    "content": "---\ntitle: Use Stagehand in Next.js\nsidebarTitle: Configuration\ndescription: Next.js is a popular framework for developing web-based applications in production. It powers Stagehand apps like [Director](https://director.ai), [Brainrot](https://brainrot.run) and [Open Operator](https://operator.browserbase.com).\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<Card\n  title=\"Check out the Stagehand Next.js Quickstart\"\n  icon=\"github\"\n  href=\"https://github.com/browserbase/stagehand-nextjs-quickstart\"\n>\n  Clone our [GitHub repo](https://github.com/browserbase/stagehand-nextjs-quickstart) to get started with Stagehand (v2) in Next.js.\n</Card>\n\n## Add Stagehand to an existing Next.js project\nIf you'd like to start from scratch, you can run:\n\n<Tabs>\n<Tab title=\"npm\">\n```bash\nnpm create next-app@latest stagehand-nextjs --yes\ncd stagehand-nextjs\n```\n</Tab>\n<Tab title=\"pnpm\">\n```bash\npnpm create next-app@latest stagehand-nextjs --yes\ncd stagehand-nextjs\n```\n</Tab>\n<Tab title=\"yarn\">\n```bash\nyarn create next-app@latest stagehand-nextjs --yes\ncd stagehand-nextjs\n```\n</Tab>\n</Tabs>\n\nIf you'd like to add Stagehand to an existing Next.js project, you can do so by installing the dependencies:\n<Tabs>\n\t<Tab title=\"npm\">\n\t```bash\n\tnpm install @browserbasehq/stagehand @browserbasehq/sdk playwright zod\n\t```\n\t</Tab>\n\n\t<Tab title=\"pnpm\">\n\t```bash\n\tpnpm add @browserbasehq/stagehand @browserbasehq/sdk playwright zod\n\t```\n\t</Tab>\n\n\t<Tab title=\"yarn\">\n\t```bash\n\tyarn add @browserbasehq/stagehand @browserbasehq/sdk playwright zod\n\t```\n\t</Tab>\n</Tabs>\n\n### Add environment variables\nNext, let's add the environment variables to a `.env` file.\n```env\nBROWSERBASE_API_KEY=your-browserbase-api-key\nBROWSERBASE_PROJECT_ID=your-browserbase-project-id\nOPENAI_API_KEY=your-openai-api-key\n```\n\n### Write a server action\nNext, let's define our `main` function as a server action in `app/stagehand/main.ts`. This file will have the following three functions:\n\n1. **`main`: Run the main Stagehand script**\n2. **`runStagehand`: Initialize and run the `main` function**\n3. **`startBBSSession`: Start a Browserbase session**\n\n```ts app/stagehand/main.ts\n// 🤘 Welcome to Stagehand!\n// This file is from the [Stagehand docs](https://docs.stagehand.dev/sections/examples/nextjs).\n\n\"use server\";\n\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\nimport { Browserbase } from \"@browserbasehq/sdk\";\n\n/**\n * Run the main Stagehand script\n */\nasync function main(stagehand: Stagehand) {\n  // You can use the `page` instance to write any Playwright code\n  // For more info: https://playwright.dev/docs/pom\n  const page = stagehand.context.activePage();\n\n  // In this example, we'll get the title of the Stagehand quickstart page\n  await page?.goto(\"https://docs.stagehand.dev/\");\n  await stagehand.act(\"click the quickstart link\");\n  const { title } = await stagehand.extract(\n    \"extract the main heading of the page\",\n    z.object({\n      title: z.string(),\n    }),\n  );\n\n  return title;\n}\n\n/**\n * Initialize and run the main() function\n */\nexport async function runStagehand(sessionId?: string) {\n  const stagehand = new Stagehand({\n    env: \"BROWSERBASE\",\n    apiKey: process.env.BROWSERBASE_API_KEY,\n    projectId: process.env.BROWSERBASE_PROJECT_ID,\n    verbose: 1,\n    logger: console.log,\n    browserbaseSessionID: sessionId,\n    disablePino: true,\n  });\n  await stagehand.init();\n  const result = await main(stagehand);\n  console.log(result);\n  await stagehand.close();\n}\n\n/**\n * Start a Browserbase session\n */\nexport async function startBBSSession() {\n  const browserbase = new Browserbase();\n  const session = await browserbase.sessions.create({\n    projectId: process.env.BROWSERBASE_PROJECT_ID!,\n  });\n  const debugUrl = await browserbase.sessions.debug(session.id);\n  return {\n    sessionId: session.id,\n    debugUrl: debugUrl.debuggerFullscreenUrl,\n  };\n}\n```\n\n### Create a client component\nNext, let's create a client component that will start a Browserbase session and run the `main` function with the server actions we just defined. We'll first create a Browserbase session and embed the session in an iframe before running the `main` function.\n\n```tsx app/components/stagehandEmbed.tsx\n\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { runStagehand, startBBSSession } from \"@/app/stagehand/main\";\n\nexport function StagehandEmbed() {\n  const [sessionId, setSessionId] = useState<string | null>(null);\n  const [debugUrl, setDebugUrl] = useState<string | null>(null);\n\n  const startSession = useCallback(async () => {\n    const { sessionId, debugUrl } = await startBBSSession();\n    setSessionId(sessionId);\n    setDebugUrl(debugUrl);\n    await runStagehand(sessionId);\n  }, []);\n\n  return (\n    <div>\n      {!sessionId && <button onClick={startSession}>Start Session</button>}\n      {sessionId && debugUrl && (\n        <iframe src={debugUrl} className=\"w-full h-full\" />\n      )}\n    </div>\n  );\n}\n```\n\n### Use the `StagehandEmbed` component\nNow, we can use the `StagehandEmbed` component in our app.\n\n```tsx app/page.tsx\nimport { StagehandEmbed } from \"@/app/components/stagehandEmbed\";\n\nexport default function Home() {\n\treturn (\n\t\t<main>\n\t\t\t<StagehandEmbed />\n\t\t</main>\n\t)\n}\n```\n\n### Run the app\nTo run the app, you can use the following command:\n\n<Tabs>\n<Tab title=\"npm\">\n```bash\nnpm run dev\n```\n</Tab>\n<Tab title=\"pnpm\">\n```bash\npnpm dev\n```\n</Tab>\n<Tab title=\"yarn\">\n```bash\nyarn dev\n```\n</Tab>\n</Tabs>\n\n### Deploy the app\nTo deploy the app, you can use the following commands. First, install the Vercel CLI:\n<Tabs>\n<Tab title=\"npm\">\n```bash\nnpm add -g vercel\n```\n</Tab>\n<Tab title=\"pnpm\">\n```bash\npnpm add -g vercel\n```\n</Tab>\n<Tab title=\"yarn\">\n```bash\nyarn add -g vercel\n```\n</Tab>\n</Tabs>\n\nThen, run the following command to deploy the app:\n```bash\nvercel\n```\n\n## References\n\n<CardGroup cols={2}>\n  <Card title=\"Deploy Template (v2)\" icon=\"rocket\" href=\"https://vercel.com/templates/ai/stagehand-next-js-quickstart\">\n    One‑click deploy the Stagehand Next.js template on Vercel (Stagehand v2)\n  </Card>\n  \n  <Card title=\"Source Cod (v2)\" icon=\"github\" href=\"https://github.com/browserbase/stagehand-nextjs-quickstart\">\n    Browse the complete template repository on GitHub (Stagehand v2)\n  </Card>\n</CardGroup>"
  },
  {
    "path": "packages/docs/v3/integrations/vercel/introduction.mdx",
    "content": "---\ntitle: \"Next.js + Vercel\"\nsidebarTitle: \"Introduction\"\ndescription: \"Build and deploy a Stagehand‑powered Next.js app to Vercel\"\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Overview\n\nThe Stagehand + Next.js Quickstart is a production‑ready template that pairs Stagehand's AI browser automation with a modern Next.js app, deployable in one click on Vercel.\n\n<CardGroup cols={3}>\n  <Card title=\"Deploy Template\" icon=\"rocket\" href=\"https://vercel.com/templates/ai/stagehand-next-js-quickstart\">\n    One‑click deploy to Vercel with environment setup\n  </Card>\n\n  <Card title=\"Live Demo\" icon=\"globe\" href=\"https://stagehand-nextjs-quickstart.vercel.app\">\n    See the deployed template in action\n  </Card>\n\n  <Card title=\"Source Code\" icon=\"github\" href=\"https://github.com/browserbase/stagehand-nextjs-quickstart\">\n    Browse the repository on GitHub (Stagehand v2)\n  </Card>\n</CardGroup>\n\n## What you get\n\n<CardGroup cols={2}>\n  <Card title=\"App Router project\" icon=\"browser\">\n    Next.js App Router scaffold with Tailwind styling\n  </Card>\n  <Card title=\"Server‑safe automation\" icon=\"shield-check\">\n    Uses Browserbase for cloud browsers (works on Vercel functions)\n  </Card>\n  <Card title=\"Prewired config\" icon=\"gear\">\n    `stagehand.config.ts` with model + provider switching\n  </Card>\n  <Card title=\"Automation ready\" icon=\"robot\">\n    Example usage of Stagehand primitives\n  </Card>\n</CardGroup>\n\n## Requirements\n\n- **Node 18+** locally\n- **Model key**: OpenAI or Anthropic (or plug a custom client)\n- **Browserbase keys**: `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` for cloud browsers\n\n<Tip>\nLocal Playwright browsers are not available on Vercel. Set Stagehand to Browserbase when deploying.\n</Tip>\n\n## Links\n\n<CardGroup cols={2}>\n  <Card title=\"Walkthrough\" icon=\"rocket\" href=\"/integrations/vercel/configuration\">\n    Run locally and deploy to Vercel in minutes\n  </Card>\n</CardGroup>\n\n"
  },
  {
    "path": "packages/docs/v3/migrations/python.mdx",
    "content": "---\ntitle: Migrate Python v2 to v3\nsidebarTitle: Migrate Python v2 to v3\ndescription: Complete migration guide from Stagehand Python SDK v2 to the new Stainless-based v3 SDK\nicon: 'snake'\n---\n\nThis guide helps you migrate from the legacy Stagehand Python SDK to the new Stainless-based SDK with a **Bring Your Own Browser (BYOB)** architecture.\n\n<Note>\nThe new Python SDK is a pure API client. You manage the browser yourself using Playwright, Selenium, Puppeteer, or any other browser automation tool. The SDK handles only the AI-powered operations.\n</Note>\n\n## Overview of Changes\n\n<CardGroup cols={2}>\n  <Card title=\"BYOB Architecture\" icon=\"browser\">\n    You bring your own browser driver (Playwright, Selenium, etc.). The SDK is now a pure API client that handles AI-powered operations.\n  </Card>\n  <Card title=\"Session-Based API\" icon=\"key\">\n    All operations require an explicit `session_id`. Start a session, perform operations, and end it when done.\n  </Card>\n  <Card title=\"Multi-Browser Control\" icon=\"browsers\">\n    Scale browsers easily and control multiple browsers at once by passing the session ID for each browser you want to control.\n  </Card>\n  <Card title=\"Simplified Client\" icon=\"code\">\n    Cleaner initialization with dedicated parameters for API keys and configuration.\n  </Card>\n</CardGroup>\n\n### Current Limitations\n\n<Warning>\nThe new SDK does **not yet support**:\n- Custom Python LLM client classes (e.g., `model_client_options`)\n- However, we do support custom endpoints like Bedrock or LLM proxies as long as they are OpenAI-API compatible\n</Warning>\n\n---\n\n## Step-by-Step Migration\n\n### 1. Update Imports\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nimport asyncio\nimport logging\nfrom stagehand import Stagehand, StagehandConfig, configure_logging\n\n# Configure logging\nconfigure_logging(\n    level=logging.INFO,\n    remove_logger_name=True,\n    quiet_dependencies=True,\n)\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\nimport os\nfrom playwright.sync_api import sync_playwright\nfrom stagehand import Stagehand\n\n# Note: Custom logging configuration is not yet supported.\n# Use standard Python logging if needed:\nimport logging\nlogging.basicConfig(level=logging.INFO)\n```\n\n</Tab>\n</Tabs>\n\n---\n\n### 2. Client Initialization\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nconfig = StagehandConfig(\n    env=\"BROWSERBASE\",\n    api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n    project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n    headless=False,\n    dom_settle_timeout_ms=3000,\n    model_name=\"google/gemini-2.0-flash\",\n    self_heal=True,\n    wait_for_captcha_solves=True,\n    system_prompt=\"You are a browser automation assistant...\",\n    model_client_options={\"apiKey\": os.getenv(\"MODEL_API_KEY\")},\n    verbose=2,\n)\n\nstagehand = Stagehand(config)\nawait stagehand.init()\npage = stagehand.page\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\nSDK_VERSION = \"3.0.6\"\n\n# Create the Stagehand API client\nclient = Stagehand(\n    browserbase_api_key=os.environ.get(\"BROWSERBASE_API_KEY\"),\n    browserbase_project_id=os.environ.get(\"BROWSERBASE_PROJECT_ID\"),\n    model_api_key=os.environ.get(\"MODEL_API_KEY\"),\n)\n\n# Start a session (returns session metadata)\nstart_response = client.sessions.start(\n    model_name=\"google/gemini-2.0-flash\",\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\nsession_id = start_response.data.session_id\nprint(f\"Session started: {session_id}\")\n\n# Connect Playwright to the Browserbase session\nplaywright = sync_playwright().start()\nbrowser = playwright.chromium.connect_over_cdp(\n    f\"wss://connect.browserbase.com?apiKey={os.environ['BROWSERBASE_API_KEY']}&sessionId={session_id}\"\n)\ncontext = browser.contexts[0]\npage = context.pages[0] if context.pages else context.new_page()\n```\n\n</Tab>\n</Tabs>\n\n<Info>\n**Key differences:**\n- Configuration options like `dom_settle_timeout_ms`, `self_heal`, `system_prompt`, and `verbose` are not available in the new SDK\n- `model_name` is specified when starting a session, not in the config\n- You must connect Playwright separately to interact with the page\n</Info>\n\n---\n\n### 3. Navigation\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nawait page.goto(\"https://google.com/\")\n```\n\n</Tab>\n<Tab title=\"New SDK (v3) - Option A: Playwright\">\n\n```python\n# Recommended for simple navigation\npage.goto(\"https://google.com/\")\n```\n\n</Tab>\n<Tab title=\"New SDK (v3) - Option B: Stagehand API\">\n\n```python\n# Use this if you need Stagehand to track navigation state\nclient.sessions.navigate(\n    id=session_id,\n    url=\"https://google.com/\",\n    frame_id=\"\",  # Empty string for main frame\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\n```\n\n</Tab>\n</Tabs>\n\n---\n\n### 4. Direct Page Interactions (Playwright)\n\nAny direct page manipulation should use Playwright's native API.\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\n# Click using Playwright locator (this was already Playwright)\nawait page.get_by_role(\"link\", name=\"About\", exact=True).click()\n\n# Keyboard input\nawait page.keyboard.press(\"Enter\")\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\n# Same Playwright API, but synchronous (or use async Playwright if preferred)\npage.get_by_role(\"link\", name=\"About\", exact=True).click()\n\n# Keyboard input\npage.keyboard.press(\"Enter\")\n```\n\n</Tab>\n</Tabs>\n\n<Note>\nIn the old SDK, `page` was a Stagehand-enhanced Playwright page. In the new SDK, `page` is a standard Playwright page. Direct Playwright methods work the same way.\n</Note>\n\n---\n\n### 5. AI-Powered Actions (`act`)\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nawait page.act(\"search for openai\")\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\nact_response = client.sessions.act(\n    id=session_id,\n    input=\"search for openai\",\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\nprint(f\"Act completed: {act_response.data.result.message}\")\n```\n\n</Tab>\n</Tabs>\n\n#### Acting on an Observed Element\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nobserved = await page.observe(\"find all articles\")\nif observed:\n    await page.act(observed[0])\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\nobserve_response = client.sessions.observe(\n    id=session_id,\n    instruction=\"find all articles\",\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\nresults = observe_response.data.result\n\nif results:\n    element = results[0]\n    act_response = client.sessions.act(\n        id=session_id,\n        input=element,  # Pass the observed element directly\n        x_language=\"python\",\n        x_sdk_version=SDK_VERSION,\n    )\n```\n\n</Tab>\n</Tabs>\n\n---\n\n### 6. Observing Elements (`observe`)\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nobserved = await page.observe(\"find all articles\")\nif len(observed) > 0:\n    element = observed[0]\n    print(f\"Found element: {element}\")\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\nobserve_response = client.sessions.observe(\n    id=session_id,\n    instruction=\"find all articles\",\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\nresults = observe_response.data.result\nprint(f\"Found {len(results)} possible actions\")\n\nif results:\n    element = results[0]\n    print(f\"Found element: {element.description}\")\n```\n\n</Tab>\n</Tabs>\n\n---\n\n### 7. Extracting Data (`extract`)\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\ndata = await page.extract(\"extract the first result from the search\")\nprint(data.model_dump_json())\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\nextract_response = client.sessions.extract(\n    id=session_id,\n    instruction=\"extract the first result from the search\",\n    schema={\n        \"type\": \"object\",\n        \"properties\": {\n            \"title\": {\n                \"type\": \"string\",\n                \"description\": \"The title of the first search result\"\n            },\n            \"url\": {\n                \"type\": \"string\",\n                \"description\": \"The URL of the first search result\"\n            }\n        },\n        \"required\": [\"title\"]\n    },\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\nextracted_data = extract_response.data.result\nprint(f\"Extracted: {extracted_data}\")\n```\n\n</Tab>\n</Tabs>\n\n<Warning>\n**Key difference:** The new SDK requires an explicit JSON schema for extraction. This provides better type safety and clearer expectations for the AI model.\n</Warning>\n\n---\n\n### 8. Closing the Session\n\n<Tabs>\n<Tab title=\"Old SDK (v2)\">\n\n```python\nawait stagehand.close()\n```\n\n</Tab>\n<Tab title=\"New SDK (v3)\">\n\n```python\n# Clean up Playwright resources\nbrowser.close()\nplaywright.stop()\n\n# End the Stagehand session\nclient.sessions.end(\n    id=session_id,\n    x_language=\"python\",\n    x_sdk_version=SDK_VERSION,\n)\n```\n\n</Tab>\n</Tabs>\n\n<Tip>\n**Important:** Always clean up both Playwright and the Stagehand session. Use a `try/finally` block to ensure cleanup happens even on errors.\n</Tip>\n\n---\n\n### 9. Async vs Sync\n\n<Tabs>\n<Tab title=\"Old SDK (v2) - Async-first\">\n\n```python\nasync def main():\n    stagehand = Stagehand(config)\n    await stagehand.init()\n    await page.goto(\"https://example.com\")\n    await stagehand.close()\n\nasyncio.run(main())\n```\n\n</Tab>\n<Tab title=\"New SDK (v3) - Sync-first\">\n\n```python\ndef main():\n    client = Stagehand(...)\n\n    with sync_playwright() as playwright:\n        # ... setup browser connection\n        page.goto(\"https://example.com\")\n        # ... cleanup\n\nmain()\n```\n\n</Tab>\n<Tab title=\"New SDK (v3) - Async\">\n\n```python\nimport asyncio\nfrom playwright.async_api import async_playwright\n\nasync def main():\n    client = Stagehand(...)  # Client is sync, but that's OK\n\n    async with async_playwright() as playwright:\n        browser = await playwright.chromium.connect_over_cdp(...)\n        # ... async Playwright operations\n\nasyncio.run(main())\n```\n\n</Tab>\n</Tabs>\n\n---\n\n## Complete Migration Example\n\n<Tabs>\n<Tab title=\"Before (Old SDK)\">\n\n```python\nimport asyncio\nimport logging\nimport os\n\nfrom dotenv import load_dotenv\nfrom stagehand import Stagehand, StagehandConfig, configure_logging\n\nconfigure_logging(level=logging.INFO, remove_logger_name=True, quiet_dependencies=True)\nload_dotenv()\n\nasync def main():\n    config = StagehandConfig(\n        env=\"BROWSERBASE\",\n        api_key=os.getenv(\"BROWSERBASE_API_KEY\"),\n        project_id=os.getenv(\"BROWSERBASE_PROJECT_ID\"),\n        headless=False,\n        model_name=\"google/gemini-2.0-flash\",\n        model_client_options={\"apiKey\": os.getenv(\"MODEL_API_KEY\")},\n        verbose=2,\n    )\n\n    stagehand = Stagehand(config)\n    await stagehand.init()\n    page = stagehand.page\n\n    print(f\"Session: {stagehand.session_id}\")\n\n    # Navigate\n    await page.goto(\"https://google.com/\")\n\n    # Direct Playwright interaction\n    await page.get_by_role(\"link\", name=\"About\", exact=True).click()\n\n    # AI-powered action\n    await page.goto(\"https://google.com/\")\n    await page.act(\"search for openai\")\n    await page.keyboard.press(\"Enter\")\n\n    # Observe and act\n    observed = await page.observe(\"find all articles\")\n    if observed:\n        await page.act(observed[0])\n\n    # Extract data\n    data = await page.extract(\"extract the first result\")\n    print(data.model_dump_json())\n\n    await stagehand.close()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n</Tab>\n<Tab title=\"After (New SDK)\">\n\n```python\nimport os\nimport logging\n\nfrom dotenv import load_dotenv\nfrom playwright.sync_api import sync_playwright\nfrom stagehand import Stagehand\n\n# Standard Python logging (custom Stagehand logging not yet supported)\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\nload_dotenv()\n\nSDK_VERSION = \"3.0.6\"\n\ndef main():\n    # Create Stagehand API client\n    client = Stagehand(\n        browserbase_api_key=os.environ.get(\"BROWSERBASE_API_KEY\"),\n        browserbase_project_id=os.environ.get(\"BROWSERBASE_PROJECT_ID\"),\n        model_api_key=os.environ.get(\"MODEL_API_KEY\"),\n    )\n\n    # Start a session\n    start_response = client.sessions.start(\n        model_name=\"google/gemini-2.0-flash\",\n        x_language=\"python\",\n        x_sdk_version=SDK_VERSION,\n    )\n    session_id = start_response.data.session_id\n    logger.info(f\"Session started: {session_id}\")\n    logger.info(f\"View live: https://www.browserbase.com/sessions/{session_id}\")\n\n    # Connect Playwright to the Browserbase session\n    with sync_playwright() as playwright:\n        browser = playwright.chromium.connect_over_cdp(\n            f\"wss://connect.browserbase.com?apiKey={os.environ['BROWSERBASE_API_KEY']}&sessionId={session_id}\"\n        )\n        context = browser.contexts[0]\n        page = context.pages[0] if context.pages else context.new_page()\n\n        try:\n            # Navigate (using Playwright directly)\n            page.goto(\"https://google.com/\")\n            logger.info(\"Navigated to Google\")\n\n            # Direct Playwright interaction\n            page.get_by_role(\"link\", name=\"About\", exact=True).click()\n            logger.info(\"Clicked About link\")\n\n            # Navigate back\n            page.goto(\"https://google.com/\")\n\n            # AI-powered action (using Stagehand API)\n            act_response = client.sessions.act(\n                id=session_id,\n                input=\"search for openai\",\n                x_language=\"python\",\n                x_sdk_version=SDK_VERSION,\n            )\n            logger.info(f\"Act completed: {act_response.data.result.message}\")\n\n            # Keyboard input (using Playwright)\n            page.keyboard.press(\"Enter\")\n\n            # Wait for results\n            page.wait_for_timeout(2000)\n\n            # Observe elements (using Stagehand API)\n            observe_response = client.sessions.observe(\n                id=session_id,\n                instruction=\"find all articles\",\n                x_language=\"python\",\n                x_sdk_version=SDK_VERSION,\n            )\n            results = observe_response.data.result\n\n            if results:\n                element = results[0]\n                logger.info(f\"Found element: {element.description}\")\n\n                # Act on observed element\n                client.sessions.act(\n                    id=session_id,\n                    input=element,\n                    x_language=\"python\",\n                    x_sdk_version=SDK_VERSION,\n                )\n            else:\n                logger.warning(\"No elements found\")\n\n            # Extract data (using Stagehand API with schema)\n            extract_response = client.sessions.extract(\n                id=session_id,\n                instruction=\"extract the first result from the search\",\n                schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"title\": {\"type\": \"string\", \"description\": \"Result title\"},\n                        \"url\": {\"type\": \"string\", \"description\": \"Result URL\"},\n                        \"snippet\": {\"type\": \"string\", \"description\": \"Result snippet\"},\n                    },\n                    \"required\": [\"title\"],\n                },\n                x_language=\"python\",\n                x_sdk_version=SDK_VERSION,\n            )\n            logger.info(f\"Extracted data: {extract_response.data.result}\")\n\n        finally:\n            # Clean up Playwright\n            browser.close()\n\n            # End the Stagehand session\n            client.sessions.end(\n                id=session_id,\n                x_language=\"python\",\n                x_sdk_version=SDK_VERSION,\n            )\n            logger.info(\"Session ended\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\n</Tab>\n</Tabs>\n\n---\n\n## Quick Reference: Method Mapping\n\n| Old SDK | New SDK |\n|---------|---------|\n| `Stagehand(config)` | `Stagehand(browserbase_api_key=..., ...)` |\n| `await stagehand.init()` | `client.sessions.start(...)` |\n| `stagehand.page` | Connect Playwright separately |\n| `stagehand.session_id` | `start_response.data.session_id` |\n| `await page.goto(url)` | `page.goto(url)` (Playwright) |\n| `await page.act(instruction)` | `client.sessions.act(id=session_id, input=instruction, ...)` |\n| `await page.observe(instruction)` | `client.sessions.observe(id=session_id, instruction=..., ...)` |\n| `await page.extract(instruction)` | `client.sessions.extract(id=session_id, instruction=..., schema=..., ...)` |\n| `await stagehand.close()` | `browser.close()` + `client.sessions.end(id=session_id, ...)` |\n| `configure_logging(...)` | Use standard `logging` module |\n\n---\n\n## Troubleshooting\n\n<AccordionGroup>\n  <Accordion title=\"Session not found errors\">\n    Ensure you're using the correct `session_id` returned from `client.sessions.start()`.\n  </Accordion>\n  \n  <Accordion title=\"Playwright connection issues\">\n    Make sure your Browserbase API key has the correct permissions and the session is still active.\n  </Accordion>\n  \n  <Accordion title=\"Missing x_language and x_sdk_version parameters\">\n    These are required for all session operations. Use `x_language=\"python\"` and `x_sdk_version=\"3.0.6\"` (or the latest version).\n  </Accordion>\n  \n  <Accordion title=\"Extraction returns unexpected format\">\n    The new SDK requires an explicit JSON schema. Make sure your schema matches the expected output structure.\n  </Accordion>\n</AccordionGroup>\n\n---\n\n## Need Help?\n\n<CardGroup cols={3}>\n  <Card title=\"Documentation\" icon=\"book\" href=\"https://docs.stagehand.dev/\">\n    Full Stagehand documentation\n  </Card>\n  <Card title=\"Browserbase Docs\" icon=\"server\" href=\"https://docs.browserbase.com/\">\n    Browserbase documentation\n  </Card>\n  <Card title=\"GitHub Issues\" icon=\"github\" href=\"https://github.com/browserbase/stagehand-python/issues\">\n    Report issues or get help\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "packages/docs/v3/migrations/v2.mdx",
    "content": "---\ntitle: Migrate TypeScript v2 to v3\nsidebarTitle: Migrate TypeScript v2 to v3\ndescription: Complete migration guide from Stagehand TypeScript SDK v2 to v3\nicon: 'arrow-up-right-dots'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n## Recommended Migration Process\n\n1. **Backup your project**. If you use a version control system, make sure all previous versions are committed.\n2. **Upgrade to Stagehand v3**.\n3. Follow the breaking changes guide below.\n4. Verify your project is working as expected.\n5. Commit your changes.\n\n## Stagehand v3 Package Version\n\nUpdate your `package.json` to use Stagehand v3:\n\n```bash Bash\nnpm install @browserbasehq/stagehand@latest\n```\n\n## Overview of Major Changes\n\nStagehand v3 introduces significant improvements to the API design and functionality:\n\n- **Removing Playwright Dependency**: Stagehand v3 is now a standalone library that does not depend on Playwright. **You can still use Stagehand with Playwright, check out our [Playwright integration](/v3/integrations/playwright) for more details.**\n- **Simplified Method Signatures**: Cleaner, more intuitive parameter structures.\n- **Unified Model Configuration**: Model configuration is now consolidated into a single `model` parameter.\n- **Automatic iframe & Shadow DOM Support**: No more manual flags required.\n- **Improved Type Safety**: Better TypeScript inference and type checking.\n- **Enhanced Multi-Page Support**: New Context API for managing multiple pages.\n- **Streamlined Timeouts**: Consistent timeout naming across all methods.\n- **Auto-caching**: Stagehand v3 now automatically caches actions and agent steps using the [file system cache](/best-practices/caching).\n- **Agent Improvements**: Renamed parameters (`instructions` → `systemPrompt`), unified model configuration, and new `executionModel` option for cost optimization.\n\n\n## Breaking Changes\n\n### Stagehand Initialization\n\n#### Model Configuration Consolidation\n\nThe `modelName` and `modelClientOptions` parameters have been unified into a single `model` parameter.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  modelName: \"openai/gpt-5\",\n  modelClientOptions: {\n    apiKey: process.env.OPENAI_API_KEY\n    baseURL: \"https://custom-proxy.com/v1\"\n  }\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Option 1: String format (recommended for simplicity, auto-loads model API key from env)\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  model: \"openai/gpt-5\"\n});\n\n// Option 2: Object format (for advanced configuration)\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  model: {\n    modelName: \"gpt-5\",\n    apiKey: process.env.OPENAI_API_KEY,\n    baseURL: \"https://custom-proxy.com/v1\"\n  }\n});\n```\n\n#### DOM Settle Timeout Rename\n\nThe `domSettleTimeoutMs` parameter has been renamed to `domSettleTimeout` for consistency.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  domSettleTimeoutMs: 5000\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  domSettleTimeout: 5000\n});\n```\n\n#### Changes to return value of `stagehand.init()`\n\nThe `init()` used to return `debugUrl`, `sessionUrl` and `sessionId`\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst result = await stagehand.init();\nconsole.log(result);\n```\nIn `v2`, the returned object contains:\n```\n{\n  debugUrl: 'https://www.browserbase.com/devtools/inspector.html?wss=connect.browserbase.com/debug/f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0/devtools/page/5474B0E0510C5B6E629BEB06E799CD70?debug=true',\n  sessionUrl: 'https://www.browserbase.com/sessions/f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0',\n  sessionId: 'f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0'\n}\n```\nIn `v3` the return value is `Promise<void>`. The `sessionId`, `sessionUrl`, and `debugUrl` are now directly accessible via the stagehand object:\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconsole.log(\"debugUrl: \", stagehand.browserbaseDebugURL)\nconsole.log(\"sessionUrl: \", stagehand.browserbaseSessionURL)\nconsole.log(\"sessionId: \", stagehand.browserbaseSessionID)\n```\nExample output:\n```\ndebugUrl: 'https://www.browserbase.com/devtools/inspector.html?wss=connect.browserbase.com/debug/f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0/devtools/page/5474B0E0510C5B6E629BEB06E799CD70?debug=true',\nsessionUrl: 'https://www.browserbase.com/sessions/f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0',\nsessionId: 'f8a21b4a-6fa1-4ab9-9007-fbfe61dc14f0'\n```\n\n#### Caching Changes\n\nThe `enableCaching` boolean has been replaced with a `cacheDir` string for more flexible cache management.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  enableCaching: true\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  cacheDir: \"./stagehand-cache\"  // Specify cache directory\n});\n```\n\n#### Page Access Changes\n\nDirect page access has changed to use the Context API.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\n\n// Direct page access\nconst page = stagehand.page;\nawait page.goto(\"https://example.com\");\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\n\n// Access via context\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\n// Or `await` the active page\nconst page = stagehand.context.awaitActivePage();\nawait page.goto(\"https://example.com\");\n```\n\n### Context and Multi-Page Management\n\n#### New Context API\n\nv3 introduces a structured Context API for managing multiple pages.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\n\n// Limited multi-page support\nconst page = stagehand.page;\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\n\n// Access all pages\nconst pages = stagehand.context.pages();\nconst mainPage = pages[0];\n\n// Create new page\nconst newPage = await stagehand.context.newPage();\n\n// Set active page\nstagehand.context.setActivePage(newPage);\n\n// implicitly takes action on newPage\nawait stagehand.act(\"click button\");\n```\n\n### act() Method Changes\n\n#### Method Signature Simplification\n\nThe `action` parameter has been removed from `ActOptions`. Now you only pass the instruction as a string.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nawait page.act({\n  action: \"click the login button\",\n  modelName: \"openai/gpt-5-mini\",\n  variables: { username: \"john\" },\n  timeoutMs: 10000,\n  domSettleTimeoutMs: 5000,\n  iframes: true\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// Clean, simple string instruction\nawait stagehand.act(\"click the login button\");\n\n// With options\nawait stagehand.act(\"click the login button\", {\n  model: \"openai/gpt-5-mini\",\n  variables: { username: \"john\" },\n  timeout: 10000,\n  page: page  // Optional: specify which page\n});\n```\n\n<Note>\n**Method Location Change**: In v3, `act()` is called on the `stagehand` instance, not the `page` object.\n</Note>\n\n#### Model Configuration in act()\n\nModel configuration follows the same pattern as initialization.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nawait page.act({\n  action: \"fill the form\",\n  modelName: \"anthropic/claude-sonnet-4-5\",\n  modelClientOptions: {\n    apiKey: process.env.ANTHROPIC_API_KEY\n  }\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// String format\nawait stagehand.act(\"fill the form\", {\n  model: \"anthropic/claude-sonnet-4-5\"\n});\n\n// Object format\nawait stagehand.act(\"fill the form\", {\n  model: {\n    modelName: \"anthropic/claude-sonnet-4-5\",\n    apiKey: process.env.ANTHROPIC_API_KEY\n  }\n});\n```\n\n#### Timeout Parameter Rename\n\n`timeoutMs` has been renamed to `timeout`.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nawait page.act({\n  action: \"click button\",\n  timeoutMs: 15000\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nawait stagehand.act(\"click button\", {\n  timeout: 15000\n});\n```\n\n#### Automatic iframe Support\n\nThe `iframes` flag has been removed. iframe support is now automatic.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nawait page.act({\n  action: \"click button inside iframe\",\n  iframes: true  // Required to interact with iframes\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// Automatic iframe support - no flag needed\nawait stagehand.act(\"click button inside iframe\");\n```\n\n<Note>\n**Automatic Support**: Stagehand v3 automatically handles iframe and Shadow DOM interactions without requiring explicit flags.\n</Note>\n\n#### Result Structure Changes\n\nThe `ActResult` structure has been enhanced with more detailed information.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst result = await page.act(\"click the button\");\nconsole.log(result.action);  // Single action string\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst result = await stagehand.act(\"click the button\");\nconsole.log(result.actionDescription);  // Overall description\nconsole.log(result.actions);  // Array of action details\n\n// ActResult structure:\n// {\n//   success: boolean;\n//   message: string;\n//   actionDescription: string;\n//   actions: Array<{\n//     selector: string;\n//     description: string;\n//     method?: string;\n//     arguments?: string[];\n//   }>;\n// }\n```\n\n### extract() Method Changes\n\n#### Method Location and Signature\n\n`extract()` has moved from the page object to the stagehand instance, with a cleaner parameter structure.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nimport { z } from \"zod\";\n\nconst result = await page.extract({\n  instruction: \"extract product details\",\n  schema: z.object({\n    name: z.string(),\n    price: z.number()\n  }),\n  modelName: \"openai/gpt-5\",\n  domSettleTimeoutMs: 5000,\n  selector: \"xpath=/html/body/div\",\n  iframes: true\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nimport { z } from \"zod\";\n\n// Cleaner parameter structure\nconst result = await stagehand.extract(\n  \"extract product details\",\n  z.object({\n    name: z.string(),\n    price: z.number()\n  }),\n  {\n    model: \"openai/gpt-5\",\n    selector: \".container\", // NEW: CSS selector support\n    timeout: 10000,\n    page: page  // Optional: specify which page\n  }\n);\n```\n\n<Note>\n**Parameter Order**: In v3, `instruction` and `schema` are separate positional parameters, with `options` as an optional third parameter.\n</Note>\n\n#### Extract Without Schema\n\nSchema-less extraction also has a simpler interface.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\n// String instruction\nconst result = await page.extract(\"get the page title\");\n// Returns: { extraction: \"Page Title\" }\n\n// Raw page content\nconst content = await page.extract();\n// Returns: { page_text: \"...\" }\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// String instruction\nconst result = await stagehand.extract(\"get the page title\");\n// Returns: { extraction: \"Page Title\" }\n\n// Raw page content\nconst content = await stagehand.extract();\n// Returns: { pageText: \"...\" }\n```\n\n#### Model Configuration in extract()\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst data = await page.extract({\n  instruction: \"extract data\",\n  schema: DataSchema,\n  modelName: \"anthropic/claude-sonnet-4-5\",\n  modelClientOptions: {\n    apiKey: process.env.ANTHROPIC_API_KEY\n  }\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst data = await stagehand.extract(\n  \"extract data\",\n  DataSchema,\n  {\n    model: \"anthropic/claude-sonnet-4-5\"\n  }\n);\n```\n\n#### Automatic iframe Support\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst data = await page.extract({\n  instruction: \"extract data from iframe\",\n  schema: DataSchema,\n  iframes: true  // Required for iframe content\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// Automatic iframe support\nconst data = await stagehand.extract(\n  \"extract data from iframe\",\n  DataSchema\n);\n```\n\n#### Array Schema Changes\n\nArray extraction now has a more ergonomic syntax.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nimport { z } from \"zod\";\n\n// Had to wrap array in object\nconst ApartmentListingsSchema = z.object({\n  apartments: z.array(z.object({\n    address: z.string(),\n    price: z.string(),\n    bedrooms: z.number()\n  }))\n});\n\nconst result = await page.extract({\n  instruction: \"extract all apartment listings\",\n  schema: ApartmentListingsSchema\n});\n\n// Access via: result.apartments\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nimport { z } from \"zod\";\n\n// Can use array schema directly\nconst ApartmentListingsSchema = z.array(\n  z.object({\n    address: z.string(),\n    price: z.string(),\n    bedrooms: z.number()\n  })\n);\n\nconst result = await stagehand.extract(\n  \"extract all apartment listings\",\n  ApartmentListingsSchema\n);\n\n// Result is directly the array\nconsole.log(result[0].address);\n```\n\n### observe() Method Changes\n\n#### Method Signature Updates\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst results = await page.observe({\n  instruction: \"find all buttons\",\n  modelName: \"openai/gpt-5\",\n  domSettleTimeoutMs: 5000,\n  drawOverlay: true,\n  iframes: true\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst results = await stagehand.observe(\"find all buttons\", {\n  model: \"openai/gpt-5\",\n  timeout: 10000,\n  selector: \".container\",  // NEW: scope observation to selector\n  page: page  // Optional: specify which page\n});\n```\n\n<Note>\n**Method Location Change**: Like `act()` and `extract()`, `observe()` is now called on the `stagehand` instance.\n</Note>\n\n#### Draw Overlay Removed\n\nThe `drawOverlay` option has been removed in v3.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst results = await page.observe({\n  instruction: \"find buttons\",\n  drawOverlay: true  // Visual debugging\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// drawOverlay is no longer available\nconst results = await stagehand.observe(\"find buttons\");\n```\n\n#### Automatic iframe Support\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst results = await page.observe({\n  instruction: \"find elements in iframe\",\n  iframes: true\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// Automatic iframe support\nconst results = await stagehand.observe(\"find elements in iframe\");\n```\n\n#### Observe with act() Integration\n\nThe observe → act workflow remains similar but with updated method signatures.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst [action] = await page.observe(\"find the login button\");\nawait page.act(action);\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst [action] = await stagehand.observe(\"find the login button\");\nawait stagehand.act(action);\n```\n\n### agent() Method Changes\n\n#### Agent Configuration Updates\n\nThe agent configuration has been significantly restructured in v3 with renamed parameters and new capabilities.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst agent = stagehand.agent({\n  provider: \"google\",\n  model: \"gemini-2.5-computer-use-preview-10-2025\",\n  instructions: \"You are a helpful assistant that can navigate websites.\",\n  options: {\n    apiKey: process.env.GEMINI_API_KEY\n  },\n  integrations: [\"https://mcp-server.example.com\"],\n  tools: customTools\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.5-computer-use-preview-10-2025\",  // Provider now in model string\n  systemPrompt: \"You are a helpful assistant that can navigate websites.\",  // Renamed from 'instructions'\n  mode: \"cua\",  // Computer Use Agent mode\n  integrations: [\"https://mcp-server.example.com\"],\n  tools: customTools\n});\n```\n\n<Note>\n**Key Changes**:\n- `provider` removed - now part of the model string (e.g., `\"anthropic/claude-sonnet-4-5\"`)\n- `instructions` renamed to `systemPrompt`\n- `options` removed - use model object format for advanced configuration\n- `executionModel` added - specify a different model for tool execution\n- `cua` flag added - enable/disable Computer Use Agent mode\n</Note>\n\n#### Model Configuration in agent()\n\nModel configuration follows the same unified pattern as other methods.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst agent = stagehand.agent({\n  provider: \"google\",\n  model: \"gemini-2.5-computer-use-preview-10-2025\",\n  options: {\n    apiKey: process.env.GEMINI_API_KEY,\n    baseURL: \"https://custom-proxy.com/v1\"\n  }\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\n// String format (recommended)\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.5-computer-use-preview-10-2025\"\n});\n\n// Object format for advanced configuration\nconst agent = stagehand.agent({\n  model: {\n    modelName: \"gemini-2.5-computer-use-preview-10-2025\",\n    apiKey: process.env.GEMINI_API_KEY,\n    baseURL: \"https://custom-proxy.com/v1\"\n  }\n});\n```\n\n#### Execute Method Changes\n\nThe `execute()` method has been simplified with some options removed and new ones added.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst result = await agent.execute({\n  instruction: \"Search for products\",\n  maxSteps: 20,\n  autoScreenshot: true,\n  waitBetweenActions: 1000,\n  context: \"Focus on electronics category\"\n});\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst result = await agent.execute({\n  instruction: \"Search for products\",\n  maxSteps: 20,\n  page: page,  // NEW: specify which page to operate on\n  highlightCursor: true  // NEW: visual cursor for debugging\n});\n```\n\n<Warning>\n**Removed Options**:\n- `autoScreenshot` - no longer available\n- `waitBetweenActions` - no longer available\n- `context` - use the `systemPrompt` in agent config instead\n</Warning>\n\n#### Execution Model Configuration\n\nv3 introduces a new `executionModel` option to use a different (often faster/cheaper) model for tool execution.\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5\",  // Main reasoning model\n  executionModel: \"anthropic/claude-haiku-4-5\"  // Faster model for tool execution (act, extract, observe)\n});\n\n// The agent will use claude-sonnet-4-5 for high-level reasoning\n// but claude-haiku-4-5 for executing individual actions\nconst result = await agent.execute(\"Complete the checkout process\");\n```\n\n#### Agent with Multi-Page Support\n\nv3 agents can now specify which page to operate on.\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst page1 = stagehand.context.pages()[0]\nconst page2 = await stagehand.context.newPage();\n\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.5-computer-use-preview-10-2025\"\n});\n\n// Execute on specific page\nawait page2.goto(\"https://example.com/dashboard\");\nconst result = await agent.execute({\n  instruction: \"Export the data table\",\n  page: page2  // Operate on page2 instead of default page\n});\n```\n\n### History and Metrics\n\n#### History API\n\nHistory is now async and returns a promise.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst history = stagehand.history;\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst history = await stagehand.history;\n```\n\n#### Metrics API\n\nMetrics is now async and returns a promise.\n\n```typescript Stagehand v2 icon=\"/images/typescript.svg\"\nconst metrics = stagehand.metrics;\n```\n\n```typescript Stagehand v3 icon=\"/images/typescript.svg\"\nconst metrics = await stagehand.metrics;\n```\n\n## Complete Migration Example\n\nHere's a complete example showing a full migration:\n\n<Tabs>\n<Tab title=\"Stagehand v2\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\n// Initialize\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  modelName: \"openai/gpt-5\",\n  modelClientOptions: {\n    apiKey: process.env.OPENAI_API_KEY\n  },\n  enableCaching: true,\n  domSettleTimeoutMs: 5000\n});\n\nawait stagehand.init();\nconst page = stagehand.page;\n\n// Navigate\nawait page.goto(\"https://example.com\");\n\n// Act\nawait page.act({\n  action: \"click the login button\",\n  timeoutMs: 10000,\n  iframes: true\n});\n\n// Extract\nconst ProductSchema = z.object({\n  name: z.string(),\n  price: z.number(),\n  inStock: z.boolean()\n});\n\nconst product = await page.extract({\n  instruction: \"extract product details\",\n  schema: ProductSchema,\n  domSettleTimeoutMs: 5000,\n  iframes: true\n});\n\n// Observe\nconst actions = await page.observe({\n  instruction: \"find all buttons\",\n  drawOverlay: false,\n  iframes: true\n});\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Stagehand v3\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\n// Initialize - simplified configuration\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  model: \"openai/gpt-5\",  // Unified model configuration\n  cacheDir: \"./cache\",      // Flexible cache directory\n  domSettleTimeout: 5000    // Consistent naming\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];  // Context API\n\n// Navigate\nawait page.goto(\"https://example.com\");\n\n// Act - cleaner interface, automatic iframe support\nawait stagehand.act(\"click the login button\", {\n  timeout: 10000\n  // No iframes flag needed - automatic!\n});\n\n// Extract - cleaner parameter order\nconst ProductSchema = z.object({\n  name: z.string(),\n  price: z.number(),\n  inStock: z.boolean()\n});\n\nconst product = await stagehand.extract(\n  \"extract product details\",\n  ProductSchema\n  // Automatic iframe support, no extra flags needed\n);\n\n// Observe - simplified\nconst actions = await stagehand.observe(\"find all buttons\");\n// Automatic iframe support\n\n// Get metrics\nconst metrics = await stagehand.metrics;\nconsole.log('Total tokens used:',\n  metrics.totalPromptTokens + metrics.totalCompletionTokens);\n\nawait stagehand.close();\n```\n\n</Tab>\n</Tabs>\n\n## Quick Reference: Breaking Changes\n\n<Expandable title=\"Stagehand Initialization\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **Model Config** | `modelName` + `modelClientOptions` | `model: \"provider/model\"` or `{ modelName, apiKey, baseURL }` |\n| **DOM Settle** | `domSettleTimeoutMs` | `domSettleTimeout` |\n| **Caching** | `enableCaching: boolean` | `cacheDir: string` |\n| **Page Access** | `stagehand.page` | `stagehand.context.pages()[0]` |\n\n</Expandable>\n\n<Expandable title=\"act()\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **Method location** | `page.act()` | `stagehand.act()` |\n| **Parameters** | `{ action, ...options }` | `(instruction, options?)` |\n| **Timeout** | `timeoutMs` | `timeout` |\n| **Result structure** | `{ action }` | `{ actionDescription, actions[] }` |\n\n</Expandable>\n\n<Expandable title=\"extract()\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **Method location** | `page.extract()` | `stagehand.extract()` |\n| **Parameters** | `{ instruction, schema, ...options }` | `(instruction, schema, options?)` |\n\n</Expandable>\n\n<Expandable title=\"observe()\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **Method location** | `page.observe()` | `stagehand.observe()` |\n| **Draw overlay** | `drawOverlay: boolean` | Removed |\n\n</Expandable>\n\n<Expandable title=\"agent()\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **Provider** | `provider: \"openai\" \\| \"anthropic\"` | Part of model string |\n| **Instructions** | `instructions: string` | `systemPrompt: string` |\n| **Model** | `model: \"model-name\"` | `model: \"provider/model-name\"` |\n| **Options** | `options: Record<string, unknown>` | Use model object format |\n| **Execute params** | `autoScreenshot`, `waitBetweenActions`, `context` | Removed; added `page`, `highlightCursor` |\n\n</Expandable>\n\n<Expandable title=\"Automatic Features\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **iframe support** | `iframes: true` flag required | Automatic (no flag needed) |\n| **Shadow DOM** | Manual handling | Automatic (no flag needed) |\n\n</Expandable>\n\n<Expandable title=\"Properties & Methods\">\n\n| Feature | Stagehand v2 | Stagehand v3 |\n|---------|--------------|--------------|\n| **History** | `stagehand.history` | `await stagehand.history` |\n| **Metrics** | `stagehand.metrics` | `await stagehand.metrics` |\n\n</Expandable>\n\n## Troubleshooting\n\n### Error: Cannot find property 'page' on Stagehand instance\n\n**Problem**: Direct `stagehand.page` is not supported in Stagehand v3.\n\n**Solution**: Use the Context API or `await` the active page:\n\n```typescript\n// Use context API (recommended)\nconst page = stagehand.context.pages()[0];\n\n// Or grab the active page\nconst page = await stagehand.context.awaitActivePage();\n```\n\n### Error: act() method not found on page\n\n**Problem**: v3 moved `act()`, `extract()`, and `observe()` to the stagehand instance.\n\n**Solution**: Call these methods on the stagehand instance:\n\n```typescript\n// v2 ❌\nawait page.act(\"click button\");\n\n// v3 ✅\nawait stagehand.act(\"click button\");\n```\n\n### TypeScript: Model configuration type errors\n\n**Problem**: TypeScript errors with model configuration.\n\n**Solution**: Use the proper format:\n\n```typescript\n// String format\nmodel: \"openai/gpt-5\"\n\n// Object format\nmodel: {\n  modelName: \"openai/gpt-5\",\n  apiKey: process.env.OPENAI_API_KEY\n}\n```\n\n### Agent configuration errors\n\n**Problem**: Using old `provider` and `instructions` parameters.\n\n**Solution**: Update to v3 format:\n\n```typescript\n// v2 ❌\nconst agent = stagehand.agent({\n  provider: \"anthropic\",\n  model: \"claude-sonnet-4-5\",\n  instructions: \"You are a helpful assistant that...\"\n});\n\n// v3 ✅\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5\",\n  systemPrompt: \"You are a helpful assistant that...\"\n});\n```\n\n### Agent execute options not recognized\n\n**Problem**: Using removed options like `autoScreenshot`, `waitBetweenActions`, or `context`.\n\n**Solution**: Remove these options and use v3 alternatives:\n\n```typescript\n// v2 ❌\nawait agent.execute({\n  instruction: \"task\",\n  autoScreenshot: true,\n  waitBetweenActions: 1000,\n  context: \"additional context\"\n});\n\n// v3 ✅\nconst agent = stagehand.agent({\n  model: \"google/gemini-2.5-computer-use-preview-10-2025\",\n  systemPrompt: \"Your context here.\"  // Move context to systemPrompt\n});\n\nawait agent.execute({\n  instruction: \"task\",\n  highlightCursor: true  // Use new option for visual feedback\n});\n```\n\n## Best Practices for v3\n\n1. **Use the string model format** for simplicity: `model: \"openai/gpt-5\"`\n2. **Leverage automatic iframe support** - remove all `iframes` flags\n3. **Use the Context API** for multi-page scenarios\n4. **Monitor metrics** to track token usage and optimize costs\n5. **Use history** for debugging and understanding automation flow\n6. **Set appropriate timeouts** based on your use case\n7. **Specify cache directory** to improve performance for repeated actions\n8. **Use executionModel for agents** - configure a faster/cheaper model for tool execution while keeping a powerful model for reasoning (e.g., `model: \"anthropic/claude-sonnet-4-5\"`, `executionModel: \"google/gemini-2.0-flash\"`)\n\n## Additional Resources\n\n- [Stagehand v3 Documentation](/v3/first-steps/introduction)\n- [API Reference](/v3/references/stagehand)\n- [Best Practices](/v3/best-practices/caching)\n- [GitHub Issues](https://github.com/browserbase/stagehand/issues)\n\nIf you encounter any issues during migration, please [open an issue](https://github.com/browserbase/stagehand/issues) on our GitHub repository.\n"
  },
  {
    "path": "packages/docs/v3/references/act.mdx",
    "content": "---\ntitle: act()\ndescription: 'Complete API reference for the act() method'\nicon: 'arrow-pointer'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Act\" icon=\"arrow-pointer\" href=\"/v3/basics/act\">\n  See how to use act() to perform browser actions\n</Card>\n</CardGroup>\n\n### Method Signatures\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// String instruction only\nawait stagehand.act(instruction: string): Promise<ActResult>\n\n// Action only - Deterministic (no LLM)\nawait stagehand.act(action: Action): Promise<ActResult>\n\n// String instruction with options\nawait stagehand.act(instruction: string, options: ActOptions): Promise<ActResult>\n\n```\n\n**Action Interface:**\n```typescript\ninterface Action {\n  selector: string;\n  description: string;\n  method: string;\n  arguments: string[];\n}\n```\n\n**ActOptions Interface:**\n```typescript\ninterface ActOptions {\n  model?: ModelConfiguration;\n  variables?: Record<string, VariableValue>;\n  timeout?: number;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  serverCache?: boolean;\n}\n\n// VariableValue can be a simple primitive or a rich object:\ntype VariableValue =\n  | string\n  | number\n  | boolean\n  | { value: string | number | boolean; description?: string };\n\n// ModelConfiguration can be either a string or an object\ntype ModelConfiguration =\n  | string  // Format: \"provider/model\" (e.g., \"openai/gpt-4o\", \"anthropic/claude-sonnet-4-6\")\n  | {\n      modelName: string;  // The model name\n      apiKey?: string;    // Optional: API key override\n      baseURL?: string;   // Optional: Base URL override\n      // Additional provider-specific options\n    }\n```\n\n</Tab>\n\n</Tabs>\n\n### Parameters\n\n<ParamField path=\"instruction | action\" type=\"string | Action\" required>\n  - **Instruction**: Natural language description of the action to perform. Use `%variableName%` syntax to reference variables.\n  - **Action**: A deterministic action to perform: \n  <Expandable title=\"Action\">\n    <ParamField path=\"selector\" type=\"string\" required>\n      The selector (XPath, CSS selector, etc.) used to target the element\n    </ParamField>\n    <ParamField path=\"description\" type=\"string\" required>\n      Description of the action - used for self-healing\n    </ParamField>\n    <ParamField path=\"method\" type=\"string\" required>\n      The method used (e.g., \"click\", \"fill\", \"type\")\n    </ParamField>\n    <ParamField path=\"arguments\" type=\"string[]\" required>\n      Arguments passed to the method\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField path=\"model\" type=\"ModelConfiguration\" optional>\n  Configure the AI model to use for this action. Can be either:\n  - A string in the format `\"provider/model\"` (e.g., `openai/gpt-5`, `google/gemini-2.5-flash`)\n  - An object with detailed configuration\n\n  <Expandable title=\"Model Configuration Object\">\n    <ParamField path=\"modelName\" type=\"string\" required>\n      The model name (e.g., `anthropic/claude-sonnet-4-5`, `google/gemini-2.5-flash`)\n    </ParamField>\n    <ParamField path=\"apiKey\" type=\"string\" optional>\n      API key for the model provider (overrides default)\n    </ParamField>\n    <ParamField path=\"baseURL\" type=\"string\" optional>\n      Base URL for the API endpoint (for custom endpoints or proxies)\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField path=\"variables\" type=\"Record<string, VariableValue>\" optional>\n  Key-value pairs for variable substitution using `%variableName%` syntax in your instruction. Variables are **not shared with LLM providers**, making them ideal for sensitive data like passwords and API keys.\n\n  Values can be simple primitives (`string`, `number`, `boolean`) or rich objects with an optional description (`{ value, description? }`).\n\n  **Example:**\n  ```typescript\n  // Simple values\n  await stagehand.act(\"type %password% into the password field\", {\n    variables: { password: process.env.USER_PASSWORD }\n  });\n\n  // Rich values with descriptions\n  await stagehand.act(\"type %password% into the password field\", {\n    variables: {\n      password: {\n        value: process.env.USER_PASSWORD,\n        description: \"The user's login password\"\n      }\n    }\n  });\n  ```\n</ParamField>\n\n<ParamField path=\"timeout\" type=\"number\" optional>\n  Maximum time in **milliseconds** to wait for the action to complete. Default varies by configuration.\n</ParamField>\n\n<ParamField path=\"page\" type=\"PlaywrightPage | PuppeteerPage | PatchrightPage | Page\" optional>\n  Optional: Specify which page to perform the action on. Supports multiple browser automation libraries:\n  - **Playwright**: Native Playwright Page objects\n  - **Puppeteer**: Puppeteer Page objects\n  - **Patchright**: Patchright Page objects\n  - **Stagehand Page**: Stagehand's wrapped Page object\n\n  If not specified, defaults to the current \"active\" page in your Stagehand instance.\n</ParamField>\n\n<ParamField path=\"serverCache\" type=\"boolean\" optional>\n  Override the instance-level `serverCache` setting for this request. When `true`, enables server-side caching. When `false`, disables it.\n\n  <Note>Only applies when `env` is `\"BROWSERBASE\"`. Has no effect in local environments.</Note>\n\n  Defaults to the value set on the Stagehand constructor (which itself defaults to `true`).\n</ParamField>\n\n### Returns `Promise<ActResult>`\n\n<ResponseField name=\"success\" type=\"boolean\" required>\n  Whether the action completed successfully\n</ResponseField>\n\n<ResponseField name=\"message\" type=\"string\" required>\n  Human-readable message describing the result\n</ResponseField>\n\n<ResponseField name=\"actionDescription\" type=\"string\" required>\n  Instruction that was used to perform the action\n</ResponseField>\n\n<ResponseField name=\"actions\" type=\"Action[]\">\n  Array of actions that were executed\n\n  <Expandable title=\"Action\">\n    <ResponseField name=\"selector\" type=\"string\">\n      The selector (XPath) used to target the element\n    </ResponseField>\n    <ResponseField name=\"description\" type=\"string\">\n      Description of the action\n    </ResponseField>\n    <ResponseField name=\"method\" type=\"string\">\n      The method used (e.g., \"click\", \"fill\", \"type\")\n    </ResponseField>\n    <ResponseField name=\"arguments\" type=\"string[]\">\n      Arguments passed to the method\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n\n<ResponseField name=\"cacheStatus\" type='\"HIT\" | \"MISS\"' optional>\n  Indicates whether the result was served from the server-side cache. Only present when running with `env: \"BROWSERBASE\"` and server-side caching is enabled.\n\n  - **`\"HIT\"`** - Result was served from cache; no LLM tokens were consumed\n  - **`\"MISS\"`** - Result was computed fresh and cached for future calls\n</ResponseField>\n\n**Example Response:**\n```json\n{\n  \"success\": true,\n  \"message\": \"Action completed successfully\",\n  \"actionDescription\": \"Clicked the submit button\",\n  \"actions\": [\n    {\n      \"selector\": \"/html/body/form/button[1]\",\n      \"description\": \"Submit button at bottom of form\",\n      \"method\": \"click\",\n      \"arguments\": []\n    }\n  ]\n}\n```\n\n### Built-in Support\n\n<Note>\n**Iframe and Shadow DOM interactions are supported out of the box.** Stagehand automatically handles iframe traversal and shadow DOM elements without requiring additional configuration or flags.\n</Note>\n\n### Code Examples\n\n<Tabs>\n<Tab title=\"Basic Usage\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\n// Simple action\nawait stagehand.act(\"click the login button\");\n```\n\n</Tab>\n<Tab title=\"Variables\">\n\n```typescript\n// Variables are NOT shared with LLM providers\nawait stagehand.act(\"type %username% into the email field\", {\n  variables: { username: \"user@example.com\" }\n});\n\nawait stagehand.act(\"type %password% into the password field\", {\n  variables: { password: process.env.USER_PASSWORD }\n});\n\nawait stagehand.act(\"click the login button\");\n```\n\n</Tab>\n<Tab title=\"Custom Model\">\n\n```typescript\n// Using string format\nawait stagehand.act(\"choose 'Peach' from the favorite color dropdown\", {\n  model: \"google/gemini-2.5-flash\",\n  timeout: 10000\n});\n\n// Using object format with custom configuration\nawait stagehand.act(\"choose 'Peach' from the favorite color dropdown\", {\n  model: {\n    modelName: \"google/gemini-2.5-flash\",\n    apiKey: process.env.GOOGLE_API_KEY,\n    baseURL: \"https://custom-api-endpoint.com\"\n  },\n  timeout: 10000\n});\n```\n\n</Tab>\n<Tab title=\"Multi-Page\">\n\n```typescript\n// Create multiple pages\nconst page1 = stagehand.context.pages()[0];\nconst page2 = await stagehand.context.newPage();\n\n// Perform actions on specific pages\nawait stagehand.act(\"click the first link\", { page: page1 });\nawait stagehand.act(\"click the second link\", { page: page2 });\n```\n\n</Tab>\n<Tab title=\"Caching\">\n\n<Tip>\n**Auto-caching is now available in v3.** See the [caching guide](/v3/best-practices/caching) for more details.\n</Tip>\n```typescript\n// Observe first to plan the action\nconst [action] = await stagehand.observe(\"click the submit button\");\n\n// Cache and reuse the action\nif (action) {\n  await stagehand.act(action);\n}\n\n// Later, reuse the same cached action\nawait stagehand.act(action);\n```\n\n</Tab>\n</Tabs>\n\n### Error Types\n\nThe following errors may be thrown by the `act()` method:\n\n- **StagehandError** - Base class for all Stagehand-specific errors\n- **StagehandElementNotFoundError** - Target element could not be located using the provided selector(s)\n- **StagehandClickError** - Failed to click on the target element\n- **StagehandEvalError** - Error occurred while evaluating JavaScript in the page context\n- **StagehandDomProcessError** - Error occurred while processing the DOM\n- **StagehandIframeError** - Unable to resolve iframe for the target element\n- **ContentFrameNotFoundError** - Unable to obtain content frame for the selector\n- **XPathResolutionError** - XPath does not resolve in the current page or frames\n- **StagehandShadowRootMissingError** - No shadow root present on the resolved host element\n- **LLMResponseError** - Error in LLM response processing\n- **MissingLLMConfigurationError** - No LLM API key or client configured\n- **UnsupportedModelError** - The specified model is not supported for this operation\n- **InvalidAISDKModelFormatError** - Model string does not follow the required `provider/model` format"
  },
  {
    "path": "packages/docs/v3/references/agent.mdx",
    "content": "---\ntitle: agent()\ndescription: 'Complete API reference for the agent() method'\nicon: 'robot'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Agent\" icon=\"robot\" href=\"/v3/basics/agent\">\n  See how to use agent() to create autonomous AI agents for multi-step browser workflows\n</Card>\n</CardGroup>\n\n### Agent Creation\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// Create agent instance\nconst agent = stagehand.agent(config?: AgentConfig): AgentInstance\n```\n\n**AgentConfig Interface:**\n```typescript\ninterface AgentConfig {\n  systemPrompt?: string;\n  integrations?: (Client | string)[];\n  tools?: ToolSet;\n  /** @deprecated Use `mode: \"cua\"` instead */\n  cua?: boolean;\n  model?: string | AgentModelConfig<string>;\n  executionModel?: string | AgentModelConfig<string>;\n  stream?: boolean; // Enable streaming mode (experimental)\n  mode?: \"dom\" | \"hybrid\" | \"cua\"; // Tool mode\n}\n\n// AgentModelConfig for advanced configuration\ntype AgentModelConfig<TModelName extends string = string> = {\n  modelName: TModelName;\n} & Record<string, unknown>;\n```\n\n**AgentInstance Interface:**\n```typescript\ninterface AgentInstance {\n  execute: (instructionOrOptions: string | AgentExecuteOptions) => Promise<AgentResult>;\n}\n```\n\n</Tab>\n\n</Tabs>\n\n### Agent Configuration\n\n<ParamField path=\"systemPrompt\" type=\"string\" optional>\n  Custom system prompt to provide to the agent. Overrides the default system prompt and defines agent behavior.\n</ParamField>\n\n<ParamField path=\"model\" type=\"string | AgentModelConfig\" optional>\n  The model to use for agent functionality. Can be either:\n  - A string in the format `\"provider/model\"` (e.g., `\"openai/computer-use-preview\"`, `\"anthropic/claude-sonnet-4-20250514\"`)\n  - An object with `modelName` and additional provider-specific options\n\n  **Available CUA Models:**\n  - `\"anthropic/claude-haiku-4-5-20251001\"`\n  - `\"anthropic/claude-sonnet-4-6\"`\n  - `\"anthropic/claude-sonnet-4-5-20250929\"`\n  - `\"anthropic/claude-opus-4-5-20251101\"`\n  - `\"anthropic/claude-opus-4-6\"`\n  - `\"google/gemini-2.5-computer-use-preview-10-2025\"`\n  - `\"google/gemini-3-flash-preview\"`\n  - `\"google/gemini-3-pro-preview\"`\n  - `\"microsoft/fara-7b\"`\n  - `\"openai/computer-use-preview\"`\n  - `\"openai/computer-use-preview-2025-03-11\"`\n\n  <Expandable title=\"AgentModelConfig Object\">\n    <ParamField path=\"modelName\" type=\"string\" required>\n      The model name\n    </ParamField>\n    <ParamField path=\"[key: string]\" type=\"unknown\" optional>\n      Additional provider-specific options (e.g., `apiKey`, `baseURL`)\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField path=\"executionModel\" type=\"string | AgentModelConfig\" optional>\n  The model to use for tool execution (observe/act calls within agent tools). If not specified, inherits from the main model configuration.\n\n  **Format:** `\"provider/model\"` (e.g., `\"openai/gpt-4o-mini\"`, `\"google/gemini-2.0-flash-exp\"`)\n</ParamField>\n\n<ParamField path=\"cua\" type=\"boolean\" optional>\n  <Warning>**Deprecated:** Use `mode: \"cua\"` instead. This option will be removed in a future version.</Warning>\n  \n  Indicates whether Computer Use Agent (CUA) mode is enabled. When false, the agent uses standard tool-based operation instead of computer control.\n</ParamField>\n\n<ParamField path=\"integrations\" type=\"(Client | string)[]\" optional>\n  MCP (Model Context Protocol) integrations for external tools and services.\n\n  **Array of:** MCP server URLs (strings) or connected Client objects\n</ParamField>\n\n<ParamField path=\"tools\" type=\"ToolSet\" optional>\n  Custom tool definitions to extend agent capabilities using the AI SDK ToolSet format.\n</ParamField>\n\n<ParamField path=\"stream\" type=\"boolean\" optional>\n  Enable streaming mode for the agent. When `true`, `execute()` returns `AgentStreamResult` with `textStream` for incremental output. When `false` (default), `execute()` returns `AgentResult` after completion.\n  \n  **Default:** `false`\n  \n  <Warning>**Non-CUA agents only.** Requires `experimental: true`. Not available when `mode: \"cua\"`.</Warning>\n</ParamField>\n\n<ParamField path=\"mode\" type='\"dom\" | \"hybrid\" | \"cua\"' optional>\n  Tool mode for the agent. Determines which set of tools are available to the agent.\n  \n  **Modes:**\n  - `\"dom\"` (default): Uses DOM-based tools (`act`, `fillForm`) for structured page interactions. Works with any model.\n  - `\"hybrid\"`: Uses both DOM-based and coordinate-based tools (`act`, `click`, `type`, `dragAndDrop`, `clickAndHold`, `fillForm`) for visual/screenshot-based interactions. Requires models with reliable coordinate-based action capabilities.\n  - `\"cua\"`: Uses Computer Use Agent (CUA) providers like Anthropic Claude, Google Gemini, or OpenAI for screenshot-based automation. This is the preferred way to enable CUA mode (replaces the deprecated `cua: true` option).\n  \n  **Default:** `\"dom\"`\n  \n  <Warning>\n  **Hybrid Mode Model Requirements:** Only use hybrid mode with models that can reliably perform coordinate-based actions:\n  - **Google:** `google/gemini-3-flash-preview`\n  - **Anthropic:** `anthropic/claude-sonnet-4-20250514`, `anthropic/claude-sonnet-4-5-20250929`, `anthropic/claude-haiku-4-5-20251001`\n  \n  Requires `experimental: true` in Stagehand constructor.\n  </Warning>\n</ParamField>\n\n### Execute Method\n\n<Tabs>\n<Tab title=\"Non-Streaming\">\n\n```typescript\n// String instruction\nawait agent.execute(instruction: string): Promise<AgentResult>\n\n// With options\nawait agent.execute(options: AgentExecuteOptions): Promise<AgentResult>\n```\n\n**AgentExecuteOptions Interface:**\n```typescript\ninterface AgentExecuteOptions {\n  instruction: string;\n  maxSteps?: number;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  highlightCursor?: boolean;\n  messages?: ModelMessage[]; // Continue from previous conversation (experimental)\n  signal?: AbortSignal; // Cancel execution (experimental)\n  excludeTools?: string[]; // Tools to exclude from this execution (experimental)\n  output?: ZodObject; // Zod schema for structured output (experimental)\n  callbacks?: AgentExecuteCallbacks;\n}\n\ninterface AgentExecuteCallbacks {\n  prepareStep?: PrepareStepFunction<ToolSet>;\n  onStepFinish?: GenerateTextOnStepFinishCallback<ToolSet>;\n}\n```\n\n</Tab>\n\n<Tab title=\"Streaming\">\n\n```typescript\n// With stream: true in AgentConfig\nawait agent.execute(options: AgentStreamExecuteOptions): Promise<AgentStreamResult>\n```\n\n**AgentStreamExecuteOptions Interface:**\n```typescript\ninterface AgentStreamExecuteOptions {\n  instruction: string;\n  maxSteps?: number;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  highlightCursor?: boolean;\n  messages?: ModelMessage[]; // Continue from previous conversation (experimental)\n  signal?: AbortSignal; // Cancel execution (experimental)\n  excludeTools?: string[]; // Tools to exclude from this execution (experimental)\n  output?: ZodObject; // Zod schema for structured output (experimental)\n  callbacks?: AgentStreamCallbacks;\n}\n\ninterface AgentStreamCallbacks {\n  prepareStep?: PrepareStepFunction<ToolSet>;\n  onStepFinish?: StreamTextOnStepFinishCallback<ToolSet>;\n  onChunk?: StreamTextOnChunkCallback<ToolSet>;\n  onFinish?: StreamTextOnFinishCallback<ToolSet>;\n  onError?: StreamTextOnErrorCallback;\n  onAbort?: (event: { steps: Array<StepResult<ToolSet>> }) => void | Promise<void>;\n}\n```\n\n</Tab>\n\n</Tabs>\n\n### Execute Parameters\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  High-level task description in natural language.\n</ParamField>\n\n<ParamField path=\"maxSteps\" type=\"number\" optional>\n  Maximum number of actions the agent can take before stopping.\n\n  **Default:** `20`\n</ParamField>\n\n<ParamField path=\"page\" type=\"PlaywrightPage | PuppeteerPage | PatchrightPage | Page\" optional>\n  Optional: Specify which page to perform the agent execution on. Supports multiple browser automation libraries:\n  - **Playwright**: Native Playwright Page objects\n  - **Puppeteer**: Puppeteer Page objects\n  - **Patchright**: Patchright Page objects\n  - **Stagehand Page**: Stagehand's wrapped Page object\n\n  If not specified, defaults to the current \"active\" page in your Stagehand instance.\n</ParamField>\n\n<ParamField path=\"highlightCursor\" type=\"boolean\" optional>\n  Whether to show a visual cursor on the page during agent execution. Useful for debugging and demonstrations.\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"messages\" type=\"ModelMessage[]\" optional>\n  Previous conversation messages to continue from. Pass the `messages` from a previous `AgentResult` to continue that conversation.\n\n  <Warning>**Non-CUA agents only.** Requires `experimental: true`. Not available when `mode: \"cua\"`.</Warning>\n</ParamField>\n\n<ParamField path=\"signal\" type=\"AbortSignal\" optional>\n  An `AbortSignal` that can be used to cancel the agent execution. When aborted, the agent will stop and throw an `AgentAbortError`.\n\n  <Warning>**Non-CUA agents only.** Requires `experimental: true`. Not available when `mode: \"cua\"`.</Warning>\n</ParamField>\n\n<ParamField path=\"excludeTools\" type=\"string[]\" optional>\n  Tools to exclude from this execution. Pass an array of tool names to prevent the agent from using those tools.\n\n  **Available tools by mode:**\n\n  **DOM mode (default):** `act`, `fillForm`, `ariaTree`, `extract`, `goto`, `scroll`, `keys`, `navback`, `screenshot`, `think`, `wait`, `search`\n\n  **Hybrid mode:** `click`, `type`, `dragAndDrop`, `clickAndHold`, `fillFormVision`, `act`, `ariaTree`, `extract`, `goto`, `scroll`, `keys`, `navback`, `screenshot`, `think`, `wait`, `search`\n\n  <Warning>**Non-CUA agents only.** Requires `experimental: true`. Not available when `cua: true`.</Warning>\n</ParamField>\n\n<ParamField path=\"output\" type=\"ZodObject\" optional>\n  A Zod schema defining structured output data to return when the task completes. The agent will populate this data based on the information it gathered during execution. The result will be available in `AgentResult.output`.\n\n  <Warning>**Non-CUA agents only.** Requires `experimental: true`. Not available when `mode: \"cua\"`.</Warning>\n\n  ```typescript\n  import { z } from \"zod\";\n\n  const result = await agent.execute({\n    instruction: \"Find the cheapest flight from NYC to LA\",\n    output: z.object({\n      price: z.string().describe(\"The price of the flight\"),\n      airline: z.string().describe(\"The airline name\"),\n      departureTime: z.string().describe(\"Departure time\"),\n    }),\n  });\n\n  console.log(result.output); // { price: \"$199\", airline: \"Delta\", departureTime: \"8:00 AM\" }\n  ```\n</ParamField>\n\n<ParamField path=\"callbacks\" type=\"AgentExecuteCallbacks | AgentStreamCallbacks\" optional>\n  Callbacks to hook into the agent's execution lifecycle. The available callbacks depend on whether streaming is enabled.\n\n  <Warning>**Non-CUA agents only.** Requires `experimental: true`. Not available when `mode: \"cua\"`.</Warning>\n\n  <Expandable title=\"Non-Streaming Callbacks (AgentExecuteCallbacks)\">\n    <ParamField path=\"prepareStep\" type=\"PrepareStepFunction<ToolSet>\" optional>\n      Called before each step to modify settings. You can change the model, tool choices, active tools, system prompt, and input messages for each step.\n    </ParamField>\n    <ParamField path=\"onStepFinish\" type=\"GenerateTextOnStepFinishCallback<ToolSet>\" optional>\n      Called when each step (LLM call) completes. Provides access to tool calls, reasoning, and step results.\n    </ParamField>\n  </Expandable>\n\n  <Expandable title=\"Streaming Callbacks (AgentStreamCallbacks)\">\n    <ParamField path=\"prepareStep\" type=\"PrepareStepFunction<ToolSet>\" optional>\n      Called before each step to modify settings.\n    </ParamField>\n    <ParamField path=\"onStepFinish\" type=\"StreamTextOnStepFinishCallback<ToolSet>\" optional>\n      Called when each step completes during streaming.\n    </ParamField>\n    <ParamField path=\"onChunk\" type=\"StreamTextOnChunkCallback<ToolSet>\" optional>\n      Called for each chunk of the stream. Stream processing will pause until the callback promise resolves.\n    </ParamField>\n    <ParamField path=\"onFinish\" type=\"StreamTextOnFinishCallback<ToolSet>\" optional>\n      Called when the stream finishes successfully.\n    </ParamField>\n    <ParamField path=\"onError\" type=\"StreamTextOnErrorCallback\" optional>\n      Called when an error occurs during streaming.\n    </ParamField>\n    <ParamField path=\"onAbort\" type=\"(event: { steps: StepResult[] }) => void | Promise<void>\" optional>\n      Called when the stream is aborted via the `signal` option.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n### Response\n\n**Returns:** `Promise<AgentResult>` (non-streaming) or `Promise<AgentStreamResult>` (streaming)\n\n<Tabs>\n<Tab title=\"Non-Streaming\">\n\n**AgentResult Interface:**\n```typescript\ninterface AgentResult {\n  success: boolean;\n  message: string;\n  actions: AgentAction[];\n  completed: boolean;\n  metadata?: Record<string, unknown>;\n  messages?: ModelMessage[]; // Conversation history for continuation (experimental)\n  output?: Record<string, unknown>; // Structured output data (experimental)\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n    reasoning_tokens?: number;\n    cached_input_tokens?: number;\n    inference_time_ms: number;\n  };\n}\n\n// AgentAction can contain various tool-specific fields\ninterface AgentAction {\n  type: string;\n  reasoning?: string;\n  taskCompleted?: boolean;\n  action?: string;\n  timeMs?: number;        // wait tool\n  pageText?: string;      // ariaTree tool\n  pageUrl?: string;       // ariaTree tool\n  instruction?: string;   // various tools\n  timestamp?: number;     // Action timestamp\n  [key: string]: unknown; // Additional tool-specific fields\n}\n```\n\n</Tab>\n\n<Tab title=\"Streaming\">\n\n**AgentStreamResult Interface:**\n```typescript\ninterface AgentStreamResult {\n  // Async iterable of text chunks for incremental output\n  textStream: AsyncIterable<string>;\n  \n  // Async iterable of all stream events (tool calls, messages, etc.)\n  fullStream: AsyncIterable<StreamPart>;\n  \n  // Promise that resolves to the final AgentResult when streaming completes\n  result: Promise<AgentResult>;\n  \n  // Additional properties from StreamTextResult<ToolSet, never>\n  // See Vercel AI SDK documentation for full details\n}\n```\n\n</Tab>\n</Tabs>\n\n<ResponseField name=\"success\" type=\"boolean\">\n  Whether the task was completed successfully.\n</ResponseField>\n\n<ResponseField name=\"message\" type=\"string\">\n  Description of the execution result and status.\n</ResponseField>\n\n<ResponseField name=\"actions\" type=\"AgentAction[]\">\n  Array of individual actions taken during execution. Each action contains tool-specific data.\n</ResponseField>\n\n<ResponseField name=\"completed\" type=\"boolean\">\n  Whether the agent believes the task is fully complete.\n</ResponseField>\n\n<ResponseField name=\"metadata\" type=\"Record<string, unknown>\" optional>\n  Additional execution metadata and debugging information.\n</ResponseField>\n\n<ResponseField name=\"messages\" type=\"ModelMessage[]\" optional>\n  The conversation messages from this execution. Pass these to a subsequent `execute()` call via the `messages` option to continue the conversation.\n\n  <Note>**Non-CUA agents only.** Requires `experimental: true`.</Note>\n</ResponseField>\n\n<ResponseField name=\"output\" type=\"Record<string, unknown>\" optional>\n  Custom structured output data extracted based on the `output` Zod schema provided in execute options. Only populated if an `output` schema was provided.\n\n  <Note>**Non-CUA agents only.** Requires `experimental: true`.</Note>\n</ResponseField>\n\n<ResponseField name=\"usage\" type=\"object\" optional>\n  Token usage and performance metrics.\n\n  <Expandable title=\"Usage Metrics\">\n    <ResponseField name=\"input_tokens\" type=\"number\">\n      Number of input tokens used\n    </ResponseField>\n    <ResponseField name=\"output_tokens\" type=\"number\">\n      Number of output tokens generated\n    </ResponseField>\n    <ResponseField name=\"reasoning_tokens\" type=\"number\" optional>\n      Number of reasoning tokens (if supported by the model)\n    </ResponseField>\n    <ResponseField name=\"cached_input_tokens\" type=\"number\" optional>\n      Number of cached input tokens (if supported by the model)\n    </ResponseField>\n    <ResponseField name=\"inference_time_ms\" type=\"number\">\n      Total inference time in milliseconds\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n\n### Example Response\n```json\n{\n  \"success\": true,\n  \"message\": \"Task completed successfully\",\n  \"actions\": [\n    {\n      \"type\": \"act\",\n      \"instruction\": \"click the submit button\",\n      \"reasoning\": \"User requested to submit the form\",\n      \"taskCompleted\": false\n    },\n    {\n      \"type\": \"observe\",\n      \"instruction\": \"check if submission was successful\",\n      \"taskCompleted\": true\n    }\n  ],\n  \"completed\": true,\n  \"metadata\": {\n    \"steps_taken\": 2\n  },\n  \"output\": {\n    \"price\": \"$199\",\n    \"airline\": \"Delta\",\n    \"departureTime\": \"8:00 AM\"\n  },\n  \"usage\": {\n    \"input_tokens\": 1250,\n    \"output_tokens\": 340,\n    \"reasoning_tokens\": 42,\n    \"cached_input_tokens\": 0,\n    \"inference_time_ms\": 2500\n  }\n}\n```\n\n### Code Examples\n\n<Tabs>\n<Tab title=\"Basic Usage\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  model: \"anthropic/claude-sonnet-4-20250514\"\n});\nawait stagehand.init();\n\nconst page = stagehand.context.pages()[0];\n// Create agent with default configuration\nconst agent = stagehand.agent();\n\n// Navigate to a page\nawait page.goto(\"https://www.google.com\");\n\n// Execute a task\nconst result = await agent.execute(\"Search for 'Stagehand automation' and click the first result\");\n\nconsole.log(result.message);\nconsole.log(`Completed: ${result.completed}`);\nconsole.log(`Actions taken: ${result.actions.length}`);\n```\n\n</Tab>\n<Tab title=\"Custom Configuration\">\n\n```typescript\n// Create agent with custom model and system prompt\nconst agent = stagehand.agent({\n  model: \"openai/computer-use-preview\",\n  systemPrompt: \"You are a helpful assistant that can navigate websites efficiently. Always verify actions before proceeding.\",\n  executionModel: \"openai/gpt-4o-mini\"  // Use faster model for tool execution\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\nconst result = await agent.execute({\n  instruction: \"Fill out the contact form with test data\",\n  maxSteps: 10,\n  highlightCursor: true\n});\n```\n\n</Tab>\n<Tab title=\"Advanced Model Config\">\n\n```typescript\n// Using AgentModelConfig for advanced configuration\nconst agent = stagehand.agent({\n  model: {\n    modelName: \"anthropic/claude-sonnet-4-20250514\",\n    apiKey: process.env.ANTHROPIC_API_KEY,\n    baseURL: \"https://custom-proxy.com/v1\"\n  }\n});\n\nconst result = await agent.execute(\"Complete the checkout process\");\n```\n\n</Tab>\n<Tab title=\"Multi-Page\">\n\n```typescript\nconst page1 = stagehand.context.pages()[0];\nconst page2 = await stagehand.context.newPage();\n\nconst agent = stagehand.agent();\n\n// Execute on specific page\nawait page2.goto(\"https://example.com/dashboard\");\nconst result = await agent.execute({\n  instruction: \"Export the data table\",\n  page: page2\n});\n```\n\n</Tab>\n<Tab title=\"Streaming\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for streaming\n});\nawait stagehand.init();\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://amazon.com\");\n\n// Create a streaming agent\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n  stream: true,\n});\n\nconst streamResult = await agent.execute({\n  instruction: \"Search for headphones and find the best deal\",\n  maxSteps: 20,\n});\n\n// Stream text output incrementally\nfor await (const delta of streamResult.textStream) {\n  process.stdout.write(delta);\n}\n\n// Get the final result\nconst finalResult = await streamResult.result;\nconsole.log(\"Completed:\", finalResult.completed);\n```\n\n</Tab>\n<Tab title=\"Callbacks\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true,\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\nconst result = await agent.execute({\n  instruction: \"Fill out the contact form\",\n  maxSteps: 10,\n  callbacks: {\n    prepareStep: async (stepContext) => {\n      console.log(`Starting step ${stepContext.stepNumber}`);\n      return stepContext;\n    },\n    onStepFinish: async (event) => {\n      console.log(`Step finished: ${event.finishReason}`);\n      if (event.toolCalls) {\n        for (const tc of event.toolCalls) {\n          console.log(`Tool: ${tc.toolName}`, tc.input);\n        }\n      }\n    },\n  },\n});\n```\n\n</Tab>\n<Tab title=\"Abort Signal\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true,\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\nconst controller = new AbortController();\n\n// Abort after 30 seconds\nsetTimeout(() => controller.abort(\"Timeout exceeded\"), 30000);\n\ntry {\n  const result = await agent.execute({\n    instruction: \"Complete a complex multi-step workflow\",\n    maxSteps: 50,\n    signal: controller.signal,\n  });\n} catch (error) {\n  if (error.name === \"AgentAbortError\") {\n    console.log(\"Task cancelled:\", error.message);\n  }\n}\n```\n\n</Tab>\n<Tab title=\"Message Continuation\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true,\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com/shop\");\n\n// First execution\nconst firstResult = await agent.execute({\n  instruction: \"Search for laptops and list the top 3 options\",\n  maxSteps: 10,\n});\n\n// Continue conversation with context from first run\nconst secondResult = await agent.execute({\n  instruction: \"Filter those results by price under $1000\",\n  maxSteps: 10,\n  messages: firstResult.messages, // Pass previous messages\n});\n\n// Chain further with accumulated context\nconst thirdResult = await agent.execute({\n  instruction: \"Add the best-rated one to cart\",\n  maxSteps: 10,\n  messages: secondResult.messages,\n});\n\nconsole.log(\"Final:\", thirdResult.message);\n```\n\n</Tab>\n<Tab title=\"MCP Integrations\">\n\n```typescript\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\n// Create agent with MCP integrations\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-20250514\",\n  integrations: [\n    \"https://mcp-server.example.com\",  // MCP server URL\n    mcpClientInstance  // Or pre-connected Client object\n  ]\n});\n\nconst result = await agent.execute(\"Use the external tool to process this data\");\n```\n\n</Tab>\n<Tab title=\"Exclude Tools\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for excludeTools\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\n// Exclude specific tools from this execution\nconst result = await agent.execute({\n  instruction: \"Navigate the page and click buttons\",\n  maxSteps: 15,\n  excludeTools: [\"screenshot\", \"extract\", \"search\"],\n});\n\nconsole.log(\"Completed:\", result.completed);\n```\n\n</Tab>\n<Tab title=\"Hybrid Mode\">\n\n```typescript\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for hybrid mode\n});\nawait stagehand.init();\n\n// Create agent with hybrid mode for coordinate-based interactions\nconst agent = stagehand.agent({\n  mode: \"hybrid\",\n  model: \"google/gemini-3-flash-preview\", // Use a model that supports coordinate-based actions\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com/form\");\n\nconst result = await agent.execute({\n  instruction: \"Fill out the registration form with test data\",\n  maxSteps: 15,\n  highlightCursor: true, // Enabled by default in hybrid mode\n});\n\nconsole.log(\"Completed:\", result.completed);\n```\n\n</Tab>\n<Tab title=\"Structured Output\">\n\n```typescript\nimport { z } from \"zod\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  experimental: true, // Required for structured output\n});\nawait stagehand.init();\n\nconst agent = stagehand.agent({\n  model: \"anthropic/claude-sonnet-4-5-20250929\",\n});\n\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://www.google.com/flights\");\n\n// Define output schema to receive structured data\nconst result = await agent.execute({\n  instruction: \"Find the cheapest flight from NYC to LA for next week\",\n  maxSteps: 20,\n  output: z.object({\n    price: z.string().describe(\"The price of the flight\"),\n    airline: z.string().describe(\"The airline name\"),\n    departureTime: z.string().describe(\"Departure time\"),\n    arrivalTime: z.string().describe(\"Arrival time\"),\n    flightNumber: z.string().optional().describe(\"Flight number if available\"),\n  }),\n});\n\n// Access the structured output\nconsole.log(\"Flight found:\");\nconsole.log(`  Price: ${result.output?.price}`);\nconsole.log(`  Airline: ${result.output?.airline}`);\nconsole.log(`  Departure: ${result.output?.departureTime}`);\nconsole.log(`  Arrival: ${result.output?.arrivalTime}`);\n```\n\n</Tab>\n<Tab title=\"Custom Tools\">\n\n```typescript\nimport { tool } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\n// Define custom tools using the tool helper from @browserbasehq/stagehand\nconst customTools = {\n  calculateTotal: tool({\n    description: \"Calculate the total of items in cart\",\n    parameters: z.object({\n      items: z.array(z.object({\n        price: z.number(),\n        quantity: z.number()\n      }))\n    }),\n    execute: async ({ items }) => {\n      const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);\n      return { total };\n    }\n  })\n};\n\nconst agent = stagehand.agent({\n  model: \"openai/computer-use-preview\",\n  tools: customTools\n});\n\nconst result = await agent.execute(\"Calculate the total cost of items in the shopping cart\");\n```\n\n</Tab>\n</Tabs>\n\n### Error Types\n\nThe following errors may be thrown by the `agent()` method:\n\n- **StagehandError** - Base class for all Stagehand-specific errors\n- **StagehandInitError** - Agent was not properly initialized\n- **MissingLLMConfigurationError** - No LLM API key or client configured\n- **UnsupportedModelError** - The specified model is not supported for agent functionality\n- **UnsupportedModelProviderError** - The specified model provider is not supported\n- **InvalidAISDKModelFormatError** - Model string does not follow the required `provider/model` format\n- **MCPConnectionError** - Failed to connect to MCP server\n- **StagehandDefaultError** - General execution error with detailed message\n- **AgentAbortError** - Thrown when agent execution is cancelled via an `AbortSignal`\n- **StreamingCallbacksInNonStreamingModeError** - Thrown when streaming-only callbacks (`onChunk`, `onFinish`, `onError`, `onAbort`) are used without `stream: true`\n- **ExperimentalNotConfiguredError** - Thrown when experimental features (callbacks, signal, messages, streaming) are used without `experimental: true` in Stagehand constructor"
  },
  {
    "path": "packages/docs/v3/references/context.mdx",
    "content": "---\ntitle: context\ndescription: 'Complete API reference for the browser context'\nicon: 'window'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Stagehand\" icon=\"wand-magic-sparkles\" href=\"/v3/references/stagehand\">\n  Learn about the main Stagehand object\n</Card>\n</CardGroup>\n\n## Overview\n\nThe `context` object manages the browser context, which is a container for multiple pages (tabs). It provides methods for creating new pages, accessing existing pages, and managing which page is currently active.\n\nAccess the context through your Stagehand instance:\n\n```typescript\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst context = stagehand.context;\n```\n\n## Methods\n\n### newPage()\n\nCreate a new page (tab) in the browser.\n\n```typescript\nawait context.newPage(url?: string): Promise<Page>\n```\n\n<ParamField path=\"url\" type=\"string\" optional>\n  The URL to navigate to in the new page.\n\n  **Default:** `\"about:blank\"`\n</ParamField>\n\n**Returns:** `Promise<Page>` - The newly created page object.\n\nThe new page is automatically set as the active page.\n\n### pages()\n\nGet all open pages in the browser context.\n\n```typescript\ncontext.pages(): Page[]\n```\n\n**Returns:** `Page[]` - Array of all open pages, ordered from oldest to newest.\n\n### activePage()\n\nGet the currently active page.\n\n```typescript\ncontext.activePage(): Page | undefined\n```\n\n**Returns:** `Page | undefined` - The most recently used page, or `undefined` if no pages exist.\n\nThe active page is determined by:\n1. Most recently interacted with page\n2. Most recently created page if no interaction history\n3. `undefined` if all pages have been closed\n\n### setActivePage()\n\nSet a specific page as the active page.\n\n```typescript\ncontext.setActivePage(page: Page): void\n```\n\n<ParamField path=\"page\" type=\"Page\" required>\n  The page to set as active. Must be a page that exists in this context.\n</ParamField>\n\nThis method:\n- Marks the page as most recently used\n- Brings the tab to the foreground (in headed mode)\n- Makes it the default page for subsequent operations\n\n### addInitScript()\n\nInject JavaScript that runs before any page scripts on every navigation.\n\n```typescript\nawait context.addInitScript<Arg>(\n  script: string | { path?: string; content?: string } | ((arg: Arg) => unknown),\n  arg?: Arg,\n): Promise<void>\n```\n\n<ParamField\n  path=\"script\"\n  type=\"string | { path?: string; content?: string } | (arg: Arg) => unknown\"\n  required\n>\n  Provide the script to inject. Pass raw source code, reference a file on disk,\n  or supply a function that Stagehand serializes before sending to the browser.\n</ParamField>\n\n<ParamField path=\"arg\" type=\"Arg\" optional>\n  Extra data that is JSON-serialized and passed to your function. Only supported\n  when `script` is a function.\n</ParamField>\n\nThis method:\n- Runs at document start, and installs the script on all currently open pages and replays it on every\n  navigation of those pages\n- Automatically applies the same script to any pages created after calling\n  `context.addInitScript()`\n- Allows referencing preload files via `{ path: \"./preloads/dom-hooks.js\" }`,\n  mirroring Playwright's `sourceURL` behavior for readable stack traces\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Add some JavaScript to automatically accept alert dialogs\nawait context.addInitScript(() => {\n   window.alert = () => {};\n   window.confirm = () => true;\n   window.prompt = () => '';\n });\n```\n\n### setExtraHTTPHeaders()\n\nSet HTTP headers that will be included in every request made by all pages in the browser context.\n\n```typescript\nawait context.setExtraHTTPHeaders(headers: Record<string, string>): Promise<void>\n```\n\n<ParamField path=\"headers\" type=\"Record<string, string>\" required>\n  A plain object of header name–value pairs. All values must be strings.\n</ParamField>\n\nThis method:\n- Applies the headers to all existing pages in the context immediately\n- Automatically applies the same headers to any pages created after calling `setExtraHTTPHeaders()`\n- Calling it again replaces all previously set extra headers (it does not merge)\n- To clear all extra headers, pass an empty object: `await context.setExtraHTTPHeaders({})`\n\n<Note>\nHeaders set via `context.setExtraHTTPHeaders()` are context-wide. They apply to every network request from every page in the context, including navigation requests, XHR/fetch calls, and subresource loads.\n</Note>\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Set custom headers for all requests\nawait context.setExtraHTTPHeaders({\n  \"X-Custom-Token\": \"my-secret-token\",\n  \"Accept-Language\": \"en-US\",\n});\n\n// All subsequent requests from any page in this context\n// will include these headers\nconst page = await context.newPage(\"https://example.com\");\n```\n\n### cookies()\n\nRetrieve browser cookies, optionally filtered by URL(s).\n\n```typescript\nawait context.cookies(urls?: string | string[]): Promise<Cookie[]>\n```\n\n<ParamField path=\"urls\" type=\"string | string[]\" optional>\n  A single URL or array of URLs to filter cookies by. When provided, only cookies that match the domain, path, and secure requirements of the given URLs are returned.\n\n  **Default:** Returns all cookies when omitted.\n</ParamField>\n\n**Returns:** `Promise<Cookie[]>` - Array of cookie objects.\n\n```typescript\n// Get all cookies\nconst allCookies = await context.cookies();\n\n// Get cookies for a specific URL\nconst siteCookies = await context.cookies(\"https://example.com\");\n\n// Get cookies for multiple URLs\nconst cookies = await context.cookies([\n  \"https://example.com\",\n  \"https://api.example.com\",\n]);\n```\n\n### addCookies()\n\nSet one or more cookies in the browser context.\n\n```typescript\nawait context.addCookies(cookies: CookieParam[]): Promise<void>\n```\n\n<ParamField path=\"cookies\" type=\"CookieParam[]\" required>\n  Array of cookie parameters to set. Each cookie must provide either `url` or both `domain` and `path` — providing both `url` and `domain` (or `url` and `path`) will throw a validation error.\n</ParamField>\n\n<Note>\nCookies set via `context.addCookies()` are shared across all pages in the context, scoped by domain and path.\n</Note>\n\n```typescript\n// Set a cookie using a URL\nawait context.addCookies([\n  {\n    name: \"session\",\n    value: \"abc123\",\n    url: \"https://example.com\",\n  },\n]);\n\n// Set a cookie using domain and path\nawait context.addCookies([\n  {\n    name: \"token\",\n    value: \"xyz789\",\n    domain: \".example.com\",\n    path: \"/\",\n    secure: true,\n    httpOnly: true,\n    sameSite: \"Strict\",\n  },\n]);\n```\n\n<Warning>\nSetting `sameSite: \"None\"` requires `secure: true`. Stagehand will throw a validation error if this requirement is not met.\n</Warning>\n\n### clearCookies()\n\nClear cookies from the browser context. Can clear all cookies or selectively filter by name, domain, or path.\n\n```typescript\nawait context.clearCookies(options?: ClearCookieOptions): Promise<void>\n```\n\n<ParamField path=\"options\" type=\"ClearCookieOptions\" optional>\n  Filter options to selectively clear cookies. When omitted, all cookies are cleared.\n\n  <Expandable title=\"ClearCookieOptions\">\n    <ParamField path=\"name\" type=\"string | RegExp\" optional>\n      Match cookies by name. Supports exact string match or RegExp.\n    </ParamField>\n\n    <ParamField path=\"domain\" type=\"string | RegExp\" optional>\n      Match cookies by domain. Supports exact string match or RegExp.\n    </ParamField>\n\n    <ParamField path=\"path\" type=\"string | RegExp\" optional>\n      Match cookies by path. Supports exact string match or RegExp.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n```typescript\n// Clear all cookies\nawait context.clearCookies();\n\n// Clear cookies by exact name\nawait context.clearCookies({ name: \"session\" });\n\n// Clear cookies by domain pattern\nawait context.clearCookies({ domain: /\\.example\\.com$/ });\n\n// Combine filters (a cookie must match ALL provided filters to be cleared)\nawait context.clearCookies({\n  name: \"token\",\n  domain: \".example.com\",\n});\n```\n\n### close()\n\nClose the browser context and all associated pages.\n\n```typescript\nawait context.close(): Promise<void>\n```\n\nThis method:\n- Closes the CDP connection\n- Cleans up all pages\n- Clears all internal mappings\n\n**Note:** This is typically called internally by `stagehand.close()`. You usually don't need to call this directly.\n\n## Code Examples\n\n<Tabs>\n<Tab title=\"Basic Usage\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Create a new page\nconst page1 = await context.newPage(\"https://example.com\");\nconsole.log(\"Created page 1\");\n\n// Create another page\nconst page2 = await context.newPage(\"https://another-site.com\");\nconsole.log(\"Created page 2\");\n\n// Get all pages\nconst allPages = context.pages();\nconsole.log(`Total pages: ${allPages.length}`);\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Multi-Page Workflow\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Start with main page\nconst mainPage = context.pages()[0];\nawait mainPage.goto(\"https://example.com\");\n\n// Open additional pages\nconst dashboardPage = await context.newPage(\"https://example.com/dashboard\");\nconst settingsPage = await context.newPage(\"https://example.com/settings\");\n\n// Work with specific page\ncontext.setActivePage(dashboardPage);\nawait stagehand.act(\"click the export button\");\n\n// Switch to another page\ncontext.setActivePage(settingsPage);\nawait stagehand.act(\"enable notifications\");\n\n// Back to main page\ncontext.setActivePage(mainPage);\nawait stagehand.act(\"click the logout button\");\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Page Management\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Create multiple pages\nconst pages = await Promise.all([\n  context.newPage(\"https://site1.com\"),\n  context.newPage(\"https://site2.com\"),\n  context.newPage(\"https://site3.com\"),\n]);\n\nconsole.log(`Opened ${pages.length} pages`);\n\n// Get the active page\nconst active = context.activePage();\nconsole.log(`Active page URL: ${active?.url()}`);\n\n// Iterate through all pages\nfor (const page of context.pages()) {\n  console.log(`Page URL: ${page.url()}`);\n  console.log(`Page title: ${await page.title()}`);\n}\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Parallel Operations\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from \"zod\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Create pages for different sites\nconst page1 = await context.newPage(\"https://site1.com\");\nconst page2 = await context.newPage(\"https://site2.com\");\nconst page3 = await context.newPage(\"https://site3.com\");\n\nconst schema = z.object({\n  title: z.string(),\n  description: z.string()\n});\n\n// Extract data from all pages in parallel\nconst results = await Promise.all([\n  stagehand.extract(\"get page info\", schema, { page: page1 }),\n  stagehand.extract(\"get page info\", schema, { page: page2 }),\n  stagehand.extract(\"get page info\", schema, { page: page3 })\n]);\n\nconsole.log(\"Extracted data:\", results);\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Active Page Tracking\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Create pages\nconst homePage = await context.newPage(\"https://example.com\");\nconst aboutPage = await context.newPage(\"https://example.com/about\");\nconst contactPage = await context.newPage(\"https://example.com/contact\");\n\n// The last created page (contactPage) is now active\nconsole.log(\"Active:\", context.activePage()?.url());\n// Output: \"https://example.com/contact\"\n\n// Switch to home page\ncontext.setActivePage(homePage);\nconsole.log(\"Active:\", context.activePage()?.url());\n// Output: \"https://example.com\"\n\n// Now act on the active page (homePage)\nawait stagehand.act(\"click the header link\");\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Custom HTTP Headers\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\n\n// Set authorization headers for all requests\nawait context.setExtraHTTPHeaders({\n  Authorization: \"Bearer my-api-token\",\n});\n\n// Navigate — the headers are sent with every request\nconst page = context.pages()[0];\nawait page.goto(\"https://api.example.com/dashboard\");\n\n// Headers also apply to new pages\nconst page2 = await context.newPage(\"https://api.example.com/settings\");\n\n// Replace headers (previous headers are removed)\nawait context.setExtraHTTPHeaders({\n  Authorization: \"Bearer refreshed-token\",\n  \"X-Request-Id\": \"abc-123\",\n});\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Cookie Management\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\nconst page = context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\n// Set authentication cookies\nawait context.addCookies([\n  {\n    name: \"session_id\",\n    value: \"abc123\",\n    domain: \".example.com\",\n    path: \"/\",\n    httpOnly: true,\n    secure: true,\n    sameSite: \"Lax\",\n  },\n]);\n\n// Read cookies back\nconst cookies = await context.cookies(\"https://example.com\");\nconsole.log(\"Cookies:\", cookies);\n\n// Clear specific cookies\nawait context.clearCookies({ name: \"session_id\" });\n\n// Clear all cookies\nawait context.clearCookies();\n\nawait stagehand.close();\n```\n\n</Tab>\n</Tabs>\n\n## Working with Active Pages\n\nThe context tracks which page is currently active:\n\n```typescript\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\n\n// Get the current active page\nconst activePage = stagehand.context.activePage();\n\n// Create a new page - it becomes active\nconst newPage = await stagehand.context.newPage();\n\n// Now context.activePage() returns newPage\nawait newPage.goto(\"https://example.com\");\n```\n\n## Relationship Between Context and Page\n\n- **Context** manages the browser-level state and multiple pages\n- **Page** represents a single tab/window with content\n- Creating a new page via `context.newPage()` automatically sets it as active\n- You can explicitly control the active page with `context.setActivePage()`\n- Use `context.activePage()` to get the currently active page\n\n```typescript\n// Get the active page\nconst activePage = stagehand.context.activePage();\n\n// Or get the first page directly\nconst firstPage = stagehand.context.pages()[0];\n```\n\n## Best Practices\n\n1. **Create pages explicitly** - Use `context.newPage()` instead of relying on popups or window.open\n2. **Track page references** - Store page objects in variables for easier management\n3. **Set active page before operations** - Ensure the correct page is active before calling Stagehand methods\n4. **Clean up properly** - Call `stagehand.close()` to close all pages and the context\n5. **Handle page order** - Remember that `context.pages()` returns pages in creation order\n6. **Use parallel operations** - Work with multiple pages simultaneously for better performance\n\n## Common Patterns\n\n### Tab Management\n\n```typescript\n// Keep track of pages by purpose\nconst pages = {\n  home: await context.newPage(\"https://example.com\"),\n  dashboard: await context.newPage(\"https://example.com/dashboard\"),\n  settings: await context.newPage(\"https://example.com/settings\")\n};\n\n// Switch between tabs\ncontext.setActivePage(pages.dashboard);\nawait stagehand.act(\"view report\");\n\ncontext.setActivePage(pages.settings);\nawait stagehand.act(\"update preferences\");\n```\n\n### Bulk Data Collection\n\n```typescript\nconst urls = [\n  \"https://site1.com\",\n  \"https://site2.com\",\n  \"https://site3.com\"\n];\n\n// Open all pages\nconst pages = await Promise.all(\n  urls.map(url => context.newPage(url))\n);\n\n// Extract data from each\nconst data = await Promise.all(\n  pages.map(page => stagehand.extract(\"get data\", schema, { page }))\n);\n```\n\n### Conditional Page Management\n\n```typescript\n// Only create a page if needed\nif (needsDashboard) {\n  const dashboard = await context.newPage(\"https://example.com/dashboard\");\n  context.setActivePage(dashboard);\n  await stagehand.act(\"generate report\");\n}\n\n// Check if we have multiple pages\nif (context.pages().length > 1) {\n  console.log(\"Multiple tabs open\");\n}\n```\n\n## Error Handling\n\nContext methods may throw the following errors:\n\n- **Timeout errors** - `newPage()` timeout waiting for page to attach\n- **CDP errors** - Connection errors with Chrome DevTools Protocol\n- **Invalid page errors** - Attempting to set an active page that doesn't exist in the context\n- **StagehandSetExtraHTTPHeadersError** - `setExtraHTTPHeaders()` failed to apply headers to one or more sessions. The error includes a `failures` array with per-session details\n\nAlways handle errors appropriately:\n\n```typescript\ntry {\n  const page = await context.newPage(\"https://example.com\");\n} catch (error) {\n  console.error(\"Failed to create page:\", error.message);\n}\n```\n\n## Type Definitions\n\n```typescript\ninterface V3Context {\n  newPage(url?: string): Promise<Page>;\n  pages(): Page[];\n  activePage(): Page | undefined;\n  setActivePage(page: Page): void;\n  setExtraHTTPHeaders(headers: Record<string, string>): Promise<void>;\n  cookies(urls?: string | string[]): Promise<Cookie[]>;\n  addCookies(cookies: CookieParam[]): Promise<void>;\n  clearCookies(options?: ClearCookieOptions): Promise<void>;\n  close(): Promise<void>;\n}\n\ninterface Cookie {\n  name: string;\n  value: string;\n  domain: string;\n  path: string;\n  /** Unix time in seconds. -1 means session cookie. */\n  expires: number;\n  httpOnly: boolean;\n  secure: boolean;\n  sameSite: \"Strict\" | \"Lax\" | \"None\";\n}\n\ninterface CookieParam {\n  name: string;\n  value: string;\n  /** If provided, domain/path/secure are derived from this URL. */\n  url?: string;\n  domain?: string;\n  path?: string;\n  /** Unix timestamp in seconds. -1 or omitted = session cookie. */\n  expires?: number;\n  httpOnly?: boolean;\n  secure?: boolean;\n  sameSite?: \"Strict\" | \"Lax\" | \"None\";\n}\n\ninterface ClearCookieOptions {\n  name?: string | RegExp;\n  domain?: string | RegExp;\n  path?: string | RegExp;\n}\n```\n"
  },
  {
    "path": "packages/docs/v3/references/deeplocator.mdx",
    "content": "---\ntitle: deepLocator\ndescription: 'Complete API reference for the deepLocator method'\nicon: 'layer-group'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Locator\" icon=\"crosshairs\" href=\"/v3/references/locator\">\n  Learn about the standard Locator class\n</Card>\n</CardGroup>\n\n## Overview\n\nThe `deepLocator()` method creates a special locator that can traverse iframe boundaries and shadow DOM using a simplified syntax. It automatically resolves the correct frame for each operation, making cross-frame interactions seamless.\n\nAccess via the page object:\n\n```typescript\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// Deep locator with iframe traversal\nconst button = page.deepLocator(\"iframe#myframe >> button.submit\");\nawait button.click();\n```\n\n## Syntax\n\n### page.deepLocator()\n\nCreate a deep locator that can cross iframe and shadow DOM boundaries.\n\n```typescript\npage.deepLocator(selector: string): DeepLocatorDelegate\n```\n\n<ParamField path=\"selector\" type=\"string\" required>\n  Selector string with optional iframe hop notation (`>>`).\n\n  Supports:\n  - **CSS selectors** - Standard CSS syntax\n  - **XPath** - Prefix with `xpath=` or start with `/`\n  - **Hop notation** - Use `>>` to traverse into iframes\n  - **Deep XPath** - Automatically handles iframe steps in XPath\n</ParamField>\n\n**Returns:** `DeepLocatorDelegate` - A locator-like object that resolves frames on each action.\n\n## Hop Notation\n\nThe `>>` operator allows you to traverse into iframes in a readable way:\n\n```typescript\n// Syntax: parent-selector >> child-selector >> target-selector\npage.deepLocator(\"iframe#outer >> iframe.inner >> button\")\n```\n\nEach segment before `>>` represents an iframe to traverse into. The final segment is the target element.\n\n### Examples\n\n```typescript\n// Single iframe hop\npage.deepLocator(\"iframe#payment >> input#card-number\")\n\n// Multiple iframe hops\npage.deepLocator(\"iframe#level1 >> iframe#level2 >> div.content\")\n\n// XPath with hops\npage.deepLocator(\"//iframe[@id='myframe'] >> //button[@class='submit']\")\n\n// CSS with XPath target\npage.deepLocator(\"iframe.widget >> xpath=//div[@data-id='123']\")\n```\n\n## Deep XPath\n\nWhen using XPath, `deepLocator` automatically recognizes `iframe` steps and traverses into them:\n\n```typescript\n// Automatically traverses into iframes\npage.deepLocator(\"//iframe//button\")\npage.deepLocator(\"//iframe[@id='myframe']//input[@name='email']\")\npage.deepLocator(\"//iframe[1]//iframe[2]//div[@class='target']\")\n```\n\nThe locator intelligently parses the XPath, identifies iframe boundaries, and resolves the correct frame for the final selector.\n\n## Methods\n\n`DeepLocatorDelegate` provides the same API as `Locator`, with automatic frame resolution:\n\n### Interaction Methods\n\nAll interaction methods from [`Locator`](/v3/references/locator) are available:\n\n- **`click(options?)`** - Click the element\n- **`fill(value)`** - Fill an input\n- **`type(text, options?)`** - Type text\n- **`hover()`** - Hover over element\n- **`selectOption(values)`** - Select dropdown options\n- **`scrollTo(percent)`** - Scroll element\n\n### State Methods\n\n- **`isVisible()`** - Check visibility\n- **`isChecked()`** - Check checkbox state\n- **`inputValue()`** - Get input value\n- **`textContent()`** - Get text content\n- **`innerText()`** - Get visible text\n- **`innerHtml()`** - Get HTML content\n\n### Selection Methods\n\n- **`count()`** - Count matching elements\n- **`nth(index)`** - Select by index\n- **`first()`** - Get first element\n\n### Utility Methods\n\n- **`highlight(options?)`** - Highlight element\n- **`centroid()`** - Get center coordinates\n- **`backendNodeId()`** - Get DOM node ID\n- **`sendClickEvent(options?)`** - Dispatch click event\n\nAll methods work identically to `Locator`, but automatically resolve the correct frame before executing.\n\n## Code Examples\n\n<Tabs>\n<Tab title=\"Basic Iframe Traversal\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\n// Click button inside iframe\nconst button = page.deepLocator(\"iframe#widget >> button.submit\");\nawait button.click();\n\n// Fill input in nested iframe\nconst input = page.deepLocator(\"iframe#outer >> iframe#inner >> input#email\");\nawait input.fill(\"user@example.com\");\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Multiple Iframes\">\n\n```typescript\n// Three-level iframe nesting\nawait page.deepLocator(\n  \"iframe#level1 >> iframe#level2 >> iframe#level3 >> div.target\"\n).click();\n\n// Different selectors at each level\nawait page.deepLocator(\n  \"iframe.container >> #payment-frame >> input[name=cardNumber]\"\n).fill(\"4111111111111111\");\n\n// Mixed CSS and XPath\nawait page.deepLocator(\n  \"iframe.widget >> xpath=//button[contains(text(), 'Submit')]\"\n).click();\n```\n\n</Tab>\n<Tab title=\"Deep XPath\">\n\n```typescript\n// Simple iframe traversal with XPath\nconst content = page.deepLocator(\"//iframe//div[@class='content']\");\nconst text = await content.textContent();\n\n// Multiple iframe levels\nconst button = page.deepLocator(\n  \"//iframe[@id='outer']//iframe[@class='inner']//button\"\n);\nawait button.click();\n\n// XPath with predicates\nconst input = page.deepLocator(\n  \"//iframe[1]//form[@id='myform']//input[@type='text'][1]\"\n);\nawait input.fill(\"test value\");\n```\n\n</Tab>\n<Tab title=\"Element Selection\">\n\n```typescript\n// Count elements across iframes\nconst buttons = page.deepLocator(\"iframe#widget >> button\");\nconst count = await buttons.count();\nconsole.log(`Found ${count} buttons in iframe`);\n\n// Select specific element\nconst firstButton = buttons.first();\nawait firstButton.click();\n\nconst thirdButton = buttons.nth(2);\nawait thirdButton.click();\n\n// Get text from all elements\nfor (let i = 0; i < count; i++) {\n  const btn = buttons.nth(i);\n  const text = await btn.innerText();\n  console.log(`Button ${i}:`, text);\n}\n```\n\n</Tab>\n<Tab title=\"Payment Forms\">\n\n```typescript\n// Common use case: payment iframe\nconst paymentFrame = \"iframe#stripe-payment-element\";\n\n// Fill card details\nawait page.deepLocator(`${paymentFrame} >> input[name=\"cardnumber\"]`)\n  .fill(\"4242424242424242\");\n\nawait page.deepLocator(`${paymentFrame} >> input[name=\"exp-date\"]`)\n  .fill(\"12/25\");\n\nawait page.deepLocator(`${paymentFrame} >> input[name=\"cvc\"]`)\n  .fill(\"123\");\n\nawait page.deepLocator(`${paymentFrame} >> input[name=\"postal\"]`)\n  .fill(\"12345\");\n\n// Submit\nawait page.deepLocator(`${paymentFrame} >> button[type=\"submit\"]`)\n  .click();\n```\n\n</Tab>\n<Tab title=\"State Checks\">\n\n```typescript\n// Check visibility across iframe\nconst modal = page.deepLocator(\"iframe#app >> .modal\");\nif (await modal.isVisible()) {\n  console.log(\"Modal is visible in iframe\");\n}\n\n// Get values from iframe inputs\nconst email = page.deepLocator(\"iframe#form >> input#email\");\nconst value = await email.inputValue();\nconsole.log(\"Email value:\", value);\n\n// Check checkbox in iframe\nconst checkbox = page.deepLocator(\"iframe#settings >> input#subscribe\");\nconst checked = await checkbox.isChecked();\nconsole.log(\"Subscribed:\", checked);\n\n// Highlight element in iframe for debugging\nawait page.deepLocator(\"iframe#widget >> .error-message\")\n  .highlight({ durationMs: 2000 });\n```\n\n</Tab>\n</Tabs>\n\n## Comparison with Standard Locator\n\n### Standard Locator (Single Frame)\n\n```typescript\n// Only works in the main frame\nconst button = page.locator(\"button.submit\");\nawait button.click();\n\n// Cannot access elements inside iframes\nconst iframeButton = page.locator(\"iframe >> button\"); // ❌ Won't work\n```\n\n### Deep Locator (Cross-Frame)\n\n```typescript\n// Works across iframe boundaries\nconst button = page.deepLocator(\"iframe#widget >> button.submit\");\nawait button.click(); // ✅ Automatically traverses into iframe\n\n// Can handle nested iframes\nconst nested = page.deepLocator(\"iframe#a >> iframe#b >> button\");\nawait nested.click(); // ✅ Handles multiple levels\n```\n\n## When to Use deepLocator\n\nUse `deepLocator()` when:\n\n1. **Targeting elements inside iframes** - Payment forms, embedded widgets, third-party content\n2. **Working with nested iframes** - Multiple levels of iframe nesting\n3. **XPath crosses iframe boundaries** - When XPath naturally includes iframe steps\n4. **Simpler syntax preferred** - Use `>>` instead of manual frame switching\n\nUse standard `locator()` when:\n\n1. **Elements are in main frame** - No iframe traversal needed\n2. **Performance critical** - Standard locator is slightly faster (no frame resolution)\n3. **Working with frame references** - You already have the frame object\n\n## Best Practices\n\n1. **Use specific selectors** - Make each segment unique to avoid ambiguity\n2. **Keep hop chains short** - Simpler is better for maintainability\n3. **Name your iframes** - Use IDs or classes on iframes for easier targeting\n4. **Test incrementally** - Verify each segment works before adding more\n5. **Cache selectors** - Store complex selectors in variables for reuse\n6. **Use highlight() for debugging** - Verify you're targeting the right element\n\n## Common Patterns\n\n### Named Iframe References\n\n```typescript\n// Define iframe selectors\nconst PAYMENT_FRAME = \"iframe#stripe-payment\";\nconst WIDGET_FRAME = \"iframe.embedded-widget\";\n\n// Use in deep locators\nawait page.deepLocator(`${PAYMENT_FRAME} >> input#card`).fill(\"4242\");\nawait page.deepLocator(`${WIDGET_FRAME} >> button`).click();\n```\n\n### Conditional Iframe Interaction\n\n```typescript\nconst errorInIframe = page.deepLocator(\"iframe#form >> .error-message\");\nif (await errorInIframe.isVisible()) {\n  const errorText = await errorInIframe.textContent();\n  console.error(\"Form error:\", errorText);\n}\n```\n\n### Dynamic Frame Selection\n\n```typescript\n// Select iframe by attribute\nconst frameSelector = `iframe[data-widget-id=\"${widgetId}\"]`;\nconst button = page.deepLocator(`${frameSelector} >> button.action`);\nawait button.click();\n```\n\n## Error Handling\n\nDeep locator operations may throw:\n\n- **Element not found** - Selector doesn't match in the target frame\n- **Frame not found** - Iframe selector doesn't resolve\n- **Timeout errors** - Frame or element resolution timed out\n- **Invalid selector** - Malformed selector syntax\n\nHandle errors appropriately:\n\n```typescript\ntry {\n  await page.deepLocator(\"iframe#widget >> button\").click();\n} catch (error) {\n  console.error(\"Deep locator failed:\", error.message);\n  // Fallback or retry logic\n}\n```\n\n## Advanced Usage\n\n### Combining with Page Methods\n\n```typescript\n// Navigate then use deep locator\nawait page.goto(\"https://example.com\");\nawait page.waitForLoadState(\"networkidle\");\n\nconst iframeButton = page.deepLocator(\"iframe#app >> button\");\nawait iframeButton.click();\n```\n\n### With AI-Powered Methods\n\n```typescript\n// Use observe to find elements in iframes\nconst actions = await stagehand.observe(\"find buttons in the payment iframe\");\n\n// Then use deep locator for precise interaction\nawait page.deepLocator(\"iframe#payment >> button.submit\").click();\n```\n\n## Technical Details\n\n### How It Works\n\n1. **Parse selector** - Splits on `>>` or parses XPath for iframe steps\n2. **Build frame chain** - Creates FrameLocator chain for each iframe segment\n3. **Resolve final frame** - Navigates through frames to find target frame\n4. **Create locator** - Returns a locator in the correct frame context\n5. **Lazy execution** - Frame resolution happens fresh on each action\n\n### Frame Resolution\n\nDeep locators use the internal `FrameLocator` and `resolveLocatorWithHops` logic to:\n\n- Track frame hierarchies\n- Handle OOPIF (out-of-process iframes)\n- Support shadow DOM piercing\n- Maintain frame references during navigation\n\n## Type Definitions\n\n```typescript\ninterface DeepLocatorDelegate {\n  // Actions\n  click(options?: { button?: MouseButton; clickCount?: number }): Promise<void>;\n  fill(value: string): Promise<void>;\n  type(text: string, options?: { delay?: number }): Promise<void>;\n  hover(): Promise<void>;\n  selectOption(values: string | string[]): Promise<string[]>;\n  scrollTo(percent: number | string): Promise<void>;\n\n  // State\n  isVisible(): Promise<boolean>;\n  isChecked(): Promise<boolean>;\n  inputValue(): Promise<string>;\n  textContent(): Promise<string>;\n  innerText(): Promise<string>;\n  innerHtml(): Promise<string>;\n\n  // Selection\n  count(): Promise<number>;\n  nth(index: number): DeepLocatorDelegate;\n  first(): DeepLocatorDelegate;\n\n  // Utilities\n  highlight(options?: HighlightOptions): Promise<void>;\n  centroid(): Promise<{ x: number; y: number }>;\n  backendNodeId(): Promise<BackendNodeId>;\n  sendClickEvent(options?: EventOptions): Promise<void>;\n}\n```\n"
  },
  {
    "path": "packages/docs/v3/references/extract.mdx",
    "content": "---\ntitle: extract()\ndescription: 'Complete API reference for the extract() method'\nicon: 'ufo-beam'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Extract\" icon=\"ufo-beam\" href=\"/v3/basics/extract\">\n  See how to use extract() to extract structured data from web pages\n</Card>\n</CardGroup>\n\n### Method Signatures\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// No parameters (raw page content)\nawait stagehand.extract(): Promise<{ pageText: string }>\n\n// Options only (for example, for targeted extraction)\nawait stagehand.extract(options: ExtractOptions): Promise<{ pageText: string }>\n\n// String instruction only\nawait stagehand.extract(instruction: string): Promise<{ extraction: string }>\n\n// With schema\nawait stagehand.extract<T extends ZodTypeAny>(\n  instruction: string,\n  schema: T,\n  options?: ExtractOptions\n): Promise<z.infer<T>>\n```\n\n**ExtractOptions Interface:**\n```typescript\ninterface ExtractOptions {\n  model?: ModelConfiguration;\n  timeout?: number;\n  selector?: string;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  serverCache?: boolean;\n}\n\n// ModelConfiguration can be either a string or an object\ntype ModelConfiguration =\n  | string  // Format: \"provider/model\" (e.g., \"openai/gpt-5-mini\", \"anthropic/claude-sonnet-4-5\")\n  | {\n      modelName: string;  // The model name\n      apiKey?: string;    // Optional: API key override\n      baseURL?: string;   // Optional: Base URL override\n      // Additional provider-specific options\n    }\n```\n\n</Tab>\n\n</Tabs>\n\n### Parameters\n\n<ParamField path=\"instruction\" type=\"string\" optional>\n  Natural language description of what data to extract. If omitted with no schema, returns raw page text.\n</ParamField>\n\n<ParamField path=\"schema\" type=\"ZodTypeAny\" optional>\n  Zod schema defining the structure of data to extract. Ensures type safety and validation. The return type is automatically inferred from the schema.\n</ParamField>\n\n<ParamField path=\"model\" type=\"ModelConfiguration\" optional>\n  Configure the AI model to use for this action. Can be either:\n  - A string in the format `\"provider/model\"` (e.g., `openai/gpt-5`, `google/gemini-2.5-flash`)\n  - An object with detailed configuration\n\n  <Expandable title=\"Model Configuration Object\">\n    <ParamField path=\"modelName\" type=\"string\" required>\n      The model name (e.g., `anthropic/claude-sonnet-4-5`, `google/gemini-2.5-flash`)\n    </ParamField>\n    <ParamField path=\"apiKey\" type=\"string\" optional>\n      API key for the model provider (overrides default)\n    </ParamField>\n    <ParamField path=\"baseURL\" type=\"string\" optional>\n      Base URL for the API endpoint (for custom endpoints or proxies)\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField path=\"timeout\" type=\"number\" optional>\n  Maximum time in milliseconds to wait for the extraction to complete. Default varies by configuration.\n</ParamField>\n\n<ParamField path=\"selector\" type=\"string\" optional>\n  Optional selector (XPath, CSS selector, etc.) to limit extraction scope to a specific part of the page. Reduces token usage and improves accuracy.\n</ParamField>\n\n<ParamField path=\"page\" type=\"PlaywrightPage | PuppeteerPage | PatchrightPage | Page\" optional>\n  Optional: Specify which page to perform the extraction on. Supports multiple browser automation libraries:\n  - **Playwright**: Native Playwright Page objects\n  - **Puppeteer**: Puppeteer Page objects\n  - **Patchright**: Patchright Page objects\n  - **Stagehand Page**: Stagehand's wrapped Page object\n\n  If not specified, defaults to the current \"active\" page in your Stagehand instance.\n</ParamField>\n\n<ParamField path=\"serverCache\" type=\"boolean\" optional>\n  Override the instance-level `serverCache` setting for this request. When `true`, enables server-side caching. When `false`, disables it.\n\n  <Note>Only applies when `env` is `\"BROWSERBASE\"`. Has no effect in local environments.</Note>\n\n  Defaults to the value set on the Stagehand constructor (which itself defaults to `true`).\n</ParamField>\n\n### Built-in Support\n\n<Note>\n**Iframe and Shadow DOM interactions are supported out of the box.** Stagehand automatically handles iframe traversal and shadow DOM elements without requiring additional configuration or flags.\n</Note>\n\n### Response Types\n\n<Tabs>\n<Tab title=\"With Schema\">\n**Returns:** `Promise<z.infer<T> & { cacheStatus?: \"HIT\" | \"MISS\" }>` where T is your schema\n\nThe returned object will be strictly typed according to your Zod schema definition. The optional `cacheStatus` field indicates whether the result was served from the server-side cache (`\"HIT\"`) or computed fresh (`\"MISS\"`). Only present when running with `env: \"BROWSERBASE\"` and server-side caching is enabled.\n</Tab>\n\n<Tab title=\"String Only\">\n**Returns:** `Promise<{ extraction: string; cacheStatus?: \"HIT\" | \"MISS\" }>`\n\n`extraction`: Simple string extraction without schema validation. The optional `cacheStatus` field indicates cache hit or miss when using Browserbase with server-side caching.\n</Tab>\n\n<Tab title=\"No Parameters\">\n**Returns:** `Promise<{ pageText: string }>`\n\n`pageText`: Raw accessibility tree representation of page content.\n</Tab>\n</Tabs>\n\n### Code Examples\n\n<Tabs>\n<Tab title=\"Single Object\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\nimport { z } from 'zod';\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com/product\");\n\n// Schema definition\nconst ProductSchema = z.object({\n  name: z.string(),\n  price: z.number(),\n  inStock: z.boolean()\n});\n\n// Extraction with v3 API\nconst product = await stagehand.extract(\n  \"extract product details\", \n  ProductSchema\n);\n```\n\n#### Example Response\n```json\n{\n  \"name\": \"Product Name\",\n  \"price\": 100,\n  \"inStock\": true\n}\n```\n\n</Tab>\n<Tab title=\"Arrays\">\n\n```typescript\nimport { z } from 'zod';\n\n// Schema definition\nconst ApartmentListingsSchema = z.array(\n  z.object({\n    address: z.string(),\n    price: z.string(),\n    bedrooms: z.number()\n  })\n);\n\n// Extraction with v3 API\nconst listings = await stagehand.extract(\n  \"extract all apartment listings\",\n  ApartmentListingsSchema\n);\n```\n\n#### Example Response\n```json\n[\n  {\n    \"address\": \"123 Main St\",\n    \"price\": \"$100,000\",\n    \"bedrooms\": 3\n  },\n  {\n    \"address\": \"456 Elm St\",\n    \"price\": \"$150,000\",\n    \"bedrooms\": 2\n  }\n]\n```\n\n</Tab>\n<Tab title=\"URLs\">\n\n```typescript\nimport { z } from 'zod';\n\n// Schema definition\nconst NavigationSchema = z.object({\n  links: z.array(z.object({\n    text: z.string(),\n    url: z.string().url()  // URL validation\n  }))\n});\n\n// Extraction with v3 API\nconst links = await stagehand.extract(\n  \"extract navigation links\",\n  NavigationSchema\n);\n```\n\n#### Example Response\n```json\n{\n  \"links\": [\n    {\n      \"text\": \"Home\",\n      \"url\": \"https://example.com\"\n    }\n  ]\n}\n```\n\n</Tab>\n<Tab title=\"Scoped\">\n\n```typescript\nimport { z } from 'zod';\n\nconst ProductSchema = z.object({\n  name: z.string(),\n  price: z.number(),\n  description: z.string()\n});\n\n// Extract from specific page section with v3 API\nconst data = await stagehand.extract(\n  \"extract product info from this section\",\n  ProductSchema,\n  { selector: \"/html/body/div/div\" }\n);\n```\n\n#### Example Response\n```json\n{\n  \"name\": \"Product Name\",\n  \"price\": 100,\n  \"description\": \"Product description\"\n}\n```\n\n</Tab>\n<Tab title=\"Schema-less\">\n\n```typescript\n// String only extraction\nconst title = await stagehand.extract(\"get the page title\");\n// Returns: { extraction: \"Page Title\" }\n\n// Raw page content\nconst content = await stagehand.extract();\n// Returns: { pageText: \"Accessibility Tree: ...\" }\n```\n\n#### Example Response\n```json\n{\n  \"extraction\": \"Page Title\"\n}\n```\n\n</Tab>\n<Tab title=\"Advanced\">\n\n```typescript\nimport { z } from 'zod';\n\n// Schema with descriptions and validation\nconst ProductSchema = z.object({\n  price: z.number().describe(\"Product price in USD\"),\n  rating: z.number().min(0).max(5).describe(\"Customer rating out of 5\"),\n  available: z.boolean().describe(\"Whether product is in stock\"),\n  tags: z.array(z.string()).optional()\n});\n\n// Nested schema\nconst EcommerceSchema = z.object({\n  product: z.object({\n    name: z.string(),\n    price: z.object({\n      current: z.number(),\n      original: z.number().optional()\n    })\n  }),\n  reviews: z.array(z.object({\n    rating: z.number(),\n    comment: z.string()\n  }))\n});\n```\n\n#### Example Response\n```json\n{\n  \"product\": {\n    \"name\": \"Product Name\",\n    \"price\": {\n      \"current\": 100,\n      \"original\": 120\n    }\n  },\n  \"reviews\": [\n    {\n      \"rating\": 4,\n      \"comment\": \"Great product!\"\n    }\n  ]\n}\n```\n\n</Tab>\n</Tabs>\n\n### Additional Examples\n\n<Tabs>\n<Tab title=\"Custom Model\">\n\n```typescript\nimport { z } from 'zod';\n\nconst DataSchema = z.object({\n  title: z.string(),\n  content: z.string()\n});\n\n// Using string format\nconst data1 = await stagehand.extract(\n  \"extract article data\",\n  DataSchema,\n  { model: \"openai/gpt-5-mini\" }\n);\n\n// Using object format with custom configuration\nconst data2 = await stagehand.extract(\n  \"extract article data\",\n  DataSchema,\n  {\n    model: {\n      modelName: \"claude-sonnet-4-6\",\n      apiKey: process.env.ANTHROPIC_API_KEY\n    }\n  }\n);\n```\n\n</Tab>\n<Tab title=\"Multi-Page\">\n\n```typescript\nimport { z } from 'zod';\n\nconst page1 = stagehand.context.pages()[0];\nconst page2 = await stagehand.context.newPage();\n\nconst Schema = z.object({ title: z.string() });\n\nconst data1 = await stagehand.extract(\"get title\", Schema, { page: page1 });\nconst data2 = await stagehand.extract(\"get title\", Schema, { page: page2 });\n```\n\n</Tab>\n</Tabs>\n\n### Error Types\n\nThe following errors may be thrown by the `extract()` method:\n\n- **StagehandError** - Base class for all Stagehand-specific errors\n- **ZodSchemaValidationError** - Extracted data does not match the provided Zod schema\n- **StagehandDomProcessError** - Error occurred while processing the DOM\n- **StagehandEvalError** - Error occurred while evaluating JavaScript in the page context\n- **StagehandIframeError** - Unable to resolve iframe for the target element\n- **ContentFrameNotFoundError** - Unable to obtain content frame for the selector\n- **XPathResolutionError** - XPath does not resolve in the current page or frames\n- **StagehandShadowRootMissingError** - No shadow root present on the resolved host element\n- **LLMResponseError** - Error in LLM response processing\n- **MissingLLMConfigurationError** - No LLM API key or client configured\n- **UnsupportedModelError** - The specified model is not supported for this operation\n- **InvalidAISDKModelFormatError** - Model string does not follow the required `provider/model` format"
  },
  {
    "path": "packages/docs/v3/references/locator.mdx",
    "content": "---\ntitle: locator\ndescription: 'Complete API reference for the Locator class'\nicon: 'crosshairs'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Page\" icon=\"browser\" href=\"/v3/references/page\">\n  Learn about the Page object that creates locators\n</Card>\n</CardGroup>\n\n## Overview\n\nThe `Locator` class provides precise element interaction capabilities. It resolves CSS or XPath selectors within a frame and performs low-level actions using Chrome DevTools Protocol (CDP).\n\nCreate a locator through the page object:\n\n```typescript\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// Create a locator\nconst button = page.locator(\"button.submit\");\nawait button.click();\n```\n\n## Key Features\n\n- **Lazy resolution** - Selectors are resolved fresh on each action\n- **Isolated execution** - Runs in an isolated world, separate from page scripts\n- **CDP-based** - Uses Chrome DevTools Protocol for reliable interactions\n- **Automatic cleanup** - Releases remote objects automatically\n- **Iframe support** - Works seamlessly with iframes and shadow DOM\n\n## Interaction Methods\n\n### click()\n\nClick the element at its visual center.\n\n```typescript\nawait locator.click(options?: ClickOptions): Promise<void>\n```\n\n<ParamField path=\"button\" type='\"left\" | \"right\" | \"middle\"' optional>\n  Mouse button to use for the click.\n\n  **Default:** `\"left\"`\n</ParamField>\n\n<ParamField path=\"clickCount\" type=\"number\" optional>\n  Number of consecutive clicks (for double-click, triple-click).\n\n  **Default:** `1`\n</ParamField>\n\nThe method:\n1. Scrolls element into view\n2. Gets element geometry\n3. Moves mouse to center\n4. Dispatches mousePressed and mouseReleased events\n\n### fill()\n\nFill an input, textarea, or contenteditable element.\n\n```typescript\nawait locator.fill(value: string): Promise<void>\n```\n\n<ParamField path=\"value\" type=\"string\" required>\n  The text value to fill into the element.\n</ParamField>\n\nThe method intelligently handles different input types:\n- Uses native value setter for special inputs (date, number, etc.)\n- Types text character-by-character for regular inputs\n- Clears existing content before filling\n\n### type()\n\nType text into the element with optional delay between keystrokes.\n\n```typescript\nawait locator.type(text: string, options?: TypeOptions): Promise<void>\n```\n\n<ParamField path=\"text\" type=\"string\" required>\n  The text to type.\n</ParamField>\n\n<ParamField path=\"delay\" type=\"number\" optional>\n  Delay in milliseconds between each keystroke.\n\n  If not specified, uses `Input.insertText` for efficiency.\n</ParamField>\n\n### hover()\n\nMove the mouse cursor to the element's center without clicking.\n\n```typescript\nawait locator.hover(): Promise<void>\n```\n\nScrolls the element into view and dispatches a mouse move event.\n\n### selectOption()\n\nSelect one or more options in a `<select>` element.\n\n```typescript\nawait locator.selectOption(values: string | string[]): Promise<string[]>\n```\n\n<ParamField path=\"values\" type=\"string | string[]\" required>\n  Option value(s) to select. For multi-select elements, pass an array.\n</ParamField>\n\n**Returns:** `Promise<string[]>` - Array of values that were actually selected.\n\n### setInputFiles()\n\nSet files on an `<input type=\"file\">` element.\n\n```typescript\nawait locator.setInputFiles(files: FileInput): Promise<void>\n```\n\n<ParamField path=\"files\" type=\"string | string[] | FilePayload | FilePayload[]\" required>\n  File paths or file payloads to upload.\n\n  **File Path:** Absolute or relative path to a file\n\n  **File Payload:** Object with `{ name, mimeType, buffer }`\n</ParamField>\n\n**FilePayload Interface:**\n```typescript\ninterface FilePayload {\n  name: string;\n  mimeType: string;\n  buffer: ArrayBuffer | Uint8Array | Buffer | string;\n}\n```\n\nPass an empty array to clear the file selection.\n\n## State Methods\n\n### isVisible()\n\nCheck if the element is visible.\n\n```typescript\nawait locator.isVisible(): Promise<boolean>\n```\n\n**Returns:** `Promise<boolean>` - `true` if element is attached and visible.\n\n### isChecked()\n\nCheck if a checkbox or radio button is checked.\n\n```typescript\nawait locator.isChecked(): Promise<boolean>\n```\n\n**Returns:** `Promise<boolean>` - `true` if checked. Also considers `aria-checked` for ARIA widgets.\n\n### inputValue()\n\nGet the current value of an input element.\n\n```typescript\nawait locator.inputValue(): Promise<string>\n```\n\n**Returns:** `Promise<string>` - The element's input value.\n\nWorks with: `<input>`, `<textarea>`, `<select>`, contenteditable elements.\n\n### textContent()\n\nGet the element's text content (raw).\n\n```typescript\nawait locator.textContent(): Promise<string>\n```\n\n**Returns:** `Promise<string>` - The element's `textContent` property.\n\n### innerText()\n\nGet the element's visible text (layout-aware).\n\n```typescript\nawait locator.innerText(): Promise<string>\n```\n\n**Returns:** `Promise<string>` - The element's `innerText` property.\n\n### innerHtml()\n\nGet the element's HTML content.\n\n```typescript\nawait locator.innerHtml(): Promise<string>\n```\n\n**Returns:** `Promise<string>` - The element's `innerHtml`.\n\n## Selection Methods\n\n### count()\n\nGet the number of elements matching the selector.\n\n```typescript\nawait locator.count(): Promise<number>\n```\n\n**Returns:** `Promise<number>` - Count of matching elements.\n\n### nth()\n\nGet a locator for the element at a specific index.\n\n```typescript\nlocator.nth(index: number): Locator\n```\n\n<ParamField path=\"index\" type=\"number\" required>\n  Zero-based index of the element to select.\n</ParamField>\n\n**Returns:** `Locator` - New locator targeting the nth element.\n\n```typescript\n// Get the third button\nconst thirdButton = page.locator(\"button\").nth(2);\nawait thirdButton.click();\n```\n\n### first()\n\nGet a locator for the first matching element.\n\n```typescript\nlocator.first(): Locator\n```\n\n**Returns:** `Locator` - Returns the same locator (querySelector already returns first match).\n\n## Utility Methods\n\n### highlight()\n\nVisually highlight the element with an overlay.\n\n```typescript\nawait locator.highlight(options?: HighlightOptions): Promise<void>\n```\n\n<ParamField path=\"durationMs\" type=\"number\" optional>\n  How long to display the highlight in milliseconds.\n\n  **Default:** `800`\n</ParamField>\n\n<ParamField path=\"borderColor\" type=\"{ r, g, b, a? }\" optional>\n  Border color RGBA values (0-255).\n\n  **Default:** `{ r: 255, g: 0, b: 0, a: 0.9 }` (red)\n</ParamField>\n\n<ParamField path=\"contentColor\" type=\"{ r, g, b, a? }\" optional>\n  Content fill color RGBA values (0-255).\n\n  **Default:** `{ r: 255, g: 200, b: 0, a: 0.2 }` (yellow)\n</ParamField>\n\nUseful for debugging and visual verification.\n\n### scrollTo()\n\nScroll the element to a specific position.\n\n```typescript\nawait locator.scrollTo(percent: number | string): Promise<void>\n```\n\n<ParamField path=\"percent\" type=\"number | string\" required>\n  Scroll position as percentage (0-100).\n</ParamField>\n\nFor `<html>` or `<body>` elements, scrolls the window. Otherwise, scrolls the element itself.\n\n### centroid()\n\nGet the center coordinates of the element.\n\n```typescript\nawait locator.centroid(): Promise<{ x: number; y: number }>\n```\n\n**Returns:** `Promise<{ x, y }>` - Center point in CSS pixels.\n\n### backendNodeId()\n\nGet the DOM backend node ID for the element.\n\n```typescript\nawait locator.backendNodeId(): Promise<BackendNodeId>\n```\n\n**Returns:** `Promise<BackendNodeId>` - Unique identifier for the DOM node.\n\nUseful for identity comparisons without maintaining element handles.\n\n### sendClickEvent()\n\nDispatch a DOM click event directly on the element.\n\n```typescript\nawait locator.sendClickEvent(options?: EventOptions): Promise<void>\n```\n\n<ParamField path=\"bubbles\" type=\"boolean\" optional>\n  Whether the event bubbles.\n\n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"cancelable\" type=\"boolean\" optional>\n  Whether the event is cancelable.\n\n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"composed\" type=\"boolean\" optional>\n  Whether the event crosses shadow DOM boundaries.\n\n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"detail\" type=\"number\" optional>\n  Click count detail.\n\n  **Default:** `1`\n</ParamField>\n\nThis dispatches an event directly without synthesizing real pointer input. Useful for elements that rely on click handlers without needing hit-testing.\n\n## Code Examples\n\n<Tabs>\n<Tab title=\"Basic Interaction\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\n// Click a button\nconst submitButton = page.locator(\"button[type=submit]\");\nawait submitButton.click();\n\n// Fill an input\nconst emailInput = page.locator(\"input[name=email]\");\nawait emailInput.fill(\"user@example.com\");\n\n// Type with delay\nconst searchBox = page.locator(\"input[type=search]\");\nawait searchBox.type(\"stagehand\", { delay: 100 });\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Forms\">\n\n```typescript\n// Fill multiple form fields\nconst form = page.locator(\"form#login\");\n\nawait page.locator(\"#username\").fill(\"myuser\");\nawait page.locator(\"#password\").fill(\"mypass\");\n\n// Select from dropdown\nawait page.locator(\"select#country\").selectOption(\"US\");\n\n// Multi-select\nawait page.locator(\"select#skills\").selectOption([\"js\", \"ts\", \"react\"]);\n\n// Check checkbox\nconst termsCheckbox = page.locator(\"input#terms\");\nconst isChecked = await termsCheckbox.isChecked();\nif (!isChecked) {\n  await termsCheckbox.click();\n}\n\n// Submit\nawait page.locator(\"button[type=submit]\").click();\n```\n\n</Tab>\n<Tab title=\"File Upload\">\n\n```typescript\n// Upload from file path\nconst fileInput = page.locator(\"input[type=file]\");\nawait fileInput.setInputFiles(\"/path/to/document.pdf\");\n\n// Upload multiple files\nawait fileInput.setInputFiles([\n  \"/path/to/image1.jpg\",\n  \"/path/to/image2.jpg\"\n]);\n\n// Upload from buffer\nawait fileInput.setInputFiles({\n  name: \"data.json\",\n  mimeType: \"application/json\",\n  buffer: JSON.stringify({ key: \"value\" })\n});\n\n// Clear file selection\nawait fileInput.setInputFiles([]);\n```\n\n</Tab>\n<Tab title=\"Element Selection\">\n\n```typescript\n// Count elements\nconst buttons = page.locator(\"button\");\nconst count = await buttons.count();\nconsole.log(`Found ${count} buttons`);\n\n// Click the first button\nawait buttons.first().click();\n\n// Click the third button\nawait buttons.nth(2).click();\n\n// Iterate with nth\nfor (let i = 0; i < count; i++) {\n  const button = buttons.nth(i);\n  const text = await button.innerText();\n  console.log(`Button ${i}: ${text}`);\n}\n```\n\n</Tab>\n<Tab title=\"State Checks\">\n\n```typescript\n// Check visibility\nconst modal = page.locator(\".modal\");\nif (await modal.isVisible()) {\n  console.log(\"Modal is visible\");\n}\n\n// Check checkbox state\nconst checkbox = page.locator(\"input#subscribe\");\nconst checked = await checkbox.isChecked();\nconsole.log(\"Subscribed:\", checked);\n\n// Get input value\nconst email = page.locator(\"input#email\");\nconst value = await email.inputValue();\nconsole.log(\"Email:\", value);\n\n// Get text content\nconst heading = page.locator(\"h1\");\nconst text = await heading.textContent();\nconsole.log(\"Heading:\", text);\n```\n\n</Tab>\n<Tab title=\"Advanced Actions\">\n\n```typescript\n// Hover to reveal menu\nconst menuButton = page.locator(\"button.menu\");\nawait menuButton.hover();\n\n// Wait for submenu\nawait page.waitForLoadState(\"networkidle\");\n\n// Click submenu item\nawait page.locator(\"a.submenu-item\").click();\n\n// Highlight for debugging\nawait page.locator(\"div.error\").highlight({\n  durationMs: 2000,\n  borderColor: { r: 255, g: 0, b: 0 },\n  contentColor: { r: 255, g: 0, b: 0, a: 0.1 }\n});\n\n// Scroll element into position\nconst section = page.locator(\"#section-3\");\nawait section.scrollTo(50); // Scroll to 50%\n\n// Get element position\nconst { x, y } = await section.centroid();\nconsole.log(`Element center: ${x}, ${y}`);\n```\n\n</Tab>\n</Tabs>\n\n## Selector Support\n\nLocators support both CSS and XPath selectors:\n\n### CSS Selectors\n\n```typescript\npage.locator(\"button\");                    // Tag\npage.locator(\".submit-btn\");              // Class\npage.locator(\"#login-form\");              // ID\npage.locator(\"button.primary\");           // Tag + class\npage.locator(\"input[type=email]\");        // Attribute\npage.locator(\"div > p\");                  // Child\npage.locator(\"h1 + p\");                   // Adjacent sibling\npage.locator(\"div.container button\");     // Descendant\n```\n\n### XPath Selectors\n\n```typescript\npage.locator(\"//button\");                               // Tag\npage.locator(\"//button[@class='submit']\");             // Attribute\npage.locator(\"//div[@id='content']//p\");               // Descendant\npage.locator(\"//button[contains(text(), 'Submit')]\");  // Text content\npage.locator(\"(//button)[1]\");                         // First button\npage.locator(\"//input[@type='text'][1]\");              // First text input\n```\n\n## Best Practices\n\n1. **Use specific selectors** - Prefer IDs or unique attributes over generic selectors\n2. **Chain with nth()** - Use `locator().nth()` instead of putting index in selector\n3. **Check state before action** - Use `isVisible()`, `isChecked()` for conditional logic\n4. **Let locators auto-resolve** - Don't store element handles, use locators which re-resolve\n5. **Use fill() for inputs** - Prefer `fill()` over `click()` + `type()` for better reliability\n6. **Handle file uploads properly** - Use absolute paths or buffer payloads for `setInputFiles()`\n7. **Highlight for debugging** - Use `highlight()` during development to verify targeting\n\n## Common Patterns\n\n### Conditional Interaction\n\n```typescript\nconst errorMessage = page.locator(\".error-message\");\nif (await errorMessage.isVisible()) {\n  const text = await errorMessage.textContent();\n  console.log(\"Error:\", text);\n}\n```\n\n### Wait and Interact\n\n```typescript\n// Locators automatically wait during actions\nconst dynamicButton = page.locator(\"button.dynamic\");\nawait dynamicButton.click(); // Waits for element to exist\n```\n\n### Loop Through Elements\n\n```typescript\nconst items = page.locator(\"li.item\");\nconst count = await items.count();\n\nfor (let i = 0; i < count; i++) {\n  const item = items.nth(i);\n  const text = await item.innerText();\n  console.log(`Item ${i}:`, text);\n}\n```\n\n## Error Handling\n\nLocator methods may throw the following errors:\n\n- **Element not found** - Selector doesn't match any elements\n- **Element not visible** - Element exists but is not visible (for actions requiring visibility)\n- **Invalid selector** - Malformed CSS or XPath selector\n- **Timeout errors** - Operation exceeded timeout limits\n- **CDP errors** - Chrome DevTools Protocol communication errors\n\nHandle errors appropriately:\n\n```typescript\ntry {\n  await page.locator(\"button.submit\").click();\n} catch (error) {\n  console.error(\"Click failed:\", error.message);\n}\n```\n\n## Type Definitions\n\n```typescript\ninterface Locator {\n  // Actions\n  click(options?: { button?: MouseButton; clickCount?: number }): Promise<void>;\n  fill(value: string): Promise<void>;\n  type(text: string, options?: { delay?: number }): Promise<void>;\n  hover(): Promise<void>;\n  selectOption(values: string | string[]): Promise<string[]>;\n  setInputFiles(files: FileInput): Promise<void>;\n\n  // State\n  isVisible(): Promise<boolean>;\n  isChecked(): Promise<boolean>;\n  inputValue(): Promise<string>;\n  textContent(): Promise<string>;\n  innerText(): Promise<string>;\n  innerHtml(): Promise<string>;\n\n  // Selection\n  count(): Promise<number>;\n  nth(index: number): Locator;\n  first(): Locator;\n\n  // Utilities\n  highlight(options?: HighlightOptions): Promise<void>;\n  scrollTo(percent: number | string): Promise<void>;\n  centroid(): Promise<{ x: number; y: number }>;\n  backendNodeId(): Promise<BackendNodeId>;\n  sendClickEvent(options?: EventOptions): Promise<void>;\n}\n```\n"
  },
  {
    "path": "packages/docs/v3/references/observe.mdx",
    "content": "---\ntitle: observe()\ndescription: 'Complete API reference for the observe() method'\nicon: 'magnifying-glass'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Observe\" icon=\"magnifying-glass\" href=\"/v3/basics/observe\">\n  See how to use observe() to discover actionable elements and analyze web page structure\n</Card>\n</CardGroup>\n\n### Method Signatures\n\n<Tabs>\n<Tab title=\"TypeScript\">\n\n```typescript\n// String instruction only\nawait stagehand.observe(instruction: string): Promise<Action[]>\n\n// String instruction with options\nawait stagehand.observe(instruction: string, options: ObserveOptions): Promise<Action[]>\n```\n\n**ObserveOptions Interface:**\n```typescript\ninterface ObserveOptions {\n  model?: ModelConfiguration;\n  timeout?: number;\n  selector?: string;\n  page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n  serverCache?: boolean;\n}\n\n// ModelConfiguration can be either a string or an object\ntype ModelConfiguration =\n  | string  // Format: \"provider/model\" (e.g., \"openai/gpt-4o\", \"anthropic/claude-sonnet-4-6\")\n  | {\n      modelName: string;  // The model name\n      apiKey?: string;    // Optional: API key override\n      baseURL?: string;   // Optional: Base URL override\n      // Additional provider-specific options\n    }\n```\n\n</Tab>\n\n</Tabs>\n\n### Parameters\n\n<ParamField path=\"instruction\" type=\"string\" required>\n  Natural language description of elements or actions to discover. If not provided, defaults to finding all interactive elements on the page.\n</ParamField>\n\n<ParamField path=\"model\" type=\"ModelConfiguration\" optional>\n  Configure the AI model to use for this observation. Can be either:\n  - A string in the format `\"provider/model\"` (e.g., `\"openai/gpt-4o\"`, `\"anthropic/claude-sonnet-4-6\"`)\n  - An object with detailed configuration\n\n  <Expandable title=\"Model Configuration Object\">\n    <ParamField path=\"modelName\" type=\"string\" required>\n      The model name (e.g., \"gpt-4o\", \"claude-sonnet-4-6\", \"gemini-2.5-flash\")\n    </ParamField>\n    <ParamField path=\"apiKey\" type=\"string\" optional>\n      API key for the model provider (overrides default)\n    </ParamField>\n    <ParamField path=\"baseURL\" type=\"string\" optional>\n      Base URL for the API endpoint (for custom endpoints or proxies)\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField path=\"timeout\" type=\"number\" optional>\n  Maximum time in milliseconds to wait for the observation to complete. Default varies by configuration.\n</ParamField>\n\n<ParamField path=\"selector\" type=\"string\" optional>\n  Optional XPath selector to focus the observation on a specific part of the page. Useful for narrowing down the search area.\n</ParamField>\n\n<ParamField path=\"page\" type=\"PlaywrightPage | PuppeteerPage | PatchrightPage | Page\" optional>\n  Optional: Specify which page to perform the observation on. Supports multiple browser automation libraries:\n  - **Playwright**: Native Playwright Page objects\n  - **Puppeteer**: Puppeteer Page objects\n  - **Patchright**: Patchright Page objects\n  - **Stagehand Page**: Stagehand's wrapped Page object\n\n  If not specified, defaults to the current \"active\" page in your Stagehand instance.\n</ParamField>\n\n<ParamField path=\"serverCache\" type=\"boolean\" optional>\n  Override the instance-level `serverCache` setting for this request. When `true`, enables server-side caching. When `false`, disables it.\n\n  <Note>Only applies when `env` is `\"BROWSERBASE\"`. Has no effect in local environments.</Note>\n\n  Defaults to the value set on the Stagehand constructor (which itself defaults to `true`).\n</ParamField>\n\n### Returns `Promise<Action[]>`\n\nArray of discovered actionable elements, ordered by relevance.\n\n<ResponseField name=\"selector\" type=\"string\">\n  XPath selector that precisely locates the element on the page.\n</ResponseField>\n\n<ResponseField name=\"description\" type=\"string\">\n  Human-readable description of the element and its purpose.\n</ResponseField>\n\n<ResponseField name=\"method\" type=\"string\" optional>\n  Suggested interaction method for the element (e.g., `\"click\"`, `\"fill\"`, `\"type\"`).\n</ResponseField>\n\n<ResponseField name=\"arguments\" type=\"string[]\" optional>\n  Additional parameters for the suggested action, if applicable.\n</ResponseField>\n\n**Action Interface:**\n```typescript\ninterface Action {\n  selector: string;        // XPath selector to locate element\n  description: string;     // Human-readable description\n  method?: string;         // Suggested action method\n  arguments?: string[];    // Additional action parameters\n}\n```\n\n**Example Response:**\n```json\n[\n  {\n    \"selector\": \"/html/body/div[1]/header/nav/button[1]\",\n    \"description\": \"Login button in the navigation bar\",\n    \"method\": \"click\",\n    \"arguments\": []\n  },\n  {\n    \"selector\": \"/html/body/main/form/input[1]\",\n    \"description\": \"Email input field in the login form\",\n    \"method\": \"fill\",\n    \"arguments\": []\n  }\n]\n```\n\n### Built-in Support\n\n<Note>\n**Iframe and Shadow DOM interactions are supported out of the box.** Stagehand automatically handles iframe traversal and shadow DOM elements without requiring additional configuration or flags.\n</Note>\n\n### Code Examples\n\n<Tabs>\n<Tab title=\"Basic Usage\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\n\n// Basic element discovery\nconst buttons = await stagehand.observe(\"find all clickable buttons\");\nconst formFields = await stagehand.observe(\"locate form input fields\");\n\n// Working with results\nconst [loginButton] = await stagehand.observe(\"find the login button\");\nif (loginButton) {\n  console.log(\"Found:\", loginButton.description);\n  console.log(\"Selector:\", loginButton.selector);\n  await stagehand.act(loginButton); // Execute the action\n}\n```\n\n</Tab>\n<Tab title=\"Custom Model\">\n\n```typescript\n// Using string format model\nconst elements = await stagehand.observe(\"find important call-to-action buttons\", {\n  model: \"openai/gpt-4o\",\n  timeout: 45000\n});\n\n// Using object format with custom configuration\nconst actions = await stagehand.observe(\"find navigation links\", {\n  model: {\n    modelName: \"claude-sonnet-4-6\",\n    apiKey: process.env.ANTHROPIC_API_KEY\n  },\n  timeout: 30000\n});\n```\n\n</Tab>\n<Tab title=\"Scoped\">\n\n```typescript\n// Focus observation on a specific part of the page\nconst tableActions = await stagehand.observe(\"find all table rows\", {\n  selector: \"/html/body/main/table\"\n});\n```\n\n</Tab>\n<Tab title=\"Multi-Page\">\n\n```typescript\n// Observe on specific pages\nconst page1 = stagehand.context.pages()[0];\nconst page2 = await stagehand.context.newPage();\n\nconst page1Actions = await stagehand.observe(\"find navigation\", { page: page1 });\nconst page2Actions = await stagehand.observe(\"find buttons\", { page: page2 });\n```\n\n</Tab>\n<Tab title=\"Filter Results\">\n\n```typescript\nconst submitButtons = await stagehand.observe(\"find all submit buttons\");\nconst primarySubmit = submitButtons.find(btn =>\n  btn.description.toLowerCase().includes('primary')\n);\n```\n\n</Tab>\n</Tabs>\n\n### Integration Patterns\n\n```typescript\n// Observe → Act workflow\nconst actions = await stagehand.observe(\"find checkout elements\");\nfor (const action of actions) {\n  await stagehand.act(action);\n  await page.waitForTimeout(1000);\n}\n\n// Observe → Extract workflow\nconst tables = await stagehand.observe(\"find data tables\");\nif (tables.length > 0) {\n  const data = await stagehand.extract({\n    instruction: \"extract the table data\",\n    selector: tables[0].selector,\n    schema: DataSchema\n  });\n}\n\n// Element validation\nconst requiredElements = await stagehand.observe(\"find the login form\");\nif (requiredElements.length === 0) {\n  throw new Error(\"Login form not found\");\n}\n```\n\n### Error Types\n\nThe following errors may be thrown by the `observe()` method:\n\n- **StagehandError** - Base class for all Stagehand-specific errors\n- **StagehandDomProcessError** - Error occurred while processing the DOM\n- **StagehandEvalError** - Error occurred while evaluating JavaScript in the page context\n- **StagehandIframeError** - Unable to resolve iframe for the target element\n- **ContentFrameNotFoundError** - Unable to obtain content frame for the selector\n- **XPathResolutionError** - XPath does not resolve in the current page or frames\n- **StagehandShadowRootMissingError** - No shadow root present on the resolved host element\n- **LLMResponseError** - Error in LLM response processing\n- **MissingLLMConfigurationError** - No LLM API key or client configured\n- **UnsupportedModelError** - The specified model is not supported for this operation\n- **InvalidAISDKModelFormatError** - Model string does not follow the required `provider/model` format\n"
  },
  {
    "path": "packages/docs/v3/references/page.mdx",
    "content": "---\ntitle: page\ndescription: 'Complete API reference for the Stagehand Page object'\nicon: 'page'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n<CardGroup cols={1}>\n<Card title=\"Page\" icon=\"browser\" href=\"/v3/references/page\">\n  Learn about the Stagehand Page object and browser navigation\n</Card>\n</CardGroup>\n\n## Overview\n\nThe `page` object is the main interface for interacting with browser pages in Stagehand. It provides standard browser automation capabilities for navigation, interaction, and page inspection.\n\nAccess the page object through your Stagehand instance:\n\n```typescript\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n```\n\n## Navigation Methods\n\n### goto()\n\nNavigate the page to a URL and wait for a lifecycle state.\n\n```typescript\nawait page.goto(url: string, options?: GotoOptions): Promise<Response | null>\n```\n\nReturns a [Response](/v3/references/response) when the navigation produces a network document request, otherwise `null` (e.g. `data:` URLs or same-document navigations).\n\n<ParamField path=\"url\" type=\"string\" required>\n  The URL to navigate to. Can be absolute or relative.\n</ParamField>\n\n<ParamField path=\"waitUntil\" type=\"LoadState\" optional>\n  When to consider navigation succeeded.\n\n  **Options:**\n  - `\"load\"` - Wait for the load event\n  - `\"domcontentloaded\"` - Wait for DOMContentLoaded event (default)\n  - `\"networkidle\"` - Wait for network to be idle\n\n  **Default:** `\"domcontentloaded\"`\n</ParamField>\n\n<ParamField path=\"timeoutMs\" type=\"number\" optional>\n  Maximum time to wait for navigation in milliseconds.\n\n  **Default:** `15000`\n</ParamField>\n\n### reload()\n\nReload the current page.\n\n```typescript\nawait page.reload(options?: ReloadOptions): Promise<Response | null>\n```\n\nResolves with a [Response](/v3/references/response) for the refreshed document when one is reported, otherwise `null`.\n\n<ParamField path=\"waitUntil\" type=\"LoadState\" optional>\n  When to consider reload complete. See `goto()` for options.\n</ParamField>\n\n<ParamField path=\"timeoutMs\" type=\"number\" optional>\n  Maximum time to wait for reload in milliseconds.\n\n  **Default:** `15000`\n</ParamField>\n\n<ParamField path=\"ignoreCache\" type=\"boolean\" optional>\n  Whether to bypass the browser cache.\n\n  **Default:** `false`\n</ParamField>\n\n### goBack()\n\nNavigate back in browser history.\n\n```typescript\nawait page.goBack(options?: NavigationOptions): Promise<Response | null>\n```\n\nReturns a [Response](/v3/references/response) when the history entry triggers a network fetch; otherwise `null`.\n\n<ParamField path=\"waitUntil\" type=\"LoadState\" optional>\n  When to consider navigation complete.\n</ParamField>\n\n<ParamField path=\"timeoutMs\" type=\"number\" optional>\n  Maximum time to wait in milliseconds.\n\n  **Default:** `15000`\n</ParamField>\n\n### goForward()\n\nNavigate forward in browser history.\n\n```typescript\nawait page.goForward(options?: NavigationOptions): Promise<Response | null>\n```\n\nReturns a [Response](/v3/references/response) when the navigation loads a new document from the network; otherwise `null`.\n\n<ParamField path=\"waitUntil\" type=\"LoadState\" optional>\n  When to consider navigation complete.\n</ParamField>\n\n<ParamField path=\"timeoutMs\" type=\"number\" optional>\n  Maximum time to wait in milliseconds.\n\n  **Default:** `15000`\n</ParamField>\n\n## Page Information\n\n### url()\n\nGet the current page URL (synchronous).\n\n```typescript\npage.url(): string\n```\n\n**Returns:** The current page URL as a string.\n\n### title()\n\nGet the current page title.\n\n```typescript\nawait page.title(): Promise<string>\n```\n\n**Returns:** The page title as a string.\n\n## Interaction Methods\n\n### click()\n\nClick at absolute page coordinates.\n\n```typescript\nawait page.click(x: number, y: number, options?: ClickOptions): Promise<string>\n```\n\n**Returns:** A string containing the XPath of the clicked element when `returnXpath` is `true`, otherwise an empty string.\n\n<ParamField path=\"x\" type=\"number\" required>\n  X coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"y\" type=\"number\" required>\n  Y coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"options\" type=\"object\" optional>\n  Optional click configuration.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"button\" type=\"string\">\n      Mouse button to use: `\"left\"` | `\"right\"` | `\"middle\"`\n\n      Default: `\"left\"`\n    </ParamField>\n\n    <ParamField path=\"clickCount\" type=\"number\">\n      Number of consecutive clicks.\n\n      Default: `1`\n    </ParamField>\n\n    <ParamField path=\"returnXpath\" type=\"boolean\">\n      If `true`, the returned string contains the XPath of the clicked element. If `false`, returns an empty string.\n\n      Default: `false`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n### hover()\n\nHover at absolute page coordinates without clicking.\n\n```typescript\nawait page.hover(x: number, y: number, options?: HoverOptions): Promise<string>\n```\n\n**Returns:** A string containing the XPath of the hovered element when `returnXpath` is `true`, otherwise an empty string.\n\n<ParamField path=\"x\" type=\"number\" required>\n  X coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"y\" type=\"number\" required>\n  Y coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"options\" type=\"object\" optional>\n  Optional hover configuration.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"returnXpath\" type=\"boolean\">\n      If `true`, the returned string contains the XPath of the hovered element. If `false`, returns an empty string.\n\n      Default: `false`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n### scroll()\n\nScroll at absolute page coordinates using mouse wheel events.\n\n```typescript\nawait page.scroll(x: number, y: number, deltaX: number, deltaY: number, options?: ScrollOptions): Promise<string>\n```\n\n**Returns:** A string containing the XPath of the element at the scroll position when `returnXpath` is `true`, otherwise an empty string.\n\n<ParamField path=\"x\" type=\"number\" required>\n  X coordinate in CSS pixels where the scroll occurs.\n</ParamField>\n\n<ParamField path=\"y\" type=\"number\" required>\n  Y coordinate in CSS pixels where the scroll occurs.\n</ParamField>\n\n<ParamField path=\"deltaX\" type=\"number\" required>\n  Horizontal scroll amount in pixels. Positive values scroll right.\n</ParamField>\n\n<ParamField path=\"deltaY\" type=\"number\" required>\n  Vertical scroll amount in pixels. Positive values scroll down.\n</ParamField>\n\n<ParamField path=\"options\" type=\"object\" optional>\n  Optional scroll configuration.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"returnXpath\" type=\"boolean\">\n      If `true`, the returned string contains the XPath of the element at the scroll position. If `false`, returns an empty string.\n\n      Default: `false`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n### dragAndDrop()\n\nDrag from one position to another using mouse events.\n\n```typescript\nconst [fromXpath, toXpath] = await page.dragAndDrop(fromX, fromY, toX, toY, options?)\n```\n\n**Returns:** An array of two strings containing the XPaths of the elements at the start and end positions when `returnXpath` is `true`, otherwise empty strings.\n\n<ParamField path=\"fromX\" type=\"number\" required>\n  Starting X coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"fromY\" type=\"number\" required>\n  Starting Y coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"toX\" type=\"number\" required>\n  Ending X coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"toY\" type=\"number\" required>\n  Ending Y coordinate in CSS pixels.\n</ParamField>\n\n<ParamField path=\"options\" type=\"object\" optional>\n  Optional drag configuration.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"button\" type=\"string\">\n      Mouse button to use: `\"left\"` | `\"right\"` | `\"middle\"`\n\n      Default: `\"left\"`\n    </ParamField>\n\n    <ParamField path=\"steps\" type=\"number\">\n      Number of intermediate mouse move events during the drag.\n\n      Default: `1`\n    </ParamField>\n\n    <ParamField path=\"delay\" type=\"number\">\n      Delay in milliseconds between intermediate move events.\n\n      Default: `0`\n    </ParamField>\n\n    <ParamField path=\"returnXpath\" type=\"boolean\">\n      If `true`, the returned array contains the XPaths of the elements at the start and end positions. If `false`, returns empty strings.\n\n      Default: `false`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n### type()\n\nType text into the page (dispatches keyboard events).\n\n```typescript\nawait page.type(text: string, options?: TypeOptions): Promise<void>\n```\n\n<ParamField path=\"text\" type=\"string\" required>\n  The text to type.\n</ParamField>\n\n<ParamField path=\"options\" type=\"object\" optional>\n  Optional typing configuration.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"delay\" type=\"number\">\n      Delay between key presses in milliseconds.\n    </ParamField>\n\n    <ParamField path=\"withMistakes\" type=\"boolean\">\n      Simulates typing with occasional mistakes and corrections.\n\n      Default: `false`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n### locator()\n\nCreate a locator for querying elements.\n\n```typescript\npage.locator(selector: string): Locator\n```\n\n<ParamField path=\"selector\" type=\"string\" required>\n  CSS selector or XPath for the element.\n</ParamField>\n\n**Returns:** A `Locator` object for interacting with the element.\n\n## Evaluation\n\n### evaluate()\n\nEvaluate JavaScript code in the page context.\n\n```typescript\nawait page.evaluate<R, Arg>(\n  pageFunctionOrExpression: string | ((arg: Arg) => R | Promise<R>),\n  arg?: Arg\n): Promise<R>\n```\n\n<ParamField path=\"pageFunctionOrExpression\" type=\"string | function\" required>\n  JavaScript expression as a string or a function to execute in the page context.\n</ParamField>\n\n<ParamField path=\"arg\" type=\"any\" optional>\n  Optional argument to pass to the function.\n</ParamField>\n\n**Returns:** The result of the evaluation (must be JSON-serializable).\n\n## Initialization Scripts\n\n### addInitScript()\n\nInject JavaScript that runs before any of the page's scripts on every navigation.\n\n```typescript\nawait page.addInitScript<Arg>(\n  script: string | { path?: string; content?: string } | ((arg: Arg) => unknown),\n  arg?: Arg,\n): Promise<void>\n```\n\n<ParamField\n  path=\"script\"\n  type=\"string | { path?: string; content?: string } | (arg: Arg) => unknown\"\n  required\n>\n  Provide the script to inject. Pass raw source, reference a preload file on disk,\n  or supply a function that Stagehand serializes before sending to the browser.\n</ParamField>\n\n<ParamField path=\"arg\" type=\"Arg\" optional>\n  Extra data that is JSON-serialized and passed to your function. Only supported\n  when `script` is a function.\n</ParamField>\n\nThis method:\n- Runs at document start for the current page (including adopted iframe sessions) on every navigation\n- Reinstalls the script for all future navigations of this page without affecting other pages\n- Mirrors Playwright's `page.addInitScript()` ordering semantics; use  [`context.addInitScript()`](/v3/references/context#addinitscript) to target every page in the context\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst context = stagehand.context;\nconst page = await context.awaitActivePage();\n\nawait page.addInitScript(() => {\n  window.Math.random = () => 42;\n});\n\nawait page.goto(\"https://example.com\", { waitUntil: \"load\" });\n\nconst result = await page.evaluate(() => Math.random());\nconsole.log(\"Math.random() returned:\", result);\n\n// Math.random() returned: 42\n```\n\n## HTTP Headers\n\n### setExtraHTTPHeaders()\n\nSet HTTP headers that will be included in every request made by this page.\n\n```typescript\nawait page.setExtraHTTPHeaders(headers: Record<string, string>): Promise<void>\n```\n\n<ParamField path=\"headers\" type=\"Record<string, string>\" required>\n  A plain object of header name–value pairs. All values must be strings.\n</ParamField>\n\nThis method:\n- Applies the headers to the page's main CDP session and all of its child sessions (e.g. out-of-process iframes)\n- Automatically applies the same headers to any child sessions adopted after calling `setExtraHTTPHeaders()`\n- Calling it again replaces all previously set extra headers (it does not merge)\n- To clear all extra headers, pass an empty object: `await page.setExtraHTTPHeaders({})`\n\n<Note>\nHeaders set via `page.setExtraHTTPHeaders()` are page-scoped. They apply to every network request from this page only, including navigation requests, XHR/fetch calls, and subresource loads. Use [`context.setExtraHTTPHeaders()`](/v3/references/context#setextrahttpheaders) to set headers across all pages in the context.\n</Note>\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// Set custom headers for all requests from this page\nawait page.setExtraHTTPHeaders({\n  \"X-Custom-Token\": \"my-secret-token\",\n  \"Accept-Language\": \"en-US\",\n});\n\n// All subsequent requests from this page will include these headers\nawait page.goto(\"https://example.com\");\n```\n\n## Screenshot\n\n### screenshot()\n\nCapture a screenshot of the page.\n\n```typescript\nawait page.screenshot(options?: ScreenshotOptions): Promise<Buffer>\n```\n\n<ParamField path=\"fullPage\" type=\"boolean\" optional>\n  Capture the entire scrollable page instead of just the current viewport.\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"clip\" type=\"ScreenshotClip\" optional>\n  Limit the capture to the provided rectangle in CSS pixels (`{ x, y, width, height }`).\n  Cannot be combined with `fullPage`.\n</ParamField>\n\n<ParamField path=\"type\" type=\"'png' | 'jpeg'\" optional>\n  Image format for the screenshot.\n\n  **Default:** `\"png\"`\n</ParamField>\n\n<ParamField path=\"quality\" type=\"number\" optional>\n  JPEG quality (0–100). Only used when `type` is `\"jpeg\"`.\n</ParamField>\n\n<ParamField path=\"scale\" type=\"'css' | 'device'\" optional>\n  Rendering scale. Use `\"css\"` for one pixel per CSS pixel, or `\"device\"` for the\n  device pixel ratio.\n\n  **Default:** `\"device\"`\n</ParamField>\n\n<ParamField path=\"animations\" type=\"'allow' | 'disabled'\" optional>\n  Control CSS/Web animations and transitions. `\"disabled\"` fast-forwards finite\n  animations and pauses infinite ones before capture.\n\n  **Default:** `\"allow\"`\n</ParamField>\n\n<ParamField path=\"caret\" type=\"hide | initial\" optional>\n  Hide the text caret during capture (`\"hide\"`) or leave it untouched (`\"initial\"`).\n\n  **Default:** `\"hide\"`\n</ParamField>\n\n<ParamField path=\"mask\" type=\"Locator[]\" optional>\n  List of locators to cover with a colored overlay while the screenshot is taken.\n</ParamField>\n\n<ParamField path=\"maskColor\" type=\"string\" optional>\n  CSS color to use for masked overlays.\n\n  **Default:** `#FF00FF`\n</ParamField>\n\n<ParamField path=\"style\" type=\"string\" optional>\n  Additional CSS text injected into every frame just before capture. Useful for\n  hiding or tweaking dynamic UI.\n</ParamField>\n\n<ParamField path=\"omitBackground\" type=\"boolean\" optional>\n  Make the default page background transparent (PNG only).\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"timeout\" type=\"number\" optional>\n  Maximum time in milliseconds to wait for the capture before throwing.\n</ParamField>\n\n<ParamField path=\"path\" type=\"string\" optional>\n  Write the screenshot to the provided file path. The image is still returned as\n  a buffer.\n</ParamField>\n\n**Returns:** A `Promise<Buffer>` containing the screenshot image data.\n\n## Page Snapshot\n\n### snapshot()\n\nCapture a structured accessibility snapshot of the current page. The returned data combines a human-readable accessibility tree with lookup maps so you can relate each node to DOM selectors or URLs.\n\n```typescript\nawait page.snapshot(options?: PageSnapshotOptions): Promise<SnapshotResult>\n```\n\n<ParamField path=\"options\" type=\"PageSnapshotOptions\" optional>\n  Optional configuration for the snapshot.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"includeIframes\" type=\"boolean\">\n      Whether to include iframe content in the snapshot.\n\n      **Default:** `true`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n**Returns:** A `Promise<SnapshotResult>` describing the captured accessibility tree.\n\n<Expandable title=\"SnapshotResult properties\">\n  <ParamField path=\"formattedTree\" type=\"string\">\n    Multiline text representing the accessibility tree hierarchy with encoded IDs.\n  </ParamField>\n  <ParamField path=\"xpathMap\" type=\"Record<string, string>\">\n    Maps each encoded ID to the element's absolute XPath for quick DOM lookups.\n  </ParamField>\n  <ParamField path=\"urlMap\" type=\"Record<string, string>\">\n    Maps encoded IDs for link-like nodes to their resolved URLs.\n  </ParamField>\n</Expandable>\n\nSee [SnapshotResult](#snapshotresult) for the static type definition.\n\nThe formatted tree represents every accessibility node with:\n- A unique encoded ID in brackets (e.g., `[0-1]`) for cross-referencing with the maps\n- The node's accessibility role (`RootWebArea`, `heading`, `link`, `button`, etc.)\n- The node's accessible name, when available\n\n**Example formatted output:**\n\n```txt\n[0-1] RootWebArea: Example Domain\n  [0-3] heading: Example Domain\n  [0-5] paragraph: This domain is for use in illustrative examples in documents.\n  [0-8] link: More information...\n```\n\n**Example usage:**\n\n```typescript\nconst page = stagehand.context.pages()[0];\nawait page.goto(\"https://example.com\");\n\nconst { formattedTree, xpathMap, urlMap } = await page.snapshot();\n\n// Print the accessibility tree\nconsole.log(formattedTree);\n\n// Look up a specific element's XPath by encoded ID\nconst linkId = \"0-8\";\nconsole.log(xpathMap[linkId]); // e.g., \"/html/body/div/p[2]/a\"\n\n// Resolve a link's URL via the urlMap\nconsole.log(urlMap[linkId]); // e.g., \"https://www.example.com\"\n\n// Exclude iframe content when you only need the main document\nconst mainDocumentSnapshot = await page.snapshot({ includeIframes: false });\n```\n\n## Viewport\n\n### setViewportSize()\n\nSet the page viewport size.\n\n```typescript\nawait page.setViewportSize(\n  width: number,\n  height: number,\n  options?: ViewportOptions\n): Promise<void>\n```\n\n<ParamField path=\"width\" type=\"number\" required>\n  Viewport width in CSS pixels.\n</ParamField>\n\n<ParamField path=\"height\" type=\"number\" required>\n  Viewport height in CSS pixels.\n</ParamField>\n\n<ParamField path=\"deviceScaleFactor\" type=\"number\" optional>\n  Device scale factor (pixel ratio).\n\n  **Default:** `1`\n</ParamField>\n\n## Wait Methods\n\n### waitForLoadState()\n\nWait for the page to reach a specific lifecycle state.\n\n```typescript\nawait page.waitForLoadState(state: LoadState, timeoutMs?: number): Promise<void>\n```\n\n<ParamField path=\"state\" type=\"LoadState\" required>\n  The lifecycle state to wait for.\n\n  **Options:** `\"load\"`, `\"domcontentloaded\"`, `\"networkidle\"`\n</ParamField>\n\n<ParamField path=\"timeoutMs\" type=\"number\" optional>\n  Maximum time to wait in milliseconds.\n\n  **Default:** `15000`\n</ParamField>\n\n### waitForSelector()\n\nWait for an element matching the selector to reach a specific state in the DOM. Uses a MutationObserver for efficiency, pierces shadow DOM by default, and supports iframe hops when needed.\n\n```typescript\nawait page.waitForSelector(\n  selector: string,\n  options?: {\n    state?: \"attached\" | \"detached\" | \"visible\" | \"hidden\";\n    timeout?: number;\n    pierceShadow?: boolean;\n  }\n): Promise<boolean>\n```\n\n<ParamField path=\"selector\" type=\"string\" required>\n  CSS selector or XPath expression to wait for. Supports iframe hops (e.g., `/html/div/iframe/html/div/button`).\n</ParamField>\n\n<ParamField path=\"options\" type=\"object\" optional>\n  Optional wait configuration.\n\n  <Expandable title=\"properties\">\n    <ParamField path=\"state\" type=\"'attached' | 'detached' | 'visible' | 'hidden'\">\n      Element state to wait for.\n\n      **Options:**\n      - `\"attached\"` - Element is present in DOM (even if hidden)\n      - `\"detached\"` - Element is removed from DOM\n      - `\"visible\"` - Element is visible\n      - `\"hidden\"` - Element is hidden\n\n      **Default:** `\"visible\"`\n    </ParamField>\n\n    <ParamField path=\"timeout\" type=\"number\">\n      Maximum time to wait in milliseconds before timing out.\n\n      **Default:** `30000`\n    </ParamField>\n\n    <ParamField path=\"pierceShadow\" type=\"boolean\">\n      Whether to search inside open and closed shadow DOM boundaries.\n\n      **Default:** `true`\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n**Returns:** `true` when the condition is met.\n\n**Throws:** Error if timeout is reached before the condition is met.\n\n## Events\n\n### on(\"console\")\n\nListen for console output produced by the page and any adopted iframe sessions. Returns the page instance so calls can be chained.\n\n```typescript\nimport type { ConsoleMessage } from \"@browserbasehq/stagehand\";\n\nconst handleConsole = (message: ConsoleMessage) => {\n  console.log(`[${message.type()}] ${message.text()}`);\n  console.log(\"Arguments:\", message.args());\n  const location = message.location();\n  if (location?.url) {\n    console.log(`Emitted from ${location.url}:${location.lineNumber ?? 0}`);\n  }\n};\n\npage.on(\"console\", handleConsole);\n```\n\n`ConsoleMessage` exposes helpers for working with console events:\n\n- `message.type()` – console API category such as `log`, `error`, or `warning`\n- `message.text()` – string representation of the console arguments\n- `message.args()` – underlying CDP `RemoteObject` arguments array\n- `message.location()` – source URL, line, and column when available\n- `message.timestamp()` – CDP timestamp for the event\n- `message.raw()` – access to the original `Runtime.consoleAPICalledEvent`\n\n### once(\"console\")\n\nRegister a listener that removes itself after the first console event.\n\n```typescript\npage.once(\"console\", (message) => {\n  console.log(\"First console message:\", message.text());\n});\n```\n\n### off(\"console\")\n\nRemove a previously registered listener. The reference must match the original listener passed to `on()`.\n\n```typescript\npage.off(\"console\", handleConsole);\n```\n\n## Code Examples\n\n<Tabs>\n<Tab title=\"Basic Navigation\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Initialize with Browserbase (API key and project ID from environment variables)\n// Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your environment\nconst stagehand = new Stagehand({ env: \"BROWSERBASE\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// Navigate to a URL\nawait page.goto(\"https://example.com\");\n\n// Get current URL and title\nconsole.log(\"URL:\", page.url());\nconsole.log(\"Title:\", await page.title());\n\n// Navigate back and forward\nawait page.goBack();\nawait page.goForward();\n\n// Reload the page\nawait page.reload();\n```\n\n</Tab>\n<Tab title=\"Screenshots\">\n\n```typescript\n// Capture viewport screenshot\nconst screenshot = await page.screenshot();\nawait fs.writeFile(\"screenshot.png\", screenshot);\n\n// Capture full page screenshot\nconst fullPage = await page.screenshot({ fullPage: true });\nawait fs.writeFile(\"fullpage.png\", fullPage);\n\n// Capture JPEG with styling overrides and a masked element\nconst styled = await page.screenshot({\n  type: \"jpeg\",\n  quality: 80,\n  style: \"body { filter: grayscale(1); }\",\n  mask: [page.locator(\".ads-banner\")],\n  maskColor: \"rgba(0, 0, 0, 0.3)\",\n});\nawait fs.writeFile(\"styled.jpg\", styled);\n```\n\n</Tab>\n<Tab title=\"JavaScript Evaluation\">\n\n```typescript\n// Execute JavaScript expression\nconst pageHeight = await page.evaluate(\"document.body.scrollHeight\");\nconsole.log(\"Page height:\", pageHeight);\n\n// Execute function with arguments\nconst result = await page.evaluate((selector) => {\n  const element = document.querySelector(selector);\n  return element ? element.textContent : null;\n}, \"h1\");\nconsole.log(\"H1 text:\", result);\n\n// Async function evaluation\nconst data = await page.evaluate(async () => {\n  const response = await fetch(\"/api/data\");\n  return response.json();\n});\n```\n\n</Tab>\n<Tab title=\"Interaction\">\n\n```typescript\n// Click at coordinates\nawait page.click(100, 200);\n\n// Double click\nawait page.click(100, 200, { clickCount: 2 });\n\n// Click and get the XPath of the clicked element\nconst xpath = await page.click(100, 200, { returnXpath: true });\nconsole.log(\"Clicked element xpath:\", xpath); // e.g., \"/html/body/div[1]/button\"\n\n// Hover at coordinates\nawait page.hover(300, 150);\n\n// Hover and get the XPath of the hovered element\nconst hoverXpath = await page.hover(300, 150, { returnXpath: true });\n\n// Scroll down at a position\nawait page.scroll(400, 300, 0, 200); // scroll down 200px\n\n// Drag and drop between two points\nconst [fromXpath, toXpath] = await page.dragAndDrop(100, 100, 300, 300, { returnXpath: true });\n\n// Type text\nawait page.type(\"Hello, World!\");\n\n// Type with delay between keystrokes\nawait page.type(\"Slow typing\", { delay: 100 });\n\n// Use locator for element interaction\nconst button = page.locator(\"button.submit\");\nawait button.click();\n```\n\n</Tab>\n<Tab title=\"Wait for Load\">\n\n```typescript\n// Navigate and wait for full load\nawait page.goto(\"https://example.com\", {\n  waitUntil: \"load\",\n  timeoutMs: 30000\n});\n\n// Wait for network idle after navigation\nawait page.goto(\"https://spa-app.com\", {\n  waitUntil: \"networkidle\"\n});\n\n// Wait for specific load state\nawait page.waitForLoadState(\"domcontentloaded\");\n```\n\n</Tab>\n<Tab title=\"Wait for Selector\">\n\n```typescript\n// Wait for element to be visible (default)\nawait page.waitForSelector(\"#submit-btn\");\n\n// Wait for element to appear with custom timeout\nawait page.waitForSelector(\".loading-spinner\", {\n  state: \"visible\",\n  timeout: 10000\n});\n\n// Wait for element to be removed from DOM\nawait page.waitForSelector(\".loading-spinner\", {\n  state: \"detached\"\n});\n\n// Wait for element to become hidden\nawait page.waitForSelector(\".modal\", {\n  state: \"hidden\"\n});\n\n// Wait for element inside an iframe\nawait page.waitForSelector(\"iframe#checkout >> .pay-button\");\n\n// Wait for element in shadow DOM (enabled by default)\nawait page.waitForSelector(\"#shadow-button\", {\n  pierceShadow: true\n});\n\n// Wait for element with XPath\nawait page.waitForSelector(\"/html/div/button\");\n```\n\n</Tab>\n<Tab title=\"Viewport\">\n\n```typescript\n// Set viewport size\nawait page.setViewportSize(1920, 1080);\n\n// Set mobile viewport with device scale\nawait page.setViewportSize(375, 667, {\n  deviceScaleFactor: 2\n});\n\n// Then take a screenshot at this size\nconst screenshot = await page.screenshot();\n```\n\n</Tab>\n<Tab title=\"Custom HTTP Headers\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// Set authorization headers for requests from this page\nawait page.setExtraHTTPHeaders({\n  Authorization: \"Bearer my-api-token\",\n});\n\n// Navigate — the headers are sent with every request from this page\nawait page.goto(\"https://api.example.com/dashboard\");\n\n// Replace headers (previous headers are removed)\nawait page.setExtraHTTPHeaders({\n  Authorization: \"Bearer refreshed-token\",\n  \"X-Request-Id\": \"abc-123\",\n});\n\n// Clear all extra headers\nawait page.setExtraHTTPHeaders({});\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Snapshot\">\n\n```typescript\n// Capture the page's accessibility tree snapshot\nconst { formattedTree, xpathMap, urlMap } = await page.snapshot();\n\n// The formattedTree shows the page structure:\n// [0-1] RootWebArea: Example Domain\n//   [0-3] heading: Example Domain\n//   [0-8] link: More information...\n\nconsole.log(formattedTree);\n\n// Use xpathMap to get the XPath selector for any element by ID\nconst linkXpath = xpathMap[\"0-8\"];\nconsole.log(\"Link XPath:\", linkXpath); // \"/html/body/div/p[2]/a\"\n\n// Use urlMap to get URLs associated with link elements\nconst linkUrl = urlMap[\"0-8\"];\nconsole.log(\"Link URL:\", linkUrl); // \"https://www.iana.org/domains/example\"\n\n// Exclude iframe content from the snapshot\nconst mainPageOnly = await page.snapshot({ includeIframes: false });\n```\n\n</Tab>\n</Tabs>\n\n## Types\n\n### LoadState\n\n```typescript\ntype LoadState = \"load\" | \"domcontentloaded\" | \"networkidle\";\n```\n\n- **`\"load\"`** - Wait for the `load` event (all resources loaded)\n- **`\"domcontentloaded\"`** - Wait for the `DOMContentLoaded` event (DOM is ready)\n- **`\"networkidle\"`** - Wait for network connections to be idle\n\n### AnyPage\n\n```typescript\ntype AnyPage = PlaywrightPage | PuppeteerPage | PatchrightPage | Page;\n```\n\nStagehand supports multiple browser automation libraries. The `AnyPage` type represents any compatible page object.\n\n### ScreenshotClip\n\n```typescript\ninterface ScreenshotClip {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n```\n\nRepresents the CSS-pixel rectangle to capture when `clip` is provided.\n\n### ScreenshotOptions\n\n```typescript\ninterface ScreenshotOptions {\n  fullPage?: boolean;\n  clip?: ScreenshotClip;\n  type?: \"png\" | \"jpeg\";\n  quality?: number;\n  scale?: \"css\" | \"device\";\n  animations?: \"allow\" | \"disabled\";\n  caret?: \"hide\" | \"initial\";\n  mask?: Locator[];\n  maskColor?: string;\n  style?: string;\n  omitBackground?: boolean;\n  timeout?: number;\n  path?: string;\n}\n```\n\nMatches Playwright's screenshot signature with sensible defaults to control how a\ncapture is produced.\n\n### PageSnapshotOptions\n\n```typescript\ntype PageSnapshotOptions = {\n  includeIframes?: boolean;\n};\n```\n\n- **`includeIframes`** - Whether to include iframe content in the snapshot. Defaults to `true`\n\n### SnapshotResult\n\n```typescript\ntype SnapshotResult = {\n  formattedTree: string;\n  xpathMap: Record<string, string>;\n  urlMap: Record<string, string>;\n};\n```\n\n- **`formattedTree`** - A formatted string representation of the page's accessibility tree with encoded IDs, roles, and names\n- **`xpathMap`** - A mapping from encoded element IDs to their absolute XPath selectors\n- **`urlMap`** - A mapping from encoded element IDs to their associated URLs (for links and other navigable elements)\n\n## Error Handling\n\nPage methods may throw the following errors:\n\n- **Navigation Errors** - Timeout or network issues during navigation\n- **Evaluation Errors** - JavaScript execution errors in `evaluate()`\n- **Interaction Errors** - Failed clicks or typing operations\n- **Screenshot Errors** - Issues capturing screenshots\n\nAll errors should be caught and handled appropriately:\n\n```typescript\ntry {\n  await page.goto(\"https://example.com\");\n} catch (error) {\n  console.error(\"Navigation failed:\", error.message);\n}\n```\n"
  },
  {
    "path": "packages/docs/v3/references/response.mdx",
    "content": "---\ntitle: Response\ndescription: 'Complete API reference for the Response object'\nicon: 'reply'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Navigation\" icon=\"compass\" href=\"/v3/references/page\">\n  See how pages expose Response objects from navigation methods\n</Card>\n</CardGroup>\n\n## Overview\n\n`Response` mirrors Playwright’s [Response](https://playwright.dev/docs/api/class-response) interface and is returned from Stagehand navigation helpers such as `page.goto()`, `page.reload()`, `page.goBack()`, and `page.goForward()`. It provides a convenient way to inspect the HTTP metadata associated with a navigation, retrieve the response body on demand, and monitor when the underlying request finishes.\n\nStagehand automatically returns `null` for navigations that do not yield a network request (for example `data:` URLs, `about:blank`, or same-document history changes), matching Playwright’s behaviour.\n\n## Getting a Response\n\n```typescript\nconst response = await page.goto(\"https://example.com\", {\n  waitUntil: \"networkidle\",\n});\n\nif (!response) {\n  throw new Error(\"Navigation did not produce a network response\");\n}\n\nconsole.log(\"Status\", response.status(), response.statusText());\nconst body = await response.text();\n```\n\nWhen a navigation does not produce a response object you will receive `null`, allowing you to branch early:\n\n```typescript\nconst inline = await page.goto(\"data:text/html,<h1>inline</h1>\");\nif (inline === null) {\n  // No network fetch happened; handle accordingly\n}\n```\n\n## Status & Metadata\n\n### url()\n\n```typescript\nresponse.url(): string\n```\n\nReturns the final URL associated with the navigation request.\n\n### status()\n\n```typescript\nresponse.status(): number\n```\n\nReturns the HTTP status code.\n\n### statusText()\n\n```typescript\nresponse.statusText(): string\n```\n\nReturns the human-readable status text (for example `OK`).\n\n### ok()\n\n```typescript\nresponse.ok(): boolean\n```\n\nConvenience helper that resolves to `true` for 2xx responses and `false` otherwise.\n\n### frame()\n\n```typescript\nresponse.frame(): Frame | null\n```\n\nReturns the Stagehand `Frame` that initiated the navigation. When the frame is no longer available, `null` is returned.\n\n### fromServiceWorker()\n\n```typescript\nresponse.fromServiceWorker(): boolean\n```\n\nIndicates whether the response was served from a Service Worker fetch handler.\n\n### securityDetails()\n\n```typescript\nawait response.securityDetails(): Promise<Protocol.Network.SecurityDetails | null>\n```\n\nResolves with TLS/security metadata when available (issuer, protocol, validity window). Returns `null` for insecure or non-network responses.\n\n### serverAddr()\n\n```typescript\nawait response.serverAddr(): Promise<{ ipAddress: string; port: number } | null>\n```\n\nProvides the remote IP/port reported by Chrome, when known.\n\n## Header Helpers\n\n### headers()\n\n```typescript\nresponse.headers(): Record<string, string>\n```\n\nReturns a lowercase header map, matching Playwright’s `headers()` behaviour.\n\n### allHeaders()\n\n```typescript\nawait response.allHeaders(): Promise<Record<string, string>>\n```\n\nIncludes additional headers only surfaced via Chrome’s `responseReceivedExtraInfo` event (such as `set-cookie`).\n\n### headerValue()\n\n```typescript\nawait response.headerValue(name: string): Promise<string | null>\n```\n\nReturns a comma-joined string of all values for the specified header. Resolves to `null` when the header is absent.\n\n### headerValues()\n\n```typescript\nawait response.headerValues(name: string): Promise<string[]>\n```\n\nReturns an array of header values, keeping multiple entries separate.\n\n### headersArray()\n\n```typescript\nawait response.headersArray(): Promise<Array<{ name: string; value: string }>>\n```\n\nReturns the header list while preserving the original casing and order reported by the browser.\n\n## Body Helpers\n\n### body()\n\n```typescript\nawait response.body(): Promise<Buffer>\n```\n\nFetches the raw response body. The buffer is base64-decoded for you when Chrome sends it that way.\n\n### text()\n\n```typescript\nawait response.text(): Promise<string>\n```\n\nReturns the response body decoded as UTF-8 text.\n\n### json()\n\n```typescript\nawait response.json<T = unknown>(): Promise<T>\n```\n\nParses the response body as JSON. Throws if the body cannot be parsed or is not valid JSON.\n\n<Note>\nAll body helper calls (`body()`, `text()`, `json()`) only succeed once the browser reports the response body is available. Stagehand handles this timing automatically.\n</Note>\n\n## Completion\n\n### finished()\n\n```typescript\nawait response.finished(): Promise<null | Error>\n```\n\nResolves to `null` when the main navigation request completes successfully, or to an `Error` if Chrome reports `Network.loadingFailed`. This mirrors Playwright’s `response.finished()` contract and is especially helpful for catching late failures such as network resets or blocked responses.\n\n```typescript\nconst result = await response.finished();\nif (result instanceof Error) {\n  console.error(\"Navigation failed\", result.message);\n}\n```\n\n## Usage Patterns\n\n### Inspect status and headers\n\n```typescript\nconst response = await page.goto(\"https://httpbin.org/headers\");\n\nif (response) {\n  console.log(response.status(), response.statusText());\n  const headers = await response.headersArray();\n  headers.forEach(({ name, value }) => {\n    console.log(`${name}: ${value}`);\n  });\n}\n```\n\n### Handle non-network navigations\n\n```typescript\nconst result = await page.goto(\"data:text/html,<p>inline</p>\");\n\nif (result === null) {\n  console.log(\"No network response (data URL)\");\n} else {\n  // Process as usual\n}\n```\n\n### Await completion\n\n```typescript\nconst response = await page.goto(\"https://example.com/slow\");\n\nif (response) {\n  const finished = await response.finished();\n  if (finished instanceof Error) {\n    console.error(\"Navigation failed\", finished.message);\n  }\n}\n```\n\n## Returned From\n\n- `await page.goto(url, options?)`\n- `await page.reload(options?)`\n- `await page.goBack(options?)`\n- `await page.goForward(options?)`\n\nEach method resolves with `Response | null` depending on whether Chrome reported a document-level network response.\n\n## See Also\n\n- [Page reference](/v3/references/page) for details on navigation helpers\n"
  },
  {
    "path": "packages/docs/v3/references/stagehand.mdx",
    "content": "---\ntitle: Stagehand\ndescription: 'Complete API reference for the Stagehand class'\nicon: 'hand-horns'\n---\nimport { V3Banner } from '/snippets/v3-banner.mdx';\n\n<V3Banner />\n\n\n<CardGroup cols={1}>\n<Card title=\"Getting Started\" icon=\"rocket\" href=\"/v3/first-steps/quickstart\">\n  The fastest way to start using Stagehand\n</Card>\n</CardGroup>\n\n## Overview\n\nThe `Stagehand` class is the main entry point for Stagehand v3. It manages browser lifecycle, provides AI-powered automation methods, and handles both local and remote browser environments.\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand(options);\nawait stagehand.init();\n```\n\n## Constructor\n\n### new Stagehand()\n\nCreate a new Stagehand instance.\n\n```typescript\nconst stagehand = new Stagehand(options: V3Options);\n```\n\n**V3Options Interface:**\n```typescript\ninterface V3Options {\n  env: \"LOCAL\" | \"BROWSERBASE\";\n\n  // Browserbase options (required when env = \"BROWSERBASE\")\n  apiKey?: string;\n  projectId?: string;\n  browserbaseSessionID?: string;\n  browserbaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams;\n\n  // Local browser options\n  localBrowserLaunchOptions?: LocalBrowserLaunchOptions;\n\n  // AI/LLM configuration\n  model?: ModelConfiguration;\n  llmClient?: LLMClient;\n  systemPrompt?: string;\n\n  // Behavior options\n  selfHeal?: boolean;\n  experimental?: boolean;\n  domSettleTimeout?: number;\n  cacheDir?: string;\n  keepAlive?: boolean;\n  serverCache?: boolean;\n\n  // Logging options\n  verbose?: 0 | 1 | 2;\n  logInferenceToFile?: boolean;\n  disablePino?: boolean;\n  logger?: (line: LogLine) => void;\n}\n```\n\n### Configuration Parameters\n\n<ParamField path=\"env\" type='\"LOCAL\" | \"BROWSERBASE\"' required>\n  Environment to run the browser in.\n\n  - **`\"LOCAL\"`** - Run browser locally using Chrome/Chromium\n  - **`\"BROWSERBASE\"`** - Run browser on Browserbase cloud platform\n</ParamField>\n\n#### Browserbase Options\n\n<ParamField path=\"apiKey\" type=\"string\" optional>\n  Browserbase API key. Required when `env` is `\"BROWSERBASE\"`.\n\n  Can also be set via `BROWSERBASE_API_KEY` environment variable.\n</ParamField>\n\n<ParamField path=\"projectId\" type=\"string\" optional>\n  Browserbase project ID. Required when `env` is `\"BROWSERBASE\"`.\n\n  Can also be set via `BROWSERBASE_PROJECT_ID` environment variable.\n</ParamField>\n\n<ParamField path=\"browserbaseSessionID\" type=\"string\" optional>\n  Resume an existing Browserbase session by ID instead of creating a new one.\n</ParamField>\n\n<ParamField path=\"browserbaseSessionCreateParams\" type=\"object\" optional>\n  Additional parameters for Browserbase session creation. See [Browserbase documentation](https://docs.browserbase.com) for details.\n</ParamField>\n\n#### Local Browser Options\n\n<ParamField path=\"localBrowserLaunchOptions\" type=\"LocalBrowserLaunchOptions\" optional>\n  Configuration for local Chrome/Chromium browser.\n\n  <Expandable title=\"LocalBrowserLaunchOptions\">\n    <ParamField path=\"headless\" type=\"boolean\" optional>\n      Run browser in headless mode.\n\n      **Default:** `true`\n    </ParamField>\n\n    <ParamField path=\"executablePath\" type=\"string\" optional>\n      Path to Chrome/Chromium executable.\n    </ParamField>\n\n    <ParamField path=\"port\" type=\"number\" optional>\n      Fixed Chrome DevTools Protocol (CDP) debugging port for external tool connections.\n\n      **Default:** Randomly assigned\n    </ParamField>\n\n    <ParamField path=\"args\" type=\"string[]\" optional>\n      Additional Chrome launch arguments.\n    </ParamField>\n\n    <ParamField path=\"userDataDir\" type=\"string\" optional>\n      Path to user data directory for browser profile.\n    </ParamField>\n\n    <ParamField path=\"viewport\" type=\"{ width: number; height: number }\" optional>\n      Default viewport size.\n    </ParamField>\n\n    <ParamField path=\"devtools\" type=\"boolean\" optional>\n      Auto-open DevTools for each tab.\n\n      **Default:** `false`\n    </ParamField>\n\n    <ParamField path=\"proxy\" type=\"object\" optional>\n      Proxy configuration.\n\n      **Properties:** `server`, `bypass`, `username`, `password`\n    </ParamField>\n\n    <ParamField path=\"ignoreHTTPSErrors\" type=\"boolean\" optional>\n      Ignore HTTPS certificate errors.\n\n      **Default:** `false`\n    </ParamField>\n\n    <ParamField path=\"cdpUrl\" type=\"string\" optional>\n      Attach to existing Chrome instance via CDP WebSocket URL.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n#### AI/LLM Configuration\n\n<ParamField path=\"model\" type=\"ModelConfiguration\" optional>\n  Configure the AI model to use for automation. Can be either:\n  - A string in the format `\"provider/model\"` (e.g., `\"openai/gpt-4o\"`, `\"anthropic/claude-sonnet-4-6\"`)\n  - An object with detailed configuration\n\n  <Expandable title=\"Model Configuration Object\">\n    <ParamField path=\"modelName\" type=\"string\" required>\n      The model name (e.g., \"gpt-4o\", \"claude-sonnet-4-6\", \"gemini-2.5-flash\")\n    </ParamField>\n    <ParamField path=\"apiKey\" type=\"string\" optional>\n      API key for the model provider (overrides environment variables)\n    </ParamField>\n    <ParamField path=\"baseURL\" type=\"string\" optional>\n      Base URL for the API endpoint (for custom endpoints or proxies)\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField path=\"llmClient\" type=\"LLMClient\" optional>\n  Provide a custom LLM client implementation instead of using the default.\n</ParamField>\n\n<ParamField path=\"systemPrompt\" type=\"string\" optional>\n  Custom system prompt to guide AI behavior across all operations.\n</ParamField>\n\n#### Behavior Options\n\n<ParamField path=\"selfHeal\" type=\"boolean\" optional>\n  Enable self-healing mode where actions can recover from failures.\n\n  **Default:** `true`\n</ParamField>\n\n<ParamField path=\"experimental\" type=\"boolean\" optional>\n  Enable experimental features (may change between versions).\n\n  **Default:** `false`\n  <Warning>**Use with caution in production**. Experimental features may break or change between versions without notice.</Warning>\n\n</ParamField>\n\n<ParamField path=\"domSettleTimeout\" type=\"number\" optional>\n  Default timeout for waiting for DOM to stabilize (in milliseconds).\n\n  **Default:** `30000`\n</ParamField>\n\n<ParamField path=\"cacheDir\" type=\"string\" optional>\n  Directory path for caching action observations to improve performance.\n</ParamField>\n\n<ParamField path=\"keepAlive\" type=\"boolean\" optional>\n  Controls whether the browser remains running after `stagehand.close()` is called or the parent process exits unexpectedly.\n\n  - **`true`** - Browser continues running independently. On Browserbase, the session stays active. Locally, the Chrome process is kept alive.\n  - **`false`** - Browser is terminated and resources are cleaned up on close or crash.\n\n  When set, this overrides any value in `browserbaseSessionCreateParams.keepAlive`.\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"serverCache\" type=\"boolean\" optional>\n  Enable or disable server-side caching for `act()`, `extract()`, and `observe()` requests. When enabled, repeated calls with the same inputs return instantly without consuming LLM tokens.\n\n  <Note>Only applies when `env` is `\"BROWSERBASE\"`. Has no effect in local environments.</Note>\n\n  Can be overridden per-call via the `serverCache` option on `act()`, `extract()`, and `observe()`.\n\n  **Default:** `true`\n</ParamField>\n\n#### Logging Options\n\n<ParamField path=\"verbose\" type=\"0 | 1 | 2\" optional>\n  Logging verbosity level.\n\n  - **`0`** - Minimal logging\n  - **`1`** - Standard logging (default)\n  - **`2`** - Detailed debug logging\n\n  **Default:** `1`\n</ParamField>\n\n<ParamField path=\"logInferenceToFile\" type=\"boolean\" optional>\n  Log AI inference details to files for debugging.\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"disablePino\" type=\"boolean\" optional>\n  Disable the Pino logging backend (useful for custom logging integrations).\n\n  **Default:** `false`\n</ParamField>\n\n<ParamField path=\"logger\" type=\"(line: LogLine) => void\" optional>\n  Custom logger function to receive log events.\n</ParamField>\n\n## Methods\n\n### init()\n\nInitialize the Stagehand instance and launch the browser.\n\n```typescript\nawait stagehand.init(): Promise<void>\n```\n\n**Must be called before using any other methods.**\n\n### close()\n\nClose the browser and clean up resources.\n\n```typescript\nawait stagehand.close(options?: { force?: boolean }): Promise<void>\n```\n\n<ParamField path=\"force\" type=\"boolean\" optional>\n  Force close even if already closing.\n\n  **Default:** `false`\n</ParamField>\n\n<Note>\nWhen `keepAlive` is `true`, calling `close()` disconnects Stagehand from the browser without terminating it. The browser session continues running independently and can be reconnected to later using `browserbaseSessionID`. When `keepAlive` is `false` (the default), `close()` fully terminates the browser and cleans up all resources.\n</Note>\n\n### agent()\n\nCreate an AI agent instance for autonomous multi-step workflows.\n\n```typescript\nstagehand.agent(config?: AgentConfig): AgentInstance\n```\n\nSee the [agent() reference](/v3/references/agent) for detailed documentation.\n\n## Properties\n\n### page\n\nAccess pages for browser automation. Pages are accessed through the context.\n\n```typescript\n// Get the first page (created automatically on init)\nconst page = stagehand.context.pages()[0];\n\n// Or get the active page\nconst activePage = stagehand.context.activePage();\n\n// Create a new page\nconst newPage = await stagehand.context.newPage();\n```\n\n**Type:** [`Page`](/v3/references/page)\n\nThe page object provides methods for:\n- Navigation (`goto()`, `reload()`, `goBack()`, `goForward()`)\n- Interaction (`click()`, `type()`, `keyPress()`, `locator()`, `deepLocator()`)\n- Inspection (`url()`, `title()`, `screenshot()`)\n- JavaScript evaluation (`evaluate()`)\n\n<Note>\n**Important:** AI-powered methods ([`act()`](/v3/references/act), [`extract()`](/v3/references/extract), [`observe()`](/v3/references/observe)) are called on the stagehand instance, not on the page object.\n</Note>\n\n### context\n\nAccess the browser context for managing multiple pages.\n\n```typescript\nconst context = stagehand.context;\n```\n\n**Type:** `V3Context`\n\nThe context object provides:\n- `newPage()` - Create a new page/tab\n- `pages()` - Get all open pages\n- `setActivePage(page)` - Switch active page\n\n### metrics\n\nGet usage metrics for AI operations.\n\n```typescript\nconst metrics = await stagehand.metrics;\n```\n\n**Returns:** `Promise<StagehandMetrics>`\n\n**StagehandMetrics Interface:**\n```typescript\ninterface StagehandMetrics {\n  // Act metrics\n  actPromptTokens: number;\n  actCompletionTokens: number;\n  actReasoningTokens: number;\n  actCachedInputTokens: number;\n  actInferenceTimeMs: number;\n\n  // Extract metrics\n  extractPromptTokens: number;\n  extractCompletionTokens: number;\n  extractReasoningTokens: number;\n  extractCachedInputTokens: number;\n  extractInferenceTimeMs: number;\n\n  // Observe metrics\n  observePromptTokens: number;\n  observeCompletionTokens: number;\n  observeReasoningTokens: number;\n  observeCachedInputTokens: number;\n  observeInferenceTimeMs: number;\n\n  // Agent metrics\n  agentPromptTokens: number;\n  agentCompletionTokens: number;\n  agentReasoningTokens: number;\n  agentCachedInputTokens: number;\n  agentInferenceTimeMs: number;\n\n  // Totals\n  totalPromptTokens: number;\n  totalCompletionTokens: number;\n  totalReasoningTokens: number;\n  totalCachedInputTokens: number;\n  totalInferenceTimeMs: number;\n}\n```\n\n### history\n\nGet the history of all operations performed.\n\n```typescript\nconst history = await stagehand.history;\n```\n\n**Returns:** `Promise<ReadonlyArray<HistoryEntry>>`\n\n**HistoryEntry Interface:**\n```typescript\ninterface HistoryEntry {\n  method: \"act\" | \"extract\" | \"observe\" | \"navigate\";\n  parameters: unknown;\n  result: unknown;\n  timestamp: string;\n}\n```\n\n### browserbaseSessionID\n\nBrowserbase session identifier for the active Browserbase run.\n\n```typescript\nconst sessionId = stagehand.browserbaseSessionID;\n```\n\n**Type:** `string | undefined` — undefined for LOCAL runs or before `init()`.\n\n### browserbaseSessionURL\n\nShareable link to the active Browserbase session dashboard.\n\n```typescript\nconst sessionUrl = stagehand.browserbaseSessionURL;\n```\n\n**Type:** `string | undefined` — undefined until a Browserbase session is active.\n\n### browserbaseDebugURL\n\nDebugger URL returned by Browserbase for direct CDP inspection.\n\n```typescript\nconst debugUrl = stagehand.browserbaseDebugURL;\n```\n\n**Type:** `string | undefined` — undefined for LOCAL runs or if Browserbase doesn’t provide one.\n\n## Code Examples\n\n<Tabs>\n<Tab title=\"Browserbase\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Remote browser on Browserbase\nconst stagehand = new Stagehand({\n  env: \"BROWSERBASE\",\n  apiKey: process.env.BROWSERBASE_API_KEY,\n  projectId: process.env.BROWSERBASE_PROJECT_ID,\n  model: \"anthropic/claude-sonnet-4-6\"\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\nconst data = await stagehand.extract(\"get page title\", z.object({\n  title: z.string()\n}));\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Local\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\n// Local browser\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  model: \"openai/gpt-4o\"\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\n// Use the page\nawait page.goto(\"https://example.com\");\nawait stagehand.act(\"click the login button\");\n\n// Cleanup\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"Custom Model Config\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  model: {\n    modelName: \"gpt-4o\",\n    apiKey: process.env.OPENAI_API_KEY,\n    baseURL: \"https://custom-proxy.com/v1\"\n  },\n  systemPrompt: \"You are a helpful automation assistant.\",\n  verbose: 2,\n  selfHeal: true\n});\n\nawait stagehand.init();\n```\n\n</Tab>\n<Tab title=\"Multi-Page\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({ env: \"LOCAL\" });\nawait stagehand.init();\n\n// Get the first page\nconst page1 = stagehand.context.pages()[0];\nawait page1.goto(\"https://example.com\");\n\n// Create second page\nconst page2 = await stagehand.context.newPage();\nawait page2.goto(\"https://another-site.com\");\n\n// Switch active page\nstagehand.context.setActivePage(page2);\n\n// Now context.activePage() returns page2\nawait stagehand.act(\"click the button\");\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"With Metrics\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  model: \"openai/gpt-4o\"\n});\n\nawait stagehand.init();\nconst page = stagehand.context.pages()[0];\n\nawait page.goto(\"https://example.com\");\nawait stagehand.act(\"fill out the form\");\nawait stagehand.extract(\"get form data\", schema);\n\n// Get usage metrics\nconst metrics = await stagehand.metrics;\nconsole.log(\"Total tokens used:\", metrics.totalPromptTokens + metrics.totalCompletionTokens);\nconsole.log(\"Act operations:\", {\n  tokens: metrics.actPromptTokens + metrics.actCompletionTokens,\n  time: metrics.actInferenceTimeMs\n});\n\nawait stagehand.close();\n```\n\n</Tab>\n<Tab title=\"With Custom Logger\">\n\n```typescript\nimport { Stagehand } from \"@browserbasehq/stagehand\";\n\nconst stagehand = new Stagehand({\n  env: \"LOCAL\",\n  verbose: 2,\n  logger: (logLine) => {\n    console.log(`[${logLine.category}] ${logLine.message}`);\n    if (logLine.auxiliary) {\n      console.log(\"Details:\", logLine.auxiliary);\n    }\n  }\n});\n\nawait stagehand.init();\n// All operations will now log through your custom logger\n```\n\n</Tab>\n</Tabs>\n\n## Error Handling\n\nStagehand methods may throw the following errors:\n\n- **StagehandInitError** - Failed to initialize Stagehand\n- **StagehandNotInitializedError** - Methods called before `init()`\n- **BrowserbaseSessionNotFoundError** - Browserbase session not found\n- **MissingLLMConfigurationError** - No LLM API key or client configured\n- **MissingEnvironmentVariableError** - Required environment variable not set\n- **StagehandEnvironmentError** - Invalid environment configuration\n\nAlways handle errors appropriately:\n\n```typescript\ntry {\n  const stagehand = new Stagehand({ env: \"LOCAL\" });\n  await stagehand.init();\n  // ... use stagehand\n} catch (error) {\n  console.error(\"Stagehand error:\", error.message);\n} finally {\n  await stagehand?.close();\n}\n```\n\n## Best Practices\n\n1. **Always call `init()`** before using any other methods\n2. **Always call `close()`** when done to clean up resources\n3. **Use try-finally** to ensure cleanup even on errors\n4. **Set appropriate timeouts** based on your use case\n5. **Enable `selfHeal`** for more robust automation\n6. **Use metrics** to monitor token usage and costs\n7. **Configure custom logger** for production debugging\n8. **Cache directory** can significantly improve performance for repeated actions\n\n## Environment Variables\n\nStagehand recognizes the following environment variables:\n\n- `BROWSERBASE_API_KEY` - Browserbase API key\n- `BROWSERBASE_PROJECT_ID` - Browserbase project ID\n- `OPENAI_API_KEY` - OpenAI API key\n- `ANTHROPIC_API_KEY` - Anthropic API key\n- `GOOGLE_API_KEY` - Google AI API key\n\nThese can be overridden by passing values in the constructor options.\n"
  },
  {
    "path": "packages/docs/v3/sdk/go.mdx",
    "content": "---\ntitle: \"Go SDK\"\ndescription: \"Official Stagehand SDK for Go\"\n---\n\n<Note>\n  This documentation is automatically synced from the [Go SDK GitHub repository](https://github.com/browserbase/stagehand-go).\n</Note>\n\n## What is Stagehand?\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## Why Stagehand?\n\nMost existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.\n\n1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.\n\n2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.\n\n3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks.\n\n## Installation\n\n```go\nimport (\n\t\"github.com/browserbase/stagehand-go\" // imported as stagehand\n)\n```\n\nOr to pin the version:\n\n```sh\ngo get -u 'github.com/browserbase/stagehand-go@v0.17.1'\n```\n\n## Requirements\n\nThis library requires Go 1.22+.\n\n## Usage\n\nThe full API of this library can be found in [api.md](https://github.com/browserbase/stagehand-go/blob/main/api.md).\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/browserbase/stagehand-go\"\n\t\"github.com/browserbase/stagehand-go/option\"\n)\n\nfunc main() {\n\t// Create a new Stagehand client with your credentials\n\tclient := stagehand.NewClient(\n\t\toption.WithBrowserbaseAPIKey(\"My Browserbase API Key\"),       // defaults to os.LookupEnv(\"BROWSERBASE_API_KEY\")\n\t\toption.WithBrowserbaseProjectID(\"My Browserbase Project ID\"), // defaults to os.LookupEnv(\"BROWSERBASE_PROJECT_ID\")\n\t\toption.WithModelAPIKey(\"My Model API Key\"),                   // defaults to os.LookupEnv(\"MODEL_API_KEY\")\n\t)\n\n\t// Start a new browser session\n\tstartResponse, err := client.Sessions.Start(context.TODO(), stagehand.SessionStartParams{\n\t\tModelName: \"gpt-5-nano\",\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf(\"Session started: %s\\n\", startResponse.Data.SessionID)\n\n\tsessionID := startResponse.Data.SessionID\n\n\t// Navigate to a webpage\n\t_, err = client.Sessions.Navigate(\n\t\tcontext.TODO(),\n\t\tsessionID,\n\t\tstagehand.SessionNavigateParams{\n\t\t\tURL: \"https://news.ycombinator.com\",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Println(\"Navigated to Hacker News\")\n\n\t// Use Observe to find possible actions on the page\n\tobserveResponse, err := client.Sessions.Observe(\n\t\tcontext.TODO(),\n\t\tsessionID,\n\t\tstagehand.SessionObserveParams{\n\t\t\tInstruction: stagehand.String(\"find the link to view comments for the top post\"),\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tactions := observeResponse.Data.Result\n\tfmt.Printf(\"Found %d possible actions\\n\", len(actions))\n\n\tif len(actions) == 0 {\n\t\tfmt.Println(\"No actions found\")\n\t\treturn\n\t}\n\n\t// Take the first action returned by Observe\n\taction := actions[0]\n\tfmt.Printf(\"Acting on: %s\\n\", action.Description)\n\n\t// Pass the structured action to Act\n\t// The action contains selector, description, method, and arguments\n\tactResponse, err := client.Sessions.Act(\n\t\tcontext.TODO(),\n\t\tsessionID,\n\t\tstagehand.SessionActParams{\n\t\t\tInput: stagehand.SessionActParamsInputUnion{\n\t\t\t\tOfAction: &stagehand.ActionParam{\n\t\t\t\t\tDescription: action.Description,\n\t\t\t\t\tSelector:    action.Selector,\n\t\t\t\t\tMethod:      stagehand.String(action.Method),\n\t\t\t\t\tArguments:   action.Arguments,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf(\"Act completed: %s\\n\", actResponse.Data.Result.Message)\n\n\t// Extract structured data from the page using a JSON schema\n\textractResponse, err := client.Sessions.Extract(\n\t\tcontext.TODO(),\n\t\tsessionID,\n\t\tstagehand.SessionExtractParams{\n\t\t\tInstruction: stagehand.String(\"extract the text of the top comment\"),\n\t\t\tSchema: map[string]any{\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": map[string]any{\n\t\t\t\t\t\"commentText\": map[string]any{\n\t\t\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\t\t\"description\": \"The text content of the top comment\",\n\t\t\t\t\t},\n\t\t\t\t\t\"author\": map[string]any{\n\t\t\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\t\t\"description\": \"The username of the comment author\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf(\"Extracted: %+v\\n\", extractResponse.Data.Result)\n\n\t// Run an autonomous agent to accomplish a goal\n\t// The agent can navigate, click, type, and interact with pages\n\texecuteResponse, err := client.Sessions.Execute(\n\t\tcontext.TODO(),\n\t\tsessionID,\n\t\tstagehand.SessionExecuteParams{\n\t\t\tExecuteOptions: stagehand.SessionExecuteParamsExecuteOptions{\n\t\t\t\tInstruction: \"Find the profile page for the top commenter\",\n\t\t\t\tMaxSteps:    stagehand.Float(10),\n\t\t\t},\n\t\t\tAgentConfig: stagehand.SessionExecuteParamsAgentConfig{\n\t\t\t\t// Model config with provider/model format and API key\n\t\t\t\tModel: stagehand.ModelConfigUnionParam{\n\t\t\t\t\tOfModelConfigModelConfigObject: &stagehand.ModelConfigModelConfigObjectParam{\n\t\t\t\t\t\tModelName: \"openai/gpt-4.1-mini\",\n\t\t\t\t\t\tAPIKey:    stagehand.String(\"sk-your-api-key\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCua: stagehand.Bool(false),\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf(\"Agent result: %s\\n\", executeResponse.Data.Result.Message)\n\n\t// End the session to clean up resources\n\t_, err = client.Sessions.End(\n\t\tcontext.TODO(),\n\t\tsessionID,\n\t\tstagehand.SessionEndParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Println(\"Session ended\")\n}\n```\n\n### Running the example\n\nA complete working example is available in `examples/basic.go`. To run it:\n\n1. **Set up environment variables** by creating a `.env` file in the repository root:\n\n```bash\nBROWSERBASE_API_KEY=your_browserbase_api_key\nBROWSERBASE_PROJECT_ID=your_browserbase_project_id\nMODEL_API_KEY=your_openai_api_key\n```\n\nYou can get your Browserbase API key and project ID from the [Browserbase dashboard](https://www.browserbase.com/).\n\n2. **Install dependencies**:\n\n```bash\ngo mod tidy\n```\n\n3. **Run the example**:\n\n```bash\ngo run examples/basic.go\n```\n\nThe example demonstrates the full Stagehand workflow: starting a session, navigating to a page, observing actions, clicking elements, extracting data, and running an autonomous agent.\n\n### Request fields\n\nThe stagehand library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson)\nsemantics from the Go 1.24+ `encoding/json` release for request fields.\n\nRequired primitive fields (`int64`, `string`, etc.) feature the tag `json:\"...,required\"`. These\nfields are always serialized, even their zero values.\n\nOptional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `stagehand.String(string)`, `stagehand.Int(int64)`, etc.\n\nAny `param.Opt[T]`, map, slice, struct or string enum uses the\ntag `json:\"...,omitzero\"`. Its zero value is considered omitted.\n\nThe `param.IsOmitted(any)` function can confirm the presence of any `omitzero` field.\n\n```go\np := stagehand.ExampleParams{\n\tID:   \"id_xxx\",                // required property\n\tName: stagehand.String(\"...\"), // optional property\n\n\tPoint: stagehand.Point{\n\t\tX: 0,                // required field will serialize as 0\n\t\tY: stagehand.Int(1), // optional field will serialize as 1\n\t\t// ... omitted non-required fields will not be serialized\n\t},\n\n\tOrigin: stagehand.Origin{}, // the zero value of [Origin] is considered omitted\n}\n```\n\nTo send `null` instead of a `param.Opt[T]`, use `param.Null[T]()`.\nTo send `null` instead of a struct `T`, use `param.NullStruct[T]()`.\n\n```go\np.Name = param.Null[string]()       // 'null' instead of string\np.Point = param.NullStruct[Point]() // 'null' instead of struct\n\nparam.IsNull(p.Name)  // true\nparam.IsNull(p.Point) // true\n```\n\nRequest structs contain a `.SetExtraFields(map[string]any)` method which can send non-conforming\nfields in the request body. Extra fields overwrite any struct fields with a matching\nkey. For security reasons, only use `SetExtraFields` with trusted data.\n\nTo send a custom value instead of a struct, use `param.Override[T](https://github.com/browserbase/stagehand-go/blob/main/value)`.\n\n```go\n// In cases where the API specifies a given type,\n// but you want to send something else, use [SetExtraFields]:\np.SetExtraFields(map[string]any{\n\t\"x\": 0.01, // send \"x\" as a float instead of int\n})\n\n// Send a number instead of an object\ncustom := param.Override[stagehand.FooParams](https://github.com/browserbase/stagehand-go/blob/main/12)\n```\n\n### Request unions\n\nUnions are represented as a struct with fields prefixed by \"Of\" for each of its variants,\nonly one field can be non-zero. The non-zero field will be serialized.\n\nSub-properties of the union can be accessed via methods on the union struct.\nThese methods return a mutable pointer to the underlying data, if present.\n\n```go\n// Only one field can be non-zero, use param.IsOmitted() to check if a field is set\ntype AnimalUnionParam struct {\n\tOfCat *Cat `json:\",omitzero,inline`\n\tOfDog *Dog `json:\",omitzero,inline`\n}\n\nanimal := AnimalUnionParam{\n\tOfCat: &Cat{\n\t\tName: \"Whiskers\",\n\t\tOwner: PersonParam{\n\t\t\tAddress: AddressParam{Street: \"3333 Coyote Hill Rd\", Zip: 0},\n\t\t},\n\t},\n}\n\n// Mutating a field\nif address := animal.GetOwner().GetAddress(); address != nil {\n\taddress.ZipCode = 94304\n}\n```\n\n### Response objects\n\nAll fields in response structs are ordinary value types (not pointers or wrappers).\nResponse structs also include a special `JSON` field containing metadata about\neach property.\n\n```go\ntype Animal struct {\n\tName   string `json:\"name,nullable\"`\n\tOwners int    `json:\"owners\"`\n\tAge    int    `json:\"age\"`\n\tJSON   struct {\n\t\tName        respjson.Field\n\t\tOwner       respjson.Field\n\t\tAge         respjson.Field\n\t\tExtraFields map[string]respjson.Field\n\t} `json:\"-\"`\n}\n```\n\nTo handle optional data, use the `.Valid()` method on the JSON field.\n`.Valid()` returns true if a field is not `null`, not present, or couldn't be marshaled.\n\nIf `.Valid()` is false, the corresponding field will simply be its zero value.\n\n```go\nraw := `{\"owners\": 1, \"name\": null}`\n\nvar res Animal\njson.Unmarshal([]byte(raw), &res)\n\n// Accessing regular fields\n\nres.Owners // 1\nres.Name   // \"\"\nres.Age    // 0\n\n// Optional field checks\n\nres.JSON.Owners.Valid() // true\nres.JSON.Name.Valid()   // false\nres.JSON.Age.Valid()    // false\n\n// Raw JSON values\n\nres.JSON.Owners.Raw()                  // \"1\"\nres.JSON.Name.Raw() == \"null\"          // true\nres.JSON.Name.Raw() == respjson.Null   // true\nres.JSON.Age.Raw() == \"\"               // true\nres.JSON.Age.Raw() == respjson.Omitted // true\n```\n\nThese `.JSON` structs also include an `ExtraFields` map containing\nany properties in the json response that were not specified\nin the struct. This can be useful for API features not yet\npresent in the SDK.\n\n```go\nbody := res.JSON.ExtraFields[\"my_unexpected_field\"].Raw()\n```\n\n### Response Unions\n\nIn responses, unions are represented by a flattened struct containing all possible fields from each of the\nobject variants.\nTo convert it to a variant use the `.AsFooVariant()` method or the `.AsAny()` method if present.\n\nIf a response value union contains primitive values, primitive fields will be alongside\nthe properties but prefixed with `Of` and feature the tag `json:\"...,inline\"`.\n\n```go\ntype AnimalUnion struct {\n\t// From variants [Dog], [Cat]\n\tOwner Person `json:\"owner\"`\n\t// From variant [Dog]\n\tDogBreed string `json:\"dog_breed\"`\n\t// From variant [Cat]\n\tCatBreed string `json:\"cat_breed\"`\n\t// ...\n\n\tJSON struct {\n\t\tOwner respjson.Field\n\t\t// ...\n\t} `json:\"-\"`\n}\n\n// If animal variant\nif animal.Owner.Address.ZipCode == \"\" {\n\tpanic(\"missing zip code\")\n}\n\n// Switch on the variant\nswitch variant := animal.AsAny().(type) {\ncase Dog:\ncase Cat:\ndefault:\n\tpanic(\"unexpected type\")\n}\n```\n\n### RequestOptions\n\nThis library uses the functional options pattern. Functions defined in the\n`option` package return a `RequestOption`, which is a closure that mutates a\n`RequestConfig`. These options can be supplied to the client or at individual\nrequests. For example:\n\n```go\nclient := stagehand.NewClient(\n\t// Adds a header to every request made by the client\n\toption.WithHeader(\"X-Some-Header\", \"custom_header_info\"),\n)\n\nclient.Sessions.Start(context.TODO(), ...,\n\t// Override the header\n\toption.WithHeader(\"X-Some-Header\", \"some_other_custom_header_info\"),\n\t// Add an undocumented field to the request body, using sjson syntax\n\toption.WithJSONSet(\"some.json.path\", map[string]string{\"my\": \"object\"}),\n)\n```\n\nThe request option `option.WithDebugLog(nil)` may be helpful while debugging.\n\nSee the [full list of request options](https://pkg.go.dev/github.com/browserbase/stagehand-go/option).\n\n### Errors\n\nWhen the API returns a non-success status code, we return an error with type\n`*stagehand.Error`. This contains the `StatusCode`, `*http.Request`, and\n`*http.Response` values of the request, as well as the JSON of the error body\n(much like other response objects in the SDK).\n\nTo handle errors, we recommend that you use the `errors.As` pattern:\n\n```go\n_, err := client.Sessions.Start(context.TODO(), stagehand.SessionStartParams{\n\tModelName: \"openai/gpt-5-nano\",\n})\nif err != nil {\n\tvar apierr *stagehand.Error\n\tif errors.As(err, &apierr) {\n\t\tprintln(string(apierr.DumpRequest(true)))  // Prints the serialized HTTP request\n\t\tprintln(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response\n\t}\n\tpanic(err.Error()) // GET \"/v1/sessions/start\": 400 Bad Request { ... }\n}\n```\n\nWhen other errors occur, they are returned unwrapped; for example,\nif HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.\n\n### Timeouts\n\nRequests do not time out by default; use context to configure a timeout for a request lifecycle.\n\nNote that if a request is [retried](#retries), the context timeout does not start over.\nTo set a per-retry timeout, use `option.WithRequestTimeout()`.\n\n```go\n// This sets the timeout for the request, including all the retries.\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\ndefer cancel()\nclient.Sessions.Start(\n\tctx,\n\tstagehand.SessionStartParams{\n\t\tModelName: \"openai/gpt-5-nano\",\n\t},\n\t// This sets the per-retry timeout\n\toption.WithRequestTimeout(20*time.Second),\n)\n```\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\nWe retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,\nand >=500 Internal errors.\n\nYou can use the `WithMaxRetries` option to configure or disable this:\n\n```go\n// Configure the default for all requests:\nclient := stagehand.NewClient(\n\toption.WithMaxRetries(0), // default is 2\n)\n\n// Override per-request:\nclient.Sessions.Start(\n\tcontext.TODO(),\n\tstagehand.SessionStartParams{\n\t\tModelName: \"openai/gpt-5-nano\",\n\t},\n\toption.WithMaxRetries(5),\n)\n```\n\n### Accessing raw response data (e.g. response headers)\n\nYou can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when\nyou need to examine response headers, status codes, or other details.\n\n```go\n// Create a variable to store the HTTP response\nvar response *http.Response\nresponse, err := client.Sessions.Start(\n\tcontext.TODO(),\n\tstagehand.SessionStartParams{\n\t\tModelName: \"openai/gpt-5-nano\",\n\t},\n\toption.WithResponseInto(&response),\n)\nif err != nil {\n\t// handle error\n}\nfmt.Printf(\"%+v\\n\", response)\n\nfmt.Printf(\"Status Code: %d\\n\", response.StatusCode)\nfmt.Printf(\"Headers: %+#v\\n\", response.Header)\n```\n\n### Making custom/undocumented requests\n\nThis library is typed for convenient access to the documented API. If you need to access undocumented\nendpoints, params, or response properties, the library can still be used.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs.\n`RequestOptions` on the client, such as retries, will be respected when making these requests.\n\n```go\nvar (\n    // params can be an io.Reader, a []byte, an encoding/json serializable object,\n    // or a \"…Params\" struct defined in this library.\n    params map[string]any\n\n    // result can be an []byte, *http.Response, a encoding/json deserializable object,\n    // or a model defined in this library.\n    result *http.Response\n)\nerr := client.Post(context.Background(), \"/unspecified\", params, &result)\nif err != nil {\n    …\n}\n```\n\n#### Undocumented request params\n\nTo make requests using undocumented parameters, you may use either the `option.WithQuerySet()`\nor the `option.WithJSONSet()` methods.\n\n```go\nparams := FooNewParams{\n    ID:   \"id_xxxx\",\n    Data: FooNewParamsData{\n        FirstName: stagehand.String(\"John\"),\n    },\n}\nclient.Foo.New(context.Background(), params, option.WithJSONSet(\"data.last_name\", \"Doe\"))\n```\n\n#### Undocumented response properties\n\nTo access undocumented response properties, you may either access the raw JSON of the response as a string\nwith `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with\n`result.JSON.Foo.Raw()`.\n\nAny fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.\n\n### Middleware\n\nWe provide `option.WithMiddleware` which applies the given\nmiddleware to requests.\n\n```go\nfunc Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {\n\t// Before the request\n\tstart := time.Now()\n\tLogReq(req)\n\n\t// Forward the request to the next handler\n\tres, err = next(req)\n\n\t// Handle stuff after the request\n\tend := time.Now()\n\tLogRes(res, err, start - end)\n\n    return res, err\n}\n\nclient := stagehand.NewClient(\n\toption.WithMiddleware(Logger),\n)\n```\n\nWhen multiple middlewares are provided as variadic arguments, the middlewares\nare applied left to right. If `option.WithMiddleware` is given\nmultiple times, for example first in the client then the method, the\nmiddleware in the client will run first and the middleware given in the method\nwill run next.\n\nYou may also replace the default `http.Client` with\n`option.WithHTTPClient(client)`. Only one http client is\naccepted (this overwrites any previous client) and receives requests after any\nmiddleware has been applied.\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n2. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/browserbase/stagehand-go/issues) with questions, bugs, or suggestions.\n\n## Contributing\n\nSee [the contributing documentation](https://github.com/browserbase/stagehand-go/blob/main/./CONTRIBUTING.md)."
  },
  {
    "path": "packages/docs/v3/sdk/java.mdx",
    "content": "---\ntitle: \"Java SDK\"\ndescription: \"Official Stagehand SDK for Java\"\n---\n\n<Note>\n  This documentation is automatically synced from the [Java SDK GitHub repository](https://github.com/browserbase/stagehand-java).\n</Note>\n\n## What is Stagehand?\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## Why Stagehand?\n\nMost existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.\n\n1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.\n\n2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.\n\n3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks.\n\n## Installation\n\n### Gradle\n\n```java\nimplementation(\"com.browserbase.api:stagehand-java:0.6.1\")\n```\n\n### Maven\n\n```xml\n<dependency>\n  <groupId>com.browserbase.api</groupId>\n  <artifactId>stagehand-java</artifactId>\n  <version>0.6.1</version>\n</dependency>\n```\n\n## Requirements\n\nThis library requires Java 8 through Java 21. Java 22+ is not currently supported.\n\n## Running the Example\n\nA complete working example is available at [`stagehand-java-example/src/main/java/com/stagehand/api/example/Main.java`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-example/src/main/java/com/stagehand/api/example/Main.java).\n\nTo run it, first export the required environment variables, then use Gradle:\n\n```bash\nexport BROWSERBASE_API_KEY=\"your-bb-api-key\"\nexport BROWSERBASE_PROJECT_ID=\"your-bb-project-uuid\"\nexport MODEL_API_KEY=\"sk-proj-your-llm-api-key\"\n\n./gradlew :stagehand-java-example:run\n```\n\n## Usage\n\nThis example demonstrates the full Stagehand workflow: starting a session, navigating to a page, observing possible actions, acting on elements, extracting data, and running an autonomous agent.\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\nimport com.browserbase.api.core.JsonValue;\nimport com.browserbase.api.models.sessions.*;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class Main {\n    public static void main(String[] args) {\n        // Create client using environment variables:\n        // BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY\n        StagehandClient client = StagehandOkHttpClient.fromEnv();\n\n        // Start a new browser session\n        SessionStartResponse startResponse = client.sessions().start(\n            SessionStartParams.builder()\n                .modelName(\"openai/gpt-5-nano\")\n                .build()\n        );\n\n        String sessionId = startResponse.data().sessionId();\n        System.out.println(\"Session started: \" + sessionId);\n\n        try {\n            // Navigate to a webpage\n            client.sessions().navigate(\n                SessionNavigateParams.builder()\n                    .id(sessionId)\n                    .url(\"https://news.ycombinator.com\")\n                    .build()\n            );\n            System.out.println(\"Navigated to Hacker News\");\n\n            // Observe to find possible actions on the page\n            SessionObserveResponse observeResponse = client.sessions().observe(\n                SessionObserveParams.builder()\n                    .id(sessionId)\n                    .instruction(\"find the link to view comments for the top post\")\n                    .build()\n            );\n\n            List<SessionObserveResponse.Data.Result> results = observeResponse.data().result();\n            System.out.println(\"Found \" + results.size() + \" possible actions\");\n\n            if (results.isEmpty()) {\n                System.out.println(\"No actions found\");\n                return;\n            }\n\n            // Take the first action returned by Observe\n            // Convert the result to an Action to pass to Act\n            SessionObserveResponse.Data.Result result = results.get(0);\n            Action action = JsonValue.from(result).convert(Action.class);\n            System.out.println(\"Acting on: \" + action.description());\n\n            // Pass the structured action to Act\n            SessionActResponse actResponse = client.sessions().act(\n                SessionActParams.builder()\n                    .id(sessionId)\n                    .input(action)\n                    .build()\n            );\n            System.out.println(\"Act completed: \" + actResponse.data().result().message());\n\n            // Extract structured data from the page using a JSON schema\n            SessionExtractResponse extractResponse = client.sessions().extract(\n                SessionExtractParams.builder()\n                    .id(sessionId)\n                    .instruction(\"extract the text of the top comment on this page\")\n                    .schema(SessionExtractParams.Schema.builder()\n                        .putAdditionalProperty(\"type\", JsonValue.from(\"object\"))\n                        .putAdditionalProperty(\"properties\", JsonValue.from(Map.of(\n                            \"commentText\", Map.of(\n                                \"type\", \"string\",\n                                \"description\", \"The text content of the top comment\"\n                            ),\n                            \"author\", Map.of(\n                                \"type\", \"string\",\n                                \"description\", \"The username of the comment author\"\n                            )\n                        )))\n                        .putAdditionalProperty(\"required\", JsonValue.from(List.of(\"commentText\")))\n                        .build())\n                    .build()\n            );\n\n            JsonValue extractedResult = extractResponse.data()._result();\n            System.out.println(\"Extracted data: \" + extractedResult);\n\n            // Get the author from the extracted data\n            String author = extractedResult.asObject()\n                .flatMap(obj -> Optional.ofNullable(obj.get(\"author\")))\n                .flatMap(JsonValue::asString)\n                .orElse(\"unknown\");\n            System.out.println(\"Looking up profile for author: \" + author);\n\n            // Run an autonomous agent to accomplish a complex task\n            SessionExecuteResponse executeResponse = client.sessions().execute(\n                SessionExecuteParams.builder()\n                    .id(sessionId)\n                    .executeOptions(SessionExecuteParams.ExecuteOptions.builder()\n                        .instruction(String.format(\n                            \"Find any personal website, GitHub, or LinkedIn profile for user '%s'. \" +\n                            \"Click on their username to view their profile page.\",\n                            author\n                        ))\n                        .maxSteps(10.0)\n                        .build())\n                    .agentConfig(SessionExecuteParams.AgentConfig.builder()\n                        .model(ModelConfig.ofModelConfigObject(\n                            ModelConfig.ModelConfigObject.builder()\n                                .modelName(\"openai/gpt-5-nano\")\n                                .apiKey(System.getenv(\"MODEL_API_KEY\"))\n                                .build()\n                        ))\n                        .cua(false)\n                        .build())\n                    .build()\n            );\n\n            System.out.println(\"Agent completed: \" + executeResponse.data().result().message());\n            System.out.println(\"Agent success: \" + executeResponse.data().result().success());\n\n        } finally {\n            // End the browser session to clean up resources\n            client.sessions().end(\n                SessionEndParams.builder()\n                    .id(sessionId)\n                    .build()\n            );\n            System.out.println(\"Session ended\");\n        }\n    }\n}\n```\n\n## Client configuration\n\nConfigure the client using system properties or environment variables:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\n\n// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties\n// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables\nStagehandClient client = StagehandOkHttpClient.fromEnv();\n```\n\nOr manually:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .browserbaseApiKey(\"My Browserbase API Key\")\n    .browserbaseProjectId(\"My Browserbase Project ID\")\n    .modelApiKey(\"My Model API Key\")\n    .build();\n```\n\nOr using a combination of the two approaches:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    // Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties\n    // Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables\n    .fromEnv()\n    .browserbaseApiKey(\"My Browserbase API Key\")\n    .build();\n```\n\nSee this table for the available options:\n\n| Setter                 | System property                  | Environment variable     | Required | Default value                             |\n| ---------------------- | -------------------------------- | ------------------------ | -------- | ----------------------------------------- |\n| `browserbaseApiKey`    | `stagehand.browserbaseApiKey`    | `BROWSERBASE_API_KEY`    | true     | -                                         |\n| `browserbaseProjectId` | `stagehand.browserbaseProjectId` | `BROWSERBASE_PROJECT_ID` | true     | -                                         |\n| `modelApiKey`          | `stagehand.modelApiKey`          | `MODEL_API_KEY`          | true     | -                                         |\n| `baseUrl`              | `stagehand.baseUrl`              | `STAGEHAND_BASE_URL`     | true     | `\"https://api.stagehand.browserbase.com\"` |\n\nSystem properties take precedence over environment variables.\n\n> [!TIP]\n> Don't create more than one client in the same application. Each client has a connection pool and\n> thread pools, which are more efficient to share between requests.\n\n### Modifying configuration\n\nTo temporarily use a modified client configuration, while reusing the same connection and thread pools, call `withOptions()` on any client or service:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\n\nStagehandClient clientWithOptions = client.withOptions(optionsBuilder -> {\n    optionsBuilder.modelApiKey(\"sk-your-llm-api-key-here\");\n    optionsBuilder.maxRetries(42);\n});\n```\n\nThe `withOptions()` method does not affect the original client or service.\n\n## Requests and responses\n\nTo send a request to the Stagehand API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.\n\nFor example, `client.sessions().act(...)` should be called with an instance of `SessionActParams`, and it will return an instance of `SessionActResponse`.\n\n## Immutability\n\nEach class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.\n\nEach class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.\n\nBecause each class is immutable, builder modification will _never_ affect already built class instances.\n\n## Asynchronous execution\n\nThe default client is synchronous. To switch to asynchronous execution, call the `async()` method:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\nimport com.browserbase.api.models.sessions.SessionActParams;\nimport com.browserbase.api.models.sessions.SessionActResponse;\nimport java.util.concurrent.CompletableFuture;\n\n// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties\n// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables\nStagehandClient client = StagehandOkHttpClient.fromEnv();\n\nSessionActParams params = SessionActParams.builder()\n    .id(\"00000000-your-session-id-000000000000\")\n    .input(\"click the first link on the page\")\n    .build();\nCompletableFuture<SessionActResponse> response = client.async().sessions().act(params);\n```\n\nOr create an asynchronous client from the beginning:\n\n```java\nimport com.browserbase.api.client.StagehandClientAsync;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClientAsync;\nimport com.browserbase.api.models.sessions.SessionActParams;\nimport com.browserbase.api.models.sessions.SessionActResponse;\nimport java.util.concurrent.CompletableFuture;\n\n// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties\n// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables\nStagehandClientAsync client = StagehandOkHttpClientAsync.fromEnv();\n\nSessionActParams params = SessionActParams.builder()\n    .id(\"00000000-your-session-id-000000000000\")\n    .input(\"click the first link on the page\")\n    .build();\nCompletableFuture<SessionActResponse> response = client.sessions().act(params);\n```\n\nThe asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.\n\n## Streaming\n\nThe SDK defines methods that return response \"chunk\" streams, where each chunk can be individually processed as soon as it arrives instead of waiting on the full response. Streaming methods generally correspond to [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) or [JSONL](https://jsonlines.org) responses.\n\nSome of these methods may have streaming and non-streaming variants, but a streaming method will always have a `Streaming` suffix in its name, even if it doesn't have a non-streaming variant.\n\nThese streaming methods return [`StreamResponse`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/StreamResponse.kt) for synchronous clients:\n\n```java\nimport com.browserbase.api.core.http.StreamResponse;\nimport com.browserbase.api.models.sessions.StreamEvent;\n\ntry (StreamResponse<StreamEvent> streamResponse = client.sessions().actStreaming(params)) {\n    streamResponse.stream().forEach(chunk -> {\n        System.out.println(chunk);\n    });\n    System.out.println(\"No more chunks!\");\n}\n```\n\nOr [`AsyncStreamResponse`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/AsyncStreamResponse.kt) for asynchronous clients:\n\n```java\nimport com.browserbase.api.core.http.AsyncStreamResponse;\nimport com.browserbase.api.models.sessions.StreamEvent;\nimport java.util.Optional;\n\nclient.async().sessions().actStreaming(params).subscribe(chunk -> {\n    System.out.println(chunk);\n});\n\n// If you need to handle errors or completion of the stream\nclient.async().sessions().actStreaming(params).subscribe(new AsyncStreamResponse.Handler<>() {\n    @Override\n    public void onNext(StreamEvent chunk) {\n        System.out.println(chunk);\n    }\n\n    @Override\n    public void onComplete(Optional<Throwable> error) {\n        if (error.isPresent()) {\n            System.out.println(\"Something went wrong!\");\n            throw new RuntimeException(error.get());\n        } else {\n            System.out.println(\"No more chunks!\");\n        }\n    }\n});\n\n// Or use futures\nclient.async().sessions().actStreaming(params)\n    .subscribe(chunk -> {\n        System.out.println(chunk);\n    })\n    .onCompleteFuture();\n    .whenComplete((unused, error) -> {\n        if (error != null) {\n            System.out.println(\"Something went wrong!\");\n            throw new RuntimeException(error);\n        } else {\n            System.out.println(\"No more chunks!\");\n        }\n    });\n```\n\nAsync streaming uses a dedicated per-client cached thread pool [`Executor`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) to stream without blocking the current thread. This default is suitable for most purposes.\n\nTo use a different `Executor`, configure the subscription using the `executor` parameter:\n\n```java\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.Executors;\n\nExecutor executor = Executors.newFixedThreadPool(4);\nclient.async().sessions().actStreaming(params).subscribe(\n    chunk -> System.out.println(chunk), executor\n);\n```\n\nOr configure the client globally using the `streamHandlerExecutor` method:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\nimport java.util.concurrent.Executors;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .fromEnv()\n    .streamHandlerExecutor(Executors.newFixedThreadPool(4))\n    .build();\n```\n\n## Raw responses\n\nThe SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.\n\nTo access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:\n\n```java\nimport com.browserbase.api.core.http.Headers;\nimport com.browserbase.api.core.http.HttpResponseFor;\nimport com.browserbase.api.models.sessions.SessionStartParams;\nimport com.browserbase.api.models.sessions.SessionStartResponse;\n\nSessionStartParams params = SessionStartParams.builder()\n    .modelName(\"openai/gpt-5-nano\")\n    .build();\nHttpResponseFor<SessionStartResponse> response = client.sessions().withRawResponse().start(params);\n\nint statusCode = response.statusCode();\nHeaders headers = response.headers();\n```\n\nYou can still deserialize the response into an instance of a Java class if needed:\n\n```java\nimport com.browserbase.api.models.sessions.SessionStartResponse;\n\nSessionStartResponse parsedResponse = response.parse();\n```\n\n## Error handling\n\nThe SDK throws custom unchecked exception types:\n\n- [`StagehandServiceException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:\n\n  | Status | Exception                                                                                                                          |\n  | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |\n  | 400    | [`BadRequestException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/BadRequestException.kt)                     |\n  | 401    | [`UnauthorizedException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnauthorizedException.kt)                 |\n  | 403    | [`PermissionDeniedException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/PermissionDeniedException.kt)         |\n  | 404    | [`NotFoundException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/NotFoundException.kt)                         |\n  | 422    | [`UnprocessableEntityException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnprocessableEntityException.kt)   |\n  | 429    | [`RateLimitException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/RateLimitException.kt)                       |\n  | 5xx    | [`InternalServerException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/InternalServerException.kt)             |\n  | others | [`UnexpectedStatusCodeException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnexpectedStatusCodeException.kt) |\n\n  [`SseException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/SseException.kt) is thrown for errors encountered during [SSE streaming](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) after a successful initial HTTP response.\n\n- [`StagehandIoException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandIoException.kt): I/O networking errors.\n\n- [`StagehandRetryableException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandRetryableException.kt): Generic error indicating a failure that could be retried by the client.\n\n- [`StagehandInvalidDataException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.\n\n- [`StagehandException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.\n\n## Logging\n\nThe SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).\n\nEnable logging by setting the `STAGEHAND_LOG` environment variable to `info`:\n\n```sh\nexport STAGEHAND_LOG=info\n```\n\nOr to `debug` for more verbose logging:\n\n```sh\nexport STAGEHAND_LOG=debug\n```\n\n## ProGuard and R8\n\nAlthough the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `stagehand-java-core` is published with a [configuration file](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).\n\nProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.\n\n## Jackson\n\nThe SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.\n\nThe SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).\n\nIf the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`StagehandOkHttpClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) or [`StagehandOkHttpClientAsync`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt).\n\n> [!CAUTION]\n> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.\n\n## Network options\n\n### Retries\n\nThe SDK automatically retries 2 times by default, with a short exponential backoff between requests.\n\nOnly the following error types are retried:\n\n- Connection errors (for example, due to a network connectivity problem)\n- 408 Request Timeout\n- 409 Conflict\n- 429 Rate Limit\n- 5xx Internal\n\nThe API may also explicitly instruct the SDK to retry or not retry a request.\n\nTo set a custom number of retries, configure the client using the `maxRetries` method:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .fromEnv()\n    .maxRetries(4)\n    .build();\n```\n\n### Timeouts\n\nRequests time out after 1 minute by default.\n\nTo set a custom timeout, configure the method call using the `timeout` method:\n\n```java\nimport com.browserbase.api.models.sessions.SessionStartResponse;\n\nSessionStartResponse response = client.sessions().start(\n  params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()\n);\n```\n\nOr configure the default for all method calls at the client level:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\nimport java.time.Duration;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .fromEnv()\n    .timeout(Duration.ofSeconds(30))\n    .build();\n```\n\n### Proxies\n\nTo route requests through a proxy, configure the client using the `proxy` method:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .fromEnv()\n    .proxy(new Proxy(\n      Proxy.Type.HTTP, new InetSocketAddress(\n        \"https://example.com\", 8080\n      )\n    ))\n    .build();\n```\n\n### HTTPS\n\n> [!NOTE]\n> Most applications should not call these methods, and instead use the system defaults. The defaults include\n> special optimizations that can be lost if the implementations are modified.\n\nTo configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .fromEnv()\n    // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa.\n    .sslSocketFactory(yourSSLSocketFactory)\n    .trustManager(yourTrustManager)\n    .hostnameVerifier(yourHostnameVerifier)\n    .build();\n```\n\n### Custom HTTP client\n\nThe SDK consists of three artifacts:\n\n- `stagehand-java-core`\n  - Contains core SDK logic\n  - Does not depend on [OkHttp](https://square.github.io/okhttp)\n  - Exposes [`StagehandClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClient.kt), [`StagehandClientAsync`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsync.kt), [`StagehandClientImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt), and [`StagehandClientAsyncImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), all of which can work with any HTTP client\n- `stagehand-java-client-okhttp`\n  - Depends on [OkHttp](https://square.github.io/okhttp)\n  - Exposes [`StagehandOkHttpClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) and [`StagehandOkHttpClientAsync`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt), which provide a way to construct [`StagehandClientImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt) and [`StagehandClientAsyncImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), respectively, using OkHttp\n- `stagehand-java`\n  - Depends on and exposes the APIs of both `stagehand-java-core` and `stagehand-java-client-okhttp`\n  - Does not have its own logic\n\nThis structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.\n\n#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html)\n\n> [!TIP]\n> Try the available [network options](#network-options) before replacing the default client.\n\nTo use a customized `OkHttpClient`:\n\n1. Replace your [`stagehand-java` dependency](#installation) with `stagehand-java-core`\n2. Copy `stagehand-java-client-okhttp`'s [`OkHttpClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt) class into your code and customize it\n3. Construct [`StagehandClientImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt) or [`StagehandClientAsyncImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), similarly to [`StagehandOkHttpClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) or [`StagehandOkHttpClientAsync`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt), using your customized client\n\n### Completely custom HTTP client\n\nTo use a completely custom HTTP client:\n\n1. Replace your [`stagehand-java` dependency](#installation) with `stagehand-java-core`\n2. Write a class that implements the [`HttpClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpClient.kt) interface\n3. Construct [`StagehandClientImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt) or [`StagehandClientAsyncImpl`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), similarly to [`StagehandOkHttpClient`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) or [`StagehandOkHttpClientAsync`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt), using your new client class\n\n## Undocumented API functionality\n\nThe SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.\n\n### Parameters\n\nTo set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:\n\n```java\nimport com.browserbase.api.core.JsonValue;\nimport com.browserbase.api.models.sessions.SessionActParams;\n\nSessionActParams params = SessionActParams.builder()\n    .putAdditionalHeader(\"Secret-Header\", \"42\")\n    .putAdditionalQueryParam(\"secret_query_param\", \"42\")\n    .putAdditionalBodyProperty(\"secretProperty\", JsonValue.from(\"42\"))\n    .build();\n```\n\nThese can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.\n\nTo set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:\n\n```java\nimport com.browserbase.api.core.JsonValue;\nimport com.browserbase.api.models.sessions.SessionActParams;\n\nSessionActParams params = SessionActParams.builder()\n    .options(SessionActParams.Options.builder()\n        .putAdditionalProperty(\"secretProperty\", JsonValue.from(\"42\"))\n        .build())\n    .build();\n```\n\nThese properties can be accessed on the nested built object later using the `_additionalProperties()` method.\n\nTo set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt) object to its setter:\n\n```java\nimport com.browserbase.api.core.JsonValue;\nimport com.browserbase.api.models.sessions.SessionActParams;\n\nSessionActParams params = SessionActParams.builder()\n    .input(JsonValue.from(42))\n    .build();\n```\n\nThe most straightforward way to create a [`JsonValue`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt) is using its `from(...)` method:\n\n```java\nimport com.browserbase.api.core.JsonValue;\nimport java.util.List;\nimport java.util.Map;\n\n// Create primitive JSON values\nJsonValue nullValue = JsonValue.from(null);\nJsonValue booleanValue = JsonValue.from(true);\nJsonValue numberValue = JsonValue.from(42);\nJsonValue stringValue = JsonValue.from(\"Hello World!\");\n\n// Create a JSON array value equivalent to `[\"Hello\", \"World\"]`\nJsonValue arrayValue = JsonValue.from(List.of(\n  \"Hello\", \"World\"\n));\n\n// Create a JSON object value equivalent to `{ \"a\": 1, \"b\": 2 }`\nJsonValue objectValue = JsonValue.from(Map.of(\n  \"a\", 1,\n  \"b\", 2\n));\n\n// Create an arbitrarily nested JSON equivalent to:\n// {\n//   \"a\": [1, 2],\n//   \"b\": [3, 4]\n// }\nJsonValue complexValue = JsonValue.from(Map.of(\n  \"a\", List.of(\n    1, 2\n  ),\n  \"b\", List.of(\n    3, 4\n  )\n));\n```\n\nNormally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.\n\nTo forcibly omit a required parameter or property, pass [`JsonMissing`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt):\n\n```java\nimport com.browserbase.api.core.JsonMissing;\nimport com.browserbase.api.models.sessions.SessionActParams;\n\nSessionActParams params = SessionActParams.builder()\n    .input(\"Click the login button\")\n    .id(JsonMissing.of())\n    .build();\n```\n\n### Response properties\n\nTo access undocumented response properties, call the `_additionalProperties()` method:\n\n```java\nimport com.browserbase.api.core.JsonValue;\nimport java.util.Map;\n\nMap<String, JsonValue> additionalProperties = client.sessions().act(params)._additionalProperties();\nJsonValue secretPropertyValue = additionalProperties.get(\"secretProperty\");\n\nString result = secretPropertyValue.accept(new JsonValue.Visitor<>() {\n    @Override\n    public String visitNull() {\n        return \"It's null!\";\n    }\n\n    @Override\n    public String visitBoolean(boolean value) {\n        return \"It's a boolean!\";\n    }\n\n    @Override\n    public String visitNumber(Number value) {\n        return \"It's a number!\";\n    }\n\n    // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`\n    // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden\n});\n```\n\nTo access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:\n\n```java\nimport com.browserbase.api.core.JsonField;\nimport com.browserbase.api.models.sessions.SessionActParams;\nimport java.util.Optional;\n\nJsonField<SessionActParams.Input> input = client.sessions().act(params)._input();\n\nif (input.isMissing()) {\n  // The property is absent from the JSON response\n} else if (input.isNull()) {\n  // The property was set to literal null\n} else {\n  // Check if value was provided as a string\n  // Other methods include `asNumber()`, `asBoolean()`, etc.\n  Optional<String> jsonString = input.asString();\n\n  // Try to deserialize into a custom type\n  MyClass myObject = input.asUnknown().orElseThrow().convert(MyClass.class);\n}\n```\n\n### Response validation\n\nIn rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.\n\nBy default, the SDK will not throw an exception in this case. It will throw [`StagehandInvalidDataException`](https://github.com/browserbase/stagehand-java/blob/main/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt) only if you directly access the property.\n\nIf you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:\n\n```java\nimport com.browserbase.api.models.sessions.SessionActResponse;\n\nSessionActResponse response = client.sessions().act(params).validate();\n```\n\nOr configure the method call to validate the response using the `responseValidation` method:\n\n```java\nimport com.browserbase.api.models.sessions.SessionActResponse;\n\nSessionActResponse response = client.sessions().act(\n  params, RequestOptions.builder().responseValidation(true).build()\n);\n```\n\nOr configure the default for all method calls at the client level:\n\n```java\nimport com.browserbase.api.client.StagehandClient;\nimport com.browserbase.api.client.okhttp.StagehandOkHttpClient;\n\nStagehandClient client = StagehandOkHttpClient.builder()\n    .fromEnv()\n    .responseValidation(true)\n    .build();\n```\n\n## FAQ\n\n### Why don't you use plain `enum` classes?\n\nJava `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.\n\n### Why do you represent fields using `JsonField<T>` instead of just plain `T`?\n\nUsing `JsonField<T>` enables a few features:\n\n- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)\n- Lazily [validating the API response against the expected shape](#response-validation)\n- Representing absent vs explicitly null values\n\n### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?\n\nIt is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.\n\n### Why don't you use checked exceptions?\n\nChecked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.\n\nChecked exceptions:\n\n- Are verbose to handle\n- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error\n- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)\n- Don't play well with lambdas (also due to the function coloring problem)\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n2. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/browserbase/stagehand-java/issues) with questions, bugs, or suggestions."
  },
  {
    "path": "packages/docs/v3/sdk/python.mdx",
    "content": "---\ntitle: \"Python SDK\"\ndescription: \"Official Stagehand SDK for Python\"\n---\n\n<Note>\n  This documentation is automatically synced from the [Python SDK GitHub repository](https://github.com/browserbase/stagehand-python).\n</Note>\n\n<Note>\n  Migrating from the old v2 Python SDK? See our [migration guide here](/v3/migrations/python).\n</Note>\n\n## What is Stagehand?\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## Why Stagehand?\n\nMost existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.\n\n1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.\n\n2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.\n\n3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks.\n\n## Installation\n\n```sh\nuv pip install stagehand\n```\n\nFor local development or when working from this repository, sync the dependency lockfile with `uv` (see the Local development section below) before running project scripts.\n\n## Requirements\n\nPython 3.9 or higher.\n\n## Running the Example\n\nA complete working example is available at [`examples/full_example.py`](https://github.com/browserbase/stagehand-python/blob/main/examples/full_example.py).\n\nTo run it, first export the required environment variables, then use Python:\n\n```bash\nexport BROWSERBASE_API_KEY=\"your-bb-api-key\"\nexport BROWSERBASE_PROJECT_ID=\"your-bb-project-uuid\"\nexport MODEL_API_KEY=\"sk-proj-your-llm-api-key\"\n\nuv run python examples/full_example.py\n```\n\n## Local mode example\n\nIf you want to run Stagehand locally, use the local example (`examples/local_example.py`). It shows how to configure the client for `server=\"local\"`.\n\nLocal mode runs Stagehand’s embedded server and launches a **local Chrome/Chromium** browser (it is **not bundled** with the Python wheel), so you must have Chrome installed on the machine running the example.\n\nIf Chrome is installed but Stagehand can’t find it, set `CHROME_PATH` to your browser executable (or pass `browser.launchOptions.executablePath` when starting the session).\n\nCommon Windows paths:\n- `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe`\n- `C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe`\n\nPowerShell:\n\n```powershell\n# optional if you don't already have Chrome installed\nwinget install -e --id Google.Chrome\n\n# optional if Stagehand can't auto-detect Chrome\n$env:CHROME_PATH=\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\"\n\nuv run python examples/local_example.py\n```\n\n```bash\nuv pip install stagehand\nuv run python examples/local_example.py\n```\n\n## Streaming logging example\n\nSee [`examples/logging_example.py`](https://github.com/browserbase/stagehand-python/blob/main/examples/logging_example.py) for a remote-only flow that streams `StreamEvent`s with `verbose=2`, `stream_response=True`, and `x_stream_response=\"true\"` so you can watch the SDK’s logs as they arrive.\n\n```bash\nuv run python examples/logging_example.py\n```\n\n<details>\n<summary><strong>Local development</strong></summary>\n\nThis repository relies on `uv` to install the sanctioned Python version and dependencies. After cloning, bootstrap the environment with:\n\n```sh\n./scripts/bootstrap\n```\nOnce the environment is ready, execute repo scripts with `uv run`:\n\n```sh\nuv run python examples/full_example.py\n```\n</details>\n\n## Usage\n\nThis example demonstrates the full Stagehand workflow: starting a session, navigating to a page, observing possible actions, acting on elements, extracting data, and running an autonomous agent.\n\n```python\nimport asyncio\n\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    # Create client using environment variables:\n    # BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY\n    client = AsyncStagehand()\n\n    # Start a new browser session (returns a session helper bound to a session_id)\n    session = await client.sessions.create(model_name=\"openai/gpt-5-nano\")\n\n    print(f\"Session started: {session.id}\")\n\n    try:\n        # Navigate to a webpage\n        await session.navigate(\n            url=\"https://news.ycombinator.com\",\n        )\n        print(\"Navigated to Hacker News\")\n\n        # Observe to find possible actions on the page\n        observe_response = await session.observe(\n            instruction=\"find the link to view comments for the top post\",\n        )\n\n        results = observe_response.data.result\n        print(f\"Found {len(results)} possible actions\")\n        if not results:\n            return\n\n        # Take the first action returned by Observe and pass it to Act\n        action = results[0].to_dict(exclude_none=True)\n        print(\"Acting on:\", action.get(\"description\"))\n\n        act_response = await session.act(input=action)\n        print(\"Act completed:\", act_response.data.result.message)\n\n        # Extract structured data from the page using a JSON schema\n        extract_response = await session.extract(\n            instruction=\"extract the text of the top comment on this page\",\n            schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"commentText\": {\"type\": \"string\"},\n                    \"author\": {\"type\": \"string\"},\n                },\n                \"required\": [\"commentText\"],\n            },\n        )\n\n        extracted = extract_response.data.result\n        author = extracted.get(\"author\", \"unknown\") if isinstance(extracted, dict) else \"unknown\"\n        print(\"Extracted author:\", author)\n\n        # Run an autonomous agent to accomplish a complex task\n        execute_response = await session.execute(\n            execute_options={\n                \"instruction\": f\"Find any personal website, GitHub, or LinkedIn profile for the Hacker News user '{author}'.\",\n                \"max_steps\": 10,\n            },\n            agent_config={\"model\": \"openai/gpt-5-nano\"},\n            timeout=300.0,\n        )\n\n        print(\"Agent completed:\", execute_response.data.result.message)\n        print(\"Agent success:\", execute_response.data.result.success)\n    finally:\n        # End the browser session to clean up resources\n        await session.end()\n        print(\"Session ended\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Client configuration\n\nConfigure the client using environment variables:\n\n```python\nfrom stagehand import AsyncStagehand\n\nclient = AsyncStagehand()\n```\n\nOr manually:\n\n```python\nfrom stagehand import AsyncStagehand\n\nclient = AsyncStagehand(\n    browserbase_api_key=\"My Browserbase API Key\",\n    browserbase_project_id=\"My Browserbase Project ID\",\n    model_api_key=\"My Model API Key\",\n)\n```\n\nOr using a combination of the two approaches:\n\n```python\nfrom stagehand import AsyncStagehand\n\nclient = AsyncStagehand(\n    # Configures using environment variables\n    browserbase_api_key=\"My Browserbase API Key\",  # override just this one\n)\n```\n\nSee this table for the available options:\n\n| Keyword argument         | Environment variable     | Required | Default value                             |\n| ------------------------ | ------------------------ | -------- | ----------------------------------------- |\n| `browserbase_api_key`    | `BROWSERBASE_API_KEY`    | true     | -                                         |\n| `browserbase_project_id` | `BROWSERBASE_PROJECT_ID` | true     | -                                         |\n| `model_api_key`          | `MODEL_API_KEY`          | true     | -                                         |\n| `base_url`               | `STAGEHAND_BASE_URL`     | false    | `\"https://api.stagehand.browserbase.com\"` |\n\nKeyword arguments take precedence over environment variables.\n\n> [!TIP]\n> Don't create more than one client in the same application. Each client has a connection pool, which is more efficient to share between requests.\n\n### Modifying configuration\n\nTo temporarily use a modified client configuration while reusing the same connection pool, call `with_options()` on any client:\n\n```python\nclient_with_options = client.with_options(model_api_key=\"sk-your-llm-api-key-here\", max_retries=42)\n```\n\nThe `with_options()` method does not affect the original client.\n\n## Requests and responses\n\nTo send a request to the Stagehand API, call the corresponding client method using keyword arguments.\n\nNested request parameters are dictionaries typed using [`TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods like:\n\n- Serializing back into JSON: `model.to_json()`\n- Converting to a dictionary: `model.to_dict()`\n\n## Immutability\n\nResponse objects are Pydantic models. If you want to build a modified copy, prefer `model.model_copy(update={...})` (Pydantic v2) rather than mutating in place.\n\n## Asynchronous execution\n\nThis SDK recommends using `AsyncStagehand` and `await`ing each API call:\n\n```python\nimport asyncio\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    client = AsyncStagehand()\n    session = await client.sessions.create(model_name=\"openai/gpt-5-nano\")\n    response = await session.act(input=\"click the first link on the page\")\n    print(response.data)\n\nasyncio.run(main())\n```\n\n### With aiohttp\n\nBy default, the async client uses `httpx` for HTTP requests. For improved concurrency performance you may also use `aiohttp` as the HTTP backend.\n\nInstall `aiohttp`:\n\n```sh\nuv run pip install stagehand[aiohttp]\n```\n\nThen instantiate the client with `http_client=DefaultAioHttpClient()`:\n\n```python\nimport asyncio\nfrom stagehand import AsyncStagehand, DefaultAioHttpClient\n\nasync def main() -> None:\n    async with AsyncStagehand(http_client=DefaultAioHttpClient()) as client:\n        session = await client.sessions.create(model_name=\"openai/gpt-5-nano\")\n        response = await session.act(input=\"click the first link on the page\")\n        print(response.data)\n\nasyncio.run(main())\n```\n\n## Streaming responses\n\nWe provide support for streaming responses using Server-Sent Events (SSE).\n\nTo enable SSE streaming, you must:\n\n1. Ask the server to stream by setting `x_stream_response=\"true\"` (header), and\n2. Tell the client to parse an SSE stream by setting `stream_response=True`.\n\n```python\nimport asyncio\n\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    async with AsyncStagehand() as client:\n        session = await client.sessions.create(model_name=\"openai/gpt-5-nano\")\n\n        stream = await client.sessions.act(\n            id=session.id,\n            input=\"click the first link on the page\",\n            stream_response=True,\n            x_stream_response=\"true\",\n        )\n        async for event in stream:\n            # event is a StreamEvent (type: \"system\" | \"log\")\n            print(event.type, event.data)\n\nasyncio.run(main())\n```\n\n## Raw responses\n\nThe SDK defines methods that deserialize responses into Pydantic models. However, these methods don't provide access to response headers, status code, or the raw response body.\n\nTo access this data, prefix any HTTP method call on a client or service with `with_raw_response`:\n\n```python\nimport asyncio\n\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    async with AsyncStagehand() as client:\n        response = await client.sessions.with_raw_response.start(model_name=\"openai/gpt-5-nano\")\n        print(response.headers.get(\"X-My-Header\"))\n\n        session = response.parse()  # get the object that `sessions.start()` would have returned\n        print(session.data)\n\nasyncio.run(main())\n```\n\n### `.with_streaming_response`\n\nThe `with_raw_response` interface eagerly reads the full response body when you make the request.\n\nTo stream the response body (not SSE), use `with_streaming_response` instead. It requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`.\n\n```python\nimport asyncio\n\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    async with AsyncStagehand() as client:\n        async with client.sessions.with_streaming_response.start(model_name=\"openai/gpt-5-nano\") as response:\n            print(response.headers.get(\"X-My-Header\"))\n            async for line in response.iter_lines():\n                print(line)\n\nasyncio.run(main())\n```\n\n## Error handling\n\nWhen the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `stagehand.APIConnectionError` is raised.\n\nWhen the API returns a non-success status code (that is, 4xx or 5xx response), a subclass of `stagehand.APIStatusError` is raised, containing `status_code` and `response` properties.\n\nAll errors inherit from `stagehand.APIError`.\n\n```python\nimport asyncio\n\nimport stagehand\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    async with AsyncStagehand() as client:\n        try:\n            await client.sessions.start(model_name=\"openai/gpt-5-nano\")\n        except stagehand.APIConnectionError as e:\n            print(\"The server could not be reached\")\n            print(e.__cause__)  # an underlying Exception, likely raised within httpx.\n        except stagehand.RateLimitError:\n            print(\"A 429 status code was received; we should back off a bit.\")\n        except stagehand.APIStatusError as e:\n            print(\"A non-200-range status code was received\")\n            print(e.status_code)\n            print(e.response)\n\nasyncio.run(main())\n```\n\nError codes are as follows:\n\n| Status Code | Error Type                 |\n| ----------- | -------------------------- |\n| 400         | `BadRequestError`          |\n| 401         | `AuthenticationError`      |\n| 403         | `PermissionDeniedError`    |\n| 404         | `NotFoundError`            |\n| 422         | `UnprocessableEntityError` |\n| 429         | `RateLimitError`           |\n| >=500       | `InternalServerError`      |\n| N/A         | `APIConnectionError`       |\n\n### Retries\n\nCertain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default.\n\nYou can use the `max_retries` option to configure or disable retry settings:\n\n```python\nimport asyncio\n\nfrom stagehand import AsyncStagehand\n\nasync def main() -> None:\n    async with AsyncStagehand(max_retries=0) as client:\n        # Or, configure per-request:\n        await client.with_options(max_retries=5).sessions.start(model_name=\"openai/gpt-5-nano\")\n\nasyncio.run(main())\n```\n\n### Timeouts\n\nBy default requests time out after 1 minute. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object.\n\nOn timeout, an `APITimeoutError` is thrown. Note that requests that time out are [retried twice by default](#retries).\n\n## Logging\n\nThe SDK uses the standard library [`logging`](https://docs.python.org/3/library/logging.html) module.\n\nEnable logging by setting the `STAGEHAND_LOG` environment variable to `info`:\n\n```sh\nexport STAGEHAND_LOG=info\n```\n\nOr to `debug` for more verbose logging:\n\n```sh\nexport STAGEHAND_LOG=debug\n```\n\n## Undocumented API functionality\n\nThis library is typed for convenient access to the documented API, but you can still access undocumented endpoints, request params, or response properties when needed.\n\n### Undocumented endpoints\n\nTo make requests to undocumented endpoints, use `client.get`, `client.post`, and other HTTP verbs. Client options (such as retries) are respected.\n\n```python\nimport httpx\nfrom stagehand import AsyncStagehand\n\nimport asyncio\n\nasync def main() -> None:\n    async with AsyncStagehand() as client:\n        response = await client.post(\"/foo\", cast_to=httpx.Response, body={\"my_param\": True})\n        print(response.headers.get(\"x-foo\"))\n\nasyncio.run(main())\n```\n\n### Undocumented request params\n\nTo send extra params that aren't available as keyword args, use `extra_query`, `extra_body`, and `extra_headers`.\n\n### Undocumented response properties\n\nTo access undocumented response properties, you can access extra fields like `response.unknown_prop`. You can also get all extra fields as a dict with [`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra).\n\n## Response validation\n\nIn rare cases, the API may return a response that doesn't match the expected type.\n\nBy default, the SDK is permissive and will only raise an error if you later try to use the invalid data.\n\nIf you would prefer to validate responses upfront, instantiate the client with `_strict_response_validation=True`. An `APIResponseValidationError` will be raised if the API responds with invalid data for the expected schema.\n\n```python\nimport asyncio\n\nfrom stagehand import APIResponseValidationError, AsyncStagehand\n\ntry:\n    async def main() -> None:\n        async with AsyncStagehand(_strict_response_validation=True) as client:\n            await client.sessions.start(model_name=\"openai/gpt-5-nano\")\n\n    asyncio.run(main())\nexcept APIResponseValidationError as e:\n    print(\"Response failed schema validation:\", e)\n```\n\n## FAQ\n\n### Why are some values typed as `Literal[...]` instead of Python `Enum`s?\n\nUsing `Literal[...]` types is forwards compatible: the API can introduce new enum values without breaking older SDKs at runtime.\n\n### How can I tell whether `None` means `null` or “missing” in a response?\n\nIn an API response, a field may be explicitly `null`, or missing entirely; in either case its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:\n\n```python\nif response.my_field is None:\n    if \"my_field\" not in response.model_fields_set:\n        print('Got json like {}, without a \"my_field\" key present at all.')\n    else:\n        print('Got json like {\"my_field\": null}.')\n```\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes that only affect static types, without breaking runtime behavior.\n2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n3. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/browserbase/stagehand-python/issues) with questions, bugs, or suggestions.\n\n### Determining the installed version\n\nIf you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version.\n\nYou can determine the version that is being used at runtime with:\n\n```python\nimport stagehand\n\nprint(stagehand.__version__)\n```"
  },
  {
    "path": "packages/docs/v3/sdk/ruby.mdx",
    "content": "---\ntitle: \"Ruby SDK\"\ndescription: \"Official Stagehand SDK for Ruby\"\n---\n\n<Note>\n  This documentation is automatically synced from the [Ruby SDK GitHub repository](https://github.com/browserbase/stagehand-ruby).\n</Note>\n\n## What is Stagehand?\n\nStagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.\n\n## Why Stagehand?\n\nMost existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.\n\n1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.\n\n2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.\n\n3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks.\n\n## Installation\n\nTo use this gem, install via Bundler by adding the following to your application's `Gemfile`:\n\n```ruby\ngem \"stagehand\", \"~> 0.6.2\"\n```\n\n## Usage\n\n```ruby\nrequire \"bundler/setup\"\nrequire \"stagehand\"\n\n# Create a new Stagehand client with your credentials\nclient = Stagehand::Client.new(\n  browserbase_api_key: ENV[\"BROWSERBASE_API_KEY\"],      # defaults to ENV[\"BROWSERBASE_API_KEY\"]\n  browserbase_project_id: ENV[\"BROWSERBASE_PROJECT_ID\"], # defaults to ENV[\"BROWSERBASE_PROJECT_ID\"]\n  model_api_key: ENV[\"MODEL_API_KEY\"]                   # defaults to ENV[\"MODEL_API_KEY\"]\n)\n\n# Start a new browser session\nstart_response = client.sessions.start(\n  model_name: \"openai/gpt-5-nano\"\n)\nputs \"Session started: #{start_response.data.session_id}\"\n\nsession_id = start_response.data.session_id\n\n# Navigate to a webpage\nclient.sessions.navigate(\n  session_id,\n  url: \"https://news.ycombinator.com\"\n)\nputs \"Navigated to Hacker News\"\n\n# Use Observe to find possible actions on the page\nobserve_response = client.sessions.observe(\n  session_id,\n  instruction: \"find the link to view comments for the top post\"\n)\n\nactions = observe_response.data.result\nputs \"Found #{actions.length} possible actions\"\n\n# Take the first action returned by Observe\naction = actions.first\nputs \"Acting on: #{action.description}\"\n\n# Pass the structured action to Act\n# Convert the observe result to a hash and ensure method is set to \"click\"\nact_response = client.sessions.act(\n  session_id,\n  input: action.to_h.merge(method: \"click\")\n)\nputs \"Act completed: #{act_response.data.result[:message]}\"\n\n# Extract data from the page\n# We're now on the comments page, so extract the top comment text\nextract_response = client.sessions.extract(\n  session_id,\n  instruction: \"extract the text of the top comment on this page\",\n  schema: {\n    type: \"object\",\n    properties: {\n      comment_text: {\n        type: \"string\",\n        description: \"The text content of the top comment\"\n      },\n      author: {\n        type: \"string\",\n        description: \"The username of the comment author\"\n      }\n    },\n    required: [\"comment_text\"]\n  }\n)\nputs \"Extracted data: #{extract_response.data.result}\"\n\n# Get the author from the extracted data\nextracted_data = extract_response.data.result\nauthor = extracted_data[:author]\nputs \"Looking up profile for author: #{author}\"\n\n# Use the Agent to find the author's profile\n# Execute runs an autonomous agent that can navigate and interact with pages\nexecute_response = client.sessions.execute(\n  session_id,\n  execute_options: {\n    instruction: \"Find any personal website, GitHub, LinkedIn, or other best profile URL for the Hacker News user '#{author}'. \" \\\n                 \"Click on their username to go to their profile page and look for any links they have shared.\",\n    max_steps: 15\n  },\n  agent_config: {\n    model: Stagehand::ModelConfig::ModelConfigObject.new(\n      model_name: \"openai/gpt-5-nano\",\n      api_key: ENV[\"MODEL_API_KEY\"]\n    ),\n    cua: false\n  }\n)\nputs \"Agent completed: #{execute_response.data.result[:message]}\"\nputs \"Agent success: #{execute_response.data.result[:success]}\"\nputs \"Agent actions taken: #{execute_response.data.result[:actions]&.length || 0}\"\n\n# End the session to cleanup browser resources\nclient.sessions.end_(session_id)\nputs \"Session ended\"\n```\n\n### Running the Example\n\nSet the required environment variables and run the example script:\n\n```bash\n# Set your credentials\nexport BROWSERBASE_API_KEY=\"your-browserbase-api-key\"\nexport BROWSERBASE_PROJECT_ID=\"your-browserbase-project-id\"\nexport MODEL_API_KEY=\"your-openai-api-key\"\n\n# Install dependencies and run\nbundle install\nbundle exec ruby examples/basic.rb\n```\n\n### Streaming\n\nWe provide support for streaming responses using Server-Sent Events (SSE).\n\n```ruby\nstream = stagehand.sessions.act_streaming(\n  \"00000000-your-session-id-000000000000\",\n  input: \"click the first link on the page\"\n)\n\nstream.each do |session|\n  puts(session.data)\nend\n```\n\n### Handling errors\n\nWhen the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Stagehand::Errors::APIError` will be thrown:\n\n```ruby\nbegin\n  session = stagehand.sessions.start(model_name: \"openai/gpt-5-nano\")\nrescue Stagehand::Errors::APIConnectionError => e\n  puts(\"The server could not be reached\")\n  puts(e.cause)  # an underlying Exception, likely raised within `net/http`\nrescue Stagehand::Errors::RateLimitError => e\n  puts(\"A 429 status code was received; we should back off a bit.\")\nrescue Stagehand::Errors::APIStatusError => e\n  puts(\"Another non-200-range status code was received\")\n  puts(e.status)\nend\n```\n\nError codes are as follows:\n\n| Cause            | Error Type                 |\n| ---------------- | -------------------------- |\n| HTTP 400         | `BadRequestError`          |\n| HTTP 401         | `AuthenticationError`      |\n| HTTP 403         | `PermissionDeniedError`    |\n| HTTP 404         | `NotFoundError`            |\n| HTTP 409         | `ConflictError`            |\n| HTTP 422         | `UnprocessableEntityError` |\n| HTTP 429         | `RateLimitError`           |\n| HTTP >= 500      | `InternalServerError`      |\n| Other HTTP error | `APIStatusError`           |\n| Timeout          | `APITimeoutError`          |\n| Network error    | `APIConnectionError`       |\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\n\nConnection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, >=500 Internal errors, and timeouts will all be retried by default.\n\nYou can use the `max_retries` option to configure or disable this:\n\n```ruby\n# Configure the default for all requests:\nstagehand = Stagehand::Client.new(\n  max_retries: 0 # default is 2\n)\n\n# Or, configure per-request:\nstagehand.sessions.start(model_name: \"openai/gpt-5-nano\", request_options: {max_retries: 5})\n```\n\n### Timeouts\n\nBy default, requests will time out after 60 seconds. You can use the timeout option to configure or disable this:\n\n```ruby\n# Configure the default for all requests:\nstagehand = Stagehand::Client.new(\n  timeout: nil # default is 60\n)\n\n# Or, configure per-request:\nstagehand.sessions.start(model_name: \"openai/gpt-5-nano\", request_options: {timeout: 5})\n```\n\nOn timeout, `Stagehand::Errors::APITimeoutError` is raised.\n\nNote that requests that time out are retried by default.\n\n## Advanced concepts\n\n### BaseModel\n\nAll parameter and response objects inherit from `Stagehand::Internal::Type::BaseModel`, which provides several conveniences, including:\n\n1. All fields, including unknown ones, are accessible with `obj[:prop]` syntax, and can be destructured with `obj => {prop: prop}` or pattern-matching syntax.\n\n2. Structural equivalence for equality; if two API calls return the same values, comparing the responses with == will return true.\n\n3. Both instances and the classes themselves can be pretty-printed.\n\n4. Helpers such as `#to_h`, `#deep_to_h`, `#to_json`, and `#to_yaml`.\n\n### Making custom or undocumented requests\n\n#### Undocumented properties\n\nYou can send undocumented parameters to any endpoint, and read undocumented response properties, like so:\n\nNote: the `extra_` parameters of the same name overrides the documented parameters.\n\n```ruby\nresponse =\n  stagehand.sessions.start(\n    model_name: \"openai/gpt-5-nano\",\n    request_options: {\n      extra_query: {my_query_parameter: value},\n      extra_body: {my_body_parameter: value},\n      extra_headers: {\"my-header\": value}\n    }\n  )\n\nputs(response[:my_undocumented_property])\n```\n\n#### Undocumented request params\n\nIf you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` under the `request_options:` parameter when making a request, as seen in the examples above.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints while retaining the benefit of auth, retries, and so on, you can make requests using `client.request`, like so:\n\n```ruby\nresponse = client.request(\n  method: :post,\n  path: '/undocumented/endpoint',\n  query: {\"dog\": \"woof\"},\n  headers: {\"useful-header\": \"interesting-value\"},\n  body: {\"hello\": \"world\"}\n)\n```\n\n### Concurrency & connection pooling\n\nThe `Stagehand::Client` instances are threadsafe, but are only are fork-safe when there are no in-flight HTTP requests.\n\nEach instance of `Stagehand::Client` has its own HTTP connection pool with a default size of 99. As such, we recommend instantiating the client once per application in most settings.\n\nWhen all available connections from the pool are checked out, requests wait for a new connection to become available, with queue time counting towards the request timeout.\n\nUnless otherwise specified, other classes in the SDK do not have locks protecting their underlying data structure.\n\n## Sorbet\n\nThis library provides comprehensive [RBI](https://sorbet.org/docs/rbi) definitions, and has no dependency on sorbet-runtime.\n\nYou can provide typesafe request parameters like so:\n\n```ruby\nstagehand.sessions.act(\"00000000-your-session-id-000000000000\", input: \"click the first link on the page\")\n```\n\nOr, equivalently:\n\n```ruby\n# Hashes work, but are not typesafe:\nstagehand.sessions.act(\"00000000-your-session-id-000000000000\", input: \"click the first link on the page\")\n\n# You can also splat a full Params class:\nparams = Stagehand::SessionActParams.new(input: \"click the first link on the page\")\nstagehand.sessions.act(\"00000000-your-session-id-000000000000\", **params)\n```\n\n### Enums\n\nSince this library does not depend on `sorbet-runtime`, it cannot provide [`T::Enum`](https://sorbet.org/docs/tenum) instances. Instead, we provide \"tagged symbols\" instead, which is always a primitive at runtime:\n\n```ruby\n# :typescript\nputs(Stagehand::SessionActParams::XLanguage::TYPESCRIPT)\n\n# Revealed type: `T.all(Stagehand::SessionActParams::XLanguage, Symbol)`\nT.reveal_type(Stagehand::SessionActParams::XLanguage::TYPESCRIPT)\n```\n\nEnum parameters have a \"relaxed\" type, so you can either pass in enum constants or their literal value:\n\n```ruby\n# Using the enum constants preserves the tagged type information:\nstagehand.sessions.act(\n  x_language: Stagehand::SessionActParams::XLanguage::TYPESCRIPT,\n  # …\n)\n\n# Literal values are also permissible:\nstagehand.sessions.act(\n  x_language: :typescript,\n  # …\n)\n```\n\n## Versioning\n\nThis package follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions. As the library is in initial development and has a major version of `0`, APIs may change at any time.\n\nThis package considers improvements to the (non-runtime) `*.rbi` and `*.rbs` type definitions to be non-breaking changes.\n\n## Requirements\n\nRuby 3.2.0 or higher.\n\n## Contributing\n\nSee [the contributing documentation](https://github.com/browserbase/stagehand-ruby/tree/main/CONTRIBUTING.md)."
  },
  {
    "path": "packages/evals/CHANGELOG.md",
    "content": "# @browserbasehq/stagehand-evals\n\n## 1.1.9\n\n### Patch Changes\n\n- Updated dependencies [[`505e8c6`](https://github.com/browserbase/stagehand/commit/505e8c6736f3706328dbc8df670c49a018058388), [`2f43ffa`](https://github.com/browserbase/stagehand/commit/2f43ffac11778152d17e4c44405770cc32c3ec8c), [`63ee247`](https://github.com/browserbase/stagehand/commit/63ee247ac6bf2992046d4f6b2759f46b15643e36), [`7dc35f5`](https://github.com/browserbase/stagehand/commit/7dc35f5e25689e6518d68b25ef71536d2781c8aa), [`335cf47`](https://github.com/browserbase/stagehand/commit/335cf4730e73bce33e92331d04bda4b0fd42685d), [`6ba0a1d`](https://github.com/browserbase/stagehand/commit/6ba0a1db7fc2d5d5a2f8927b1417d8f1d15eda10), [`4ff3bb8`](https://github.com/browserbase/stagehand/commit/4ff3bb831a6ef6e2d57148e7afb68ea8d23e395d), [`c27054b`](https://github.com/browserbase/stagehand/commit/c27054bbd0508431ade91d655f89efc87bbf5867), [`2abf5b9`](https://github.com/browserbase/stagehand/commit/2abf5b90f1e2bb1442509ef3a686b6128c9cdcf6), [`7817fcc`](https://github.com/browserbase/stagehand/commit/7817fcc315eee4455ce04567cf56c9ec801caf0b), [`7390508`](https://github.com/browserbase/stagehand/commit/73905088c5ed5923d276da9cce2efd0a0a3a46eb), [`611f43a`](https://github.com/browserbase/stagehand/commit/611f43ac8d4c580216d55d2b217c14a9a9c11013), [`521a10e`](https://github.com/browserbase/stagehand/commit/521a10e3698fc5631e219947bc90dad0f8bddaa8), [`2402a3c`](https://github.com/browserbase/stagehand/commit/2402a3c4d50270391b3e6440f4385cdcf5e1eb64)]:\n  - @browserbasehq/stagehand@3.2.0\n\n## 1.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`7584f3e`](https://github.com/browserbase/stagehand/commit/7584f3e92e60a557d2b3e0e0d2a2af04c3527523), [`1e1c9c1`](https://github.com/browserbase/stagehand/commit/1e1c9c15773e49d5c3cd36021dbc1d23495c1bce), [`6bef890`](https://github.com/browserbase/stagehand/commit/6bef89090ebd231e77d8092b2c32a0f06303d5a9), [`ffd4b33`](https://github.com/browserbase/stagehand/commit/ffd4b335a873d0f4dcd76ea22d44f47919bf8e49), [`677bff5`](https://github.com/browserbase/stagehand/commit/677bff5834c879a2d95f7dbff918b8e1510516b3), [`65ff464`](https://github.com/browserbase/stagehand/commit/65ff464bc13388eb109eba0a2cf533c1cc202854), [`101bcf2`](https://github.com/browserbase/stagehand/commit/101bcf2da8b527fd6ace6aa291ada5d0f2d90344), [`0a94301`](https://github.com/browserbase/stagehand/commit/0a94301caa991d1aa4cdade6e28a065b1aefb3e2), [`b27c04d`](https://github.com/browserbase/stagehand/commit/b27c04d278c290364347acd0c354a878ea9b7c2d), [`afbd08b`](https://github.com/browserbase/stagehand/commit/afbd08bb6367a9c9f65f67e453667987e4659918), [`e3db9aa`](https://github.com/browserbase/stagehand/commit/e3db9aa863f44270792215801fe6e3a02a1321aa), [`0e8d569`](https://github.com/browserbase/stagehand/commit/0e8d5695f662040f7384e64f46301152802e3c62), [`ff0f979`](https://github.com/browserbase/stagehand/commit/ff0f9795f3b2c1cf4f2610a80ebcb3341a24f987), [`2d89d2b`](https://github.com/browserbase/stagehand/commit/2d89d2b35ce812431956b28e0c8b52d32ddc7a27), [`aac9a19`](https://github.com/browserbase/stagehand/commit/aac9a19bdfbe62e4508631337ab0bfbcf8ae62b2), [`06de50f`](https://github.com/browserbase/stagehand/commit/06de50ff377fd31f1b0fcf79adb996d04562d2c0), [`aa4d981`](https://github.com/browserbase/stagehand/commit/aa4d981e440bdd0e3d3f42ccc310d5958aa25cc6), [`18b1e3b`](https://github.com/browserbase/stagehand/commit/18b1e3bd2b16b721845d52fcf1a45c6158e2403f), [`957d82b`](https://github.com/browserbase/stagehand/commit/957d82b9845b4413b123539e81a2e4a490e74a8a), [`b65756e`](https://github.com/browserbase/stagehand/commit/b65756e9e85643055446aa4a51956f7d6627c89f), [`22e371a`](https://github.com/browserbase/stagehand/commit/22e371ae4c25deb6350328fe02832bf2b2197b94), [`d29b91f`](https://github.com/browserbase/stagehand/commit/d29b91fa506636ca36f724fcf106320de54ec3f3), [`7b4f817`](https://github.com/browserbase/stagehand/commit/7b4f817cafb9829ac81c4b5890c318c7f9521fe4), [`176d420`](https://github.com/browserbase/stagehand/commit/176d42002cc0a2c7d13b4c0ffbbd56b70fdc49e8), [`3f9ca4d`](https://github.com/browserbase/stagehand/commit/3f9ca4d9acc109101357378d29cf969168991608), [`8a3c066`](https://github.com/browserbase/stagehand/commit/8a3c06600a9ba98485db7e9ed5c3cc43ea180334), [`49ead1e`](https://github.com/browserbase/stagehand/commit/49ead1e1e8678a8da0f87ad2042491dacc6b01d7), [`3673369`](https://github.com/browserbase/stagehand/commit/36733691f90c15386cf2a7b47d04ef429b7195ae), [`c465e87`](https://github.com/browserbase/stagehand/commit/c465e87ab41942435132c76338518fb3fa8e7896), [`ae533e4`](https://github.com/browserbase/stagehand/commit/ae533e40195181b53833f8055b1259fb360a927b), [`ea33052`](https://github.com/browserbase/stagehand/commit/ea330520a325583b71b87d85beb740df4bdb9b2d), [`5764ede`](https://github.com/browserbase/stagehand/commit/5764edee7aab00ef1aafafb68fc56eb26c0a70b2), [`f09b184`](https://github.com/browserbase/stagehand/commit/f09b184cc5e774736280ae8c94ba3f4f13adda80), [`a7d29de`](https://github.com/browserbase/stagehand/commit/a7d29decee0f7d12e2437267b9eef1795d3b4e3a), [`d334399`](https://github.com/browserbase/stagehand/commit/d3343990041bf9cd5613569840afb0c17131e33c), [`44416da`](https://github.com/browserbase/stagehand/commit/44416da7ff33301bb32d3811e6c3be8782a7d168), [`bdd8b4e`](https://github.com/browserbase/stagehand/commit/bdd8b4ee3c697a02728375510ab7fae764990576)]:\n  - @browserbasehq/stagehand@3.1.0\n\n## 1.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`40ce5cc`](https://github.com/browserbase/stagehand/commit/40ce5cc83ec758f4e8c37132a7f4ac8eeea7ca34), [`5506f41`](https://github.com/browserbase/stagehand/commit/5506f416d2609d112b553263984e21d7a30e32b1), [`84c05ca`](https://github.com/browserbase/stagehand/commit/84c05ca8de4587181faf128e5c7464fd960caacc), [`692ffa0`](https://github.com/browserbase/stagehand/commit/692ffa0346ad3d121686aba503c0a22844293efa), [`1ef8901`](https://github.com/browserbase/stagehand/commit/1ef8901e1314e90f43b36be20192e652d3b5598f), [`72ac775`](https://github.com/browserbase/stagehand/commit/72ac775a831d6f0f376ceda4426525f93cc21452), [`3d5af07`](https://github.com/browserbase/stagehand/commit/3d5af07f66d6d26d1f5ac4bd9be7183c3381dd92), [`40e1d80`](https://github.com/browserbase/stagehand/commit/40e1d80776b9216422a25a81070ccb3105e56ec2), [`56c0d24`](https://github.com/browserbase/stagehand/commit/56c0d244f9b2431218bfa832ddfc0587930ae038), [`16d72fb`](https://github.com/browserbase/stagehand/commit/16d72fb4c4081dd33bf45605d75c27644ea4c00e), [`088c4cc`](https://github.com/browserbase/stagehand/commit/088c4cc31dc924bb232a9d5a09ab42cd961c2d36), [`4276f4a`](https://github.com/browserbase/stagehand/commit/4276f4abc8bbde215faac6c0321bf243484c376b), [`6005786`](https://github.com/browserbase/stagehand/commit/600578637e65f6fd18b0cdb322b9e0b857708b2f), [`6fbf5fc`](https://github.com/browserbase/stagehand/commit/6fbf5fc811e5e5d9d22f10c5309fbd336892263a), [`704cf18`](https://github.com/browserbase/stagehand/commit/704cf18cb2bdd187ba06c35f05ccb47317a7668c), [`091296e`](https://github.com/browserbase/stagehand/commit/091296e438bb2374c8bb10ef6c08283978145ebf), [`e56c6eb`](https://github.com/browserbase/stagehand/commit/e56c6eb139bf3aad37e98b16626fff13a6c671d0), [`2cb78d0`](https://github.com/browserbase/stagehand/commit/2cb78d0f5ddef9f7337a9a2fe3137f1421df700a), [`5dad639`](https://github.com/browserbase/stagehand/commit/5dad63938f08d968d434bb1ee2804f1e54fb836a), [`b7c2571`](https://github.com/browserbase/stagehand/commit/b7c2571ad4ac563f3ca0518e1f29a40da93e33bc), [`4c69117`](https://github.com/browserbase/stagehand/commit/4c6911748953199dc9aad3eabe98bcf325f871e4)]:\n  - @browserbasehq/stagehand@3.0.8\n\n## 1.1.6\n\n### Patch Changes\n\n- [#1373](https://github.com/browserbase/stagehand/pull/1373) [`cadd192`](https://github.com/browserbase/stagehand/commit/cadd192066c8aa5d19bb4d5daa630ed5981b5d7e) Thanks [@tkattkat](https://github.com/tkattkat)! - Update screenshot collector in agent evals cli\n\n- Updated dependencies [[`0f3991e`](https://github.com/browserbase/stagehand/commit/0f3991eedc0aaff72ef718dda3ddb0839cf4a464), [`e0e22e0`](https://github.com/browserbase/stagehand/commit/e0e22e06bc752a8ffde30f3dbfa58d91e24e6c09), [`f261051`](https://github.com/browserbase/stagehand/commit/f2610517d74774374de9ee93191e663439ef55e5), [`e021674`](https://github.com/browserbase/stagehand/commit/e021674f9641c1c5f9d0c1817c3fdf599eea124d), [`6a5496f`](https://github.com/browserbase/stagehand/commit/6a5496f17dbb716be1ee1aaa4e5ba9d8c723b30b), [`fea1700`](https://github.com/browserbase/stagehand/commit/fea1700552af3319052f463685752501c8e71de3), [`5b288d9`](https://github.com/browserbase/stagehand/commit/5b288d9ac37406ff22460ac8050bea26b87a378e), [`e822f5a`](https://github.com/browserbase/stagehand/commit/e822f5a8898df9eb48ca32c321025f0c74b638f0), [`638efc7`](https://github.com/browserbase/stagehand/commit/638efc7fea401bc43dd05dceedf4c13a3495a728), [`a890f16`](https://github.com/browserbase/stagehand/commit/a890f16fa3a752f308f858e5ab9c9a0faf6b3b34), [`934f492`](https://github.com/browserbase/stagehand/commit/934f492ec587bef81f0ce75b45a35b44ab545712), [`bd2db92`](https://github.com/browserbase/stagehand/commit/bd2db925f66a826d61d58be1611d55646cbdb560), [`51e0170`](https://github.com/browserbase/stagehand/commit/51e01709ce1c947c1947b4e2cb0b1f4f97b77182), [`05f5580`](https://github.com/browserbase/stagehand/commit/05f5580937c3c157550e3c25ae6671f44f562211), [`f56a9c2`](https://github.com/browserbase/stagehand/commit/f56a9c296d4ddce25a405358c66837f8ce4d679f), [`b40ae11`](https://github.com/browserbase/stagehand/commit/b40ae11391af49c3581fce27faa1b7483fc4a169), [`0d2b398`](https://github.com/browserbase/stagehand/commit/0d2b398cd40b32a9ecaf28ede70853036b7c91bd), [`cd01f29`](https://github.com/browserbase/stagehand/commit/cd01f290578eac703521f801ba3712f5332918f3), [`a734fca`](https://github.com/browserbase/stagehand/commit/a734fca0b4573753767d3ebc48ec414baf4f23e1), [`b342acf`](https://github.com/browserbase/stagehand/commit/b342acfaae058127fb57664644c5fd965db02bf2), [`2987cd1`](https://github.com/browserbase/stagehand/commit/2987cd1e5ffabefa9411936609635d4a638faed5), [`dfab1d5`](https://github.com/browserbase/stagehand/commit/dfab1d566299c8c5a63f20565a6da07dc8f61ccd), [`4d71162`](https://github.com/browserbase/stagehand/commit/4d71162beb119635b69b17637564a2bbd0e373e7)]:\n  - @browserbasehq/stagehand@3.0.7\n\n## 1.1.5\n\n### Patch Changes\n\n- [#1364](https://github.com/browserbase/stagehand/pull/1364) [`ca0630e`](https://github.com/browserbase/stagehand/commit/ca0630e4d96bf86708b9ff202ad8da0d0761bda8) Thanks [@tkattkat](https://github.com/tkattkat)! - Update model handling in agent evals cli\n\n- Updated dependencies [[`605ed6b`](https://github.com/browserbase/stagehand/commit/605ed6b81a3ff8f25d4022f1e5fce6b42aecfc19), [`34e7e5b`](https://github.com/browserbase/stagehand/commit/34e7e5b292f5e6af6efc0da60118663310c5f718), [`943d2d7`](https://github.com/browserbase/stagehand/commit/943d2d79d0f289ac41c9164578f2f1dd876058f2), [`0e95cd2`](https://github.com/browserbase/stagehand/commit/0e95cd2f67672f64f0017024fd47d8b3aef59a95), [`d4237e4`](https://github.com/browserbase/stagehand/commit/d4237e40951ecd10abfdbe766672d498f8806484), [`86975e7`](https://github.com/browserbase/stagehand/commit/86975e795db7505804949a267b20509bd16b5256), [`d5e119b`](https://github.com/browserbase/stagehand/commit/d5e119be5eec84915a79f8d611b6ba0546f48c99), [`4e051b2`](https://github.com/browserbase/stagehand/commit/4e051b23add7ae276b0dbead38b4587838cfc1c1), [`6b5a3c9`](https://github.com/browserbase/stagehand/commit/6b5a3c9035654caaed2da375085b465edda97de4), [`bb85ad9`](https://github.com/browserbase/stagehand/commit/bb85ad912738623a7a866f0cb6e8d5807c6c2738), [`88d28cc`](https://github.com/browserbase/stagehand/commit/88d28cc6f31058d1cf6ec6dc948a4ae77a926b3c), [`45bcef0`](https://github.com/browserbase/stagehand/commit/45bcef0e5788b083f9e38dfd7c3bc63afcd4b6dd), [`6aa9d45`](https://github.com/browserbase/stagehand/commit/6aa9d455aa5836ec2ee8ab2e8b9df3fb218e5381), [`d382084`](https://github.com/browserbase/stagehand/commit/d382084745fff98c3e71413371466394a2625429), [`1df08cc`](https://github.com/browserbase/stagehand/commit/1df08ccb0a2cf73b5c37a91c129721114ff6371c), [`2b56600`](https://github.com/browserbase/stagehand/commit/2b566009606fcbba987260f21b075b318690ce99)]:\n  - @browserbasehq/stagehand@3.0.6\n\n## 1.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`fa18cfd`](https://github.com/browserbase/stagehand/commit/fa18cfdc45f28e35e6566587b54612396e6ece45), [`767d168`](https://github.com/browserbase/stagehand/commit/767d1686285cf9c57675595f553f8a891f13c63b), [`f27a99c`](https://github.com/browserbase/stagehand/commit/f27a99c11b020b33736fe67af8f7f0e663c6f45f), [`91a1ca0`](https://github.com/browserbase/stagehand/commit/91a1ca07d9178c46269bfb951abb20a215eb7c29), [`1dd7d43`](https://github.com/browserbase/stagehand/commit/1dd7d4330de9022dc6cd45a8b5c86cb9e1b575ec), [`c0f3b98`](https://github.com/browserbase/stagehand/commit/c0f3b98277c15c77b2b4c3f55503e61ef3d27cf3), [`44bb4f5`](https://github.com/browserbase/stagehand/commit/44bb4f51dcccbdca8df07e4d7f8d28a7e6e793ec), [`2b70347`](https://github.com/browserbase/stagehand/commit/2b7034771bc6d6b1fabb13deaa56c299881b3728)]:\n  - @browserbasehq/stagehand@3.0.4\n\n## 1.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`ab51232`](https://github.com/browserbase/stagehand/commit/ab51232db428be048957c0f5d67f2176eb7a5194), [`c76ade0`](https://github.com/browserbase/stagehand/commit/c76ade009ef81208accae6475ec4707d3906e566), [`ffb5e5d`](https://github.com/browserbase/stagehand/commit/ffb5e5d2ab49adcb2efdfc9e5c76e8c96268b5b3), [`772e735`](https://github.com/browserbase/stagehand/commit/772e73543e45106d7fa0fafd95ade46ae11023bc)]:\n  - @browserbasehq/stagehand@3.0.3\n\n## 1.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`a224b33`](https://github.com/browserbase/stagehand/commit/a224b3371b6c1470baf342742fb745c7192b52c6), [`6fc9de2`](https://github.com/browserbase/stagehand/commit/6fc9de2a1079e4f2fb0b1633d8df0bb7a9f7f89f), [`4935be7`](https://github.com/browserbase/stagehand/commit/4935be788b3431527f3d110864c0fd7060cfaf7c), [`bdd76fc`](https://github.com/browserbase/stagehand/commit/bdd76fcd1e48079fc5ab8cf040ebb5997dfc6c99), [`7ea18a4`](https://github.com/browserbase/stagehand/commit/7ea18a420fc033d1b72556db83a1f41735e5a022), [`d4de014`](https://github.com/browserbase/stagehand/commit/d4de014235a18f9e1089240bc72e28cbfe77ca1c), [`2d1b573`](https://github.com/browserbase/stagehand/commit/2d1b5732dc441a3331f5743cdfed3e1037d8b3b5), [`5556041`](https://github.com/browserbase/stagehand/commit/5556041e2deaed5012363303fd7a8ac00e3242cd), [`7e4b43e`](https://github.com/browserbase/stagehand/commit/7e4b43ed46fbdd2074827e87d9a245e2dc96456b), [`7e72adf`](https://github.com/browserbase/stagehand/commit/7e72adfd7e4af5ec49ac2f552e7f1f57c1acc554), [`9bf09d0`](https://github.com/browserbase/stagehand/commit/9bf09d041111870d71cb9ffcb3ac5fa2c4b1399d), [`92d32ea`](https://github.com/browserbase/stagehand/commit/92d32eafe91a4241615cc65501b8461c6074a02b), [`ebcf3a1`](https://github.com/browserbase/stagehand/commit/ebcf3a1ffa859374d71de4931c6a9b982a565e46), [`c29a4f2`](https://github.com/browserbase/stagehand/commit/c29a4f2eca91ae2902ed9d48b2385b4436f7b664), [`6d21efa`](https://github.com/browserbase/stagehand/commit/6d21efa8b30317aa3ce3e37ac6c2222af3b967b5), [`525ef0c`](https://github.com/browserbase/stagehand/commit/525ef0c1243aaf3452ee7e4ea81b4208f4c2efd1), [`9ddb872`](https://github.com/browserbase/stagehand/commit/9ddb872e350358214e12a91cf6a614fd2ec1f74c)]:\n  - @browserbasehq/stagehand@3.0.2\n\n## 1.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`55da8c6`](https://github.com/browserbase/stagehand/commit/55da8c6e9575cbad3246c55b17650cf6b293ddbe), [`0a5ee63`](https://github.com/browserbase/stagehand/commit/0a5ee638bde051d109eb2266e665934a12f3dc31), [`ee76881`](https://github.com/browserbase/stagehand/commit/ee7688156cb67a9f0f90dfe0dbab77423693a332), [`9e95add`](https://github.com/browserbase/stagehand/commit/9e95add37eb30db4f85e73df7760c7e63fb4131e), [`98e212b`](https://github.com/browserbase/stagehand/commit/98e212b27887241879608c6c1b6c2524477a40d7), [`d5ecbfc`](https://github.com/browserbase/stagehand/commit/d5ecbfc8e419a59b91c2115fd7f984378381d3d0)]:\n  - @browserbasehq/stagehand@3.0.1\n\n## 1.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`09b5e1e`](https://github.com/browserbase/stagehand/commit/09b5e1e9c23c845903686db6665cc968ac34efbb), [`e3734b9`](https://github.com/browserbase/stagehand/commit/e3734b9c98352d5f0a4eca49791b0bbf2130ab41), [`8244ab2`](https://github.com/browserbase/stagehand/commit/8244ab247cd679962685ae2f7c54e874ce1fa614), [`be85b19`](https://github.com/browserbase/stagehand/commit/be85b19679a826f19702e00f0aae72fce1118ec8), [`88d1565`](https://github.com/browserbase/stagehand/commit/88d1565c65bb65a104fea2d5f5e862bbbda69677), [`ab5d6ed`](https://github.com/browserbase/stagehand/commit/ab5d6ede19aabc059badc4247f1cb2c6c9e71bae)]:\n  - @browserbasehq/stagehand@2.5.0\n\n## 1.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`9e8c173`](https://github.com/browserbase/stagehand/commit/9e8c17374fdc8fbe7f26e6cf802c36bd14f11039)]:\n  - @browserbasehq/stagehand@2.4.4\n\n## 1.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`f45afdc`](https://github.com/browserbase/stagehand/commit/f45afdccc8680650755fee66ffbeac32b41e075d), [`261bba4`](https://github.com/browserbase/stagehand/commit/261bba43fa79ac3af95328e673ef3e9fced3279b), [`8de7bd8`](https://github.com/browserbase/stagehand/commit/8de7bd8635c2051cd8025e365c6c8aa83d81c7e7), [`3d80421`](https://github.com/browserbase/stagehand/commit/3d804210a106a6828c7fa50f8b765b10afd4cc6a), [`0ead63d`](https://github.com/browserbase/stagehand/commit/0ead63d6526f6c286362b74b6407c8bebc900e69), [`8422828`](https://github.com/browserbase/stagehand/commit/8422828c4cd5fd5ebcf348cfbdb40c768bb76dd9), [`b769206`](https://github.com/browserbase/stagehand/commit/b7692060f98a2f49aeeefb90d8789ed034b08ec2), [`72d2683`](https://github.com/browserbase/stagehand/commit/72d2683202af7e578d98367893964b33e0828de5)]:\n  - @browserbasehq/stagehand@2.4.3\n\n## 1.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`6b4e6e3`](https://github.com/browserbase/stagehand/commit/6b4e6e3f31d5496cf15728e9018eddeb04839542), [`e77d018`](https://github.com/browserbase/stagehand/commit/e77d0188683ebf596dfb78dfafbbca1dc32993f0), [`c20adb9`](https://github.com/browserbase/stagehand/commit/c20adb95539fed8c56a4aa413262a9c65a8e6474), [`b86df93`](https://github.com/browserbase/stagehand/commit/b86df93b9136aae96292121a29c25f3d74d84bf7), [`023c2c2`](https://github.com/browserbase/stagehand/commit/023c2c273b46d3792d7e5d3c902089487b16b531), [`8c28647`](https://github.com/browserbase/stagehand/commit/8c2864755ecd05c8f7de235d4198deec0dd5f78e), [`87e09c6`](https://github.com/browserbase/stagehand/commit/87e09c618940f364ec8af00455a19a17ec63cbd3), [`a611115`](https://github.com/browserbase/stagehand/commit/a61111525d70b450bdfc43f112380f44899c9e97), [`69913fe`](https://github.com/browserbase/stagehand/commit/69913fe1dfb8201ae2aeffa5f049fb46ab02cbc2), [`b1b83a1`](https://github.com/browserbase/stagehand/commit/b1b83a1d334fe76e5f5f9dd32dc92c16b7d40ce6), [`be8497c`](https://github.com/browserbase/stagehand/commit/be8497cb6b142cc893cea9692b8c47bd19514c60), [`98704c9`](https://github.com/browserbase/stagehand/commit/98704c9ed225ca25bbde4bb3dc286936e9c54471), [`04978bd`](https://github.com/browserbase/stagehand/commit/04978bdd30d2edcbc69eb9fd91358a16975ea2eb)]:\n  - @browserbasehq/stagehand@2.4.2\n\n## 1.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`8a43c5a`](https://github.com/browserbase/stagehand/commit/8a43c5a86d4da40cfaedd9cf2e42186928bdf946), [`890ffcc`](https://github.com/browserbase/stagehand/commit/890ffccac5e0a60ade64a46eb550c981ffb3e84a), [`64c1072`](https://github.com/browserbase/stagehand/commit/64c10727bda50470483a3eb175c02842db0923a1), [`b077d3f`](https://github.com/browserbase/stagehand/commit/b077d3f48a97f47a71ccc79ae39b41e7f07f9c04), [`8bcb5d7`](https://github.com/browserbase/stagehand/commit/8bcb5d77debf6bf7601fd5c090efd7fde75c5d5e), [`7bf10c5`](https://github.com/browserbase/stagehand/commit/7bf10c55b267078fe847c1d7f7a60d604f9c7c94)]:\n  - @browserbasehq/stagehand@2.4.1\n\n## 1.0.4\n\n### Patch Changes\n\n- [#831](https://github.com/browserbase/stagehand/pull/831) [`5812b02`](https://github.com/browserbase/stagehand/commit/5812b027e4919d005321cc00626b057e6e04074b) Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - added -man & -h commands for explaining how to run evals\n\n- Updated dependencies [[`124e0d3`](https://github.com/browserbase/stagehand/commit/124e0d3bb54ddb6738ede6d7aa99a945ef1cacd1), [`6a18c1e`](https://github.com/browserbase/stagehand/commit/6a18c1ee1e46d55c6e90c4d5572e17ed8daa140c), [`1660751`](https://github.com/browserbase/stagehand/commit/1660751cd14cb5b27d44f8167216afb8d1c3c45c), [`cadac9d`](https://github.com/browserbase/stagehand/commit/cadac9da09123d12e5d496a0e8b12660964c1b33), [`759da55`](https://github.com/browserbase/stagehand/commit/759da55775eb2df81d56ae18c0f386fd9b02a9f0), [`a175a51`](https://github.com/browserbase/stagehand/commit/a175a519b8c14300db6f1ed30709e113d18e99db), [`8527a80`](https://github.com/browserbase/stagehand/commit/8527a80522c3eedb9516a6caa1a0e4e4be981a3d), [`55fca2f`](https://github.com/browserbase/stagehand/commit/55fca2f7da63cc0ef6e27b45a33f63c666cdce7e)]:\n  - @browserbasehq/stagehand@2.4.0\n\n## 1.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`12a99b3`](https://github.com/browserbase/stagehand/commit/12a99b398d8a4c3eea3ca69a3cf793faaaf4aea3), [`2451797`](https://github.com/browserbase/stagehand/commit/2451797f64c0efa4a72fd70265110003c8d0a6cd), [`1d631a5`](https://github.com/browserbase/stagehand/commit/1d631a57a197390f672b718ae5199991ab27cfb1), [`9c398bb`](https://github.com/browserbase/stagehand/commit/9c398bb9ec2d10bdb53ad5aa7e3b58cce24fdb2b), [`c19ad7f`](https://github.com/browserbase/stagehand/commit/c19ad7f1e082e91fdeaa9c2ef63767a5a2b3a195)]:\n  - @browserbasehq/stagehand@2.3.1\n\n## 1.0.2\n\n### Patch Changes\n\n- Updated dependencies [[`5680d25`](https://github.com/browserbase/stagehand/commit/5680d2509352c383ad502c9f4fabde01fa638833), [`4de92a8`](https://github.com/browserbase/stagehand/commit/4de92a8af461fc95063faf39feee1d49259f58ba), [`6ef6073`](https://github.com/browserbase/stagehand/commit/6ef60730cab0ad9025f44b6eeb2c83751d1dcd35)]:\n  - @browserbasehq/stagehand@2.3.0\n\n## 1.0.1\n\n### Patch Changes\n\n- Updated dependencies [[`be8652e`](https://github.com/browserbase/stagehand/commit/be8652e770b57fdb3299fa0b2efa4eb0e816434e), [`6b413b7`](https://github.com/browserbase/stagehand/commit/6b413b7ad00b13ca0bd53ee2e7393023821408b6), [`7eafbd9`](https://github.com/browserbase/stagehand/commit/7eafbd9b1a73b37effa444929767df7c592caf02), [`1b50aa6`](https://github.com/browserbase/stagehand/commit/1b50aa61cf0a429dd6cb2760a08f7f698a50454b), [`f2b7f1f`](https://github.com/browserbase/stagehand/commit/f2b7f1f284eef1f96753319b66c7d0b273a6f8cd), [`c8d672f`](https://github.com/browserbase/stagehand/commit/c8d672f7c410c256defbc2e87ead99239837aa28), [`bebf204`](https://github.com/browserbase/stagehand/commit/bebf2044502333c694743078c5b0c9deae11fb79), [`37d6810`](https://github.com/browserbase/stagehand/commit/37d6810a704773d0383a86f98f5f17c7d5b21975)]:\n  - @browserbasehq/stagehand@2.2.1\n"
  },
  {
    "path": "packages/evals/README.md",
    "content": "# Stagehand Evals CLI\n\nA powerful command-line interface for running Stagehand evaluation suites and benchmarks.\n\n## Installation\n\n```bash\n# From the stagehand root directory\npnpm install\npnpm run build:cli\n```\n\n## Usage\n\nThe evals CLI provides a clean, intuitive interface for running evaluations:\n\n```bash\npnpm evals <command> <target> [options]\n```\n\n## Commands\n\n### `run` - Execute evaluations\n\nRun custom evals or external benchmarks.\n\n```bash\n# Run all custom evals\npnpm evals run all\n\n# Run specific category\npnpm evals run act\npnpm evals run extract\npnpm evals run observe\n\n# Run specific eval by name\npnpm evals run extract/extract_text\n\n# Run external benchmarks\npnpm evals run benchmark:gaia\n```\n\n### `list` - View available evals\n\nList all available evaluations and benchmarks.\n\n```bash\n# List all categories and benchmarks\npnpm evals list\n\n# Show detailed task list\npnpm evals list --detailed\n```\n\n### `config` - Manage defaults\n\nConfigure default settings for all eval runs.\n\n```bash\n# View current configuration\npnpm evals config\n\n# Set default values\npnpm evals config set env browserbase\npnpm evals config set trials 5\npnpm evals config set concurrency 10\n\n# Reset to defaults\npnpm evals config reset\npnpm evals config reset trials  # Reset specific key\n```\n\n### `help` - Show help\n\n```bash\npnpm evals help\n```\n\n## Options\n\n### Core Options\n\n- `-e, --env` - Environment: `local` or `browserbase` (default: local)\n- `-t, --trials` - Number of trials per eval (default: 3)\n- `-c, --concurrency` - Max parallel sessions (default: 3)\n- `-m, --model` - Model override (e.g., gpt-4o, claude-3.5)\n- `-p, --provider` - Provider override (openai, anthropic, etc.)\n- `--api` - Use Stagehand API instead of SDK\n\n### Benchmark-Specific Options\n\n- `-l, --limit` - Max tasks to run (default: 25)\n- `-s, --sample` - Random sample before limit\n- `-f, --filter` - Benchmark-specific filters (key=value)\n\n## Examples\n\n### Running Custom Evals\n\n```bash\n# Run with custom settings\npnpm evals run act -e browserbase -t 5 -c 10\n\n# Run with specific model\npnpm evals run observe -m gpt-4o -p openai\n\n# Run using API\npnpm evals run extract --api\n```\n\n### Running Benchmarks\n\n```bash\n# WebBench with filters\npnpm evals run b:webbench -l 10 -f difficulty=easy -f category=READ\n\n# GAIA with sampling\npnpm evals run b:gaia -s 100 -l 25 -f level=1\n\n# WebVoyager with limit\npnpm evals run b:webvoyager -l 50\n```\n\n## Available Benchmarks\n\n### OnlineMind2Web (`b:onlineMind2Web`)\n\nReal-world web interaction tasks for evaluating web agents.\n\n### GAIA (`b:gaia`)\n\nGeneral AI Assistant benchmark for complex reasoning.\n\n**Filters:**\n\n- `level`: 1, 2, 3 (difficulty levels)\n\n### WebVoyager (`b:webvoyager`)\n\nWeb navigation and task completion benchmark.\n\n### WebBench (`b:webbench`)\n\nReal-world web automation tasks across live websites.\n\n**Filters:**\n\n- `difficulty`: easy, hard\n- `category`: READ, CREATE, UPDATE, DELETE, FILE_MANIPULATION\n- `use_hitl`: true/false\n\n### OSWorld (`b:osworld`)\n\nChrome browser automation tasks from the OSWorld benchmark.\n\n**Filters:**\n\n- `source`: Mind2Web, test_task_1, etc.\n- `evaluation_type`: url_match, string_match, dom_state, custom\n\n## Configuration\n\nThe CLI uses a configuration file at `evals/evals.config.json` which contains:\n\n- **defaults**: Default values for CLI options\n- **benchmarks**: Metadata for external benchmarks\n- **tasks**: Registry of all evaluation tasks\n\nYou can modify defaults either through the `config` command or by editing the file directly.\n\n## Environment Variables\n\nWhile the CLI reduces the need for environment variables, some are still supported for CI/CD:\n\n- `EVAL_ENV` - Override environment setting\n- `EVAL_TRIAL_COUNT` - Override trial count\n- `EVAL_MAX_CONCURRENCY` - Override concurrency\n- `EVAL_PROVIDER` - Override LLM provider\n- `USE_API` - Use Stagehand API\n\n## Development\n\n### Adding New Evals\n\n1. Create your eval file in `evals/tasks/<category>/`\n2. Add it to `evals.config.json` under the `tasks` array\n3. Run with: `pnpm evals run <category>/<eval_name>`\n\n## Troubleshooting\n\n### Command not found\n\nIf `evals` command is not found, make sure you've:\n\n1. Run `pnpm install` from the project root\n2. Run `pnpm run build:cli` to compile the CLI\n\n### Build errors\n\nIf you encounter build errors:\n\n```bash\n# Clean and rebuild\nrm -rf packages/evals/dist/cli\npnpm run build:cli\n```\n\n### Permission errors\n\nIf you get permission errors:\n\n```bash\nchmod +x packages/evals/dist/cli/cli.js\n```\n\n## Contributing\n\nWhen adding new features to the CLI:\n\n1. Update the command in `evals/cli.ts`\n2. Add new options to the help text\n3. Update this README with examples\n4. Test with various flag combinations\n"
  },
  {
    "path": "packages/evals/args.ts",
    "content": "import process from \"process\";\nimport { EvalCategorySchema } from \"./types/evals.js\";\nimport chalk from \"chalk\";\nimport { dedent } from \"./utils.js\";\n\nconst HELP_REGEX = /^(?:--?)?(?:h|help)$/i;\nconst MAN_REGEX = /^(?:--?)?man$/i;\n\nconst rawArgs = process.argv.slice(2);\n\nconst parsedArgs: {\n  evalName?: string;\n  env?: string;\n  api?: string;\n  trials?: number;\n  concurrency?: number;\n  provider?: string;\n  dataset?: string;\n  max_k?: number;\n  leftover: string[];\n} = {\n  leftover: [],\n};\n\nfor (const arg of rawArgs) {\n  if (arg.startsWith(\"env=\")) {\n    parsedArgs.env = arg.split(\"=\")[1]?.toLowerCase();\n  } else if (arg.startsWith(\"api=\")) {\n    parsedArgs.api = arg.split(\"=\")[1]?.toLowerCase();\n  } else if (arg.startsWith(\"name=\")) {\n    parsedArgs.evalName = arg.split(\"=\")[1];\n  } else if (arg.startsWith(\"trials=\")) {\n    const val = parseInt(arg.split(\"=\")[1], 10);\n    if (!isNaN(val)) {\n      parsedArgs.trials = val;\n    }\n  } else if (arg.startsWith(\"concurrency=\")) {\n    const val = parseInt(arg.split(\"=\")[1], 10);\n    if (!isNaN(val)) {\n      parsedArgs.concurrency = val;\n    }\n  } else if (arg.startsWith(\"provider=\")) {\n    parsedArgs.provider = arg.split(\"=\")[1]?.toLowerCase();\n  } else if (arg.startsWith(\"--dataset=\")) {\n    parsedArgs.dataset = arg.split(\"=\")[1]?.toLowerCase();\n  } else if (arg.startsWith(\"max_k=\")) {\n    const val = parseInt(arg.split(\"=\")[1], 10);\n    if (!isNaN(val)) {\n      parsedArgs.max_k = val;\n    }\n  } else {\n    parsedArgs.leftover.push(arg);\n  }\n}\n\n/** Apply environment defaults or overrides */\nif (parsedArgs.env === \"browserbase\") {\n  process.env.EVAL_ENV = \"BROWSERBASE\";\n} else if (parsedArgs.env === \"local\") {\n  process.env.EVAL_ENV = \"LOCAL\";\n}\n\nif (parsedArgs.api === \"true\") {\n  process.env.USE_API = \"true\";\n} else if (parsedArgs.api === \"false\") {\n  process.env.USE_API = \"false\";\n}\n\nif (parsedArgs.trials !== undefined) {\n  process.env.EVAL_TRIAL_COUNT = String(parsedArgs.trials);\n}\nif (parsedArgs.concurrency !== undefined) {\n  process.env.EVAL_MAX_CONCURRENCY = String(parsedArgs.concurrency);\n}\nif (parsedArgs.max_k !== undefined) {\n  process.env.EVAL_MAX_K = String(parsedArgs.max_k);\n}\nif (parsedArgs.dataset !== undefined) {\n  process.env.EVAL_DATASET = parsedArgs.dataset;\n}\n\nconst DEFAULT_EVAL_CATEGORIES = process.env.EVAL_CATEGORIES\n  ? process.env.EVAL_CATEGORIES.split(\",\")\n  : [\n      \"observe\",\n      \"act\",\n      \"combination\",\n      \"extract\",\n      \"experimental\",\n      \"targeted_extract\",\n      \"regression_llm_providers\",\n      \"regression\",\n      \"llm_clients\",\n      \"agent\",\n      \"external_agent_benchmarks\",\n    ];\n\nconst providerDefault = process.env.EVAL_PROVIDER ?? undefined;\n\nfunction buildUsage(detailed = false): string {\n  const header = chalk.blue.bold(\"Stagehand • Eval Runner\");\n  const synopsis = chalk.cyan(\n    `pnpm run evals [key=value]… [category <name>] | name=<evalName>`,\n  );\n\n  const examplesSection = `\n      ${chalk.magenta.underline(\"Examples\")}\n\n      ${chalk.dim(\"# Run every evaluation locally with default settings\")}\n      ${chalk.green(\"pnpm run evals\")}\n\n      ${chalk.dim(\"# Same as above but in Browserbase with three trials\")}  \n      ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"env=\")}${chalk.yellow(\"browserbase\")} ${chalk.cyan(\"trials=\")}${chalk.yellow(\"3\")}\n\n      ${chalk.dim(\"# Run evals using the Stagehand API\")}\n      ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"env=\")}${chalk.yellow(\"browserbase\")} ${chalk.cyan(\"api=\")}${chalk.yellow(\"true\")}\n\n      ${chalk.dim(\"# Run evals from only the 'act' category with a max of 4 running at any given time\")}\n      ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"category\")} ${chalk.yellow(\"act\")} ${chalk.cyan(\"concurrency=\")}${chalk.yellow(\"4\")}\n\n      ${chalk.dim(\"# Execute a specific eval by filename\")}\n      ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"name=\")}${chalk.yellow(\"my_eval_name\")}\n  `;\n\n  const body = dedent`\n    ${chalk.magenta.underline(\"Keys\\n\")}\n  ${chalk.cyan(\"env\".padEnd(12))} ${\"target environment\".padEnd(24)}\n    (default ${chalk.dim(\"LOCAL\")})                [${chalk.yellow(\"browserbase\")}, ${chalk.yellow(\"local\")}]\n\n  ${chalk.cyan(\"api\".padEnd(12))} ${\"use the Stagehand API\".padEnd(24)}\n    (default ${chalk.dim(\"false\")})                [${chalk.yellow(\"true\")}, ${chalk.yellow(\"false\")}]\n\n  ${chalk.cyan(\"trials\".padEnd(12))} ${\"number of trials per task\".padEnd(24)}\n    (default ${chalk.dim(\"3\")})\n\n  ${chalk.cyan(\"concurrency\".padEnd(12))} ${\"max parallel sessions\".padEnd(24)}\n    (default ${chalk.dim(\"3\")})\n\n  ${chalk.cyan(\"provider\".padEnd(12))} ${\"override LLM provider\".padEnd(24)}\n    (default ${chalk.dim(providerDefault || \"varies by model\")})        [${chalk.yellow(\"openai\")}, ${chalk.yellow(\"anthropic\")}, ${chalk.yellow(\"google\")}, ${chalk.yellow(\"together\")}, ${chalk.yellow(\"groq\")}, ${chalk.yellow(\"cerebras\")}]\n\n  ${chalk.cyan(\"max_k\".padEnd(12))} ${\"max test cases per dataset\".padEnd(24)}\n    (default ${chalk.dim(\"25\")})\n\n  ${chalk.cyan(\"--dataset\".padEnd(12))} ${\"filter to specific benchmark\".padEnd(24)}\n    (optional)              [${chalk.yellow(\"gaia\")}, ${chalk.yellow(\"webvoyager\")}, ${chalk.yellow(\"webbench\")}, ${chalk.yellow(\"osworld\")}, ${chalk.yellow(\"onlineMind2Web\")}]\n\n\n    ${chalk.magenta.underline(\"Positional filters\\n\")}\n      \n      category <category_name>   \n      \n        ${chalk.gray(\"Available categories:\")}\n        ${DEFAULT_EVAL_CATEGORIES.slice(0, 5)\n          .map((c) => chalk.yellow(c))\n          .join(\", \")},\n        ${DEFAULT_EVAL_CATEGORIES.slice(5, 10)\n          .map((c) => chalk.yellow(c))\n          .join(\", \")}${DEFAULT_EVAL_CATEGORIES.slice(10).length > 0 ? \",\" : \"\"}\n        ${DEFAULT_EVAL_CATEGORIES.slice(10)\n          .map((c) => chalk.yellow(c))\n          .join(\", \")}\n  `;\n\n  if (!detailed)\n    return `${header}\\n\\n${synopsis}\\n\\nFor more details: ${chalk.bold(\n      \"pnpm run evals -man\\n\",\n    )}`;\n\n  const externalBenchmarksSection = dedent`\n    ${chalk.magenta.underline(\"\\nExternal Benchmarks\\n\")}\n    \n    ${chalk.cyan.bold(\"WebBench\")} - 5,607 real-world web automation tasks across 452 live websites\n    \n      ${chalk.dim(\"Run:\")} ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"name=\")}${chalk.yellow(\"agent/webbench\")}\n      \n      ${chalk.dim(\"Or:\")}  ${chalk.green(\"EVAL_DATASET=webbench pnpm run evals\")}\n      \n      ${chalk.gray(\"Environment Variables:\")}\n      \n      EVAL_WEBBENCH_LIMIT       max tasks to run (default: 25)\n      EVAL_WEBBENCH_SAMPLE      random sample count before limit\n      EVAL_WEBBENCH_DIFFICULTY  filter: [${chalk.yellow(\"easy\")}, ${chalk.yellow(\"hard\")}] (254 easy, 61 hard tasks)\n      EVAL_WEBBENCH_CATEGORY    filter: [${chalk.yellow(\"READ\")}, ${chalk.yellow(\"CREATE\")}, ${chalk.yellow(\"UPDATE\")}, ${chalk.yellow(\"DELETE\")}, ${chalk.yellow(\"FILE_MANIPULATION\")}]\n      EVAL_WEBBENCH_USE_HITL    use only HITL dataset with difficulty ratings (true/false)\n      \n      ${chalk.dim(\"Examples:\")}\n      \n      ${chalk.green(\"EVAL_WEBBENCH_DIFFICULTY=easy EVAL_WEBBENCH_LIMIT=10 pnpm run evals name=agent/webbench\")}\n      \n      ${chalk.green(\"EVAL_DATASET=webbench EVAL_WEBBENCH_CATEGORY=READ pnpm run evals\")}\n    \n    \n    ${chalk.cyan.bold(\"GAIA\")} - General AI Assistant benchmark for complex reasoning\n    \n      ${chalk.dim(\"Run:\")} ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"name=\")}${chalk.yellow(\"agent/gaia\")}\n      \n      ${chalk.dim(\"Or:\")}  ${chalk.green(\"EVAL_DATASET=gaia pnpm run evals\")}\n      \n      ${chalk.gray(\"Environment Variables:\")}\n      \n      EVAL_GAIA_LIMIT           max tasks to run (default: 25)\n      EVAL_GAIA_SAMPLE          random sample count before limit\n      EVAL_GAIA_LEVEL           filter by difficulty level [${chalk.yellow(\"1\")}, ${chalk.yellow(\"2\")}, ${chalk.yellow(\"3\")}]\n      \n      ${chalk.dim(\"Example:\")}\n      \n      ${chalk.green(\"EVAL_GAIA_LEVEL=1 EVAL_GAIA_LIMIT=10 pnpm run evals name=agent/gaia\")}\n    \n    \n    ${chalk.cyan.bold(\"WebVoyager\")} - Web navigation and task completion benchmark\n    \n      ${chalk.dim(\"Run:\")} ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"name=\")}${chalk.yellow(\"agent/webvoyager\")}\n      \n      ${chalk.dim(\"Or:\")}  ${chalk.green(\"EVAL_DATASET=webvoyager pnpm run evals\")}\n      \n      ${chalk.gray(\"Environment Variables:\")}\n      \n      EVAL_WEBVOYAGER_LIMIT     max tasks to run (default: 25)\n      EVAL_WEBVOYAGER_SAMPLE    random sample count before limit\n      \n      ${chalk.gray(\"Ground Truth Evaluation:\")}\n      \n      WebVoyager uses ground truth answers for improved accuracy:\n      • Checks agent's \"Final Answer:\" against reference answers\n      • Supports golden (ideal) and possible (acceptable) answers\n      • Falls back to screenshot evaluation when uncertain\n      • Reference data: evals/datasets/webvoyager/reference-answers.json\n      \n      ${chalk.dim(\"Example:\")}\n      \n      ${chalk.green(\"EVAL_WEBVOYAGER_SAMPLE=50 EVAL_WEBVOYAGER_LIMIT=10 pnpm run evals name=agent/webvoyager\")}\n    \n    \n    ${chalk.cyan.bold(\"OSWorld\")} - Chrome browser automation tasks from the OSWorld benchmark\n    \n      ${chalk.dim(\"Run:\")} ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"name=\")}${chalk.yellow(\"agent/osworld\")}\n      \n      ${chalk.dim(\"Or:\")}  ${chalk.green(\"EVAL_DATASET=osworld pnpm run evals\")}\n      \n      ${chalk.gray(\"Environment Variables:\")}\n      \n      EVAL_OSWORLD_LIMIT           max tasks to run (default: 25)\n      EVAL_OSWORLD_SAMPLE          random sample count before limit\n      EVAL_OSWORLD_SOURCE          filter by source: [${chalk.yellow(\"Mind2Web\")}, ${chalk.yellow(\"test_task_1\")}, ...]\n      EVAL_OSWORLD_EVALUATION_TYPE filter by eval type: [${chalk.yellow(\"url_match\")}, ${chalk.yellow(\"string_match\")}, ${chalk.yellow(\"dom_state\")}, ${chalk.yellow(\"custom\")}]\n      EVAL_OSWORLD_TIMEOUT         timeout per task in milliseconds (default: 60000)\n      \n      ${chalk.dim(\"Examples:\")}\n      \n      ${chalk.green(\"EVAL_OSWORLD_SOURCE=Mind2Web EVAL_OSWORLD_LIMIT=10 pnpm run evals name=agent/osworld\")}\n      \n      ${chalk.green(\"EVAL_DATASET=osworld EVAL_OSWORLD_EVALUATION_TYPE=url_match pnpm run evals\")}\n    \n    \n    ${chalk.cyan.bold(\"Mind2Web\")} - Real-world web interaction tasks for evaluating web agents\n    \n      ${chalk.dim(\"Run:\")} ${chalk.green(\"pnpm run evals\")} ${chalk.cyan(\"name=\")}${chalk.yellow(\"agent/onlineMind2Web\")}\n      \n      ${chalk.dim(\"Or:\")}  ${chalk.green(\"EVAL_DATASET=onlineMind2Web pnpm run evals\")}\n      \n      ${chalk.gray(\"Environment Variables:\")}\n      \n      EVAL_ONLINEMIND2WEB_LIMIT     max tasks to run (default: 25)\n      EVAL_ONLINEMIND2WEB_SAMPLE    random sample count before limit\n      \n      ${chalk.dim(\"Example:\")}\n      \n      ${chalk.green(\"EVAL_ONLINEMIND2WEB_SAMPLE=50 EVAL_ONLINEMIND2WEB_LIMIT=10 pnpm run evals name=agent/onlineMind2Web\")}\n  `;\n\n  const envSection = dedent`\n    ${chalk.magenta.underline(\"\\nGlobal Environment Variables\\n\")}\n      \n      EVAL_ENV              target environment, overridable via ${chalk.cyan(\"env=\")}\n      \n      EVAL_TRIAL_COUNT      number of trials, overridable via ${chalk.cyan(\"trials=\")}\n      \n      EVAL_MAX_CONCURRENCY  parallel sessions, overridable via ${chalk.cyan(\"concurrency=\")}\n      \n      EVAL_PROVIDER         LLM provider, overridable via ${chalk.cyan(\"provider=\")}\n      \n      EVAL_MAX_K            global limit for all benchmarks (overrides individual limits)\n      \n      EVAL_DATASET          filter to specific benchmark, overridable via ${chalk.cyan(\"--dataset=\")}\n      \n      USE_API               use Stagehand API, overridable via ${chalk.cyan(\"api=\")}\n      \n      EVAL_MODELS           comma-separated list of models to use\n      \n      AGENT_EVAL_MAX_STEPS  max steps for agent tasks (default: 50)\n  `;\n\n  return `${header}\\n\\n${synopsis}\\n\\n${body}\\n${examplesSection}\\n${externalBenchmarksSection}\\n${envSection}\\n`;\n}\n\nconst wantsHelp = rawArgs.some((a) => HELP_REGEX.test(a));\nconst wantsMan = rawArgs.some((a) => MAN_REGEX.test(a));\n\nif (wantsHelp || wantsMan) {\n  console.log(buildUsage(wantsMan));\n  process.exit(0);\n}\n\nlet filterByCategory: string | null = null;\nlet filterByEvalName: string | null = null;\n\nif (parsedArgs.evalName) {\n  filterByEvalName = parsedArgs.evalName;\n}\n\nif (!filterByEvalName && parsedArgs.leftover.length > 0) {\n  if (parsedArgs.leftover[0].toLowerCase() === \"category\") {\n    filterByCategory = parsedArgs.leftover[1];\n    if (!filterByCategory) {\n      console.error(chalk.red(\"Error: Category name not specified.\"));\n      process.exit(1);\n    }\n    try {\n      EvalCategorySchema.parse(filterByCategory);\n    } catch {\n      console.error(\n        chalk.red(\n          `Error: Invalid category \"${filterByCategory}\". Valid categories are: ${DEFAULT_EVAL_CATEGORIES.join(\n            \", \",\n          )}`,\n        ),\n      );\n      process.exit(1);\n    }\n  } else {\n    // If leftover[0] is not \"category\", interpret it as a task/eval name\n    filterByEvalName = parsedArgs.leftover[0];\n  }\n}\n\nif (parsedArgs.provider !== undefined) {\n  process.env.EVAL_PROVIDER = parsedArgs.provider;\n}\n\nexport {\n  filterByCategory,\n  filterByEvalName,\n  DEFAULT_EVAL_CATEGORIES,\n  parsedArgs,\n};\n"
  },
  {
    "path": "packages/evals/assets/cart.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <script>\n      function getQueryParam(param) {\n        const urlParams = new URLSearchParams(window.location.search);\n        return urlParams.get(param);\n      }\n      const item = getQueryParam(\"item\");\n      document.addEventListener(\"DOMContentLoaded\", function () {\n        document.getElementById(\"cartItem\").textContent =\n          `Congratulations, you have 1 ${item} in your cart`;\n      });\n    </script>\n    <div id=\"cartItem\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/evals/assets/peeler.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <h1>Welcome to Our Page</h1>\n\n    <div class=\"product-card\">\n      <div class=\"product-info\">\n        <h2>Knife Set</h2>\n        <p>\n          High-quality stainless steel knives for all your cooking needs.<a\n            >my stuff</a\n          >\n          more stuff\n        </p>\n      </div>\n      <button onclick=\"location.href='cart.html?item=B'\">Add to cart</button>\n    </div>\n    <div class=\"product-card\">\n      <div class=\"product-info\">\n        <h2>Peeler</h2>\n        <p>The ultimate tool for peeling fruits and vegetables.</p>\n      </div>\n      <button onclick=\"location.href='cart.html?item=A'\">Add to cart</button>\n    </div>\n    <a href=\"cart.html\" aria-role=\"button\">\n      <div>hi world</div>\n    </a>\n    <p>\n      Baseball evolved from older\n      <a href=\"/wiki/Bat-and-ball_games\" title=\"Bat-and-ball games\"\n        >bat-and-ball games</a\n      >\n      already being played in England by the mid-18th century. This game was\n      brought by immigrants to North America,\n      <a\n        href=\"/wiki/History_of_baseball_in_the_United_States\"\n        title=\"History of baseball in the United States\"\n        >where the modern version developed</a\n      >.\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/evals/browserbaseCleanup.ts",
    "content": "import type { V3 } from \"@browserbasehq/stagehand\";\n\nconst CLOSE_TIMEOUT_MS = 5_000;\n\nasync function settleWithTimeout(\n  promise: Promise<unknown>,\n  timeoutMs: number,\n): Promise<void> {\n  let timeoutId: NodeJS.Timeout | undefined;\n  const timeout = new Promise<void>((resolve) => {\n    timeoutId = setTimeout(resolve, timeoutMs);\n  });\n  try {\n    await Promise.race([promise.catch(() => {}), timeout]);\n  } finally {\n    if (timeoutId) clearTimeout(timeoutId);\n  }\n}\n\nexport async function endBrowserbaseSession(v3?: V3 | null): Promise<void> {\n  if (!v3?.isBrowserbase) return;\n  if ((process.env.USE_API ?? \"\").toLowerCase() === \"true\") return;\n\n  try {\n    await settleWithTimeout(\n      v3.context.conn.send(\"Browser.close\"),\n      CLOSE_TIMEOUT_MS,\n    );\n  } catch {\n    // best-effort cleanup\n  }\n}\n"
  },
  {
    "path": "packages/evals/cli.ts",
    "content": "import process from \"process\";\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { spawn } from \"child_process\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nconst moduleDir = getCurrentDirPath();\nconst CONFIG_PATH = path.join(moduleDir, \"evals.config.json\");\n\ninterface Config {\n  defaults: {\n    env: string;\n    trials: number;\n    concurrency: number;\n    provider: string | null;\n    model: string | null;\n    api: boolean;\n  };\n  benchmarks: Record<\n    string,\n    {\n      limit: number;\n      filters?: string[];\n      timeout?: number;\n    }\n  >;\n  tasks: Array<{ name: string; categories: string[] }>;\n}\n\nfunction loadConfig(): Config {\n  return JSON.parse(fs.readFileSync(CONFIG_PATH, \"utf-8\"));\n}\n\nfunction saveConfig(config: Config): void {\n  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));\n}\n\nfunction printHelp(): void {\n  console.log(\n    chalk.yellow(`⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡾⠻⣶⡀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⢠⡶⠛⢳⡆⠀⠀⠀⠀⢸⡇⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⢸⣷⠶⣦⣴⠶⣾⡇⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⢸⡇⠀⢸⡇⠀⢸⡇⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠘⠷⣤⢾⡏⠉⠉⠉⠙⣾⡇⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠈⣻⡿⠟⠂⠀⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠈⣷⠀⠀⠀⠀⢰⡏⠀⠀⠀⢀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣷⡀⠀⠀⠀⠀⠀⠀⢀⡾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠷⣦⣤⣤⣴⠾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀`),\n  );\n  console.log(chalk.yellow.bold(\"\\nStagehand Evals CLI\"));\n  console.log(chalk.cyan(\"\\nevals <command> <target> [options]\\n\"));\n\n  console.log(chalk.magenta.underline(\"Commands\"));\n  console.log(\"  run       Execute evals or benchmarks\");\n  console.log(\"  list      List available evals/benchmarks\");\n  console.log(\"  config    Get/set default configuration\");\n  console.log(\"  help      Show this help message\\n\");\n\n  console.log(chalk.magenta.underline(\"Examples\"));\n  console.log(chalk.dim(\"  # Run all custom evals\"));\n  console.log(chalk.green(\"  evals run all\\n\"));\n\n  console.log(chalk.dim(\"  # Run specific category\"));\n  console.log(\n    chalk.green(\"  evals run act\") + chalk.cyan(\" -e browserbase -t 5\\n\"),\n  );\n\n  console.log(chalk.dim(\"  # Run specific eval\"));\n  console.log(chalk.green(\"  evals run login\\n\"));\n\n  console.log(chalk.dim(\"  # Run benchmark\"));\n  console.log(\n    chalk.green(\"  evals run benchmark:onlineMind2Web\") +\n      chalk.cyan(\" -l 10 -f difficulty=easy\\n\"),\n  );\n\n  console.log(chalk.dim(\"  # Configure defaults\"));\n  console.log(chalk.green(\"  evals config set env browserbase\"));\n  console.log(chalk.green(\"  evals config set trials 5\\n\"));\n\n  console.log(chalk.magenta.underline(\"Options\"));\n  console.log(\n    chalk.cyan(\"  -e, --env\".padEnd(20)) + \"Environment: local|browserbase\",\n  );\n  console.log(\n    chalk.cyan(\"  -t, --trials\".padEnd(20)) + \"Number of trials per eval\",\n  );\n  console.log(\n    chalk.cyan(\"  -c, --concurrency\".padEnd(20)) + \"Max parallel sessions\",\n  );\n  console.log(chalk.cyan(\"  -m, --model\".padEnd(20)) + \"Model override\");\n  console.log(chalk.cyan(\"  -p, --provider\".padEnd(20)) + \"Provider override\");\n  console.log(chalk.cyan(\"  --api\".padEnd(20)) + \"Use Stagehand API\\n\");\n\n  console.log(chalk.dim(\"  Benchmark-specific:\"));\n  console.log(chalk.cyan(\"  -l, --limit\".padEnd(20)) + \"Max tasks to run\");\n  console.log(\n    chalk.cyan(\"  -s, --sample\".padEnd(20)) + \"Random sample before limit\",\n  );\n  console.log(\n    chalk.cyan(\"  -f, --filter\".padEnd(20)) + \"Benchmark filters (key=value)\\n\",\n  );\n}\n\nfunction handleConfig(args: string[]): void {\n  const config = loadConfig();\n\n  if (args.length === 0) {\n    // Show current config\n    console.log(chalk.blue.bold(\"\\nCurrent Configuration\"));\n    console.log(chalk.cyan(\"\\nDefaults:\"));\n    Object.entries(config.defaults).forEach(([key, value]) => {\n      console.log(`  ${key}: ${chalk.yellow(value ?? \"not set\")}`);\n    });\n    return;\n  }\n\n  if (args[0] === \"set\" && args.length >= 3) {\n    const [, key, ...valueParts] = args;\n    const value = valueParts.join(\" \");\n\n    if (!(key in config.defaults)) {\n      console.error(chalk.red(`Error: Unknown config key \"${key}\"`));\n      console.log(\n        chalk.dim(`Valid keys: ${Object.keys(config.defaults).join(\", \")}`),\n      );\n      process.exit(1);\n    }\n\n    // Parse value based on type\n    let parsedValue: string | number | boolean | null = value;\n    if (key === \"trials\" || key === \"concurrency\") {\n      parsedValue = parseInt(value, 10);\n      if (isNaN(parsedValue)) {\n        console.error(chalk.red(`Error: ${key} must be a number`));\n        process.exit(1);\n      }\n    } else if (key === \"api\") {\n      parsedValue = value === \"true\";\n    } else if (value === \"null\" || value === \"none\") {\n      parsedValue = null;\n    }\n\n    // Type-safe assignment\n    if (key === \"env\") {\n      config.defaults.env = parsedValue as string;\n    } else if (key === \"trials\") {\n      config.defaults.trials = parsedValue as number;\n    } else if (key === \"concurrency\") {\n      config.defaults.concurrency = parsedValue as number;\n    } else if (key === \"provider\") {\n      config.defaults.provider = parsedValue as string | null;\n    } else if (key === \"model\") {\n      config.defaults.model = parsedValue as string | null;\n    } else if (key === \"api\") {\n      config.defaults.api = parsedValue as boolean;\n    }\n    saveConfig(config);\n    console.log(chalk.green(`✓ Set ${key} to ${parsedValue}`));\n  } else if (args[0] === \"reset\") {\n    const defaultConfig: Config[\"defaults\"] = {\n      env: \"local\",\n      trials: 3,\n      concurrency: 3,\n      provider: null,\n      model: null,\n      api: false,\n    };\n\n    if (args[1] && args[1] in config.defaults) {\n      const key = args[1];\n      // Type-safe reset by key\n      if (key === \"env\") {\n        config.defaults.env = defaultConfig.env;\n      } else if (key === \"trials\") {\n        config.defaults.trials = defaultConfig.trials;\n      } else if (key === \"concurrency\") {\n        config.defaults.concurrency = defaultConfig.concurrency;\n      } else if (key === \"provider\") {\n        config.defaults.provider = defaultConfig.provider;\n      } else if (key === \"model\") {\n        config.defaults.model = defaultConfig.model;\n      } else if (key === \"api\") {\n        config.defaults.api = defaultConfig.api;\n      }\n      saveConfig(config);\n      console.log(chalk.green(`✓ Reset ${args[1]} to default`));\n    } else if (!args[1]) {\n      config.defaults = defaultConfig;\n      saveConfig(config);\n      console.log(chalk.green(\"✓ Reset all settings to defaults\"));\n    } else {\n      console.error(chalk.red(`Error: Unknown config key \"${args[1]}\"`));\n      process.exit(1);\n    }\n  } else if (args[0] === \"path\") {\n    console.log(CONFIG_PATH);\n  } else {\n    console.error(chalk.red(\"Error: Invalid config command\"));\n    console.log(\n      chalk.dim(\"Usage: evals config [set <key> <value> | reset [key] | path]\"),\n    );\n    process.exit(1);\n  }\n}\n\nfunction handleList(args: string[]): void {\n  const config = loadConfig();\n\n  console.log(chalk.blue.bold(\"\\nAvailable Evals\\n\"));\n\n  // Group tasks by category\n  const categories = new Map<string, string[]>();\n  config.tasks.forEach((task) => {\n    task.categories.forEach((cat) => {\n      if (!categories.has(cat)) {\n        categories.set(cat, []);\n      }\n      categories.get(cat)!.push(task.name);\n    });\n  });\n\n  // Show custom eval categories\n  console.log(chalk.magenta.underline(\"Custom Eval Categories\"));\n  Array.from(categories.entries())\n    .filter(([cat]) => !cat.includes(\"external_agent_benchmarks\"))\n    .forEach(([category, tasks]) => {\n      console.log(\n        `  ${chalk.cyan(category)} ${chalk.dim(`(${tasks.length} evals)`)}`,\n      );\n    });\n\n  console.log(chalk.magenta.underline(\"\\nBenchmarks\"));\n  Object.keys(config.benchmarks).forEach((name) => {\n    const shorthand = `b:${name}`;\n    console.log(\n      `  ${chalk.cyan(shorthand.padEnd(20))} ${chalk.dim(`benchmark:${name}`)}`,\n    );\n  });\n\n  if (args.includes(\"--detailed\") || args.includes(\"-d\")) {\n    console.log(chalk.magenta.underline(\"\\n\\nDetailed Task List\"));\n    categories.forEach((tasks, category) => {\n      if (!category.includes(\"external_agent_benchmarks\")) {\n        console.log(chalk.cyan(`\\n${category}:`));\n        tasks.forEach((task) => {\n          console.log(`  - ${task}`);\n        });\n      }\n    });\n  } else {\n    console.log(\n      chalk.yellow(\n        \"\\n💡 Tip: Use 'evals list --detailed' to see all individual tasks\",\n      ),\n    );\n  }\n}\n\nfunction parseArgs(rawArgs: string[]): {\n  options: Record<string, string | number | boolean>;\n  target?: string;\n  filters: Array<[string, string]>;\n} {\n  const options: Record<string, string | number | boolean> = {};\n  const filters: Array<[string, string]> = [];\n  let target: string | undefined;\n\n  for (let i = 0; i < rawArgs.length; i++) {\n    const arg = rawArgs[i];\n\n    if (arg.startsWith(\"-\")) {\n      // Handle options\n      const flagName = arg.replace(/^--?/, \"\");\n\n      // Map short flags to long names\n      const flagMap: Record<string, string> = {\n        e: \"env\",\n        t: \"trials\",\n        c: \"concurrency\",\n        m: \"model\",\n        p: \"provider\",\n        l: \"limit\",\n        s: \"sample\",\n        f: \"filter\",\n      };\n\n      const optionName = flagMap[flagName] || flagName;\n\n      if (optionName === \"api\") {\n        options.api = true;\n      } else if (optionName === \"filter\") {\n        // Parse filter as key=value\n        const filterValue = rawArgs[++i];\n        if (filterValue && filterValue.includes(\"=\")) {\n          const [key, value] = filterValue.split(\"=\");\n          filters.push([key, value]);\n        }\n      } else {\n        // Get next value\n        const value = rawArgs[++i];\n        if (value && !value.startsWith(\"-\")) {\n          // Parse numbers\n          if (\n            [\"trials\", \"concurrency\", \"limit\", \"sample\"].includes(optionName)\n          ) {\n            options[optionName] = parseInt(value, 10);\n          } else {\n            options[optionName] = value;\n          }\n        }\n      }\n    } else if (!target) {\n      target = arg;\n    }\n  }\n\n  return { options, target, filters };\n}\n\nfunction handleRun(args: string[]): void {\n  const config = loadConfig();\n  const { options, target, filters } = parseArgs(args);\n\n  // Merge with defaults\n  const stagehandTarget = (process.env.STAGEHAND_BROWSER_TARGET ?? \"\")\n    .toLowerCase()\n    .trim();\n  if (\n    !options.env &&\n    (stagehandTarget === \"local\" || stagehandTarget === \"browserbase\")\n  ) {\n    options.env = stagehandTarget;\n  }\n  const finalOptions = { ...config.defaults, ...options };\n\n  // Build environment variables\n  const env = { ...process.env };\n\n  // Set core environment variables\n  if (finalOptions.env === \"browserbase\") {\n    env.EVAL_ENV = \"BROWSERBASE\";\n  } else {\n    env.EVAL_ENV = \"LOCAL\";\n  }\n\n  if (finalOptions.api) {\n    env.USE_API = \"true\";\n  }\n\n  if (finalOptions.trials) {\n    env.EVAL_TRIAL_COUNT = String(finalOptions.trials);\n  }\n\n  if (finalOptions.concurrency) {\n    env.EVAL_MAX_CONCURRENCY = String(finalOptions.concurrency);\n  }\n\n  if (finalOptions.provider) {\n    env.EVAL_PROVIDER = finalOptions.provider;\n  }\n\n  if (finalOptions.model) {\n    env.EVAL_MODEL_OVERRIDE = finalOptions.model;\n  }\n\n  // Handle benchmark-specific options\n  let evalName: string | undefined;\n  let categoryFilter: string | undefined;\n\n  if (target) {\n    if (target.startsWith(\"b:\") || target.startsWith(\"benchmark:\")) {\n      // Running a benchmark\n      const benchmarkName = target.replace(/^(b:|benchmark:)/, \"\");\n\n      if (!config.benchmarks[benchmarkName]) {\n        console.error(chalk.red(`Error: Unknown benchmark \"${benchmarkName}\"`));\n        console.log(\n          chalk.dim(\n            `Available benchmarks: ${Object.keys(config.benchmarks).join(\", \")}`,\n          ),\n        );\n        process.exit(1);\n      }\n\n      // Map to the actual eval name\n      const benchmarkMap: Record<string, string> = {\n        webbench: \"agent/webbench\",\n        gaia: \"agent/gaia\",\n        webvoyager: \"agent/webvoyager\",\n        osworld: \"agent/osworld\",\n        onlineMind2Web: \"agent/onlineMind2Web\",\n        webtailbench: \"agent/webtailbench\",\n      };\n\n      evalName = benchmarkMap[benchmarkName];\n      env.EVAL_DATASET = benchmarkName;\n\n      // Set benchmark-specific options\n      if (options.limit) {\n        env.EVAL_MAX_K = String(options.limit);\n        env[`EVAL_${benchmarkName.toUpperCase()}_LIMIT`] = String(\n          options.limit,\n        );\n      }\n\n      if (options.sample) {\n        env[`EVAL_${benchmarkName.toUpperCase()}_SAMPLE`] = String(\n          options.sample,\n        );\n      }\n\n      // Apply filters\n      filters.forEach(([key, value]) => {\n        const envKey = `EVAL_${benchmarkName.toUpperCase()}_${key.toUpperCase()}`;\n        env[envKey] = value;\n      });\n    } else if (target === \"all\") {\n      // Run all evals (no filter)\n    } else if (target.includes(\"/\") || target.includes(\"*\")) {\n      // Pattern matching - treat as eval name\n      evalName = target;\n    } else {\n      // Check if it's a category\n      const categories = new Set<string>();\n      config.tasks.forEach((task) => {\n        task.categories.forEach((cat) => categories.add(cat));\n      });\n\n      if (categories.has(target)) {\n        categoryFilter = target;\n      } else {\n        // Assume it's a specific eval name\n        evalName = target;\n      }\n    }\n  }\n\n  // Build the legacy command\n  const legacyArgs: string[] = [];\n\n  if (evalName) {\n    legacyArgs.push(`name=${evalName}`);\n  } else if (categoryFilter) {\n    legacyArgs.push(\"category\", categoryFilter);\n  }\n\n  // Run the existing eval system with our environment\n  console.log(chalk.blue.bold(\"\\nRunning evals...\\n\"));\n\n  // Build first if needed\n  const buildChild = spawn(\"pnpm\", [\"run\", \"build\"], {\n    stdio: \"inherit\",\n    shell: true,\n  });\n\n  buildChild.on(\"exit\", (buildCode) => {\n    if (buildCode !== 0) {\n      process.exit(buildCode || 1);\n    }\n\n    const compiledEvalPath = path.resolve(\n      moduleDir,\n      \"..\",\n      \"esm\",\n      \"index.eval.js\",\n    );\n    // When built to packages/evals/dist/cli/cli.js, moduleDir is packages/evals/dist/cli/\n    // Source is at packages/evals/index.eval.ts from repo root\n    const sourceEvalPath = path.resolve(\n      moduleDir,\n      \"..\",\n      \"..\",\n      \"packages\",\n      \"evals\",\n      \"index.eval.ts\",\n    );\n\n    let child;\n\n    if (fs.existsSync(compiledEvalPath)) {\n      child = spawn(process.execPath, [compiledEvalPath, ...legacyArgs], {\n        env,\n        stdio: \"inherit\",\n        shell: true,\n      });\n    } else {\n      let tsxCliPath: string | undefined;\n      try {\n        // Resolve the local tsx CLI entry within this package installation\n        // This avoids requiring a globally installed tsx binary\n        tsxCliPath = require.resolve(\"tsx/dist/cli.js\");\n      } catch {\n        // no-op; will fall back to shell-resolved \"tsx\" if not found\n      }\n\n      const tsxArgs = [sourceEvalPath, ...legacyArgs];\n\n      if (tsxCliPath) {\n        child = spawn(process.execPath, [tsxCliPath, ...tsxArgs], {\n          env,\n          stdio: \"inherit\",\n          shell: true,\n        });\n      } else {\n        child = spawn(\"tsx\", tsxArgs, {\n          env,\n          stdio: \"inherit\",\n          shell: true,\n        });\n      }\n    }\n\n    child.on(\"exit\", (code) => {\n      process.exit(code || 0);\n    });\n\n    // Forward SIGINT (Ctrl+C) and SIGTERM to child process\n    process.on(\"SIGINT\", () => {\n      console.log(\"\\n\\nReceived SIGINT, killing child process...\");\n      child.kill(\"SIGINT\");\n      setTimeout(() => {\n        child.kill(\"SIGKILL\");\n        process.exit(130);\n      }, 1000);\n    });\n\n    process.on(\"SIGTERM\", () => {\n      console.log(\"\\n\\nReceived SIGTERM, killing child process...\");\n      child.kill(\"SIGTERM\");\n      setTimeout(() => {\n        child.kill(\"SIGKILL\");\n        process.exit(143);\n      }, 1000);\n    });\n  });\n}\n\n// Main CLI logic\nfunction main(): void {\n  const args = process.argv.slice(2);\n  const command = args[0];\n  const commandArgs = args.slice(1);\n\n  switch (command) {\n    case \"run\":\n      handleRun(commandArgs);\n      break;\n\n    case \"list\":\n      handleList(commandArgs);\n      break;\n\n    case \"config\":\n      handleConfig(commandArgs);\n      break;\n\n    case \"help\":\n    case \"--help\":\n    case \"-h\":\n      printHelp();\n      break;\n\n    case undefined:\n      console.error(chalk.red(\"Error: No command specified\"));\n      printHelp();\n      process.exit(1);\n      break;\n\n    default:\n      // Check if it's a direct target (backward compatibility)\n      if (!command.startsWith(\"-\")) {\n        handleRun(args);\n      } else {\n        console.error(chalk.red(`Error: Unknown command \"${command}\"`));\n        printHelp();\n        process.exit(1);\n      }\n  }\n}\n\n// Run the CLI\nmain();\n"
  },
  {
    "path": "packages/evals/datasets/gaia/GAIA_web.jsonl",
    "content": "{\"task_id\": \"e1fc63a2-da7a-432f-be78-7c4a95598703\", \"Level\": 1, \"Final answer\": \"17\", \"id\": \"level1-0\", \"web\": \"https://www.google.com/\", \"ques\": \"If Eliud Kipchoge could maintain his record-making marathon pace indefinitely, how many thousand hours would it take him to run the distance between the Earth and the Moon its closest approach? Please use the minimum perigee value on the Wikipedia page for the Moon when carrying out your calculation. Round your result to the nearest 1000 hours and do not use any comma separators if necessary.\"}\n{\"task_id\": \"8e867cd7-cff9-4e6c-867a-ff5ddc2550be\", \"Level\": 1, \"Final answer\": \"3\", \"id\": \"level1-1\", \"web\": \"https://www.google.com/\", \"ques\": \"How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.\"}\n{\"task_id\": \"5d0080cb-90d7-4712-bc33-848150e917d3\", \"Level\": 1, \"Final answer\": \"0.1777\", \"id\": \"level1-2\", \"web\": \"https://www.google.com/\", \"ques\": \"What was the volume in m^3 of the fish bag that was calculated in the University of Leicester paper \\\"Can Hiccup Supply Enough Fish to Maintain a Dragon\\u2019s Diet?\\\"\"}\n{\"task_id\": \"a1e91b78-d3d8-4675-bb8d-62741b4b68a6\", \"Level\": 1, \"Final answer\": \"3\", \"id\": \"level1-3\", \"web\": \"https://www.google.com/\", \"ques\": \"In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?\"}\n{\"task_id\": \"46719c30-f4c3-4cad-be07-d5cb21eee6bb\", \"Level\": 1, \"Final answer\": \"Mapping Human Oriented Information to Software Agents for Online Systems Usage\", \"id\": \"level1-4\", \"web\": \"https://www.google.com/\", \"ques\": \"Of the authors (First M. Last) that worked on the paper \\\"Pie Menus or Linear Menus, Which Is Better?\\\" in 2015, what was the title of the first paper authored by the one that had authored prior papers?\"}\n{\"task_id\": \"4b6bb5f7-f634-410e-815d-e673ab7f8632\", \"Level\": 1, \"Final answer\": \"THE CASTLE\", \"id\": \"level1-5\", \"web\": \"https://www.google.com/\", \"ques\": \"In Series 9, Episode 11 of Doctor Who, the Doctor is trapped inside an ever-shifting maze. What is this location called in the official script for the episode? Give the setting exactly as it appears in the first scene heading.\"}\n{\"task_id\": \"b816bfce-3d80-4913-a07d-69b752ce6377\", \"Level\": 1, \"Final answer\": \"fluffy\", \"id\": \"level1-6\", \"web\": \"https://www.google.com/\", \"ques\": \"In Emily Midkiff's June 2014 article in a journal named for the one of Hreidmar's sons that guarded his house, what word was quoted from two different authors in distaste for the nature of dragon depictions?\"}\n{\"task_id\": \"72e110e7-464c-453c-a309-90a95aed6538\", \"Level\": 1, \"Final answer\": \"Guatemala\", \"id\": \"level1-7\", \"web\": \"https://www.google.com/\", \"ques\": \"Under DDC 633 on Bielefeld University Library's BASE, as of 2020, from what country was the unknown language article with a flag unique from the others?\"}\n{\"task_id\": \"b415aba4-4b68-4fc6-9b89-2c812e55a3e1\", \"Level\": 1, \"Final answer\": \"diamond\", \"id\": \"level1-8\", \"web\": \"https://www.google.com/\", \"ques\": \"In Nature journal's Scientific Reports conference proceedings from 2012, in the article that did not mention plasmons or plasmonics, what nano-compound is studied? Don't use the prefix nano in your answer if there is one.\"}\n{\"task_id\": \"935e2cff-ae78-4218-b3f5-115589b19dae\", \"Level\": 1, \"Final answer\": \"research\", \"id\": \"level1-9\", \"web\": \"https://www.google.com/\", \"ques\": \"In the year 2022, and before December, what does \\\"R\\\" stand for in the three core policies of the type of content that was violated in the public logs on the Legume Wikipedia page?\"}\n{\"task_id\": \"4fc2f1ae-8625-45b5-ab34-ad4433bc21f8\", \"Level\": 1, \"Final answer\": \"FunkMonk\", \"id\": \"level1-10\", \"web\": \"https://www.google.com/\", \"ques\": \"Who nominated the only Featured Article on English Wikipedia about a dinosaur that was promoted in November 2016?\"}\n{\"task_id\": \"5188369a-3bbe-43d8-8b94-11558f909a08\", \"Level\": 1, \"Final answer\": \"Annie Levin\", \"id\": \"level1-11\", \"web\": \"https://www.google.com/\", \"ques\": \"What writer is quoted by Merriam-Webster for the Word of the Day from June 27, 2022?\"}\n{\"task_id\": \"9d191bce-651d-4746-be2d-7ef8ecadb9c2\", \"Level\": 1, \"Final answer\": \"Extremely\", \"id\": \"level1-12\", \"web\": \"https://www.google.com/\", \"ques\": \"Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec.\\n\\nWhat does Teal'c say in response to the question \\\"Isn't that hot?\\\"\"}\n{\"task_id\": \"cabe07ed-9eca-40ea-8ead-410ef5e83f91\", \"Level\": 1, \"Final answer\": \"Louvrier\", \"id\": \"level1-13\", \"web\": \"https://www.google.com/\", \"ques\": \"What is the surname of the equine veterinarian mentioned in 1.E Exercises from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023?\"}\n{\"task_id\": \"d0633230-7067-47a9-9dbf-ee11e0a2cdd6\", \"Level\": 1, \"Final answer\": \"BaseLabelPropagation\", \"id\": \"level1-14\", \"web\": \"https://www.google.com/\", \"ques\": \"In the Scikit-Learn July 2017 changelog, what other predictor base command received a bug fix? Just give the name, not a path.\"}\n{\"task_id\": \"0383a3ee-47a7-41a4-b493-519bdefe0488\", \"Level\": 1, \"Final answer\": \"Rockhopper penguin\", \"id\": \"level1-15\", \"web\": \"https://www.google.com/\", \"ques\": \"On the BBC Earth YouTube video of the Top 5 Silliest Animal Moments, what species of bird is featured?\"}\n{\"task_id\": \"11af4e1a-5f45-467d-9aeb-46f4bb0bf034\", \"Level\": 1, \"Final answer\": \"6\", \"id\": \"level1-16\", \"web\": \"https://www.google.com/\", \"ques\": \"How many more blocks (also denoted as layers) in BERT base encoder than the encoder from the architecture proposed in Attention is All You Need?\"}\n{\"task_id\": \"7673d772-ef80-4f0f-a602-1bf4485c9b43\", \"Level\": 1, \"Final answer\": \"inference\", \"id\": \"level1-17\", \"web\": \"https://www.google.com/\", \"ques\": \"On Cornell Law School website's legal information institute, under the fifth section of federal rules alphabetically, what word was deleted in the last amendment to the first rule in the article that has \\\"witnesses\\\" in the most titles as of 2021?\"}\n{\"task_id\": \"c365c1c7-a3db-4d5e-a9a1-66f56eae7865\", \"Level\": 1, \"Final answer\": \"Braintree, Honolulu\", \"id\": \"level1-18\", \"web\": \"https://www.google.com/\", \"ques\": \"Of the cities within the United States where U.S. presidents were born, which two are the farthest apart from the westernmost to the easternmost going east, giving the city names only? Give them to me in alphabetical order, in a comma-separated list\"}\n{\"task_id\": \"7d4a7d1d-cac6-44a8-96e8-ea9584a70825\", \"Level\": 1, \"Final answer\": \"22\", \"id\": \"level1-19\", \"web\": \"https://www.google.com/\", \"ques\": \"According to Girls Who Code, how long did it take in years for the percentage of computer scientists that were women to change by 13% from a starting point of 37%?\"}\n{\"task_id\": \"dc22a632-937f-4e6a-b72f-ba0ff3f5ff97\", \"Level\": 1, \"Final answer\": \"Five Hundred Things To Eat Before It's Too Late: and the Very Best Places to Eat Them\", \"id\": \"level1-20\", \"web\": \"https://www.google.com/\", \"ques\": \"What was the complete title of the book in which two James Beard Award winners recommended the restaurant where Ali Khan enjoyed a New Mexican staple in his cost-conscious TV show that started in 2015? Write the numbers in plain text if there are some in the title.\"}\n{\"task_id\": \"3f57289b-8c60-48be-bd80-01f8099ca449\", \"Level\": 1, \"Final answer\": \"519\", \"id\": \"level1-21\", \"web\": \"https://www.google.com/\", \"ques\": \"How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?\"}\n{\"task_id\": \"23dd907f-1261-4488-b21c-e9185af91d5e\", \"Level\": 1, \"Final answer\": \"2\", \"id\": \"level1-22\", \"web\": \"https://www.google.com/\", \"ques\": \"In Audre Lorde\\u2019s poem \\u201cFather Son and Holy Ghost\\u201d, what is the number of the stanza in which some lines are indented?\"}\n{\"task_id\": \"840bfca7-4f7b-481a-8794-c560c340185d\", \"Level\": 1, \"Final answer\": \"80GSFC21M0002\", \"id\": \"level1-23\", \"web\": \"https://www.google.com/\", \"ques\": \"On June 6, 2023, an article by Carolyn Collins Petersen was published in Universe Today. This article mentions a team that produced a paper about their observations, linked at the bottom of the article. Find this paper. Under what NASA award number was the work performed by R. G. Arendt supported by?\"}\n{\"task_id\": \"a0068077-79f4-461a-adfe-75c1a4148545\", \"Level\": 1, \"Final answer\": \"90\", \"id\": \"level1-24\", \"web\": \"https://www.google.com/\", \"ques\": \"What was the actual enrollment count of the clinical trial on H. pylori in acne vulgaris patients from Jan-May 2018 as listed on the NIH website?\"}\n{\"task_id\": \"bda648d7-d618-4883-88f4-3466eabd860e\", \"Level\": 1, \"Final answer\": \"Saint Petersburg\", \"id\": \"level1-25\", \"web\": \"https://www.google.com/\", \"ques\": \"Where were the Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper eventually deposited? Just give me the city name without abbreviations.\"}\n{\"task_id\": \"c61d22de-5f6c-4958-a7f6-5e9707bd3466\", \"Level\": 2, \"Final answer\": \"egalitarian\", \"id\": \"level2-0\", \"web\": \"https://www.google.com/\", \"ques\": \"A paper about AI regulation that was originally submitted to arXiv.org in June 2022 shows a figure with three axes, where each axis has a label word at both ends. Which of these words is used to describe a type of society in a Physics and Society article submitted to arXiv.org on August 11, 2016?\"}\n{\"task_id\": \"17b5a6a3-bc87-42e8-b0fb-6ab0781ef2cc\", \"Level\": 2, \"Final answer\": \"34689\", \"id\": \"level2-1\", \"web\": \"https://www.google.com/\", \"ques\": \"I\\u2019m researching species that became invasive after people who kept them as pets released them. There\\u2019s a certain species of fish that was popularized as a pet by being the main character of the movie Finding Nemo. According to the USGS, where was this fish found as a nonnative species, before the year 2020? I need the answer formatted as the five-digit zip codes of the places the species was found, separated by commas if there is more than one place.\"}\n{\"task_id\": \"04a04a9b-226c-43fd-b319-d5e89743676f\", \"Level\": 2, \"Final answer\": \"41\", \"id\": \"level2-2\", \"web\": \"https://www.google.com/\", \"ques\": \"If we assume all articles published by Nature in 2020 (articles, only, not book reviews/columns, etc) relied on statistical significance to justify their findings and they on average came to a p-value of 0.04, how many papers would be incorrect as to their claims of statistical significance? Round the value up to the next integer.\"}\n{\"task_id\": \"14569e28-c88c-43e4-8c32-097d35b9a67d\", \"Level\": 2, \"Final answer\": \"backtick\", \"id\": \"level2-3\", \"web\": \"https://www.google.com/\", \"ques\": \"In Unlambda, what exact charcter or text needs to be added to correct the following code to output \\\"For penguins\\\"? If what is needed is a character, answer with the name of the character. If there are different names for the character, use the shortest. The text location is not needed. Code:\\n\\n`r```````````.F.o.r. .p.e.n.g.u.i.n.si\"}\n{\"task_id\": \"3627a8be-a77f-41bb-b807-7e1bd4c0ebdf\", \"Level\": 2, \"Final answer\": \"142\", \"id\": \"level2-4\", \"web\": \"https://www.google.com/\", \"ques\": \"The object in the British Museum's collection with a museum number of 2012,5015.17 is the shell of a particular mollusk species. According to the abstract of a research article published in Science Advances in 2021, beads made from the shells of this species were found that are at least how many thousands of years old?\"}\n{\"task_id\": \"7619a514-5fa8-43ef-9143-83b66a43d7a4\", \"Level\": 2, \"Final answer\": \"04/15/18\", \"id\": \"level2-5\", \"web\": \"https://www.google.com/\", \"ques\": \"According to github, when was Regression added to the oldest closed numpy.polynomial issue that has the Regression label in MM/DD/YY?\"}\n{\"task_id\": \"2a649bb1-795f-4a01-b3be-9a01868dae73\", \"Level\": 2, \"Final answer\": \"3.1.3.1; 1.11.1.7\", \"id\": \"level2-6\", \"web\": \"https://www.google.com/\", \"ques\": \"What are the EC numbers of the two most commonly used chemicals for the virus testing method in the paper about SPFMV and SPCSV in the Pearl Of Africa from 2016? Return the semicolon-separated numbers in the order of the alphabetized chemicals.\"}\n{\"task_id\": \"87c610df-bef7-4932-b950-1d83ef4e282b\", \"Level\": 2, \"Final answer\": \"Morarji Desai\", \"id\": \"level2-7\", \"web\": \"https://www.google.com/\", \"ques\": \"In April of 1977, who was the Prime Minister of the first place mentioned by name in the Book of Esther (in the New International Version)?\"}\n{\"task_id\": \"624cbf11-6a41-4692-af9c-36b3e5ca3130\", \"Level\": 2, \"Final answer\": \"So we had to let it die.\", \"id\": \"level2-8\", \"web\": \"https://www.google.com/\", \"ques\": \"What's the last line of the rhyme under the flavor name on the headstone visible in the background of the photo of the oldest flavor's headstone in the Ben & Jerry's online flavor graveyard as of the end of 2022?\"}\n{\"task_id\": \"dd3c7503-f62a-4bd0-9f67-1b63b94194cc\", \"Level\": 2, \"Final answer\": \"6\", \"id\": \"level2-9\", \"web\": \"https://www.google.com/\", \"ques\": \"Use density measures from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023.\\n\\nI have a gallon of honey and a gallon of mayonnaise at 25C. I remove one cup of honey at a time from the gallon of honey. How many times will I need to remove a cup to have the honey weigh less than the mayonaise? Assume the containers themselves weigh the same.\"}\n{\"task_id\": \"f0f46385-fc03-4599-b5d3-f56496c3e69f\", \"Level\": 2, \"Final answer\": \"Indonesia, Myanmar\", \"id\": \"level2-10\", \"web\": \"https://www.google.com/\", \"ques\": \"In terms of geographical distance between capital cities, which 2 countries are the furthest from each other within the ASEAN bloc according to wikipedia? Answer using a comma separated list, ordering the countries by alphabetical order.\"}\n{\"task_id\": \"e4e91f1c-1dcd-439e-9fdd-cb976f5293fd\", \"Level\": 2, \"Final answer\": \"cloak\", \"id\": \"level2-11\", \"web\": \"https://www.google.com/\", \"ques\": \"I need to fact-check a citation. This is the citation from the bibliography:\\n\\nGreetham, David. \\\"Uncoupled: OR, How I Lost My Author(s).\\\" Textual Cultures: Texts, Contexts, Interpretation, vol. 3 no. 1, 2008, p. 45-46. Project MUSE, doi:10.2979/tex.2008.3.1.44.\\n\\nAnd this is the in-line citation:\\n\\nOur relationship with the authors of the works we read can often be \\u201cobscured not by a \\\"cloak of print\\\" but by the veil of scribal confusion and mis-transmission\\u201d (Greetham 45-46).\\n\\nDoes the quoted text match what is actually in the article? If Yes, answer Yes, otherwise, give me the word in my citation that does not match with the correct one (without any article).\"}\n{\"task_id\": \"56137764-b4e0-45b8-9c52-1866420c3df5\", \"Level\": 2, \"Final answer\": \"Li Peng\", \"id\": \"level2-12\", \"web\": \"https://www.google.com/\", \"ques\": \"Which contributor to the version of OpenCV where support was added for the Mask-RCNN model has the same name as a former Chinese head of government when the names are transliterated to the Latin alphabet?\"}\n{\"task_id\": \"8b3379c0-0981-4f5b-8407-6444610cb212\", \"Level\": 2, \"Final answer\": \"1.8\", \"id\": \"level2-13\", \"web\": \"https://www.google.com/\", \"ques\": \"What is the maximum length in meters of #9 in the first National Geographic short on YouTube that was ever released according to the Monterey Bay Aquarium website? Just give the number.\"}\n{\"task_id\": \"0ff53813-3367-4f43-bcbd-3fd725c1bf4b\", \"Level\": 2, \"Final answer\": \"beta geometric\", \"id\": \"level2-14\", \"web\": \"https://www.google.com/\", \"ques\": \"What two-word type of model did Manash Pratim Kashyap's and PS Fader's studies in customer retention studies published during 2018-2019 have in common (no punctuation)?\"}\n{\"task_id\": \"a7feb290-76bb-4cb7-8800-7edaf7954f2f\", \"Level\": 2, \"Final answer\": \"31\", \"id\": \"level2-15\", \"web\": \"https://www.google.com/\", \"ques\": \"How many High Energy Physics - Lattice articles listed in January 2020 on Arxiv had ps versions available?\"}\n{\"task_id\": \"b4cc024b-3f5e-480e-b96a-6656493255b5\", \"Level\": 2, \"Final answer\": \"Russian-German Legion\", \"id\": \"level2-16\", \"web\": \"https://www.google.com/\", \"ques\": \"The photograph in the Whitney Museum of American Art's collection with accession number 2022.128 shows a person holding a book. Which military unit did the author of this book join in 1813? Answer without using articles.\"}\n{\"task_id\": \"33d8ea3b-6c6b-4ff1-803d-7e270dea8a57\", \"Level\": 2, \"Final answer\": \"2\", \"id\": \"level2-17\", \"web\": \"https://www.google.com/\", \"ques\": \"What is the minimum number of page links a person must click on to go from the english Wikipedia page on The Lord of the Rings (the book) to the english Wikipedia page on A Song of Ice and Fire (the book series)? In your count, include each link you would click on to get to the page. Use the pages as they appeared at the end of the day on July 3, 2023.\"}\n{\"task_id\": \"e8cb5b03-41e0-4086-99e5-f6806cd97211\", \"Level\": 2, \"Final answer\": \"shrimp\", \"id\": \"level2-18\", \"web\": \"https://www.google.com/\", \"ques\": \"I went to Virtue restaurant & bar in Chicago for my birthday on March 22, 2021 and the main course I had was delicious!  Unfortunately, when I went back about a month later on April 21, it was no longer on the dinner menu.  Using the Wayback Machine, can you help me figure out which main course was on the dinner menu for Virtue on March 22, 2021 but not April 21, 2021? Answer using the singular form, without articles.\"}\n{\"task_id\": \"f46b4380-207e-4434-820b-f32ce04ae2a4\", \"Level\": 2, \"Final answer\": \"Harbinger, Tidal\", \"id\": \"level2-19\", \"web\": \"https://www.google.com/\", \"ques\": \"It is 1999. Before you party like it is 1999, please assist me in settling a bet.\\n\\nFiona Apple and Paula Cole released albums prior to 1999. Of these albums, which didn't receive a letter grade from Robert Christgau? Provide your answer as a comma delimited list of album titles, sorted alphabetically.\"}\n{\"task_id\": \"05407167-39ec-4d3a-a234-73a9120c325d\", \"Level\": 2, \"Final answer\": \"Format Document\", \"id\": \"level2-20\", \"web\": \"https://www.google.com/\", \"ques\": \"In the 2018 VSCode blog post on replit.com, what was the command they clicked on in the last video to remove extra lines?\"}\n{\"task_id\": \"b9763138-c053-4832-9f55-86200cb1f99c\", \"Level\": 2, \"Final answer\": \"3\", \"id\": \"level2-21\", \"web\": \"https://www.google.com/\", \"ques\": \"Compute the check digit the Tropicos ID for the Order Helotiales would have if it were an ISBN-10 number.\"}\n{\"task_id\": \"16d825ff-1623-4176-a5b5-42e0f5c2b0ac\", \"Level\": 2, \"Final answer\": \"6:41 PM\", \"id\": \"level2-22\", \"web\": \"https://www.google.com/\", \"ques\": \"What time was the Tri-Rail train that carried the most passengers on May 27, 2019 scheduled to arrive in Pompano Beach? Express your answer in the 12-hour digital clock format without leading zero if any, and include whether it is AM or PM.\"}\n{\"task_id\": \"544b7f0c-173a-4377-8d56-57b36eb26ddf\", \"Level\": 2, \"Final answer\": \"A Nightmare on Elm Street\", \"id\": \"level2-23\", \"web\": \"https://www.google.com/\", \"ques\": \"In Valentina Re\\u2019s contribution to the 2017 book \\u201cWorld Building: Transmedia, Fans, Industries\\u201d, what horror movie does the author cite as having popularized metalepsis between a dream world and reality? Use the complete name with article if any.\"}\n{\"task_id\": \"6b078778-0b90-464d-83f6-59511c811b01\", \"Level\": 2, \"Final answer\": \"Alfonso Visconti\", \"id\": \"level2-24\", \"web\": \"https://www.google.com/\", \"ques\": \"The Metropolitan Museum of Art has a portrait in its collection with an accession number of 29.100.5. Of the consecrators and co-consecrators of this portrait's subject as a bishop, what is the name of the one who never became pope?\"}\n{\"task_id\": \"08cae58d-4084-4616-b6dd-dd6534e4825b\", \"Level\": 2, \"Final answer\": \"2018\", \"id\": \"level2-25\", \"web\": \"https://www.google.com/\", \"ques\": \"According to Google Finance, when was the first year the Apple stock went above $50 (without adjusting for stock split)?\"}\n{\"task_id\": \"2dfc4c37-fec1-4518-84a7-10095d30ad75\", \"Level\": 2, \"Final answer\": \"6\", \"id\": \"level2-26\", \"web\": \"https://www.google.com/\", \"ques\": \"According to Box Office Mojo's 2020 Worldwide Box Office list, how many of the top 10 highest-grossing worldwide movies are also on the top 10 highest-grossing domestic movies? Your answer should be a numerical integer value.\"}\n{\"task_id\": \"9f41b083-683e-4dcf-9185-ccfeaa88fa45\", \"Level\": 2, \"Final answer\": \"0\", \"id\": \"level2-27\", \"web\": \"https://www.google.com/\", \"ques\": \"How many pages if the 2023 IPCC report (85 pages version) mentions nuclear energy?\"}\n{\"task_id\": \"ecbc4f94-95a3-4cc7-b255-6741a458a625\", \"Level\": 2, \"Final answer\": \"13\", \"id\": \"level2-28\", \"web\": \"https://www.google.com/\", \"ques\": \"How many images are there in the latest 2022 Lego english wikipedia article?\"}\n{\"task_id\": \"71345b0a-9c7d-4b50-b2bf-937ec5879845\", \"Level\": 2, \"Final answer\": \"Here be dragons\", \"id\": \"level2-29\", \"web\": \"https://www.google.com/\", \"ques\": \"On a leap day before the year 2008, a joke was removed from the Wikipedia page for \\u201cDragon\\u201d. What was the phrase that was removed? Give the phrase as it appeared on the page, but without punctuation.\"}\n{\"task_id\": \"7b5377b0-3f38-4103-8ad2-90fe89864c04\", \"Level\": 2, \"Final answer\": \"563.9\", \"id\": \"level2-30\", \"web\": \"https://www.google.com/\", \"ques\": \"Find the value of x to the nearest tenth: Lx = (d/dx * (A * x-squared)) + 4-thousand'n'ninety-7 minus C\\nWhere L is the last two digits of the year of the Venezuelan Declaration of Independence,\\nA is the number of colors in the TikTok logo as of July 2023, excluding black and white,\\nand C is the height of the average woman in the Philippines according to a July 2023 Business Insider article, rounded to the nearest whole centimeter\"}\n{\"task_id\": \"114d5fd0-e2ae-4b6d-a65a-870da2d19c08\", \"Level\": 2, \"Final answer\": \"4\", \"id\": \"level2-31\", \"web\": \"https://www.google.com/\", \"ques\": \"In the endnote found in the second-to-last paragraph of page 11 of the book with the doi 10.2307/j.ctv9b2xdv, what date in November was the Wikipedia article accessed? Just give the day of the month.\"}\n{\"task_id\": \"ad37a656-079a-49f9-a493-7b739c9167d1\", \"Level\": 2, \"Final answer\": \"Bravo\", \"id\": \"level2-32\", \"web\": \"https://www.google.com/\", \"ques\": \"On July 15, 2008, Phys.org published an article about a catastrophe. Find the explosive force of this catastrophe according to Encyclopedia Britannica, then find the name of the US nuclear test that had the same yield. Your answer should only be the last word of the name of the test.\"}\n{\"task_id\": \"f3917a3d-1d17-4ee2-90c5-683b072218fe\", \"Level\": 2, \"Final answer\": \"2732\", \"id\": \"level2-33\", \"web\": \"https://www.google.com/\", \"ques\": \"How many edits were made to the Wikipedia page on Antidisestablishmentarianism from its inception until June of 2023?\"}\n{\"task_id\": \"48eb8242-1099-4c26-95d4-ef22b002457a\", \"Level\": 2, \"Final answer\": \"6\", \"id\": \"level2-34\", \"web\": \"https://www.google.com/\", \"ques\": \"How many nonindigenous crocodiles were found in Florida from the year 2000 through 2020? You can get the data from the USGS Nonindigenous Aquatic Species database.\"}\n{\"task_id\": \"c8b7e059-c60d-472e-ad64-3b04ae1166dc\", \"Level\": 2, \"Final answer\": \"8\", \"id\": \"level2-35\", \"web\": \"https://www.google.com/\", \"ques\": \"The work referenced in footnote 397 of Federico Lauria's 2014 dissertation is also the source for the titles of two paintings in the Smithsonian American Art Museum's collection, as of August 2023. What is the absolute difference between the chapter numbers of the chapters that the titles of these two paintings quote?\"}\n{\"task_id\": \"d1af70ea-a9a4-421a-b9cc-94b5e02f1788\", \"Level\": 2, \"Final answer\": \"736455\", \"id\": \"level2-36\", \"web\": \"https://www.google.com/\", \"ques\": \"As of the 2020 census, what was the population difference between the largest county seat and smallest county seat, by land area of the county seat, in Washington state? For population figures, please use the official data from data.census.gov. Please report the integer difference.\"}\n{\"task_id\": \"ded28325-3447-4c56-860f-e497d6fb3577\", \"Level\": 2, \"Final answer\": \"Picnic is in Ploybius Plaza.\", \"id\": \"level2-37\", \"web\": \"https://www.google.com/\", \"ques\": \"This is a secret message my friend gave me. It says where we should meet for our picnic on Friday. The only problem is, it\\u2019s encrypted in the Caesar cipher, so I can\\u2019t read it. Can you tell me what it says? This is the message:\\n\\nZsmxsm sc sx Zyvilsec Zvkjk.\"}\n{\"task_id\": \"d700d50d-c707-4dca-90dc-4528cddd0c80\", \"Level\": 2, \"Final answer\": \"Roger Miller\", \"id\": \"level2-38\", \"web\": \"https://www.google.com/\", \"ques\": \"Who composed the song that was performed by a rooster and a hamster in separate animated videos at separate tempos with different lyrics? Answer using the format First name Last name.\"}\n{\"task_id\": \"0a3cd321-3e76-4622-911b-0fda2e5d6b1a\", \"Level\": 2, \"Final answer\": \"Brunei, China, Morocco, Singapore\", \"id\": \"level2-39\", \"web\": \"https://www.google.com/\", \"ques\": \"According to the World Bank, which countries had gross savings of over 35% of GDP for every year in the period 2001-2010? Give your answer as a comma-separated list of countries in alphabetical order. Use the countries most common names in english when answering.\"}\n{\"task_id\": \"f2feb6a4-363c-4c09-a804-0db564eafd68\", \"Level\": 2, \"Final answer\": \"900000\", \"id\": \"level2-40\", \"web\": \"https://www.google.com/\", \"ques\": \"I\\u2019m thinking about selling my home, so I want to learn more about how homes in my area sold recently. I live in Pearl City, Hawaii, which is on the island of Oahu. I know two homes near me that sold in 2022 were 2072 Akaikai Loop, and 2017 Komo Mai Drive. Find which of those homes sold for more in 2022, and tell me how much it sold for. Don\\u2019t put commas or decimal places in the answer.\"}\n{\"task_id\": \"0b260a57-3f3a-4405-9f29-6d7a1012dbfb\", \"Level\": 2, \"Final answer\": \"0.269\", \"id\": \"level2-41\", \"web\": \"https://www.google.com/\", \"ques\": \"On ScienceDirect, what is the difference to 3 decimal places in the sample standard deviations of the number of Reference Works in each Life Science domain compared to Health Sciences as of 2022?\"}\n{\"task_id\": \"ed58682d-bc52-4baa-9eb0-4eb81e1edacc\", \"Level\": 2, \"Final answer\": \"stare\", \"id\": \"level2-42\", \"web\": \"https://www.google.com/\", \"ques\": \"What is the last word before the second chorus of the King of Pop's fifth single from his sixth studio album?\"}\n{\"task_id\": \"023e9d44-96ae-4eed-b912-244ee8c3b994\", \"Level\": 2, \"Final answer\": \"8\", \"id\": \"level2-43\", \"web\": \"https://www.google.com/\", \"ques\": \"It's May 2023, and I'm about to drive across the U.S. from California to Maine. I always recycle my water bottles at the end of a trip, and I drink 5 12-ounce water bottles for every 100 miles I travel, rounded to the nearest 100. Assuming I follow I-40 from Los Angeles to Cincinnati, then take I-90 from Cincinnati to Augusta, how many dollars will I get back according to Wikipedia?\"}\n{\"task_id\": \"0e9e85b8-52b9-4de4-b402-5f635ab9631f\", \"Level\": 2, \"Final answer\": \"1927\", \"id\": \"level2-44\", \"web\": \"https://www.google.com/\", \"ques\": \"What is the latest chronological year date written in the image on the webpage found when following the first citation reference link on the latest version of Carl Nebel's Wikipedia page as of August 2023?\"}\n{\"task_id\": \"20194330-9976-4043-8632-f8485c6c71b2\", \"Level\": 2, \"Final answer\": \"4\", \"id\": \"level2-45\", \"web\": \"https://www.google.com/\", \"ques\": \"The YouTube channel Game Grumps began a Let\\u2019s Play of the game Sonic the Hedgehog (2006) in the year 2012. Thirty seconds into the first episode, a phrase is shown on the screen in white letters on a red background. How many times does the letter \\\"E\\\" appear in this phrase?\"}\n{\"task_id\": \"65638e28-7f37-4fa7-b7b9-8c19bb609879\", \"Level\": 2, \"Final answer\": \"Kleinpaul\", \"id\": \"level2-46\", \"web\": \"https://www.google.com/\", \"ques\": \"The book with the doi 10.1353/book.24372 concerns a certain neurologist. According to chapter 2 of the book, what author influenced this neurologist\\u2019s belief in \\u201cendopsychic myths\\u201d? Give the last name only.\"}\n{\"task_id\": \"3ff6b7a9-a5bd-4412-ad92-0cd0d45c0fee\", \"Level\": 2, \"Final answer\": \"56000\", \"id\": \"level2-47\", \"web\": \"https://www.google.com/\", \"ques\": \"The longest-lived vertebrate is named after an island.  According to Wikipedia as of January 1, 2021, what is the 2020 estimated population of that island, to the nearest thousand?\"}\n{\"task_id\": \"708b99c5-e4a7-49cb-a5cf-933c8d46470d\", \"Level\": 2, \"Final answer\": \"Citations\", \"id\": \"level2-48\", \"web\": \"https://www.google.com/\", \"ques\": \"On the DeepFruits fruit detection graph on Connected Papers from 2016, what feature caused the largest bubble to be the size it is?\"}\n{\"task_id\": \"0a65cb96-cb6e-4a6a-8aae-c1084f613456\", \"Level\": 2, \"Final answer\": \"Holabird\", \"id\": \"level2-49\", \"web\": \"https://www.google.com/\", \"ques\": \"During the first week of August 2015, one of the NASA Astronomy Pictures of the Day shows the lights of a city on the horizon. The namesake of this city also has a landmark building in Chicago named after him. What is the name of the architectural firm that designed this landmark building? Give the first name appearing in the name of the firm as of June 2023.\"}\n{\"task_id\": \"65da0822-a48a-4a68-bbad-8ed1b835a834\", \"Level\": 2, \"Final answer\": \"Santa Clara, Boston\", \"id\": \"level2-50\", \"web\": \"https://www.google.com/\", \"ques\": \"All of the individuals who formally held the position of United States secretary of homeland security prior to April 2019, excluding those who held the position in an acting capacity, have a bachelor's degree. Of the universities that these bachelor's degrees were from, which is the westernmost university and which is the easternmost university? Give them to me as a comma-separated list, I only want the name of the cities where the universities are located, with the westernmost city listed first.\"}\n{\"task_id\": \"73c1b9fe-ee1d-4cf4-96ca-35c08f97b054\", \"Level\": 2, \"Final answer\": \"1954\", \"id\": \"level2-51\", \"web\": \"https://www.google.com/\", \"ques\": \"According to the USGS, in what year was the American Alligator first found west of Texas (not including Texas)?\"}\n{\"task_id\": \"e2d69698-bc99-4e85-9880-67eaccd66e6c\", \"Level\": 2, \"Final answer\": \"Michele Fitzgerald\", \"id\": \"level2-52\", \"web\": \"https://www.google.com/\", \"ques\": \"As of August 2023, who is the only winner of the US version of Survivor to be born in the month of May?\"}\n{\"task_id\": \"a56f1527-3abf-41d6-91f8-7296d6336c3f\", \"Level\": 2, \"Final answer\": \"185\", \"id\": \"level2-53\", \"web\": \"https://www.google.com/\", \"ques\": \"The cover of the August 2021 issue of Vogue shows a famous landmark in the background behind some trees. How tall is this monument in yards, rounded to the nearest yard? Give the number only.\"}\n{\"task_id\": \"42d4198c-5895-4f0a-b0c0-424a66465d83\", \"Level\": 2, \"Final answer\": \"60\", \"id\": \"level2-54\", \"web\": \"https://www.google.com/\", \"ques\": \"I'm curious about how much information is available for popular video games before their release. Find the Wikipedia page for the 2019 game that won the British Academy Games Awards. How many revisions did that page have before the month listed as the game's release date on that Wikipedia page (as of the most recent entry from 2022)?\"}\n{\"task_id\": \"a26649c6-1cb2-470a-871e-6910c64c3e53\", \"Level\": 2, \"Final answer\": \"116\", \"id\": \"level2-55\", \"web\": \"https://www.google.com/\", \"ques\": \"What is the absolute difference in tens of thousands between the population of chinstrap penguins on the Wikipedia page for penguin species populations as of the end of 2018 and the population recorded in the Nature.com \\\"global population assessment of the Chinstrap penguin\\\" article from 2020, assuming two penguins per breeding pair?\"}\n{\"task_id\": \"d5141ca5-e7a0-469f-bf3e-e773507c86e2\", \"Level\": 2, \"Final answer\": \"19/02/2009\", \"id\": \"level2-56\", \"web\": \"https://www.google.com/\", \"ques\": \"When was a picture of St. Thomas Aquinas first added to the Wikipedia page on the Principle of double effect? Answer using the format DD/MM/YYYY.\"}\n{\"task_id\": \"1dcc160f-c187-48c2-b68e-319bd4354f3d\", \"Level\": 2, \"Final answer\": \"3\", \"id\": \"level2-57\", \"web\": \"https://www.google.com/\", \"ques\": \"According to Openreview.net, at the NeurIPS 2022 Conference, how many papers by an author named Yuri were accepted with a \\\"certain\\\" recommendation?\"}\n{\"task_id\": \"e0c10771-d627-4fd7-9694-05348e54ee36\", \"Level\": 2, \"Final answer\": \"234.9\", \"id\": \"level2-58\", \"web\": \"https://www.google.com/\", \"ques\": \"Take the gender split from the 2011 Bulgarian census about those who have completed tertiary education. Subtract the smaller number from the larger number, then return the difference in thousands of women. So if there were 30.1 thousand more men, you'd give \\\"30.1\\\"\"}\n{\"task_id\": \"e29834fd-413a-455c-a33e-c3915b07401c\", \"Level\": 2, \"Final answer\": \"21\", \"id\": \"level2-59\", \"web\": \"https://www.google.com/\", \"ques\": \"I'd like to learn more about some popular reality television competition shows. As of the end of the 44th season of the American version of Survivor, how many more unique winners have there been compared to the number of winners of American Idol?\"}\n{\"task_id\": \"08c0b6e9-1b43-4c2e-ae55-4e3fce2c2715\", \"Level\": 2, \"Final answer\": \"orange, white\", \"id\": \"level2-60\", \"web\": \"https://www.google.com/\", \"ques\": \"In the film Goldfinger, what color was the object that James Bond concealed himself and his companion Pussy Galore at the end of the film? If there are multiple colors, put them in a comma-separated list in alphabetical order.\"}\n{\"task_id\": \"db4fd70a-2d37-40ea-873f-9433dc5e301f\", \"Level\": 2, \"Final answer\": \"10\", \"id\": \"level2-61\", \"web\": \"https://www.google.com/\", \"ques\": \"As of May 2023, how many stops are between South Station and Windsor Gardens on MBTA\\u2019s Franklin-Foxboro line (not included)?\"}\n{\"task_id\": \"853c8244-429e-46ca-89f2-addf40dfb2bd\", \"Level\": 2, \"Final answer\": \"11\", \"id\": \"level2-62\", \"web\": \"https://www.google.com/\", \"ques\": \"In the 2015 Metropolitan Museum of Art exhibition titled after the Chinese zodiac animal of 2015, how many of the \\\"twelve animals of the Chinese zodiac\\\" have a hand visible?\"}\n{\"task_id\": \"7a4a336d-dcfa-45a0-b014-824c7619e8de\", \"Level\": 2, \"Final answer\": \"1:41.614\", \"id\": \"level2-63\", \"web\": \"https://www.google.com/\", \"ques\": \"At the two-minute mark in the YouTube video uploaded by the channel \\u201cGameGrumps\\u201d on May 14, 2017 as part of their playthrough of the game Mario Kart 8 Deluxe, the shows\\u2019 hosts are competing on one of the game\\u2019s racetracks. What was the world record time for that track in the game\\u2019s 150cc mode as of June 7, 2023? Express your answer in minutes and seconds, rounding the seconds to the nearest hundredth, e.g. 1:01.001.\"}\n"
  },
  {
    "path": "packages/evals/datasets/onlineMind2Web/onlineMind2Web.jsonl",
    "content": "{\"task_id\": \"b7258ee05d75e6c50673a59914db412e\", \"confirmed_task\": \"Find the store location and hours of the closest Gamestop to zip code 90028 and set it as the home store on Gamestop.\", \"website\": \"https://www.gamestop.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"ade4c09ad3fdb1607209750924cd232f\", \"confirmed_task\": \"Compare available plans for the AeroAPI on Flightaware.\", \"website\": \"https://www.flightaware.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"fb7b4f784cfde003e2548fdf4e8d6b4f\", \"confirmed_task\": \"Open the page with an overview of the submission of releases on Discogs.\", \"website\": \"https://www.discogs.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"824eb7bb0ef1ce40bfd49c12182d9428\", \"confirmed_task\": \"Get the lowest priced women's plus size one piece swimsuit in color black with a customer rating of at least 5 on Kohls.\", \"website\": \"https://www.kohls.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"046138801a05ddf56ad94e8672942496\", \"confirmed_task\": \"Find discussions of the community and open one with the most replies on Flightaware.\", \"website\": \"https://www.flightaware.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"92a3d4236f167af4afdc08876a902ba6\", \"confirmed_task\": \"Find a 2022 Tesla Model 3 on CarMax.\", \"website\": \"https://www.carmax.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"48c73f3f53e2611c4a1052457c1033db\", \"confirmed_task\": \"Get the report from the final environmental impact statement for the Jamaica Bus Depot expansion on new.mta.info.\", \"website\": \"https://new.mta.info/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"8f2611047de227a2ca8bda13f6e2e5fb\", \"confirmed_task\": \"Find the used 2012-2013 Honda Crosstour with the lowest mileage for under $25,000 near zip code 49102 on CarGurus.\", \"website\": \"https://www.cargurus.com/\", \"reference_length\": 17, \"level\": \"hard\"}\n{\"task_id\": \"b320c68bffc1f3c7f2a8dc9d5478fb27\", \"confirmed_task\": \"Find a walkthrough for the game \\\"The Legend of Zelda: Breath of the Wild\\\" on ign.\", \"website\": \"https://www.ign.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"aa4b5cb7114fcc138ade82b4b9716d24\", \"confirmed_task\": \"Find an editor's choice review with a score of 10 in the boardgame category on ign.\", \"website\": \"https://www.ign.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"005be9dd91c95669d6ddde9ae667125c\", \"confirmed_task\": \"Find the weight of baggage allowance for economy class on Qatar Airways.\", \"website\": \"https://www.qatarairways.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"323bd85e3559655d89e5496b951a25e8\", \"confirmed_task\": \"Tell me information about what identification I need to bring on my trip on Amtrak.\", \"website\": \"https://www.amtrak.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"123e8c2fc453f55fadd1d0b9aaf94df4\", \"confirmed_task\": \"Browse used Audi cars made before 2015 and sort by lowest price on KBB.\", \"website\": \"https://www.kbb.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"56f8890a837c49f7df766b9c981646f3\", \"confirmed_task\": \"Show crazy credits for the movie \\\" Prometheus\\\" on IMDb.\", \"website\": \"https://www.imdb.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"644a856c3897665e475e0dce50bf217d\", \"confirmed_task\": \"Find a pair of wireless headphones on Amazon with active noise canceling for $100 or less and add them to the cart.\", \"website\": \"https://www.amazon.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"62f1626ce249c31098854f8b38bdd6cf\", \"confirmed_task\": \"Find Playstation 5 digital edition on gamestop.\", \"website\": \"https://www.gamestop.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"561693d6eec7bbfba3fefe9e4b26decb\", \"confirmed_task\": \"Browse Marriott Bonvoy credit cards on Marriott.\", \"website\": \"https://www.marriott.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"b7a9a6b5d451164c09bbd27b670bc2ae\", \"confirmed_task\": \"Show me the list of Men's Blazers, Black, Size M on Uniqlo.\", \"website\": \"https://www.uniqlo.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"bfa2de159be6978acf2702be31a2eeeb\", \"confirmed_task\": \"Show me the options for a roundtrip leaving from Las Vegas on flexible dates on the interactive map on united.\", \"website\": \"https://www.united.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"4091bdd3fa64a5b0d912bc08eaf9c824\", \"confirmed_task\": \"Find the list of neighborhood maps for Brooklyn on new.mta.info.\", \"website\": \"https://new.mta.info/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"79f0bd7df6e685f30f20025cc6755c0a\", \"confirmed_task\": \"Find me the cheapest external Hard Drive for an Xbox One on GameStop.\", \"website\": \"https://www.gamestop.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"6ebde509dca8f15c0fa1bd74f071e8d6\", \"confirmed_task\": \"Search for a job in Miami, Florida, in Human Resources on target.\", \"website\": \"https://www.target.com/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"34ccd15a8ea8fd3895af83f5ccf62369\", \"confirmed_task\": \"Find out what to do when I lose an item on a bus on us.megabus.\", \"website\": \"https://us.megabus.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"c698ff3fc0f6cbce39947c597ab5749b\", \"confirmed_task\": \"Browse the page with event planning tips on Eventbrite.\", \"website\": \"https://www.eventbrite.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"b6d10e9bd19b4009a02dea0e98f4e1ae\", \"confirmed_task\": \"Check the current standings for MLS on Fox Sports.\", \"website\": \"https://www.foxsports.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"d71be72aa25c3eab8eea47a0e60382e2\", \"confirmed_task\": \"Find technical specs for the latest Macbook Air on Apple.\", \"website\": \"https://www.apple.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"0b51b4fa0295ae80ccd176ebdad6fff6\", \"confirmed_task\": \"Search for a red Toyota Corolla from model years 2018 to 2023 on CarMax.\", \"website\": \"https://www.carmax.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"3f312ae3efc3c3e90ababe050dd4e7ae\", \"confirmed_task\": \"Find the current NFL standings for the AFC East division on NFL.com and go to the page on which team is in first place.\", \"website\": \"https://www.nfl.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"95cad96f2e43f3c0d8efad1331c77c8c\", \"confirmed_task\": \"View the list of the Most Popular TV on rotten tomatoes.\", \"website\": \"https://www.rottentomatoes.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"bf3b311cc8dce16d3de844f4b5875dfd\", \"confirmed_task\": \"Compare Apple watches and  learn more about the ultra version on apple.\", \"website\": \"https://www.apple.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"b64f938af842f6a1b4489d0e49a785a7\", \"confirmed_task\": \"Get the frozen vegan cheese pizza between 5 to 10 USD on Target.\", \"website\": \"https://www.target.com/\", \"reference_length\": 17, \"level\": \"hard\"}\n{\"task_id\": \"5e1b8254c123c80178cc28e0afdb14f0\", \"confirmed_task\": \"Find a help page about buying tickets on seatgeek.\", \"website\": \"https://seatgeek.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"f27c0a7b8b0bb33d37698dff227fc8d7\", \"confirmed_task\": \"Browse used Mercedes-Benz cars from model years 2004 to 2012 on KBB and sort by highest price.\", \"website\": \"https://www.kbb.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"8fdec8eeffd3491e6526cc78c028120b\", \"confirmed_task\": \"See Nissan and Honda cars for sale near Kentwood, MI 49512 on CarMax.\", \"website\": \"https://www.carmax.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"7b182a5087347d494b48a29dbc0f1d3e\", \"confirmed_task\": \"Find a shelter or rescue group near zip code 90011.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"828c2d98616a9478d5864d847d5a1b28\", \"confirmed_task\": \"Browse the list of Civil Division forms.\", \"website\": \"https://www.justice.gov/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"608c595eec271fa5dc03506923519994\", \"confirmed_task\": \"Calculate a FedEx Ground shipping rate for a 3-pound package from zip code 10019 to zip code 90028.\", \"website\": \"https://www.fedex.com/en-us/home.html\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"a7a73c8fa75441fc76df9746c327bdd6\", \"confirmed_task\": \"Estimate the cost of a photographer in 07055 for a 4-hour project.\", \"website\": \"https://www.thumbtack.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"cfafe3771369d1d261e9f7ecd44c296d\", \"confirmed_task\": \"Find the highest-rated dealer for Cadillac with a rating above 4 stars within 20 miles of zip 60606.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"bbbc243b4f18a7a897f0bc84e11d293f\", \"confirmed_task\": \"Find out how many assists Chris Paul has been averaging in the current season.\", \"website\": \"https://www.nba.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"816851ff92ff0219acf4364dcc2c4692\", \"confirmed_task\": \"Search for boys' infant pajamas below $40.\", \"website\": \"https://www.macys.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"8244409b2c82043f966cad05f9afe132\", \"confirmed_task\": \"Find the best Audiologist within 50 miles of New York, NY, with a rating of 4 and above.\", \"website\": \"https://doctor.webmd.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"e7301bb694871429bf2eb36c3a72186c\", \"confirmed_task\": \"Find baby shoes priced under $20 with a 5-star rating.\", \"website\": \"https://www.macys.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"905cb53061c33aa2d77e485fe1fca516\", \"confirmed_task\": \"Browse dermatologists within 10 miles of zip code 10019 and filter by only those who accept Blue Medicare Advantage.\", \"website\": \"https://www.healthgrades.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"fcf4952d2a1d80ea505c555c3c3b54e7\", \"confirmed_task\": \"Find the cheapest used  8-cylinder bmw made between 2005-2015 and priced from 25,000 to  50,000 dollars with mileage less than 50,000 miles or less.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"3c1ffc3f494e423b3c434c79e35da8f3\", \"confirmed_task\": \"Find 12 Monkeys community and view the latest posts mentioning James Cole.\", \"website\": \"https://www.reddit.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"26a0e5c21c145dd8448aa92f35bec5ea\", \"confirmed_task\": \"Browse optometrists who offer telehealth services in Columbus, OH.\", \"website\": \"https://www.healthgrades.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"070c907d34a4ce71dfdbea38f9c5d4d8\", \"confirmed_task\": \"Find a dentist who specializes in pediatric dentistry and is located near zip code 90210 (within 5-mile distance).\", \"website\": \"https://www.healthgrades.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"43a1ca251f11c6b0bdd0379766cc49e6\", \"confirmed_task\": \"Find a neurosurgeon who is over 50 years old and has an appointment available tomorrow.\", \"website\": \"https://www.healthgrades.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"b3f8bd9198d9d157e0848109563c4b23\", \"confirmed_task\": \"Find a permanent job in Logistics within 20 miles of New York, zip 11005, in the middle-income range for a high school diploma holder.\", \"website\": \"https://ohiomeansjobs.ohio.gov/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"20a460a8fe1971b84411c5b1e6ac4186\", \"confirmed_task\": \"Show theatre events for Las Vegas and select one.\", \"website\": \"https://www.stubhub.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"db1ffb5e60578597d1c3aa3c389ac7b1\", \"confirmed_task\": \"Search for smart TVs with a screen size of 55 to 65 inches and filter the results to show only those that have an LED display.\", \"website\": \"https://www.google.com/shopping?udm=28\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"7be8cd8dba885cddd9af5320f49bc41b\", \"confirmed_task\": \"Find roofing contractors within 5 miles of zip code 10002.\", \"website\": \"https://www.bbb.org/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"239a29bde438fe44fe17fe1390ef1634\", \"confirmed_task\": \"Find me a gluten-free diet to lose weight for a pregnant woman.\", \"website\": \"https://www.healthline.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"9f1cba613830ca1c6a58f9498c06e679\", \"confirmed_task\": \"Find a premier real estate agent in St Augustine, FL.\", \"website\": \"https://www.redfin.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"75146b7b67388b9244e0f21a1527c022\", \"confirmed_task\": \"Find a male senior boxer near zip code 90028.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"59b7b990b4828bc305ab0d7ed6071b55\", \"confirmed_task\": \"Get owner-financing homesite land for sale in New Mexico, Luna County,  listed in the last 30 days, and contact the cheapest per acre land seller.\", \"website\": \"https://www.landwatch.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"9c97bab9c2abfb90a426cbe9addae8d0\", \"confirmed_task\": \"Check the details of order 12345 with email 12345@gmail.com.\", \"website\": \"https://www.macys.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"871e7771cecb989972f138ecc373107b\", \"confirmed_task\": \"Find the weather for Vancouver, British Columbia for the next seven days.\", \"website\": \"https://www.theweathernetwork.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"b69eb4de621e9e265676daac44938f3f\", \"confirmed_task\": \"Find an adult husky near zip code 10019.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"9bb63ad0e38d5691a618932a8b31c05a\", \"confirmed_task\": \"Look for reviews of a Nest Hello Video Doorbell and filter by 1-star ratings.\", \"website\": \"https://www.google.com/shopping?udm=28\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"8ae510355d978424f490798f900bfa2c\", \"confirmed_task\": \"Show me the shared rooms in any university in Melbourne that has a private bathroom wifi, and gas included in the bills.\", \"website\": \"https://www.student.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"4c186c6ed888d0c8d4cf4adb39443080\", \"confirmed_task\": \"Find a medium Devin Booker jersey and add it to the shopping cart.\", \"website\": \"https://www.nba.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"2fc51dd3febd447f0fdcdabca8d944ce\", \"confirmed_task\": \"Locate a self-storage unit near zip code 60538 that can fit about a dorm room full of items and is climate-controlled.\", \"website\": \"https://www.extraspace.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"eb323dc584156d0eb3a2b90bb8c4b791\", \"confirmed_task\": \"Find the latest 2 bed and 1.5+ bath apartment listing for rent in New York.\", \"website\": \"https://www.redfin.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"87f4c5128e36cdb9366a138a7b61bb00\", \"confirmed_task\": \"View the speakers that are bluetooth and wireless and filter the results to only show models that are on sale and cost less than $50.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"354b4ddf048815f8fd4163d0d7e1aaa3\", \"confirmed_task\": \"Browse marketing jobs and filter by Bachelor's Degree education level.\", \"website\": \"https://ohiomeansjobs.ohio.gov/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"e4e097222d13a2560db6f6892612dab6\", \"confirmed_task\": \"Search for a young spayed male dog cared for by a private owner within 50 miles of zip 33109.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"f389398d2eeb29e5571e00439c57eb76\", \"confirmed_task\": \"Find the latest climate news.\", \"website\": \"https://www.theweathernetwork.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"8ea6c3a2ea3f59150619935261a76d19\", \"confirmed_task\": \"Find a staffed FedEx location near zip code 10019 to return a package.\", \"website\": \"https://www.fedex.com/en-us/home.html\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"c1d6ea6f2196d25782cc3646ff3090db\", \"confirmed_task\": \"Create a list of drip coffee makers that are on sale and within $25-60 and have a black finish.\", \"website\": \"https://www.google.com/shopping?udm=28\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"2dd41b1d0e8f389d0683f4a4627abfe6\", \"confirmed_task\": \"Show houses for sale in Maryland with a maximum price of $60,000.\", \"website\": \"https://www.landwatch.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"f2097f92a10d42a842c14179f422311e\", \"confirmed_task\": \"Add a $50 Uber gift card to the cart.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"85b284c18d7e78c9b5a9e074e7aa3b98\", \"confirmed_task\": \"View the cheapest apartment available for students at the University of Leeds with bills that include WIFI and cleaning services.\", \"website\": \"https://www.student.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"853afd530c72f4b00ffc32ae854efaf8\", \"confirmed_task\": \"Show me the wind flow map for Belo Horizonte.\", \"website\": \"https://www.accuweather.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"c09721cc937d4dcfb391a0bc2c574b28\", \"confirmed_task\": \"Find the next available date for Albion Basin.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"4c572a627b53b0f9a734ab37f21819b8\", \"confirmed_task\": \"Browse apartments with at least 2 bedrooms and 2 bathrooms and a max price of $4000 per month.\", \"website\": \"https://craigslist.org/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"301f267f421b93045874726183e8f722\", \"confirmed_task\": \"Find healthy savory vegan snack recipes which can be cooked within 5 minutes and contain a high level of protein.\", \"website\": \"https://www.healthline.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"4f903626f632586fe4728d6664947bab\", \"confirmed_task\": \"Find press releases by the antitrust division in 2022.\", \"website\": \"https://www.justice.gov/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"3ec0f6138d37fadcb989347a6088ec45\", \"confirmed_task\": \"Open the page to learn more about how to get accredited.\", \"website\": \"https://www.bbb.org/\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"2207bb4f21786690cfed20b37253fb8b\", \"confirmed_task\": \"Check the current wind speed in Calgary, Alberta.\", \"website\": \"https://www.theweathernetwork.com/\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"9c04b71bb8db6cf8e743b2290cbc8797\", \"confirmed_task\": \"Find a UPS drop-off point near Miami Florida.\", \"website\": \"https://www.ups.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"4e0f5561a76478da87995dee00b09572\", \"confirmed_task\": \"Show me the monthly weather forecast for Florida City.\", \"website\": \"https://www.accuweather.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"7562d9b4e4829a44245aafce2e1f62db\", \"confirmed_task\": \"Find the nearest location to zip code 54620 that offers size 4 P.O. Boxes.\", \"website\": \"https://www.usps.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"bd1e3770b7181f6fce9c35e18caa9785\", \"confirmed_task\": \"Browse service listings for a solar panel installer and hide duplicates.\", \"website\": \"https://craigslist.org/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"330cd04c773ac498f51afa4665461ec8\", \"confirmed_task\": \"Browse couches for sale, sort by cheapest, and search in titles only.\", \"website\": \"https://craigslist.org/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"ec78d3a635e417bc2a80d03ca93d7165\", \"confirmed_task\": \"What are the benefits and financial support a single person living in England, over the state pension age, unemployed, with no health conditions, or caring for someone with one, can get?\", \"website\": \"https://www.gov.uk/\", \"reference_length\": 16, \"level\": \"hard\"}\n{\"task_id\": \"a0a18ca6a3529f3e97c771aadd42d3a0\", \"confirmed_task\": \"Add a men's T-shirt that is in large size with a stripe pattern, short sleeve, and under the Best Sellers group to the cart.\", \"website\": \"https://www.macys.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"82eb3bfedd78456a0230b389f4e7a938\", \"confirmed_task\": \"Open the XRP yearly chart.\", \"website\": \"https://coinmarketcap.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"e7f6cca9a8875f98fee3b711ead3a444\", \"confirmed_task\": \"Find the comments made by the user Separate-Camp7202.\", \"website\": \"https://www.reddit.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"75a1b5dcd2c28508a971d98d51fe5767\", \"confirmed_task\": \"Open the reviews of a recipe with beef sirloin.\", \"website\": \"https://www.allrecipes.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"c03ee2be3d73556ab789c0ad1cbd3451\", \"confirmed_task\": \"Find a dog groomer for nail trimming within 100 miles of zip code 10005 and check the detailed service prices of the first one.\", \"website\": \"https://www.akc.org/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"05483c50cc9b04c8ac44c574758fb2bd\", \"confirmed_task\": \"Look for the best rated BBB accredited charity near 12023.\", \"website\": \"https://www.bbb.org/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"a172a5d9ffaf5ef02bd550ec4fe24e6d\", \"confirmed_task\": \"Browse the natural products database.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"7e1047f4803237f319c004f7a7f6bccb\", \"confirmed_task\": \"Discover the trade-in value of my Intel 7th generation i3 Windows 10, HP laptop in fair condition,  which has 8 GB memory and can be powered on, proceed for the in-store trade-in.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"f2be37a9a60fbc25b6b11cf622d17352\", \"confirmed_task\": \"Find obedience trials in state of New York during the month of May.\", \"website\": \"https://www.akc.org/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"e24662008c3be5d56f986f232fcec447\", \"confirmed_task\": \"Find the stock price for WWE over the last month.\", \"website\": \"https://www.google.com/finance/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"0170ca95038b05fa58d463fe627ac605\", \"confirmed_task\": \"Check if a visa is required to work in the UK for longer than 6 months in Healthcare as an American citizen.\", \"website\": \"https://www.gov.uk/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"b3a7da968de13bbdcaed12ffe4993df6\", \"confirmed_task\": \"Compare the breeds Afghan Hound, Akita and Azawakh.\", \"website\": \"https://www.akc.org/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"515f2e5811cfdd5e0e669e40f17886d8\", \"confirmed_task\": \"Search for a new internal M2 Samsung SSD drive between $25 and $200.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"4d3157aab34b54e5f0c4b965dfe930f3\", \"confirmed_task\": \"Show me community posts about pregnancy fever from the past 30 days.\", \"website\": \"https://www.babycenter.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"e4e19e04286f644d747d8c5a79d17fac\", \"confirmed_task\": \"Find the Drug Interaction Report for Viagra and alcohol.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"cad62d2be0c53f08a416457486b3db23\", \"confirmed_task\": \"Search for adoptable dogs near 21122 zip code.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"9ef1a8972f375db59c0e6329e11b7939\", \"confirmed_task\": \"Find Farms land in Wilkes County, NC with the lowest price.\", \"website\": \"https://www.landwatch.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"a11ecdff735b51372d536c866011af6f\", \"confirmed_task\": \"Explore courses related to Psychology.\", \"website\": \"https://www.coursera.org/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"7fff82864f21ddeccf4104a220892824\", \"confirmed_task\": \"Find the lowest 27\\\"-32\\\" Samsung or LG computer monitors nearby which have 4k, IPS display.\", \"website\": \"https://www.google.com/shopping?udm=28\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"50d91eabde542906937ab4c5b6f8f23a\", \"confirmed_task\": \"Calculate Pregnancy Weight Gain for a 5-week pregnancy with a 169lb weight before pregnancy and a 175lb after pregnancy with a 5.6ft height.\", \"website\": \"https://www.babycenter.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"dcd26e662a616d373ddd339747c6ce5b\", \"confirmed_task\": \"Take a weight management quiz to find a motivating article for a non-exercising, mostly eating out and can't control portions and cravings, and who has a strong support system, enjoys traveling, loves family time and cooking.\", \"website\": \"https://www.healthline.com/\", \"reference_length\": 22, \"level\": \"hard\"}\n{\"task_id\": \"eb2db4b769c145dbe6ba4f74f3e0de98\", \"confirmed_task\": \"Find an energetic hairless dog with medium barking.\", \"website\": \"https://www.akc.org/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"c0fa2c0e622971955cabf5bcf7b777e8\", \"confirmed_task\": \"Search for rentals in Corning, CA with a maximum price of $1500.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"ce616721ce9aeda69890fbccb29677a6\", \"confirmed_task\": \"Calculate the price to ship a large flat-rate box from 77449 to 77084 at the first available date and time.\", \"website\": \"https://www.usps.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"9d09bc948462db032bac98968b11b008\", \"confirmed_task\": \"Find NHL events occurring in Boston.\", \"website\": \"https://www.stubhub.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"29526b17a32485742b5ab63507e99417\", \"confirmed_task\": \"Browse Humira dosage information.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"d7c955b47af68e01766fa86d0bee08a7\", \"confirmed_task\": \"Add Elevate at Chicago, IL, to favorites and show a virtual tour.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"9d090a15c214eb070d9caa8a034d03c1\", \"confirmed_task\": \"Find the lowest-priced Student housing near Liverpool International College which has been priced between 100 to 300 pounds and has a private bathroom.\", \"website\": \"https://www.student.com/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"5916018d1cad999881018cac1216a692\", \"confirmed_task\": \"Find a personal trainer service at 10040 for a 25-year-old client aiming to build muscle.\", \"website\": \"https://www.thumbtack.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"0059adc6b12a3822305deb68929b2de8\", \"confirmed_task\": \"Find support services jobs in Bentonville, in the state of Arkansas.\", \"website\": \"https://careers.walmart.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"07bdc595306729a028ba06cc7451a80a\", \"confirmed_task\": \"Select a high speed train ticket with a departure time before 23:00  from Shanghai to Beijing.\", \"website\": \"https://us.trip.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"64b76158720a69e4a5c31a55d54928bf\", \"confirmed_task\": \"Compare two pescatarian diets for eating healthier.\", \"website\": \"https://www.healthline.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"e3ab665e01e7632ce33ac1aeca14aff6\", \"confirmed_task\": \"Find the next available dates for Alley Creek Camp.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"2d5a7f95f951a26838289dfd629ae850\", \"confirmed_task\": \"Find a list of houses for sale in zip code 85747 with a private pool.\", \"website\": \"https://www.redfin.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"26810ed9c123a62992e3eed31db3c5ee\", \"confirmed_task\": \"Show daily weather for New York City.\", \"website\": \"https://www.accuweather.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"c181f903ec1107b850032c17cad88393\", \"confirmed_task\": \"Help me identify a pink round pill with 150 written on it.\", \"website\": \"https://www.webmd.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"ef289e34a2f59a707cb07e2a6229ff03\", \"confirmed_task\": \"Compare the Acura CL 2003 with the ILX 2022.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"84f806c7fc15576673915f195efa72df\", \"confirmed_task\": \"Find a nationwide nearest animal shelter for birds around zip 10012.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"be9e7dca1222714571ef3d7d59d2a41c\", \"confirmed_task\": \"Find out the cold and flu forecast and today's air quality in Champaign, IL.\", \"website\": \"https://weather.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"11abb668c751dd56bb41f296a8bb3a13\", \"confirmed_task\": \"Find a store near zip 30010 that provides authorized Apple services for imacs and make this one my store.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"207e933d1bba815bcb58664b5d82c085\", \"confirmed_task\": \"Find Ohio City apartments with parking, a fitness center, and an elevator.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"29b7372d5a3884a2ba831af2d117af3c\", \"confirmed_task\": \"Browse the first top news of Microsoft stock on Google Finance.\", \"website\": \"https://www.google.com/finance/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"5c00e9561eae94789443f405525a5869\", \"confirmed_task\": \"Find the recommended dosage for Vivitrol.\", \"website\": \"https://www.healthline.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"2532fd402d3c741b79894e6ff2269f53\", \"confirmed_task\": \"find electricians near 10203.\", \"website\": \"https://www.thumbtack.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"9829f3087ab1f9c8eba6b6dd2b831d25\", \"confirmed_task\": \"Play the latest video from NBA TV.\", \"website\": \"https://www.nba.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"783ce6a3499fa7cf25bc12f8f0ecbbbb\", \"confirmed_task\": \"Find Florida internship programs in the Mayo Clinic College of Medicine and Science.\", \"website\": \"https://www.mayoclinic.org/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"6db4a0e346976f2729ba9afcd3208941\", \"confirmed_task\": \"Look up tracking information for shipment #3023858502.\", \"website\": \"https://www.fedex.com/en-us/home.html\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"1fc28d91d25ccd1c6ba268101326a654\", \"confirmed_task\": \"Find the 5-day price chart for Bitcoin.\", \"website\": \"https://www.google.com/finance/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"255bf27c43fd3f9254d6b81a5f36d3a9\", \"confirmed_task\": \"Look for the largest hunting land for auction in Kansas high plain region with mineral rights posted in the last seven days.\", \"website\": \"https://www.landwatch.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"a8b9edd598561d2de901864d5f40fe67\", \"confirmed_task\": \"Calculate the shipping cost for 4 pound package from Texas to New York.\", \"website\": \"https://www.fedex.com/en-us/home.html\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"a6f0434ce6aff5f9b03681241b03ad82\", \"confirmed_task\": \"Find the closing stock price for Tesla on March 17, 2023.\", \"website\": \"https://finance.yahoo.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"415bf9da6f3db3a735ecbba3b0c76c15\", \"confirmed_task\": \"Find the nearest vet within 50 miles of zip 75228.\", \"website\": \"https://www.akc.org/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"8103786e0e5976ebf961bd062d5f39cd\", \"confirmed_task\": \"Find possible causes for the symptoms of chest pain which is sharp which is accompanied by anxiety.\", \"website\": \"https://www.mayoclinic.org/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"92160852a6bbbc165cee4e14ab0b1d59\", \"confirmed_task\": \"Find the shipping cost of a Common medium-sized box in flat-rate shipping and compare it with other parcel services.\", \"website\": \"https://www.ups.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"502e864440283214e0180645015f568b\", \"confirmed_task\": \"Check permit availability for a group of 4 in Brooks Camp, Katmai National Park on May 22.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"7680a920359cb1a508fbddb001b98167\", \"confirmed_task\": \"See the prediction about the girl child's height, whose current height at seven years is 4 feet and whose weight is 55 lbs, her mother is 5 feet 2, and her father is 5 feet 8.\", \"website\": \"https://www.babycenter.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"07ec4a12cba8090e2dc524d558ac7675\", \"confirmed_task\": \"Check drug interaction for melatonin and Folate Forte.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"987bad7c6d4726d64232a8a1c3386888\", \"confirmed_task\": \"Find the seller info and seller's notes about the used car model 2011 BMW 135 with a max price of $30000.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"15be05973fba714e490cd9c884e4f072\", \"confirmed_task\": \"Find the procedure to get the license for Athletic Trainer.\", \"website\": \"https://ohio.gov/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"3adeea7627f4343069f38adae40f73d0\", \"confirmed_task\": \"Within 25 Miles of 96817, find a nursing home that accepts medicare.\", \"website\": \"https://health.usnews.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"c94551d2b18f9ad0ab31b0bd98ca42e3\", \"confirmed_task\": \"Find cats available for adoption within 10 miles of zip code 94587, Young or adult-age cats, sorted by Oldest Addition.\", \"website\": \"https://www.petfinder.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"4e801ba102dfaf22c7cf7a126b107609\", \"confirmed_task\": \"Find Linux platform software developers in 10080 who master the Python language and Java language with web interface project type.\", \"website\": \"https://www.thumbtack.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"39c388cdc468688c8139cc2bb5157c13\", \"confirmed_task\": \"Calculate the estimated car loan payment amount for an average credit-rated person for a 15,000-dollar car with a down payment of 2000 dollars and loan tenure of 48 months in zip 65215 and shop for the lowest-priced car.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"c8d7f2aa7eb5dd074c48c9f76f8659ad\", \"confirmed_task\": \"Show Teen Driver Safety program information.\", \"website\": \"https://www.dmv.virginia.gov/\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"fd787623166785d84093565bf945fd24\", \"confirmed_task\": \"Check the interaction between Novolin N and Novolin R.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"c3307a70bb12ebf56cc9ec926b368f15\", \"confirmed_task\": \"Find the interactions between Eulexin and hepatic dysfunction.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"9586827ad04ee2362f4f0076bf0f0468\", \"confirmed_task\": \"Find the side effects of taking Montelukast.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"34992feb69eb8e788faa06868b365c49\", \"confirmed_task\": \"Submit a request for vehicle registration renewal with title number X123456 and last 4 digits of VIN is 1234.\", \"website\": \"https://www.dmv.virginia.gov/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"47b93b9e649eadeb8d96a6e3df715c2d\", \"confirmed_task\": \"Show me Diagnoses & Treatment for Female infertility.\", \"website\": \"https://www.mayoclinic.org/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"3443e9c3151fef19a3c3a45eb2c13640\", \"confirmed_task\": \"Search for the ovulation calculator and enter Mar 1 as the first date of the period and calculate the date of ovulation and pregnancy test day.\", \"website\": \"https://www.webmd.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"6b5be1764692d1dc8f17dc4375b2daa8\", \"confirmed_task\": \"Show me historical data for EUR/USD.\", \"website\": \"https://finance.yahoo.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"16200f51d63f0a47a58fa17acd49e368\", \"confirmed_task\": \"Find a recipe that includes eggplant and mushrooms.\", \"website\": \"https://cookpad.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"c2153fc053112e89c2f103869c4d6890\", \"confirmed_task\": \"Find a house cleaning service in 10001 on a weekly basis.\", \"website\": \"https://www.thumbtack.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"5e4e89c9b6fdaee7a41aca5601b82e04\", \"confirmed_task\": \"Identify a pill with a pink color and oval shape with 894 5 number on it.\", \"website\": \"https://www.drugs.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"60cbbbd58eb9d28b053aef945f464228\", \"confirmed_task\": \"Look up if the phone number 555555555 is a scam.\", \"website\": \"https://www.bbb.org/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"8f80e64e44e1fada018997b2fe869683\", \"confirmed_task\": \"What are the top posts of all time on Reddit?\", \"website\": \"https://www.reddit.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"65c4030f22fb6eb101acfee4825f1318\", \"confirmed_task\": \"Find a female MD Cardiologist in Jacksonville, Florida.\", \"website\": \"https://www.mayoclinic.org/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"6ca20f1da01edeb49a7a42c816d8c6fe\", \"confirmed_task\": \"Find the Eligibility to get the child benefit and How it works and how to claim\", \"website\": \"https://www.gov.uk/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"2e4e21cf1449c6894b17d571c47b77ea\", \"confirmed_task\": \"Find an English bulldog near zip code 90028 that was cared for by a private owner.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"1df24ec81137386d6476bcf343a79012\", \"confirmed_task\": \"Search for NordicTrack with the lowest price.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"4639a54f3ab549864fd8d60b7398b1e1\", \"confirmed_task\": \"Find a white female kitten within 35 miles of zip 77494.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"9af05e392cf3f5a8ff17aa764ba5bda6\", \"confirmed_task\": \"Get a quote from C and above-rated solar energy equipment company within 10 miles of Miami, Florida.\", \"website\": \"https://www.bbb.org/\", \"reference_length\": 16, \"level\": \"hard\"}\n{\"task_id\": \"627f7a18d85f29a687234f1ade4585c2\", \"confirmed_task\": \"Find the current league leader in total blocked shots.\", \"website\": \"https://www.nba.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"0b838cd54f826c59c71f600c56b89a11\", \"confirmed_task\": \"Find all the locations for the second-best-rated used car dealer less than 5 miles from New York.\", \"website\": \"https://www.bbb.org/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"5dec0e6620849459f29e6465982c597e\", \"confirmed_task\": \"Search for 33 to 49inch Qled gaming monitor with a 240hz refresh rate that is within $1000 to $2000.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"52efbab520734ef9bf7c09ba0f62cdc8\", \"confirmed_task\": \"Find the app for iOS.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"b1ce968a361e1088ce8d2ade6c2c9af0\", \"confirmed_task\": \"Find young cats in Seattle and show off the newest additions.\", \"website\": \"https://www.petfinder.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"23204728192da9f73197a613d9681c18\", \"confirmed_task\": \"What are the Symptoms and causes of fever?\", \"website\": \"https://www.mayoclinic.org/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"a69d2934fe54fef165490a5a2d95bf38\", \"confirmed_task\": \"Show me recipes for pancakes with wheat and without beetroot.\", \"website\": \"https://cookpad.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"e9f4dfc67e0e6aa37f05f7cc5aa7428c\", \"confirmed_task\": \"Browse pediatricians near zip code 90028 who specialize in Internal Medicine and have a rating of at least 4 stars.\", \"website\": \"https://www.healthgrades.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"2218042362d8fae73756eb309848c2b2\", \"confirmed_task\": \"Compare Audi A7 with Audi A6, both made in 2023, and hide similarities.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"ba2a469af584f16da93ce6a7430cf7e5\", \"confirmed_task\": \"Search for a beginner\\u2019s course in computer science that includes advertisement skills.\", \"website\": \"https://www.coursera.org/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"26784156ae9859a0dd6c5920eb106f91\", \"confirmed_task\": \"calculate and search rent for a $6000 monthly income with 30% rent budget near 90012 area.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"47e314cc452c540524ffb7cf520285a3\", \"confirmed_task\": \"Find the park that offers the cheapest paddling permits.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"271b36efd4346721b5542488ff997042\", \"confirmed_task\": \"Browse 8K Samsung TVs that are open box.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"6b2cfae0ef25c73d1224b6ab74cb8b63\", \"confirmed_task\": \"Find Devin Booker's highest-scoring points per game playoff run.\", \"website\": \"https://www.nba.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"0a54069a0ef542e571d1fee7f39c93d5\", \"confirmed_task\": \"Browse senior spayed/neutered dogs near zip code 90028.\", \"website\": \"https://www.adoptapet.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"4e3f6a538cc1f7321cfc50260db9545d\", \"confirmed_task\": \"Look up the current temperature for zip code 10019.\", \"website\": \"https://www.theweathernetwork.com/\", \"reference_length\": 2, \"level\": \"easy\"}\n{\"task_id\": \"f00e7accfb4a5e09680bdb326e6274ad\", \"confirmed_task\": \"Check the hourly forecast for Boston.\", \"website\": \"https://www.accuweather.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"6174e5ddd40cfbdc33ee1502f40bac39\", \"confirmed_task\": \"Find a day-use park that offers horseback riding near Nashville.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"547f5729c59d5d12a457a3ebb74c31c6\", \"confirmed_task\": \"Search for 3 bedroom condos with 2 bathrooms within $1500- $2500 range in NYC.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"0b2623e9fa5cea997f76490bcbc5220f\", \"confirmed_task\": \"Find a list of shorthaired dogs available for adoption within 100 miles of zip code 94587 that are good with kids and cats, and have been on Petfinder for over 30 days.\", \"website\": \"https://www.petfinder.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"3ae28b3c440efe87dc700480b78ac608\", \"confirmed_task\": \"Find the closest 5-star rated dentist to zip code 98011.\", \"website\": \"https://www.healthgrades.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"0632e496d37badee0350dad358f047c5\", \"confirmed_task\": \"Browse recipes for gluten-free chocolate chip cookies that can be made without nuts.\", \"website\": \"https://cookpad.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"aafd1fddea1558466ac6133934d35156\", \"confirmed_task\": \"Find a Single-Family House for Rent in Houston, TX with 1 bed.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"246d654fab7c31d9651007e39e75f74f\", \"confirmed_task\": \"Open the most helpful 5-star reviews of Alpine Ridge.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"690d7b4a285fdb1e9dabf973bf46ae4d\", \"confirmed_task\": \"Browse iPhone X for sale that is in good condition, has a max price of 400, and searches in titles only.\", \"website\": \"https://craigslist.org/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"c43a7dccf5c44f7b45a821e712dd1970\", \"confirmed_task\": \"Take a newsletter subscription with my email id (buckeye.foobar@gmail.com) for Allergies and asthma, Anxiety and depression, nutrition, diabetes, breast cancer, and migraine with email id.\", \"website\": \"https://www.healthline.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"d5c34bf39eb6096ae5d439325cde4d32\", \"confirmed_task\": \"Find a DMV center in Richmond.\", \"website\": \"https://www.dmv.virginia.gov/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"180ed2ec377ef3a4af9035a21522091a\", \"confirmed_task\": \"Find the way to give a gift to UM-Dearborn.\", \"website\": \"https://umich.edu/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"c521933dad9c0ef9f1dfa2f38b8e4405\", \"confirmed_task\": \"See the monthly forecast for Atlanta, GA.\", \"website\": \"https://www.accuweather.com/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"9b5dfe54a1c14c5c6336bae7374c3bb5\", \"confirmed_task\": \"Find a UPS Access Point near SPRING, TX and services provided by them.\", \"website\": \"https://www.ups.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"c073ac1bcf40f84c599affc97edbc396\", \"confirmed_task\": \"Search for the cheapest apartment in Detroit for a student.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"73d08420706ae205a9c5be28b6d4e80f\", \"confirmed_task\": \"Show me the rules and cancellation for Alley Spring.\", \"website\": \"https://www.recreation.gov/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"0a0fa834ce41b5297c6474293383759d\", \"confirmed_task\": \"What are the onboard activities of the highest-rated Regent Seven Seas Cruise ship based on Costco member reviews?\", \"website\": \"https://www.costco.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"a13e4231a3d6a7000c622c56448d97ba\", \"confirmed_task\": \"Find an Airbnb in Cleveland for three nights. The check-in date is the day after tomorrow. We have 2 adults, 2 kids, and 1 pet. The budget is $100 to $300 per night. Essential amenities include free parking, a washer, and a gym.\", \"website\": \"https://www.airbnb.com/\", \"reference_length\": 19, \"level\": \"hard\"}\n{\"task_id\": \"bb518416a786fdb9b9bbf0c78515595e\", \"confirmed_task\": \"Browse the class schedule of graduate-level computer science courses.\", \"website\": \"https://www.osu.edu/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"b99c02965196d51e80ac7539e33f335b\", \"confirmed_task\": \"Please find graduate-level computer science courses scheduled on Tuesdays starting time from 2:00 to 6:00 PM in the Fall 2023 semester.\", \"website\": \"https://www.berkeley.edu/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"27fa3ac20745d3d35e89fae157f63069\", \"confirmed_task\": \"Browse the class schedule of graduate-level chemistry courses on Monday afternoons in the winter of 2023.\", \"website\": \"https://www.stanford.edu/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"b4aa7315e31dfcdc52baf7771be260c9\", \"confirmed_task\": \"Find the HGX H100 driver for Ubuntu 22.04 on AMD64 CPU.\", \"website\": \"https://www.nvidia.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"442a450e696a96085257db6297891a4d\", \"confirmed_task\": \"Using a calculator to determine how much I can have in my 401(k) account at retirement, if I work from age 22 to 65, with an annual rate of return of 3%, annual employee contributions of $8,000, and annual employer contributions of $8,000.\", \"website\": \"https://www.chase.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"9ed3827266b3b804f485859c3d00401e\", \"confirmed_task\": \"If I'm 30, plan to retire at 65, and can save $300/month, with a 3% annual return, 13% current tax rate, and 24% retirement tax rate, show the comparison chart between Traditional and Roth IRA.\", \"website\": \"https://www.chase.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"c801d1c951f59297f526bab84fa86c6e\", \"confirmed_task\": \"Browse the latest negative reviews from players with over 100 hours of playtime for the game that won the 2023 VR Game of the Year Award.\", \"website\": \"https://store.steampowered.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"7c09c2c7c87cf6bb1138701eb54284ea\", \"confirmed_task\": \"Find the comments for the most popular news in the past month under the Quantum Physics topic.\", \"website\": \"https://phys.org/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"afcebfed28bea091d58f49ea6cb8194b\", \"confirmed_task\": \"Find the most reviewed gluten-free multivitamins from CVS Health Brand under $15.\", \"website\": \"https://www.cvs.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"64345c365f544375357c7b67917f08a0\", \"confirmed_task\": \"Look for the newest refrigerator that is 34-36 inches wide, priced between $1,000 and $2,000, and has a customer review rating of 4 stars or higher.\", \"website\": \"https://www.costco.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"ab6ee3b83aab6cd283320f5e01003cff\", \"confirmed_task\": \"Find the tech specs of the MacBook Pro 16-inch introduced in November 2023.\", \"website\": \"https://www.apple.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"33bd2cdcea4fcc42a09a8a1e4e5841c6\", \"confirmed_task\": \"Add a 5-piece Tenders Combo to my bag with Sweet Corn as the side, Sweet Tea as the drink, and both Honey BBQ and Honey Mustard sauces. Select the store closest to Zip code 10001 for pick-up tomorrow at 12:00 PM.\", \"website\": \"https://www.kfc.com/\", \"reference_length\": 23, \"level\": \"hard\"}\n{\"task_id\": \"47186fac8e7c7277af01144644eb4e0b\", \"confirmed_task\": \"What is the ownership cost of the first car in the list \\\"top buys 2025\\\"?\", \"website\": \"https://www.parkers.co.uk/\", \"reference_length\": 3, \"level\": \"easy\"}\n{\"task_id\": \"fa9adb815b85d259f943d81874a052e5\", \"confirmed_task\": \"Browse a user homepage that reposted the top song from the Top 50 Rock chart.\", \"website\": \"https://soundcloud.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"b922508886ded315c9835457a6eb43ea\", \"confirmed_task\": \"Browse tenured/tenure-track faculty positions in Computer Sciences & Technology in California.\", \"website\": \"https://jobs.chronicle.com\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"5d542a7ec1fa142ba73cc87d970caf39\", \"confirmed_task\": \"Find the most cited publication at the 2022 CVPR main conference.\", \"website\": \"https://dblp.org/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"864244b6969e0f8733b0eb1ca06cd51f\", \"confirmed_task\": \"Find the race time for who wins the first place in the last race of the 2023 Formula 1 (F1).\", \"website\": \"https://www.espn.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"01abae9608f2d8752a83e08f136f720c\", \"confirmed_task\": \"Show me the code for the company that is the top mover in the Cboe Europe Technology Sector Index (BEPTEC) as of the latest market close.\", \"website\": \"https://www.cboe.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"da8f3823a827c7d3a492f383808e7912\", \"confirmed_task\": \"Find and open the earliest press release.\", \"website\": \"https://www.instructure.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"8689af4d33ce00bf2cdd8987d3bbfd86\", \"confirmed_task\": \"Add the cheapest certified refurbished iPad Air with 256GB of storage in any shade of blue to my bag.\", \"website\": \"https://www.apple.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"78f397336b6fd1cbba0127db7a8cd502\", \"confirmed_task\": \"Browse the upcoming SuperBike events taking place in Italy.\", \"website\": \"https://www.redbull.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"3dca7cbe7d086619d837ff9f5312cebc\", \"confirmed_task\": \"Can you show me products under the category path 'Automotive' -> 'Car Jack', with an additional filter for the color pink?\", \"website\": \"https://us.shein.com/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"b962927dfe03bf2274a54381127ed433\", \"confirmed_task\": \"Find the best-selling vinyl record by an artist from New York City in the classical music genre.\", \"website\": \"https://bandcamp.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"78baf9dbe7c3532f7d7ef4cc22a7f065\", \"confirmed_task\": \"Find the most popular digital trends report in the Finance & Insurance industry within the region of China.\", \"website\": \"https://www.statista.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"c7c07ec10c668625a21ba64165d719bb\", \"confirmed_task\": \"Find the total monthly price for four prepaid unlimited lines without autopay discounts.\", \"website\": \"https://www.verizon.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"512fd4deab099b8dc0dcfc0ec48a3c63\", \"confirmed_task\": \"Identify the open issue with the most comments in the first trending open-source repository this week.\", \"website\": \"https://github.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"d9d8b7d84a3f8d057e368254fe8d65e2\", \"confirmed_task\": \"Find the first commit submitted by NielsRogge to the official repository of the SAM2 model.\", \"website\": \"https://github.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"157f4a79d55e8fa3fd55ba772ba40fbc\", \"confirmed_task\": \"Find the most popular blue Lilo & Stitch toys.\", \"website\": \"https://www.disney.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"62c8d970b3d13891f355911e5a8f4030\", \"confirmed_task\": \"Find the top game listed in the Steam Deck's top-played list over the past year. Then, browse reviews for that game from players who have played over 100 hours and primarily use a Steam Deck.\", \"website\": \"https://store.steampowered.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"11857213ca01510f12813740afd59918\", \"confirmed_task\": \"Add the most top-selling Adidas men's basketball shoe in red, size 10 to my cart.\", \"website\": \"https://www.adidas.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"47bfe8a7e0e4e7efc837287b407fbe90\", \"confirmed_task\": \"Compare the first and second most popular smartphones manufactured by Xiaomi and show the comparison chart.\", \"website\": \"https://versus.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"bb314cb80f0f8489135cbf59074d11e2\", \"confirmed_task\": \"Open the page for the first Best Paper Award video recording of talks from ICLR 2016.\", \"website\": \"https://iclr.cc/\", \"reference_length\": 4, \"level\": \"easy\"}\n{\"task_id\": \"1aeca99e6a60b0e3aefb3ef212bdce79\", \"confirmed_task\": \"Find full-time legal occupation jobs in San Diego County with a minimum salary of $4,000+ per month.\", \"website\": \"https://www.ca.gov/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"d730f4ff450da1bd60a836163736ef6a\", \"confirmed_task\": \"Find the best-selling GORE-TEX men's hiking shoe priced between $100.00 and $199.99 with a rating of 4 stars or higher, and show its most helpful comment.\", \"website\": \"https://www.rei.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"fe33894188d20d7469f37a9fd855e7ff\", \"confirmed_task\": \"Find me Python 3.9 packages on PyPI that are designed for the Web Environment, licensed under MIT, have a stable production status, and are intended for developers.\", \"website\": \"https://pypi.org/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"71f8de1834599fba443f40dbbfab8edd\", \"confirmed_task\": \"Search for papers related to reinforcement learning under the topics of computer science and mathematics on arxiv, with recent submission dates between September 2024 and January 2025.\", \"website\": \"https://arxiv.org/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"c8c1ff115879b3afd14280beb1559b13\", \"confirmed_task\": \"Find the latest Doraemon video in MP4 format that is over 20 minutes long and has a medium file size.\", \"website\": \"https://www.4shared.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"d4fb78b7e74508cd3b33f01cf9200997\", \"confirmed_task\": \"Show the figure comparing Occupational Fatalities Trends between Ohio and New York.\", \"website\": \"https://www.americashealthrankings.org/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"0e42c3a73f2aece1f854e0ba55b7c8b0\", \"confirmed_task\": \"Find a gas station in Manhattan, NY with a rating above 4.0, and sort the user reviews by the lowest rating.\", \"website\": \"https://www.google.com/maps/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"96afb3c51146b0c2a9c55f039a5ea6d6\", \"confirmed_task\": \"Find the most frequent word that rhymes with \\\"thought\\\" and has three syllables.\", \"website\": \"https://www.merriam-webster.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"59912927c1fddee6ded8a49986896bc2\", \"confirmed_task\": \"Look for the most useful reviews of the highest-rated anti-reflective TVs with screen sizes from 55\\\" to 64\\\" and prices ranging from $300 to $1500.\", \"website\": \"https://www.samsung.com/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"e43cbc8a0bf9e999884928d11006f894\", \"confirmed_task\": \"Browse the list of things to do in Miami that have a rating of 9+ (wonderful), last between 1 to 4 hours per session, cost under $100 per person, and are available for booking between next Monday and next Friday.\", \"website\": \"https://www.expedia.com/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"1b867afecf072cb877ebfa4069263746\", \"confirmed_task\": \"Display the figure comparing unemployment trends among women in Illinois and Michigan.\", \"website\": \"https://www.americashealthrankings.org/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"c3a333968fc3c43d7f2688f425a0d633\", \"confirmed_task\": \"Find the cheapest certified pre-owned Porsche 911 with a model year of 2019 or newer, within a 200-mile radius of ZIP code 97007.\", \"website\": \"https://www.porsche.com/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"bb5d90e6f2fbc0ae146f7c1998c2b4a1\", \"confirmed_task\": \"Find the most viewed TED talk on the topic of robots that lasts between 12 and 18 minutes.\", \"website\": \"https://www.ted.com/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"c577a14301a725e09ccd269a3e0b271e\", \"confirmed_task\": \"Return the page for the highest-rated red wine from Oregon under $40 that pairs well with either mushrooms or veal.\", \"website\": \"https://www.vivino.com/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"c6c9dc6079677cef594cec2fa6b16602\", \"confirmed_task\": \"Add the cheapest black sofa with at least three seats, a leather finish, and at least four stars to my cart.\", \"website\": \"https://www.ikea.com/\", \"reference_length\": 16, \"level\": \"hard\"}\n{\"task_id\": \"c39d6c245f8243993e707d54d2f4acec\", \"confirmed_task\": \"Browse the final skin in the list for the champion Ahri.\", \"website\": \"https://www.leagueoflegends.com/\", \"reference_length\": 18, \"level\": \"hard\"}\n{\"task_id\": \"b2f4fde2fce122a93c7b578086cb0585\", \"confirmed_task\": \"Find the cheapest hotel + flight + car package from New York to San Francisco, departing tomorrow and returning on the fourth day from departure, for two adults and a six-year-old child. The package should be one room with free breakfast and spa access.\", \"website\": \"https://www.booking.com/\", \"reference_length\": 19, \"level\": \"hard\"}\n{\"task_id\": \"d02d236836924919f35f2438d9ed2374\", \"confirmed_task\": \"Browse the top 250 movies and find one movie that is available on AMC+.\", \"website\": \"https://www.imdb.com/\", \"reference_length\": 22, \"level\": \"hard\"}\n{\"task_id\": \"3621b099326c7aebd2e2dac6be3b52d1\", \"confirmed_task\": \"Open the profile page of the leader of the Nvidia Learning and Perception Lab.\", \"website\": \"https://www.nvidia.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"f27b393bbd2082f92b566270c4b74fe6\", \"confirmed_task\": \"Find a large van for sale from the year 2024 or newer with up to 10,000 miles.\", \"website\": \"https://www.parkers.co.uk/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"ba01ea557b73f864c35ebba0dd6f3cb2\", \"confirmed_task\": \"Find the top-rated hotel in Manhattan, NY, suitable for 4 guests, and identify the fastest public transportation option from the hotel to LGA airport.\", \"website\": \"https://www.google.com/maps/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"662ae0f2d3ac851dbcdd245f908277e3\", \"confirmed_task\": \"What is the second stop among the best stops along the road trip from Yellowstone National Park to Las Vegas?\", \"website\": \"https://wanderlog.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"461ab9b0c7b20ac5f912704480979c65\", \"confirmed_task\": \"Find the NYSE Rule 605 Market Center Files data for July 2024.\", \"website\": \"https://www.nyse.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"a96fca87a17d792644e736d1d10d3cbe\", \"confirmed_task\": \"View the pricing plan for 'Business'. Specifically, we have 100 users. We need a 1PB storage quota and a 50 TB transfer quota.\", \"website\": \"https://mega.io/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"2c8ef01a92c71ba9ef2e59bb17eea2b3\", \"confirmed_task\": \"If there are any discounts on the Apple Mac Studio, add the one with the largest absolute discount to my cart; otherwise, add the cheapest one.\", \"website\": \"https://www.costco.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"3084bc225219fcb73dc1cb0f97276c1c\", \"confirmed_task\": \"Get quotes for a package weighing 10 lbs with dimensions of 2 inches in length, width, and height, being shipped from Long Beach, 90802 to Portland, 97201.\", \"website\": \"https://www.ups.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"949dc965a6c23a95663b3bc2ca2c3a8a\", \"confirmed_task\": \"Find UA or AA flights from London to New York that arrive between 8:00 PM and 11:00 PM on FlightAware.\", \"website\": \"https://www.flightaware.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"636b07af4dd97c1793733db1fd1b90b8\", \"confirmed_task\": \"Filter handbags to evening bags that are blue, and polyester and cost less than $100.\", \"website\": \"https://www.macys.com/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"38203be65401943aea2179c4c680059a\", \"confirmed_task\": \"Check the status of bus S92 for any disruptions on new.mta.info.\", \"website\": \"https://new.mta.info/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"cf757a775fa1224acfc7998489e199a8\", \"confirmed_task\": \"Find a flight from Dublin to anywhere under $100 tomorrow on Ryanair.\", \"website\": \"https://www.ryanair.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"d8e2a81fa621ce4737e5ea85671b630e\", \"confirmed_task\": \"Search for regular weekday jobs around 14810 that I can start within two weeks or three.\", \"website\": \"https://hiring.amazon.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"63d6866fc000fcb1f153e07604bd1395\", \"confirmed_task\": \"What are the Nearby Attractions from the most popular attraction in Hong Kong?\", \"website\": \"https://us.trip.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"199be0b54a436daee74247971fc684ee\", \"confirmed_task\": \"Add a Macy's Happy Birthday E-Gift Card worth $50 from Shak to my cart, with the birthday wish message \\\"Happy birthday, wish you many more years to come\\\", addressed to christene (christenson@gmail.com).\", \"website\": \"https://www.macys.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"c00437fd76a7a83b57f3dc4e5dbc41f8\", \"confirmed_task\": \"Check the most recent full-time medical health and safety jobs, requiring 1-3 years of industry experience available in the US.\", \"website\": \"https://www.amazon.jobs/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"fc53ddd3421411a41c1020a3fdc84ec4\", \"confirmed_task\": \"I want to purchase an open-box Samsung Galaxy S25 Plus in excellent condition and trade in a gray Galaxy S20 5G (Verizon), with a perfect screen, in good condition. How much would it cost?\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 17, \"level\": \"hard\"}\n{\"task_id\": \"9d46ccb915eff39ee1ae1e7328f5f20d\", \"confirmed_task\": \"Get a quote for the fastest shipping available for 5 lbs with dimensions of 4 inches in length, width, and height from New York, NY 10001, USA to Truckee, California 96162, USA.\", \"website\": \"https://www.ups.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"d1970c16271496cbbe166ecbecc0a1d8\", \"confirmed_task\": \"I'm 25 and located in Texas. Shop for 2020 made dry red wine made in United States priced between 15-20 dollars and add 5 bottles to the cart.\", \"website\": \"https://macyswineshop.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"7211af65d266402f99499053924262e9\", \"confirmed_task\": \"View the most recent job posting for a full-time pharmacy position in the US.\", \"website\": \"https://www.amazon.jobs/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"4464a8421f8bc8786524a499258dfad3\", \"confirmed_task\": \"Check the specifications of the best-selling HP FHD laptop with 16 GB RAM and core i7 running on Windows 11.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"f707d765bca668830745d20807d7bee6\", \"confirmed_task\": \"Show me the list of young female English Spot rabbits available for adoption in Chicago, IL, within 50 miles.\", \"website\": \"https://www.petfinder.com/\", \"reference_length\": 14, \"level\": \"hard\"}\n{\"task_id\": \"d392e154c1c6ffbb26e2331c3afafc67\", \"confirmed_task\": \"Add a $100 Best Buy gift card for a birthday to my cart.\", \"website\": \"https://www.bestbuy.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"a5c87cc1c94a090c9a8dc2c8b6a125d0\", \"confirmed_task\": \"Find the SO2 air quality over the past hour for Maine North, County Cork, Ireland.\", \"website\": \"https://www.accuweather.com/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"367d843c640637745e8fafa741cca13b\", \"confirmed_task\": \"Find a condo for rent in Houston, TX, with a monthly rent of no more than 30% of an income of $8000. The condo should have a minimum area of 600 square feet, and the move-in date is the 1st of next month.\", \"website\": \"https://www.apartments.com/\", \"reference_length\": 15, \"level\": \"hard\"}\n{\"task_id\": \"84ef883a37af638c3bcf7561f28ce80a\", \"confirmed_task\": \"Find the cheapest used hatchback car listing in Madison which has black interiors with a heated seat option and premium sound system.\", \"website\": \"https://www.cars.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"d9a8689393effeed75ea0866e44e1def\", \"confirmed_task\": \"Find the address and phone of the Office of the Inspector General (OIG).\", \"website\": \"https://www.justice.gov/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"1bc154377120ec15b18dbabdba49c741\", \"confirmed_task\": \"Book 4 tickets in the upper for any Kevin Hart show in New York in the next three months and view ticket prices with estimated fees.\", \"website\": \"https://www.stubhub.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"28e7574e7bd6d14f36d2988a5ef2bd23\", \"confirmed_task\": \"Get a part-time job within 5 miles of Moscow, Idaho in the accommodation and food services industry, as a chef, and show jobs for corporate only.\", \"website\": \"https://ohiomeansjobs.ohio.gov/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"1c3b747ae12ccee895745f82e3f2ef8a\", \"confirmed_task\": \"Identify the ongoing competition that offers the highest prize and find the code that received the most votes in that competition.\", \"website\": \"https://www.kaggle.com/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"d1807551297ac60ecaaabbd2a2ed301a\", \"confirmed_task\": \"Find the No.1 children's hospital in the California that specializes in Neonatology.\", \"website\": \"https://health.usnews.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"7abdceee212151f187ee1a1744c57606\", \"confirmed_task\": \"Can you show me the page with the filing fee for a self-petitioned I-140 application?\", \"website\": \"https://www.uscis.gov/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"0e5536aaad9d3462b06cf725e6ed535a\", \"confirmed_task\": \"Show me the page with average wait times for U.S. citizens arriving at Raleigh-Durham International Airport on 2025-03-12.\", \"website\": \"https://www.cbp.gov/\", \"reference_length\": 11, \"level\": \"hard\"}\n{\"task_id\": \"bc2ce7f206045dd2d322e5695a947219\", \"confirmed_task\": \"Estimate the federal income tax I would owe on $158,500 of taxable income in ZIP code 97007, filing as single.\", \"website\": \"https://smartasset.com/\", \"reference_length\": 6, \"level\": \"medium\"}\n{\"task_id\": \"7e6993f2c5cd72c44809024f0bc85dc1\", \"confirmed_task\": \"Create a meme with a frog as the background and leave the only text with \\\"Enjoy your life\\\".\", \"website\": \"https://imgur.com/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"a48e2f1ee8d87eaeea56fe5e730427e6\", \"confirmed_task\": \"Pass the first trending chess puzzle.\", \"website\": \"https://www.chess.com/\", \"reference_length\": 7, \"level\": \"medium\"}\n{\"task_id\": \"dd44c665cec1e9c929a4c5f074e7844a\", \"confirmed_task\": \"Find parking near the San Francisco Museum of Modern Art from June 18, 1:00 PM to 5:00 PM. I'm driving a Ford F-150 and need a garage that allows in-and-out privileges. If there are multiple options, show me the details of the one with the lowest price.\", \"website\": \"https://spothero.com/\", \"reference_length\": 17, \"level\": \"hard\"}\n{\"task_id\": \"99daaed9a83c266341d28aa40067d376\", \"confirmed_task\": \"Find the most popular board game on the 'The Hotness' list that has a rating above 7.5 and is suitable for 2 players.\", \"website\": \"https://boardgamegeek.com/\", \"reference_length\": 5, \"level\": \"easy\"}\n{\"task_id\": \"7072d09436972a5d5fe7476e3e9f1559\", \"confirmed_task\": \"Show me the comparison of the first two personal credit cards that do not charge foreign transaction fees.\", \"website\": \"https://www.americanexpress.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"2c20d87a046fadcb6ff07ee877bfbf37\", \"confirmed_task\": \"Open the form 8843 for tax year 2022.\", \"website\": \"https://www.irs.gov/\", \"reference_length\": 8, \"level\": \"medium\"}\n{\"task_id\": \"753f372c189d3b306623cb0c65b50320\", \"confirmed_task\": \"Compare the U.S. ETP Odd Lot Rate (%) between Quartile 1 and Quartile 4, viewing quartiles by price, and display the chart with a logarithmic scale on the vertical axis.\", \"website\": \"https://www.sec.gov/\", \"reference_length\": 9, \"level\": \"medium\"}\n{\"task_id\": \"733f1d8bf79d5bc2240c5357f928ffff\", \"confirmed_task\": \"Find the cheapest travel deal or discount to Thailand that lasts more than 10 days, departs in next month, and show the total price.\", \"website\": \"https://www.tourradar.com/\", \"reference_length\": 10, \"level\": \"medium\"}\n{\"task_id\": \"f05e87c5b92d9869e08806103c1c15a1\", \"confirmed_task\": \"Find all startup companies from the 2022 and 2023 Y Combinator batches that are based in France and currently have job openings.\", \"website\": \"https://www.ycombinator.com/\", \"reference_length\": 12, \"level\": \"hard\"}\n{\"task_id\": \"3ef64f34eae59c9fac7ee9a4f18b4a0c\", \"confirmed_task\": \"Find and open an animal learning course on YouTube Kids for my 6-year-old without login in. As a parent born in 1992, I would prefer not to enable search.\", \"website\": \"https://www.youtube.com/\", \"reference_length\": 16, \"level\": \"hard\"}\n{\"task_id\": \"f158345f8489e0d1d91e28768c39bca1\", \"confirmed_task\": \"Estimate the total cost (with basic support) of using 5 million input tokens and 5 million output tokens each for GPT-4o and GPT-4o Mini, both deployed in the US/EU Data Zones under Standard (On-Demand) in the East US region.\", \"website\": \"https://azure.microsoft.com/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"1ab384fb3a791edfb410213cc6b82151\", \"confirmed_task\": \"Show me the result of a proton emission decay for a Beryllium nucleus with 6 protons and 4 neutrons in the simulation.\", \"website\": \"https://phet.colorado.edu/\", \"reference_length\": 13, \"level\": \"hard\"}\n{\"task_id\": \"1223b07536a87e0170ff87cbbebd1d3c\", \"confirmed_task\": \"Complete a multiplication quiz on https://www.coolmath4kids.com/, covering multiplication facts for 11-12. The quiz should consist of 10 questions, with unlimited time allowed for each. The goal is to achieve a perfect score of 10 out of 10.\", \"website\": \"https://www.coolmath4kids.com/\", \"reference_length\": 24, \"level\": \"hard\"}\n"
  },
  {
    "path": "packages/evals/datasets/webtailbench/WebTailBench_data.jsonl",
    "content": "{\"id\":\"united_13\",\"category\":\"flights\",\"ques\":\"What is the price difference between economy and business class on United Airlines direct flights from Chicago to São Paulo from 11/24/2025 to 12/14/2025? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ryanair_55\",\"category\":\"flights\",\"ques\":\"How many seats with extra legroom are available on Ryanair from Birmingham, UK to Porto, Portugal flying out 11/23/2025 and coming back 11/18/2025? If there are no available flights for those dates or this is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"westjet_47\",\"category\":\"flights\",\"ques\":\"What is the checked baggage allowance and any associated fees for WestJet flights from Waterloo, Ontario to Calgary, Alberta September 10, 2026 - September 27, 2026 round trip? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"airasia_88\",\"category\":\"flights\",\"ques\":\"How much does it cost to select a window seat on a direct AirAsia flight from Singapore to Langkawi from November 24 to November 27? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"jal_61\",\"category\":\"flights\",\"ques\":\"What meal options are available in premium economy on Japan Airlines from Dallas/Fort Worth to Singapore leaving on April 23 returning May 3? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"cathaypacific_59\",\"category\":\"flights\",\"ques\":\"How much would it cost to upgrade from economy to business class on Cathay Pacific from Manila to Hong Kong November 17 - December 12? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"alitalia_37\",\"category\":\"flights\",\"ques\":\"What are the flight duration and number of daily flights with ITA from Rome to Naples leaving on February 23 returning March 18? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"jetstar_22\",\"category\":\"flights\",\"ques\":\"What is the cancellation and change fee policy for Jetstar from Darwin to Adelaide in a month for a two week trip? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"alaskaair_6\",\"category\":\"flights\",\"ques\":\"How many exit row seats are still available on Alaska Airlines flights from Seattle, WA to Honolulu, HI 11/29/2025 - 12/03/2025? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"easyjet_87\",\"category\":\"flights\",\"ques\":\"What is the total cost including all fees and taxes for the cheapest EasyJet flight from Palma de Mallorca to Newcastle December 3 - December 23? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"jetstar_10\",\"category\":\"flights\",\"ques\":\"Does Jetstar offer any bundle deals or packages for flights from Adelaide to Sunshine Coast November 18 - November 25 round trip? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"singaporeair_9\",\"category\":\"flights\",\"ques\":\"Can you help me find just the flight numbers of a Singapore Airlines flight from London (LHR) to Sydney (SYD) via Singapore (SIN) leaving July 2 and coming back July 28? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"spirit_9\",\"category\":\"flights\",\"ques\":\"How much more expensive is a \\\"Big Front Seat\\\" compared to standard economy on Spirit Airlines from Houston to Los Angeles beginning March 5 till March 20? If there are no available flights for those dates, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"goindigo_52\",\"category\":\"flights\",\"ques\":\"How much are business class seats on IndiGo from Sharjah (SHJ) to Delhi (DEL) outbound on January 13 returning January 19, if available? If there are no available flights for those dates or business class is not available, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"thaiairways_13\",\"category\":\"flights\",\"ques\":\"Book a flight with Thai Airways from Bangkok, Thailand to Singapore. outbound on November 19 returning December 4. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"vueling_15\",\"category\":\"flights\",\"ques\":\"Book a flight with Vueling from Birmingham, UK to Barcelona, Spain departing November 28 and returning December 16. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"turkishairlines_11\",\"category\":\"flights\",\"ques\":\"Book a round-trip flight with Turkish Airlines from Istanbul Airport (IST) to John F. Kennedy International Airport (JFK) for a two week trip starting the upcoming Saturday. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"airasia_50\",\"category\":\"flights\",\"ques\":\"Book a flight with AirAsia from Hong Kong to Manila leaving December 2 and coming back December 8. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"britishairways_11\",\"category\":\"flights\",\"ques\":\"Book a round-trip flight with British Airways from Manchester Airport to London Heathrow from the upcoming Friday for four days. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"suncountry_2\",\"category\":\"flights\",\"ques\":\"Book a flight with Sun Country Airlines from Duluth, MN to Phoenix, AZ from January 17 to January 31. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"thaiairways_9\",\"category\":\"flights\",\"ques\":\"Book a flight with Thai Airways from Bangkok to London departing November 16 and returning November 26. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"lufthansa_39\",\"category\":\"flights\",\"ques\":\"Book a flight with Lufthansa from Frankfurt, Germany to Tel Aviv, Israel beginning November 18 till November 30. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"suncountry_9\",\"category\":\"flights\",\"ques\":\"Book a flight with Sun Country Airlines from Tampa, FL to Dallas, TX outbound on February 9 returning February 28. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"klm_9\",\"category\":\"flights\",\"ques\":\"Book a flight with KLM from Lagos, Nigeria to Frankfurt, Germany flying out 11/18/2025 → coming back 11/25/2025. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"jetstar_82\",\"category\":\"flights\",\"ques\":\"Book a flight with Jetstar from Brisbane to Perth from 03/20/2026 → 04/03/2026. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"aircanada_54\",\"category\":\"flights\",\"ques\":\"Book a flight with Air Canada from Vancouver to Penticton June 9 - July 4. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"southwest_18\",\"category\":\"flights\",\"ques\":\"Book a flight with Southwest Airlines from Portland, OR to Salt Lake City, UT flying out 05/15/2026 → coming back 05/17/2026. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"allegiantair_18\",\"category\":\"flights\",\"ques\":\"Book a flight with United Airlines from Houston to Newark, NJ February 11 - March 2. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"airasia_7\",\"category\":\"flights\",\"ques\":\"Book a round-trip flight with Delta from Boston, MA to San Francisco, CA outbound in the Saturday after next week. Make the round-trip be two weeks length. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"philippineairlines_45\",\"category\":\"flights\",\"ques\":\"Book a flight with Philippine Airlines from Manila to Singapore from November 16 to December 15. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"aircanada_27\",\"category\":\"flights\",\"ques\":\"Book a flight with Air Canada from Toronto, ON to New York City, NY leaving on December 10 returning January 7. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"singaporeair_41\",\"category\":\"flights\",\"ques\":\"Book a flight with Singapore Airlines from Singapore to Naha, Japan beginning February 10 till February 17. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"suncountry_12\",\"category\":\"flights\",\"ques\":\"Book a flight with Sun Country Airlines from San Francisco (SFO) to Minneapolis (MSP) December 18- January 3 round trip. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"lot_5\",\"category\":\"flights\",\"ques\":\"Book a flight with LOT Polish Airlines from Warsaw, Poland to New York City, USA March 25 - April 22 round trip. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"allegiantair_53\",\"category\":\"flights\",\"ques\":\"Book a flight with Allegiant Air from Asheville, NC to Boston, MA leaving on November 22 returning December 12. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"spirit_5\",\"category\":\"flights\",\"ques\":\"Book a Spirit Airlines flight from BWI airport to Newark Liberty International Airport (EWR) beginning May 2 till June 2. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"malaysiaairlines_95\",\"category\":\"flights\",\"ques\":\"Book a flight with Malaysia Airlines from Kuala Lumpur to Kathmandu outbound on March 4 returning March 21. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"swiss_48\",\"category\":\"flights\",\"ques\":\"Book a Swiss Airlines flight to Mumbai from Zurich outbound on November 22 returning December 12. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"iberia_41\",\"category\":\"flights\",\"ques\":\"Book a flight for two people with Iberia from Madrid, Spain to Santiago, Chile beginning July 17 till August 11. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"vueling_28\",\"category\":\"flights\",\"ques\":\"Book a flight with Vueling from London to Asturias Airport (OVD) from May 22 to  June 17. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ana_22\",\"category\":\"flights\",\"ques\":\"Book a flight with ANA from Singapore to Fukuoka March 24 - March 27. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"thaiairways_11\",\"category\":\"flights\",\"ques\":\"Book a flight with Thai Airways from Thailand to Sydney, Australia from November 16 through December 11. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"wizzair_96\",\"category\":\"flights\",\"ques\":\"Book a flight with Wizz Air from Larnaca, Cyprus to Athens, Greece outbound on February 9 returning February 21. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"jetstar_66\",\"category\":\"flights\",\"ques\":\"Book a cheap flight with Jetstar from Sydney to Hobart outbound on December 20 returning January 6. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"klm_21\",\"category\":\"flights\",\"ques\":\"Book a flight with KLM from Geneva, Switzerland to Osaka, Japan from 11/22/2025 → 11/28/2025. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"iberia_27\",\"category\":\"flights\",\"ques\":\"Book a flight with Iberia from Alicante to Funchal leaving on March 11 returning March 25. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"koreanair_0\",\"category\":\"flights\",\"ques\":\"Book a cheap flight with Korean Air from Los Angeles, CA to Seoul, South Korea from November 30 to December 30. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"thaiairways_18\",\"category\":\"flights\",\"ques\":\"Book a VTL flight with Thai Airways from Bangkok to Singapore leaving on May 1 returning May 21. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"jetblue_48\",\"category\":\"flights\",\"ques\":\"Book a flight with JetBlue from Orlando, FL to Denver, CO from December 19 through January12. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"goindigo_24\",\"category\":\"flights\",\"ques\":\"Book a flight with IndiGo from Bhubaneswar (BBSR) to Delhi (DEL) from February 20 to March 3. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"aerlingus_93\",\"category\":\"flights\",\"ques\":\"Book a direct flight with Aer Lingus from Dublin to Orlando outbound on December 7 returning December 22. If there are no available flights for those dates or the booking is not possible, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"samsung_9702\",\"category\":\"shopping_head\",\"ques\":\"I want to buy the Samsung Galaxy Tab S11+ 256GB Wi-Fi from Samsung.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_8235\",\"category\":\"shopping_head\",\"ques\":\"Can you help me purchase the Electrosport ESR 150 from Amazon?\\r\",\"web\":\"\"}\n{\"id\":\"amazon_9969\",\"category\":\"shopping_head\",\"ques\":\"I'm looking to buy Disney Grumpy stuffed plush toy from Amazon.\\r\",\"web\":\"\"}\n{\"id\":\"underarmour_6889\",\"category\":\"shopping_head\",\"ques\":\"I need to purchase Under Armour Men's Project Rock BSR size 8 training shoes from Under Armour.\\r\",\"web\":\"\"}\n{\"id\":\"publix_9146\",\"category\":\"shopping_head\",\"ques\":\"Could you help me order Febreze Air Freshener from publix for delivery (use 32204 zip code for the store)?\\r\",\"web\":\"\"}\n{\"id\":\"rockauto_4460\",\"category\":\"shopping_head\",\"ques\":\"I'd like to get an E450 parking brake rotor and brake pad kit from RockAuto.\\r\",\"web\":\"\"}\n{\"id\":\"underarmour_3963\",\"category\":\"shopping_head\",\"ques\":\"Can you order Under Armour kids' lunch boxes from Under Armour for me?\\r\",\"web\":\"\"}\n{\"id\":\"rockauto_6656\",\"category\":\"shopping_head\",\"ques\":\"Help me buy a radiator for a 1995 Ford F-350 Powerstroke 7.3 from RockAuto.\\r\",\"web\":\"\"}\n{\"id\":\"hobbylobby_351\",\"category\":\"shopping_head\",\"ques\":\"I'm trying to purchase 1/4-inch square hardwood dowels from Hobby Lobby.\\r\",\"web\":\"\"}\n{\"id\":\"overstock_8717\",\"category\":\"shopping_head\",\"ques\":\"I want to order a wall-hung bathroom sink (14\\\" x 12\\\") from Overstock.\\r\",\"web\":\"\"}\n{\"id\":\"publix_2256\",\"category\":\"shopping_head\",\"ques\":\"Can you help me buy a pack of organic broccoli florets from publix for delivery? (use 32204 zip code for the store)\\r\",\"web\":\"\"}\n{\"id\":\"amazon_1934\",\"category\":\"shopping_head\",\"ques\":\"I need to get The Witches movie (widescreen edition) from Amazon.\\r\",\"web\":\"\"}\n{\"id\":\"bestbuy_5569\",\"category\":\"shopping_head\",\"ques\":\"I'm looking for a refrigerator with a built-in water dispenser from Best Buy.\\r\",\"web\":\"\"}\n{\"id\":\"ebay_1007\",\"category\":\"shopping_head\",\"ques\":\"I'd like to purchase the Ninco BMW Amprex from eBay.\\r\",\"web\":\"\"}\n{\"id\":\"sears_4887\",\"category\":\"shopping_head\",\"ques\":\"Can you help me order a 30-inch Café induction cooktop on sale from Sears?\\r\",\"web\":\"\"}\n{\"id\":\"ebay_8268\",\"category\":\"shopping_head\",\"ques\":\"I want to buy a 1939 issue of Adventure magazine from eBay.\\r\",\"web\":\"\"}\n{\"id\":\"bestbuy_8406\",\"category\":\"shopping_head\",\"ques\":\"I need to order a 20-foot printer cable from Best Buy.\\r\",\"web\":\"\"}\n{\"id\":\"westelm_7538\",\"category\":\"shopping_head\",\"ques\":\"I'm looking to get a green rug of size 8'x10' or something close from West Elm.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_7859\",\"category\":\"shopping_head\",\"ques\":\"Could you help me buy Storm Fury Book 1 from Amazon?\\r\",\"web\":\"\"}\n{\"id\":\"rei_4150\",\"category\":\"shopping_head\",\"ques\":\"I want to purchase the Currex Insole M size from REI.\\r\",\"web\":\"\"}\n{\"id\":\"ikea_4872\",\"category\":\"shopping_head\",\"ques\":\"I'm looking for the cheapest queen size mattress from Ikea\\r\",\"web\":\"\"}\n{\"id\":\"gap_3164\",\"category\":\"shopping_head\",\"ques\":\"I need to buy white Modern V-Neck T-Shirt m size from Gap\\r\",\"web\":\"\"}\n{\"id\":\"sears_6088\",\"category\":\"shopping_head\",\"ques\":\"Can you help me get the cheapest 18 cu ft freezer from Sears?\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_7304\",\"category\":\"shopping_head\",\"ques\":\"Purchase the DeWalt Atomic Sawzall from Home Depot.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_4045\",\"category\":\"shopping_head\",\"ques\":\"Purchase the book \\\"El vuelo de una abeja\\\" from Amazon.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_970\",\"category\":\"shopping_head\",\"ques\":\"Purchase 20 disposable plastic bowls on amazon\\r\",\"web\":\"\"}\n{\"id\":\"hobbylobby_299\",\"category\":\"shopping_head\",\"ques\":\"Buy some metal cake stand from Hobby Lobby\\r\",\"web\":\"\"}\n{\"id\":\"zappos_9900\",\"category\":\"shopping_head\",\"ques\":\"Purchase navy Clarks shoes for women size 8 from Zappos.\\r\",\"web\":\"\"}\n{\"id\":\"publix_4839\",\"category\":\"shopping_head\",\"ques\":\"Buy a Jimmy Dean pork sausages from Publix for delivery (use 32204 zip code for the store).\\r\",\"web\":\"\"}\n{\"id\":\"overstock_9388\",\"category\":\"shopping_head\",\"ques\":\"Purchase Steve Madden tall women's boots 9 size\\r\",\"web\":\"\"}\n{\"id\":\"underarmour_7483\",\"category\":\"shopping_head\",\"ques\":\"Purchase the Under Armour mens beanie from Under Armour.\\r\",\"web\":\"\"}\n{\"id\":\"potterybarn_7344\",\"category\":\"shopping_head\",\"ques\":\"Purchase a light color around 90' long Chesterfield-style sectional sofa from Pottery Barn.\\r\",\"web\":\"\"}\n{\"id\":\"potterybarn_1237    \",\"category\":\"shopping_head\",\"ques\":\"Help me purchase a rectangular drop leaf dining table from Pottery Barn that's at least 54\\\" long.\\r\",\"web\":\"\"}\n{\"id\":\"kohls_8946\",\"category\":\"shopping_head\",\"ques\":\"Purchase pink Skechers girls’ slip-on shoes size 13 from Kohl’s.\\r\",\"web\":\"\"}\n{\"id\":\"rockauto_1225\",\"category\":\"shopping_head\",\"ques\":\"Purchase intake coolant hoses (molded, silicone) from RockAuto.\\r\",\"web\":\"\"}\n{\"id\":\"wholefoodsmarket_5324\",\"category\":\"shopping_head\",\"ques\":\"Purchase 6 fcans of zero-sugar cola from Whole Foods Market.\\r\",\"web\":\"\"}\n{\"id\":\"overstock_9756\",\"category\":\"shopping_head\",\"ques\":\"Purchase ~20\\\" wide by ~30\\\" high medicine cabinets from Overstock.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_1230\",\"category\":\"shopping_head\",\"ques\":\"Purchase configuration of RT81 Turntable with AT95E Cartridge (no more than 350$ configuration) from Amazon\\r\",\"web\":\"\"}\n{\"id\":\"lowes_8758\",\"category\":\"shopping_head\",\"ques\":\"Purchase a cotoneaster plant from Lowe's\\r\",\"web\":\"\"}\n{\"id\":\"ikea_2219\",\"category\":\"shopping_head\",\"ques\":\"Purchase a hammock chair with stand from IKEA.\\r\",\"web\":\"\"}\n{\"id\":\"westelm_19\",\"category\":\"shopping_head\",\"ques\":\"Purchase the Gemini Bed from West Elm.\\r\",\"web\":\"\"}\n{\"id\":\"target_4231\",\"category\":\"shopping_head\",\"ques\":\"Purchase 12 cups of Snack Pack sugar-free pudding from Target.\\r\",\"web\":\"\"}\n{\"id\":\"sears_4759\",\"category\":\"shopping_head\",\"ques\":\"Purchase Lush Decor Bohemian Stripe window curtains in turquoise and orange from Sears.\\r\",\"web\":\"\"}\n{\"id\":\"ulta_1473\",\"category\":\"shopping_head\",\"ques\":\"Purchase the Dashing Dive Glaze Starter Kit from Ulta.\\r\",\"web\":\"\"}\n{\"id\":\"overstock_2959\",\"category\":\"shopping_head\",\"ques\":\"Purchase a cheapest Costway dog bed from Overstock with shipping to Canada.\\r\",\"web\":\"\"}\n{\"id\":\"underarmour_784\",\"category\":\"shopping_head\",\"ques\":\"Purchase the Under Armour Men's UA Base 4 long sleeve M size from Under Armour.\\r\",\"web\":\"\"}\n{\"id\":\"wholefoodsmarket_4455\",\"category\":\"shopping_head\",\"ques\":\"Purchase 4 bottles of Belvoir Lemonade from Whole Foods.\\r\",\"web\":\"\"}\n{\"id\":\"lowes_6063\",\"category\":\"shopping_head\",\"ques\":\"Purchase 4 tier chrome shelving from Lowe’s approximately 35 inches width and 50 inches height.\\r\",\"web\":\"\"}\n{\"id\":\"target_6682\",\"category\":\"shopping_head\",\"ques\":\"Purchase Aveeno sunscreen lotion with 60 spf from Target.\\r\",\"web\":\"\"}\n{\"id\":\"michaels_2250\",\"category\":\"shopping_head\",\"ques\":\"Purchase baby fabric sold by the half yard from Michaels.\\r\",\"web\":\"\"}\n{\"id\":\"publix_8722\",\"category\":\"shopping_head\",\"ques\":\"Have Publix deliver Heinz Apple Cider Vinegar (use 32204 zip code for the store).\\r\",\"web\":\"\"}\n{\"id\":\"crateandbarrel_2072\",\"category\":\"shopping_head\",\"ques\":\"Purchase a ceramic photo frame from Crate & Barrel.\\r\",\"web\":\"\"}\n{\"id\":\"nordstrom_5374\",\"category\":\"shopping_head\",\"ques\":\"Purchase women's full-length leather coat S size less than 200$ from Nordstrom.\\r\",\"web\":\"\"}\n{\"id\":\"publix_3096\",\"category\":\"shopping_head\",\"ques\":\"Find prepared pasta salads from publix for delivery (use 32204 zip code for the store).\\r\",\"web\":\"\"}\n{\"id\":\"petsmart_5650\",\"category\":\"shopping_head\",\"ques\":\"Purchase a 20-gallon fish tank from PetSmart.\\r\",\"web\":\"\"}\n{\"id\":\"kohls_7716\",\"category\":\"shopping_head\",\"ques\":\"Purchase a Starter Pittsburgh Steelers hoodie from Kohl's.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_93\",\"category\":\"things_to_do\",\"ques\":\"Submit a request form to book a tasting tour at St. Michaels Winery in maryland (but don't hit \\\"send\\\"). Then give me their phone number to confirm.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_97\",\"category\":\"things_to_do\",\"ques\":\"Book tickets for the next murder mystery dinner event for me and my wife in Ocala, Florida and tell me the total price\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_110\",\"category\":\"things_to_do\",\"ques\":\"What is the next recreational event (like cherry blossom festival) coming up on the City of Monterey Park, California municipal calendar?\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_128\",\"category\":\"things_to_do\",\"ques\":\"Find 2 ziplining places in Marylan, and provide their address. Which is closer to Baltimore?\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_162\",\"category\":\"things_to_do\",\"ques\":\"Find a deep sea fishing tour option on Viator in Moorea, Society Islands and give me the total cost and start time of the tour\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_126\",\"category\":\"things_to_do\",\"ques\":\"Find the next board of commissioners meeting for the city of Covington, Kentucky and tell me where I can livestream it at\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_plan_a_trip_13\",\"category\":\"things_to_do\",\"ques\":\"Buy a one day MONT BLANC MultiPass for hiking for the next available date and tell me the price, for one adult\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_243\",\"category\":\"things_to_do\",\"ques\":\"What is the top rated hiking trail in Creekside Park, Salinas, California and provide details on the length and difficulty\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_92\",\"category\":\"things_to_do\",\"ques\":\"Register me for the turkey trot event coming up in Coppell, Texas, tell me how much it costs and when it is.\\r\",\"web\":\"\"}\n{\"id\":\"hipcamp_find_111\",\"category\":\"things_to_do\",\"ques\":\"I want to book a camping spot at Bridge Bay in Yellowstone for the next available slot; how much is the nightly rate?\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_question_answering_148\",\"category\":\"things_to_do\",\"ques\":\"help me register for the new years day 5k in chesapeake city, MD on raceroster.com. Then tell me who is the event contact.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_51\",\"category\":\"things_to_do\",\"ques\":\"help me plan a weekend going to events with my kids on discover baltimore county websites\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_question_answering_185\",\"category\":\"things_to_do\",\"ques\":\"Write a review on tripadvisor giving the NCL excursion to Volcano Winery on the Island of Hawaii a 4 start review\\r\",\"web\":\"\"}\n{\"id\":\"sixflags_find_71\",\"category\":\"things_to_do\",\"ques\":\"What is the price of a military discount ticket for Six Flags at Darien Lake, New York and then try to book a ticket. Stop once I am asked to login to verify my military membership.\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_recommend_158\",\"category\":\"things_to_do\",\"ques\":\"Reserve an airboat ride with more than 500 reviews in Kissimmee, Florida on tripadvisor\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_general_activity_20\",\"category\":\"things_to_do\",\"ques\":\"Provide information on visiting historic sites in Camden, Maine, including one must-see landmark or site\\r\",\"web\":\"\"}\n{\"id\":\"disneyworld.disney.go_find_180\",\"category\":\"things_to_do\",\"ques\":\"Find out the opening hours and ticket prices for Disney's Animal Kingdom Theme Park in Orlando, Florida.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_118\",\"category\":\"things_to_do\",\"ques\":\"book tickets for the next Greater Haitian-American Chamber of Commerce event near tampa, FL\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_206\",\"category\":\"things_to_do\",\"ques\":\"What are the alerts, if any, for the petrified forest loop trail on alltrails.com\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_recommend_9\",\"category\":\"things_to_do\",\"ques\":\"Submit a form to plan a safari trip in johannesburg  on jacadatravel.com for a family of 4 with 2 kids, including a private dinner with a budget of $15000\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_153\",\"category\":\"things_to_do\",\"ques\":\"Buy tickets for the St. Petersburg Pirate Museum in Florida, and inform me of the including visiting hours and total price for 2 adults.\\r\",\"web\":\"\"}\n{\"id\":\"metmuseum_find_24\",\"category\":\"things_to_do\",\"ques\":\"Buy tickets for the Met on the next available day, using 11201 as the zipcode for discounts and pay only the ticket price.\\r\",\"web\":\"\"}\n{\"id\":\"smithsonianmag_question_answering_24\",\"category\":\"things_to_do\",\"ques\":\"Find the oldest Nez Perce site on the Salmon River and then tell me what road I would take to get there from Cottonwood, ID\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_37\",\"category\":\"things_to_do\",\"ques\":\"Find and book a kayaking event in Winter Haven, Florida.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_71\",\"category\":\"things_to_do\",\"ques\":\"what are the next three events happening at miami beach convention center\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_286\",\"category\":\"things_to_do\",\"ques\":\"book tickets for the next dinner show at Pigeon Forge, Tennessee and tell me the price\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_109\",\"category\":\"things_to_do\",\"ques\":\"buy tickets for a sumo wrestling event in tokyo\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_66\",\"category\":\"things_to_do\",\"ques\":\"Book tickets for a murder mystery dinner in Chambersburg, Pennsylvania\\r\",\"web\":\"\"}\n{\"id\":\"sixflags_question_answering_79\",\"category\":\"things_to_do\",\"ques\":\"Find out operating hours and ticket prices for Six Flags New England\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_general_activity_194\",\"category\":\"things_to_do\",\"ques\":\"Plan an airboat tour at Lake Trafford in Florida and check if alligator sightings are guaranteed\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_81\",\"category\":\"things_to_do\",\"ques\":\"tell me when daffodil day at the garden club of virginia is and add it to my calendar if you can\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_250\",\"category\":\"things_to_do\",\"ques\":\"Locate and provide options for ziplining in Bavaria, Germany.\\r\",\"web\":\"\"}\n{\"id\":\"hipcamp_question_answering_4\",\"category\":\"things_to_do\",\"ques\":\"order a nonresident Annual Park Pass from new jersey state park service\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_223\",\"category\":\"things_to_do\",\"ques\":\"Find the best hiking trails in Pendleton, Oregon and include details such as trail length and difficulty\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_102\",\"category\":\"things_to_do\",\"ques\":\"Find a cooking class in Bethesda, Maryland and book a session if available\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_174\",\"category\":\"things_to_do\",\"ques\":\"Find the price and availability for tours of Waverly Hills Sanatorium in Kentucky, and help me book tickets if possible.\\r\",\"web\":\"\"}\n{\"id\":\"disneyworld.disney.go_plan_a_trip_2\",\"category\":\"things_to_do\",\"ques\":\"Plan a visit to Disney World in Orlando, Florida, including ticket options and must-see attractions\\r\",\"web\":\"\"}\n{\"id\":\"sixflags_general_activity_11\",\"category\":\"things_to_do\",\"ques\":\"Check for opening hours and ticket prices for the Wild Safari at Six Flags in New Jersey\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_45\",\"category\":\"things_to_do\",\"ques\":\"what are the upcoming events at pershing square, LA on bandsintown websites\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_recommend_220\",\"category\":\"things_to_do\",\"ques\":\"What free events or activities are happening in Ithaca, New York this weekend?\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_232\",\"category\":\"things_to_do\",\"ques\":\"buy a backcountry permit for Thunder River and Deer Creek trail at the grand canyon, or tell me when I can apply if not available.\\r\",\"web\":\"\"}\n{\"id\":\"metmuseum_question_answering_49\",\"category\":\"things_to_do\",\"ques\":\"What are the current exhibits at the Metropolitan Museum of Art in New York City, New York?\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_95\",\"category\":\"things_to_do\",\"ques\":\"tell me the date and time of the next event at Fort Gibson historic site in Oklahoma, and what to expect at the event.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_1\",\"category\":\"things_to_do\",\"ques\":\"sign up for a family membership for the oklahoma historical society\\r\",\"web\":\"\"}\n{\"id\":\"sixflags_general_activity_16\",\"category\":\"things_to_do\",\"ques\":\"buy a season pass to hurricane harbor in arlington tx and tell me the price\\r\",\"web\":\"\"}\n{\"id\":\"tiqets_tickets_book_4\",\"category\":\"things_to_do\",\"ques\":\"purchase tickets to the Azulejo Tile Museum directly from their website\\r\",\"web\":\"\"}\n{\"id\":\"trailforks_question_answering_3\",\"category\":\"things_to_do\",\"ques\":\"Check the current conditions of the Lake Eiler Trail and report any closures or hazards.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_136\",\"category\":\"things_to_do\",\"ques\":\"buy tickets for the next upcoming Edgar Allan Poe speakeasy event (in whichever city)\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_282\",\"category\":\"things_to_do\",\"ques\":\"Find the top 3 hiking trails in Pike National Forest and provide a table detailing their difficulty level, number of reviews, and length in miles.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_112\",\"category\":\"things_to_do\",\"ques\":\"Book tickets for the underground NYC tour known as 'Empire Beneath the Streets' in New York City, New York\\r\",\"web\":\"\"}\n{\"id\":\"recreation.gov_question_answering_26\",\"category\":\"things_to_do\",\"ques\":\"Find the hours of operation and available activities at Colter Bay Visitor Center in Wyoming.\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_recommend_275\",\"category\":\"things_to_do\",\"ques\":\"Recommend activities or attractions to visit near Yankee Stadium in Bronx, New York before a Yankee game\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_101\",\"category\":\"things_to_do\",\"ques\":\"buy tickets for family of 4 (2 kids) at the denver museum of nature and science\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_question_answering_278\",\"category\":\"things_to_do\",\"ques\":\"which time slot in the next upcoming Saturday has the most availability at the denver museum of nature and science\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_190\",\"category\":\"things_to_do\",\"ques\":\"book a ziplining tour at fox fire adventure park in Sevierville, TN\\r\",\"web\":\"\"}\n{\"id\":\"hipcamp_recommend_5\",\"category\":\"things_to_do\",\"ques\":\"What are the best camping parks in Languedoc-Roussillon, France, and what amenities do they offer?\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_77\",\"category\":\"things_to_do\",\"ques\":\"buy 1 colorado resident and another non-resident ticket to the denver art museum on the next available Tuesday\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_plan_a_trip_118\",\"category\":\"things_to_do\",\"ques\":\"buy tickets a tour of teatro colon and then dinner/tango show in La Ventana, Buenos Aires\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_83\",\"category\":\"things_to_do\",\"ques\":\"buy tickets to the next wine festival anywhere in the US -- I really need more wine\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_plan_a_trip_162\",\"category\":\"things_to_do\",\"ques\":\"Plan a road trip itinerary with interesting places to stop between Glacier National Park and Red Lodge, Montana\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_find_40\",\"category\":\"things_to_do\",\"ques\":\"rsvp to an event involving food at visitlakegeneva.com\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_128\",\"category\":\"things_to_do\",\"ques\":\"buy tickets for the next weekend show at the Barrymore Theatre in Fort Lee, New Jersey\\r\",\"web\":\"\"}\n{\"id\":\"tiqets_tickets_book_9\",\"category\":\"things_to_do\",\"ques\":\"buy next available tickets for La Lonja de la Seda in Valencia, Spain\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_2\",\"category\":\"things_to_do\",\"ques\":\"book tickets for the next event in Grapevine, TX on eventbrite so I can plan my weekend\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_plan_a_trip_226\",\"category\":\"things_to_do\",\"ques\":\"Help me plan a trip with recommendations for hotels, day tours, and attractions in Palawan, Philippines\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_170\",\"category\":\"things_to_do\",\"ques\":\"book tickets to visit the chrysler building observation deck in NYC\\r\",\"web\":\"\"}\n{\"id\":\"tiqets_tickets_book_15\",\"category\":\"things_to_do\",\"ques\":\"book tickets to the Pinacoteca di Brera in Milan, Italy on their official site\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_23\",\"category\":\"things_to_do\",\"ques\":\"Identify the best waterfalls to see while hiking in the Superstition Mountains, Arizona\\r\",\"web\":\"\"}\n{\"id\":\"sixflags_find_48\",\"category\":\"things_to_do\",\"ques\":\"Find the operational hours and entry prices for Sky Harbor Waterpark in Phoenix, Arizona\\r\",\"web\":\"\"}\n{\"id\":\"hipcamp_find_90\",\"category\":\"things_to_do\",\"ques\":\"Locate the available campgrounds near Little Bighorn Battlefield National Monument in Montana and provide details about the amenities they offer.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_57\",\"category\":\"things_to_do\",\"ques\":\"which day in the upcoming month is cheapest to buy admission tickets to chicago botanic garden and what is the price?\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_73\",\"category\":\"things_to_do\",\"ques\":\"register for the next open house at the NY campus of the culinary institute of america\\r\",\"web\":\"\"}\n{\"id\":\"disneyworld.disney.go_question_answering_147\",\"category\":\"things_to_do\",\"ques\":\"when is the next available day to schedule a divequest at sea base aquarium at epcot and what is the price? Then proceed to book.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_67\",\"category\":\"things_to_do\",\"ques\":\"sign up for a guided tour at the Leland Stanford mansion for the next available Saturday\\r\",\"web\":\"\"}\n{\"id\":\"tiqets_tickets_book_5\",\"category\":\"things_to_do\",\"ques\":\"purchase a ticket to visit the The Odeon of Herodes Atticus in Athens, Greece\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_76\",\"category\":\"things_to_do\",\"ques\":\"book tickets to the next event at the African-American Research Library and Cultural Center, Ft lauderdale FL\\r\",\"web\":\"\"}\n{\"id\":\"alltrails_find_237\",\"category\":\"things_to_do\",\"ques\":\"Find the starting point and trail length for hiking Mount Oxford in New Zealand\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_find_41\",\"category\":\"things_to_do\",\"ques\":\"Find 2 museums located in Iowa City, Iowa, and provide the addresses or websites for them.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_tickets_book_74\",\"category\":\"things_to_do\",\"ques\":\"Find and book tickets to a dinner show happening this weekend in Memphis, Tennessee\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_find_279\",\"category\":\"things_to_do\",\"ques\":\"Find upcoming Indian or Hindu festivals taking place in Pittsburgh, Pennsylvania and provide details about the events.\\r\",\"web\":\"\"}\n{\"id\":\"mgmgrand.mgmresorts_1\",\"category\":\"hotels_head\",\"ques\":\"I need to reserve a room at MGM Grand in Las Vegas, Nevada, this weekend at mgmgrand.mgmresorts.com checking in November 27 until December 9. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"kayak_256\",\"category\":\"hotels_head\",\"ques\":\"What's the cheapest room price at Red Roof Inn in St. Louis, Missouri with kayak.com staying from November 23 to December 4? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"caesars_313\",\"category\":\"hotels_head\",\"ques\":\"Can you help me book a stay at Harrah's Cherokee in Cherokee, North Carolina using caesars.com 11/25/2025 - 11/27/2025? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"holidayinnclub_211\",\"category\":\"hotels_head\",\"ques\":\"How many rooms are available at Holiday Inn Club Scottsdale in Scottsdale, Arizona using holidayinnclub.com from December 6 through December 19? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hilton_248\",\"category\":\"hotels_head\",\"ques\":\"I'm looking to get a room at DoubleTree by Hilton Rapid City Downtown Convention Center in Rapid City, South Dakota using hilton.com staying from December 17 to December 30. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"airbnb_437\",\"category\":\"hotels_head\",\"ques\":\"What do the taxes and fees amount to for a stay at Bella's House from Twilight in St. Helens, Oregon through airbnb.com 11/13/2025 - 11/25/2025? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"orbitz_8\",\"category\":\"hotels_head\",\"ques\":\"I'd like to reserve a room at Legoland Hotel in Carlsbad, California using orbitz.com checking in November 19 - November 21. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"resortsandlodges_43\",\"category\":\"hotels_head\",\"ques\":\"Can you help me find a pet-friendly resort in New Jersey for my vacation at resortsandlodges.com from December 18 to January 1? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hotels_131\",\"category\":\"hotels_head\",\"ques\":\"What's the price for the cheapest hotel in Edisto Beach, South Carolina at hotels.com 12/18/2025 - 12/28/2025? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"uniquehotels.me_13\",\"category\":\"hotels_head\",\"ques\":\"I'm trying to book a unique accommodation in Havelock North, New Zealand through uniquehotels.me from 11/17/2025 → 11/19/2025. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_347\",\"category\":\"hotels_head\",\"ques\":\"How many hotels are available near the Grand Canyon in Las Vegas, Nevada through tripadvisor.com February 3 checking out February 8? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"choicehotels_52\",\"category\":\"hotels_head\",\"ques\":\"I need to get a room at Clarion Inn in Idaho Falls, Idaho with choicehotels.com from January 18 through January 31. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"choicehotels_25\",\"category\":\"hotels_head\",\"ques\":\"What are the total taxes and fees for a room at Radisson Resort in Miami Beach, Florida at choicehotels.com January 8 checking out January 13? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"motel6_76\",\"category\":\"hotels_head\",\"ques\":\"Can you book me a room at Motel 6 in Lenexa, Kansas with motel6.com November 26 - November 30? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hostelworld_27\",\"category\":\"hotels_head\",\"ques\":\"I'm looking for a cheap hostel in Mykonos, Greece through hostelworld.com checking in on November 24 and leaving December 6. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"holidayinnclub_103\",\"category\":\"hotels_head\",\"ques\":\"Help me reserve a room at Orange Lake Resort by Holiday Inn in Kissimmee, Florida with holidayinnclub.com from December 11 to December 15. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hilton_150\",\"category\":\"hotels_head\",\"ques\":\"What's the cheapest available room at Hampton Inn and Suites Albany in Albany, Georgia at hilton.com from 12/10/2025 → 12/15/2025? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"agoda_121\",\"category\":\"hotels_head\",\"ques\":\"I want to book a room at SO Sofitel Hua Hin in Hua Hin, Cha-Am, Thailand on Agoda using agoda.com checking in on December 18 and leaving December 23. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"travelocity_36\",\"category\":\"hotels_head\",\"ques\":\"How many rooms are still available in Lauderdale-by-the-Sea, Florida using travelocity.com February 4 checking out February 11? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"oyster_72\",\"category\":\"hotels_head\",\"ques\":\"I'd like to get a 2-bedroom suite at Ocean Lodge in St. Simons Island using oyster.com checking in January 4 - January 15. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"vrbo_282\",\"category\":\"hotels_head\",\"ques\":\"What do the total fees and taxes come to for Harbor House in Treasure Island, Florida through vrbo.com from December 14 to December 16? If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"motel6_83\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Motel 6 in Shartlesville, Pennsylvania through motel6.com December 12 checking out December 16.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ihg_11\",\"category\":\"hotels_head\",\"ques\":\"Book a hotel in Green River, Utah at ihg.com January 5 checking out January 17.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"marriott_20\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Gaylord Opryland Resort and Convention Center in Nashville, Tennessee with marriott.com from 01/13/2025 → 01/15/2025.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"bestwestern_370\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Best Western Wapakoneta Inn in Wapakoneta, Ohio using bestwestern.com staying from December 18 to December 22.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"bluegreenvacations_23\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Bluegreen at Tradewinds in Florida with bluegreenvacations.com from December 3 through December 5.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"marriott_490\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Courtyard by Marriott Anchorage Airport in Anchorage, Alaska at marriott.com checking in on January 25 and leaving January 31.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hyattinclusivecollection_265\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Dreams Onyx Resort & Spa - All Inclusive in the Dominican Republic with hyattinclusivecollection.com checking in December 16, checking out December 27.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"premierinn_26\",\"category\":\"hotels_head\",\"ques\":\"Book a Premier Inn hotel Edinburgh City Centre in Scotland using premierinn.com checking in December 3, checking out December 8.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"planethollywoodhotels_25\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Planet Hollywood Cancun Resort with Star Class in Cancun, Mexico at planethollywoodhotels.com from December 19 through December 24.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"motel6_32\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Motel 6 in Branford, Connecticut using motel6.com staying from November 25 to November 29.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"druryhotels_224\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Drury Inn and Suites Columbus Polaris in Columbus, Ohio at druryhotels.com from February 9 through February 22.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hyatt_305\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Hyatt Regency Hotel at Orlando International Airport in Orlando, Florida through hyatt.com from 12/06/2025 → 12/19/2025.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"bestwestern_409\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Best Western Plus Capitola By-the-Sea Inn & Suites in Capitola, California using bestwestern.com checking in on January 23 and leaving January 25.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"airbnb_192\",\"category\":\"hotels_head\",\"ques\":\"Book a place to stay in Plainfield Township, Michigan with airbnb.com checking in December 12 until December 16.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hyatt_115\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Hyatt Vacation Club at the Ranahan in Colorado with hyatt.com checking in January 15 - January 25.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"motel6_59\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Motel 6 in Harrisburg, Pennsylvania with motel6.com checking in December 4, checking out December 16.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hiltongrandvacations_128\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Hilton Grand Vacations in South Lake Tahoe, California through hiltongrandvacations.com arriving 11/20/2025 to 11/25/2025.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ihg_236\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Holiday Inn in Toronto, Ontario, Canada at ihg.com checking in on February 14 and leaving February 16.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_280\",\"category\":\"hotels_head\",\"ques\":\"Book a hotel in Concord, New Hampshire using tripadvisor.com checking in November 19 - November 27.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hilton_312\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Homewood Suites in Wallingford, Connecticut with hilton.com checking in January 9 - January 13.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"marriott-hotels.marriott_9\",\"category\":\"hotels_head\",\"ques\":\"Book a Marriott hotel with a lounge in Orlando, Florida at marriott-hotels.marriott.com November 19 checking out November 29.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"sandals_14\",\"category\":\"hotels_head\",\"ques\":\"Book an all-inclusive stay at Sandals Turks and Caicos through sandals.com staying from Jan 27 to Feb 4.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"kempinski_30\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Kempinski Budapest Hotel in Budapest, Hungary at kempinski.com from November 29 through December 6.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"caesars_162\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Harrah's Lake Tahoe in Lake Tahoe, Nevada through caesars.com February 6 checking out on the 13.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"bestwestern_354\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Best Western Venice Mestre Hotel in Mestre, Italy through bestwestern.com checking in January 17, checking out January 30.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"airbnb_161\",\"category\":\"hotels_head\",\"ques\":\"Book a bed and breakfast in Leadville, Colorado using airbnb.com January 4 - January 15.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"bluegreenvacations_66\",\"category\":\"hotels_head\",\"ques\":\"Book a stay at Bluegreen Odyssey Dells in Wisconsin Dells, Wisconsin through bluegreenvacations.com checking in February 11 until February 22.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"bestwestern_467\",\"category\":\"hotels_head\",\"ques\":\"Book a room at SureStay by Best Western Glendive Yellowstone River in Glendive, Montana with bestwestern.com from November 22 to November 27.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"holidayinnclub_277\",\"category\":\"hotels_head\",\"ques\":\"Book a stay at Holiday Inn Vacation Club Orange Lake Resort in Orlando, Florida using holidayinnclub.com December 12 checking out December 18.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hilton_262\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Home2 Suites by Hilton in St. Louis, Missouri using hilton.com December 13 - December 20.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hyatt_335\",\"category\":\"hotels_head\",\"ques\":\"Book a room at Hyatt Place Pasadena in California at hyatt.com checking in December 22, checking out December 27.. If the hotel doesn't take reservations for that date or there are no available rooms for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"gardenofeatn_1\",\"category\":\"restaurants_tail\",\"ques\":\"Find some vegan options at Garden of Eatin in Sacramento, CA.\\r\",\"web\":\"\"}\n{\"id\":\"eatleven_2\",\"category\":\"restaurants_tail\",\"ques\":\"Find me a deli in Downtown Denver and its most meat-filled option at the deli.\\r\",\"web\":\"\"}\n{\"id\":\"thekafeneo_1\",\"category\":\"restaurants_tail\",\"ques\":\"Find a vegetarian item on the menu for Kafe Neo in Bainbridge\\r\",\"web\":\"\"}\n{\"id\":\"indytoday.6amcity_8\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Yazsh Cafe and Bistro in Indianapolis on Thursday for brunch time.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"antioch.eatatanastasias_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation for two at Anastasia Restaurant in Antioch on November 20 at 11:15 AM.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"queensyardnyc_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Rose Room in New York at 10 PM. If it doesn't take reservations or is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ronskenosha_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Ron's Place in Kenosha for the soonest available time.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"portofinoutica_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a brunch reservationfor three at 11 AM on the upcoming Sunday for Mother's Day at Portofino in Utica, NY. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ophchicagoland_2\",\"category\":\"restaurants_tail\",\"ques\":\"What are some famous pancakes on the menu at The Original Pancake House in Hyde Park.\\r\",\"web\":\"\"}\n{\"id\":\"firebowlcafe_1\",\"category\":\"restaurants_tail\",\"ques\":\"What are the cheapest rice/noodle dishes featuring meat at Fire Bowl Cafe in McKinney, TX?\\r\",\"web\":\"\"}\n{\"id\":\"theshopsatcolumbuscircle_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at a restaurant in Time Warner Center at 7 pm on 11/30/25. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"gillhouseny_2\",\"category\":\"restaurants_tail\",\"ques\":\"What specials do they have featured at Gill House in Henderson Harbor, NY.\\r\",\"web\":\"\"}\n{\"id\":\"greatwoksecaucus_1\",\"category\":\"restaurants_tail\",\"ques\":\"Do they have any spicy beef or chicken dishes available for takeout at Great Wok in Secaucus, NJ\\r\",\"web\":\"\"}\n{\"id\":\"mauihawaii_3\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at a restaurant in Lahaina, Maui for the earliest available reservation this week.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"brunchpubcenterville_2\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at The Brunch Pub in Centerville for the upcoming Friday at 7 pm. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"aubergeresorts_8\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at The Conservatory Restaurant in Newport for Novemeber 26 at 11:15 AM.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"reysolcoffee_1\",\"category\":\"restaurants_tail\",\"ques\":\"What is the most expensive dish on the menu for Rey Sol Coffee in Morristown, NJ\\r\",\"web\":\"\"}\n{\"id\":\"duffystavernlg_1\",\"category\":\"restaurants_tail\",\"ques\":\"What kinda chicken wings and drinks they got at Duffy's Tavern in Lake George.\\r\",\"web\":\"\"}\n{\"id\":\"restaurantsinsarasota_9\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Gen Korean restaurant in UTC Mall, Sarasota, FL for Tuesday at 6:30 PM. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"tallahasseetimes_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation with outdoor setaing at a 347 Grille in Tallahassee, FL any day over the next three weeknds between 5:30 and 8 pm. Let them know that I have peanut allergies too. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"ritual.co_4\",\"category\":\"restaurants_tail\",\"ques\":\"What is the most popular dish on the menu for Java Java Coffee on Fleet Street, London\\r\",\"web\":\"\"}\n{\"id\":\"brennanssportsbar_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Brennan's Sports Bar in the Phoenix area on December 2 for the next free slot. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"thecapitalburger_3\",\"category\":\"restaurants_tail\",\"ques\":\"Find a vegetarian item on the menu and prices for The Capital Burger in Washington, DC\\r\",\"web\":\"\"}\n{\"id\":\"carinos_2\",\"category\":\"restaurants_tail\",\"ques\":\"List some types of lasagna featured at Johnny Carino's in Downey, CA during lunchtime.\\r\",\"web\":\"\"}\n{\"id\":\"gazette_5\",\"category\":\"restaurants_tail\",\"ques\":\"What chicken dishes are available at  Masala Mingle Indian Bistro and Bar in Colorado Springs\\r\",\"web\":\"\"}\n{\"id\":\"bestnewyork.us_5\",\"category\":\"restaurants_tail\",\"ques\":\"In the upcoming Friday or Saturday, book a reservation for four people at Buffet House in Queens, NY.\\r\",\"web\":\"\"}\n{\"id\":\"mounthorebchamber_1\",\"category\":\"restaurants_tail\",\"ques\":\"Make a reservation for four people at Campo Di Bella in Mt Horeb, WI on Nov. 22. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer.\\r\",\"web\":\"\"}\n{\"id\":\"mallsinamerica_7\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at SkyDome restaurant for two in Pentagon Row for Novemeber 22nd at 6:00 PM.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"utcsarasota_6\",\"category\":\"restaurants_tail\",\"ques\":\"Make a reservation at Isan Thai Restaurant in Sarastoa, FL for a party of 3 at at around 6 PM.  If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"rockawave_1\",\"category\":\"restaurants_tail\",\"ques\":\"What are some special drinks or cuisine found at Fitzgerald's Bar in Rockaway, NY ?\\r\",\"web\":\"\"}\n{\"id\":\"sloansrestaurant_1\",\"category\":\"restaurants_tail\",\"ques\":\"What are some common American breakfast foods found at Sloan's Restaurant in Indio during its breakfast/lunch time?\\r\",\"web\":\"\"}\n{\"id\":\"mainkitchenma_1\",\"category\":\"restaurants_tail\",\"ques\":\"Are there any duck dishes served at Peking House on Carew St in Springfield, MA.\\r\",\"web\":\"\"}\n{\"id\":\"longshots-bar_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation for 6 people at Longshots Bar and Grill in Fairmount Park, IL for Saturday, Novebmer 22 at 7:00 PM.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"wearetravelgirls_3\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation for a party of 12 at Magnolias in Charleston, SC for a bachelorette party on 12/12/2025 at 8 PM. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"local.starmarket_1\",\"category\":\"restaurants_tail\",\"ques\":\"Order two birthday cakes from Star Market Bakery in Quincy, MA. The budget is capped at $100. If there are no two birthday cakes that exceed $100, do not order a cake.\\r\",\"web\":\"\"}\n{\"id\":\"grilledcheeseandcrabcakeco_1\",\"category\":\"restaurants_tail\",\"ques\":\"Find a vegetarian item on the menu for The Grilled Cheese and Crab Cake Company in Cocoa Beach\\r\",\"web\":\"\"}\n{\"id\":\"epicureantravelerblog_2\",\"category\":\"restaurants_tail\",\"ques\":\"Is Marro's Italian Restaurant in Saugatuck, MI a romantic restaurant? If so, book a reservation for two on November 18 at 7:00 PM.  If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"gulelerestaurant_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Gulele Restaurant in Gaithersburg, MD on the upcoming Sunday for weekend brunch at 11:00 AM. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"sinners.co_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Sinners Restaurant in Bloomington for lunchtime on 12/19.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"nothingbundtcakes_6\",\"category\":\"restaurants_tail\",\"ques\":\"Order a cake from Nothing Bundt Cakes in Lincoln, NE.\\r\",\"web\":\"\"}\n{\"id\":\"sawasdeethaicuisine-asheville_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Sawasdee Thai in Asheville, NC on November 21 at 1:00 PM. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"mammamaria_3\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Mamma Maria in the North End, Boston for the upcoming Monday dinnretime.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"foodieflashpacker_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at one of the best restaurants in Laramie, WY for an early dinner at around 5 PM on 11/20/2025. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"skny.io_2\",\"category\":\"restaurants_tail\",\"ques\":\"Book a private room for 20 people at Dead Rabbit Grocery and Grog in New York  City on 12/18/25. If there are no bookings availble for a party of such size, please indicate that in your answer.\\r\",\"web\":\"\"}\n{\"id\":\"restaurants_6\",\"category\":\"restaurants_tail\",\"ques\":\"Find soul food hidden gem restaurants in Towaco, New Jersey that are open during lunchtime on 11/21/2025.\\r\",\"web\":\"\"}\n{\"id\":\"theplacearizona_1\",\"category\":\"restaurants_tail\",\"ques\":\"What are some specialty cocktails featured at The Place Restaurant in Arizona.\\r\",\"web\":\"\"}\n{\"id\":\"uptown-pizza2.website.spoton_1\",\"category\":\"restaurants_tail\",\"ques\":\"List all healthy options available at Uptown Pizza in Tomah, WI. Then, put together an order that would satiate a party of 4.\\r\",\"web\":\"\"}\n{\"id\":\"birchsonthelake_1\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation for a party of two at a restaurant along a body of water in Long Lake, WI on November 19 at 7:00 PM. Let the staff know that this is a date. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"refugeinthewoodlands_3\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation at Refuge Restaurant in The Woodlands for a party of four on 12/02/2025 for 9:-0 PM.. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"weatherfordbar_1\",\"category\":\"restaurants_tail\",\"ques\":\"Can you help me book a reservation for a party of 5 at Fire Oak Grill in Weatherford, TX on November 22 for the first available table of that day. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"hamadaya-bakery_1\",\"category\":\"restaurants_tail\",\"ques\":\"Looking at Hamadaya Bakery in Irvine, compile an order featuring cakes, pastries, and sandwiches to feed a family of three for a meal.\\r\",\"web\":\"\"}\n{\"id\":\"valerienewyorkcity_2\",\"category\":\"restaurants_tail\",\"ques\":\"Book a reservation for the next available Sunday brunch at Valerie's in NYC. If the restaurant doesn't take reservations or it is unavailable for that time, please indicate that in your answer\\r\",\"web\":\"\"}\n{\"id\":\"kelty_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a 65-liter capacity internal frame backpack from Kelty and a rain cover to protect it\\r\",\"web\":\"\"}\n{\"id\":\"kancanusa_3\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase mid-rise denim bermuda shorts, size 26, from KancanUSA and a blue top, size M, to go with them.\\r\",\"web\":\"\"}\n{\"id\":\"goat_7\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Reebok pump sneakers for men in size 10 from Goat and athletic socks to pair with the sneakers, doesn't matter the color.\\r\",\"web\":\"\"}\n{\"id\":\"medline_14\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase replacement wheels for the Guardian K3 wheelchair from Medline and a tire repair kit for the wheelchair wheels.\\r\",\"web\":\"\"}\n{\"id\":\"irishsetterboots_3\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Irish Setter Kasota 6-inch work boots in size 9.5 regular width from irishsetterboots.com, and a pair of brown chukka boots in the same size.\\r\",\"web\":\"\"}\n{\"id\":\"agwheelexpress_5\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 14 x 38 double bevel rims in JD yellow from AgWheelExpress, and include a mount hub as well.\\r\",\"web\":\"\"}\n{\"id\":\"birkenstock_11\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Birkenstocks Arizona style in black for women from Birkenstock's website and a shoe care kit to keep them in good condition\\r\",\"web\":\"\"}\n{\"id\":\"acrylux_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Add semi-gloss Acrylux Exterior Paint to my cart Acrylux.com and also add brushes or rollers for painting to my cart on Amazon.\\r\",\"web\":\"\"}\n{\"id\":\"colgate_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 5000 ppm fluoride toothpaste in regular mint flavor from Colgate and a soft bristle toothbrush to use with it.\\r\",\"web\":\"\"}\n{\"id\":\"tcl_11\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a 27-inch monitor from TCL.com and a pair of headphones.\\r\",\"web\":\"\"}\n{\"id\":\"shop.rolltide_3\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase an Alabama vintage t-shirt from the official Alabama Crimson Tide shop and a matching Alabama Crimson Tide cap.\\r\",\"web\":\"\"}\n{\"id\":\"americanstandard-us_23\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase American Standard 19-inch high toilet in white from American Standard's official website and a electric bidet seat to go with it.\\r\",\"web\":\"\"}\n{\"id\":\"ronellclock_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 8-inch extra fancy large clock hands from Ronell Clock and a brass brush to help keep it clean\\r\",\"web\":\"\"}\n{\"id\":\"vevor_23\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Vevor food process that is at least 10Quarts from Vevor.com and 7.5in meat slicer.\\r\",\"web\":\"\"}\n{\"id\":\"oceanstatejoblot_4\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a 9'x12' rectangular indoor/outdoor rug from Ocean State Job Lot and a 18in by 30in kitchen mat.\\r\",\"web\":\"\"}\n{\"id\":\"golfpride_7\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Golf Pride tour classic putter grip from Golf Pride and a grip tape to install the putter grip.\\r\",\"web\":\"\"}\n{\"id\":\"craftsman_9\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Craftsman 6-gallon portable air compressor from Craftsman.com and a 16 gauge nailer.\\r\",\"web\":\"\"}\n{\"id\":\"m2motorsportinc_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 22-inch IROC wheels from M2 Motorsport Inc., along with lug nuts suitable for the wheels.\\r\",\"web\":\"\"}\n{\"id\":\"catholicshop_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a cheap wood rosary from Catholic Shop along with a rosary holder.\\r\",\"web\":\"\"}\n{\"id\":\"beatsbydre_5\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase studio headphones from Beats by Dre and an extra usb-c charging cable for them.\\r\",\"web\":\"\"}\n{\"id\":\"tagwoodbbq_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a medium-sized Argentinian charcoal grill from Tagwood BBQ and a cover to go with it.\\r\",\"web\":\"\"}\n{\"id\":\"spreadshirt_3\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a black classic rock sweatshirt from Spreadshirt and a hat to go with it.\\r\",\"web\":\"\"}\n{\"id\":\"extremerate_3\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 3rd party Switch Joy-Con shells in black or blue from ExtremeRate and a screen protector for my Switch.\\r\",\"web\":\"\"}\n{\"id\":\"surfboards_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a 9ft longboard surfboard in white, black, blue or green from Surfboards.com and a surfboard leash for it.\\r\",\"web\":\"\"}\n{\"id\":\"tomsstudio_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a Fountain Pen in any color from Tom's Studio along with a bottle of fountain pen ink for refills.\\r\",\"web\":\"\"}\n{\"id\":\"bacteriostaticwater_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a 30 mL vial of bacteriostatic water for injection from BacteriostaticWater.com, along with sterile syringes or needles for use with it.\\r\",\"web\":\"\"}\n{\"id\":\"fiestafactorydirect_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a 12 piece mixed dinnerware set and blue (or green) luncheon plate.\\r\",\"web\":\"\"}\n{\"id\":\"mcfeelys_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a pack (less than 100) 1/4-20 T-nuts from McFeely's and also a pack of 1/4-20 softwood threaded inserts.\\r\",\"web\":\"\"}\n{\"id\":\"housebeautiful_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase an outdoor smoker online and some wood chips to use with it.\\r\",\"web\":\"\"}\n{\"id\":\"whitemountainshoes_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase heeled sandals for women as well as some winter boots, size 8, from WhiteMountainShoes.com\\r\",\"web\":\"\"}\n{\"id\":\"eyeglasses_16\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Swarovski SK1011 frames in black from Eyeglasses.com and then a pair of Guess sunglasses to go with them\\r\",\"web\":\"\"}\n{\"id\":\"frandenim_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase size 30 athletic cut jeans for women from Fran Denim and then another pair of medium wash straight cut jeans.\\r\",\"web\":\"\"}\n{\"id\":\"recwatches_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Preorder a DNA edition Lotus 98T-4 watch and a 24mm strap for it from REC Watches\\r\",\"web\":\"\"}\n{\"id\":\"awaytravel_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Away \\\"carry-on\\\" and \\\"The bigger carry on\\\"  luggages from AwayTravel.com\\r\",\"web\":\"\"}\n{\"id\":\"replacementkeys_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a replacement 703 Yale lock key from EasyKeys and a graphite lubricant for the lock\\r\",\"web\":\"\"}\n{\"id\":\"skipsgarage_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a professional regulation-size wooden cornhole set from Skip's Garage and cornhole bags to go with it.\\r\",\"web\":\"\"}\n{\"id\":\"gymshark_12\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Gymshark Arrival 7\\\" shorts in navy, size medium, from Gymshark, and a matching regular fit Arrival t-shirt.\\r\",\"web\":\"\"}\n{\"id\":\"computers.microsoft_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a black Surface Pro 13 tablet with snapdragon X Elite processor and 16GB RAM with a matching keyboard on the official Microsoft store\\r\",\"web\":\"\"}\n{\"id\":\"walgreens_10\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a heated foot spa from Walgreens, and Epsom salt to enhance the foot spa experience\\r\",\"web\":\"\"}\n{\"id\":\"vogue-eyewear_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a pair of pink cat eye sunglasses and a pair of black metal framed sunglasses from Vogue Eyewear\\r\",\"web\":\"\"}\n{\"id\":\"simpletire_5\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 4 BFGoodrich 35x10R17 Jeep tires and another 4 Continental ExtremeContact DW tires SimpleTire\\r\",\"web\":\"\"}\n{\"id\":\"picktrampoline_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 8.5 inch 14ft trampoline replacement springs (pack of 84) from Trampoline Parts And Supply and a heavy duty safety pad cover.\\r\",\"web\":\"\"}\n{\"id\":\"uniqlo_8\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a men's jacket in size Medium and a matching pair of gloves from Uniqlo.\\r\",\"web\":\"\"}\n{\"id\":\"rvusa_11\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase the new Aliner 2025 Evolution from RVUSA, and also buy a towing cover for the RV.\\r\",\"web\":\"\"}\n{\"id\":\"frederickbuechner_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase 'Wishful Thinking: A Seeker's ABC' by Frederick Buechner (1993) and \\\"Godric: A Novel\\\" from Amazon\\r\",\"web\":\"\"}\n{\"id\":\"saraschildrensbtq_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase boys' size 10 communion suit and a matching tie from Sara's Children's Boutique in Jamison, PA.\\r\",\"web\":\"\"}\n{\"id\":\"everythingarcticcatoffroad_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase any appropriate black Arctic Cat Prowler Pro side mirrors and review mirros from Everything Arctic Cat Off-Road.\\r\",\"web\":\"\"}\n{\"id\":\"polaroid_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase a Polaroid Now Gen 3 Memories Set from Polaroid's website and extra Color I-type film to go with it.\\r\",\"web\":\"\"}\n{\"id\":\"birdbgone_1\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase silicone adhesive and a dripless caulking gun to apply it from Bird BGone.\\r\",\"web\":\"\"}\n{\"id\":\"vintagesingerparts_2\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase Singer Sewhandy Model 50 machine needles, Size 14, from Vintage Singer Parts, and extra bobbins for the sewing machine.\\r\",\"web\":\"\"}\n{\"id\":\"landsend_23\",\"category\":\"shopping_lists_tail\",\"ques\":\"Purchase men's knit nightshirt in size Large and a pair of slippers to complement it, both from Lands' End.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_comparison_shopping_41\",\"category\":\"price_comparison\",\"ques\":\"help me compare the price of the red George Foreman Indoor/Outdoor Electric Grill that can make 12 servings at both walmart and target. Make sure to check the actual product pages; which one is cheaper?\\r\",\"web\":\"\"}\n{\"id\":\"samsclub_comparison_shopping_2\",\"category\":\"price_comparison\",\"ques\":\"help me compare the price of the yellow/navy women's adidas Originals Samba sneaker at both amazon and foot locker. Output a table of the price of each after you check their respective product pages.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_comparison_shopping_297\",\"category\":\"price_comparison\",\"ques\":\"can you compare the price and dimensions of outdoor drop box mailboxes on uline and home depot? Which one is bigger and which one is cheaper?\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_421\",\"category\":\"price_comparison\",\"ques\":\"what standard length of vinyl outside corner trim does homedepot sell vs Southeastern Building Products, and what is the price per unit they sell? Make sure to confirm the product details on the webpages.\\r\",\"web\":\"\"}\n{\"id\":\"napaonline_comparison_shopping_8\",\"category\":\"price_comparison\",\"ques\":\"help me compare coil spring boosters/spacers (front) from rock auto and napa. What are the part numbers and prices from each website?\\r\",\"web\":\"\"}\n{\"id\":\"lowes_comparison_shopping_216\",\"category\":\"price_comparison\",\"ques\":\"I want to know where to buy a 3-arm wall-mounted pivoting Towel Bar between homedepot and wayfair. Figure out which one is cheaper and which one has more reviews by visiting the product pages.\\r\",\"web\":\"\"}\n{\"id\":\"lowes_comparison_shopping_231\",\"category\":\"price_comparison\",\"ques\":\"please help compare the price of the CRAFTSMAN Cmmt45305 mechanic tool set at both walmart and acmetools, which is cheaper and how many pieces are in the set?\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_154\",\"category\":\"price_comparison\",\"ques\":\"can you look up the prices of the 40v Kobalt Cordless 15-inch String trimmer on both amazon and walmart (it's blue) and tell me which one is cheaper and how much a 2-year warranty add-on would be for each?\\r\",\"web\":\"\"}\n{\"id\":\"kohls_comparison_shopping_1\",\"category\":\"price_comparison\",\"ques\":\"can you compare the IZOD Men's Golf Swing Flex Cargo Short on kohls and amazon and tell me the price and level of sun protection they offer for each?\\r\",\"web\":\"\"}\n{\"id\":\"autozone_comparison_shopping_61\",\"category\":\"price_comparison\",\"ques\":\"compare the price of a replacement 2016 Hyundai Genesis Grille from carparts.com and amazon. What is the price and Partslinks number from each websites?\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_90\",\"category\":\"price_comparison\",\"ques\":\"Can you compare the pricing and package sizes for the Rockshark 36V e-bike battery charger between eBay and Amazon? Please check the actual product pages to confirm prices and package details.\\r\",\"web\":\"\"}\n{\"id\":\"basspro_comparison_shopping_2\",\"category\":\"price_comparison\",\"ques\":\"Compare the pricing and package sizes for dog beds between Bass Pro Shops and Chewy to find the best value—make sure to check the actual product pages for each bed’s price and dimensions.\\r\",\"web\":\"\"}\n{\"id\":\"aliexpress_comparison_shopping_11\",\"category\":\"price_comparison\",\"ques\":\"can you compare the price and length of a dual 8Pin-to-16Pin Graphics Card Power Adapter Cable (it is a Y-shaped cord) on both ebay and newegg.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_comparison_shopping_98\",\"category\":\"price_comparison\",\"ques\":\"I want you to compare the price of Regis Rossi's \\\"Intelligence émotionnelle\\\" book between Amazon and Apple books?\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_440\",\"category\":\"price_comparison\",\"ques\":\"Help me compare the price of the Direct Drive wireless keypad garage door opener at Home Depot and Amazon\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_482\",\"category\":\"price_comparison\",\"ques\":\"Can you help me compare the features and specifications of Terro Indoor Liquid Ant Killer Baits at both home depot and uline, what the price and number of baits per box sold at each?\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_454\",\"category\":\"price_comparison\",\"ques\":\"what is the price of a dozen Vital Farms Pasture Raised Eggs at Whole Foods and Walmart?\\r\",\"web\":\"\"}\n{\"id\":\"wholefoodsmarket_comparison_shopping_7\",\"category\":\"price_comparison\",\"ques\":\"what is the price of a dozen Vital Farms Pasture Raised Eggs at Target and Giant?\\r\",\"web\":\"\"}\n{\"id\":\"dickssportinggoods_comparison_shopping_6\",\"category\":\"price_comparison\",\"ques\":\"Compare the prices of boys' black swim trunks between Dick's Sporting Goods and Amazon by checking the actual product pages for shipping costs and estimated delivery windows.\\r\",\"web\":\"\"}\n{\"id\":\"bestbuy_comparison_shopping_74\",\"category\":\"price_comparison\",\"ques\":\"Help me compare the price of the iBUYPOWER Scale gaming desktop PC (Intel Core i5-14400F, NVIDIA GeForce RTX 4060, 16GB DDR5, 1TB NVMe) at Best Buy and Walmart to determine which is cheaper. Make sure to check the actual product pages to confirm current pricing.\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_13\",\"category\":\"price_comparison\",\"ques\":\"Does Home Depot or Amazon offer more color options for the Samsung 27-inch laundry pedestal storage drawer? What are the color options available from each retailer? Make sure to check the actual product pages to confirm available finishes.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_comparison_shopping_77\",\"category\":\"price_comparison\",\"ques\":\"Can you help me compare the price and dimensions of the NECA Dungeons & Dragons Ultimate Strongheart action figure available at Target  vs Walmart formatted as a table? Make sure to check the actual product pages to confirm details.\\r\",\"web\":\"\"}\n{\"id\":\"bestbuy_comparison_shopping_45\",\"category\":\"price_comparison\",\"ques\":\"I would like you to compare the price of Xbox Series X black console at Best Buy vs Microsoft's websites, format your output as a table including the url, retailer, and price.\\r\",\"web\":\"\"}\n{\"id\":\"heb_comparison_shopping_1\",\"category\":\"price_comparison\",\"ques\":\"Compare the price and brands for  cherry flavored night time cold & flu relief liquid between H-E-B and Amazon by checking the actual product pages. Specifically, output a table of the product name, price, and price per ounce for each.\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_230\",\"category\":\"price_comparison\",\"ques\":\"which store sells the Nitecore EDC31 Compact Tactical EDC Flashlight for less -- Amazon or walmart?\\r\",\"web\":\"\"}\n{\"id\":\"lowes_comparison_shopping_227\",\"category\":\"price_comparison\",\"ques\":\"which retailer sells the marey 2.0 GPM Electric Tankless Water Heater for less homedepot or lowes?\\r\",\"web\":\"\"}\n{\"id\":\"samsclub_comparison_shopping_16\",\"category\":\"price_comparison\",\"ques\":\"Help me compare the price of ribeye steak at target and walmart, noting how many steaks per tray.\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_450\",\"category\":\"price_comparison\",\"ques\":\"Help me compare the price of Super Mario 3D All-Stars for Nintendo Switch at eBay and Amazon, which is cheaper? Make sure to check the actual product pages to confirm the price.\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_270\",\"category\":\"price_comparison\",\"ques\":\"Compare the shipping options and delivery times for a Pro Lift  lawn mower jack between Walmart and Amazon. Make sure to check the actual product pages for available shipping methods and estimated delivery windows.\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_245\",\"category\":\"price_comparison\",\"ques\":\"Compare options and prices for buying sports whistles between Walmart and Amazon, checking the actual product pages to confirm details.\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_22\",\"category\":\"price_comparison\",\"ques\":\"Compare the bulk pricing and package sizes for top soil between Walmart and Home Depot to find the best value per unit. Please check the actual product pages to confirm package weights and prices.\\r\",\"web\":\"\"}\n{\"id\":\"nordstrom_comparison_shopping_46\",\"category\":\"price_comparison\",\"ques\":\"Compare the pricing for women's navy blazers between Nordstrom and Macy's to find which retailer offers the best value—make sure to check the actual product pages for current prices and size availability.\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_375\",\"category\":\"price_comparison\",\"ques\":\"Can you help me compare the price and dimensions of kids bumper cars at Walmart vs Amazon formatted as a table? Please check the actual product pages to confirm each spec.\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_424\",\"category\":\"price_comparison\",\"ques\":\"compare the price of the Dyson V11 cordless vacuum from their official website vs bestbuy, how much are the monthly payments with each of their suggested buy now, pay later options?\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_58\",\"category\":\"price_comparison\",\"ques\":\"how much more is the The Enforcer Blue-ray than the DVD on amazon? How much is the DVD at BestBuy?\\r\",\"web\":\"\"}\n{\"id\":\"target_comparison_shopping_112\",\"category\":\"price_comparison\",\"ques\":\"how much is a 6 pack of white undershirts at target vs at walmart?\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_18\",\"category\":\"price_comparison\",\"ques\":\"how many different options of 3-way coaxial cable splitters does HomeDepot sell and what is the difference between the cheapest and most expensive option\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_220\",\"category\":\"price_comparison\",\"ques\":\"Help me compare the price of Food For Life Baking Co. Organic Ezekiel 4:9 Sprouted Whole Grain Cereal (16 oz) at Walmart and Amazon to determine which is more cost-effective. Please check the actual product pages to confirm the prices.\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_436\",\"category\":\"price_comparison\",\"ques\":\"How much more is the Elephant Terry 33 cm than the Miffy ECO Tiny Teddy - 23 cm on bontontoys.com\\r\",\"web\":\"\"}\n{\"id\":\"sephora_comparison_shopping_8\",\"category\":\"price_comparison\",\"ques\":\"how much is Giorgio Men's Acqua di Giò Eau de Toilette Spray, 1.6 oz at Macy's vs at Sephora?\\r\",\"web\":\"\"}\n{\"id\":\"dickssportinggoods_comparison_shopping_28\",\"category\":\"price_comparison\",\"ques\":\"I’m thinking of getting my son a Justin Jefferson jersey for his birthday, how much more is a small on the vikings' official website than on Dick's sporting goods?\\r\",\"web\":\"\"}\n{\"id\":\"ulta_comparison_shopping_4\",\"category\":\"price_comparison\",\"ques\":\"Look at the price and number of reviews of Ouai Hair and Body Mist Travel size on their official site vs on Ulta, and output a table with the price, retailer, and number of reviews.\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_285\",\"category\":\"price_comparison\",\"ques\":\"what are the all the different colors men's 7\\\" sweat shorts are available in on Old Navy, and is that less or more than the equivalent product on Target's website?\\r\",\"web\":\"\"}\n{\"id\":\"rockauto_comparison_shopping_4\",\"category\":\"price_comparison\",\"ques\":\"what is the MSRP for a GM Genuine 84440529 Side Object Sensor Module on gmparts.com, and how much more is that than on gmpartscenter.net\\r\",\"web\":\"\"}\n{\"id\":\"dickssportinggoods_comparison_shopping_40\",\"category\":\"price_comparison\",\"ques\":\"find three different online retailers that sell GM part number 84440529 and list their prices from lowest to highest\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_147\",\"category\":\"price_comparison\",\"ques\":\"Help me compare the price of the FRAM CV10134 TrueAir Premium cabin air filter for a 2012 Honda Civic at Walmart and AutoZone, which is cheaper? Make sure to check the actual product pages to confirm the price.\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_97\",\"category\":\"price_comparison\",\"ques\":\"how much more is the 4-in x 6-in x 12-ft pressure-treated ground-contact southern pine timber on homedepot than their 4 x 4 x 10 ft?\\r\",\"web\":\"\"}\n{\"id\":\"walmart_comparison_shopping_125\",\"category\":\"price_comparison\",\"ques\":\"can you find three options of where to buy Smino Luv 4 Rent translucent green 2-LP explicit vinyl and list their prices and urls\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_118\",\"category\":\"price_comparison\",\"ques\":\"create a table of three retailers where you can buy  For Whom the Bell Tolls and in the columns put the price for the paperback and hardcover separately\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_20\",\"category\":\"price_comparison\",\"ques\":\"help me research where to buy A Tale of Two Cities and output a table of retailers in the rows, and in the columns put the price for the paperback and hardcover separately\\r\",\"web\":\"\"}\n{\"id\":\"homedepot_comparison_shopping_165\",\"category\":\"price_comparison\",\"ques\":\"I need to buy a 6-pack of ankle athletic socks, please find 2 different retailers and the price at which they offer the product\\r\",\"web\":\"\"}\n{\"id\":\"ebay_comparison_shopping_113\",\"category\":\"price_comparison\",\"ques\":\"find three different options of where to buy purple leather paisley pants and output a list of the prices for each site.\\r\",\"web\":\"\"}\n{\"id\":\"tractorsupply_comparison_shopping_19\",\"category\":\"price_comparison\",\"ques\":\"Could you compare the pricing and capacity (in gallons) of steel water troughs between Tractor Supply Co and Amazon to see which offers the best value per gallon? Please check the actual product pages to confirm prices and tank sizes.\\r\",\"web\":\"\"}\n{\"id\":\"zappos_comparison_shopping_1\",\"category\":\"price_comparison\",\"ques\":\"Can you help me compare the price of the cheapest men's Adidas Stan Smith sneakers at Zappos vs Foot Locker and tell me which site is cheaper overall?\\r\",\"web\":\"\"}\n{\"id\":\"target_comparison_shopping_27\",\"category\":\"price_comparison\",\"ques\":\"find the pack of papermate rainbow pens at target that has the most colors, and tell me how many more or less colors it has in it than the most colorful pack at walmart?\\r\",\"web\":\"\"}\n{\"id\":\"wayfair_comparison_shopping_3\",\"category\":\"price_comparison\",\"ques\":\"Can you help me compare the features and specifications (material, fill weight, care instructions, dimensions) of California King burgundy bedspreads available at Wayfair vs Amazon formatted as a table? Please check the actual product pages to confirm the details.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_comparison_shopping_456\",\"category\":\"price_comparison\",\"ques\":\"Can you help me compare the type of rope and length it is sold in of clothesline rope available at Amazon vs Home Depot. Please check the actual product pages to confirm details like material, length, diameter, and weight capacity.\\r\",\"web\":\"\"}\n{\"id\":\"composite_116\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Check Steam for the first  top-selling game today that has a TV series adaptation if any, then use JustWatch.com to find streaming services for the series adaptation.\\r\",\"web\":\"\"}\n{\"id\":\"composite_23\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Eventbrite.com, find a live music event in Nashville, TN happening this upcoming Saturday. Then on Spotify.com, find a songs by any of the performing artists from that event, if any. \\r\",\"web\":\"\"}\n{\"id\":\"composite_78\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Look at the amazon page for \\\"The Innovator's Dilemma\\\", see what it ranks in books overall, and then find a repair service anywhere in the US whose phone number contains that rank as a sub-string. Output the name and phone number of that repair service.\\r\",\"web\":\"\"}\n{\"id\":\"composite_121\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Wikipedia.org, look up Harvard University to find its location; then on Google Maps, get walking directions to Boston City Hall from this location.\\r\",\"web\":\"\"}\n{\"id\":\"composite_62\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Locate a coding bootcamp company in brooklyn, NYC, and tell me how much full-time tuition would cost there. Then use Google Maps to tel lme which bus I can take from Grand Army Plaza to reach there. Output the name of the bootcamp, the tuition cost, and the bus service name.\\r\",\"web\":\"\"}\n{\"id\":\"composite_89\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Go to lettuce.com and find the first restaurant after filtering their portfolio for spanish cuisine, then go their website to order, and add the 4 most commonly-ordered items to the cart and proceed to checkout. Also output and the prices of those 4 items.\\r\",\"web\":\"\"}\n{\"id\":\"composite_6\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Booking.com, find the cheapest available 8/10+ scored hotel room for a three-night stay starting December 15, 2025, in Jakarta for 2 adults. Use the hotel's address to search for the closest coffee shop, output it's name and address.\\r\",\"web\":\"\"}\n{\"id\":\"composite_87\",\"category\":\"compositional_tasks_v2\",\"ques\":\"on bklynlibrary.org find the northern-most library branch that has a teen tech help center, then find the year that branch opened to the public, how many square feet of space it has, and who the managing librarian is.\\r\",\"web\":\"\"}\n{\"id\":\"composite_81\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Retrieve the lowest-price round-trip flight from Dallas (DFW) to Miami (MIA) on Jan 20, 2026, to Jan 25, 2026, using Google Flights. Noting the flight's arrival timestamp in miami, book the cheapest compact car from Miami International on Rentalcars.com beginning no less than one hour after the flight arrives. For the first result output the price per day, make/model, and number of seats.\\r\",\"web\":\"\"}\n{\"id\":\"composite_56\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find what xbox.com says is a  top-selling xbox game; note who it was published by and the release date. Then tell me how many years have elapsed since when the CEO or head of that gaming studio was born and the release date.\\r\",\"web\":\"\"}\n{\"id\":\"composite_99\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Search for a \\\"applied scientist\\\" position on careers.microsoft.com in redmond, WA and for the first result, extract what the team or group name the job posting is for, and then search externally for what that group does and who it is led by.\\r\",\"web\":\"\"}\n{\"id\":\"composite_51\",\"category\":\"compositional_tasks_v2\",\"ques\":\"at the denver museum of nature and science, find the next show held at the Infinity Theater, and find out who the producer is, and furthermore the names of up to three other films/movies they produced.\\r\",\"web\":\"\"}\n{\"id\":\"composite_50\",\"category\":\"compositional_tasks_v2\",\"ques\":\"List all the members of the bands Nsync and BackStreet Boys. Find the net worth of the one with the longest last name.\\r\",\"web\":\"\"}\n{\"id\":\"composite_40\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Search for women's clothes on sale at zara, take the first result that is marked down, find out what materials it is composed of, and then tell me at what temperature the primary material ignites.\\r\",\"web\":\"\"}\n{\"id\":\"composite_79\",\"category\":\"compositional_tasks_v2\",\"ques\":\"on amazon, find the #3 best selling pantry staple item, and then on AllRecipes, find a recipe which contains that item as an ingredient. Output the full ingredients list along with the recipe name.\\r\",\"web\":\"\"}\n{\"id\":\"composite_120\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Please help me find the first news article published on universityofcalifornia.edu websites, then tell me two other articles published by the same author.\\r\",\"web\":\"\"}\n{\"id\":\"composite_67\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find the next upcoming exhibit at the George H.W. Bush library and tell me what dates it will be available. Tell me whether any total solar eclipse will occur at all within that time frame.\\r\",\"web\":\"\"}\n{\"id\":\"composite_38\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Find a vegetarian restaurant in San Francisco with a rating ≥4.5 and ≥100 reviews; use its address to book a compact car nearest to that location on Rentalcars.com from December 15 to December 18, 2025.\\r\",\"web\":\"\"}\n{\"id\":\"composite_100\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find a reddit post in r/golf talking about how golf courses take up \\\"3000 sq miles\\\" of land in the USA. Summarize the top upvoted comment for that post, and then find another website that substantiates any major claim that comment makes.\\r\",\"web\":\"\"}\n{\"id\":\"composite_123\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Eventbrite.com, find an art exhibition happening this month in Portland and extract the exact date and venue; then check Google Flights for the cheapest same-day round-trip tickets from Seattle (SEA) to Portland (PDX), completing the task before purchase.\\r\",\"web\":\"\"}\n{\"id\":\"composite_5\",\"category\":\"compositional_tasks_v2\",\"ques\":\"From Google Flights, record the least expensive one-way flight from Edinburgh (EDI) to Manchester (MAN) on December 28, 2025, then figure out what aircraft type the flight is on, and how many fewer passengers that aircraft type can carry compared to a 747-8 all-economy configuration.\\r\",\"web\":\"\"}\n{\"id\":\"composite_68\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Plan an itinerary of getting from central park, manhattan, to miami by taking trains only!\\r\",\"web\":\"\"}\n{\"id\":\"composite_111\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find out how many views Adele's \\\"Rolling in the Deep (Official Music Video)\\\" has, and then determine what percent of the worlds population that is using a calculator or equivalent search tool.\\r\",\"web\":\"\"}\n{\"id\":\"composite_21\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Wikipedia.org, look up the first Sister City of the city in which Massachusetts Institute of Technology (MIT) resides, and retrieve the 5-day weather forecast for that sister city.\\r\",\"web\":\"\"}\n{\"id\":\"composite_61\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find the location of the first race listed on raceroster.com, and then find the address of a café or coffee shop nearby that I can wait for my husband at while he finishes the race.\\r\",\"web\":\"\"}\n{\"id\":\"composite_22\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Locate the location of the upcoming NeurIPS conference in 2025 and then find the best local food near the event venue\\r\",\"web\":\"\"}\n{\"id\":\"composite_114\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Locate the top-seller RPG game on Steam and identify its matching game controller. On Amazon, find this controller and add it to the cart, stopping at the review page.\\r\",\"web\":\"\"}\n{\"id\":\"composite_106\",\"category\":\"compositional_tasks_v2\",\"ques\":\"use a mortgage rate calculator tool online to see what my estimated monthly payment will be (including only principal and interest) for a $500,000 home with a down payment of $80,000 over 30 years at an interest rate of 6.0% in 98101.\\r\",\"web\":\"\"}\n{\"id\":\"composite_94\",\"category\":\"compositional_tasks_v2\",\"ques\":\"I want to learn how much I should save for my 2-year olds college fund. Use the Office of Financial Rediness college savings calculator and input the following fields: 3% education cost inflation, $50,000 in current savings, $250 in monthly contributions with 6% rate of return. If their tuition is going to be $50,000 per year and room/board $12,000, how much more per month do i need to save according to the tool? (Hint: do not use the sliders)\\r\",\"web\":\"\"}\n{\"id\":\"composite_75\",\"category\":\"compositional_tasks_v2\",\"ques\":\"go to investor.gov and compute how much money I will have with an initial principle of $10000, to which I make monthly contributions of $200 over 10 years. Assume an interest rate of 5.0 compounded quarterly. Additionally, tell me the colors of the lines it plots in the results.\\r\",\"web\":\"\"}\n{\"id\":\"composite_96\",\"category\":\"compositional_tasks_v2\",\"ques\":\"can you go the latest news release from the US Dept. of Labor, and tell me who the media contact is and how many other contacts there are in their department?\\r\",\"web\":\"\"}\n{\"id\":\"composite_31\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Find one of Beyonce's favorite soul food restaurants in houston, go to their website, and find out when they opened. How much older are they than Beyonce herself?\\r\",\"web\":\"\"}\n{\"id\":\"composite_58\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Wikipedia.org, find the city containing the oldest university in the US,  use this location to find the lowest priced compact car rental for November 17-19, 2025, on Rentalcars.com.\\r\",\"web\":\"\"}\n{\"id\":\"composite_82\",\"category\":\"compositional_tasks_v2\",\"ques\":\"can you find a quote from Dario Amodei saying that AI will take a lot of jobs. What did he predict the unemployment rate would be, and how many percentage points higher is that than the maximum unemployment the US experienced in 2001?\\r\",\"web\":\"\"}\n{\"id\":\"composite_74\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Find a job on USA jobs in the 10003 area code, and tell me whether the salary of the first listing is above or below the median for that role nationally on salary.com\\r\",\"web\":\"\"}\n{\"id\":\"composite_25\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find an official microsoft support page showing a tutorial about pivot tables. Somewhere on that page, they must have an example spreadsheet or screenshot of one. What is the first row of that example table?\\r\",\"web\":\"\"}\n{\"id\":\"composite_55\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Steam, find the top-selling horror game and note its associated guidebook. On Amazon, search for this guidebook and add it to the cart, stopping at the cart review page.\\r\",\"web\":\"\"}\n{\"id\":\"composite_7\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On Booking.com, find the cheapest hotel available for a four-night stay from November 20–14, 2025, in San Francisco, California, for 1 adult. Use the hotel's address to identify the closest grocery store and tell me its name and address.\\r\",\"web\":\"\"}\n{\"id\":\"composite_60\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Search for any AI  conferences or workshops in San Francisco this month, noting the date and location; then on Google Flights, secure a viable round-trip flight from Toronto (YYZ) to San Francisco  on the summit date, stopping before booking.\\r\",\"web\":\"\"}\n{\"id\":\"composite_91\",\"category\":\"compositional_tasks_v2\",\"ques\":\"I need to find a job with Secret security clearance on USAjobs.com, can you find the first job in the list that has an annual salary, and then use another tool to compute what my after tax takehome pay would be for that job?\\r\",\"web\":\"\"}\n{\"id\":\"composite_42\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On LinkedIn.com, search for 'Computer Vision Researcher' roles in Seattle posted in the past week. Find me the latest computer vision course from stanford available for free online to prep.\\r\",\"web\":\"\"}\n{\"id\":\"composite_29\",\"category\":\"compositional_tasks_v2\",\"ques\":\"look at the first article published on searchengineland.com, summarize the key takeaway, and then find another article from a different site that supports / verifies it.\\r\",\"web\":\"\"}\n{\"id\":\"composite_112\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Locate a headline jazz event in Los Angeles featuring multiple artists in the near future, select the headline artist, and subsequently find and play a song from this artist on Spotify.com.\\r\",\"web\":\"\"}\n{\"id\":\"composite_4\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Using Google Maps, tell me how many miles it is to drive from Manchester Airport to Etihad Stadium, and whether that is longer or shorter than the distance from the george washington bridge to the NYSE.\\r\",\"web\":\"\"}\n{\"id\":\"composite_53\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Identify three jazz clubs in Chicago, and determine their neighborhoods; afterward, use Booking.com to find the least expensive hotel for a one-night stay in the first of those neighborhoods (sorted alphabetically) on December 28, 2025, for 2 adults.\\r\",\"web\":\"\"}\n{\"id\":\"composite_27\",\"category\":\"compositional_tasks_v2\",\"ques\":\"find the best mens face wash according to GQ or mens health, then buy it from amazon.com\\r\",\"web\":\"\"}\n{\"id\":\"composite_85\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Find the address for the office of 'Bright Future Forever' based in Seattle, WA; and then tell me the name of one of the DDS that works at the dental office across the street and where they graduated from undergrad.\\r\",\"web\":\"\"}\n{\"id\":\"composite_63\",\"category\":\"compositional_tasks_v2\",\"ques\":\"I want to find a Compliance Specialist job on NYC jobs for the city of new york and calculate my takehome pay if I were to get it. Assume the maximum end of the salary range and use smartasset.com tell me both what the take-home pay would be and effective tax rate.\\r\",\"web\":\"\"}\n{\"id\":\"composite_52\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On reddit, search for blues club in New Orleans and take the first one mentioned in the comments. What was the most recent comment that user made according to their reddit profile, and does it appear from their comments they actually live in Louisiana?\\r\",\"web\":\"\"}\n{\"id\":\"composite_16\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Find the names of the three \\\"dynasties\\\" that preside over broadway theater houses, and find out how many theaters each owns.\\r\",\"web\":\"\"}\n{\"id\":\"composite_84\",\"category\":\"compositional_tasks_v2\",\"ques\":\"during the first week of December, find the cheapest hotel in New York in times square then find tickets for the lion king or MJ the musical that week\\r\",\"web\":\"\"}\n{\"id\":\"composite_124\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Can you tell me the cost structure of a one-year certificate program in New York City at the International Center of Photography and how it is different than the same program at the New York Film Academy.\\r\",\"web\":\"\"}\n{\"id\":\"composite_57\",\"category\":\"compositional_tasks_v2\",\"ques\":\"I'm deciding between enrolling in stanford vs johns hopkins as a freshman, can you tell me how much a full-year (2 semester or 3 quarter) meal plan costs at each university (assuming I will eat the maximum number allowed or unlimited meals).\\r\",\"web\":\"\"}\n{\"id\":\"composite_43\",\"category\":\"compositional_tasks_v2\",\"ques\":\"On genentech's website, first tell me how many open roles there are in the regulatory & quality department at each job level, and secondly filter to the most senior job level and tell me what it's salary range is.\\r\",\"web\":\"\"}\n{\"id\":\"composite_98\",\"category\":\"compositional_tasks_v2\",\"ques\":\"Find top 'Software Engineer' roles in Seattle for an established big-tech company on LinkedIn.com and retrieve the associated company name; use the company name on Wikipedia.org to find the year it was founded.\\r\",\"web\":\"\"}\n{\"id\":\"tripadvisor_other_event_5\",\"category\":\"ticketing\",\"ques\":\"I plan on going to Fantastic Caverns in Springfield, MO on 12/2/2025 with my wife and two kids under 12. See if there are any discounts available online, either for a group discount or an age discount, and book tickets if any tickets are available at 3:00 PM.\\r\",\"web\":\"\"}\n{\"id\":\"discounts.aaa_theme_park_16\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Walt Disney World theme park tickets for two in Florida from AAA online for any day over 12/11-12/15 ? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"vacourts.gov_citation_13\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my Virginia traffic ticket online via the Virginia Courts website? Please let me know if there are any issues with completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"palaciodemafra.pt_other_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buya  Mafra Museum visit ticket online from the Palácio de Mafra website on December 20th? Please let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"universalorlando_theme_park_16\",\"category\":\"ticketing\",\"ques\":\"I plan on going to Universal Orlando Resort in Orlando, FL around Christmastime with my family of four. Could you please assist me in securing tickets? Please let me know if there are any issues or if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"pacificbonsaimuseum_other_event_1\",\"category\":\"ticketing\",\"ques\":\"I really want to see the Weyerhaeuser Company Bonsai Exhibit at the Pacific Bonsai Museum in Federal Way, WA with my boyfriend. Would it be possible to book tickets online for November 28th? Let me know if they're not available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"americasriverroots_music_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy the soonest Celebration Belle Riverfest 2025 tickets for me and my parents? Let me know if they're no longer available.\\r\",\"web\":\"\"}\n{\"id\":\"ludlowgaragecincinnati_music_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me the upcoming buy Barrington Levy concert tickets online within a 50 mile radius of Cincinnati, OH? Please let me know if they aren't available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"eventbrite_other_event_36\",\"category\":\"ticketing\",\"ques\":\"I am based in thie Chicago, IL. Can you help me buy the next Marriott bridal show tickets at a Marriott Hotel in the greater Chicago area on Eventbrite? Please let me know if the tickets aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"romehacks_music_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me get two tickets for the Vatican museums on January 9th at 1 PM? Let me know if they're not available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"caminitodelrey.info_other_event_2\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy 5 Caminito del Rey tickets online in Malaga, Spain from the official Caminito del Rey website in two wees? Please let me know if they aren't available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"ticketmaster_music_event_192\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy a ticket at an upcoming event at Shoreline Amphitheatre in Mountain View, CA on Ticketmaster? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"fandango_movie_87\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy 12 Superman 2025 movie tickets online on Fandango next Wednesday? We have four youth in our group. Please let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"anaheim_citation_3\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my City of Anaheim Police Department traffic ticket online? Please let me know if there are any issues with completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"bahn.de_transportation_3\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Deutsche Bahn train tickets from Munich to Vienna online on bahn.de on February 3rd? Please let me know if the tickets aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"amazon_lottery_2\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy pre-numbered blank raffle tickets on Amazon? Please let me know if they're not available for purchase.\\r\",\"web\":\"\"}\n{\"id\":\"portalnjmcdirect-cloud.njcourts.gov_citation_35\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my New Jersey municipal court ticket online via NJMCDirect? Please let me know if there are any issues with completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"showtimes_movie_44\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy a Downton Abbey movie tickets online for Dietrich Theater in Tunkhannock, PA? I would prefer seats in the center back. Let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"morgancountyutah.gov_citation_1\",\"category\":\"ticketing\",\"ques\":\"Could you help me pay my ticket online at the Morgan County, Utah District Court? Please let me know if there are any issues with the payment process.\\r\",\"web\":\"\"}\n{\"id\":\"confirmtkt_transportation_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me book six round-trip railway tickets online on ConfirmTkt from New Dehli to Mumbai Central?  I would like to travel over March 1st-14th. Let me know if there aren't any tickets available.\\r\",\"web\":\"\"}\n{\"id\":\"buckeyecountrysuperfest_music_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Buckeye Countryfest tickets from the Buckeye Country Superfest website? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"united_transportation_10\",\"category\":\"ticketing\",\"ques\":\"Could you help me book a United Airlines direct flight ticket on January 7th from Little Rock, Arl to Providence, RI online through United.com? Let me know if there are any issues or if the tickets aren’t available.\\r\",\"web\":\"\"}\n{\"id\":\"costco_theme_park_12\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Universal Studios theme park tickets online from Costco from December 20th to January 2nd for two people? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"ticketmaster_sporting_event_31\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy  three Toronto Maple Leafs game tickets in Toronto, ON  for the upcoming game on NHL.com? Please let me know if they're not available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"lacourt.ca.gov_citation_13\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my Los Angeles County speeding ticket online on the LA Court website? Please let me know if there are any issues with the payment process.\\r\",\"web\":\"\"}\n{\"id\":\"regmovies_movie_58\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy three tickets for the any PG-13 movie online at Regal Cinemas near Fairbanks, AK? Please let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"thesphere_other_event_20\",\"category\":\"ticketing\",\"ques\":\"Can you help me purchase four The Wizard of Oz Experience tickets online on The Sphere website in Las Vegas on December 1st at 5:00 PM? Please let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"albemarle.edu_music_event_1\",\"category\":\"ticketing\",\"ques\":\"Could you help me reserve two tickets for any event online from the College of the Albemarle Performing Arts Center in Elizabeth City, NC? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"koobit_music_event_2\",\"category\":\"ticketing\",\"ques\":\"Can you help me purchase Florence + The Machine Everybody Scream Tour tickets on StubHUb? Please let me know if they are sold out.\\r\",\"web\":\"\"}\n{\"id\":\"azfamily_citation_1\",\"category\":\"ticketing\",\"ques\":\"Can you please help me pay my photo radar traffic ticket online in Paradise Valley, AZ? Let me know if there are any issues processing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"sanbernardino.courts.ca.gov_citation_3\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my San Bernardino County traffic ticket online via the San Bernardino County Superior Court website? Please let me know if there are any issues completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"flyontario_transportation_1\",\"category\":\"ticketing\",\"ques\":\"Could you help me book the first available flight tickets from Ontario International Airport  to New York City using FlyOntario? Please let me know if there are any issues with availability.\\r\",\"web\":\"\"}\n{\"id\":\"stpaul.gov_citation_1\",\"category\":\"ticketing\",\"ques\":\"Could you please pay my City of St. Paul parking ticket online for me? Let me know if there are any issues with completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"mncourts.gov_citation_3\",\"category\":\"ticketing\",\"ques\":\"Could you please pay my St. Louis County, MN speeding ticket online through the Minnesota Courts website? Let me know if there are any issues or if you can't complete the payment.\\r\",\"web\":\"\"}\n{\"id\":\"ges.wcs.edu_other_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy the next GES Fest tickets online in Dallas, TX? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"nerdwallet_theme_park_9\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy discounted Epic Universe theme park tickets in Orlando, FL online around Christmastime? Consider looking at blogposts for resources, as well as AAA, Undercover tourist, and other sites with discounted websites. Please let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"seattlegreatwheel_theme_park_1\",\"category\":\"ticketing\",\"ques\":\"Could you assist me with purchasing Seattle Great Wheel tickets online from the Seattle Great Wheel website on the upcoming Sunday at around 7 PM? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"aquarionwater_theme_park_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy discounted Mystic Aquarium tickets online in Mystic, CT for me and my veteran father? I plan on going the upcoming Saturday morning. Let me know if they aren't available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"ticketmaster_music_event_25\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy the upcoming Malcolm Todd concert tickets on Ticketmaster? I can travel anywhere in the world. Please let me know if they're no longer available.\\r\",\"web\":\"\"}\n{\"id\":\"pay.baltimorecity.gov_citation_3\",\"category\":\"ticketing\",\"ques\":\"Could you help me pay my Baltimore parking tickets online through the Baltimore City website? Please let me know if there are any issues with the payment process.\\r\",\"web\":\"\"}\n{\"id\":\"etickets_sporting_event_1\",\"category\":\"ticketing\",\"ques\":\"Could you help me buy Calgary Stampede 2026 tickets online from eTickets.com in Calgary, AB on July 6? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"quickcourt.biz_citation_4\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my Henderson, LA traffic ticket online using QuickCourt? Please let me know if there are any issues processing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"expedia_transportation_67\",\"category\":\"ticketing\",\"ques\":\"Can you help me find cheap plane tickets from New Orleans, LA to El Paso, TX on Expedia? Let me know if there aren't any available flights.\\r\",\"web\":\"\"}\n{\"id\":\"transact2.dmv.ny.gov_citation_3\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay a New York traffic ticket online through the NY DMV? Please let me know if there are any issues with completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"arlandaexpress_transportation_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy two round-trip Arlanda Express train tickets from Arlanda Express online? I plan on traveling leaving anytime next Friday and staying there for a week. Find discounts if possible. Let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"stagepittsburgh_music_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy tickets for any upcoming Stage AE 2026 music event at Stage AE in Pittsburgh, PA online? Let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"wetzeltaxpiled-technologies_citation_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my Wetzel County Sheriff's current tax ticket online? Please let me know if there are any issues with completing the payment.\\r\",\"web\":\"\"}\n{\"id\":\"ticketmaster_music_event_162\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Lady Gaga Mayhem 2026 concert tickets in California on Ticketmaster? Please let me know if they're sold out.\\r\",\"web\":\"\"}\n{\"id\":\"cityofvancouver.us_citation_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me pay my City of Vancouver, WA parking ticket online? Please let me know if there are any issues with the payment process.\\r\",\"web\":\"\"}\n{\"id\":\"sugarbowl_other_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Sugar Bowl ski resort tickets online at SugarBowl.com for Lake Tahoe? I want to go with my family of 5, with 3 young kids. Let me know if it's not available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"reddit_sporting_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Giants football tickets online the next time they play a home game? Please let me know if they're unavailable.\\r\",\"web\":\"\"}\n{\"id\":\"help.ticketmaster_music_event_10\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy two Ariana Grande 2026 tour tickets on Ticketmaster in Los Angeles, CA? Let me know if they're not available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"alltrippers_other_event_1\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy London New Year's Eve tickets online? Please let me know if they're not available anymore.\\r\",\"web\":\"\"}\n{\"id\":\"whichmuseum_other_event_21\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy five discounted tickets for the upcoming Sunday at 1 PM to the Greater Cleveland Aquarium in Cleveland, OH online? I have three cihldren, ages 7, 10, 13, and I'm traveling with my husband. Let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"seaworld_theme_park_10\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy SeaWorld Orlando theme park tickets online using the ID.me military discount? Please let me know if tickets aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"artic.edu_other_event_1\",\"category\":\"ticketing\",\"ques\":\"Could you assist me in getting Art Institute of Chicago college student admission tickets online from the Art Institute of Chicago website? Please let me know if they're not available.\\r\",\"web\":\"\"}\n{\"id\":\"plandisney.disney.go_theme_park_6\",\"category\":\"ticketing\",\"ques\":\"Can you help me buy Disneyland theme park tickets online from Sam’s Club in Anaheim, CA? I plan on going during Christmastime with my fiance. Please let me know if they aren't available.\\r\",\"web\":\"\"}\n{\"id\":\"buy_condo_port_aransas__tx_11146\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a condominium in Sea Gull, Port Aransas, TX, that's under $900k, with 2 or more bedrooms, a water view, and low HOA fees. Can you help me find one?\\r\",\"web\":\"\"}\n{\"id\":\"buy_land_naples__fl_13486\",\"category\":\"realestate_complex\",\"ques\":\"I'm interested in buying land in Naples, FL. I'd like some options with over 0.5 acres, that are new listings, have no HOA, and preferably offer a water view. Can you help me find something that fits these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_condo_titusville__fl_7914\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking for a condo for sale in Titusville, Florida that’s under $500k, has 2 or more bathrooms, offers a water view, and has low HOA fees. Can you help me find something that matches these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_other_alice__tx_18179\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find a commercial property for sale in Alice, Texas that is new to the market, priced between $300k-$600k, and has central AC?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_amherst__nh_2032\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find a home for sale in Amherst, NH? I'm looking for something between $300k-$600k, with 4 or more bedrooms, over 2000 square feet, and in an area with top-rated schools.\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_madison__wi_6412\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a home in Madison, WI near Sunfield Street. Ideally, I'd like it to have at least 3 bedrooms, 2 bathrooms, central AC, and be located in a walkable neighborhood. Can you help me find something that fits these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_land_lake_county__in_4991\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy land for sale by owner in Lake County, Indiana, under $500k, over 0.5 acres, with active listings. Can you show me options that meet my criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_gallatin__tn_11755\",\"category\":\"realestate_complex\",\"ques\":\"I'm interested in buying a home in Gallatin, TN, ideally on Duncan Ave. My budget is between $300k-$600k, and I'm looking for a place with at least 3 bedrooms, a 2-car garage, and access to top-rated schools. Could you help me find listings that meet these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"rent_other_arcata__ca_7137\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to rent a property in Arcata, CA with 2+ bedrooms and in-unit laundry in a walkable neighborhood.\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_provo__ut_15202\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find a house for sale in Provo, UT with 3 or more bedrooms, that's new to the market and has a mountain view?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_westfield__chatham_hills_5479\",\"category\":\"realestate_complex\",\"ques\":\"I'm interested in buying a home in Chatham Hills, Westfield that has 4 or more bedrooms, was built after 2000, and is near top-rated schools. Can you help me find a listing that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_chambers_county__tx_2343\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a house in Chambers County, Texas with 3+ bedrooms, 2+ bathrooms, on a large lot, and under $500k. Can you show me listings that meet these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_pittsburgh__pa_13147\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a home with a river view in a walkable neighborhood in Pittsburgh, PA. Ideally, it should have 3+ bedrooms, 2+ bathrooms, and be built after 2000. Can you help me find something that fits these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_heath__tx_3681\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find new homes for sale in Heath, TX with pools, built after 2000, that have 4+ bedrooms, are new listings, and sit on large lots?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_houston__tx_15257\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find a move-in ready mobile home to buy in Houston, TX? I'm looking for something under $500k with 3 bedrooms and 2+ bathrooms. You can check listings for me online.\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_florida_18531\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find homes for sale in Florida that are between $300k-$600k, have 3 or more bedrooms, central AC, and are near transit?\\r\",\"web\":\"\"}\n{\"id\":\"buy_land_gun_barrel_city__tx_4916\",\"category\":\"realestate_complex\",\"ques\":\"I'm interested in buying land near Gun Barrel City, TX. Can you find active listings over 0.5 acres and under $500k?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_jackson__tn_2638\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a move-in ready home with 3 bedrooms and central AC in Jackson, TN, priced between $300k and $600k. Can you help me find one that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_townhouse_bolingbrook__il_3053\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find townhomes for sale in Bolingbrook, Illinois with 3 or more bedrooms, at least 2 bathrooms, priced under $400k, and that are new to the market?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_bossier_city__la_20568\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a small house with 3 bedrooms and 2+ bathrooms under $300k in Bossier City, LA. Can you help me find one that fits these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_denton__tx_732\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a home in Robson Ranch, Denton with 3 bedrooms, 2+ bathrooms, an active listing, and a 2-car garage. Can you help me find something that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"rent_apartment_sayville__ny_10236\",\"category\":\"realestate_complex\",\"ques\":\"I'm searching for an apartment to rent in Sayville, NY with 2 or more bedrooms, in-unit laundry, and a walkable neighborhood. Can you help me find one?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_highland__mi_2862\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find homes for sale in Highland, MI with at least 3 bedrooms, 2+ bathrooms, and a large lot?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_bartlett__tn_12368\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a home in Bartlett, TN with 4+ bedrooms, 2+ bathrooms, a large lot, and central AC. Can you find a listing that meets my criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_staten_island__ny_2532\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a house in Staten Island, NY that has 4 or more bedrooms, a large lot, and access to top-rated schools. Can you help me find a listing that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_columbus__ga_10335\",\"category\":\"realestate_complex\",\"ques\":\"Can you show me the latest listings of homes for sale in Columbus, GA with 4+ bedrooms, 2+ bathrooms, under $400k, and central AC?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_montesano__wa_7329\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find houses for sale in Montesano, WA with 3 or more bedrooms, at least 2 bathrooms, on over 0.5 acres, and that are new to the market?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_jenks__ok_10654\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a home in Jenks, Oklahoma with 3+ bedrooms, central AC, and a large lot. Can you show me listings?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_lambertville__mi_20673\",\"category\":\"realestate_complex\",\"ques\":\"Could you help me find homes for sale in Lambertville, MI with 3 or more bedrooms, 2 or more bathrooms, a large lot, and central AC?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_little_rock__ar_17955\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a move-in ready small house in Little Rock, Arkansas. Ideally, it should be under $500k, have 3 bedrooms, and include a 2-car garage. Can you show me options?\\r\",\"web\":\"\"}\n{\"id\":\"rent_house_nashville__tn_8900\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to rent a 3-bedroom, pet-friendly house with central AC in the Morrow Rd area of Nashville, TN. Could you find listings that meet these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_the_villages__fl_14171\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find move-in ready homes for sale in The Villages, FL with 3+ bedrooms, 2+ bathrooms, priced between $300k-$600k?\\r\",\"web\":\"\"}\n{\"id\":\"buy_other_lafayette__co_19861\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking for condominiums or townhouses for sale in Lafayette, CO with 2+ bathrooms, central AC, and low HOA fees. Could you find me some options?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_aiken__sc_20679\",\"category\":\"realestate_complex\",\"ques\":\"I'm interested in buying a home on Equinox Loop in Aiken, SC with 4+ bedrooms, 2.5+ bathrooms, a large lot, and central AC. Can you find a listing that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_temperance__mi_11916\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find homes for sale in Temperance, Michigan with 3 or more bedrooms, at least 2 bathrooms, and priced under $500k?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_tacoma__wa_12334\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking for homes for sale in Tacoma, WA that have 3 bedrooms, 2 or more bathrooms, and are under $500k. Can you show me some options?\\r\",\"web\":\"\"}\n{\"id\":\"rent_land_brodheadsville__pa_12988\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking for a commercial lot for rent near Brodheadsville, PA that's under $500k, over 0.5 acres, and new to market. Can you help me find one?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_lorain__oh_13583\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a move-in ready split level home in Lorain, Ohio with 3 bedrooms, 2+ bathrooms, and over 2000 sq ft. Could you find a listing that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_hillsboro__oh_5688\",\"category\":\"realestate_complex\",\"ques\":\"I'm interested in buying a house with 3 or more bedrooms, a 2-car garage, a large lot, and central AC in the Hillsboro, Ohio area. Could you show me listings that meet these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_oviedo__fl_3554\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find a 3 bedroom house with at least 2 bathrooms in Oviedo, Florida, located near top-rated schools?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_williamstown__nj_14447\",\"category\":\"realestate_complex\",\"ques\":\"Could you assist me in finding move-in ready, new listings with 4 or more bedrooms for sale in Williamstown, NJ?\\r\",\"web\":\"\"}\n{\"id\":\"buy_condo_cranston__ri_16769\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking for a condo for sale in Cranston, RI that meets the following criteria: under $500k, 2 bedrooms, low HOA fees, and located in a walkable neighborhood. Can you help me find an option that fits these requirements?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_lapeer_county__mi_19012\",\"category\":\"realestate_complex\",\"ques\":\"I'm searching for a home in Lapeer County, MI that's under $330k. Ideally, it should have 3 bedrooms, 2+ bathrooms, a large lot, and be move-in ready. Can you find options for me?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_omaha__ne_11006\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a house in Omaha, NE with 4 or more bedrooms, a large lot, and near top-rated schools. Can you find a listing that meets these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_other_minnesota_2733\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find farms for sale in Minnesota that are over 0.5 acres, have central AC, are recently reduced in price, and are move-in ready?\\r\",\"web\":\"\"}\n{\"id\":\"buy_other__13924\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy an oceanfront property that is under $500k, has 4 or more bedrooms, offers a water view, and is a new construction. Can you help me find something that fits these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_4059_10th_avenue_dr_sw__nc_19159\",\"category\":\"realestate_complex\",\"ques\":\"Can you help me find homes with at least 3 bedrooms, 2 or more bathrooms, and built after 2000 in the SW area of North Carolina? Please show me listings that meet these criteria.\\r\",\"web\":\"\"}\n{\"id\":\"buy_house_wyoming__mi_17426\",\"category\":\"realestate_complex\",\"ques\":\"I'm looking to buy a home in Wyoming, MI with 3 bedrooms, 2+ bathrooms, and central AC in a walkable neighborhood. Can you show me listings that meet these criteria?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_1239\",\"category\":\"jobs\",\"ques\":\"Help me apply for an administrative position listed on CareerBuilder and name at least three employers hiring for it according to CareerBuilder's listings\\r\",\"web\":\"\"}\n{\"id\":\"wildcard_wildcard_1969\",\"category\":\"jobs\",\"ques\":\"How many remote full time fullfillment center warehouse associate positions are available at Amazon according to their latest job listings?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2864\",\"category\":\"jobs\",\"ques\":\"Help me apply for an accounting position in Los Angeles listed on Robert Half's website and output all permanent senior accounting and  accounting manager positions according to Robert Half's listings\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_1219\",\"category\":\"jobs\",\"ques\":\"Help me apply for a chemistry research scientist position in Madison, WI found on LinkedIn and output five listings that would have me be an early application according to the listings. If there are less than five such listings, output them all.\\r\",\"web\":\"\"}\n{\"id\":\"pay_grades_pay_grades_1238\",\"category\":\"jobs\",\"ques\":\"I have a CDL. Find 3 entry-level driving jobs in Houston, TX on SimplyHired. What is the guaranteed minimum salary for each of these jobs according to the listings?\\r\",\"web\":\"\"}\n{\"id\":\"employer_landscape_employer_landscape_1359\",\"category\":\"jobs\",\"ques\":\"List at least three customer support specialist positions available on the Apple Careers site in New York.\\r\",\"web\":\"\"}\n{\"id\":\"job_id_job_id_127\",\"category\":\"jobs\",\"ques\":\"What are the locations for the motorcoach driver positions listed in Iowa on GoWindstar according to GoWindstar's job listings?\\r\",\"web\":\"\"}\n{\"id\":\"salary_range_salary_range_982\",\"category\":\"jobs\",\"ques\":\"What is the salary range for at least three available positions that are hiring immediately in McDonough, GA, according to the McDonough job openings page?\\r\",\"web\":\"\"}\n{\"id\":\"benefits_benefits_1624\",\"category\":\"jobs\",\"ques\":\"What some benefits are offered for positions listed on RL Carriers Careers?\\r\",\"web\":\"\"}\n{\"id\":\"company_size_company_size_661\",\"category\":\"jobs\",\"ques\":\"Tell me how many vacancies there are for Physical Scientist at NOAA on usajobs.gov and where the vacancies are located\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2979\",\"category\":\"jobs\",\"ques\":\"I'm looking for a cook position that pays at least $18/hr within 25 miles of Cumming, GA. Help me apply for 5 cook positions that meet such criteria on SimpliyHired.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_353\",\"category\":\"jobs\",\"ques\":\"Help me apply for an economist position in Washington, D.C. listed on USAJobs. Output at least two agencies hiring according to the listings if at least two agencies exist.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2635\",\"category\":\"jobs\",\"ques\":\"I'm looking for Home Infusion Nurse positions at the Cigna Group. How many open positions are there in California for this role?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2473\",\"category\":\"jobs\",\"ques\":\"I'm seeking a job in Charlotte, NC with Spectrum. Through the Spectrum Jobs website, find me 3 cusomter service jobs\\r\",\"web\":\"\"}\n{\"id\":\"job_titles_job_titles_139\",\"category\":\"jobs\",\"ques\":\"how many open opportunities are there at Howard Brown Health careers page in Chicago? What is the first position listed and its Requisition Number?\\r\",\"web\":\"\"}\n{\"id\":\"responsibilities_responsibilities_1537\",\"category\":\"jobs\",\"ques\":\"what are the first three \\\"essential functions\\\" of a driver with Fedex Freight as listed on one of their job postings?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_1546\",\"category\":\"jobs\",\"ques\":\"Help me apply for 3 retail sales associate positions near Glen Burnie, MD  that are friendly to veternas\\r\",\"web\":\"\"}\n{\"id\":\"job_id_job_id_253\",\"category\":\"jobs\",\"ques\":\"What is the requisition number, salary range, and posting closing date of the first \\\"comptroller\\\" job listed on https://jobs.myflorida.com/? And who is the office contact?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2317\",\"category\":\"jobs\",\"ques\":\"Help me apply for a customer support position at Thermo Fisher Scientific on their career page. I am looking for a position that only requires a high school diploma, and I would prefer it to be remote.\\r\",\"web\":\"\"}\n{\"id\":\"wording_wording_163\",\"category\":\"jobs\",\"ques\":\"What is the exact wording of the first sentence of the job description for a paralegal position on the Nevada Bar Jobs site? Output the job ID as well for my later reference.\\r\",\"web\":\"\"}\n{\"id\":\"wording_wording_2464\",\"category\":\"jobs\",\"ques\":\"Find  the exact wording of the first sentence of a job description on The Bair Foundation's Careers page based in Pennsylvania. Also return the ID of the job.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2810\",\"category\":\"jobs\",\"ques\":\"I have experience with the Microsoft Office Suite and covers medical insurance. Help me apply for a logistics coordinator position that meets such requirements in Miami, FL using CareerBuilder.\\r\",\"web\":\"\"}\n{\"id\":\"employer_landscape_employer_landscape_961\",\"category\":\"jobs\",\"ques\":\"Help me apply for a police officer position in Soldotna, AK  on their government jobs portal if it still exists, and tell me which form I need to fill out and what the hourly wage is.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2022\",\"category\":\"jobs\",\"ques\":\"Help me apply for an anthropologist (i.e. researcher, scientist, or professor) position in Washington, D.C. listed on Careers in Anthropology, if available, with a minimum salary of $60,000. Output three organiziations, univerisites, or companies hiring that meets these constraints according to these listings\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_1003\",\"category\":\"jobs\",\"ques\":\"Help me apply for a full-time sales position at Farmers Insurance by navigating their careers page in the US, and let me know if none exist. List the three closest listings to Boston, MA if at least three exist.\\r\",\"web\":\"\"}\n{\"id\":\"salary_range_salary_range_1277\",\"category\":\"jobs\",\"ques\":\"What is the salary range for finance positions available at Bank of Texas in Dallas, TX as listed on BOK Financial's career site, specifically for full-time roles? Output at least three of the job listings and the required years of experience for those positions.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_174\",\"category\":\"jobs\",\"ques\":\"Help me apply for a computer science position located in Rancho Cucamonga, CA, with a minimum salary of $80,000 if available, using LinkedIn. Provide 5  URLs to forms for me to fill out myself.\\r\",\"web\":\"\"}\n{\"id\":\"benefits_benefits_2600\",\"category\":\"jobs\",\"ques\":\"Output at least three psychologist positions and their benefits in Kentucky found on LinkedIn that require a Master's degree, if available? Provide links to their forms for job application as well in your output\\r\",\"web\":\"\"}\n{\"id\":\"salary_range_salary_range_1684\",\"category\":\"jobs\",\"ques\":\"What is the salary range for any job opening listed on the SSENSE Careers page requiring a Bachelor's degree, if available? Provide a URL for such a job if it exists.\\r\",\"web\":\"\"}\n{\"id\":\"responsibilities_responsibilities_1471\",\"category\":\"jobs\",\"ques\":\"What are the main responsibilities listed in a production operations job posting at Grande Cheese from their careers page, specifically for positions that require a minimum of three years of relevant experience?\\r\",\"web\":\"\"}\n{\"id\":\"qualifications_qualifications_724\",\"category\":\"jobs\",\"ques\":\"What are the qualifications for environmental scientist positions listed on the South Florida Water Management District careers page open to the public? How do the qualifications vary across listings?\\r\",\"web\":\"\"}\n{\"id\":\"wildcard_wildcard_2597\",\"category\":\"jobs\",\"ques\":\"List the salary or salary ranges for five different filing tax consultant positions based in Chicago, IL on Robert Half that require a CPA certification? Output pairs of (employers, salary) in decreasing order of salary.\\r\",\"web\":\"\"}\n{\"id\":\"responsibilities_responsibilities_2088\",\"category\":\"jobs\",\"ques\":\"What are the main responsibilities listed in the first administrative position post in Mililani, Hawaii that offers health insurance, if available? Output a link to the job listing as well.\\r\",\"web\":\"\"}\n{\"id\":\"salary_range_salary_range_633\",\"category\":\"jobs\",\"ques\":\"What is the salary range for the first logistics coordinator job posting in Miami, FL on LinkedIn, if any exist? Does the job require full-time on-site? How many people does it indicate have already applied?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2722\",\"category\":\"jobs\",\"ques\":\"Help me apply for a mid-level software development position at Amazon by reviewing available job postings on their official careers site that offer have a six-figure salary and require proficiency in JavaScript, if any exist. Provide a link to the form for the job.\\r\",\"web\":\"\"}\n{\"id\":\"employer_landscape_employer_landscape_624\",\"category\":\"jobs\",\"ques\":\"Can you find any roles for equipment operator positions in Houston, prefereably but not necessarily from Waste Management, offering a minimum salary of $50,000 and at least three years of experience, if available.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2720\",\"category\":\"jobs\",\"ques\":\"Help me apply for a finance position at Veritas Partners by exploring opportunities available on HireVeritas. I have five years of work experience and a bachelors in finance, which role would be most appropriate for me?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_1288\",\"category\":\"jobs\",\"ques\":\"Help me apply for a dentist position in Kentucky on the ADA CareerCenter with at least 401 (k) benefits and effective pay of at least $100/hr, if any exist.\\r\",\"web\":\"\"}\n{\"id\":\"wording_wording_2838\",\"category\":\"jobs\",\"ques\":\"What is the exact wording of the first sentence of the job description for the first airline job opening listed in Atlanta on ATL Careers that offers a minimum salary of $50,000 and requires a Bachelor's degree, if any exist? Direct me to a form to the job from the listing as well. Pre-fill the form with the city and state being Atlanta and Georiga, respectively.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_1737\",\"category\":\"jobs\",\"ques\":\"Help me apply for a firefighter position in Orange County, CA on GovernmentJobs that offers a minimum salary of $50,000 and is open to applicants with a Bachelor's degree, if any exist. List at least three such job postings and summarize how they differ at a high level.\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_410\",\"category\":\"jobs\",\"ques\":\"Help me apply for a maintenance job located in Chicago, IL, that offers a minimum salary of $50,000 and requires at least two years of experience, if any exist. What's a suitable option that can hire immediately?\\r\",\"web\":\"\"}\n{\"id\":\"requirements_requirements_7\",\"category\":\"jobs\",\"ques\":\"What are the in-person requirements listed for Kroger jobs available in Atlanta, GA on Kroger Family Careers that are full-time positions and offer health insurance, if any exist? What are the hours like for such positions based on the listings?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_2\",\"category\":\"jobs\",\"ques\":\"Help me apply for three administrative position in Minnesota listed on GovernmentJobs that pays at least $18/hr and requires a high school diploma, if any exist.\\r\",\"web\":\"\"}\n{\"id\":\"job_titles_job_titles_2726\",\"category\":\"jobs\",\"ques\":\"List the titles of available cashier positions at Walmart on their careers page that are full-time and within 10 miles of Chicago, if any exist. What are the 401(k) benefits and salary range?\\r\",\"web\":\"\"}\n{\"id\":\"job_id_job_id_1431\",\"category\":\"jobs\",\"ques\":\"What are the job numbers/ID s of three full-time warehouse supervisor positions in the greater Chicago area listed on SimplyHired that offers at least a $60,000 annual salary and requires a bachelor's degree, if any exist? What are the benefits of each of the positions? How many years of experience are required for each one?\\r\",\"web\":\"\"}\n{\"id\":\"employer_landscape_employer_landscape_2701\",\"category\":\"jobs\",\"ques\":\"find a store manager or assistant store manager position at dollar general close to new york, NY and tell me the location of the first such listing\\r\",\"web\":\"\"}\n{\"id\":\"salary_range_salary_range_2829\",\"category\":\"jobs\",\"ques\":\"is Triomics hiring? Can you tell me the job that has the highest salary that they are hiring for, and would I qualify for it given that I have a masters in clinical biology?\\r\",\"web\":\"\"}\n{\"id\":\"wildcard_wildcard_542\",\"category\":\"jobs\",\"ques\":\"What are the 3 most recent job openings shown on The Lash Lounge Careers site and what locations are they for?\\r\",\"web\":\"\"}\n{\"id\":\"qualifications_qualifications_2504\",\"category\":\"jobs\",\"ques\":\"I want to apply for a position at Bluegrass Chiro in kentucky; how many positions do they have open and what roles are they for?\\r\",\"web\":\"\"}\n{\"id\":\"apply_apply_50\",\"category\":\"jobs\",\"ques\":\"Help me apply for a full-time security position in Florida with allied universal, let me know the requisition id.\",\"web\":\"\"}"
  },
  {
    "path": "packages/evals/datasets/webvoyager/WebVoyager_data.jsonl",
    "content": "{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--0\", \"ques\": \"Provide a recipe for vegetarian lasagna with more than 100 reviews and a rating of at least 4.5 stars suitable for 6 people.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--1\", \"ques\": \"Find a recipe for a vegetarian lasagna that has at least a four-star rating and uses zucchini.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--2\", \"ques\": \"Find a recipe for a vegetarian lasagna under 600 calories per serving that has a prep time of less than 1 hour.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--3\", \"ques\": \"Locate a recipe for vegan chocolate chip cookies with over 60 reviews and a rating of at least 4.5 stars on Allrecipes.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--4\", \"ques\": \"Find a recipe for Baked Salmon that takes less than 30 minutes to prepare and has at least a 4 star rating based on user reviews.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--5\", \"ques\": \"Search for a popular Pasta Sauce with more than 1000 reviews and a rating above 4 stars. Create a shopping list of ingredients for this recipe.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--6\", \"ques\": \"Search for a vegetarian lasagna recipe that has at least a four-star rating and over 500 reviews.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--7\", \"ques\": \"Find a popular recipe for a chocolate chip cookie and list the ingredients and preparation steps.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--8\", \"ques\": \"Search for a recipe for Beef Wellington on Allrecipes that has at least 200 reviews and an average rating of 4.5 stars or higher. List the main ingredients required for the dish.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--9\", \"ques\": \"Find a high-rated recipe for vegetarian lasagna, list the key ingredients required, and include the total preparation and cook time stated on the recipe.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--10\", \"ques\": \"Find The Most Popular Recipes of the 1960s, noting the recipe name, preparation time and total time of the second recipe in this collection.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--11\", \"ques\": \"Discover a suitable chocolate cupcake recipe on Allrecipes that has a preparation time of under 1 hour and at least 100 user reviews.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--12\", \"ques\": \"Search for a popular cookie recipe on Allrecipes with more than 1000 reviews and a rating of 4.5 stars or better. Provide the list of ingredients needed.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--13\", \"ques\": \"Find a recipe with over 100 reviews for Fried Fish on Allrecipes, list the Full Nutrition Label and tell me the amount of Iron per Serving.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--14\", \"ques\": \"Search for a recipe that includes \\\"chicken breast\\\" and \\\"quinoa\\\" with preparation time under 30 minutes on Allrecipes.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--15\", \"ques\": \"Choose a dessert recipe on Allrecipes with a prep time of less than 30 minutes, has chocolate as an ingredient, and has a user rating of 4 stars or higher. Provide the name of the recipe, ingredients list, and step-by-step instructions.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--16\", \"ques\": \"Find a five-star rated chocolate chip cookie recipe that takes less than 1 hour to make on Allrecipes. Note how many reviews the recipe has and the main ingredients required.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--17\", \"ques\": \"Find the Easy Vegetarian Spinach Lasagna recipe on Allrecipes and tell me what the latest review says.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--18\", \"ques\": \"Find a recipe for a vegetarian lasagna that has over 300 reviews and an average rating of 4.5 or higher on Allrecipes.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--19\", \"ques\": \"Find a vegan lasagna recipe on Allrecipes that requires 10 ingredients or less and has feedback of more than 200 reviews. Provide a brief overview of the ingredient list and the total prep and cook time.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--20\", \"ques\": \"Find a recipe for a cauliflower pizza crust that has a preparation time of under 30 minutes and a rating of at least 4 stars on Allrecipes. Include the number of calories per serving.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--21\", \"ques\": \"Locate a high-rated recipe for gluten-free brownies on Allrecipes with at least 50 reviews. List the main ingredients and the total time required for preparation and cooking.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--22\", \"ques\": \"Find a recipe for a healthy avocado salad on Allrecipes that has a preparation time of less than 20 minutes and more than 30 user reviews. Include the nutritional information per serving.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--23\", \"ques\": \"Search Allrecipes for a baked lemon chicken recipe that has a prep time under 45 minutes, with at least a 4.5-star rating based on user reviews, and over 200 reviews. List the primary ingredients required.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--24\", \"ques\": \"Locate a recipe for an eggplant Parmesan on Allrecipes with a rating of at least 4.5 stars and over 50 reviews. Include the preparation time and the number of servings provided by the recipe.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--25\", \"ques\": \"Find a popular quinoa salad recipe on Allrecipes with more than 500 reviews and a rating above 4 stars. Create a shopping list of ingredients for this recipe and include the total cooking and preparation time.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--26\", \"ques\": \"Search for a high-protein vegetarian chili recipe on Allrecipes that has at least 50 reviews and a rating of 4 stars or higher. Provide the ingredient list, cooking time, and a brief description of the cooking steps.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--27\", \"ques\": \"Locate a chicken curry recipe on Allrecipes that has been reviewed more than 30 times and has a rating of at least 4 stars. Provide a summary of the recipe including ingredients, preparation time, and cooking instructions.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--28\", \"ques\": \"On Allrecipes, find a vegan brownie recipe that has at least 40 reviews and a rating of 4.5 or higher. Include the list of ingredients, total prep and cook time, and a brief overview of the preparation steps.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--29\", \"ques\": \"Search for a Mediterranean-style grilled fish recipe on Allrecipes that includes ingredients like olives, has at least a 4-star rating, and more than 25 reviews. Detail the ingredients, cooking method, and total time required for preparation and cooking.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--30\", \"ques\": \"Find a recipe for a vegan smoothie bowl on Allrecipes that includes bananas and leaves, has more than 20 reviews, and a rating of at least 4 stars. Provide a list of ingredients, preparation time, and a summary of the recipe steps.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--31\", \"ques\": \"Search for a seafood paella recipe on Allrecipes with a minimum of 4.5 stars rating and at least 50 reviews. The recipe should include shrimp and mussels. Provide the ingredients, total time, and an overview of the preparation steps.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--32\", \"ques\": \"Find a high-rated beef stew recipe on Allrecipes that requires a slow cooker and has at least 30 reviews. Detail the cooking time and the first five ingredients listed in the recipe.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--33\", \"ques\": \"Find a recipe for a low-carb breakfast on Allrecipes with at least 25 reviews. Show the Nutrition Facts and the total carbohydrate content per serving.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--34\", \"ques\": \"Locate a baked salmon recipe on Allrecipes that has at least 50 reviews and a rating of 4.5 stars or higher. Note the primary seasoning or herb used and the estimated cooking time.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--35\", \"ques\": \"Search for an Italian-style meatball recipe on Allrecipes that has more than 100 reviews. Detail the type of meat used and the overall cooking time required.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--36\", \"ques\": \"Locate a recipe for an American apple pie on Allrecipes with a rating of at least 4 stars and more than 50 reviews. Note the maximum temperature mentioned in the Directions.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--37\", \"ques\": \"Search for a Greek salad recipe on Allrecipes that has a prep time of under 25 minutes and more than 15 reviews. Include the primary cheese used and the type of dressing recommended.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--38\", \"ques\": \"Find a French ratatouille recipe on Allrecipes with a 4-star rating or higher and at least 15 reviews. Note the variety of vegetables included and the overall cooking time.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--39\", \"ques\": \"Locate a recipe for sushi rolls on Allrecipes with a minimum of 20 reviews. Show the Nutrition Facts and the main ingredients. Tell me how to store these rolls.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--40\", \"ques\": \"Browse the about us section of Allrecipes for a brief introduction to The Allrecipes Allstars.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--41\", \"ques\": \"List 3 recommended dinner recipes in the Allrecipes Dinners section.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--42\", \"ques\": \"Find a recipe for banana bread with more than 200 reviews and a rating of at least 4.0 stars on Allrecipes.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--43\", \"ques\": \"Find a recipe for a vegan pumpkin pie on Allrecipes with a minimum four-star rating and a total cook time exceeding 1 hour.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Allrecipes\", \"id\": \"Allrecipes--44\", \"ques\": \"List at least 6 holiday recipes sections mentioned in the Occasions section of Allrecipes.\", \"web\": \"https://www.allrecipes.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--0\", \"ques\": \"Search an Xbox Wireless controller with green color and rated above 4 stars.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--1\", \"ques\": \"Search for women's golf polos in m size, priced between 50 to 75 dollars, and save the lowest priced among results.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--2\", \"ques\": \"Find a gaming desktop with Windows 11 Home, and the disk size should be 1TB.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--3\", \"ques\": \"Find climbing gears and sort the results by price high to low. Answer the first 3 results after sorting.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--4\", \"ques\": \"Find the used Nintendo Switch Lite on Amazon then filter by 'Used - Good', tell me the cheapest one that is 'Used - Good'.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--5\", \"ques\": \"Find a Blue iPhone 12 Pro 128gb and add to cart.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--6\", \"ques\": \"Browse black strollers within $100 to $200 on Amazon. Then find one Among these black strollers with over 20,000 reviews and a rating greater than 4 star.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--7\", \"ques\": \"Browse the women's hiking boots on Amazon and filter the results to show only those that are waterproof and have a rating of at least 4 stars and size 6.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--8\", \"ques\": \"Find the cheapest Samsung-made Android tablet with screen between 10-10.9 inches on Amazon. Only answer the cheapest one.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--9\", \"ques\": \"Find a dog bed on Amazon that is washable and has a length of at least 30 inches.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--10\", \"ques\": \"Find the cost of a 2-year protection for PS4 on Amazon.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--11\", \"ques\": \"Find a stainless steel kitchen sink with double bowls on Amazon. Sort the results and find the cheapest one with FREE delivery.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--12\", \"ques\": \"Check reviews for a Ride On Car with 100+ reviews & 4+ stars rating on Amazon. Give me the top review about this Ride On Car.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--13\", \"ques\": \"Browse best selling black hoodies in mens size Big and Tall that is between $25 and $50 on Amazon.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--14\", \"ques\": \"Find the new surge protector on Amazon with 6 to 8 outlets under 25 dollars with customer reviews above 4+ stars.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--15\", \"ques\": \"Find a pair of mens running shoes in black, size 7, 4+ stars and under $50 and add them to my cart on Amazon.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--16\", \"ques\": \"Find the Return Policy for Mens Rhinestone Skull Graphic Shirt on Amazon. Color: Black, Size: XX-Large. If Free return is avaliable, tell me how to return this item.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--17\", \"ques\": \"Show me the list of baby products that are on sale and under 10 dollars on Amazon. Provide at least 2 on sale products\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--18\", \"ques\": \"Open Amazon's home page and tell me what the deal is that is going on at the moment, list the names of at least 2 items that are on offer and tell me what percent off they are.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--19\", \"ques\": \"Look for an English language book on roman empire history in the Amazon Kindle store. Sort by newests arrivals and look for a title that will be released within a month.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--20\", \"ques\": \"Search for a wireless ergonomic keyboard with backlighting and a rating of at least 4 stars. The price should be between $40 to $60. Save the product with the 500+ customer reviews.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--21\", \"ques\": \"Find a stainless steel, 12-cup programmable coffee maker on Amazon. The price range should be between $100 to $200. Report the one with the 4+ customer rating.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--22\", \"ques\": \"Search for a set of non-stick, oven-safe cookware on Amazon. The set should include at least 10 pieces and be priced under $150.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--23\", \"ques\": \"Look for a men's waterproof digital sports watch with a heart rate monitor on Amazon. It should be priced between $50 to $100.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--24\", \"ques\": \"Browse for a compact air fryer on Amazon with a capacity of 2 to 3 quarts. It should have a digital display, auto shutoff and be priced under $100.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--25\", \"ques\": \"Search for a queen-sized, hypoallergenic mattress topper on Amazon. It should have a memory foam material and be priced between $50 to $100.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--26\", \"ques\": \"Find a portable Bluetooth speaker on Amazon with a water-resistant design, under $50. It should have a minimum battery life of 10 hours.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--27\", \"ques\": \"Look for a USB-C hub on Amazon compatible with MacBook Pro, featuring at least 4 ports, including HDMI and SD card reader. The price should be under $50. Select the one after sorting by Best Sellers.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--28\", \"ques\": \"Search for a yoga mat on Amazon that is at least 6mm thick, non-slip, and eco-friendly. The price should be under $50.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--29\", \"ques\": \"Find a set of solar-powered garden lights on Amazon with a minimum pack of 10 lights. They should be LED and priced under $50.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--30\", \"ques\": \"Locate the highest-rated fiction book released in 2024 on Amazon, with a minimum of 50 customer reviews.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--31\", \"ques\": \"Find a compact digital camera on Amazon with a zoom capability of at least 10x, rated 4 stars or higher, and priced between $100 to $300.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--32\", \"ques\": \"Search for an electric kettle on Amazon with a capacity of at least 1.5 liters, made of stainless steel, and with a customer rating of 4 stars or above.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--33\", \"ques\": \"Search for a portable air conditioner on Amazon suitable for a room size of 300 sq ft, with energy efficiency rating, and compare the prices of the top three search results.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--34\", \"ques\": \"Find a beginner's acrylic paint set on Amazon, with at least 24 colors, suitable for canvas painting, and priced under $40.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--35\", \"ques\": \"Find a men's leather wallet on Amazon with RFID blocking, at least 6 card slots, and priced below $50. Check if it's available for FREE delivery.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--36\", \"ques\": \"Search for a children's science experiment kit on Amazon suitable for ages 8-13, with at least a 4-star rating and priced under $30.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--37\", \"ques\": \"Locate a queen-sized bedspread on Amazon with a floral pattern, and check if it's available in blue color.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--38\", \"ques\": \"Find a bird feeder on Amazon suitable for small birds, with an anti-squirrel mechanism, and check if it's available with free shipping.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--39\", \"ques\": \"Locate a travel guide book on Amazon for Japan, published in 2024, with at least 20 customer reviews.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Amazon\", \"id\": \"Amazon--40\", \"ques\": \"Locate a women's yoga mat in purple, with a thickness of at least 5mm, rated 4+ stars, and priced under $30 on Amazon. Check how many colors are available in total, and what is the return and delivery policy.\", \"web\": \"https://www.amazon.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--0\", \"ques\": \"Compare the prices of the latest models of MacBook Air available on Apple's website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--1\", \"ques\": \"Research the new features of the iOS 17 on Apple support and check its compatibility with the iPhone 12.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--2\", \"ques\": \"Compare the prices and chips for the iPhone 14 Pro and iPhone 15 Pro models directly from Apple's website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--3\", \"ques\": \"Find the latest model of the iPhone and compare the price and screen size between the pro and pro max.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--4\", \"ques\": \"How much does it cost to buy a Macbook pro, 16-inch, Apple M3 Max chip with 16-core CPU, 40-core GPU, 64GB unified memory, 1TB SSD.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--5\", \"ques\": \"Check the release date and price for the latest version of the iPhone.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--6\", \"ques\": \"Find AirPods on Apple and how many types are currently available.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--7\", \"ques\": \"When and where the Apple Vision Pro will be released.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--8\", \"ques\": \"Identify and list the specifications of the latest iPad model released by Apple, including its storage options, processor type, and display features.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--9\", \"ques\": \"Check the Apple Store for the availability of the latest iPhone model and schedule an in-store pickup at the nearest Apple Store for January 10, 2024.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--10\", \"ques\": \"Find information on the latest (as of today's date) MacBook model, including its key features such as processor type, memory size, and storage capacity.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--11\", \"ques\": \"Get information about the latest iPad model released by Apple, including its release date, base storage capacity, and starting price available on Apple's official website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--12\", \"ques\": \"What Apple Repair ways are mentioned on apple website, answer 2 of them.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--13\", \"ques\": \"How many colors does the latest MacBook Air come in?\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--14\", \"ques\": \"Identify the upgrade options available for the cheapest base model of the MacBook Pro 14-inch with M3 chip, and calculate the total price difference from the base model to the maximum upgrade (no Pre-Installed Software) offered by Apple.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--15\", \"ques\": \"On Apple's website, how many different types of keyboards are available when customizing your 14-inch MacBook Pro?\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--16\", \"ques\": \"Find on Apple website how many types of AirPods (3rd generation) are available and what is the price difference.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--17\", \"ques\": \"Search Apple for the accessory Smart Folio for iPad and check the closest pickup availability next to zip code 90038.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--18\", \"ques\": \"Check if there are trade-in offers for the latest model of iPhone.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--19\", \"ques\": \"On Apple's website, what is the slogan for the Mac and what is the slogan for the Macbook pro.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--20\", \"ques\": \"Check the price for an Apple iPhone 14 Plus with 256GB storage in Purple color.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--21\", \"ques\": \"Identify the available storage options for the latest iPad Pro on the Apple website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--22\", \"ques\": \"Find out the trade-in value for an iPhone 13 Pro Max in good condition on the Apple website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--23\", \"ques\": \"Determine the price difference between the latest series of Apple Watch and Apple Watch SE on the Apple website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--24\", \"ques\": \"Find out the starting price for the most recent model of the iMac on the Apple website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--25\", \"ques\": \"On the Apple website, look up the processor for the latest model of the Apple TV.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--26\", \"ques\": \"Find the maximum video recording resolution supported by the latest iPad mini on the Apple website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--27\", \"ques\": \"On Apple's website, check if the HomePod mini in store is available in multiple colors and list them.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--28\", \"ques\": \"On the Apple website, find out if the Mac Mini can be configured with a GPU larger than 16-core.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--29\", \"ques\": \"On Apple's website, check the estimated battery life of the latest MacBook Air during web browsing in Tech Specs.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--30\", \"ques\": \"Check the storage options and prices for the latest iPad Pro models on Apple's website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--31\", \"ques\": \"On Apple's website, what is the slogan for the latest Apple Watch Series.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--32\", \"ques\": \"Investigate the trade-in value for an iPhone 11 Pro Max on Apple's website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--33\", \"ques\": \"Look for the color options available for the newest iMac.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--34\", \"ques\": \"Identify the size and weight for the Apple TV 4K and list the Siri Remote features introduced.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--35\", \"ques\": \"How many types of Apple Pencil are currently available on the Apple's website? Which one supports Wireless pairing and charging.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--36\", \"ques\": \"Browse Apple Music on the entertainment section of the Apple's website, and see which singers' names are included in the pictures on this page.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--37\", \"ques\": \"Compare the color options of iPhone 13 Pro, iPhone 14 Pro and iPhone 15 Pro.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--38\", \"ques\": \"Explore accessories for Apple Vision Pro, list at least three accessories.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--39\", \"ques\": \"Find solutions on Apple's website if you forgot your Apple ID password.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--40\", \"ques\": \"Find information on Apple website, and tell me the device weight of Apple Vision Pro and list 5 Built-in Apps it supports.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--41\", \"ques\": \"How much does it cost to buy an ipad mini with 64GB storage and Wi-Fi + Cellular connectivity? (no engraving, no apple pencil, no smart folio, no apple trade-in).\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"Apple\", \"id\": \"Apple--42\", \"ques\": \"Find updates for Apple Watch Series 7,8,9 on Apple's website.\", \"web\": \"https://www.apple.com/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--0\", \"ques\": \"Search for the latest preprints about 'quantum computing'.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--1\", \"ques\": \"Search for the latest research papers on quantum computing submitted to ArXiv within the last two days.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--2\", \"ques\": \"Look up the most recent papers related to 'cs.CL', select one and show its abstract.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--3\", \"ques\": \"Locate the most recent research paper about 'Algebraic Topology' under Mathematics published on ArXiv. Provide the title of the paper, the name of the authors, and the abstract.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--4\", \"ques\": \"Find the most recent research papers in Astrophysics of Galaxies. How many papers have been announced in the last day?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--5\", \"ques\": \"Search papers about \\\"quantum computing\\\" which has been submitted to the Quantum Physics category on ArXiv. How many results in total. What if search in all archives?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--6\", \"ques\": \"How many figures and tables are in the paper \\\"On the Sentence Embeddings from Pre-trained Language Models\\\"?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--7\", \"ques\": \"Find the most recent paper submitted on machine learning in the Computer Science category posted on ArXiv.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--8\", \"ques\": \"What is the latest news on ArXiv?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--9\", \"ques\": \"Find the latest research paper about neural networks published on ArXiv which has been submitted within the last week.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--10\", \"ques\": \"Visit ArXiv Help on how to withdraw an article if the submission is not yet announced.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--11\", \"ques\": \"For Non-English submissions, do I need to provide a multi-language abstract, if need, answer the separator between the multiple abstracts.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--12\", \"ques\": \"Find store in arXiv Help, tell me how many styles of arXiv Logo Shirt are available?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--13\", \"ques\": \"How many articles on ArXiv with 'SimCSE' in the title?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--14\", \"ques\": \"On ArXiv, how many articles have 'SimCSE' in the article and are originally announced in October 2023?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--15\", \"ques\": \"Searching Chinese Benchmark on ArXiv, how many papers announced in December 2023 mention being accepted for AAAI 2024?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--16\", \"ques\": \"Locate the latest research about gravitational waves that were uploaded to ArXiv this week and provide a brief summary of one article's main findings.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--17\", \"ques\": \"Find the paper 'GPT-4 Technical Report', when was v3 submitted?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--18\", \"ques\": \"Download the paper 'Dense Passage Retrieval for Open-Domain Question Answering'. How many formulas are in the article and which one is the loss function?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--19\", \"ques\": \"Which university maintains and manages ArXiv. Accessing the university's website from ArXiv, how many underegraduate students are currently at the university.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--20\", \"ques\": \"Find the latest paper on 'machine learning in the Statistics section of ArXiv and provide its abstract.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--21\", \"ques\": \"Search for papers on 'neural networks for image processing' in the Computer Science category on ArXiv and report how many were submitted in the last week.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--22\", \"ques\": \"Locate the ArXiv Help section and find instructions on how to subscribe to daily listing emails for new submissions in a specific category.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--23\", \"ques\": \"Determine how many articles with the keyword 'autonomous vehicles' were published in the 'Electrical Engineering and Systems Science' section of ArXiv yesterday.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--24\", \"ques\": \"Identify the most recent paper related to 'graph neural networks' on ArXiv and determine the affiliation of the first author.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--25\", \"ques\": \"Browse the ArXiv store and let me know how many different types of merchandise are available.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--26\", \"ques\": \"Search for papers related to 'climate change modeling' on ArXiv and find out how many have been published in the Earth and Planetary Astrophysics (astro-ph.EP) category in the last week.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--27\", \"ques\": \"On ArXiv, what categories does Economics include, and what are their abbreviations?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--28\", \"ques\": \"Search 'Poly encoder' by title on ArXiv and check whether the articles in the search results provide HTML access.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--29\", \"ques\": \"On ArXiv, search for papers with 'Neural Network Optimization' in the title published in 2023, and provide the number of such papers.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--30\", \"ques\": \"Look up the submission guidelines on ArXiv for submitting a paper and tell me the formats for figures.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--31\", \"ques\": \"Search ArXiv for papers with 'Graph Neural Networks' in the abstract that were submitted between Jan 1, 2024, and Jan 3, 2024, and determine how many of these papers have more than five authors.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--32\", \"ques\": \"Locate the latest paper on ArXiv within the 'Nonlinear Sciences - Chaotic Dynamics' category, summarize the abstract and note the submission date.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--33\", \"ques\": \"Query ArXiv for the latest research article in the category of Systems and Control under Computer Science. Summarize the main objective or hypothesis presented in the paper and provide the names of the authors.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--34\", \"ques\": \"Search for the most recent paper related to non-commutative geometry submitted by an author with the first name John. Provide the title and the abstract.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--35\", \"ques\": \"Retrieve the latest research paper in Quantum Physics from ArXiv and provide the title, author(s), and date of submission.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--36\", \"ques\": \"Search 'CVPR 2023' and 'CVPR2023' through journal ref on ArXiv to see how many results there are respectively.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--37\", \"ques\": \"Find the names of people in ArXiv's Leadership Team.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--38\", \"ques\": \"Find the ArXiv Blog on the ArXiv website and summarize the content of its latest article.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--39\", \"ques\": \"Search the title 'GPT-4 Technical Report' and access this paper through HTML format. Read the paper on this page and tell me what is 'one of the main goals of developing such models' mentioned in the Introduction.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--40\", \"ques\": \"How many articles are there on each of the three most recent announce days in the Solar and Stellar Astrophysics section of ArXiv. Choose one at random and answer its title and when the first version was uploaded?\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--41\", \"ques\": \"Find the button to share arxiv non-profit store and follow the QR code to share the shop. Then add arXiv Forever short sleeve (XL) to your cart.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"ArXiv\", \"id\": \"ArXiv--42\", \"ques\": \"Find an article published between 1 January 2000 and 1 January 2005 that requires Support Vector Machines in the title and its Journey ref is ACL Workshop.\", \"web\": \"https://arxiv.org/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--0\", \"ques\": \"Find a report on the BBC News website about recent developments in renewable energy technologies in the UK.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--1\", \"ques\": \"Read the latest health-related news article published on BBC News and summarize the key points discussed.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--2\", \"ques\": \"Read the latest article regarding the environmental impacts of deforestation published within the last two days.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--3\", \"ques\": \"Check the leaderboard for Golf's DP World Tour in the SPORT section, what was the name of the most recent tournament, and how many teams have a Total of -10 strokes.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--4\", \"ques\": \"Find the latest article regarding the economic implications of climate change in Europe as reported by BBC News and summarize the central points.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--5\", \"ques\": \"Find the article \\\"What is climate change? A really simple guide\\\" and use it to answer what human activities are causing climate change.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--6\", \"ques\": \"Find the top story from BBC News in the technology section for today.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--7\", \"ques\": \"Find a AI-related story under Technology of Business. What is in the first picture in the story?\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--8\", \"ques\": \"Get a brief overview of the economic implications of the UK's latest trade deal posted on BBC News and the date when the article was published.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--9\", \"ques\": \"Find out which musician made the headlines in Music News.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--10\", \"ques\": \"Identify the main headlines covering the UK's plan to tackle climate change on BBC News.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--11\", \"ques\": \"Find out how many teams are in the Scottish Premiership of the Football Tournament and when did the Hibernian team's most recent match start?\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--12\", \"ques\": \"Find a picture in the travel section that contains food, tell me what the food is called and what region it comes from.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--13\", \"ques\": \"Search for recent news related to Trump and summarize the main points.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--14\", \"ques\": \"Find a news article on BBC News about the impact of the recent tech industry layoffs on the global economy. Summarize the key points and the name of the author, and provide the date of publication.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--15\", \"ques\": \"What does the current headline in Natural Wonders tell about.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--16\", \"ques\": \"Identify the most recent development or update in Brexit negotiations as reported on BBC News and report the key points and any stated impacts on European economies.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--17\", \"ques\": \"How many War related sections are currently in BBC News.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--18\", \"ques\": \"Visit BBC News Audio, What are the best PodCasts for 2023? List 2 of them.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--19\", \"ques\": \"Visit the Athletics calendar for the date of the next earliest game.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--20\", \"ques\": \"Find the latest article in the Green Living section on BBC News and provide a summary of its main points.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--21\", \"ques\": \"Identify the top headline in the World News section on BBC News and describe the region it is related to.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--22\", \"ques\": \"Determine the current top business story on BBC News and give a brief overview of its economic implications.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--23\", \"ques\": \"Identify the latest health-related news on BBC News and summarize the main findings or recommendations.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--24\", \"ques\": \"Search the latest article about space exploration on BBC News and summarize its key points.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--25\", \"ques\": \"Find the most recent sports analysis article on BBC News related to the English Premier League and summarize its key insights.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--26\", \"ques\": \"Locate the latest report on BBC News about the impact of recent natural disasters in Asia and summarize the key points and areas affected.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--27\", \"ques\": \"Find the most recent article on BBC News about archaeological discoveries and summarize the main findings and their significance.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--28\", \"ques\": \"Find the Market Data section on BBC News and tell me which company the data comes from.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--29\", \"ques\": \"Visit BBC News Audio and find out which podcast episode is currently featured as the \\\"New Releases\\\".\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--30\", \"ques\": \"In the Culture section, identify the latest film release reviewed and provide a brief summary of the review.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--31\", \"ques\": \"Check the Sports section for the result of the most recent Manchester United football match.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--32\", \"ques\": \"Find the artificial intelligence section, what is the top headline at this time, and which companies are involved?\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--33\", \"ques\": \"In the World News section, find the latest war situations of Middle East and provide a brief summary.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--34\", \"ques\": \"Find The SpeciaList section in Travel and browse the page to see which cities are mentioned.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--35\", \"ques\": \"In the Asia section, browse and identify the most recent report about technological advancements and summarize its content.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--36\", \"ques\": \"Look up recent articles in the Africa news section in World, summarize what topics most of these news are about\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--37\", \"ques\": \"Identify the latest book review featured in the Culture section and provide the title and author of the book.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--38\", \"ques\": \"Find news related to the storm in Weather section and indicate where and when the severe weather occurred.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--39\", \"ques\": \"Check the Horse Racing results in Sport section, browse all the games that took place yesterday and see which one had the highest number of runners.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--40\", \"ques\": \"Read and summarise a recent story on BBC News about people being injured or killed in wars.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"BBC News\", \"id\": \"BBC News--41\", \"ques\": \"Find Golf in BBC News, check the Leaderboard at this point in Women's Majors and count which country has the most players in the top 20? Which player has the best score amongst the Australian players and in what place.\", \"web\": \"https://www.bbc.com/news/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--0\", \"ques\": \"Find a Mexico hotel with deals for December 25-26.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--1\", \"ques\": \"Find the cheapest available hotel room for a three night stay from 1st Jan in Jakarta. The room is for 2 adults, just answer the cheapest hotel room and the price.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--2\", \"ques\": \"Find a hotel in Ohio From December 20th to December 23th for 3 adults and 2 rooms.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--3\", \"ques\": \"Find a hotel with 4 star and above rating in Los Angeles for 3 days from Dec 18th.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--4\", \"ques\": \"Search for the cheapest Hotel near Kashi Vishwanath Temple that offer breakfast from Dec 25th - Dec 26th.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--5\", \"ques\": \"Search a hotel with free WiFi and air conditioning in Bali from Jan 1 to Jan 4, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--6\", \"ques\": \"Book one room which provides breakfast, and airport shuttle from Jan 22 to 25 in Los Angeles.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--7\", \"ques\": \"Find a hotel room on January 3-6 that is closest to National University of Singapore and costs less than $500\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--8\", \"ques\": \"Get the hotel with highest review score and free cancelation in Chennai for 20/12/2023 - 21/12/2023.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--9\", \"ques\": \"Find hotels for 2 adults in London with a price less than 250 dollars for four days starting from December 25. You must browse the page and offer at least 3 options.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--10\", \"ques\": \"Find a well-reviewed hotel in Paris with available bookings suitable for a couple (2 adults) on Valentine's Day week, February 14-21, 2024, that offers free cancellation options.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--11\", \"ques\": \"Reserve a hotel in downtown Chicago with a rating of 9 or higher for a stay from March 20-27, 2024, which offers free cancellation and includes a fitness center.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--12\", \"ques\": \"Find a hotel in Paris with a customer review score of 8 or higher, free Wi-Fi, and available for a 5-night stay starting on January 5th, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--13\", \"ques\": \"Find and book a hotel in Paris with suitable accommodations for a family of four (two adults and two children) offering free cancellation for the dates of February 14-21, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--14\", \"ques\": \"Book a highly-rated hotel with a swimming pool and free WiFi near the Louvre Museum in Paris for the weekend of March 3-5, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--15\", \"ques\": \"Find the highest-rated luxury hotel in Rome available for booking from January 10, 2024, to January 20, 2024, for 2 adults. Include the cost, amenities offered, and customer rating.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--16\", \"ques\": \"Look for a hotel in Paris with a user rating of 9 or higher and available for a 5-night stay starting January 15, 2024. The hotel should also offer free Wi-Fi and breakfast included in the price. Provide the name, location, and price per night.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--17\", \"ques\": \"Find a hotel in Paris with a fitness center and a rating of 8 or higher available for a 5-night stay starting from February 14, 2024, and sort the results by best reviewed.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--18\", \"ques\": \"Search a hotel in London with a user rating of 8 or higher for a stay between February 14th, 2024, and February 21st, 2024, suitable for a couple. Provide the name and a short description of the hotel.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--19\", \"ques\": \"Look for a hotel with customer ratings above an 8.0 in Paris, France for a weekend stay from March 18, 2024, to March 20, 2024, and list top three suggestions based on user reviews.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--20\", \"ques\": \"Locate a hotel in Rome with a good rating (7 or above) that offers free cancellation and breakfast included, for a three-night stay from February 28 to March 2, 2024, for two adults.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--21\", \"ques\": \"Find a hotel in Sydney with a rating of 8 or higher, providing free Wi-Fi and parking, available for a four-night stay starting on March 10, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--22\", \"ques\": \"Search for a hotel in Amsterdam with a customer review score of 9 or higher, offering bicycle rentals, for a week-long stay from March 15 to March 22, 2024, for two adults.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--23\", \"ques\": \"Identify a hotel in Tokyo with a spa and wellness center, rated 9 or above, with availability for a five-night stay starting on February 20, 2024. Check if free cancellation is offered.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--24\", \"ques\": \"Find a hotel in Barcelona for a stay from February 25-28, 2024. Please sort the results by distance from the beach and make sure they offer free Wi-Fi and breakfast.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--25\", \"ques\": \"Search for a hotel in Lisbon with airport shuttle, rated 8.5 or above, available for a six-night stay from March 1 to March 7, 2024, for two adults, breakfast included.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--26\", \"ques\": \"Check Booking.com for a 3-star hotel or higher in Paris with a guest rating above 8.0 and available parking for dates February 20-23, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--27\", \"ques\": \"Locate a hotel in Melbourne offering free parking and free WiFi, for a stay from February 28 to March 4, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--28\", \"ques\": \"Find a hotel in Dubai with a swimming pool, for a week-long stay from February 22 to February 29, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--29\", \"ques\": \"Search for a hotel in Toronto with a fitness center and a rating of 8+, available for a two-night stay from March 5 to March 7, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--30\", \"ques\": \"Search for hotels in London from March 20 to March 23, 2024, on Booking. How many hotels are left after applying the Breakfast included and Fitness center filters?\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--31\", \"ques\": \"Search for hotels in Rio de Janeiro from March 1-7, 2024, check the Brands filter to see which brand has the most hotels and which brand has the fewest.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--32\", \"ques\": \"Look for hotels in Sydney from February 24 to February 27, 2024, on Booking. Once the Swimming Pool and Airport Shuttle filters are applied, what is the total number of hotels available?\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--33\", \"ques\": \"Find the Customer Service on the Booking website, browse the questions about cancellation, and tell me 'how do I know whether my booking has been cancelled'.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--34\", \"ques\": \"Search for a hotel in Berlin available for a three-night stay from March 15 to March 18, 2024, for one adult. Tell me the price in USD and CNY for the three-night stay.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--35\", \"ques\": \"Browse the booking website to get inspiration for your next trip, and summarize at least three places mentioned in one of the travel articles.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--36\", \"ques\": \"Search for a budget hotel in Rome under $100 per night for one adult from March 20 to March 23, 2024. Sort the results by price, identify if any of top three results offer breakfast.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--37\", \"ques\": \"Search for a resort (not hotel) in Bali, detailing the available dates between March 20, 2024, and March 25, 2024, and checking any provided tour or cultural experiences.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--38\", \"ques\": \"Look up Vienna hotel options with availability for a 4-night stay from February 28 to March 4, 2024, with amenities that include a Parking, breakfast included, and a rating of 8+ on Booking.com.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--39\", \"ques\": \"Find a pet-friendly hotel with parking available in downtown Toronto for the stay of February 24-26, 2024.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--40\", \"ques\": \"I need to choose a hotel in Shenzhen, please select date (6 March to 8 March 2024) and click the search button. How much it costs when convert the price to Chinese Yuan on the page.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--41\", \"ques\": \"Browse Booking's homepage to find out which company it belongs to.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--42\", \"ques\": \"Search for a hotel in Hokkaido for the period March 1 to March 7, 2024, with a rating of 9+, check out its user reviews, which categories are greater than 9 and which are less than 9?\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Booking\", \"id\": \"Booking--43\", \"ques\": \"Search for properties in Los Angeles, browse the results page to see what filters are available, list some of them.\", \"web\": \"https://www.booking.com/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--0\", \"ques\": \"Look up the pronunciation and definition of the word \\\"sustainability\\\" on the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--1\", \"ques\": \"Find the pronunciation, definition, and a sample sentence for the word 'serendipity'.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--2\", \"ques\": \"Look up the pronunciation, definition, and example sentence for the word \\\"ubiquitous\\\" in UK and US English.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--3\", \"ques\": \"Look up the definition, pronunciation, and examples of the word \\\"zeitgeist.\\\"\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--4\", \"ques\": \"Look for the British English pronunciation of the word \\\"innovate\\\" and write down the International Phonetic Alphabet (IPA) notation, then find one example sentence provided in the Cambridge Dictionary that uses this word.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--5\", \"ques\": \"Learn the UK and US pronunciation of the word \\\"procrastination\\\", and find one example sentence that reflects its use in context.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--6\", \"ques\": \"Search for the word \\\"sustainability\\\" on the Cambridge Dictionary, what is the translation of sustainability into Chinese and French in the dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--7\", \"ques\": \"Look up the meaning, pronunciation, and an example sentence of the word \\\"gestalt\\\" using the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--8\", \"ques\": \"Find three different meanings of \\\"dog\\\" in Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--9\", \"ques\": \"Look up the British pronunciation of the word \\\"euphoria\\\" and find an example sentence using that word on the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--10\", \"ques\": \"Look up the definition and pronunciation of the word \\\"impeccable\\\" and also find an example sentence using that word.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--11\", \"ques\": \"Look up the pronunciation and definition of the word \\\"ameliorate,\\\" and provide an example sentence using the word.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--12\", \"ques\": \"Find the pronunciation, definition, and a sample sentence for the word \\\"resilience\\\" in the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--13\", \"ques\": \"Find one word, one phase and one idiom related to euphoria in Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--14\", \"ques\": \"Use the Cambridge Dictionary to find the pronunciation, definition, and one example sentence for the word \\\"concatenate\\\".\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--15\", \"ques\": \"Find the pronunciation and a sample sentence for the word \\\"pandemic.\\\"\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--16\", \"ques\": \"Look up the definition of \\\"cryptocurrency\\\" on Cambridge Dictionary, provide the pronunciation, and use it in two example sentences that illustrate different contexts.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--17\", \"ques\": \"How many meanings of \\\"unblemished\\\" are given in Cambridge Dictionary? Please browse the page and give the number directly.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--18\", \"ques\": \"Search for \\\"to behave well\\\" in Cambridge Dictionary's Thesaurus and see which synonyms the dictionary gives.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--19\", \"ques\": \"Try a Cambridge Dictionary translation and tell me which company provided the translation.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--20\", \"ques\": \"Look up the definition, pronunciation (both UK and US), and find one example sentence for the word \\\"altruism\\\" in the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--21\", \"ques\": \"Search for the word \\\"ephemeral\\\" on Cambridge Dictionary and find its translation into Spanish.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--22\", \"ques\": \"Use the Cambridge Dictionary to find the definition, UK pronunciation, and an example sentence for the word \\\"quintessential.\\\"\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--23\", \"ques\": \"Find the US English pronunciation of the word \\\"meticulous\\\" using the Cambridge Dictionary and note the International Phonetic Alphabet (IPA) notation, then find one example sentence provided in the dictionary using this word.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--24\", \"ques\": \"Look up the definition and both UK and US pronunciation of the word \\\"reverie,\\\" and provide an example sentence using the word from Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--25\", \"ques\": \"Find two different meanings of the word \\\"harmony\\\" in the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--26\", \"ques\": \"Search for the word \\\"nostalgia\\\" in the Cambridge Dictionary and report the translation of this word into Chinese.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--27\", \"ques\": \"Look up the meaning, pronunciation, and an example sentence of the word \\\"solitude\\\" using the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--28\", \"ques\": \"Search for \\\"feel giddy\\\" in Cambridge Dictionary's Thesaurus and list the synonyms the dictionary provides.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--29\", \"ques\": \"Go to the Plus section of Cambridge Dictionary, find Image quizzes and do an easy quiz about Animals and tell me your final score.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--30\", \"ques\": \"Find the grammar for present perfect simple uses in English, including examples of affirmative, negative, and interrogative sentences, on the Cambridge Dictionary website.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--31\", \"ques\": \"Look up the use of modal verbs in grammar section for expressing possibility (e.g., 'might', 'could', 'may') and find examples of their usage in sentences on the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--32\", \"ques\": \"Search for the differences between \\\"fewer\\\" and \\\"less\\\" in grammar section, and provide examples illustrating their correct usage from the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--33\", \"ques\": \"Find explanations and examples of the passive voice in Grammar on the Cambridge Dictionary website.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--34\", \"ques\": \"Use the Cambridge Dictionary to understand the rules for forming and using comparative and superlative adjectives in English Grammar, including example sentences.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--35\", \"ques\": \"Find the most common prepositions that consist of groups of words on the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--36\", \"ques\": \"Search for guidelines on using indirect speech in English, with examples of how to change direct speech to indirect speech, on the Cambridge Dictionary.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--37\", \"ques\": \"Use Cambridge Dictionary to understand the use of articles ('a', 'an', 'the') in English Grammar, including examples of usage with both countable and uncountable nouns.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--38\", \"ques\": \"Go to the Plus section of Cambridge Dictionary, finish a recommended Grammar quiz without login and tell me your final score.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--39\", \"ques\": \"Try the Word Scramble game in the Plus section, Can you beat the clock by unscrambling the letters to spell the word? (Just try the first example.)\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--40\", \"ques\": \"Look up the definition, pronunciation in UK English, and at least one example using the word 'mitigate'.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--41\", \"ques\": \"Find and browse Cambridge Dictionary Shop section, listing 3 items.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Cambridge Dictionary\", \"id\": \"Cambridge Dictionary--42\", \"ques\": \"Convert the Cambridge Dictionary homepage from English (UK) to Deutsch.\", \"web\": \"https://dictionary.cambridge.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--0\", \"ques\": \"Find a beginner-level online course about '3d printing' which lasts 1-3 months, and is provided by a renowned university.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--1\", \"ques\": \"Search for a beginner-level online course about Python programming, suitable for someone who has no programming experience on Coursera.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--2\", \"ques\": \"Find a Beginner's Spanish Specialization on Coursera and show all the courses in this Specialization.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--3\", \"ques\": \"Identify a new course or Specialization on Coursera related to Python Data Science, sort the courses by newest, what the first course is and which institution offers it.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--4\", \"ques\": \"Identify a course or Specialization on Coursera that helps business process management with with a rating 4.7.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--5\", \"ques\": \"Identify a Specialization on Coursera that teaches C++ programming for beginners, provide the name and what the learning outcomes are.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--6\", \"ques\": \"Identify a course on Coursera related to 'Artificial Intelligence for Healthcare' and note the course duration along with the number of quizzes in Assessments.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--7\", \"ques\": \"Find a course on Coursera that teaches Reinforcement Learning for Intermediate with a rating of at least 4.5. Provide the name of the course, the institution offering it, and the number of reviews it has received.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--8\", \"ques\": \"Find a free course related to 'R for Data Science' available on Coursera. Scroll to find a course with the Free tag. What language the course is taught in?\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--9\", \"ques\": \"Identify a Coursera course on artificial intelligence ethics that has a duration of less than 20 hours to complete and has been rated 4+ stars by participants.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--10\", \"ques\": \"Locate an introductory course related to artificial intelligence on Coursera, ensuring it's suitable for beginners and contains at least one module discussing Ethical Considerations.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--11\", \"ques\": \"Search for a Specialization on Coursera about project management that is produced by a university, show a testimonial for this Specialization.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--12\", \"ques\": \"Look for a Coursera course (not Specialization) that teaches Java programming basics.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--13\", \"ques\": \"Look for a Specialization on Coursera that teaches Python programming, and identify the skills you will learn by taking this Specialization.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--14\", \"ques\": \"Find a course on Coursera related to Introductory Project Management that includes modules on Agile methodology.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--15\", \"ques\": \"Find a course on Coursera named 'Introduction to Mathematical Thinking' offered by Stanford, what is the percentage (rounded) of 5 star ratings in reviews and which level has the least percentage?.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--16\", \"ques\": \"Identify a course on Coursera named 'Introduction to Finance: The Basics', who is the course instructor and what other courses does he/she teach.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--17\", \"ques\": \"How many results are there for a search on Coursera for Machine Learning, then filtered by Credit Eligible and 1-4 Years duration?\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--18\", \"ques\": \"Identify a Coursera course that teaches JavaScript, which is beginner-friendly and includes a certificate upon completion.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--19\", \"ques\": \"Identify a course on Coursera that provides an introduction to Psychology, list the instructor's name, the institution offering it, and how many hours it will approximately take to complete.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--20\", \"ques\": \"Find an Intermediate-level online course on Coursera about 'Blockchain Technology' which lasts between 1 to 4 weeks, and is provided by a well-known institution. Also, note the course's main goals and the instructor's name.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--21\", \"ques\": \"Search for an online course on Coursera about 'Digital Marketing', suitable for beginner-level learners. Specify the course duration, the main learning outcomes, and the institution offering the course.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--22\", \"ques\": \"Identify a Specialization on Coursera that focuses on 'Human Resource', list the courses included in this Specialization, and the institution offering it.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--23\", \"ques\": \"Find a course on Coursera about 'Artificial Intelligence Ethics', which has a duration of less than 5 weeks and has been rated 4.5 stars or higher. Provide the course name and the instructor's name.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--24\", \"ques\": \"Locate an online course on Coursera related to 'Sustainability' that belongs to Physical Science and Engineering subject. The course should include a module on Measuring Sustainability. Note the course duration and the offering institution.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--25\", \"ques\": \"Find a course on Coursera about 'Relativity' for beginners. List the course's main topics and the estimated time (in hours) required to complete it.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--26\", \"ques\": \"Identify a Specialization on Coursera that offers an overview of 'Renewable Energy'. The Specialization should be beginner-level and include a course on Renewable Energy Futures. Note the instructor's name and the number of weeks required to complete the course if I spend 5 hours a week.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--27\", \"ques\": \"Search for a Specialization on Coursera about 'Data Visualization' that includes a project. Provide the name of the Specialization, the institution offering it, and the skills that will be developed by completing it.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--28\", \"ques\": \"Locate a Coursera Guided project related to 'Astrophysics' suitable for advanced learners. Mention the course duration, the institution offering it, and the main subjects covered in the course.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--29\", \"ques\": \"Browse the Coursera website and find the price required for one year of Coursera Plus. How much is the discount? Then list 3 companies that work with Coursera.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--30\", \"ques\": \"Locate the course 'Modern Art & Ideas' on Coursera offered by The Museum of Modern Art. Find out the percentage (rounded) of 3-star ratings in the reviews and note which star level has the lowest percentage.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--31\", \"ques\": \"Search for the course 'Exploring Quantum Physics' on Coursera, offered by the University of Maryland, College Park. Identify the percentage (rounded) of 5-star ratings in the reviews.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--32\", \"ques\": \"Search for 'Data Analysis' courses on Coursera. Apply filters to find courses that are 'Beginner Level' and have a duration ranging from 1 to 3 months. Determine the total count of courses that match these specifications.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--33\", \"ques\": \"Find a beginner level Coursera course related to \\\"Internet of Things (IoT)\\\" with a high rating. Provide the course name, instructor's name, and a brief summary of the skills that will be taught.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--34\", \"ques\": \"Find the course on Coursera named 'Essentials of Global Health'. Determine the instructor of this course and summarize his bio, note if there are any additional courses he offers on Coursera.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--35\", \"ques\": \"Find a Coursera course on Sustainable Agriculture practices, and detail the course's objectives and the background of the lead instructor.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--36\", \"ques\": \"Browse Coursera, which universities offer Master of Advanced Study in Engineering degrees? Tell me what is the latest application deadline for this degree?\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--37\", \"ques\": \"Browse the Coursera homepage and list at least three free courses.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--38\", \"ques\": \"Browse Coursera, which universities and companies from Australia are partners of Coursera? List all of them.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--39\", \"ques\": \"Find the Space Safety course offered by TUM on Coursera. How many videos are there in module 2? What is the name of each video?\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--40\", \"ques\": \"Browse Coursera for Business and Coursera for Teams and summarise some of their advantages.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"Coursera\", \"id\": \"Coursera--41\", \"ques\": \"Browse online degrees section on Coursera and list 3 Bachelor's degree programmes.\", \"web\": \"https://www.coursera.org/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--0\", \"ques\": \"Look up the current standings for the NBA Eastern Conference on ESPN.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--1\", \"ques\": \"Check the latest articles on ESPN for updates on any trades that occurred in the NBA within the past 2 days.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--2\", \"ques\": \"Show the scores and main highlight of the Milwaukee Bucks game that took place within the last 2 days on ESPN.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--3\", \"ques\": \"Retrieve the final score from the most recent NBA game broadcast on ESPN, including the playing teams' names and the date of the match.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--4\", \"ques\": \"Check ESPN for the final scores of NBA games that were played yesterday.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--5\", \"ques\": \"Identify the top scorer in the NBA from the latest completed game and note down the points scored, the team they play for, and their position on the team.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--6\", \"ques\": \"Find the result of the latest basketball game between the Los Angeles Lakers and the Boston Celtics, including the final score and top scorer from the match.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--7\", \"ques\": \"Retrieve the final score and a brief summary of the latest NBA game played by the Los Angeles Lakers as reported on ESPN.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--8\", \"ques\": \"Find information on ESPN about the top three scoring leaders in the NBA as of the last day of the regular season, and note which teams they play for.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--9\", \"ques\": \"Search on ESPN for how many teams have Los Angeles in their name and how many of them are NBA.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--10\", \"ques\": \"Check ESPN for the score and a brief recap of the latest college football championship game.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--11\", \"ques\": \"How many NBA teams are there and list all the teams with 'New' in their name.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--12\", \"ques\": \"The first three Top Headlines in the current ESPN home page correspond to which sports leagues?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--13\", \"ques\": \"Identify today's top headline in the Basketball section of ESPN, and summarize the main points of that article.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--14\", \"ques\": \"Find the latest news about NBA trades or player movements on ESPN and report the most recent trade deal OR player acquisition.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--15\", \"ques\": \"Check the scores of the NBA games played on December 25, 2023.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--16\", \"ques\": \"Check the schedule for the NBA game on December 25, 2023, and provide the teams that are playing and their current standings in their respective conferences.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--17\", \"ques\": \"Check out the NBA Basketball Power Index 2023-24 to see which teams are in first place and which are in last place.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--18\", \"ques\": \"How many sports leagues can you choose from on the ESPN home page?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--19\", \"ques\": \"Who has the highest salary in Boston Celtics Roster 2023-24?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--20\", \"ques\": \"Look up the current leaders in rebounds and assists in the NBA Western Conference on ESPN.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--21\", \"ques\": \"Show the scores and main highlight of the Denver Nuggets game that occurred within the last 3 days on ESPN.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--22\", \"ques\": \"Find the latest Team transactions in the NBA within the past week.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--23\", \"ques\": \"Find the result of the latest basketball game between the Miami Heat and the New York Knicks, including the final score and top rebounder from the match.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--24\", \"ques\": \"Find the final score from the most recent NFL game broadcast on ESPN, including the teams' names and the date of the match.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--25\", \"ques\": \"Identify the player with the most assists in the latest NBA game and show me the assists, the team they play for, and their position.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--26\", \"ques\": \"Find information on ESPN NBA schedule. Tell me yesterday's matchups in which the loser high was higher than the winner high.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--27\", \"ques\": \"Search on ESPN for how many teams have 'Golden' in their name and how many of them are in the NHL.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--28\", \"ques\": \"How many MLB teams are there and list all the teams with 'City' in their name.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--29\", \"ques\": \"Identify today's top headline in the Soccer section of ESPN, and summarize the main points of that article.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--30\", \"ques\": \"Check out the NHL Standings 2023-24 on ESPN to see which teams are at the top and which are at the bottom in Eastern and Western Conference. What about the situation in Division.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--31\", \"ques\": \"Who has the heaviest weight among infielders in the New York Yankees Roster 2023-24?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--32\", \"ques\": \"Review yesterday's NHL game results on ESPN, focusing on teams' performance.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--33\", \"ques\": \"Locate the latest ESPN articles discussing potential MVP candidates in the NFL for 2023 season.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--34\", \"ques\": \"Visit ESPN to view the Philadelphia 76ers' latest injuries.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--35\", \"ques\": \"Browse ESPN to find out when the next game of the Los Angeles Lakers will start. Then navigate to the ticket purchasing website from ESPN, what is the cheapest ticket available.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--36\", \"ques\": \"Search for Lionel Messi's last 5 games, which teams has he played for, and what are the results?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--37\", \"ques\": \"Check out LeBron James' Stats to see how many games he has played in his career so far.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--38\", \"ques\": \"Check Los Angeles Lakers Stats 2023-24, calculate Anthony Davis' games played (GP) percentage, tell me if there are other players with the same games played percentage as Anthony Davis.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--39\", \"ques\": \"Check the New York Jets Depth Chart in the NFL section of ESPN and identify the players listed as injured in the 2ND position.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--40\", \"ques\": \"Browse the ESPN+ page from ESPN for a brief summary of what ESPN+ Tools is used for.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--41\", \"ques\": \"Find out which four teams the NFC North contains in the NFL on ESPN.\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--42\", \"ques\": \"Check out NCAAM standings on ESPN, what are the teams with equal wins and losses in the America East Conference currently?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"ESPN\", \"id\": \"ESPN--43\", \"ques\": \"Check out NCAAW recruiting on ESPN, what colleges are the top three players from?\", \"web\": \"https://www.espn.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--0\", \"ques\": \"Search for an open-source project related to 'climate change data visualization' on GitHub and report the project with the most stars.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--1\", \"ques\": \"Search for an open-source repository for machine learning in Python, specifically focused on decision trees, updated within the last 2 days.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--2\", \"ques\": \"Look for the trending Python repositories on GitHub with most stars.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--3\", \"ques\": \"Find out how much more package storage the Enterprise version has over Team in GitHub Pricing.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--4\", \"ques\": \"Find a popular JavaScript repository created in the last 30 days on GitHub with a Readme file.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--5\", \"ques\": \"Find a Python repository on GitHub that has been updated in the past 2 days and has at least 500 stars.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--6\", \"ques\": \"Search for an open-source project related to 'cryptocurrency wallet' updated in the past 30 days and provide the top three contributors.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--7\", \"ques\": \"Find the official GitHub repository for ALBERT and show me what files the repo changed in the most recent commit.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--8\", \"ques\": \"Look up the latest stable release version of Vuex and find out when it was published.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--9\", \"ques\": \"Locate a repository on GitHub that was created in the last week and has 50 or more stars. Provide brief details about the project's purpose and its programming language.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--10\", \"ques\": \"If I start using Copilot Individual, how much US dollars will it cost per year and what features does it have?\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--11\", \"ques\": \"Find a newly created open-source project on GitHub related to 'climate change' that has been initiated in January 2023; check the main programming language used and the project's description.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--12\", \"ques\": \"Retrieve the latest release from the 'electron/electron' repository on GitHub and note down the release version number and date.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--13\", \"ques\": \"Identify the latest top-trending open-source project in the category of 'Machine Learning' on GitHub, and check the number of stars it has received.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--14\", \"ques\": \"Locate the repository for the open-source project \\\"vscode\\\" and identify the top three contributors.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--15\", \"ques\": \"Locate a repository on GitHub related to 'quantum computing' that has been updated within the last week and has at least 50 stars. Provide a brief description of the project.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--16\", \"ques\": \"Find the GitHub Skill section and how many courses are under the 'First day on GitHub' heading.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--17\", \"ques\": \"Locate a C++ project on GitHub that has been recently updated in the last week and has at least 500 stars, then describe its main purpose.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--18\", \"ques\": \"Identify and report the most popular (in terms of stars) open-source image processing tool on GitHub.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--19\", \"ques\": \"Look up the most recently updated Python repository on GitHub that is tagged with 'web scraping' and has over 100 stars.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--20\", \"ques\": \"Open GitHub Copilot's FAQs to find the official answer to when Copilot chat can be used on mobile.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--21\", \"ques\": \"Find the Security topic in GitHub Resources and answer the role of GitHub Advanced Security.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--22\", \"ques\": \"Find an open-source repository on GitHub focused on natural language processing in Ruby, updated within the last week.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--23\", \"ques\": \"Find the wiki page of ohmyzsh on GitHub and tell me how to change the theme of zsh to agnoster.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--24\", \"ques\": \"Locate the GitHub repository for the open-source project \\\"angular\\\" and identify the last three issues closed.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--25\", \"ques\": \"Search for a 'virtual reality' related repository on GitHub updated in the last 10 days with at least 200 stars and summarize its main objective.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--26\", \"ques\": \"Find the Resolve merge conflicts course in GitHub Skills and what actions learners will perform in this course.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--27\", \"ques\": \"Find a Ruby repository on GitHub that has been updated in the past 3 days and has at least 1000 stars.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--28\", \"ques\": \"Identify the most starred JavaScript repositories on GitHub that were created after 2023-12-29.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--29\", \"ques\": \"Compare the maximum number of private repositories allowed in the Free and Pro plans in GitHub Pricing.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--30\", \"ques\": \"Search for an open-source project related to 'blockchain technology' on GitHub updated in the past 15 days and list the top five contributors.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--31\", \"ques\": \"Find the official GitHub repository for TensorFlow and list the files changed in the last commit. Tell me the name of changed files, total additions and total deletion.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--32\", \"ques\": \"Discover the latest C# repository on GitHub related to 'game development' and having over 150 stars, and describe its main features.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--33\", \"ques\": \"Find Customer Stories on the GitHub page and list the 2 stories that appear on the web page.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--34\", \"ques\": \"Search for an open-source project on GitHub related to 'Protein prediction' and identify the project with the highest number of forks.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--35\", \"ques\": \"Check the latest release version of React and the date it was published on GitHub.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--36\", \"ques\": \"Identify a new open-source project on GitHub related to 'AI agriculture' that created in 2022, and note its main programming language and description.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--37\", \"ques\": \"List the 3 features mentioned in GitHub's Copilot product page.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--38\", \"ques\": \"Identify and report the most popular (by stars) open-source repo related to cybersecurity on GitHub.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--39\", \"ques\": \"Browse the GitHub Trending and find out which developer is currently ranked first this month and the corresponding repository.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"GitHub\", \"id\": \"GitHub--40\", \"ques\": \"Select Sign up on the GitHub homepage to see if email 'test123@gmail.com' already exists.\", \"web\": \"https://github.com/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--0\", \"ques\": \"Book a journey with return option on same day from Edinburg to Manchester on December 28th and show me the lowest price option available.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--1\", \"ques\": \"Show me the list of one-way flights today (February 17, 2024) from Chicago to Paris.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--2\", \"ques\": \"Find the lowest fare from all eligible one-way flights for 1 adult from JFK to Heathrow on Jan. 22.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--3\", \"ques\": \"Search for the one-way flight available from Calgary to New York on Jan. 1st with the lowest carbon dioxide emissions.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--4\", \"ques\": \"Search for one-way flights from New York to London on Dec. 26th and filter the results to show only non-stop flights.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--5\", \"ques\": \"Find flights from Chicago to London on 20 December and return on 23 December.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--6\", \"ques\": \"Search for a flight on December 19 and return on December 26 from Tel Aviv to Venice and Select First Class.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--7\", \"ques\": \"Find a round trip from Phoenix to Miami (Dec. 25th - Dec. 28th), show the First Class plane tickets for me that do not exceed $1320..\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--8\", \"ques\": \"Search a one-way filght from Dublin To Athens Greece for 1 Adult that leaves on December 30 and analyse the price graph for the next 2 months.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--9\", \"ques\": \"Find a one way economy flight from Pune to New York in Jan. 15th and show me how long it will take for flight transfer.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--10\", \"ques\": \"Locate the cheapest round-trip flights from New York to Tokyo leaving on January 25, 2024, and returning on February 15, 2024.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--11\", \"ques\": \"Compare the prices for round-trip flights from New York to Tokyo for a departure on February 10, 2024, and a return on February 24, 2024, and select the option with the least number of stops.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--12\", \"ques\": \"Find the best-priced round-trip flight from New York to London leaving on December 25, 2023, and returning on January 5, 2024, with one stop or fewer.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--13\", \"ques\": \"Find the cheapest round-trip flight option from New York City to Tokyo for a departure on January 10, 2024, and a return on January 24, 2024.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--14\", \"ques\": \"Compare flight options and find the lowest round trip fare from New York to London departing on January 10, 2024, and returning on January 17, 2024.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--15\", \"ques\": \"Compare the prices and total duration of non-stop flights from New York to Tokyo Narita Airport departing on February 12th, 2024, and returning on February 26th, 2024.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--16\", \"ques\": \"Find the cheapest one-way flight from New York to Tokyo departing on January 15, 2024, and provide the airline and total flight duration.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--17\", \"ques\": \"Find the cheapest round-trip flight from New York to Paris leaving on December 27, 2023, and returning on January 10, 2024.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--18\", \"ques\": \"Compare flight options from New York to Tokyo for a round trip leaving on January 25, 2024, and returning on February 15, 2024, for one adult. Prioritize the comparisons by the shortest travel time.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--19\", \"ques\": \"Find the cheapest one-way flight from London to Paris, departing on January 25, 2024. Include the airline, total travel time, and layovers for the chosen flight.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--20\", \"ques\": \"Book a round-trip flight from San Francisco to Berlin, departing on March 5, 2024, and returning on March 12, 2024, and find the option with the shortest total travel time.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--21\", \"ques\": \"Locate the lowest-priced one-way flight from Tokyo to Sydney for an adult, departing on February 25, 2024, and include the flight duration and number of layovers.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--22\", \"ques\": \"Find a round-trip flight from Rio de Janeiro to Los Angeles, leaving on March 15, 2024, and returning on March 22, 2024, and select the option with the least carbon dioxide emissions.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--23\", \"ques\": \"Search for a one-way flight from Mumbai to Vancouver on February 28, 2024, filtering the results to show only 1-stop flights.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--24\", \"ques\": \"Compare prices for economy class round-trip flights from Dubai to Rome, departing on March 1, 2024, and returning on March 8, 2024, and select the option with the fewest stops.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--25\", \"ques\": \"Find a one-way business class flight from Buenos Aires to Amsterdam on March 10, 2024, and provide the details of the flight with the shortest duration.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--26\", \"ques\": \"Search for the cheapest round-trip flights from Bangkok to Madrid, leaving on February 26, 2024, and returning on February 28, 2024, and provide options under $1000.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--27\", \"ques\": \"Locate a one-way flight from Johannesburg to Toronto on March 30, 2024, for one adult, and analyze the price trends for the following month.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--28\", \"ques\": \"Find the best-priced round-trip flight from Seattle to Paris, departing on February 27, 2024, and returning on March 1, 2024, with a maximum of one stop.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--29\", \"ques\": \"Compare the prices and total travel time of non-stop flights from Mexico City to Frankfurt, departing on March 5, 2024, and returning on March 15, 2024.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--30\", \"ques\": \"Find the most affordable one-way flight from Cape Town to Singapore, departing on March 20, 2024, and include the airline and total number of layovers.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--31\", \"ques\": \"Find a one-way economy flight from Auckland to Honolulu on March 25, 2024, browse the full page and display a flight option with the most stops.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--32\", \"ques\": \"Search for round-trip flights from Stockholm to Toronto, departing on March 3, 2024, and returning on March 10, 2024, and sort the results to find the shortest total travel time.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--33\", \"ques\": \"Find a one-way flight from Shanghai to Vancouver on February 27, 2024, and compare the options based on carbon dioxide emissions.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--34\", \"ques\": \"Compare business class flight options from Lisbon to Singapore for a one-way trip on March 15, 2024, select one of the flights and see which websites offer its booking options. Which one is the cheapest.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--35\", \"ques\": \"Find the lowest-priced one-way flight from Cairo to Montreal on February 21, 2024, including the total travel time and number of stops.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--36\", \"ques\": \"Search for round-trip flights from Helsinki to New Delhi, departing on March 28, 2024, and returning on April 4, 2024, and filter the results to show only flights under $1000.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--37\", \"ques\": \"Locate a round-trip flight from Buenos Aires to Beijing, leaving on February 28, 2024, and returning on March 3, 2024, check out one of the options and tell me if the airline for my return flight is the same as my departure flight.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--38\", \"ques\": \"Compare the prices and flight durations for economy class flights from Oslo to Dubai, departing on March 8, 2024, and show the options with no more than two layovers.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--39\", \"ques\": \"Find a one-way flight from Prague to a city in Japan on March 20, 2024, which city in Japan is cheaper to go to, Tokyo or a certain city in Hokkaido?\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--40\", \"ques\": \"Browse destinations on the Google Flights homepage from Seattle, look at destinations on a map, and recommend some famous places to travel that are within a reasonable distance and price.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Flights\", \"id\": \"Google Flights--41\", \"ques\": \"Choose one way business class ticket from Hong Kong to Glacier National Park on 8 March 2024, offering a 1 stop ticket.\", \"web\": \"https://www.google.com/travel/flights/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--0\", \"ques\": \"Find 5 beauty salons with ratings greater than 4.8 in Seattle, WA.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--1\", \"ques\": \"Tell me one bus stop that is nearest to the intersection of main street and Amherst street in Altavista.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--2\", \"ques\": \"Find Apple Stores close to zip code 90028\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--3\", \"ques\": \"The least amount of walking from Central Park Zoo to the Broadway Theater in New York.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--4\", \"ques\": \"Plan a trip from Boston Logan Airport to North Station.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--5\", \"ques\": \"Search for a parking garage near Thalia Hall in Chicago that isn't open 24 hours.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--6\", \"ques\": \"Find all Uniqlo locations in Chicago, IL.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--7\", \"ques\": \"Find bus stops in Alanson, MI\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--8\", \"ques\": \"Find a place to climb within 2 miles of zip code 90028.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--9\", \"ques\": \"Find the art gallery that is nearest to Los Angeles Hindu Temple.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--10\", \"ques\": \"Search for a park in the state of California called Castle Mountains National Monument and find out it's Basic Information.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--11\", \"ques\": \"Locate a large store in Washington that has kids' and maternity products, also check if it has a parking lot.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--12\", \"ques\": \"Find 5 places that serve burgers near 44012 zip code and sort these 5 places by highest rating.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--13\", \"ques\": \"Find a parking lot in Gloucester and book a ride from there to North Plymouth, view the map to understand the route better.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--14\", \"ques\": \"Find motorcycle parking near Radio City Music Hall.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--15\", \"ques\": \"Find daytime only parking nearest to Madison Square Garden. Summarize what people are saying about it. \", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--16\", \"ques\": \"Find EV charging supported parking closest to Smithsonian museum.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--17\", \"ques\": \"Search for locksmiths open now but not open 24 hours in Texas City.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--18\", \"ques\": \"Find a route between Chicago to Los Angeles, then print the route details.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--19\", \"ques\": \"I will arrive Pittsburgh Airport soon. Provide the name of the Hilton hotel closest to the airport. Then, tell me the the walking time to the nearest supermarket from the hotel.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--20\", \"ques\": \"Find Tesla Destination Charger closest to the National Air and Space Museum.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--21\", \"ques\": \"Identify the nearest bus stop to the corner of Elm Street and Oak Street in Massachusetts.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--22\", \"ques\": \"Find a Best Buy store near zip code 33139.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--23\", \"ques\": \"Determine the shortest walking route from The Metropolitan Museum of Art to Times Square in New York.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--24\", \"ques\": \"Plan a journey from San Francisco International Airport to Union Square via driving.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--25\", \"ques\": \"Search for a parking facility near the Fox Theater in Detroit that closes at night.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--26\", \"ques\": \"Search for Los Angeles on Google Map, try to print the map as PDF and summarize the information on the map.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--27\", \"ques\": \"Locate the Target stores in Atlanta, GA. How many results are shown on the map.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--28\", \"ques\": \"Find the search settings for Google Map, what options are shown on that page?\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--29\", \"ques\": \"Identify bus stops in Ypsilanti, MI, list three of them.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--30\", \"ques\": \"Locate a parking lot near the Brooklyn Bridge that open 24 hours. Review the user comments about it.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--31\", \"ques\": \"First search New York's Central Park Zoo on Google Map, and then find the way to share the map. What is the generated sharing link?\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--32\", \"ques\": \"Search for plumbers available now but not open 24 hours in Orlando, FL.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--33\", \"ques\": \"Check out Denver International Airport's information and tell me: 1) which level has the least proportion in reviews; 2) what are its Accessibility and Amenities.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--34\", \"ques\": \"Find a hiking trail within 2 miles of zip code 80202.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--35\", \"ques\": \"Search for a natural reserve in Texas called Big Bend National Park and gather its Basic Information.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--36\", \"ques\": \"Identify 5 restaurants serving pizza near the 30309 zip code and rank them by their ratings.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--37\", \"ques\": \"Locate a parking area in Salem and find a route from there to Marblehead, including map directions for better understanding.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--38\", \"ques\": \"Search for bicycle parking near the Empire State Building.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--39\", \"ques\": \"Find a route from Miami to New Orleans, and provide the detailed route information.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Map\", \"id\": \"Google Map--40\", \"ques\": \"Find a restaurant in Boston that eats Boston lobster and asks for a rating of 4.6 or higher, and check out what a one-star review says.\", \"web\": \"https://www.google.com/maps/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--0\", \"ques\": \"Find the initial release date for Guardians of the Galaxy Vol. 3 the movie.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--1\", \"ques\": \"Find Kevin Durant's bio\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--2\", \"ques\": \"Search for the latest news title about the NBA team the Los Angeles Lakers.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--3\", \"ques\": \"Show me a list of comedy movies, sorted by user ratings. Show me the Top 5 movies.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--4\", \"ques\": \"Show most played games in Steam. And tell me the number of players in In game at this time\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--5\", \"ques\": \"find the score of the latest nba game played by the phoenix suns.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--6\", \"ques\": \"Browse the monthly trending searches in Columbus.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--7\", \"ques\": \"Find the software requirements for iPhones that support AirDrop's ability to continue transmitting over the web when out of range.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--8\", \"ques\": \"Find the video on YouTube: 'Oscars 2023: Must-See Moments!'. Tell me who the first comment displayed under that video belongs to, and how many thumbs up and replies it has.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--9\", \"ques\": \"Show the rating of Prometheus movie on IMDb and Rotten Tomatoes.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--10\", \"ques\": \"Find the no. 1 weekly charts ranked artist based on Billboard and tell me 10 most played song by this artist until now.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--11\", \"ques\": \"According to FlightAware, tell me the busiest airport last week and its total arrivals and departures last week.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--12\", \"ques\": \"Find the year that Tom Brady had the most touchdowns in a single seasson.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--13\", \"ques\": \"What are Jerry Trainor's upcoming projects?\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--14\", \"ques\": \"Find the retired players the year before last named James Smith and tell me which club he has been a member of from 2020\\u20132021.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--15\", \"ques\": \"Please try to log in to twitter with email: webagenttest@testmail.com and password: test123456. Let me know if the login was successful.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--16\", \"ques\": \"How many members are there in the OpenAI community on Reddit, and what is the hottest news right now?\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--17\", \"ques\": \"Tell me the names of Trump's kids\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--18\", \"ques\": \"When and where the most recent World Cup was held, and which team was the winner?\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--19\", \"ques\": \"What are the first 7 bits of the SHA of the Bert's latest commit on GitHub, and what exactly was changed in that commit.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--20\", \"ques\": \"Find the release date for the latest \\\"Fast & Furious\\\" movie.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--21\", \"ques\": \"Show a list of the top 5 highest-grossing animated movies, sorted by box office earnings.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--22\", \"ques\": \"Browse and list the top three trending topics this month in New York City.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--23\", \"ques\": \"Retrieve a short biography of LeBron James.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--24\", \"ques\": \"What is the name of the star system closest to the Solar System, and what are the discovered planets in it?\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--25\", \"ques\": \"Get the latest news headline about the English Premier League football club Manchester United.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--26\", \"ques\": \"Identify the hardware requirements for using the latest version of Adobe Photoshop on a Mac.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--27\", \"ques\": \"Check the current air quality index in Paris.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--28\", \"ques\": \"Check the IMDb and Metacritic scores of the movie \\\"Inception.\\\"\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--29\", \"ques\": \"Find out the current world record for the men's 100m sprint.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--30\", \"ques\": \"Find the current number one artist on the Spotify Global Top 50 chart and list his/her top 10 songs as of now.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--31\", \"ques\": \"Discover which year Cristiano Ronaldo scored the most goals in a single season.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--32\", \"ques\": \"Find out where and when the most recent UEFA Champions League final was held, and which team won.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--33\", \"ques\": \"Find and copy the SHA of the latest commit in the TensorFlow repository on GitHub, then find a textbox to paste and tell me what the SHA is.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--34\", \"ques\": \"Determine the distance from Earth to Mars as of today's date.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--35\", \"ques\": \"Look up the latest research paper related to black holes published in the journal \\\"Nature Astronomy\\\".\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--36\", \"ques\": \"Search for the most recent Nobel Prize winner in Physics and their contribution to the field.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--37\", \"ques\": \"Find the current top 3 super-earth planets and give a brief introduction to them.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--38\", \"ques\": \"Search for the next visible solar eclipse in North America and its expected date, and what about the one after that.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--39\", \"ques\": \"Identify the top-10 trending travel destination for 2024 through a blog, how many of them are in Asian.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--40\", \"ques\": \"Look up the elevation of Mount Kilimanjaro on Google Search.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--41\", \"ques\": \"Look up the current statistics of air pollution level in Los Angeles using Google Search.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Google Search\", \"id\": \"Google Search--42\", \"ques\": \" Use Google Search to find an article that explains the major differences between American English and British English.\", \"web\": \"https://www.google.com/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--0\", \"ques\": \"Find a pre-trained natural language processing model on Hugging Face that can perform sentiment analysis, and make sure the model's last update is within March 2023.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--1\", \"ques\": \"Use the Huggingface Inference API to generate a short story about a dragon and a wizard.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--2\", \"ques\": \"Discover three new and popular open-source NLP models for language translation released in the past month on Huggingface.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--3\", \"ques\": \"Look up a model with a license of cc-by-sa-4.0 with the most likes on Hugging face.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--4\", \"ques\": \"Locate an open-source conversational AI model on Hugging Face, trained in English and list its main features and applications.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--5\", \"ques\": \"Find a model released on Hugging Face for recipe generation. Retrieve the information of the model, including its name, model size and tensor type.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--6\", \"ques\": \"Find the model sentence-transformers/all-MiniLM-L6-v2 and use the Inference API on the webpage to get the similarity of the following two sentences: 'Tomorrow is Sunday', 'Eat a burger on Sunday'.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--7\", \"ques\": \"Which is the most downloaded audio related dataset on Hugging face currently.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--8\", \"ques\": \"Retrieve an example of a pre-trained language model in natural language processing and identify the tasks it is specifically designed for, like translation or text summarization.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--9\", \"ques\": \"Find the most download machine translation model on Huggingface which focuses on English and Japanese (en-ja) and report the evaluation metrics stated for it.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--10\", \"ques\": \"Open space: argilla/notux-chat-ui and interact with it by asking it 'which team trained you'. What is its answer.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--11\", \"ques\": \"Identify the latest updated image to video model available on Huggingface and summarize its main features.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--12\", \"ques\": \"Find the most recently updated machine learning model on Huggingface which focuses on Error Correction.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--13\", \"ques\": \"Search for LLaMA in the huggingface doc, what type is the spaces_between_special_tokens parameter in LlamaTokenizer and what is its default value.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--14\", \"ques\": \"How much is the Pro account of Hugging face for a month and what are the features?\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--15\", \"ques\": \"Identify the most downloaded models on Hugging face that use the PaddlePaddle library.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--16\", \"ques\": \"Find information on the latest (as of today's date) pre-trained language model on Huggingface suitable for text classification and briefly describe its intended use case and architecture.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--17\", \"ques\": \"Find the most recently updated open-source project related to natural language processing on the Huggingface platform. Provide the project's name, creator, and a brief description of its functionality.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--18\", \"ques\": \"Look up TRL's forward modelling in the hugging face documentation on how to add a margin to a loss.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--19\", \"ques\": \"Explore and summarize the features of the most recent open-source NLP model released by Hugging Face for English text summarization.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--20\", \"ques\": \"Locate a pre-trained natural language processing model on Hugging Face that specializes in named entity recognition (NER), confirm that the model was last updated in 2022 and has 1M+ downloads.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--21\", \"ques\": \"Look up the tour about how to use the 'pipeline' feature in the Hugging Face Transformers library for sentiment analysis, and identify the default model it uses.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--22\", \"ques\": \"Identify the steps to convert a PyTorch model to TensorFlow using the Hugging Face Transformers library as described in their documentation.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--23\", \"ques\": \"Identify three innovative and widely recognized open-source NLP models for automatic speech recognition released in the past month on Huggingface.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--24\", \"ques\": \"Search for a model on Hugging Face with an Apache-2.0 license that has received the highest number of likes.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--25\", \"ques\": \"In the Hugging Face documentation, find the tutorial on loading adapters with PEFT, tell me how to load in 8bit or 4bit.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--26\", \"ques\": \"Identify a model on Hugging Face designed for generating travel chats. Obtain information about the model, including its name, size and training framwork.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--27\", \"ques\": \"Determine the most downloaded dataset related to Text Retrieval in NLP on Hugging Face.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--28\", \"ques\": \"Retrieve an example of a pre-trained model on Hugging Face that is optimized for question answering tasks and detail the languages it supports.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--29\", \"ques\": \"Summarize the description of the recent open-source NLP model released on Hugging Face for medical summarization.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--30\", \"ques\": \"Identify the most downloaded English-Chinese (en-zh) machine translation model on Huggingface and report its latest performance metrics and usage guidelines.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--31\", \"ques\": \"Identify the latest machine learning model on Huggingface that specializes in detecting fake news, including the date of its last update.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--32\", \"ques\": \"On the Hugging Face website, search for the model 'GPT-J-6B' and find the 'temperature' parameter in its settings. What is the default value of this parameter?\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--33\", \"ques\": \"List three hugging face docs. How many GitHub stars have they earned so far?\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--34\", \"ques\": \"List the benefits of hugging face classroom mentioned on Hugging face website.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--35\", \"ques\": \"Find the latest Diffusion-related blog on Hugging Face, and read its intro or overview section to roughly summarize the content of the blog.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--36\", \"ques\": \"Summarize all the payment plans and their advantages in huggingface pricing.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--37\", \"ques\": \"Browse the daily paper on Hugging Face. What is the title of the first article, how many upvotes has it received, and is there any related model or data release?\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--38\", \"ques\": \"Investigate the 'transformers' library in the Hugging Face documentation, focusing on how to add new tokens to a tokenizer.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--39\", \"ques\": \"Investigate in the Hugging Face documentation how to utilize the 'Trainer' API for training a model on a custom dataset, and note the configurable parameters of the Trainer class.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--40\", \"ques\": \"Check out Text Embeddings Inference in Hugging face's Doc to summarise the strengths of the toolkit.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--41\", \"ques\": \"What is the current Text-to-3D model with the highest number of downloads and tell me are there Spaces that use the model.\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Huggingface\", \"id\": \"Huggingface--42\", \"ques\": \"Check the Dataset Viewer for ai2lumos/lumos_complex_qa_plan_onetime on Hugging face. what is the content corresponding to user in the first message?\", \"web\": \"https://huggingface.co/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--0\", \"ques\": \"derivative of x^2 when x=5.6\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--1\", \"ques\": \"Give a constraint on the set of inequalities for the inner region of the pentagram.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--2\", \"ques\": \"Calculate 3^71 and retain 5 significant figures in scientific notation.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--3\", \"ques\": \"Let g(x) be the integral of x^2 cos(2x). Write the expression of g(x).\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--4\", \"ques\": \"Pack 24 circles in a circle radius r. Compare Densest known packing and Square packing. Then tell me the radius of the inner circles.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--5\", \"ques\": \"Show the solution of y\\\"(z) + sin(y(z)) = 0 from wolframalpha.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--6\", \"ques\": \"Simplify x^5-20x^4+163x^3-676x^2+1424x-1209 so that it has fewer items.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--7\", \"ques\": \"Give the final angle and final length after 6s of a Spring pendulum with spring equilibrium length=0.12m, initial length=0.24m, initial angle=80deg, mass=1kg, spring constant=120 N/m .\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--8\", \"ques\": \"Give 12 lbs of 4-cyanoindole, converted to molar and indicate the percentage of C, H, N.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--9\", \"ques\": \"Annual energy production of Diablo Canyon 2 in 2010.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--10\", \"ques\": \"Give the geomagnetic field on June 20, 2023 in Oslo.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--11\", \"ques\": \"Show the electrical resistivity of UNS A92024 and UNS G10800 at 20 degrees Celsius.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--12\", \"ques\": \"Which character in unicode 8900 to 8920 looks like a snowflake\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--13\", \"ques\": \"What is 10,000 US dollars worth now in 1980 and in 1970?\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--14\", \"ques\": \"Compare the total Calories: whopper vs baconator vs big mac. Assume that each serving of food is 300g.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--15\", \"ques\": \"Show the blood relationship fraction between you and your father's mother's sister's son.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--16\", \"ques\": \"Weight lose for a male with current weight 90 kg, 40 year old, 175 cm. If he intakes 1500 calories every day, how long will it take to lose 17 kg.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--17\", \"ques\": \"Show the average price of movie ticket in Providence, Nashville, Boise in 2023.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--18\", \"ques\": \"Plot Albert Einstein curve with Parametric equations.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--19\", \"ques\": \"Standing in the sun from 11:00 am with SPF 5 in Australia. Approximate time to sunburn for each skin type.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--20\", \"ques\": \"Compute the integral of 3e^(2x) from x=0 to x=5.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--21\", \"ques\": \"Calculate (1+0.1*i)^8 + (1\\u22120.2*i)^8  where i is a complex number.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--22\", \"ques\": \"Determine the area of a regular hexagon with a side length of 7 cm.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--23\", \"ques\": \"Calculate the population growth rate of Canada from 2020 to 2023 using Wolfram Alpha.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--24\", \"ques\": \"Solve the differential equation y''(t) - 2y'(t) + 10y(t) = 0 and display its general solution.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--25\", \"ques\": \"Calculate the final position and velocity of a projectile launched at 45 degrees with an initial speed of 30 m/s after 3 seconds.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--26\", \"ques\": \"Convert 15 kilograms of sulfuric acid to moles and display the percentage composition of H, S, and O by weight.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--27\", \"ques\": \"Display the thermal conductivity of Copper (Cu) and Aluminum (Al) at 25 degrees Celsius.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--28\", \"ques\": \"Identify the character in Unicode range 9632 to 9650 that represents a hollow parallelogram.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--29\", \"ques\": \"Create a plot of cat curve using wolfram alpha.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--30\", \"ques\": \"Calculate the estimated time to sunburn for different skin types when exposed to the sun at 1:00 pm with SPF 1 in Brazil.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--31\", \"ques\": \"Using Wolfram Alpha, determine the current temperature and wind speed in Chicago, IL.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--32\", \"ques\": \"Print all prime numbers between 1000 and 1200 using Wolfram alpha.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--33\", \"ques\": \"Identify the electrical energy output of a hydroelectric power plant named Itaipu Dam in 2023 using Wolfram Alpha.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--34\", \"ques\": \"Calculate the mass of Jupiter compared to Earth using Wolfram Alpha. Also, find the length of one day on Jupiter.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--35\", \"ques\": \"Calculate the determinant of a 6x6 Hilbert matrix.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--36\", \"ques\": \"Determine the convergence or divergence of the series \\u03a3 (n=1 to \\u221e) of 1/(n^3 + 1).\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--37\", \"ques\": \"How many days are there between February 12, 2024 and August 9, 2050?\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--38\", \"ques\": \"Compute the length of a curve defined by y = 2x^3 - 3x^2 + 4x - 5 from x = 0 to x = 3.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--39\", \"ques\": \"Use Wolfram alpha to write the expression of the ellipse x^2 + 3 y^2 = 4 rotated 33 degrees counterclockwise.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--40\", \"ques\": \"Approximate amount of fat burned by a 28yo, 172cm tall, 70kg woman running for 30min at a pace of 6min/mile.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--41\", \"ques\": \"What is the approximate Heart Rate Reserve of a 50 year old man who has a heart rate of 60bpm at rest.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--42\", \"ques\": \"What is the raw memory of a 100.2\\\" * 123.5\\\" true colour picture at 72 ppi?\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--43\", \"ques\": \"A polyominoes of order 6 means you have 6 identical squares to combine different shapes (2-sided). How many combinations are there? Looking at all the shapes in the result, how many of them have only 2 rows in total?\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--44\", \"ques\": \"Solve the ODE, g' + cos(g) = 0, if there is a constant in the result, determine the value of the constant by the condition that g(0) = 1.\", \"web\": \"https://www.wolframalpha.com/\"}\n{\"web_name\": \"Wolfram Alpha\", \"id\": \"Wolfram Alpha--45\", \"ques\": \"A 175cm tall, 85kg, 40yo man climbs 2500 steps at about 18cm per step and 40 steps per minute. summarise the Metabolic properties.\", \"web\": \"https://www.wolframalpha.com/\"}"
  },
  {
    "path": "packages/evals/env.ts",
    "content": "/**\n * Determine the current environment in which the evaluations are running:\n * - BROWSERBASE or LOCAL\n *\n * The environment is read from the EVAL_ENV environment variable.\n */\nexport const env: \"BROWSERBASE\" | \"LOCAL\" =\n  process.env.EVAL_ENV?.toLowerCase() === \"browserbase\"\n    ? \"BROWSERBASE\"\n    : \"LOCAL\";\n"
  },
  {
    "path": "packages/evals/evals.config.json",
    "content": "{\n  \"defaults\": {\n    \"env\": \"local\",\n    \"trials\": 3,\n    \"concurrency\": 10,\n    \"provider\": null,\n    \"model\": null,\n    \"api\": false\n  },\n  \"benchmarks\": {\n    \"webbench\": {\n      \"limit\": 25,\n      \"filters\": [\"difficulty\", \"category\", \"use_hitl\"]\n    },\n    \"gaia\": {\n      \"limit\": 25,\n      \"filters\": [\"level\"]\n    },\n    \"webvoyager\": {\n      \"limit\": 25\n    },\n    \"osworld\": {\n      \"limit\": 25,\n      \"filters\": [\"source\", \"evaluation_type\"],\n      \"timeout\": 60000\n    },\n    \"onlineMind2Web\": {\n      \"limit\": 25\n    },\n    \"webtailbench\": {\n      \"limit\": 25\n    }\n  },\n  \"tasks\": [\n    {\n      \"name\": \"history\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"extract_repo_name\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"amazon_add_to_cart\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"instructions\",\n      \"categories\": [\"regression\", \"combination\"]\n    },\n    {\n      \"name\": \"bidnet\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"ionwave\",\n      \"categories\": [\"act\", \"regression\"]\n    },\n    {\n      \"name\": \"nonsense_action\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"peeler_simple\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"simple_google_search\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"vantechjournal\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"wikipedia\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"allrecipes\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"arxiv\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"extract_collaborators\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"extract_github_commits\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"imdb_movie_details\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"peeler_complex\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"sciquest\",\n      \"categories\": [\"combination\"]\n    },\n    {\n      \"name\": \"wichita\",\n      \"categories\": [\"combination\", \"regression\"]\n    },\n    {\n      \"name\": \"hn_aisdk\",\n      \"categories\": [\"llm_clients\"]\n    },\n    {\n      \"name\": \"hn_langchain\",\n      \"categories\": [\"llm_clients\"]\n    },\n    {\n      \"name\": \"hn_customOpenAI\",\n      \"categories\": [\"llm_clients\"]\n    },\n    {\n      \"name\": \"apple\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"combination_sauce\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"costar\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"extract_aigrant_companies\",\n      \"categories\": [\"regression\"]\n    },\n    {\n      \"name\": \"extract_capacitor_info\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"extract_partners\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"extract_press_releases\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"extract_snowshoeing_destinations\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"homedepot\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"rakuten_jp\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"stock_x\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"ted_talk\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"extract_baptist_health\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_github_stars\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_memorial_healthcare\",\n      \"categories\": [\"extract\", \"regression\"]\n    },\n    {\n      \"name\": \"extract_nhl_stats\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_professional_info\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_csa\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_resistor_info\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_rockauto\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_staff_members\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"ionwave_observe\",\n      \"categories\": [\"observe\"]\n    },\n    {\n      \"name\": \"panamcs\",\n      \"categories\": [\"observe\"]\n    },\n    {\n      \"name\": \"vanta_h\",\n      \"categories\": [\"experimental\"]\n    },\n    {\n      \"name\": \"extract_area_codes\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_public_notices\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_jstor_news\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_apartments\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_zillow\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"observe_github\",\n      \"categories\": [\"observe\", \"regression\"]\n    },\n    {\n      \"name\": \"observe_vantechjournal\",\n      \"categories\": [\"observe\", \"regression\"]\n    },\n    {\n      \"name\": \"observe_amazon_add_to_cart\",\n      \"categories\": [\"observe\"]\n    },\n    {\n      \"name\": \"observe_simple_google_search\",\n      \"categories\": [\"observe\"]\n    },\n    {\n      \"name\": \"observe_yc_startup\",\n      \"categories\": [\"observe\"]\n    },\n    {\n      \"name\": \"observe_taxes\",\n      \"categories\": [\"observe\"]\n    },\n    {\n      \"name\": \"observe_iframes1\",\n      \"categories\": [\"regression\", \"observe\"]\n    },\n    {\n      \"name\": \"observe_iframes2\",\n      \"categories\": [\"regression\", \"observe\"]\n    },\n    {\n      \"name\": \"extract_hamilton_weather\",\n      \"categories\": [\"targeted_extract\", \"regression\"]\n    },\n    {\n      \"name\": \"extract_regulations_table\",\n      \"categories\": [\"targeted_extract\"]\n    },\n    {\n      \"name\": \"extract_recipe\",\n      \"categories\": [\"targeted_extract\"]\n    },\n    {\n      \"name\": \"extract_aigrant_targeted\",\n      \"categories\": [\"targeted_extract\"]\n    },\n    {\n      \"name\": \"extract_aigrant_targeted_2\",\n      \"categories\": [\"targeted_extract\"]\n    },\n    {\n      \"name\": \"extract_geniusee\",\n      \"categories\": [\"targeted_extract\"]\n    },\n    {\n      \"name\": \"extract_geniusee_2\",\n      \"categories\": [\"targeted_extract\"]\n    },\n    {\n      \"name\": \"scroll_50\",\n      \"categories\": [\"regression\", \"act\"]\n    },\n    {\n      \"name\": \"scroll_75\",\n      \"categories\": [\"regression\", \"act\"]\n    },\n    {\n      \"name\": \"next_chunk\",\n      \"categories\": [\"regression\", \"act\"]\n    },\n    {\n      \"name\": \"prev_chunk\",\n      \"categories\": [\"regression\", \"act\"]\n    },\n    {\n      \"name\": \"google_flights\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"extract_jfk_links\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"extract_single_link\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"dropdown\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"radio_btn\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"checkboxes\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"agent/iframe_form\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/iframe_form_multiple\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/google_flights\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/github_react_version\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/steam_games\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/ubereats\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/kith\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/apple_tv\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/apple_trade_in\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/arxiv_gpt_report\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/sf_library_card\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/sf_library_card_multiple\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/hugging_face\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/google_maps_3\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"login\",\n      \"categories\": [\"act\", \"regression\"]\n    },\n    {\n      \"name\": \"iframe_hn\",\n      \"categories\": [\"extract\"]\n    },\n    {\n      \"name\": \"iframe_same_proc\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"iframe_form_filling\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"iframes_nested\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"no_js_click\",\n      \"categories\": [\"act\", \"regression\"]\n    },\n    {\n      \"name\": \"tab_handling\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"agent/kayak\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"multi_tab\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"shadow_dom\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"os_dropdown\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"custom_dropdown\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"hidden_input_dropdown\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"nested_iframes_2\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"heal_scroll_50\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"heal_simple_google_search\",\n      \"categories\": [\"regression\", \"act\"]\n    },\n    {\n      \"name\": \"heal_custom_dropdown\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"agent/trivago\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/google_maps\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/google_maps_2\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/sign_in\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"osr_in_oopif\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"csr_in_oopif\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"csr_in_spif\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"csr_in_spif\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"spif_in_osr\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"oopif_in_osr\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"spif_in_csr\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"oopif_in_csr\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"osr_in_spif\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"namespace_xpath\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"iframe_scroll\",\n      \"categories\": [\"act\"]\n    },\n    {\n      \"name\": \"agent/gaia\",\n      \"categories\": [\"external_agent_benchmarks\"]\n    },\n    {\n      \"name\": \"agent/webvoyager\",\n      \"categories\": [\"external_agent_benchmarks\"]\n    },\n    {\n      \"name\": \"agent/nba_trades\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/hotel_booking\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/github\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/all_recipes\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/onlineMind2Web\",\n      \"categories\": [\"external_agent_benchmarks\"]\n    },\n    {\n      \"name\": \"agent/webtailbench\",\n      \"categories\": [\"external_agent_benchmarks\"]\n    },\n    {\n      \"name\": \"agent/alibaba_supplier_search\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/amazon_shoes_cart\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/columbia_tuition\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/flipkart_laptops\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/google_shopping\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/hotels_paris_amenities\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/instacart_organic_bananas\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/kfc_tenders_combo\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/made_in_china_supplier\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/nvidia_hgx_driver\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/oed_word_search\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/radiotimes_tv_schedule\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/redfin_apartment_rental\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/thegamer_opinion_article\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/trailhead_superbadge\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/trustpilot_hr_companies\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/uniqlo_mens_blazers\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/webmd_audiologist_search\",\n      \"categories\": [\"agent\"]\n    },\n    {\n      \"name\": \"agent/webmd_ovulation_calculator\",\n      \"categories\": [\"agent\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/evals/index.eval.ts",
    "content": "/**\n * This script orchestrates the running of evaluations against a set of tasks.\n * It uses Braintrust to run multiple testcases (each testcase representing a\n * given task-model combination) and then aggregates the results, producing\n * a summary of passes, failures, and categorized success rates.\n *\n * Overview:\n * - Reads a configuration file `evals.config.json` to determine what tasks (evaluations)\n *   are available and which categories they belong to.\n * - Supports filtering which tasks to run either by evaluation category or by specific task name.\n * - Supports multiple models, defaulting to certain sets of models depending on the category.\n * - Runs each selected task against each selected model in parallel, collecting results.\n * - Saves a summary of the evaluation results to `../../eval-summary.json`.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { pathToFileURL } from \"node:url\";\nimport {\n  DEFAULT_EVAL_CATEGORIES,\n  filterByCategory,\n  filterByEvalName,\n} from \"./args.js\";\nimport { generateExperimentName } from \"./utils.js\";\nimport { exactMatch, errorMatch } from \"./scoring.js\";\nimport {\n  tasksByName,\n  tasksConfig,\n  getModelList,\n  getAgentModelEntries,\n} from \"./taskConfig.js\";\nimport { Eval } from \"braintrust\";\nimport { SummaryResult, Testcase, EvalInput } from \"./types/evals.js\";\nimport { EvalLogger } from \"./logger.js\";\nimport {\n  AvailableModel,\n  LLMClient,\n  StagehandEvalError,\n  AgentProvider,\n  loadApiKeyFromEnv,\n  LogLine,\n  getAISDKLanguageModel,\n} from \"@browserbasehq/stagehand\";\nimport { AISdkClientWrapped } from \"./lib/AISdkClientWrapped.js\";\nimport { env } from \"./env.js\";\nimport { initV3 } from \"./initV3.js\";\nimport { generateSummary } from \"./summary.js\";\nimport { buildGAIATestcases } from \"./suites/gaia.js\";\nimport { buildWebVoyagerTestcases } from \"./suites/webvoyager.js\";\nimport { buildOnlineMind2WebTestcases } from \"./suites/onlineMind2Web.js\";\nimport { endBrowserbaseSession } from \"./browserbaseCleanup.js\";\nimport { buildWebTailBenchTestcases } from \"./suites/webtailbench.js\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nimport dotenv from \"dotenv\";\ndotenv.config();\n\nconst moduleDir = getCurrentDirPath();\n\n/**\n * Read max concurrency and trial count from environment variables set in args.ts.\n * Fallback to defaults (20 and 5) if they're not provided.\n */\nconst MAX_CONCURRENCY = process.env.EVAL_MAX_CONCURRENCY\n  ? parseInt(process.env.EVAL_MAX_CONCURRENCY, 10)\n  : 3;\n\nconst TRIAL_COUNT = process.env.EVAL_TRIAL_COUNT\n  ? parseInt(process.env.EVAL_TRIAL_COUNT, 10)\n  : 3;\n\nconst USE_API: boolean = (process.env.USE_API ?? \"\").toLowerCase() === \"true\";\nconsole.log(`[EVALS] USE_API: ${USE_API}`);\n\n/**\n * generateFilteredTestcases:\n * Based on the chosen filters (category or specific eval name) and environment,\n * this function generates the set of testcases to run. Each testcase is a combination\n * of a task and a model.\n *\n * Steps:\n * - Dynamically determine the list of models based on filters.\n * - Start with all combinations of tasks (from `tasksByName`) and the determined models.\n * - Filter by category if a category filter was specified.\n * - Filter by evaluation name if specified.\n * - In the BROWSERBASE environment, exclude certain tasks that are not suitable.\n */\nconst generateFilteredTestcases = (): Testcase[] => {\n  let taskNamesToRun: string[];\n  let effectiveCategory: string | null = filterByCategory; // Start with the command-line filter\n\n  if (filterByEvalName) {\n    // If a specific task name is given, that's the only one we run\n    taskNamesToRun = [filterByEvalName];\n    // Check if this single task belongs to agent-related categories to override models\n    const taskCategories = tasksByName[filterByEvalName]?.categories || [];\n    if (\n      taskCategories.length === 1 &&\n      (taskCategories[0] === \"agent\" ||\n        taskCategories[0] === \"external_agent_benchmarks\")\n    ) {\n      // Treat this run as an agent category run for model selection\n      effectiveCategory = taskCategories[0];\n      console.log(\n        `Task ${filterByEvalName} is in ${taskCategories[0]} category, using agent models.`,\n      );\n    }\n  } else if (filterByCategory) {\n    // If filtering by category, get all tasks in that category\n    taskNamesToRun = Object.keys(tasksByName).filter((name) =>\n      tasksByName[name].categories.includes(filterByCategory!),\n    );\n  } else {\n    // If no specific task or category filter, run tasks from default categories\n    taskNamesToRun = Object.keys(tasksByName).filter((name) =>\n      DEFAULT_EVAL_CATEGORIES.some((category) =>\n        tasksByName[name].categories.includes(category),\n      ),\n    );\n  }\n\n  // Dynamically determine the MODELS based on the effective category\n  const currentModels = getModelList(effectiveCategory);\n\n  console.log(\n    `Using models for this run (${effectiveCategory || \"default\"}):`,\n    currentModels,\n  );\n\n  // Check for dataset filter from environment\n  const datasetFilter = process.env.EVAL_DATASET;\n\n  // Special handling: fan out GAIA dataset for agent/gaia\n  const isGAIATaskIncluded = taskNamesToRun.includes(\"agent/gaia\");\n  // Special handling: fan out WebVoyager dataset for agent/webvoyager\n  const isWebVoyagerTaskIncluded = taskNamesToRun.includes(\"agent/webvoyager\");\n  // Special handling: fan out Mind2Web dataset for agent/onlineMind2Web\n  const isMind2WebTaskIncluded = taskNamesToRun.includes(\n    \"agent/onlineMind2Web\",\n  );\n\n  let allTestcases: Testcase[] = [];\n\n  // Only include GAIA if no dataset filter or if gaia is selected\n  if (isGAIATaskIncluded && (!datasetFilter || datasetFilter === \"gaia\")) {\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/gaia\");\n    allTestcases.push(...buildGAIATestcases(currentModels));\n  } else if (isGAIATaskIncluded && datasetFilter && datasetFilter !== \"gaia\") {\n    // Remove GAIA from tasks to run if dataset filter excludes it\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/gaia\");\n  }\n\n  // Only include WebVoyager if no dataset filter or if webvoyager is selected\n  if (\n    isWebVoyagerTaskIncluded &&\n    (!datasetFilter || datasetFilter === \"webvoyager\")\n  ) {\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/webvoyager\");\n    allTestcases.push(...buildWebVoyagerTestcases(currentModels));\n  } else if (\n    isWebVoyagerTaskIncluded &&\n    datasetFilter &&\n    datasetFilter !== \"webvoyager\"\n  ) {\n    // Remove WebVoyager from tasks to run if dataset filter excludes it\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/webvoyager\");\n  }\n\n  // Only include Mind2Web if no dataset filter or if onlineMind2Web is selected\n  if (\n    isMind2WebTaskIncluded &&\n    (!datasetFilter || datasetFilter === \"onlineMind2Web\")\n  ) {\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/onlineMind2Web\");\n    allTestcases.push(...buildOnlineMind2WebTestcases(currentModels));\n  } else if (\n    isMind2WebTaskIncluded &&\n    datasetFilter &&\n    datasetFilter !== \"onlineMind2Web\"\n  ) {\n    // Remove Mind2Web from tasks to run if dataset filter excludes it\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/onlineMind2Web\");\n  }\n\n  // Special handling: fan out WebTailBench dataset for agent/webtailbench\n  const isWebTailBenchTaskIncluded =\n    taskNamesToRun.includes(\"agent/webtailbench\");\n\n  if (\n    isWebTailBenchTaskIncluded &&\n    (!datasetFilter || datasetFilter === \"webtailbench\")\n  ) {\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/webtailbench\");\n    allTestcases.push(...buildWebTailBenchTestcases(currentModels));\n  } else if (\n    isWebTailBenchTaskIncluded &&\n    datasetFilter &&\n    datasetFilter !== \"webtailbench\"\n  ) {\n    taskNamesToRun = taskNamesToRun.filter((t) => t !== \"agent/webtailbench\");\n  }\n\n  // Create a list of all remaining testcases using the determined task names and models\n  const isAgentCategory =\n    effectiveCategory === \"agent\" ||\n    effectiveCategory === \"external_agent_benchmarks\";\n\n  // Use agent model entries (with cua flag) for agent categories, otherwise map currentModels\n  const modelEntries = isAgentCategory\n    ? getAgentModelEntries()\n    : currentModels.map((m) => ({ modelName: m, cua: false }));\n\n  const regularTestcases = modelEntries.flatMap((entry) =>\n    taskNamesToRun.map((testName) => ({\n      input: {\n        name: testName,\n        modelName: entry.modelName as AvailableModel,\n        ...(isAgentCategory && { isCUA: entry.cua }),\n      },\n      name: testName,\n      tags: [\n        entry.modelName,\n        ...(isAgentCategory ? [entry.cua ? \"cua\" : \"agent\"] : []),\n        testName,\n        ...(tasksConfig.find((t) => t.name === testName)?.categories || []).map(\n          (x) => `category/${x}`,\n        ),\n      ],\n      metadata: {\n        model: entry.modelName as AvailableModel,\n        test: testName,\n      },\n      expected: true,\n    })),\n  );\n\n  allTestcases = [...allTestcases, ...regularTestcases];\n\n  // This filtering step might now be redundant if taskNamesToRun is already filtered\n  if (filterByCategory) {\n    allTestcases = allTestcases.filter((testcase) =>\n      tasksByName[testcase.name].categories.includes(filterByCategory!),\n    );\n  }\n\n  // If running in BROWSERBASE environment, exclude tasks that are not applicable.\n  if (env === \"BROWSERBASE\") {\n    allTestcases = allTestcases.filter(\n      (testcase) => ![\"peeler_simple\", \"stock_x\"].includes(testcase.name),\n    );\n  }\n\n  console.log(\n    \"Final test cases to run:\",\n    allTestcases\n      .map(\n        (t, i) =>\n          `${i}: ${t.name} (${t.input.modelName}): ${tasksByName[t.name].categories}`,\n      )\n      .join(\"\\n\"),\n  );\n\n  return allTestcases;\n};\n\n/**\n * Main execution block:\n * - Determine experiment name\n * - Determine the project name (braintrustProjectName) based on CI or dev environment\n * - Run the Eval function with the given configuration:\n *    * experimentName: A label for this run\n *    * data: A function that returns the testcases to run\n *    * task: A function that executes each task, given input specifying model and task name\n *    * scores: An array of scoring functions\n *    * maxConcurrency: Limit on parallel tasks\n *    * trialCount: Number of trials (retries) per task\n * - Collect and summarize results using `generateSummary`.\n */\n(async () => {\n  // Generate a unique name for the experiment\n  const experimentName: string = generateExperimentName({\n    evalName: filterByEvalName || undefined,\n    category: filterByCategory || undefined,\n    environment: env,\n  });\n\n  // Determine braintrust project name to use (stagehand in CI, stagehand-dev otherwise)\n  const braintrustProjectName =\n    process.env.CI === \"true\" ? \"stagehand\" : \"stagehand-dev\";\n\n  try {\n    // Run the evaluations with the braintrust Eval function\n    const evalResult = await Eval(braintrustProjectName, {\n      experimentName,\n      data: generateFilteredTestcases,\n      // Each test is a function that runs the corresponding task module\n      task: async (input: EvalInput) => {\n        const logger = new EvalLogger();\n        // Track V3 instance at outer scope to ensure cleanup in all cases\n        let v3Input: Awaited<ReturnType<typeof initV3>> | undefined;\n        let v3ToClose: Awaited<ReturnType<typeof initV3>>[\"v3\"] | null = null;\n\n        try {\n          const taskBasePath = path.join(moduleDir, \"tasks\", input.name);\n          const taskCandidates = [`${taskBasePath}.js`, `${taskBasePath}.ts`];\n          const taskModulePath = taskCandidates.find((candidate) =>\n            fs.existsSync(candidate),\n          );\n\n          if (!taskModulePath) {\n            throw new StagehandEvalError(\n              `Failed to find task module for ${input.name}. Tried paths:\\n` +\n                taskCandidates.map((candidate) => `- ${candidate}`).join(\"\\n\"),\n            );\n          }\n\n          const taskModule = await import(pathToFileURL(taskModulePath).href);\n\n          // Extract the task function\n          const taskName = input.name.includes(\"/\")\n            ? input.name.split(\"/\").pop() // Get the last part of the path for nested tasks\n            : input.name;\n\n          const taskFunction = taskModule[taskName];\n\n          if (typeof taskFunction !== \"function\") {\n            throw new StagehandEvalError(\n              `No Eval function found for task name: ${taskName} in module ${input.name}`,\n            );\n          }\n\n          // Execute the task\n          const isAgentTask =\n            input.name.startsWith(\"agent/\") || input.name.includes(\"/agent/\");\n          if (USE_API) {\n            // Derive provider from model. Prefer explicit \"provider/model\"; otherwise infer for agent models\n            let provider: string;\n            if (input.modelName.includes(\"/\")) {\n              provider = input.modelName.split(\"/\")[0];\n            } else {\n              // Fall back to agent provider inference for bare agent model names (e.g., \"computer-use-preview\")\n              try {\n                provider = AgentProvider.getAgentProvider(input.modelName);\n              } catch {\n                // If not an agent model, leave provider undefined to trigger helpful error below\n                provider = undefined as unknown as string;\n              }\n            }\n\n            const logFn = (line: LogLine): void => logger.log(line);\n            const apiKey = loadApiKeyFromEnv(provider, logFn);\n\n            if (!apiKey) {\n              throw new StagehandEvalError(\n                `USE_API=true but no API key found for provider “${provider}”.`,\n              );\n            }\n\n            // taskInput = await initStagehand({\n            //   logger,\n            //   modelName: input.modelName,\n            //   modelClientOptions: { apiKey: apiKey },\n            // });\n            // Also initialize V3 so tasks can migrate to it progressively\n            v3Input = await initV3({\n              logger,\n              modelName: input.modelName,\n              modelClientOptions: { apiKey: apiKey },\n              createAgent: isAgentTask,\n              isCUA: input.isCUA,\n            });\n            v3ToClose = v3Input.v3;\n          } else {\n            let llmClient: LLMClient;\n            if (input.modelName.includes(\"/\")) {\n              const firstSlashIndex = input.modelName.indexOf(\"/\");\n              llmClient = new AISdkClientWrapped({\n                model: getAISDKLanguageModel(\n                  input.modelName.substring(0, firstSlashIndex),\n                  input.modelName.substring(firstSlashIndex + 1),\n                ),\n              });\n            }\n            v3Input = await initV3({\n              logger,\n              llmClient,\n              modelName: input.modelName,\n              createAgent: isAgentTask,\n              isCUA: input.isCUA,\n            });\n            v3ToClose = v3Input.v3;\n          }\n          // Pass full EvalInput to the task (data-driven params available via input.params)\n          const result = await taskFunction({ ...v3Input, input });\n\n          // Log result to console\n          if (result && result._success) {\n            console.log(`✅ ${input.name}: Passed`);\n          } else {\n            console.log(`❌ ${input.name}: Failed`);\n          }\n\n          return result;\n        } catch (error) {\n          // Log any errors that occur during task execution\n          console.error(`❌ ${input.name}: Error - ${error}`);\n          logger.error({\n            message: `Error in task ${input.name}`,\n            level: 0,\n            auxiliary: {\n              error: {\n                value: error.message,\n                type: \"string\",\n              },\n              trace: {\n                value: error.stack,\n                type: \"string\",\n              },\n            },\n          });\n          return {\n            _success: false,\n            error: JSON.parse(JSON.stringify(error, null, 2)),\n            logs: logger.getLogs(),\n          };\n        } finally {\n          // Always close V3 instance, regardless of success or failure.\n          // This ensures proper cleanup even if the task threw an error or\n          // the Browserbase session disconnected mid-execution.\n          if (v3Input?.v3) {\n            try {\n              await v3Input.v3.close();\n            } catch (closeError) {\n              // Log but don't throw - we don't want close errors to mask\n              // the original task result or prevent subsequent evals\n              console.error(\n                `Warning: Error closing V3 instance for ${input.name}:`,\n                closeError,\n              );\n            }\n          }\n          await endBrowserbaseSession(v3ToClose);\n          // Clear logger to free memory (logs already captured in result)\n          logger.clear();\n        }\n      },\n      // Use the scoring functions defined above\n      scores: [exactMatch, errorMatch],\n      maxConcurrency: MAX_CONCURRENCY,\n      trialCount: TRIAL_COUNT,\n    });\n\n    // Map results to the SummaryResult format\n    const summaryResults: SummaryResult[] = evalResult.results.map((result) => {\n      const output =\n        typeof result.output === \"boolean\"\n          ? { _success: result.output }\n          : result.output;\n\n      return {\n        input: result.input,\n        output,\n        name: result.input.name,\n        score: output._success ? 1 : 0,\n      };\n    });\n\n    // Generate and write the summary\n    await generateSummary(summaryResults, experimentName);\n  } catch (error) {\n    console.error(\"Error during evaluation run:\", error);\n    process.exit(1);\n  }\n})();\n"
  },
  {
    "path": "packages/evals/initV3.ts",
    "content": "/**\n * Initializes a V3 instance for use in evaluations without modifying\n * the existing Stagehand-based init flow. Tasks can gradually migrate\n * to consume `v3` directly.\n */\n\nimport type {\n  AvailableCuaModel,\n  AvailableModel,\n  AgentInstance,\n  ClientOptions,\n  LLMClient,\n  LocalBrowserLaunchOptions,\n  ModelConfiguration,\n  V3Options,\n  AgentModelConfig,\n} from \"@browserbasehq/stagehand\";\nimport {\n  loadApiKeyFromEnv,\n  modelToAgentProviderMap,\n  V3,\n} from \"@browserbasehq/stagehand\";\nimport { env } from \"./env.js\";\nimport { EvalLogger } from \"./logger.js\";\n\ntype InitV3Args = {\n  llmClient?: LLMClient;\n  modelClientOptions?: ClientOptions;\n  domSettleTimeoutMs?: number; // retained for parity; v3 handlers accept timeouts per-call\n  logger: EvalLogger;\n  createAgent?: boolean; // only create an agent for agent tasks\n  isCUA?: boolean;\n  configOverrides?: {\n    localBrowserLaunchOptions?: Partial<\n      Pick<LocalBrowserLaunchOptions, \"headless\" | \"args\">\n    >;\n    // Back-compat alias for args\n    chromeFlags?: string[];\n    browserbaseSessionCreateParams?: V3Options[\"browserbaseSessionCreateParams\"];\n    browserbaseSessionID?: V3Options[\"browserbaseSessionID\"];\n    experimental?: boolean;\n  };\n  actTimeoutMs?: number; // retained for parity (v3 agent tools don't use this globally)\n  modelName: AvailableModel;\n};\n\nexport type V3InitResult = {\n  v3: V3;\n  logger: EvalLogger;\n  debugUrl?: string; // not exposed by v3; placeholder for parity\n  sessionUrl?: string; // not exposed by v3; placeholder for parity\n  modelName: AvailableModel;\n  agent?: AgentInstance;\n};\n\nexport async function initV3({\n  llmClient,\n  modelClientOptions,\n  logger,\n  configOverrides,\n  modelName,\n  createAgent,\n  isCUA,\n}: InitV3Args): Promise<V3InitResult> {\n  // If CUA, choose a safe internal AISDK model for V3 handlers based on available API keys\n  let internalModel: AvailableModel = modelName;\n  if (isCUA) {\n    if (process.env.OPENAI_API_KEY)\n      internalModel = \"openai/gpt-4.1-mini\" as AvailableModel;\n    else if (\n      process.env.GEMINI_API_KEY ||\n      process.env.GOOGLE_GENERATIVE_AI_API_KEY\n    )\n      internalModel = \"google/gemini-2.0-flash\" as AvailableModel;\n    else if (process.env.ANTHROPIC_API_KEY)\n      internalModel = \"anthropic/claude-sonnet-4-6\" as AvailableModel;\n    else\n      throw new Error(\n        \"V3 init: No AISDK API key found. Set one of OPENAI_API_KEY, GEMINI_API_KEY/GOOGLE_GENERATIVE_AI_API_KEY, or ANTHROPIC_API_KEY to run CUA evals.\",\n      );\n  }\n\n  const resolvedModelConfig: ModelConfiguration =\n    !isCUA && modelClientOptions\n      ? ({\n          ...modelClientOptions,\n          modelName: internalModel,\n        } as ModelConfiguration)\n      : internalModel;\n\n  const v3Options: V3Options = {\n    env,\n    apiKey: process.env.BROWSERBASE_API_KEY,\n    projectId: process.env.BROWSERBASE_PROJECT_ID,\n    localBrowserLaunchOptions: {\n      headless: configOverrides?.localBrowserLaunchOptions?.headless ?? false,\n      args:\n        configOverrides?.localBrowserLaunchOptions?.args ??\n        configOverrides?.chromeFlags,\n    },\n    model: resolvedModelConfig,\n    experimental:\n      typeof configOverrides?.experimental === \"boolean\"\n        ? configOverrides.experimental && process.env.USE_API !== \"true\" // experimental only when not using API\n        : false,\n    verbose: 2,\n    browserbaseSessionCreateParams:\n      configOverrides?.browserbaseSessionCreateParams,\n    browserbaseSessionID: configOverrides?.browserbaseSessionID,\n    selfHeal: true,\n    disablePino: true,\n    disableAPI: process.env.USE_API !== \"true\", // Negate: USE_API=true → disableAPI=false\n    serverCache: false,\n    logger: logger.log.bind(logger),\n  };\n\n  if (!isCUA && llmClient) {\n    v3Options.llmClient = llmClient;\n  }\n\n  const v3 = new V3(v3Options);\n\n  // Associate the logger with the V3 instance\n  logger.init(v3);\n  await v3.init();\n\n  let agent: AgentInstance | undefined;\n  if (createAgent) {\n    if (isCUA) {\n      const shortModelName = modelName.includes(\"/\")\n        ? modelName.split(\"/\")[1]\n        : modelName;\n\n      const providerType = modelToAgentProviderMap[shortModelName];\n      if (!providerType) {\n        throw new Error(\n          `CUA model \"${shortModelName}\" not found in modelToAgentProviderMap. ` +\n            `Available: ${Object.keys(modelToAgentProviderMap).join(\", \")}`,\n        );\n      }\n\n      const apiKey = loadApiKeyFromEnv(providerType, logger.log.bind(logger));\n\n      const cuaModel: AvailableCuaModel | AgentModelConfig<AvailableCuaModel> =\n        apiKey && apiKey.length > 0\n          ? {\n              modelName: modelName as AvailableCuaModel,\n              apiKey,\n            }\n          : (modelName as AvailableCuaModel);\n\n      agent = v3.agent({\n        cua: true,\n        model: cuaModel,\n        systemPrompt: `You are a helpful assistant that must solve the task by browsing. At the end, produce a single line: \"Final Answer: <answer>\" summarizing the requested result (e.g., score, list, or text). ALWAYS OPERATE WITHIN THE PAGE OPENED BY THE USER, YOU WILL ALWAYS BE PROVIDED WITH AN OPENED PAGE, WHICHEVER TASK YOU ARE ATTEMPTING TO COMPLETE CAN BE ACCOMPLISHED WITHIN THE PAGE. Simple perform the task provided, do not overthink or overdo it. The user trusts you to complete the task without any additional instructions, or answering any questions.`,\n      });\n    } else {\n      agent = v3.agent({\n        model: modelName,\n        executionModel: \"google/gemini-2.5-flash\",\n      });\n    }\n  }\n\n  return {\n    v3,\n    logger,\n    debugUrl: \"\",\n    sessionUrl: \"\",\n    modelName,\n    agent,\n  };\n}\n"
  },
  {
    "path": "packages/evals/lib/AISdkClientWrapped.ts",
    "content": "import {\n  CoreAssistantMessage,\n  ModelMessage,\n  CoreSystemMessage,\n  CoreUserMessage,\n  ImagePart,\n  NoObjectGeneratedError,\n  TextPart,\n  ToolSet,\n  Tool,\n} from \"ai\";\nimport * as ai from \"ai\";\nimport { wrapAISDK } from \"braintrust\";\nimport type { LanguageModelV2 } from \"@ai-sdk/provider\";\nimport { ChatCompletion } from \"openai/resources\";\nimport {\n  AvailableModel,\n  CreateChatCompletionOptions,\n  LLMClient,\n  LogLine,\n  toJsonSchema,\n} from \"@browserbasehq/stagehand\";\n\n// Wrap AI SDK functions with Braintrust for tracing\nconst { generateObject, generateText } = wrapAISDK(ai);\n\nexport class AISdkClientWrapped extends LLMClient {\n  public type = \"aisdk\" as const;\n  private model: LanguageModelV2;\n  private logger?: (message: LogLine) => void;\n\n  constructor({\n    model,\n    logger,\n  }: {\n    model: LanguageModelV2;\n    logger?: (message: LogLine) => void;\n  }) {\n    super(model.modelId as AvailableModel);\n    this.model = model;\n    this.logger = logger;\n  }\n\n  public getLanguageModel(): LanguageModelV2 {\n    return this.model;\n  }\n\n  async createChatCompletion<T = ChatCompletion>({\n    options,\n  }: CreateChatCompletionOptions): Promise<T> {\n    this.logger?.({\n      category: \"aisdk\",\n      message: \"creating chat completion\",\n      level: 2,\n      auxiliary: {\n        options: {\n          value: JSON.stringify({\n            ...options,\n            image: undefined,\n            messages: options.messages.map((msg) => ({\n              ...msg,\n              content: Array.isArray(msg.content)\n                ? msg.content.map((c) =>\n                    \"image_url\" in c\n                      ? { ...c, image_url: { url: \"[IMAGE_REDACTED]\" } }\n                      : c,\n                  )\n                : msg.content,\n            })),\n          }),\n          type: \"object\",\n        },\n        modelName: {\n          value: this.model.modelId,\n          type: \"string\",\n        },\n      },\n    });\n\n    const formattedMessages: ModelMessage[] = options.messages.map(\n      (message) => {\n        if (Array.isArray(message.content)) {\n          if (message.role === \"system\") {\n            const systemMessage: CoreSystemMessage = {\n              role: \"system\",\n              content: message.content\n                .map((c) => (\"text\" in c ? c.text : \"\"))\n                .join(\"\\n\"),\n            };\n            return systemMessage;\n          }\n\n          const contentParts = message.content.map((content) => {\n            if (\"image_url\" in content) {\n              const imageContent: ImagePart = {\n                type: \"image\",\n                image: content.image_url.url,\n              };\n              return imageContent;\n            } else {\n              const textContent: TextPart = {\n                type: \"text\",\n                text: content.text,\n              };\n              return textContent;\n            }\n          });\n\n          if (message.role === \"user\") {\n            const userMessage: CoreUserMessage = {\n              role: \"user\",\n              content: contentParts,\n            };\n            return userMessage;\n          } else {\n            const textOnlyParts = contentParts.map((part) => ({\n              type: \"text\" as const,\n              text: part.type === \"image\" ? \"[Image]\" : part.text,\n            }));\n            const assistantMessage: CoreAssistantMessage = {\n              role: \"assistant\",\n              content: textOnlyParts,\n            };\n            return assistantMessage;\n          }\n        }\n\n        return {\n          role: message.role,\n          content: message.content,\n        };\n      },\n    );\n\n    let objectResponse: Awaited<ReturnType<typeof generateObject>>;\n    const isGPT5 = this.model.modelId.includes(\"gpt-5\");\n    const isCodex = this.model.modelId.includes(\"codex\");\n    const usesLowReasoningEffort =\n      (this.model.modelId.includes(\"gpt-5.1\") ||\n        this.model.modelId.includes(\"gpt-5.2\")) &&\n      !isCodex;\n    const isDeepSeek = this.model.modelId.includes(\"deepseek\");\n    // Kimi models only support temperature=1\n    const isKimi = this.model.modelId.includes(\"kimi\");\n    const temperature = isKimi ? 1 : options.temperature;\n    if (options.response_model) {\n      if (isDeepSeek || isKimi) {\n        const parsedSchema = JSON.stringify(\n          toJsonSchema(options.response_model.schema),\n        );\n\n        formattedMessages.push({\n          role: \"user\",\n          content: `Respond in this zod schema format:\\n${parsedSchema}\\n\nYou must respond in JSON format. respond WITH JSON. Do not include any other text, formatting or markdown in your output. Do not include \\`\\`\\` or \\`\\`\\`json in your response. Only the JSON object itself.`,\n        });\n      }\n\n      try {\n        objectResponse = await generateObject({\n          model: this.model,\n          messages: formattedMessages,\n          schema: options.response_model.schema,\n          temperature,\n          providerOptions: isGPT5\n            ? {\n                openai: {\n                  textVerbosity: isCodex ? \"medium\" : \"low\", // codex models only support 'medium'\n                  reasoningEffort: isCodex\n                    ? \"medium\"\n                    : usesLowReasoningEffort\n                      ? \"low\"\n                      : \"minimal\",\n                },\n              }\n            : undefined,\n        });\n      } catch (err) {\n        if (NoObjectGeneratedError.isInstance(err)) {\n          this.logger?.({\n            category: \"AISDK error\",\n            message: err.message,\n            level: 0,\n            auxiliary: {\n              cause: {\n                value: JSON.stringify(err.cause ?? {}),\n                type: \"object\",\n              },\n              text: {\n                value: err.text ?? \"\",\n                type: \"string\",\n              },\n              response: {\n                value: JSON.stringify(err.response ?? {}),\n                type: \"object\",\n              },\n              usage: {\n                value: JSON.stringify(err.usage ?? {}),\n                type: \"object\",\n              },\n              finishReason: {\n                value: err.finishReason ?? \"unknown\",\n                type: \"string\",\n              },\n              requestId: {\n                value: options.requestId,\n                type: \"string\",\n              },\n            },\n          });\n\n          throw err;\n        }\n        throw err;\n      }\n\n      const result = {\n        data: objectResponse.object,\n        usage: {\n          prompt_tokens: objectResponse.usage.inputTokens ?? 0,\n          completion_tokens: objectResponse.usage.outputTokens ?? 0,\n          reasoning_tokens: objectResponse.usage.reasoningTokens ?? 0,\n          cached_input_tokens: objectResponse.usage.cachedInputTokens ?? 0,\n          total_tokens: objectResponse.usage.totalTokens ?? 0,\n        },\n      } as T;\n\n      this.logger?.({\n        category: \"aisdk\",\n        message: \"response\",\n        level: 1,\n        auxiliary: {\n          response: {\n            value: JSON.stringify({\n              object: objectResponse.object,\n              usage: objectResponse.usage,\n              finishReason: objectResponse.finishReason,\n              // Omit request and response properties that might contain images\n            }),\n            type: \"object\",\n          },\n          requestId: {\n            value: options.requestId,\n            type: \"string\",\n          },\n        },\n      });\n\n      return result;\n    }\n\n    const tools: ToolSet = {};\n    if (options.tools && options.tools.length > 0) {\n      for (const tool of options.tools) {\n        tools[tool.name] = {\n          description: tool.description,\n          inputSchema: tool.parameters,\n        } as Tool;\n      }\n    }\n\n    const textResponse = await generateText({\n      model: this.model,\n      messages: formattedMessages,\n      tools: Object.keys(tools).length > 0 ? tools : undefined,\n      toolChoice:\n        Object.keys(tools).length > 0\n          ? options.tool_choice === \"required\"\n            ? \"required\"\n            : options.tool_choice === \"none\"\n              ? \"none\"\n              : \"auto\"\n          : undefined,\n      temperature,\n    });\n\n    // Transform AI SDK response to match LLMResponse format expected by operator handler\n    const transformedToolCalls = (textResponse.toolCalls || []).map(\n      (toolCall) => ({\n        id:\n          toolCall.toolCallId ||\n          `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n        type: \"function\",\n        function: {\n          name: toolCall.toolName,\n          arguments: JSON.stringify(toolCall.input),\n        },\n      }),\n    );\n\n    const result = {\n      id: `chatcmpl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n      object: \"chat.completion\",\n      created: Math.floor(Date.now() / 1000),\n      model: this.model.modelId,\n      choices: [\n        {\n          index: 0,\n          message: {\n            role: \"assistant\",\n            content: textResponse.text || null,\n            tool_calls: transformedToolCalls,\n          },\n          finish_reason: textResponse.finishReason || \"stop\",\n        },\n      ],\n      usage: {\n        prompt_tokens: textResponse.usage.inputTokens ?? 0,\n        completion_tokens: textResponse.usage.outputTokens ?? 0,\n        reasoning_tokens: textResponse.usage.reasoningTokens ?? 0,\n        cached_input_tokens: textResponse.usage.cachedInputTokens ?? 0,\n        total_tokens: textResponse.usage.totalTokens ?? 0,\n      },\n    } as T;\n\n    this.logger?.({\n      category: \"aisdk\",\n      message: \"response\",\n      level: 2,\n      auxiliary: {\n        response: {\n          value: JSON.stringify({\n            text: textResponse.text,\n            usage: textResponse.usage,\n            finishReason: textResponse.finishReason,\n            // Omit request and response properties that might contain images\n          }),\n          type: \"object\",\n        },\n        requestId: {\n          value: options.requestId,\n          type: \"string\",\n        },\n      },\n    });\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "packages/evals/llm_clients/hn_aisdk.ts",
    "content": "// import { Stagehand } from \"@browserbasehq/stagehand\";\n// import { EvalFunction } from \"@/types/evals\";\n// import { z } from \"zod\";\n//\n// export const hn_aisdk: EvalFunction = async ({\n//   debugUrl,\n//   sessionUrl,\n//   stagehandConfig,\n//   logger,\n// }) => {\n//   const stagehand = new Stagehand({\n//     ...stagehandConfig,\n//     modelName: \"openai/gpt-4o-mini\",\n//   });\n//   await stagehand.init();\n//   await stagehand.page.goto(\n//     \"https://browserbase.github.io/stagehand-eval-sites/sites/hackernews/\",\n//   );\n//\n//   let { story } = await stagehand.page.extract({\n//     instruction: \"extract the title of the top story on the page\",\n//     schema: z.object({\n//       story: z.string().describe(\"the title of the top story on the page\"),\n//     }),\n//   });\n//   // remove the (url) part of the story title\n//   story = story.split(\" (\")[0];\n//\n//   const expectedStoryElement = await stagehand.page.$(\n//     \"xpath=/html/body/center/table/tbody/tr[3]/td/table/tbody/tr[1]/td[3]/span/a\",\n//   );\n//   // remove the (url) part of the story title\n//   const expectedStory = (await expectedStoryElement?.textContent())?.split(\n//     \" (\",\n//   )?.[0];\n//\n//   if (!expectedStory) {\n//     logger.error({\n//       message: \"Could not find expected story element\",\n//       level: 0,\n//     });\n//     return {\n//       _success: false,\n//       error: \"Could not find expected story element\",\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   if (story !== expectedStory) {\n//     logger.error({\n//       message: \"Extracted story does not match expected story\",\n//       level: 0,\n//       auxiliary: {\n//         expected: {\n//           value: expectedStory,\n//           type: \"string\",\n//         },\n//         actual: {\n//           value: story,\n//           type: \"string\",\n//         },\n//       },\n//     });\n//     return {\n//       _success: false,\n//       error: \"Extracted story does not match expected story\",\n//       expectedStory,\n//       actualStory: story,\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   await stagehand.page.act(\"Click on the 'new' tab\");\n//\n//   if (stagehand.page.url() !== \"https://news.ycombinator.com/newest\") {\n//     logger.error({\n//       message: \"Page did not navigate to the 'new' tab\",\n//       level: 0,\n//       auxiliary: {\n//         expected: {\n//           value: \"https://news.ycombinator.com/newest\",\n//           type: \"string\",\n//         },\n//         actual: {\n//           value: stagehand.page.url(),\n//           type: \"string\",\n//         },\n//       },\n//     });\n//     return {\n//       _success: false,\n//       error: \"Page did not navigate to the 'new' tab\",\n//       expectedUrl: \"https://news.ycombinator.com/newest\",\n//       actualUrl: stagehand.page.url(),\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   await stagehand.close();\n//\n//   return {\n//     _success: true,\n//     expectedStory,\n//     actualStory: story,\n//     debugUrl,\n//     sessionUrl,\n//     logs: logger.getLogs(),\n//   };\n// };\n"
  },
  {
    "path": "packages/evals/llm_clients/hn_customOpenAI.ts",
    "content": "// import { EvalFunction } from \"@/types/evals\";\n// import { z } from \"zod\";\n// import { CustomOpenAIClient } from \"@/examples/external_clients/customOpenAI\";\n// import OpenAI from \"openai\";\n// import { Stagehand } from \"@browserbasehq/stagehand\";\n//\n// export const hn_customOpenAI: EvalFunction = async ({\n//   logger,\n//   stagehandConfig,\n//   debugUrl,\n//   sessionUrl,\n// }) => {\n//   const stagehand = new Stagehand({\n//     ...stagehandConfig,\n//     llmClient: new CustomOpenAIClient({\n//       modelName: \"gpt-4o-mini\",\n//       client: new OpenAI({\n//         apiKey: process.env.OPENAI_API_KEY,\n//       }),\n//     }),\n//   });\n//\n//   await stagehand.init();\n//\n//   await stagehand.page.goto(\n//     \"https://browserbase.github.io/stagehand-eval-sites/sites/hackernews/\",\n//   );\n//\n//   let { story } = await stagehand.page.extract({\n//     instruction: \"extract the title of the top story on the page\",\n//     schema: z.object({\n//       story: z.string().describe(\"the title of the top story on the page\"),\n//     }),\n//   });\n//   // remove the (url) part of the story title\n//   story = story.split(\" (\")[0];\n//\n//   const expectedStoryElement = await stagehand.page.$(\n//     \"xpath=/html/body/center/table/tbody/tr[3]/td/table/tbody/tr[1]/td[3]/span/a\",\n//   );\n//   // remove the (url) part of the story title\n//   const expectedStory = (await expectedStoryElement?.textContent())?.split(\n//     \" (\",\n//   )?.[0];\n//\n//   if (!expectedStory) {\n//     logger.error({\n//       message: \"Could not find expected story element\",\n//       level: 0,\n//     });\n//     return {\n//       _success: false,\n//       error: \"Could not find expected story element\",\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   if (story !== expectedStory) {\n//     logger.error({\n//       message: \"Extracted story does not match expected story\",\n//       level: 0,\n//       auxiliary: {\n//         expected: {\n//           value: expectedStory,\n//           type: \"string\",\n//         },\n//         actual: {\n//           value: story,\n//           type: \"string\",\n//         },\n//       },\n//     });\n//     return {\n//       _success: false,\n//       error: \"Extracted story does not match expected story\",\n//       expectedStory,\n//       actualStory: story,\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   await stagehand.page.act(\"Click on the 'new' tab\");\n//\n//   if (stagehand.page.url() !== \"https://news.ycombinator.com/newest\") {\n//     logger.error({\n//       message: \"Page did not navigate to the 'new' tab\",\n//       level: 0,\n//       auxiliary: {\n//         expected: {\n//           value: \"https://news.ycombinator.com/newest\",\n//           type: \"string\",\n//         },\n//         actual: {\n//           value: stagehand.page.url(),\n//           type: \"string\",\n//         },\n//       },\n//     });\n//     return {\n//       _success: false,\n//       error: \"Page did not navigate to the 'new' tab\",\n//       expectedUrl: \"https://news.ycombinator.com/newest\",\n//       actualUrl: stagehand.page.url(),\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   await stagehand.close();\n//\n//   return {\n//     _success: true,\n//     expectedStory,\n//     actualStory: story,\n//     debugUrl,\n//     sessionUrl,\n//     logs: logger.getLogs(),\n//   };\n// };\n"
  },
  {
    "path": "packages/evals/llm_clients/hn_langchain.ts",
    "content": "// import { EvalFunction } from \"@/types/evals\";\n// import { z } from \"zod\";\n// import { LangchainClient } from \"@/examples/external_clients/langchain\";\n// import { ChatOpenAI } from \"@langchain/openai\";\n// import { Stagehand } from \"@browserbasehq/stagehand\";\n//\n// export const hn_langchain: EvalFunction = async ({\n//   logger,\n//   stagehandConfig,\n//   debugUrl,\n//   sessionUrl,\n// }) => {\n//   const stagehand = new Stagehand({\n//     ...stagehandConfig,\n//     llmClient: new LangchainClient(\n//       new ChatOpenAI({\n//         model: \"gpt-4o-mini\",\n//       }),\n//     ),\n//   });\n//   await stagehand.init();\n//\n//   await stagehand.page.goto(\n//     \"https://browserbase.github.io/stagehand-eval-sites/sites/hackernews/\",\n//   );\n//\n//   let { story } = await stagehand.page.extract({\n//     instruction: \"extract the title of the top story on the page\",\n//     schema: z.object({\n//       story: z.string().describe(\"the title of the top story on the page\"),\n//     }),\n//   });\n//   // remove the (url) part of the story title\n//   story = story.split(\" (\")[0];\n//\n//   const expectedStoryElement = await stagehand.page.$(\n//     \"xpath=/html/body/center/table/tbody/tr[3]/td/table/tbody/tr[1]/td[3]/span/a\",\n//   );\n//   // remove the (url) part of the story title\n//   const expectedStory = (await expectedStoryElement?.textContent())?.split(\n//     \" (\",\n//   )?.[0];\n//\n//   if (!expectedStory) {\n//     logger.error({\n//       message: \"Could not find expected story element\",\n//       level: 0,\n//     });\n//     return {\n//       _success: false,\n//       error: \"Could not find expected story element\",\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   if (story !== expectedStory) {\n//     logger.error({\n//       message: \"Extracted story does not match expected story\",\n//       level: 0,\n//       auxiliary: {\n//         expected: {\n//           value: expectedStory,\n//           type: \"string\",\n//         },\n//         actual: {\n//           value: story,\n//           type: \"string\",\n//         },\n//       },\n//     });\n//     return {\n//       _success: false,\n//       error: \"Extracted story does not match expected story\",\n//       expectedStory,\n//       actualStory: story,\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   await stagehand.page.act(\"Click on the 'new' tab\");\n//\n//   if (stagehand.page.url() !== \"https://news.ycombinator.com/newest\") {\n//     logger.error({\n//       message: \"Page did not navigate to the 'new' tab\",\n//       level: 0,\n//       auxiliary: {\n//         expected: {\n//           value: \"https://news.ycombinator.com/newest\",\n//           type: \"string\",\n//         },\n//         actual: {\n//           value: stagehand.page.url(),\n//           type: \"string\",\n//         },\n//       },\n//     });\n//     return {\n//       _success: false,\n//       error: \"Page did not navigate to the 'new' tab\",\n//       expectedUrl: \"https://news.ycombinator.com/newest\",\n//       actualUrl: stagehand.page.url(),\n//       debugUrl,\n//       sessionUrl,\n//       logs: logger.getLogs(),\n//     };\n//   }\n//\n//   await stagehand.close();\n//\n//   return {\n//     _success: true,\n//     expectedStory,\n//     actualStory: story,\n//     debugUrl,\n//     sessionUrl,\n//     logs: logger.getLogs(),\n//   };\n// };\n"
  },
  {
    "path": "packages/evals/logger.ts",
    "content": "/**\n * This file defines the `EvalLogger` class, which is used to capture and manage\n * log lines during the evaluation process. The logger supports different log\n * levels (info, error, warn), stores logs in memory for later retrieval, and\n * also prints them to the console for immediate feedback.\n *\n * The `parseLogLine` function helps transform raw `LogLine` objects into a more\n * structured format (`LogLineEval`), making auxiliary data easier to understand\n * and analyze. By associating an `EvalLogger` instance with a `Stagehand` object,\n * all logs emitted during the evaluation process can be captured, persisted, and\n * reviewed after the tasks complete.\n */\nimport { logLineToString } from \"./utils.js\";\nimport { LogLineEval } from \"./types/evals.js\";\nimport { LogLine } from \"@browserbasehq/stagehand\";\nimport type { V3 } from \"@browserbasehq/stagehand\";\n\n/**\n * parseLogLine:\n * Given a LogLine, attempts to parse its `auxiliary` field into a structured object.\n * If parsing fails, logs an error and returns the original line.\n *\n * The `auxiliary` field in the log line typically contains additional metadata about the log event.\n */\nfunction parseLogLine(logLine: LogLine): LogLineEval {\n  try {\n    let parsedAuxiliary: Record<string, unknown> | undefined;\n\n    if (logLine.auxiliary) {\n      parsedAuxiliary = {};\n\n      for (const [key, entry] of Object.entries(logLine.auxiliary)) {\n        try {\n          parsedAuxiliary[key] =\n            entry.type === \"object\" ? JSON.parse(entry.value) : entry.value;\n        } catch (parseError) {\n          console.warn(`Failed to parse auxiliary entry ${key}:`, parseError);\n          // If parsing fails, use the raw value\n          parsedAuxiliary[key] = entry.value;\n        }\n      }\n    }\n\n    return {\n      ...logLine,\n      auxiliary: undefined,\n      parsedAuxiliary,\n    } as LogLineEval;\n  } catch (e) {\n    console.log(\"Error parsing log line\", logLine);\n    console.error(e);\n    return logLine;\n  }\n}\n\n/**\n * EvalLogger:\n * A logger class used during evaluations to capture and print log lines.\n *\n * Capabilities:\n * - Maintains an internal array of log lines (EvalLogger.logs) for later retrieval.\n * - Can be initialized with a Stagehand instance to provide consistent logging.\n * - Supports logging at different levels (info, error, warn).\n * - Each log line is converted to a string and printed to console for immediate feedback.\n * - Also keeps a structured version of the logs that can be returned for analysis or\n *   included in evaluation output.\n */\nexport class EvalLogger {\n  private logs: LogLineEval[] = [];\n  stagehand?: V3;\n\n  constructor() {\n    this.logs = [];\n  }\n\n  /**\n   * init:\n   * Associates this logger with a given Stagehand instance.\n   * This allows the logger to provide additional context if needed.\n   */\n  init(stagehand?: V3) {\n    this.stagehand = stagehand;\n  }\n\n  /**\n   * log:\n   * Logs a message at the default (info) level.\n   * Uses `logLineToString` to produce a readable output on the console,\n   * and then stores the parsed log line in `this.logs`.\n   */\n  log(logLine: LogLine) {\n    console.log(logLineToString(logLine));\n    this.logs.push(parseLogLine(logLine));\n  }\n\n  /**\n   * error:\n   * Logs an error message with `console.error` and stores it.\n   * Useful for capturing and differentiating error-level logs.\n   */\n  error(logLine: LogLine) {\n    console.error(logLineToString(logLine));\n    this.logs.push(parseLogLine(logLine));\n  }\n\n  /**\n   * warn:\n   * Logs a warning message with `console.warn` and stores it.\n   * Helps differentiate warnings from regular info logs.\n   */\n  warn(logLine: LogLine) {\n    console.warn(logLineToString(logLine));\n    this.logs.push(parseLogLine(logLine));\n  }\n\n  /**\n   * getLogs:\n   * Retrieves the array of stored log lines.\n   * Useful for returning logs after a task completes, for analysis or debugging.\n   */\n  getLogs(): LogLineEval[] {\n    return this.logs || [];\n  }\n\n  /**\n   * clear:\n   * Clears all stored logs to free memory.\n   * Should be called after logs have been retrieved and processed.\n   */\n  clear(): void {\n    this.logs = [];\n    this.stagehand = undefined;\n  }\n}\n"
  },
  {
    "path": "packages/evals/package.json",
    "content": "{\n  \"name\": \"@browserbasehq/stagehand-evals\",\n  \"version\": \"1.1.9\",\n  \"private\": true,\n  \"description\": \"Evaluation suite for Stagehand\",\n  \"type\": \"module\",\n  \"main\": \"./\",\n  \"bin\": {\n    \"evals\": \"./dist/cli/cli.js\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"pnpm -w --dir ../.. exec tsc -p packages/evals/tsconfig.json --noEmit\",\n    \"build\": \"pnpm --filter @browserbasehq/stagehand-evals run --parallel \\\"/^build:(esm|cli)$/\\\"\",\n    \"build:esm\": \"tsx scripts/build-esm.ts\",\n    \"build:cli\": \"tsx scripts/build-cli.ts\",\n    \"test\": \"pnpm -w --dir ../.. exec turbo run test:evals --filter=@browserbasehq/stagehand-evals --\",\n    \"test:evals\": \"tsx scripts/test-evals.ts --cli packages/evals/dist/cli/cli.js\",\n    \"lint\": \"pnpm -w --dir ../.. exec prettier --check packages/evals && pnpm -w --dir ../.. exec eslint packages/evals && pnpm run typecheck\",\n    \"format\": \"prettier --write .\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/provider\": \"^2.0.0\",\n    \"@browserbasehq/stagehand\": \"workspace:*\",\n    \"ai\": \"^5.0.133\",\n    \"dotenv\": \"^17.3.1\",\n    \"openai\": \"^4.87.1\",\n    \"sharp\": \"^0.34.5\",\n    \"zod\": \"^4.2.1\"\n  },\n  \"devDependencies\": {\n    \"braintrust\": \"^0.4.7\",\n    \"chalk\": \"^5.4.1\",\n    \"string-comparison\": \"^1.3.0\",\n    \"tsx\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/evals/run.ts",
    "content": "import { spawnSync } from \"node:child_process\";\nimport process from \"node:process\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nconst args: readonly string[] = process.argv.slice(2);\nconst moduleDir = getCurrentDirPath();\n\nconst wantsHelp: boolean = args.some((a) => /^(?:--?)?(?:h|help)$/i.test(a));\nconst wantsMan: boolean = args.some((a) => /^(?:--?)?man$/i.test(a));\n\n// Skip build if just showing help\nif (!wantsHelp && !wantsMan) {\n  const build = spawnSync(\"pnpm\", [\"run\", \"build\"], {\n    stdio: \"inherit\",\n    cwd: \"../..\",\n  });\n  if (build.status !== 0) process.exit(build.status ?? 1);\n}\n\nconst run = spawnSync(\"tsx\", [\"index.eval.ts\", ...args], {\n  stdio: \"inherit\",\n  cwd: moduleDir,\n});\nprocess.exit(run.status ?? 0);\n"
  },
  {
    "path": "packages/evals/runtimePaths.ts",
    "content": "/**\n * Keep this file in sync with:\n * - /packages/core/lib/v3/runtimePaths.ts\n * - /packages/server-v3/scripts/runtimePaths.ts\n * - /packages/server-v4/scripts/runtimePaths.ts\n * - /packages/evals/runtimePaths.ts\n * - /packages/docs/scripts/runtimePaths.js\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createRequire } from \"node:module\";\n\nconst PACKAGE_SEGMENT = \"/packages/evals/\";\nconst EVAL_FRAMES = new Set([\"[eval]\", \"[eval]-wrapper\"]);\nconst INTERNAL_FRAME_NAMES = new Set([\n  \"readCallsites\",\n  \"readCallsitePath\",\n  \"resolveCallerFilePath\",\n  \"getCurrentFilePath\",\n  \"getCurrentDirPath\",\n  \"getRepoRootDir\",\n  \"getPackageRootDir\",\n  \"createRequireFromCaller\",\n  \"isMainModule\",\n]);\n\nconst normalizePath = (value: string): string => {\n  const input = value.startsWith(\"file://\") ? fileURLToPath(value) : value;\n  return path.resolve(input).replaceAll(\"\\\\\", \"/\");\n};\n\nconst readCallsites = (): NodeJS.CallSite[] => {\n  const previousPrepare = Error.prepareStackTrace;\n  try {\n    Error.prepareStackTrace = (_, stack) => stack;\n    return (\n      (new Error().stack as unknown as NodeJS.CallSite[] | undefined) ?? []\n    );\n  } finally {\n    Error.prepareStackTrace = previousPrepare;\n  }\n};\n\ntype CallSiteWithScriptName = NodeJS.CallSite & {\n  getScriptNameOrSourceURL?: () => string | null;\n};\n\nconst readCallsitePath = (callsite: NodeJS.CallSite): string | null => {\n  const callsiteWithScript = callsite as CallSiteWithScriptName;\n  const rawPath =\n    callsite.getFileName() ?? callsiteWithScript.getScriptNameOrSourceURL?.();\n  if (!rawPath) return null;\n  if (rawPath.startsWith(\"node:\")) return null;\n  if (EVAL_FRAMES.has(rawPath)) return null;\n  return normalizePath(rawPath);\n};\n\nconst isInternalCallsite = (callsite: NodeJS.CallSite): boolean => {\n  const functionName = callsite.getFunctionName();\n  if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) return true;\n\n  const methodName = callsite.getMethodName();\n  if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) return true;\n\n  const callsiteString = callsite.toString();\n  for (const frameName of INTERNAL_FRAME_NAMES) {\n    if (callsiteString.includes(`${frameName} (`)) return true;\n    if (callsiteString.includes(`.${frameName} (`)) return true;\n  }\n  return false;\n};\n\nconst resolveCallerFilePath = (): string => {\n  const packageCandidates: string[] = [];\n  const fallbackCandidates: string[] = [];\n\n  for (const callsite of readCallsites()) {\n    const filePath = readCallsitePath(callsite);\n    if (!filePath) continue;\n    if (isInternalCallsite(callsite)) continue;\n    if (filePath.includes(PACKAGE_SEGMENT)) {\n      packageCandidates.push(filePath);\n      continue;\n    }\n    fallbackCandidates.push(filePath);\n  }\n\n  const packageCandidate = packageCandidates[0];\n  if (packageCandidate) return packageCandidate;\n\n  const fallbackCandidate = fallbackCandidates[0];\n  if (fallbackCandidate) return fallbackCandidate;\n\n  throw new Error(\"Unable to resolve caller file path.\");\n};\n\nexport const getCurrentFilePath = (): string => resolveCallerFilePath();\n\nexport const getCurrentDirPath = (): string =>\n  path.dirname(getCurrentFilePath());\n\nexport const getRepoRootDir = (): string => {\n  const currentFilePath = getCurrentFilePath();\n  const index = currentFilePath.lastIndexOf(PACKAGE_SEGMENT);\n  if (index === -1) {\n    throw new Error(\n      `Unable to determine repo root from ${currentFilePath} (missing ${PACKAGE_SEGMENT}).`,\n    );\n  }\n  return currentFilePath.slice(0, index);\n};\n\nexport const getPackageRootDir = (): string =>\n  `${getRepoRootDir()}${PACKAGE_SEGMENT.slice(0, -1)}`;\n\nexport const createRequireFromCaller = () =>\n  createRequire(getCurrentFilePath());\n\nexport const isMainModule = (): boolean => {\n  const entryScript = process.argv.at(1);\n  if (!entryScript) return false;\n  return normalizePath(entryScript) === getCurrentFilePath();\n};\n"
  },
  {
    "path": "packages/evals/scoring.ts",
    "content": "/**\n * This file implements scoring functions needed by braintrust.\n */\n\nimport { EvalArgs, EvalInput, EvalResult } from \"./types/evals.js\";\n\nfunction formatTaskOutput(output: unknown): string {\n  let value: string | undefined;\n  if (typeof output === \"string\") {\n    value = output;\n  } else if (output instanceof Error) {\n    value = output.stack ?? `${output.name}: ${output.message}`;\n  } else {\n    try {\n      value = JSON.stringify(output, (_key, current) => {\n        if (current instanceof Error) {\n          return {\n            name: current.name,\n            message: current.message,\n            stack: current.stack,\n          };\n        }\n        return current;\n      });\n    } catch {\n      value = undefined;\n    }\n    if (value === undefined) {\n      value = String(output);\n    }\n  }\n\n  if (value.length > 160) {\n    return `${value.slice(0, 157)}...`;\n  }\n  return value;\n}\n\n/**\n * Scoring function: exactMatch\n * Given the arguments (including input, output, and expected result),\n * this returns a score of 1 if the result matches the expectation, and 0 otherwise.\n *\n * If \"expected\" is true, it checks if the output indicates success.\n * If \"expected\" is a boolean or an object with _success flag,\n * it checks if output is exactly that success condition.\n */\nexport function exactMatch(\n  args: EvalArgs<EvalInput, boolean | { _success: boolean }, unknown>,\n): EvalResult {\n  console.log(\n    `Task \"${args.input.name}\" returned: ${formatTaskOutput(args.output)}`,\n  );\n\n  const expected = args.expected ?? true;\n  if (expected === true) {\n    // If we expect a success (true), then we check the output's _success flag.\n    return {\n      name: \"Exact match\",\n      score:\n        typeof args.output === \"boolean\"\n          ? args.output\n            ? 1\n            : 0\n          : args.output._success\n            ? 1\n            : 0,\n    };\n  }\n\n  // If expected is not true, just directly compare the output to expected.\n  return {\n    name: \"Exact match\",\n    score: args.output === expected ? 1 : 0,\n  };\n}\n\n/**\n * Scoring function: errorMatch\n * Determines if an error occurred in the task.\n * Scores 1 if an error is found, otherwise 0.\n */\nexport function errorMatch(\n  args: EvalArgs<\n    EvalInput,\n    boolean | { _success: boolean; error?: unknown },\n    unknown\n  >,\n): EvalResult {\n  console.log(\n    `Task \"${args.input.name}\" returned: ${formatTaskOutput(args.output)}`,\n  );\n\n  return {\n    name: \"Error rate\",\n    score:\n      typeof args.output === \"object\" && args.output.error !== undefined\n        ? 1\n        : 0,\n  };\n}\n"
  },
  {
    "path": "packages/evals/scripts/build-cli.ts",
    "content": "/**\n * Build the evals CLI (packages/evals/dist/cli/cli.js + config), including a node shebang.\n *\n * Prereqs: pnpm install.\n * Args: none.\n * Env: none.\n * Example: pnpm run build:cli\n */\nimport fs from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"../runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nconst run = (args: string[]) => {\n  const result = spawnSync(\"pnpm\", args, { stdio: \"inherit\", cwd: repoRoot });\n  if (result.status !== 0) {\n    process.exit(result.status ?? 1);\n  }\n};\n\nfs.mkdirSync(`${repoRoot}/packages/evals/dist/cli`, { recursive: true });\n\nrun([\n  \"exec\",\n  \"esbuild\",\n  \"packages/evals/cli.ts\",\n  \"--bundle\",\n  \"--platform=node\",\n  \"--format=esm\",\n  `--outfile=${repoRoot}/packages/evals/dist/cli/cli.js`,\n  \"--sourcemap\",\n  \"--packages=external\",\n  \"--banner:js=#!/usr/bin/env node\",\n  \"--log-level=warning\",\n]);\n\n/* ── merge config: always update tasks/benchmarks from source, but preserve user defaults ── */\nconst sourceConfig = JSON.parse(\n  fs.readFileSync(`${repoRoot}/packages/evals/evals.config.json`, \"utf-8\"),\n);\nconst distConfigPath = `${repoRoot}/packages/evals/dist/cli/evals.config.json`;\n\nif (fs.existsSync(distConfigPath)) {\n  try {\n    const existing = JSON.parse(fs.readFileSync(distConfigPath, \"utf-8\"));\n    if (existing.defaults) {\n      sourceConfig.defaults = {\n        ...sourceConfig.defaults,\n        ...existing.defaults,\n      };\n    }\n  } catch {\n    // invalid existing config – overwrite entirely\n  }\n}\n\nfs.writeFileSync(distConfigPath, JSON.stringify(sourceConfig, null, 2) + \"\\n\");\nfs.writeFileSync(\n  `${repoRoot}/packages/evals/dist/cli/package.json`,\n  '{\\n  \"type\": \"module\"\\n}\\n',\n);\nfs.chmodSync(`${repoRoot}/packages/evals/dist/cli/cli.js`, 0o755);\n\n/* ── auto-link the `evals` binary globally ── */\nconst link = spawnSync(\"npm\", [\"link\", \"--force\"], {\n  stdio: \"inherit\",\n  cwd: `${repoRoot}/packages/evals`,\n});\nif (link.status !== 0) {\n  console.warn(\n    \"⚠  npm link failed (non-fatal) – you can run `npm link` manually from packages/evals\",\n  );\n}\n"
  },
  {
    "path": "packages/evals/scripts/build-esm.ts",
    "content": "/**\n * Build canonical dist/esm output for evals (plus assets/config).\n *\n * Prereqs: pnpm install.\n * Args: none.\n * Env: none.\n * Example: pnpm run build:esm\n */\nimport fs from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"../runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nconst run = (args: string[]) => {\n  const result = spawnSync(\"pnpm\", args, { stdio: \"inherit\", cwd: repoRoot });\n  if (result.status !== 0) {\n    process.exit(result.status ?? 1);\n  }\n};\n\nfs.rmSync(`${repoRoot}/packages/evals/dist/esm`, {\n  recursive: true,\n  force: true,\n});\n// Evals run from dist/esm JS, but still need config/assets/datasets on disk.\nrun([\"exec\", \"tsc\", \"-p\", \"packages/evals/tsconfig.json\"]);\n\nfs.mkdirSync(`${repoRoot}/packages/evals/dist/esm`, { recursive: true });\nfs.writeFileSync(\n  `${repoRoot}/packages/evals/dist/esm/package.json`,\n  '{\\n  \"type\": \"module\"\\n}\\n',\n);\n\nconst copyFile = (filename: string) => {\n  const src = `${repoRoot}/packages/evals/${filename}`;\n  if (fs.existsSync(src)) {\n    fs.copyFileSync(src, `${repoRoot}/packages/evals/dist/esm/${filename}`);\n  }\n};\n\nconst copyDir = (dirname: string) => {\n  const srcDir = `${repoRoot}/packages/evals/${dirname}`;\n  if (fs.existsSync(srcDir)) {\n    fs.cpSync(srcDir, `${repoRoot}/packages/evals/dist/esm/${dirname}`, {\n      recursive: true,\n    });\n  }\n};\n\ncopyFile(\"evals.config.json\");\ncopyDir(\"datasets\");\ncopyDir(\"assets\");\n"
  },
  {
    "path": "packages/evals/scripts/test-evals.ts",
    "content": "/**\n * Eval runs via the evals CLI from source or dist/esm.\n *\n * Prereqs: source mode uses tsx loader; dist mode requires compiled CLI output.\n * Args: [target] [options...] (passed to evals run) | --cli <path> [target] [options...] | --list (prints JSON matrix).\n * Env: STAGEHAND_BROWSER_TARGET=local|browserbase, NODE_V8_COVERAGE, NODE_OPTIONS;\n *      writes JUnit to ctrf/evals/<target>.xml and CTRF to ctrf/evals/<target>.json.\n * Example: STAGEHAND_BROWSER_TARGET=browserbase pnpm run test:evals -- act -t 3 -c 10\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { getCurrentFilePath, getRepoRootDir } from \"../runtimePaths.js\";\n\ntype Runtime = \"source\" | \"dist-esm\";\n\ntype EvalSummaryEntry = {\n  eval: string;\n  model: string;\n  categories?: string[];\n};\n\ntype EvalSummary = {\n  passed?: EvalSummaryEntry[];\n  failed?: EvalSummaryEntry[];\n};\n\nconst toSafeName = (name: string) => name.replace(/[\\\\/]/g, \"-\");\n\nconst readEvalSummary = (summaryPath: string): EvalSummary | null => {\n  if (!fs.existsSync(summaryPath)) return null;\n  try {\n    return JSON.parse(fs.readFileSync(summaryPath, \"utf8\")) as EvalSummary;\n  } catch (error) {\n    console.warn(\n      `Failed to parse eval summary at ${summaryPath}: ${String(error)}`,\n    );\n    return null;\n  }\n};\n\nconst escapeXml = (value: string) =>\n  value\n    .replaceAll(\"&\", \"&amp;\")\n    .replaceAll(\"<\", \"&lt;\")\n    .replaceAll(\">\", \"&gt;\")\n    .replaceAll('\"', \"&quot;\")\n    .replaceAll(\"'\", \"&apos;\");\n\nconst writeEvalJunit = (\n  summaryPath: string,\n  outputPath: string,\n  category: string,\n) => {\n  const summary = readEvalSummary(summaryPath);\n  const passed = summary?.passed ?? [];\n  const failed = summary?.failed ?? [];\n  const missingSummary = summary === null;\n  const tests = missingSummary ? 1 : passed.length + failed.length;\n  const failures = missingSummary ? 1 : failed.length;\n  const suiteName = `evals-${category}`;\n  const cases: string[] = [];\n\n  if (missingSummary) {\n    cases.push(\n      `    <testcase name=\"${escapeXml(`evals/${category} summary missing`)}\" classname=\"${escapeXml(suiteName)}\" time=\"0\">`,\n      `      <failure message=\"eval summary missing\">Missing eval summary at ${escapeXml(summaryPath)}</failure>`,\n      \"    </testcase>\",\n    );\n  } else {\n    for (const item of passed) {\n      cases.push(\n        `    <testcase name=\"${escapeXml(`evals/${item.eval} [${item.model}]`)}\" classname=\"${escapeXml(suiteName)}\" time=\"0\" />`,\n      );\n    }\n    for (const item of failed) {\n      cases.push(\n        `    <testcase name=\"${escapeXml(`evals/${item.eval} [${item.model}]`)}\" classname=\"${escapeXml(suiteName)}\" time=\"0\">`,\n        `      <failure message=\"eval failed\">${escapeXml(`categories=${(item.categories ?? []).join(\",\")}`)}</failure>`,\n        \"    </testcase>\",\n      );\n    }\n  }\n\n  const xml = [\n    '<?xml version=\"1.0\" encoding=\"utf-8\"?>',\n    \"<testsuites>\",\n    `  <testsuite name=\"${escapeXml(suiteName)}\" tests=\"${tests}\" failures=\"${failures}\" errors=\"0\" skipped=\"0\" time=\"0\">`,\n    ...cases,\n    \"  </testsuite>\",\n    \"</testsuites>\",\n    \"\",\n  ].join(\"\\n\");\n\n  fs.writeFileSync(outputPath, xml);\n};\n\nconst writeEvalCtrf = (\n  summaryPath: string,\n  outputPath: string,\n  category: string,\n) => {\n  const timestamp = new Date().toISOString();\n  const summary = readEvalSummary(summaryPath);\n  if (summary) {\n    const passed = summary.passed ?? [];\n    const failed = summary.failed ?? [];\n    const toTests = (arr: typeof passed, status: \"passed\" | \"failed\") =>\n      arr.map((item) => ({\n        name: `evals/${item.eval} [${item.model}]`,\n        status,\n        duration: 0,\n        suite: [\"evals\", category, ...(item.categories ?? [])],\n      }));\n    const report = {\n      reportFormat: \"CTRF\",\n      specVersion: \"0.0.0\",\n      generatedBy: \"stagehand-evals\",\n      timestamp,\n      results: {\n        tool: { name: \"evals\" },\n        summary: {\n          tests: passed.length + failed.length,\n          passed: passed.length,\n          failed: failed.length,\n          skipped: 0,\n          pending: 0,\n          other: 0,\n          start: 0,\n          stop: 0,\n        },\n        tests: [...toTests(passed, \"passed\"), ...toTests(failed, \"failed\")],\n      },\n    };\n    fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));\n    return;\n  }\n\n  const missingReport = {\n    reportFormat: \"CTRF\",\n    specVersion: \"0.0.0\",\n    generatedBy: \"stagehand-evals\",\n    timestamp,\n    results: {\n      tool: { name: \"evals\" },\n      summary: {\n        tests: 1,\n        passed: 0,\n        failed: 1,\n        skipped: 0,\n        pending: 0,\n        other: 0,\n        start: 0,\n        stop: 0,\n      },\n      tests: [\n        {\n          name: `evals/${category} summary missing`,\n          status: \"failed\",\n          duration: 0,\n          suite: [\"evals\", category],\n        },\n      ],\n    },\n  };\n  fs.writeFileSync(outputPath, JSON.stringify(missingReport, null, 2));\n};\n\nconst repoRoot = getRepoRootDir();\nconst toPosix = (value: string) => value.replaceAll(\"\\\\\", \"/\");\nconst resolveRepoRelative = (value: string) =>\n  path.isAbsolute(value) ? value : path.resolve(repoRoot, value);\nconst inferRuntimeFromPath = (value: string) => {\n  const normalized = toPosix(value);\n  if (normalized.includes(\"/dist/cli/\") || normalized.includes(\"/dist/esm/\")) {\n    return \"dist-esm\" as const;\n  }\n  return null;\n};\nconst inferRuntimeFromExecution = () =>\n  inferRuntimeFromPath(getCurrentFilePath()) ??\n  inferRuntimeFromPath(process.cwd());\nconst rawArgs = process.argv.slice(2).filter((arg) => arg !== \"--\");\nconst listRequested = rawArgs.includes(\"--list\");\nconst stripCliArg = (values: string[]) => {\n  const filtered: string[] = [];\n  let cliPath: string | null = null;\n  for (let i = 0; i < values.length; i++) {\n    const arg = values[i];\n    if (arg === \"--cli\") {\n      if (values[i + 1] && !values[i + 1].startsWith(\"--\")) {\n        cliPath = values[i + 1];\n        i += 1;\n      } else {\n        cliPath = \"\";\n      }\n      continue;\n    }\n    if (arg.startsWith(\"--cli=\")) {\n      cliPath = arg.slice(\"--cli=\".length);\n      continue;\n    }\n    filtered.push(arg);\n  }\n  return { filtered, cliPath };\n};\nconst strippedCli = stripCliArg(rawArgs.filter((arg) => arg !== \"--list\"));\nif (strippedCli.cliPath === \"\") {\n  console.error(\"Missing value for --cli.\");\n  process.exit(1);\n}\nconst args = strippedCli.filtered;\n\nif (listRequested) {\n  const categories = (\n    process.env.EVAL_CATEGORIES ??\n    \"observe,act,combination,extract,targeted_extract,regression,agent\"\n  ).split(\",\");\n  const entries = categories.map((category) => ({\n    category,\n    name: category,\n    safe_name: toSafeName(category),\n  }));\n  console.log(JSON.stringify(entries));\n  process.exit(0);\n}\n\nif (\n  strippedCli.cliPath &&\n  toPosix(resolveRepoRelative(strippedCli.cliPath)).includes(\"/dist/cjs/\")\n) {\n  console.error(\"CJS eval runtime is not supported. Use source or dist/cli.\");\n  process.exit(1);\n}\n\nconst runtime: Runtime =\n  (strippedCli.cliPath\n    ? inferRuntimeFromPath(resolveRepoRelative(strippedCli.cliPath))\n    : null) ??\n  inferRuntimeFromExecution() ??\n  \"source\";\n\nconst cliPath = strippedCli.cliPath\n  ? resolveRepoRelative(strippedCli.cliPath)\n  : runtime === \"source\"\n    ? `${repoRoot}/packages/evals/cli.ts`\n    : `${repoRoot}/packages/evals/dist/cli/cli.js`;\nif (!fs.existsSync(cliPath)) {\n  console.error(`Missing ${cliPath}.`);\n  process.exit(1);\n}\n\nif (args.includes(\"--help\") || args.includes(\"-h\") || args[0] === \"help\") {\n  const result = spawnSync(\n    process.execPath,\n    [...(runtime === \"source\" ? [\"--import\", \"tsx\"] : []), cliPath, \"--help\"],\n    {\n      stdio: \"inherit\",\n      cwd: repoRoot,\n    },\n  );\n  process.exit(result.status ?? 0);\n}\n\nconst hasRun = args[0] === \"run\";\nconst argsAfterRun = hasRun ? args.slice(1) : args;\nconst target =\n  argsAfterRun.find((arg) => !arg.startsWith(\"-\"))?.trim() || \"all\";\nconst safeTarget = toSafeName(target);\nconst cliArgs = hasRun ? args : [\"run\", ...args];\n\nconst baseNodeOptions = \"--enable-source-maps\";\nconst nodeOptions = [process.env.NODE_OPTIONS, baseNodeOptions]\n  .filter(Boolean)\n  .join(\" \");\n\nconst coverageDir = resolveRepoRelative(\n  process.env.NODE_V8_COVERAGE ?? `${repoRoot}/coverage/evals/${safeTarget}`,\n);\nfs.mkdirSync(coverageDir, { recursive: true });\nconst summaryPath = `${repoRoot}/eval-summary.json`;\nfs.mkdirSync(`${repoRoot}/ctrf/evals`, { recursive: true });\nconst junitPath = `${repoRoot}/ctrf/evals/${safeTarget}.xml`;\nconst ctrfPath = `${repoRoot}/ctrf/evals/${safeTarget}.json`;\n\nconst env = {\n  ...process.env,\n  NODE_OPTIONS: nodeOptions,\n  NODE_V8_COVERAGE: coverageDir,\n};\n\nconst result = spawnSync(\n  process.execPath,\n  [...(runtime === \"source\" ? [\"--import\", \"tsx\"] : []), cliPath, ...cliArgs],\n  {\n    stdio: \"inherit\",\n    env,\n    cwd: repoRoot,\n  },\n);\n\nwriteEvalJunit(summaryPath, junitPath, safeTarget);\nwriteEvalCtrf(summaryPath, ctrfPath, safeTarget);\n\nprocess.exit(result.status ?? 1);\n"
  },
  {
    "path": "packages/evals/suites/gaia.ts",
    "content": "import path from \"path\";\nimport type { Testcase, EvalInput } from \"../types/evals.js\";\nimport type { AvailableModel } from \"@browserbasehq/stagehand\";\nimport { tasksConfig } from \"../taskConfig.js\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\nimport { readJsonlFile, parseJsonlRows, applySampling } from \"../utils.js\";\n\nexport const buildGAIATestcases = (models: string[]): Testcase[] => {\n  const moduleDir = getCurrentDirPath();\n  const gaiaFilePath =\n    process.env.EVAL_GAIA_FILE ||\n    path.join(moduleDir, \"..\", \"datasets\", \"gaia\", \"GAIA_web.jsonl\");\n\n  const gaiaLines = readJsonlFile(gaiaFilePath);\n\n  const levelFilter = process.env.EVAL_GAIA_LEVEL\n    ? Number(process.env.EVAL_GAIA_LEVEL)\n    : undefined;\n  // Use EVAL_MAX_K if set, otherwise fall back to EVAL_GAIA_LIMIT or default to 25\n  const maxCases = process.env.EVAL_MAX_K\n    ? Number(process.env.EVAL_MAX_K)\n    : process.env.EVAL_GAIA_LIMIT\n      ? Number(process.env.EVAL_GAIA_LIMIT)\n      : 25;\n  const sampleCount = process.env.EVAL_GAIA_SAMPLE\n    ? Number(process.env.EVAL_GAIA_SAMPLE)\n    : undefined;\n\n  type GaiaRow = {\n    id: string;\n    Level?: number;\n    web: string;\n    ques: string;\n    [key: string]: unknown;\n  };\n\n  function isGaiaRow(parsed: unknown): parsed is GaiaRow {\n    if (parsed === null || typeof parsed !== \"object\") return false;\n    const obj = parsed as Record<string, unknown>;\n    return (\n      typeof obj.id === \"string\" &&\n      typeof obj.web === \"string\" &&\n      typeof obj.ques === \"string\"\n    );\n  }\n\n  const candidates = parseJsonlRows(gaiaLines, isGaiaRow);\n\n  // Filter by level if specified\n  const filteredCandidates = levelFilter\n    ? candidates.filter((row) => row.Level === levelFilter)\n    : candidates;\n\n  const gaiaRows = applySampling(filteredCandidates, sampleCount, maxCases);\n\n  const allTestcases: Testcase[] = [];\n  for (const model of models) {\n    for (const row of gaiaRows) {\n      const finalAnswer = (row as Record<string, unknown>)[\n        \"Final answer\"\n      ] as unknown;\n      const input: EvalInput = {\n        name: \"agent/gaia\",\n        modelName: model as AvailableModel,\n        params: {\n          id: row.id,\n          level: row.Level,\n          web: row.web,\n          ques: row.ques,\n          expected: typeof finalAnswer === \"string\" ? finalAnswer : undefined,\n        },\n      };\n      allTestcases.push({\n        input,\n        name: input.name,\n        tags: [\n          model,\n          input.name,\n          ...(\n            tasksConfig.find((t) => t.name === input.name)?.categories || []\n          ).map((x) => `category/${x}`),\n          `gaia/id/${row.id}`,\n          row.Level ? `gaia/level/${row.Level}` : \"gaia/level/unknown\",\n        ],\n        metadata: {\n          model: model as AvailableModel,\n          test: `${input.name}:${row.id}`,\n        },\n        expected: true,\n      });\n    }\n  }\n\n  return allTestcases;\n};\n"
  },
  {
    "path": "packages/evals/suites/onlineMind2Web.ts",
    "content": "import path from \"path\";\nimport type { Testcase, EvalInput } from \"../types/evals.js\";\nimport type { AvailableModel } from \"@browserbasehq/stagehand\";\nimport { tasksConfig } from \"../taskConfig.js\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\nimport { readJsonlFile, parseJsonlRows, applySampling } from \"../utils.js\";\n\nexport const buildOnlineMind2WebTestcases = (models: string[]): Testcase[] => {\n  const moduleDir = getCurrentDirPath();\n  const mind2webFilePath = path.join(\n    moduleDir,\n    \"..\",\n    \"datasets\",\n    \"onlineMind2Web\",\n    \"onlineMind2Web.jsonl\",\n  );\n\n  const lines = readJsonlFile(mind2webFilePath);\n\n  // Use EVAL_MAX_K if set, otherwise fall back to EVAL_ONLINEMIND2WEB_LIMIT or default to 25\n  const maxCases = process.env.EVAL_MAX_K\n    ? Number(process.env.EVAL_MAX_K)\n    : process.env.EVAL_ONLINEMIND2WEB_LIMIT\n      ? Number(process.env.EVAL_ONLINEMIND2WEB_LIMIT)\n      : 25;\n  const sampleCount = process.env.EVAL_ONLINEMIND2WEB_SAMPLE\n    ? Number(process.env.EVAL_ONLINEMIND2WEB_SAMPLE)\n    : undefined;\n\n  type Mind2WebRow = {\n    task_id: string;\n    confirmed_task: string;\n    website: string;\n    reference_length?: number;\n    level?: string;\n    [key: string]: unknown;\n  };\n\n  function isMind2WebRow(parsed: unknown): parsed is Mind2WebRow {\n    if (parsed === null || typeof parsed !== \"object\") return false;\n    const obj = parsed as Record<string, unknown>;\n    return (\n      typeof obj.task_id === \"string\" &&\n      typeof obj.confirmed_task === \"string\" &&\n      typeof obj.website === \"string\"\n    );\n  }\n\n  const candidates = parseJsonlRows(lines, isMind2WebRow);\n  const rows = applySampling(candidates, sampleCount, maxCases);\n\n  const allTestcases: Testcase[] = [];\n  for (const model of models) {\n    for (const row of rows) {\n      const input: EvalInput = {\n        name: \"agent/onlineMind2Web\",\n        modelName: model as AvailableModel,\n        params: {\n          task_id: row.task_id,\n          confirmed_task: row.confirmed_task,\n          website: row.website,\n          reference_length: row.reference_length,\n          level: row.level,\n        },\n      };\n      const taskCategories =\n        tasksConfig.find((t) => t.name === input.name)?.categories || [];\n      allTestcases.push({\n        input,\n        name: input.name,\n        tags: [\n          model,\n          \"mind2web\", // Simple dataset tag\n        ],\n        metadata: {\n          model: model as AvailableModel,\n          test: `${input.name}:${row.task_id}`,\n          category: taskCategories[0] || \"agent\",\n          categories: taskCategories,\n          dataset: \"onlineMind2Web\",\n          task_id: row.task_id,\n          difficulty: row.level,\n          website: row.website,\n        },\n        expected: true,\n      });\n    }\n  }\n\n  return allTestcases;\n};\n"
  },
  {
    "path": "packages/evals/suites/webtailbench.ts",
    "content": "import type { Testcase, EvalInput } from \"../types/evals.js\";\nimport type { AvailableModel } from \"@browserbasehq/stagehand\";\nimport { tasksConfig } from \"../taskConfig.js\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\nimport { readJsonlFile, parseJsonlRows, applySampling } from \"../utils.js\";\n\nexport const buildWebTailBenchTestcases = (models: string[]): Testcase[] => {\n  const moduleDir = getCurrentDirPath();\n  const webtailbenchFilePath =\n    moduleDir + \"/../datasets/webtailbench/WebTailBench_data.jsonl\";\n\n  const lines = readJsonlFile(webtailbenchFilePath);\n\n  // Use EVAL_MAX_K if set, otherwise fall back to EVAL_WEBTAILBENCH_LIMIT or default to 25\n  const maxCases = process.env.EVAL_MAX_K\n    ? Number(process.env.EVAL_MAX_K)\n    : process.env.EVAL_WEBTAILBENCH_LIMIT\n      ? Number(process.env.EVAL_WEBTAILBENCH_LIMIT)\n      : 25;\n  const sampleCount = process.env.EVAL_WEBTAILBENCH_SAMPLE\n    ? Number(process.env.EVAL_WEBTAILBENCH_SAMPLE)\n    : undefined;\n\n  type WebTailBenchRow = {\n    id: string;\n    ques: string;\n    category?: string;\n    web?: string;\n    [key: string]: unknown;\n  };\n\n  function isWebTailBenchRow(parsed: unknown): parsed is WebTailBenchRow {\n    if (parsed === null || typeof parsed !== \"object\") return false;\n    const obj = parsed as Record<string, unknown>;\n    return typeof obj.id === \"string\" && typeof obj.ques === \"string\";\n  }\n\n  const candidates = parseJsonlRows(lines, isWebTailBenchRow);\n  const rows = applySampling(candidates, sampleCount, maxCases);\n\n  const allTestcases: Testcase[] = [];\n  for (const model of models) {\n    for (const row of rows) {\n      const input: EvalInput = {\n        name: \"agent/webtailbench\",\n        modelName: model as AvailableModel,\n        params: {\n          id: row.id,\n          category: row.category,\n          ques: row.ques,\n          web: row.web,\n        },\n      };\n      const taskCategories =\n        tasksConfig.find((t) => t.name === input.name)?.categories || [];\n      allTestcases.push({\n        input,\n        name: input.name,\n        tags: [model, \"webtailbench\"],\n        metadata: {\n          model: model as AvailableModel,\n          test: `${input.name}:${row.id}`,\n          category: taskCategories[0] || \"agent\",\n          categories: taskCategories,\n          dataset: \"webtailbench\",\n          task_id: row.id,\n          task_category: row.category,\n        },\n        expected: true,\n      });\n    }\n  }\n\n  return allTestcases;\n};\n"
  },
  {
    "path": "packages/evals/suites/webvoyager.ts",
    "content": "import path from \"path\";\nimport type { Testcase, EvalInput } from \"../types/evals.js\";\nimport type { AvailableModel } from \"@browserbasehq/stagehand\";\nimport { tasksConfig } from \"../taskConfig.js\";\nimport { getCurrentDirPath } from \"../runtimePaths.js\";\nimport { readJsonlFile, parseJsonlRows, applySampling } from \"../utils.js\";\n\nexport const buildWebVoyagerTestcases = (models: string[]): Testcase[] => {\n  const moduleDir = getCurrentDirPath();\n  const voyagerFilePath = path.join(\n    moduleDir,\n    \"..\",\n    \"datasets\",\n    \"webvoyager\",\n    \"WebVoyager_data.jsonl\",\n  );\n\n  const lines = readJsonlFile(voyagerFilePath);\n\n  // Use EVAL_MAX_K if set, otherwise fall back to EVAL_WEBVOYAGER_LIMIT or default to 25\n  const maxCases = process.env.EVAL_MAX_K\n    ? Number(process.env.EVAL_MAX_K)\n    : process.env.EVAL_WEBVOYAGER_LIMIT\n      ? Number(process.env.EVAL_WEBVOYAGER_LIMIT)\n      : 25;\n  const sampleCount = process.env.EVAL_WEBVOYAGER_SAMPLE\n    ? Number(process.env.EVAL_WEBVOYAGER_SAMPLE)\n    : undefined;\n\n  type VoyagerRow = {\n    id: string;\n    web: string;\n    ques: string;\n    web_name?: string;\n    [key: string]: unknown;\n  };\n\n  function isVoyagerRow(parsed: unknown): parsed is VoyagerRow {\n    if (parsed === null || typeof parsed !== \"object\") return false;\n    const obj = parsed as Record<string, unknown>;\n    return (\n      typeof obj.id === \"string\" &&\n      typeof obj.web === \"string\" &&\n      typeof obj.ques === \"string\"\n    );\n  }\n\n  const candidates = parseJsonlRows(lines, isVoyagerRow);\n  const rows = applySampling(candidates, sampleCount, maxCases);\n\n  const allTestcases: Testcase[] = [];\n  for (const model of models) {\n    for (const row of rows) {\n      const input: EvalInput = {\n        name: \"agent/webvoyager\",\n        modelName: model as AvailableModel,\n        params: {\n          id: row.id,\n          web: row.web,\n          ques: row.ques,\n          web_name: row.web_name,\n        },\n      };\n      const taskCategories =\n        tasksConfig.find((t) => t.name === input.name)?.categories || [];\n      allTestcases.push({\n        input,\n        name: input.name,\n        tags: [\n          model,\n          \"webvoyager\", // Simple dataset tag\n        ],\n        metadata: {\n          model: model as AvailableModel,\n          test: `${input.name}:${row.id}`,\n          category: taskCategories[0] || \"agent\",\n          categories: taskCategories,\n          dataset: \"webvoyager\",\n          task_id: row.id,\n          website: row.web_name || row.web,\n        },\n        expected: true,\n      });\n    }\n  }\n\n  return allTestcases;\n};\n"
  },
  {
    "path": "packages/evals/summary.ts",
    "content": "import fs from \"fs\";\nimport { tasksByName } from \"./taskConfig.js\";\nimport type { SummaryResult } from \"./types/evals.js\";\nimport { getRepoRootDir } from \"./runtimePaths.js\";\n\nconst repoRoot = getRepoRootDir();\n\nexport const generateSummary = async (\n  results: SummaryResult[],\n  experimentName: string,\n) => {\n  const passed = results\n    .filter((r) => r.output._success)\n    .map((r) => ({\n      eval: r.input.name,\n      model: r.input.modelName,\n      categories: tasksByName[r.input.name].categories,\n    }));\n\n  const failed = results\n    .filter((r) => !r.output._success)\n    .map((r) => ({\n      eval: r.input.name,\n      model: r.input.modelName,\n      categories: tasksByName[r.input.name].categories,\n    }));\n\n  const categorySuccessCounts: Record<\n    string,\n    { total: number; success: number }\n  > = {};\n  for (const taskName of Object.keys(tasksByName)) {\n    const taskCategories = tasksByName[taskName].categories;\n    const taskResults = results.filter((r) => r.input.name === taskName);\n    const successCount = taskResults.filter((r) => r.output._success).length;\n\n    for (const cat of taskCategories) {\n      if (!categorySuccessCounts[cat]) {\n        categorySuccessCounts[cat] = { total: 0, success: 0 };\n      }\n      categorySuccessCounts[cat].total += taskResults.length;\n      categorySuccessCounts[cat].success += successCount;\n    }\n  }\n\n  const categories: Record<string, number> = {};\n  for (const [cat, counts] of Object.entries(categorySuccessCounts)) {\n    categories[cat] = Math.round((counts.success / counts.total) * 100);\n  }\n\n  const models: Record<string, number> = {};\n  const allModels = [...new Set(results.map((r) => r.input.modelName))];\n  for (const model of allModels) {\n    const modelResults = results.filter((r) => r.input.modelName === model);\n    const successCount = modelResults.filter((r) => r.output._success).length;\n    models[model] = Math.round((successCount / modelResults.length) * 100);\n  }\n\n  const formattedSummary = {\n    experimentName,\n    passed,\n    failed,\n    categories,\n    models,\n  };\n\n  const summaryPath = `${repoRoot}/eval-summary.json`;\n  fs.writeFileSync(summaryPath, JSON.stringify(formattedSummary, null, 2));\n  console.log(`Evaluation summary written to ${summaryPath}`);\n};\n"
  },
  {
    "path": "packages/evals/taskConfig.ts",
    "content": "/**\n * This file is responsible for:\n * - Loading and parsing the `evals.config.json` file, which defines tasks (evaluations) and their associated categories.\n * - Building a lookup structure (`tasksByName`) to map each task name to its categories.\n * - Filtering tasks based on command-line arguments (e.g., `filterByEvalName`) and ensuring that requested tasks exist.\n * - Determining which models to use for evaluations, depending on the category and environment variables.\n * - Validating that the chosen models are supported.\n *\n * The exported objects (`tasksByName`, `MODELS`, `config`) are used by the main evaluation script and other modules\n * to know which tasks and models are available, and to configure the evaluations accordingly.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { AvailableModel } from \"@browserbasehq/stagehand\";\nimport { filterByEvalName } from \"./args.js\";\nimport { AgentModelEntry } from \"./types/evals.js\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nconst ALL_EVAL_MODELS = [\n  // GOOGLE\n  \"gemini-2.0-flash\",\n  \"gemini-2.0-flash-lite\",\n  \"gemini-1.5-flash\",\n  \"gemini-2.5-pro-exp-03-25\",\n  \"gemini-1.5-pro\",\n  \"gemini-1.5-flash-8b\",\n  \"gemini-2.5-flash-preview-04-17\",\n  \"gemini-2.5-pro-preview-03-25\",\n  // ANTHROPIC\n  \"claude-sonnet-4-6\",\n  // OPENAI\n  \"gpt-4o-mini\",\n  \"gpt-4o\",\n  \"gpt-4.5-preview\",\n  \"o3\",\n  \"o3-mini\",\n  \"o4-mini\",\n  // TOGETHER - META\n  \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n  \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n  \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n  \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n  // TOGETHER - DEEPSEEK\n  \"deepseek-ai/DeepSeek-V3\",\n  \"Qwen/Qwen2.5-7B-Instruct-Turbo\",\n  // GROQ\n  \"groq/meta-llama/llama-4-scout-17b-16e-instruct\",\n  \"groq/llama-3.3-70b-versatile\",\n  \"groq/llama3-70b-8192\",\n  \"groq/qwen-qwq-32b\",\n  \"groq/qwen-2.5-32b\",\n  \"groq/deepseek-r1-distill-qwen-32b\",\n  \"groq/deepseek-r1-distill-llama-70b\",\n  // CEREBRAS\n  \"cerebras/llama3.3-70b\",\n];\n\n// The configuration file `evals.config.json` contains a list of tasks and their associated categories.\nconst moduleDir = getCurrentDirPath();\nconst configPath = path.join(moduleDir, \"evals.config.json\");\nconst config = JSON.parse(fs.readFileSync(configPath, \"utf-8\")) satisfies {\n  tasks: {\n    name: string;\n    categories: string[];\n  }[];\n};\n\n/**\n * The `tasksConfig` defines all tasks from the config file. Each task has a name and categories.\n * We create a mapping `tasksByName` from task name to its categories for quick lookup.\n */\ntype TaskConfig = {\n  name: string;\n  categories: string[];\n};\nconst tasksConfig = config.tasks as TaskConfig[];\n\nconst tasksByName = tasksConfig.reduce<\n  Record<string, { categories: string[] }>\n>((acc, task) => {\n  acc[task.name] = {\n    categories: task.categories,\n  };\n  return acc;\n}, {});\n\n/**\n * If filtering by a specific eval name (task), ensure that this task actually exists.\n */\nif (filterByEvalName && !tasksByName[filterByEvalName]) {\n  console.error(`Error: Evaluation \"${filterByEvalName}\" does not exist.`);\n  process.exit(1);\n}\n\n/**\n * Determine which models to run the evaluations against.\n *\n * DEFAULT_EVAL_MODELS: The default set of models used for most categories.\n */\nconst DEFAULT_EVAL_MODELS = process.env.EVAL_MODELS\n  ? process.env.EVAL_MODELS.split(\",\")\n  : [\n      \"google/gemini-2.0-flash\",\n      \"openai/gpt-4.1-mini\",\n      \"anthropic/claude-haiku-4-5\",\n    ];\n\n// Standard agent models - these run with stagehand.agent()\nconst AGENT_MODELS = process.env.EVAL_AGENT_MODELS\n  ? process.env.EVAL_AGENT_MODELS.split(\",\")\n  : [\"anthropic/claude-sonnet-4-20250514\"];\n\n// CUA agent models - these run with stagehand.agent({ cua: true })\nconst AGENT_MODELS_CUA = process.env.EVAL_AGENT_MODELS_CUA\n  ? process.env.EVAL_AGENT_MODELS_CUA.split(\",\")\n  : [\n      \"openai/computer-use-preview-2025-03-11\",\n      \"anthropic/claude-sonnet-4-20250514\",\n      \"google/gemini-2.5-computer-use-preview-10-2025\",\n    ];\n\nconst AGENT_MODEL_ENTRIES: AgentModelEntry[] = [\n  ...AGENT_MODELS.map((m) => ({ modelName: m, cua: false })),\n  ...AGENT_MODELS_CUA.map((m) => ({ modelName: m, cua: true })),\n];\n\nconst DEFAULT_AGENT_MODELS = AGENT_MODEL_ENTRIES.map((e) => e.modelName);\n\n/**\n * getModelList:\n * Returns a list of models to be used for the given category.\n * If category is \"experimental\", it merges DEFAULT_EVAL_MODELS and EXPERIMENTAL_EVAL_MODELS.\n * Otherwise, returns DEFAULT_EVAL_MODELS filtered by provider if specified.\n */\nconst getModelList = (category?: string): string[] => {\n  const provider = process.env.EVAL_PROVIDER?.toLowerCase();\n\n  if (category === \"agent\" || category === \"external_agent_benchmarks\") {\n    return DEFAULT_AGENT_MODELS;\n  }\n\n  if (provider) {\n    return ALL_EVAL_MODELS.filter((model) =>\n      filterModelByProvider(model, provider),\n    );\n  }\n\n  // If no agent category and no provider, return default eval models\n  return DEFAULT_EVAL_MODELS;\n};\n\n// Helper function to contain the provider filtering logic\nconst filterModelByProvider = (model: string, provider: string): boolean => {\n  const modelLower = model.toLowerCase();\n  if (provider === \"openai\") {\n    return modelLower.startsWith(\"gpt\");\n  } else if (provider === \"anthropic\") {\n    return modelLower.startsWith(\"claude\");\n  } else if (provider === \"google\") {\n    return modelLower.startsWith(\"gemini\");\n  } else if (provider === \"together\") {\n    return (\n      modelLower.startsWith(\"meta-llama\") ||\n      modelLower.startsWith(\"llama\") ||\n      modelLower.startsWith(\"deepseek\") ||\n      modelLower.startsWith(\"qwen\")\n    );\n  } else if (provider === \"groq\") {\n    return modelLower.startsWith(\"groq\");\n  } else if (provider === \"cerebras\") {\n    return modelLower.startsWith(\"cerebras\");\n  }\n  console.warn(\n    `Unknown provider specified or model doesn't match: ${provider}`,\n  );\n  return false;\n};\n\nconst MODELS: AvailableModel[] = getModelList().map((model) => {\n  return model as AvailableModel;\n});\n\n/**\n * Get agent model entries with CUA flag for test case generation.\n */\nconst getAgentModelEntries = (): AgentModelEntry[] => AGENT_MODEL_ENTRIES;\n\nexport { tasksByName, MODELS, tasksConfig, getModelList, getAgentModelEntries };\nexport type { AgentModelEntry };\n"
  },
  {
    "path": "packages/evals/tasks/agent/alibaba_supplier_search.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const alibaba_supplier_search: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.alibaba.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Search for 'solar panels' on Alibaba and find 3 suppliers. For each supplier, tell me their company name, minimum order quantity, and price range if available.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/all_recipes.ts",
    "content": "import { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { EvalFunction } from \"../../types/evals.js\";\n\nexport const all_recipes: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.allrecipes.com/\");\n    const evaluator = new V3Evaluator(v3);\n    const agentResult = await agent.execute({\n      instruction:\n        \"Search for a recipe for Beef Wellington on Allrecipes that has at least 200 reviews and an average rating of 4.5 stars or higher. List the main ingredients required for the dish.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: \"Did the agent find a recipe for Beef Wellington\",\n    });\n\n    logger.log(agentResult);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/amazon_shoes_cart.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const amazon_shoes_cart: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.amazon.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"go to amazon, and add a pair of black running shoes to cart in size 14. stop after you add the item to cart, and reach the login page\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/apple_trade_in.ts",
    "content": "//this eval is expected to fail due to issues scrolling within the trade in dialog\nimport { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const apple_trade_in: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.apple.com/shop/trade-in\");\n    const evaluator = new V3Evaluator(v3);\n    await agent.execute({\n      instruction:\n        \"Find out the trade-in value for an iPhone 13 Pro Max in good condition on the Apple website.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Did the agent find the trade-in value for an iPhone 13 Pro Max in good condition on the Apple website?\",\n      screenshot: false,\n      answer: \"360\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/apple_tv.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const apple_tv: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.apple.com/\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Identify the size and weight for the Apple TV 4K and list the Siri Remote features introduced.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const result = await evaluator.ask({\n      question:\n        \"did the agent find the height and width of the Apple TV 4K in its reasoning which is 1.2 and 3.66?\",\n      answer: agentResult.message,\n    });\n\n    const success = result.evaluation === \"YES\";\n    if (!success) {\n      return {\n        _success: false,\n        message: agentResult.message,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/arxiv_gpt_report.ts",
    "content": "//agent often fails on this one,\nimport { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const arxiv_gpt_report: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://arxiv.org/\");\n\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Find the paper 'GPT-4 Technical Report', when was v3 submitted?\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 25,\n    });\n\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    // Mon, 27 Mar 2023 17:46:54 UTC\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `Did the agent complete this task successfully? ${instruction}, the correct answer the agent should have provided is '03-27-2023'`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/columbia_tuition.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const columbia_tuition: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://columbia.edu/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Use the search functionality to locate pages detailing tuition and fees, then extract the published tuition fee information for undergraduate programs. Only use http://columbia.edu to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/flipkart_laptops.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const flipkart_laptops: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.flipkart.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"In the 'Laptops' section, apply the filter for 'Dell' and extract the average discount percentage on the first 3 Dell laptops displayed. Only use http://flipkart.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/gaia.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\n/**\n * Data-driven GAIA agent eval\n * - Expects per-test params injected via eval runner: { id, level, web, ques }\n * - Starts at `web`, runs the agent with `ques` as instruction\n * - Requires the agent to output a final answer in the form: \"Final Answer: <value>\"\n * - Marks success if such an answer string is present (exact matching against dataset can be layered later)\n */\nexport const gaia: EvalFunction = async ({\n  v3,\n  logger,\n  debugUrl,\n  sessionUrl,\n  modelName,\n  input,\n}) => {\n  try {\n    const params = ((input && input.params) || {}) as {\n      id?: string;\n      level?: number;\n      web?: string;\n      ques?: string;\n    };\n\n    if (!params.web || !params.ques) {\n      logger.error({\n        category: \"gaia\",\n        level: 0,\n        message: `Missing GAIA params (web, ques).`,\n        auxiliary: {\n          params: { value: JSON.stringify(params), type: \"object\" },\n        },\n      });\n      return {\n        _success: false,\n        error: `Missing GAIA params (web, ques). Got: ${JSON.stringify(params)}`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    const page = v3.context.pages()[0];\n    await page.goto(params.web);\n\n    const agent = v3.agent({\n      model: modelName,\n      systemPrompt: `You are a helpful assistant that must solve the task by browsing. You must produce a single line at the end like: \"Final Answer: <answer>\". Do not ask follow up questions. Current page: ${await page.title()}`,\n    });\n\n    const result = await agent.execute({\n      instruction: params.ques,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    const expected = (params as Record<string, unknown>).expected as\n      | string\n      | undefined;\n    const evaluator = new V3Evaluator(v3);\n    const evalResult = await evaluator.ask({\n      question: `Did the agent provide the expected answer: \"${expected}\"?`,\n      answer: result?.message || \"\",\n      screenshot: false,\n    });\n\n    return {\n      _success: evalResult.evaluation === \"YES\",\n      reasoning: evalResult.reasoning,\n      expectedAnswer: expected,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    logger.error({\n      category: \"gaia\",\n      level: 0,\n      message: `Unhandled error in GAIA task`,\n      auxiliary: {\n        error: {\n          value: error instanceof Error ? error.message : String(error),\n          type: \"string\",\n        },\n        trace: {\n          value: error instanceof Error && error.stack ? error.stack : \"\",\n          type: \"string\",\n        },\n      },\n    });\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/github.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const github: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://github.com/\");\n    const evaluator = new V3Evaluator(v3);\n    const agentResult = await agent.execute({\n      instruction:\n        \"Find a Ruby repository on GitHub that has been updated in the past 3 days and has at least 1000 stars.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n    logger.log(agentResult);\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Ruby repository on GitHub that has been updated in the past 3 days and has at least 1000 stars.\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/github_react_version.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const github_react_version: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  v3,\n  agent,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://github.com/\");\n\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Check the latest release version of React and the date it was published.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `Did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/google_flights.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const google_flights: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://google.com/travel/flights\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Search for flights from San Francisco to New York for next weekend\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n    logger.log(agentResult);\n\n    const evaluator = new V3Evaluator(v3);\n    const result = await evaluator.ask({\n      question:\n        \"Does the page show flights (options, available flights, not a search form) from San Francisco to New York?\",\n    });\n\n    if (result.evaluation !== \"YES\" && result.evaluation !== \"NO\") {\n      return {\n        _success: false,\n        observations: \"Evaluator provided an invalid response\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    if (result.evaluation === \"YES\") {\n      return {\n        _success: true,\n        observations: result.reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    } else {\n      return {\n        _success: false,\n        observations: result.reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/google_maps.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const google_maps: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://maps.google.com\");\n\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"How long does it take to get from San Francisco to New York driving?\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 15,\n    });\n\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `Did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/google_maps_2.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const google_maps_2: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://maps.google.com\");\n\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Search for the fastest walking route from La Puerta de Alcalá to La Puerta del Sol\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `Did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    if (evaluation !== \"YES\") {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/google_maps_3.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const google_maps_3: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://maps.google.com/\");\n    const evaluator = new V3Evaluator(v3);\n    await agent.execute({\n      instruction:\n        \"Search for locksmiths open now but not open 24 hours in Texas City.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 35,\n    });\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Does the page show a locksmiths open now but not open 24 hours in Texas City?\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/google_shopping.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const google_shopping: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.google.com/shopping\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Find a drip coffee maker that is on sale and within $25-60 and has a black finish\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n    logger.log(agentResult);\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Does the page show a drip coffee maker that is on sale and within $25-60 and has a black finish?\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/hotel_booking.ts",
    "content": "//this eval is expected to fail.\nimport { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const hotel_booking: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.booking.com/\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Find a hotel in Sydney with a rating of 8 or higher, providing free Wi-Fi and parking, available for a four-night stay starting on December 10, 2025.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n    logger.log(agentResult);\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Does the page show a hotel in Sydney with a rating of 8 or higher, providing free Wi-Fi and parking, available for a four-night stay starting on December 10, 2025?\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/hotels_paris_amenities.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const hotels_paris_amenities: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.hotels.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Filter search results for properties in Paris available next month that offer spa amenities and bars, and list the amenities of the first three hotels. Only use http://hotels.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/hugging_face.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const hugging_face: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const evaluator = new V3Evaluator(v3);\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://huggingface.co/\");\n    const agentResult = await agent.execute({\n      instruction:\n        \"Search for a model on Hugging Face with an Apache-2.0 license that has received the highest number of likes.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n    console.log(`agentResult: ${agentResult.message}`);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Does the message mention 'kokoro-82m' or 'hexgrad/Kokoro-82M'?\",\n      answer: agentResult.message || \"\",\n      screenshot: false,\n    });\n\n    const success = evaluation === \"YES\";\n\n    console.log(`reasoning: ${reasoning}`);\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/iframe_form.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const iframe_form: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-form-filling/\",\n    );\n\n    const agentResult = await agent.execute({\n      instruction: \"Fill in the form name with 'John Smith'\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 5,\n    });\n    logger.log(agentResult);\n\n    const evaluator = new V3Evaluator(v3);\n    const result = await evaluator.ask({\n      question: \"Is the form name input filled with 'John Smith'?\",\n    });\n\n    if (result.evaluation !== \"YES\" && result.evaluation !== \"NO\") {\n      return {\n        _success: false,\n        observations: \"Evaluator provided an invalid response\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const agentResult2 = await agent.execute({\n      instruction: \"Fill in the form email with 'john.smith@example.com'\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 3,\n    });\n    logger.log(agentResult2);\n\n    await page.scroll(0, 0, 0, -1000);\n    const result2 = await evaluator.ask({\n      question: \"Is the form email input filled with 'john.smith@example.com'?\",\n      screenshot: true,\n    });\n\n    if (result2.evaluation !== \"YES\" && result2.evaluation !== \"NO\") {\n      return {\n        _success: false,\n        observations: \"Evaluator provided an invalid response\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    if (result.evaluation === \"YES\" && result2.evaluation === \"YES\") {\n      return {\n        _success: true,\n        observations: \"All fields were filled correctly\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    } else {\n      return {\n        _success: false,\n        observations: \"One or more fields were not filled correctly\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/iframe_form_multiple.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const iframe_form_multiple: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-form-filling/\",\n    );\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Fill in the first name with 'John', the last name with 'Smith', the email with 'john.smith@example.com', and select the email radio button as preferred contact method\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 10,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `Did the agent complete this task successfully? ${instruction}. The form should have: first name = 'John', last name = 'Smith', email = 'john.smith@example.com', and the email radio button selected as preferred contact method.`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/instacart_organic_bananas.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const instacart_organic_bananas: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.instacart.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Search for organic bananas on Instacart and list the top 3 prices along with their retailer names. Only use http://instacart.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/kayak.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const kayak: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const evaluator = new V3Evaluator(v3);\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.kayak.com\");\n\n    await agent.execute({\n      instruction: \"Find flights from San Francisco to Tokyo next week\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 25,\n    });\n    await agent.execute({\n      instruction: \"Sort the flights by price\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 8,\n    });\n\n    if (v3.context.pages().length !== 2) {\n      return {\n        _success: false,\n        message: \"No new pages were opened\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    const { evaluation, reasoning } = await evaluator.ask({\n      question:\n        \"Are the flights shown sorted by price? Check the sort button in the top left corner of the page. It should show cheapest first; use this as the success criteria since the page might promote other flights and not show the list in order.\",\n    });\n\n    const success = evaluation === \"YES\";\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/kfc_tenders_combo.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const kfc_tenders_combo: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.kfc.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Add a 5-piece Tenders Combo to my bag with Sweet Corn as the side, Sweet Tea as the drink, and both Honey BBQ and Honey Mustard sauces. Select the store closest to Zip code 10001 for pick-up tomorrow at 12:00 PM.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/kith.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const kith: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const evaluator = new V3Evaluator(v3);\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://kith.com/collections/nike-air-force-1/products/nkcw2288-111?variant=19439468707968\",\n    );\n\n    await agent.execute({\n      instruction:\n        \"add the shoes to cart, go to checkout, and fill the delivery information. Don't fill the payment information\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 25,\n    });\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: \"Did the agent fill the delivery information\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (success) {\n      await agent.execute({\n        instruction:\n          \"fill the credit card information, do not submit the order just add placeholders\",\n        maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 10,\n      });\n\n      const { evaluation: evaluation2, reasoning: reasoning2 } =\n        await evaluator.ask({\n          question: \"Did the agent fill the payment information\",\n        });\n\n      const success2 = evaluation2 === \"YES\";\n\n      if (success2) {\n        return {\n          _success: true,\n          debugUrl,\n          sessionUrl,\n          logs: logger.getLogs(),\n        };\n      } else {\n        return {\n          _success: false,\n          message: reasoning2,\n          debugUrl,\n          sessionUrl,\n          logs: logger.getLogs(),\n        };\n      }\n    } else {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/made_in_china_supplier.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const made_in_china_supplier: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.made-in-china.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Navigate to the suppliers profiles section, select a verified supplier offering 'electronic components', and extract the certification details provided on their profile. Only use http://made-in-china.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/nba_trades.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const nba_trades: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    const evaluator = new V3Evaluator(v3);\n    await page.goto(\"https://www.espn.com/\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Find the latest Team transaction in the NBA within the past week.\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 25,\n    });\n    logger.log(agentResult);\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: \"Did the agent make it to the nba transactions page?\",\n    });\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/nvidia_hgx_driver.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const nvidia_hgx_driver: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://nvidia.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Find the HGX H100 driver for Ubuntu 22.04 on AMD64 CPU. use https://nvidia.com/ to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/oed_word_search.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const oed_word_search: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.oed.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Filter search results to show only entries for words first used from 1500 to 1600 and list the headwords of the first 10 results. Only use http://oed.com/ to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/onlineMind2Web.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\nimport { imageResize } from \"../../utils/imageResize.js\";\n\nexport const onlineMind2Web: EvalFunction = async ({\n  v3,\n  logger,\n  debugUrl,\n  sessionUrl,\n  modelName,\n  input,\n}) => {\n  let screenshotCollector: ScreenshotCollector | null = null;\n\n  try {\n    const params = ((input && input.params) || {}) as {\n      task_id?: string;\n      confirmed_task?: string;\n      website?: string;\n      reference_length?: number;\n      level?: string;\n    };\n\n    if (!params.website || !params.confirmed_task) {\n      return {\n        _success: false,\n        error: `Missing onlineMind2Web params (website, confirmed_task). Got: ${JSON.stringify(params)}`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    const page = v3.context.pages()[0];\n    await page.goto(params.website, {\n      timeoutMs: 120_000,\n    });\n\n    const agent = v3.agent({\n      cua: true,\n      model: modelName,\n      systemPrompt: `You are a helpful assistant that must solve the task by browsing. At the end, produce a single line: \"Final Answer: <answer>\" summarizing the requested result (e.g., score, list, or text). Current page: ${await page.title()}. ALWAYS OPERATE WITHIN THE PAGE OPENED BY THE USER, WHICHEVER TASK YOU ARE ATTEMPTING TO COMPLETE CAN BE ACCOMPLISHED WITHIN THE PAGE.`,\n    });\n\n    screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 7,\n    });\n    screenshotCollector.start();\n\n    const agentResult = await agent.execute({\n      instruction: params.confirmed_task,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    // Stop collecting and get all screenshots\n    let screenshots = await screenshotCollector.stop();\n\n    // Resize screenshots if we have any\n    if (screenshots.length > 0) {\n      screenshots = await Promise.all(\n        screenshots.map(async (screenshot) => {\n          return await imageResize(screenshot, 0.7);\n        }),\n      );\n    }\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const evalResult = await evaluator.ask({\n      question: `Did the agent successfully complete this task: \"${params.confirmed_task}\"?`,\n      screenshot: screenshots,\n      agentReasoning:\n        agentResult.message ||\n        \"no reasoning available, agent potentially hit step limit\",\n    });\n\n    // Clear screenshot buffers to free memory\n    screenshots.length = 0;\n\n    return {\n      _success: evalResult.evaluation === \"YES\",\n      reasoning: evalResult.reasoning,\n      task_level: params.level,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    if (screenshotCollector) {\n      try {\n        await screenshotCollector.stop();\n      } catch {\n        // Ignore errors during cleanup\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/radiotimes_tv_schedule.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const radiotimes_tv_schedule: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://radiotimes.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Locate tonight's featured TV schedule on Radiotimes, and list the titles of shows airing on both BBC and ITV. Only use http://radiotimes.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/redfin_apartment_rental.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const redfin_apartment_rental: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://redfin.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    // Calculate move-in date as 30 days from now\n    const moveInDate = new Date();\n    moveInDate.setDate(moveInDate.getDate() + 30);\n    const moveInDateFormatted = moveInDate.toLocaleDateString(\"en-US\", {\n      year: \"numeric\",\n      month: \"long\",\n      day: \"numeric\",\n    });\n\n    const instruction = `Find a 2 bed and 1.5+ bath apartment listing for rent in New York, with a move in date of ${moveInDateFormatted}. use https://redfin.com/ to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.`;\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/sf_library_card.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const sf_library_card: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://sflib1.sfpl.org/selfreg\");\n    const agentResult = await agent.execute({\n      instruction: \"Fill in the 'street Address' field with '166 Geary St'\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 1,\n    });\n    logger.log(agentResult);\n    const evaluator = new V3Evaluator(v3);\n    const result = await evaluator.ask({\n      question:\n        \"Does the page show the 'street Address' field filled with '166 Geary St'?\",\n    });\n\n    if (result.evaluation !== \"YES\" && result.evaluation !== \"NO\") {\n      return {\n        _success: false,\n        observations: \"Evaluator provided an invalid response\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    if (result.evaluation === \"YES\") {\n      return {\n        _success: true,\n        observations: result.reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    } else {\n      return {\n        _success: false,\n        observations: result.reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/sf_library_card_multiple.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const sf_library_card_multiple: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://sflib1.sfpl.org/selfreg\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Fill in ALL the required fields with mock data. DO NOT submit the form\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 20,\n    });\n    logger.log(agentResult);\n\n    const evaluator = new V3Evaluator(v3);\n    const result = await evaluator.ask({\n      question: \"Does the page show all the required fields filled?\",\n    });\n\n    if (result.evaluation !== \"YES\" && result.evaluation !== \"NO\") {\n      return {\n        _success: false,\n        observations: \"Evaluator provided an invalid response\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    if (result.evaluation === \"YES\") {\n      return {\n        _success: true,\n        observations: result.reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    } else {\n      return {\n        _success: false,\n        observations: result.reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/sign_in.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\n\nexport const sign_in: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://v0-modern-login-flow.vercel.app/\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Sign in with the email address 'test@browserbaser.com' and the password 'stagehand=goated' \",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 15,\n    });\n    logger.log(agentResult);\n    const url = page.url();\n\n    if (url === \"https://v0-modern-login-flow.vercel.app/authorized\") {\n      return {\n        _success: true,\n        observations: url,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: false,\n      observations: url,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/steam_games.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\n\nexport const steam_games: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://store.steampowered.com/\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Show most played games in Steam. And tell me the number of players in game at this time\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    //strictly used url check and no extract as the top games / players can vary\n    const success = page.url().includes(\"https://store.steampowered.com/\");\n\n    if (!success) {\n      return {\n        _success: false,\n        message: agentResult.message,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/thegamer_opinion_article.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const thegamer_opinion_article: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.thegamer.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Locate an Opinion or Cultural Commentary article discussing modern gaming culture and summarize its central argument in one or two sentences. Only use http://thegamer.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 30,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/trailhead_superbadge.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const trailhead_superbadge: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://trailhead.salesforce.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Find the tasks needed to complete the Assess Your Access & Security Skills category in the secure your app trailhead\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/trivago.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\n\nexport const trivago: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.trivago.com/\");\n\n    const agentResult = await agent.execute({\n      instruction:\n        \"Find the cheapest room in the hotel H10 Tribeca in Madrid next weekend. Stop at the trivago page showing the results\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 13,\n    });\n    logger.log(agentResult);\n\n    const url = page.url();\n\n    if (\n      url.includes(\"hotel-h10-tribeca-madrid\") &&\n      url.includes(\"trivago.com\")\n    ) {\n      return {\n        _success: true,\n        observations: url,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    } else {\n      return {\n        _success: false,\n        observations: url,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/trustpilot_hr_companies.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const trustpilot_hr_companies: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://trustpilot.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Use Trustpilot's search function to filter HR & Recruiting located in 'London', then list the review summaries for the first three companies listed above 4.5 stars. Only use http://trustpilot.com to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/ubereats.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\n\nexport const ubereats: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const evaluator = new V3Evaluator(v3);\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.ubereats.com/\");\n\n    await agent.execute({\n      instruction:\n        \"Order a pizza from ubereats to 639 geary st in sf, call the task complete once the login page is shown after adding pizza and viewing the cart\",\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 35,\n    });\n\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: \"Did the agent make it to the login page?\",\n    });\n\n    const success =\n      evaluation === \"YES\" && page.url().includes(\"https://auth.uber.com/\");\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/uniqlo_mens_blazers.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const uniqlo_mens_blazers: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.uniqlo.com\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Show me the list of Men's Blazers, Black, Size M on Uniqlo.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/webmd_audiologist_search.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const webmd_audiologist_search: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://doctor.webmd.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Find the best Audiologist within 50 miles of New York, NY, with a rating of 4 and above. use https://doctor.webmd.com/ to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/webmd_ovulation_calculator.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\n\nexport const webmd_ovulation_calculator: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  logger,\n  agent,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.webmd.com/\");\n\n    // Start collecting screenshots throughout the agent's journey\n    const screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 15,\n    });\n    screenshotCollector.start();\n\n    const instruction =\n      \"Search for the ovulation calculator and enter Mar 1 as the first date of the period and calculate the date of ovulation and pregnancy test day. use https://www.webmd.com/ to achieve the task. Don't go to any other site. The task is achievable with just navigation from this site.\";\n    const agentResult = await agent.execute({\n      instruction,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 40,\n    });\n\n    // Stop and collect all screenshots from the journey\n    const screenshots = await screenshotCollector.stop();\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const { evaluation, reasoning } = await evaluator.ask({\n      question: `did the agent complete this task successfully? ${instruction}`,\n      screenshot: screenshots,\n      agentReasoning: agentResult.message,\n    });\n\n    console.log(`reasoning: ${reasoning}`);\n\n    const success = evaluation === \"YES\";\n\n    if (!success) {\n      return {\n        _success: false,\n        message: reasoning,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    return {\n      _success: false,\n      message: errorMessage,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/webtailbench.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\nimport { imageResize } from \"../../utils/imageResize.js\";\n\nexport const webtailbench: EvalFunction = async ({\n  v3,\n  logger,\n  debugUrl,\n  sessionUrl,\n  modelName,\n  input,\n}) => {\n  let screenshotCollector: ScreenshotCollector | null = null;\n\n  try {\n    const params = ((input && input.params) || {}) as {\n      id?: string;\n      category?: string;\n      ques?: string;\n      web?: string;\n    };\n\n    if (!params.ques) {\n      return {\n        _success: false,\n        error: `Missing webtailbench params (ques). Got: ${JSON.stringify(params)}`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const page = v3.context.pages()[0];\n    // web field is always empty in WebTailBench; start from Google\n    const startUrl = params.web || \"https://www.google.com\";\n    await page.goto(startUrl, {\n      timeoutMs: 120_000,\n    });\n\n    const agent = v3.agent({\n      cua: true,\n      model: modelName,\n      systemPrompt: `You are a helpful assistant that must solve the task by browsing. At the end, produce a single line: \"Final Answer: <answer>\" summarizing the requested result (e.g., score, list, or text). Current page: ${await page.title()}. You will need to navigate to the appropriate website to complete the task.`,\n    });\n\n    screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 8,\n    });\n    screenshotCollector.start();\n\n    const agentResult = await agent.execute({\n      instruction: params.ques,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    // Stop collecting and get all screenshots\n    let screenshots = await screenshotCollector.stop();\n\n    // Resize screenshots if we have any\n    if (screenshots.length > 0) {\n      screenshots = await Promise.all(\n        screenshots.map(async (screenshot) => {\n          return await imageResize(screenshot, 0.7);\n        }),\n      );\n    }\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const evalResult = await evaluator.ask({\n      question: `Did the agent successfully complete this task: \"${params.ques}\"? Note that the agent does not have purchasing/booking capabilities; mark as pass if the agent has successfully performed all necessary steps for the task up to the point of purchasing/booking/entering payment/user information`,\n      screenshot: screenshots,\n      agentReasoning:\n        agentResult.message ||\n        \"no reasoning available, agent potentially hit step limit\",\n    });\n\n    // Clear screenshot buffers to free memory\n    screenshots.length = 0;\n\n    return {\n      _success: evalResult.evaluation === \"YES\",\n      reasoning: evalResult.reasoning,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    if (screenshotCollector) {\n      try {\n        await screenshotCollector.stop();\n      } catch {\n        // Ignore errors during cleanup\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/agent/webvoyager.ts",
    "content": "import { EvalFunction } from \"../../types/evals.js\";\nimport { V3Evaluator } from \"@browserbasehq/stagehand\";\nimport { ScreenshotCollector } from \"../../utils/ScreenshotCollector.js\";\nimport { imageResize } from \"../../utils/imageResize.js\";\n\nexport const webvoyager: EvalFunction = async ({\n  v3,\n  logger,\n  debugUrl,\n  sessionUrl,\n  modelName,\n  input,\n}) => {\n  let screenshotCollector: ScreenshotCollector | null = null;\n\n  try {\n    const params = ((input && input.params) || {}) as {\n      id?: string;\n      web?: string;\n      ques?: string;\n      web_name?: string;\n    };\n\n    if (!params.web || !params.ques) {\n      return {\n        _success: false,\n        error: `Missing WebVoyager params (web, ques). Got: ${JSON.stringify(params)}`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const page = v3.context.pages()[0];\n    await page.goto(params.web, {\n      timeoutMs: 120_000,\n    });\n\n    const agent = v3.agent({\n      model: modelName,\n      systemPrompt: `You are a helpful assistant that must solve the task by browsing. At the end, produce a single line: \"Final Answer: <answer>\" summarizing the requested result (e.g., score, list, or text). Current page: ${await page.title()}`,\n    });\n\n    screenshotCollector = new ScreenshotCollector(v3, {\n      interval: 3000,\n      maxScreenshots: 7,\n    });\n    screenshotCollector.start();\n\n    const agentResult = await agent.execute({\n      instruction: params.ques,\n      maxSteps: Number(process.env.AGENT_EVAL_MAX_STEPS) || 50,\n    });\n\n    // Stop collecting and get all screenshots\n    let screenshots = await screenshotCollector.stop();\n\n    // Resize screenshots if we have any\n    if (screenshots.length > 0) {\n      screenshots = await Promise.all(\n        screenshots.map(async (screenshot) => {\n          return await imageResize(screenshot, 0.7);\n        }),\n      );\n    }\n\n    logger.log({\n      category: \"evaluation\",\n      message: `Collected ${screenshots.length} screenshots for evaluation`,\n      level: 1,\n    });\n\n    const evaluator = new V3Evaluator(v3);\n    const evalResult = await evaluator.ask({\n      question: `Did the agent successfully complete this task: \"${params.ques}\"?`,\n      screenshot: screenshots,\n      agentReasoning:\n        agentResult.message ||\n        \"no reasoning available, agent potentially hit step limit\",\n    });\n\n    // Clear screenshot buffers to free memory\n    screenshots.length = 0;\n\n    return {\n      _success: evalResult.evaluation === \"YES\",\n      reasoning: evalResult.reasoning,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    if (screenshotCollector) {\n      try {\n        await screenshotCollector.stop();\n      } catch {\n        // Ignore errors during cleanup\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/allrecipes.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const allrecipes: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.allrecipes.com/\", {\n      waitUntil: \"domcontentloaded\",\n    });\n\n    await v3.act('Type \"chocolate chip cookies\" in the search bar');\n    await v3.act(\"press enter\");\n\n    const recipeDetails = await v3.extract(\n      \"Extract the title of the first recipe and the total number of ratings it has received.\",\n      z.object({\n        title: z.string().describe(\"Title of the recipe\"),\n        total_ratings: z\n          .string()\n          .describe(\"Total number of ratings for the recipe\"),\n      }),\n    );\n\n    const { title, total_ratings } = recipeDetails;\n    const expectedTitle = \"Best Chocolate Chip Cookies\";\n    const expectedRatings = 19164;\n\n    const extractedRatings = parseInt(total_ratings.replace(/[^\\d]/g, \"\"), 10);\n    const isRatingsWithinRange =\n      extractedRatings >= expectedRatings - 1000 &&\n      extractedRatings <= expectedRatings + 1000;\n\n    if (title !== expectedTitle || !isRatingsWithinRange) {\n      const errors = [];\n      if (title !== expectedTitle) {\n        errors.push({\n          message: \"Extracted title does not match the expected title\",\n          expected: expectedTitle,\n          actual: title,\n        });\n      }\n      if (!isRatingsWithinRange) {\n        errors.push({\n          message: \"Extracted ratings are not within the expected range\",\n          expected: `${expectedRatings} ± 1000`,\n          actual: extractedRatings.toString(),\n        });\n      }\n\n      logger.error({\n        message: \"Failed to extract correct recipe details\",\n        level: 0,\n        auxiliary: {\n          errors: {\n            value: JSON.stringify(errors),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Recipe details extraction validation failed\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      recipeDetails: {\n        title,\n        total_ratings: extractedRatings,\n      },\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: \"Recipe details extraction validation failed\",\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/amazon_add_to_cart.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const amazon_add_to_cart: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/amazon/\",\n    );\n\n    await v3.act(\"click the 'Add to Cart' button\");\n\n    await v3.act(\"click the 'Proceed to checkout' button\");\n\n    const currentUrl = page.url();\n    const expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/amazon/sign-in.html\";\n\n    console.log(\"currentUrl\", currentUrl);\n    console.log(\"expectedUrl\", expectedUrl);\n    return {\n      _success: currentUrl === expectedUrl,\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/apple.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const apple: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.apple.com/iphone-16-pro/\");\n\n    await v3.act(\"click on the buy button\");\n    await v3.act(\"select the Pro Max model\");\n    await v3.act(\"select the natural titanium color\");\n    await v3.act(\"select the 256GB storage option\");\n    await v3.act(\"click on the 'select a smartphone' trade-in option\");\n\n    await v3.act(\"select the iPhone 13 mini model from the dropdown\");\n    await v3.act(\"select the iPhone 13 mini is in good condition\");\n\n    const successMessageLocator = page.locator(\n      'text=\"Good News. Your iPhone 13 mini qualifies for credit.\"',\n    );\n    const isVisible = await successMessageLocator.isVisible();\n\n    return {\n      _success: isVisible,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/arxiv.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const arxiv: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://arxiv.org/search/\");\n\n    await v3.act(\"type web agents with multimodal models in the search bar\");\n\n    await v3.act(\"hit enter\");\n\n    const paper_links = await v3.extract(\n      \"extract the titles and links for two papers\",\n      z.object({\n        papers: z\n          .array(\n            z.object({\n              title: z.string().describe(\"the title of the paper\"),\n              link: z.string().url().describe(\"the link to the paper\"),\n            }),\n          )\n          .describe(\"list of papers\"),\n      }),\n    );\n\n    if (\n      !paper_links ||\n      !paper_links.papers ||\n      paper_links.papers.length === 0\n    ) {\n      return {\n        _success: false,\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const papers = [];\n    for (const paper of paper_links.papers) {\n      if (paper.link) {\n        await page.goto(paper.link);\n        const abstract = await v3.extract(\n          \"extract details of the paper from the abstract\",\n          z.object({\n            category: z\n              .string()\n              .describe(\n                \"the category of the paper. one of {'Benchmark', 'Dataset', 'Model', 'Framework', 'System', 'Other'}\",\n              ),\n            problem: z\n              .string()\n              .describe(\n                \"summarize the problem that the paper is trying to solve in one sentence\",\n              )\n              .nullable(),\n            methodology: z\n              .string()\n              .describe(\n                \"summarize the methodology of the paper in one sentence\",\n              )\n              .nullable(),\n            results: z\n              .string()\n              .describe(\"summarize the results of the paper in one sentence\")\n              .nullable(),\n            conclusion: z\n              .string()\n              .describe(\"summarize the conclusion of the paper in one sentence\")\n              .nullable(),\n            code: z\n              .string()\n              .describe(\n                \"if provided, extract only the link to the code repository, without additional text. this is often optional and not always provided.\",\n              )\n              .nullable(),\n          }),\n        );\n\n        papers.push({\n          title: paper.title,\n          link: paper.link,\n          ...abstract,\n        });\n      }\n    }\n\n    if (!papers || papers.length === 0) {\n      return {\n        _success: false,\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (papers.length !== 2) {\n      logger.error({\n        message: \"incorrect number of papers extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: \"2\",\n            type: \"integer\",\n          },\n          actual: {\n            value: papers.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Incorrect number of papers extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    // Ensure that every paper has a problem and methodology\n    for (const paper of papers) {\n      if (!paper.problem || !paper.methodology) {\n        logger.error({\n          message: `paper missing problem or methodology`,\n          level: 0,\n          auxiliary: {\n            paper: {\n              value: JSON.stringify(paper),\n              type: \"object\",\n            },\n          },\n        });\n\n        return {\n          _success: false,\n          error: \"Incomplete paper information\",\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n        };\n      }\n    }\n\n    return {\n      _success: true,\n      papers,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    logger.error({\n      message: `error in arxiv function`,\n      level: 0,\n      auxiliary: {\n        error: {\n          value: error.message,\n          type: \"string\",\n        },\n        trace: {\n          value: error.stack,\n          type: \"string\",\n        },\n      },\n    });\n\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/bidnet.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const bidnet: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.bidnetdirect.com/\");\n\n    await v3.act('Click on the \"Construction\" keyword');\n\n    const expectedUrl =\n      \"https://www.bidnetdirect.com/public/solicitations/open?keywords=Construction\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl.startsWith(expectedUrl),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/checkboxes.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const checkboxes: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/checkboxes/\",\n    );\n\n    await v3.act(\"click the 'baseball' option\");\n\n    await v3.act(\"click the 'netball' option\");\n\n    const baseballChecked = await page\n      .locator('input[type=\"checkbox\"][name=\"sports\"][value=\"baseball\"]')\n      .isChecked();\n\n    const netballChecked = await page\n      .locator('input[type=\"checkbox\"][name=\"sports\"][value=\"netball\"]')\n      .isChecked();\n\n    return {\n      _success: baseballChecked && netballChecked,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (e) {\n    return {\n      _success: false,\n      error: e,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/combination_sauce.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const combination_sauce: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.saucedemo.com/\");\n\n    const { usernames, password } = await v3.extract(\n      \"extract the accepted usernames and the password for login\",\n      z.object({\n        usernames: z.array(z.string()).describe(\"the accepted usernames\"),\n        password: z.string().describe(\"the password for login\"),\n      }),\n    );\n\n    await v3.act(`enter username 'standard_user'`);\n\n    await v3.act(`enter password '${password}'`);\n\n    await v3.act(\"click on 'login'\");\n\n    const observations = await v3.observe(\"find all the 'add to cart' buttons\");\n\n    const url = page.url();\n\n    const usernamesCheck = usernames.length === 6;\n    const urlCheck = url === \"https://www.saucedemo.com/inventory.html\";\n    const observationsCheck = observations.length === 6;\n\n    return {\n      _success: usernamesCheck && urlCheck && observationsCheck,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/costar.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const costar: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.costar.com/\");\n\n    await v3.act(\"click on the first article\");\n\n    await v3.act(\"click on the learn more button for the first job\");\n\n    const articleTitle = await v3.extract(\n      \"extract the title of the article\",\n      z.object({\n        title: z.string().describe(\"the title of the article\").nullable(),\n      }),\n    );\n\n    logger.log({\n      message: \"got article title\",\n      level: 1,\n      auxiliary: {\n        articleTitle: {\n          value: JSON.stringify(articleTitle),\n          type: \"object\",\n        },\n      },\n    });\n\n    // Check if the title is more than 5 characters\n    const isTitleValid =\n      articleTitle.title !== null && articleTitle.title.length > 5;\n\n    await v3.close();\n\n    return {\n      title: articleTitle.title,\n      _success: isTitleValid,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    logger.error({\n      message: \"error in costar function\",\n      level: 0,\n      auxiliary: {\n        error: {\n          value: error.message,\n          type: \"string\",\n        },\n        trace: {\n          value: error.stack,\n          type: \"string\",\n        },\n      },\n    });\n\n    return {\n      title: null,\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/csr_in_oopif.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const csr_in_oopif: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // click inside an CSR (closed mode shadow) root that is inside an\n  // OOPIF (out of process iframe)\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-root-in-oopif/\",\n    );\n    await v3.act(\"click the button\");\n\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        message: `successfully clicked the button`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to click on the button`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/csr_in_spif.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const csr_in_spif: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // click inside an CSR (closed mode shadow) root that is inside an\n  // SPIF (same process iframe)\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-dom-in-spif/\",\n    );\n    await v3.act(\"click the button\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        message: `successfully clicked the button`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to click on the button`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/custom_dropdown.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const custom_dropdown: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  /**\n   * This eval is meant to test whether we do not incorrectly attempt\n   * the selectOptionFromDropdown method (defined in actHandlerUtils.ts) on a\n   * 'dropdown' that is not a <select> element.\n   *\n   * This kind of dropdown must be clicked to be expanded before being interacted\n   * with.\n   */\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/expand-dropdown/\",\n    );\n\n    await v3.act(\"choose Canada from the 'Select a Country' dropdown\");\n\n    // to test, we'll grab the full a11y tree, and make sure it contains 'Canada'\n    const extraction = await v3.extract();\n    const fullTree = extraction.pageText;\n\n    if (fullTree.includes(\"Canada\")) {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: \"unable to expand the dropdown\",\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error attempting to select an option from the dropdown: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/dropdown.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const dropdown: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/dropdown/\",\n    );\n\n    // click the dropdown element to expand it\n    const xpath = \"xpath=/html/body/div/div/button\";\n    await page.locator(xpath).click();\n\n    // type into the input box (which should be hidden behind the\n    // expanded dropdown)\n    await v3.act(\"type 'test fill' into the input field\");\n\n    const input = page.locator(`xpath=/html/body/div/input`);\n    const expectedValue = \"test fill\";\n\n    // get the value of the input box\n    const actualValue = await input.inputValue();\n\n    // pass if the value matches expected\n    return {\n      _success: actualValue === expectedValue,\n      expectedValue,\n      actualValue,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_aigrant_companies.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_aigrant_companies: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    const companyList = await v3.extract(\n      \"Extract all companies that received the AI grant and group them with their batch numbers as an array of objects. Each object should contain the company name and its corresponding batch number.\",\n      z.object({\n        companies: z.array(\n          z.object({\n            company: z.string(),\n            batch: z.string(),\n          }),\n        ),\n      }),\n    );\n    const companies = companyList.companies;\n    const expectedLength = 91;\n\n    const expectedFirstItem = {\n      company: \"Goodfire\",\n      batch: \"4\",\n    };\n\n    const expectedLastItem = {\n      company: \"Forefront\",\n      batch: \"1\",\n    };\n\n    if (companies.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of companies extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: companies.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of companies extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n    const firstItemMatches =\n      companies[0].company === expectedFirstItem.company &&\n      companies[0].batch === expectedFirstItem.batch;\n\n    if (!firstItemMatches) {\n      logger.error({\n        message: \"First company extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedFirstItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(companies[0]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"First company extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const lastItemMatches =\n      companies[companies.length - 1].company === expectedLastItem.company &&\n      companies[companies.length - 1].batch === expectedLastItem.batch;\n\n    if (!lastItemMatches) {\n      logger.error({\n        message: \"Last company extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedLastItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(companies[companies.length - 1]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Last company extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_aigrant_targeted.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_aigrant_targeted: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    const selector = \"/html/body/div/ul[5]/li[28]\";\n    const company = await v3.extract(\n      \"Extract the company name.\",\n      z.object({\n        company_name: z.string(),\n      }),\n      { selector: selector },\n    );\n\n    const companyName = company.company_name;\n\n    const expectedName = {\n      company_name: \"Coframe\",\n    };\n\n    const nameMatches = companyName == expectedName.company_name;\n\n    if (!nameMatches) {\n      logger.error({\n        message: \"extracted company name does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedName.company_name,\n            type: \"string\",\n          },\n          actual: {\n            value: companyName,\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Company name does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_aigrant_targeted_2.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_aigrant_targeted_2: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    const selector = \"/html/body/div/ul[5]/li[28]\";\n    const company = await v3.extract(\n      \"Extract the name of the company that comes after 'Coframe'.\",\n      z.object({\n        company_name: z.string(),\n      }),\n      { selector: selector },\n    );\n    const companyName = company.company_name;\n\n    // nameWeShouldNotGet matches the name of the company that comes after\n    // CoFrame on the website. Since we are using targeted_extract here,\n    // and passing in a selector that does NOT contain the nameWeShouldNotGet,\n    // the LLM should have no visibility into what comes after 'CoFrame' if\n    // targeted_extract is performing correctly\n    const nameWeShouldNotGet = {\n      company_name: \"OpusClip\",\n    };\n\n    const nameMatches = companyName == nameWeShouldNotGet.company_name;\n\n    if (nameMatches) {\n      logger.error({\n        message:\n          \"extracted company name matches the company name that we SHOULD NOT get\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: nameWeShouldNotGet.company_name,\n            type: \"string\",\n          },\n          actual: {\n            value: companyName,\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error:\n          \"extracted company name matches the company name that we SHOULD NOT get\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_apartments.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_apartments: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.apartments.com/san-francisco-ca/2-bedrooms/\", {\n      waitUntil: \"load\",\n    });\n    const apartment_listings = await v3.extract(\n      \"Extract all the apartment listings with their prices and their addresses.\",\n      z.object({\n        listings: z.array(\n          z.object({\n            price: z.string().describe(\"The price of the listing\"),\n            address: z.string().describe(\"The address of the listing\"),\n          }),\n        ),\n      }),\n    );\n\n    const listings = apartment_listings.listings;\n    const expectedLength = 40;\n\n    if (listings.length < expectedLength) {\n      logger.error({\n        message: \"Incorrect number of listings extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: listings.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of listings extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_area_codes.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_area_codes: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/ncc-area-codes/\",\n      { waitUntil: \"domcontentloaded\" },\n    );\n\n    const result = await v3.extract(\n      \"Extract ALL the Primary Center names and their corresponding Area Code, and the name of their corresponding Zone.\",\n      z.object({\n        primary_center_list: z.array(\n          z.object({\n            zone_name: z\n              .string()\n              .describe(\n                \"The name of the Zone that the Primary Center is in. For example, 'North Central Zone'.\",\n              ),\n            primary_center_name: z\n              .string()\n              .describe(\n                \"The name of the Primary Center. I.e., this is the name of the city or town.\",\n              ),\n            area_code: z\n              .string()\n              .describe(\n                \"The area code for the Primary Center. This will either be 2 or 3 digits.\",\n              ),\n          }),\n        ),\n      }),\n    );\n\n    const primaryCenterList = result.primary_center_list;\n    const expectedLength = 56;\n\n    const expectedFirstItem = {\n      zone_name: \"Lagos Zone\",\n      primary_center_name: \"Lagos\",\n      area_code: \"01\",\n    };\n\n    const expectedLastItem = {\n      zone_name: \"South-East\",\n      primary_center_name: \"Yenagoa\",\n      area_code: \"089\",\n    };\n\n    if (primaryCenterList.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of primary centers extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: primaryCenterList.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of primary centers extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n    const firstItemMatches =\n      primaryCenterList[0].zone_name === expectedFirstItem.zone_name &&\n      primaryCenterList[0].primary_center_name ===\n        expectedFirstItem.primary_center_name &&\n      primaryCenterList[0].area_code === expectedFirstItem.area_code;\n\n    if (!firstItemMatches) {\n      logger.error({\n        message: \"First primary center extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedFirstItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(primaryCenterList[0]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"First primary center extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const lastItemMatches =\n      primaryCenterList[primaryCenterList.length - 1].zone_name ===\n        expectedLastItem.zone_name &&\n      primaryCenterList[primaryCenterList.length - 1].primary_center_name ===\n        expectedLastItem.primary_center_name &&\n      primaryCenterList[primaryCenterList.length - 1].area_code ===\n        expectedLastItem.area_code;\n\n    if (!lastItemMatches) {\n      logger.error({\n        message: \"Last primary center extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedLastItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(\n              primaryCenterList[primaryCenterList.length - 1],\n            ),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Last primary center extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_baptist_health.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { compareStrings } from \"../utils.js\";\nimport { z } from \"zod\";\n\nexport const extract_baptist_health: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/baptist-health/\",\n    );\n\n    const result = await v3.extract(\n      \"Extract the address, phone number, and fax number of the healthcare location.\",\n      z.object({\n        address: z.string(),\n        phone: z.string(),\n        fax: z.string(),\n      }),\n    );\n\n    const { address, phone, fax } = result;\n    const expected = {\n      address: \"2055 East South Blvd; Suite 908 Montgomery, AL 36116\",\n      phone: \"334-747-2273\",\n      fax: \"334-747-7501\",\n    };\n\n    const similarityThreshold = 0.85;\n    const failedFields: Array<{\n      field: string;\n      similarity: number;\n      expected: string;\n      actual: string;\n    }> = [];\n\n    const compareField = (\n      actualVal: string,\n      expectedVal: string,\n      fieldName: string,\n    ) => {\n      const { similarity, meetsThreshold } = compareStrings(\n        actualVal,\n        expectedVal,\n        similarityThreshold,\n      );\n\n      if (!meetsThreshold) {\n        failedFields.push({\n          field: fieldName,\n          similarity,\n          expected: expectedVal,\n          actual: actualVal,\n        });\n        logger.error({\n          message: `${fieldName} extracted does not meet similarity threshold`,\n          level: 0,\n          auxiliary: {\n            field: { value: fieldName, type: \"string\" },\n            similarity: { value: similarity.toFixed(2), type: \"string\" },\n            expected: { value: expectedVal, type: \"string\" },\n            actual: { value: actualVal, type: \"string\" },\n          },\n        });\n      }\n\n      return meetsThreshold;\n    };\n\n    const addressOk = compareField(address, expected.address, \"Address\");\n    const phoneOk = compareField(phone, expected.phone, \"Phone number\");\n    const faxOk = compareField(fax, expected.fax, \"Fax number\");\n\n    if (!addressOk || !phoneOk || !faxOk) {\n      return {\n        _success: false,\n        error: \"Some fields did not meet similarity threshold\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n        failedFields,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_capacitor_info.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { normalizeString } from \"../utils.js\";\nimport { z } from \"zod\";\n\nexport const extract_capacitor_info: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/capacitor/\",\n    );\n\n    const result = await v3.extract(\n      \"Extract the ECCN Code, RoHS Status, and Impedance.\",\n      z.object({\n        ECCN_code: z.string(),\n        RoHS_Status: z.string(),\n        Impedance: z.string(),\n      }),\n    );\n\n    const { ECCN_code, RoHS_Status, Impedance } = result;\n\n    const expected = {\n      ECCN_code: \"EAR99\",\n      RoHS_Status: \"RoHS Compliant\",\n      Impedance: \"12mOhm\",\n    };\n\n    if (normalizeString(ECCN_code) !== normalizeString(expected.ECCN_code)) {\n      logger.error({\n        message: \"ECCN code extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.ECCN_code),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(ECCN_code),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"ECCN code extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (\n      normalizeString(RoHS_Status) !== normalizeString(expected.RoHS_Status)\n    ) {\n      logger.error({\n        message: \"RoHS Status extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.RoHS_Status),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(RoHS_Status),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"RoHS Status extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (normalizeString(Impedance) !== normalizeString(expected.Impedance)) {\n      logger.error({\n        message: \"Impedance extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.Impedance),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(Impedance),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Impedance extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_collaborators.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_collaborators: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://github.com/facebook/react\");\n    await v3.act(\"find and click the contributors section\");\n\n    await v3.act(\"scroll halfway down the page\");\n\n    const { contributors } = await v3.extract(\n      \"Extract top 5 contributors of this repository\",\n      z.object({\n        contributors: z.array(\n          z.object({\n            github_username: z\n              .string()\n              .describe(\"the github username of the contributor\"),\n            commits: z.number().describe(\"number of commits contributed\"),\n          }),\n        ),\n      }),\n    );\n\n    const EXPECTED_CONTRIBUTORS = [\n      \"zpao\",\n      \"gaearon\",\n      \"sebmarkbage\",\n      \"acdlite\",\n      \"sophiebits\",\n    ];\n    return {\n      _success:\n        contributors.length === EXPECTED_CONTRIBUTORS.length &&\n        contributors.every(\n          (c, i) =>\n            EXPECTED_CONTRIBUTORS[i] === c.github_username && c.commits >= 1000,\n        ),\n      contributors,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_csa.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_csa: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/csa/\",\n    );\n\n    const result = await v3.extract(\n      \"Extract all the publications on the page including the publication date, session type, publication type, and annotation\",\n      z.object({\n        publications: z.array(\n          z.object({\n            publication_date: z.string(),\n            session_type: z.string(),\n            publication_type: z.string(),\n            annotation: z.string(),\n          }),\n        ),\n      }),\n    );\n\n    const publications = result.publications;\n    const expectedLength = 14;\n\n    const expectedFirstItem = {\n      publication_date: \"11-30-2024\",\n      session_type: \"Regular Session\",\n      publication_type: \"Assembly Weekly History\",\n      annotation:\n        \"2024 -- This publication includes the complete histories of second-year bills. The complete electronic history of all bills is always available at leginfo.legislature.ca.gov\",\n    };\n\n    const expectedLastItem = {\n      publication_date: \"11-30-2016\",\n      session_type: \"1st Extraordinary Session\",\n      publication_type: \"Assembly Weekly History\",\n      annotation: \"\",\n    };\n\n    if (publications.length < expectedLength) {\n      logger.error({\n        message: \"Incorrect number of publications extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: `>= ${expectedLength}`,\n            type: \"integer\",\n          },\n          actual: {\n            value: publications.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of publications extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const hasExpectedFirstItem = publications.some((publication) => {\n      return (\n        publication.publication_date === expectedFirstItem.publication_date &&\n        publication.session_type === expectedFirstItem.session_type &&\n        publication.publication_type === expectedFirstItem.publication_type &&\n        publication.annotation === expectedFirstItem.annotation\n      );\n    });\n\n    if (!hasExpectedFirstItem) {\n      logger.error({\n        message: \"Expected 'first' item not found in publications\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedFirstItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(publications),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Expected 'first' item not found in publications\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const hasExpectedLastItem = publications.some((publication) => {\n      return (\n        publication.publication_date === expectedLastItem.publication_date &&\n        publication.session_type === expectedLastItem.session_type &&\n        publication.publication_type === expectedLastItem.publication_type &&\n        publication.annotation === expectedLastItem.annotation\n      );\n    });\n\n    if (!hasExpectedLastItem) {\n      logger.error({\n        message: \"Expected 'last' item not found in publications\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedLastItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(publications),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Expected 'last' item not found in publications\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_geniusee.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_geniusee: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/geniusee/\",\n    );\n    const selector = \"/html/body/main/div[2]/div[2]/div[2]/table\";\n    const scalability = await v3.extract(\n      \"Extract the scalability comment in the table for Gemini (Google)\",\n      z.object({\n        scalability: z.string(),\n      }),\n      { selector: selector },\n    );\n\n    const scalabilityComment = scalability.scalability;\n\n    const expectedScalabilityComment = {\n      scalability: \"Scalable architecture with API access\",\n    };\n\n    const commentMatches =\n      scalabilityComment == expectedScalabilityComment.scalability;\n\n    if (!commentMatches) {\n      logger.error({\n        message: \"extracted scalability comment does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedScalabilityComment.scalability,\n            type: \"string\",\n          },\n          actual: {\n            value: scalabilityComment,\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"extracted scalability comment does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_geniusee_2.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_geniusee_2: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/geniusee/\",\n    );\n    const selector = \"/html/body/main/div[2]/div[2]/div[2]/table/tbody/tr[9]\";\n    const scalability = await v3.extract(\n      \"Extract the scalability comment in the table for Gemini (Google)\",\n      z.object({\n        scalability: z.string(),\n      }),\n      { selector: selector },\n    );\n\n    const scalabilityComment = scalability.scalability;\n\n    // scalabilityCommentWeShouldNotGet matches a scalability comment in the table,\n    // but since we are using targeted_extract here,\n    // and passing in a selector that does NOT contain the scalabilityCommentWeShouldNotGet,\n    // the LLM should have no visibility into scalabilityCommentWeShouldNotGet if\n    // targeted_extract is performing correctly\n    const scalabilityCommentWeShouldNotGet = {\n      scalability: \"Scalable architecture with API access\",\n    };\n\n    const commentMatches =\n      scalabilityComment == scalabilityCommentWeShouldNotGet.scalability;\n\n    if (commentMatches) {\n      logger.error({\n        message:\n          \"extracted scalability comment matches the scalability comment that we SHOULD NOT get\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: scalabilityCommentWeShouldNotGet.scalability,\n            type: \"string\",\n          },\n          actual: {\n            value: scalabilityComment,\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error:\n          \"scalability comment matches the scalability comment that we SHOULD NOT get\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_github_commits.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_github_commits: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://github.com/facebook/react\");\n\n    await v3.act(\n      \"find commit history, generally described by the number of commits\",\n    );\n    const { commits } = await v3.extract(\n      \"Extract last 20 commits\",\n      z.object({\n        commits: z.array(\n          z.object({\n            commit_message: z.string(),\n            commit_url: z.string(),\n            commit_hash: z.string(),\n          }),\n        ),\n      }),\n    );\n\n    logger.log({\n      message: \"Extracted commits\",\n      level: 1,\n      auxiliary: {\n        commits: {\n          value: JSON.stringify(commits),\n          type: \"object\",\n        },\n      },\n    });\n\n    return {\n      _success: commits.length === 20,\n      commits,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_github_stars.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_github_stars: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://github.com/facebook/react\");\n\n    const { stars } = await v3.extract(\n      \"Extract the number of stars for the project\",\n      z.object({\n        stars: z.number().describe(\"the number of stars for the project\"),\n      }),\n    );\n\n    const expectedStarsString = await page\n      .locator(\"#repo-stars-counter-star\")\n      .first()\n      .innerHtml();\n\n    const expectedStars = expectedStarsString.toLowerCase().endsWith(\"k\")\n      ? parseFloat(expectedStarsString.slice(0, -1)) * 1000\n      : parseFloat(expectedStarsString);\n\n    const tolerance = 1000;\n    const isWithinTolerance = Math.abs(stars - expectedStars) <= tolerance;\n\n    return {\n      _success: isWithinTolerance,\n      stars,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_hamilton_weather.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\nimport { compareStrings } from \"../utils.js\";\n\nexport const extract_hamilton_weather: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/hamilton-weather/\",\n    );\n    const xpath =\n      \"/html/body[1]/div[5]/main[1]/article[1]/div[6]/div[2]/div[1]/table[1]\";\n\n    const weatherData = await v3.extract(\n      \"extract the weather data for Sun, Feb 23 at 11PM\",\n      z.object({\n        temperature: z.string(),\n        weather_description: z.string(),\n        wind: z.string(),\n        humidity: z.string(),\n        barometer: z.string(),\n        visibility: z.string(),\n      }),\n      { selector: xpath },\n    );\n\n    // Define the expected weather data\n    const expectedWeatherData = {\n      temperature: \"27 °F\",\n      weather_description: \"Light snow. Overcast.\",\n      wind: \"6 mph\",\n      humidity: \"93%\",\n      barometer: '30.07 \"Hg',\n      visibility: \"10 mi\",\n    };\n\n    // Check that every field matches the expected value\n    const isWeatherCorrect =\n      compareStrings(\n        weatherData.temperature,\n        expectedWeatherData.temperature,\n        0.9,\n      ).meetsThreshold &&\n      compareStrings(\n        weatherData.weather_description,\n        expectedWeatherData.weather_description,\n        0.9,\n      ).meetsThreshold &&\n      compareStrings(weatherData.wind, expectedWeatherData.wind, 0.9)\n        .meetsThreshold &&\n      compareStrings(weatherData.humidity, expectedWeatherData.humidity, 0.9)\n        .meetsThreshold &&\n      compareStrings(weatherData.barometer, expectedWeatherData.barometer, 0.9)\n        .meetsThreshold &&\n      compareStrings(\n        weatherData.visibility,\n        expectedWeatherData.visibility,\n        0.9,\n      ).meetsThreshold;\n\n    return {\n      _success: isWeatherCorrect,\n      weatherData,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_jfk_links.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_jfk_links: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/jfk/\",\n    );\n\n    const extraction = await v3.extract(\n      \"extract all the record file name and their corresponding links\",\n      z.object({\n        records: z.array(\n          z.object({\n            file_name: z.string().describe(\"the file name of the record\"),\n            link: z.string().url(),\n          }),\n        ),\n      }),\n    );\n\n    // The list of records we expect to see\n    const expectedRecords = [\n      {\n        file_name: \"104-10003-10041.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10003-10041.pdf\",\n      },\n      {\n        file_name: \"104-10004-10143 (C06932208).pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10004-10143%20(C06932208).pdf\",\n      },\n      {\n        file_name: \"104-10004-10143.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10004-10143.pdf\",\n      },\n      {\n        file_name: \"104-10004-10156.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10004-10156.pdf\",\n      },\n      {\n        file_name: \"104-10004-10213.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10004-10213.pdf\",\n      },\n      {\n        file_name: \"104-10005-10321.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10005-10321.pdf\",\n      },\n      {\n        file_name: \"104-10006-10247.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10006-10247.pdf\",\n      },\n      {\n        file_name: \"104-10007-10345.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10007-10345.pdf\",\n      },\n      {\n        file_name: \"104-10009-10021.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10009-10021.pdf\",\n      },\n      {\n        file_name: \"104-10009-10222.pdf\",\n        link: \"https://www.archives.gov/files/research/jfk/releases/2025/0318/104-10009-10222.pdf\",\n      },\n    ];\n\n    const extractedRecords = extraction.records;\n\n    // Check that all expected records exist in the extraction\n    const missingRecords = expectedRecords.filter((expected) => {\n      return !extractedRecords.some(\n        (r) => r.file_name === expected.file_name && r.link === expected.link,\n      );\n    });\n\n    // Check that the extraction array is exactly length 10\n    if (extractedRecords.length !== 10) {\n      return {\n        _success: false,\n        reason: `Extraction has ${extractedRecords.length} records (expected 10).`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    if (missingRecords.length > 0) {\n      return {\n        _success: false,\n        reason: \"Missing one or more expected records.\",\n        missingRecords,\n        extractedRecords,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    // If we reach here, the number of records is correct, and all are present\n    return {\n      _success: true,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_jstor_news.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_jstor_news: EvalFunction = async ({\n  logger,\n\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/jstor/\",\n      {\n        waitUntil: \"load\",\n      },\n    );\n    await v3.act(\"close the cookie\");\n\n    const result = await v3.extract(\n      \"Extract ALL the news report titles and their dates.\",\n      z.object({\n        reports: z.array(\n          z.object({\n            report_name: z\n              .string()\n              .describe(\"The name or title of the news report.\"),\n            publish_date: z\n              .string()\n              .describe(\"The date the news report was published.\"),\n          }),\n        ),\n      }),\n    );\n\n    const reports = result.reports;\n    const expectedLength = 10;\n\n    const expectedFirstItem = {\n      report_name: \"JSTOR retires Publisher Sales Service\",\n      publish_date: \"December 9, 2024\",\n    };\n\n    const expectedLastItem = {\n      report_name: \"Path to Open announces 2024 titles\",\n      publish_date: \"May 10, 2024\",\n    };\n\n    if (reports.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of reports extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: reports.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of reports extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n    const firstItemMatches =\n      reports[0].report_name === expectedFirstItem.report_name &&\n      reports[0].publish_date === expectedFirstItem.publish_date;\n\n    if (!firstItemMatches) {\n      logger.error({\n        message: \"First report extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedFirstItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(reports[0]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"First report extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const lastItemMatches =\n      reports[reports.length - 1].report_name ===\n        expectedLastItem.report_name &&\n      reports[reports.length - 1].publish_date ===\n        expectedLastItem.publish_date;\n\n    if (!lastItemMatches) {\n      logger.error({\n        message: \"Last report extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedLastItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(reports[reports.length - 1]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Last report extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_memorial_healthcare.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\nimport { compareStrings } from \"../utils.js\";\n\nexport const extract_memorial_healthcare: EvalFunction = async ({\n  logger,\n\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/mycmh/\",\n    );\n\n    const result = await v3.extract(\n      \"extract a list of the first three healthcare centers on this page, with their name, full address, and phone number\",\n      z.object({\n        health_centers: z.array(\n          z.object({\n            name: z.string(),\n            phone_number: z.string(),\n            address: z.string(),\n          }),\n        ),\n      }),\n    );\n\n    const health_centers: Array<\n      Partial<{ name: string; phone_number: string; address: string }>\n    > = result.health_centers;\n\n    const expectedLength = 3;\n    const similarityThreshold = 0.85;\n\n    const expectedFirstItem = {\n      name: \"Community Memorial Breast Center\",\n      phone_number: \"805-948-5093\",\n      address: \"168 North Brent Street, Suite 401, Ventura, CA 93003\",\n    };\n\n    const expectedLastItem = {\n      name: \"Community Memorial Dermatology and Mohs Surgery\",\n      phone_number: \"805-948-6920\",\n      address: \"168 North Brent Street, Suite 403, Ventura, CA 93003\",\n    };\n\n    if (health_centers.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of health centers extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: health_centers.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Incorrect number of health centers extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const validateHealthCenter = (\n      center: Partial<{ name: string; phone_number: string; address: string }>,\n    ): { name: string; phone_number: string; address: string } | null => {\n      if (center.name && center.phone_number && center.address) {\n        return center as {\n          name: string;\n          phone_number: string;\n          address: string;\n        };\n      }\n      logger.error({\n        message: \"Invalid health center data\",\n        level: 0,\n        auxiliary: {\n          center: { value: JSON.stringify(center), type: \"object\" },\n        },\n      });\n      return null;\n    };\n\n    const validHealthCenters = health_centers\n      .map(validateHealthCenter)\n      .filter(Boolean) as Array<{\n      name: string;\n      phone_number: string;\n      address: string;\n    }>;\n\n    if (validHealthCenters.length < expectedLength) {\n      return {\n        _success: false,\n        error: \"One or more health centers have missing fields\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const compareField = (\n      actual: string,\n      expected: string,\n      fieldName: string,\n    ): boolean => {\n      const { similarity, meetsThreshold } = compareStrings(\n        actual,\n        expected,\n        similarityThreshold,\n      );\n\n      if (!meetsThreshold) {\n        logger.error({\n          message: `Field \"${fieldName}\" does not meet similarity threshold`,\n          level: 0,\n          auxiliary: {\n            field: { value: fieldName, type: \"string\" },\n            similarity: { value: similarity.toFixed(2), type: \"float\" },\n            expected: { value: expected, type: \"string\" },\n            actual: { value: actual, type: \"string\" },\n          },\n        });\n      }\n\n      return meetsThreshold;\n    };\n\n    const compareItem = (\n      actual: { name: string; phone_number: string; address: string },\n      expected: { name: string; phone_number: string; address: string },\n      position: string,\n    ): boolean => {\n      const fields = [\n        { field: \"name\", actual: actual.name, expected: expected.name },\n        {\n          field: \"phone_number\",\n          actual: actual.phone_number,\n          expected: expected.phone_number,\n        },\n        {\n          field: \"address\",\n          actual: actual.address,\n          expected: expected.address,\n        },\n      ];\n\n      return fields.every(({ field, actual, expected }) =>\n        compareField(actual, expected, `${position} ${field}`),\n      );\n    };\n\n    const firstItemMatches = compareItem(\n      validHealthCenters[0],\n      expectedFirstItem,\n      \"First\",\n    );\n    const lastItemMatches = compareItem(\n      validHealthCenters[validHealthCenters.length - 1],\n      expectedLastItem,\n      \"Last\",\n    );\n\n    if (!firstItemMatches || !lastItemMatches) {\n      return {\n        _success: false,\n        error: \"One or more fields do not match expected values\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_nhl_stats.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { normalizeString } from \"../utils.js\";\nimport { z } from \"zod\";\n\nexport const extract_nhl_stats: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://www.hockeydb.com/ihdb/stats/top_league.php?lid=nhl1927&sid=1990\",\n      {\n        waitUntil: \"domcontentloaded\",\n      },\n    );\n\n    const result = await v3.extract(\n      \"Extract the name of the goal scoring leader, their number of goals they scored, and the team they played for.\",\n      z.object({\n        name: z.string(),\n        num_goals: z.string(),\n        team: z.string(),\n      }),\n    );\n\n    const { name, num_goals, team } = result;\n\n    const expected = {\n      name: \"Brett Hull\",\n      num_goals: \"72\",\n      team: \"St. Louis\",\n    };\n\n    if (normalizeString(name) !== normalizeString(expected.name)) {\n      logger.error({\n        message: \"Player name extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.name),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(name),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Player name extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (normalizeString(num_goals) !== normalizeString(expected.num_goals)) {\n      logger.error({\n        message: \"Number of goals extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.num_goals),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(num_goals),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Number of goals extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (normalizeString(team) !== normalizeString(expected.team)) {\n      logger.error({\n        message: \"Player team extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.team),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(team),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Player team extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_partners.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_partners: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://ramp.com\");\n\n    await v3.act(\"scroll to the bottom of the page\");\n\n    await v3.act(\"Close the popup.\");\n\n    await v3.act(\"click on the link that leads to the partners page.\");\n\n    const partners = await v3.extract(\n      `Extract all of the partner categories on the page.`,\n      z.object({\n        partners: z.array(\n          z.object({\n            partner_category: z.string().describe(\"The partner category\"),\n          }),\n        ),\n        explanation: z\n          .string()\n          .optional()\n          .describe(\"Any explanation about partner listing or absence thereof\"),\n      }),\n    );\n\n    const expectedPartners = [\n      \"Accounting Partners\",\n      \"Private Equity & Venture Capital Partners\",\n      \"Services Partners\",\n      \"Affiliates\",\n    ];\n\n    const foundPartners = partners.partners.map((partner) =>\n      partner.partner_category.toLowerCase(),\n    );\n\n    const allExpectedPartnersFound = expectedPartners.every((partner) =>\n      foundPartners.includes(partner.toLowerCase()),\n    );\n\n    return {\n      _success: allExpectedPartnersFound,\n      partners,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    logger.error({\n      message: \"error in extractPartners function\",\n      level: 0,\n      auxiliary: {\n        error: {\n          value: error.message,\n          type: \"string\",\n        },\n        trace: {\n          value: error.stack,\n          type: \"string\",\n        },\n      },\n    });\n\n    return {\n      _success: false,\n      debugUrl,\n      sessionUrl,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_press_releases.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\nimport { compareStrings } from \"../utils.js\";\n\nexport const extract_press_releases: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  const schema = z.object({\n    items: z.array(\n      z.object({\n        title: z.string().describe(\"The title of the press release\"),\n        publish_date: z\n          .string()\n          .describe(\"The date the press release was published\"),\n      }),\n    ),\n  });\n\n  type PressRelease = z.infer<typeof schema>[\"items\"][number];\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/press-releases/\",\n    );\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    const rawResult = await v3.extract(\n      \"extract the title and corresponding publish date of EACH AND EVERY press releases on this page. DO NOT MISS ANY PRESS RELEASES.\",\n      schema,\n    );\n\n    const parsed = schema.parse(rawResult);\n    const { items } = parsed;\n\n    const expectedLength = 28;\n    const expectedFirstItem: PressRelease = {\n      title: \"UAW Region 9A Endorses Brad Lander for Mayor\",\n      publish_date: \"Dec 4, 2024\",\n    };\n    const expectedLastItem: PressRelease = {\n      title: \"Fox Sued by New York City Pension Funds Over Election Falsehoods\",\n      publish_date: \"Nov 12, 2023\",\n    };\n\n    if (items.length <= expectedLength) {\n      logger.error({\n        message: \"Not enough items extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: `> ${expectedLength}`,\n            type: \"string\",\n          },\n          actual: {\n            value: items.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Not enough items extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const isItemMatch = (item: PressRelease, expected: PressRelease) => {\n      const titleComparison = compareStrings(item.title, expected.title, 0.9);\n      const dateComparison = compareStrings(\n        item.publish_date,\n        expected.publish_date,\n        0.9,\n      );\n      return titleComparison.meetsThreshold && dateComparison.meetsThreshold;\n    };\n\n    const foundFirstItem = items.some((item) =>\n      isItemMatch(item, expectedFirstItem),\n    );\n    const foundLastItem = items.some((item) =>\n      isItemMatch(item, expectedLastItem),\n    );\n\n    return {\n      _success: foundFirstItem && foundLastItem,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    logger.error({\n      message: `Error in extract_press_releases function`,\n      level: 0,\n      auxiliary: {\n        error: {\n          value: (error as Error).message || JSON.stringify(error),\n          type: \"string\",\n        },\n        trace: {\n          value: (error as Error).stack,\n          type: \"string\",\n        },\n      },\n    });\n    return {\n      _success: false,\n      error: \"An error occurred during extraction\",\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_professional_info.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { normalizeString } from \"../utils.js\";\nimport { z } from \"zod\";\n\nexport const extract_professional_info: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/professional-info/\",\n    );\n\n    const result = await v3.extract(\n      \"Extract the list of Practices, phone number, and fax number of the professional.\",\n      z.object({\n        practices: z.array(z.string()),\n        phone: z.string(),\n        fax: z.string(),\n      }),\n    );\n\n    await v3.close();\n\n    const { practices, phone, fax } = result;\n\n    const expected = {\n      practices: [\n        \"Restructuring\",\n        \"Finance\",\n        \"Hybrid Capital & Special Situations\",\n        \"Private Credit\",\n      ],\n      phone: \"+1-212-373-3262\",\n      fax: \"+1-212-492-0262\",\n    };\n\n    if (\n      JSON.stringify(practices.map(normalizeString)) !==\n      JSON.stringify(expected.practices.map(normalizeString))\n    ) {\n      logger.error({\n        message: \"Practices extracted do not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expected.practices),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(practices),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Practices extracted do not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (normalizeString(phone) !== normalizeString(expected.phone)) {\n      logger.error({\n        message: \"Phone number extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.phone),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(phone),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Phone number extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (normalizeString(fax) !== normalizeString(expected.fax)) {\n      logger.error({\n        message: \"Fax number extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.fax),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(fax),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Fax number extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_public_notices.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\nimport { compareStrings } from \"../utils.js\";\n\nexport const extract_public_notices: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/sars/\",\n      { waitUntil: \"load\" },\n    );\n\n    const result = await v3.extract(\n      \"Extract ALL the public notice descriptions with their corresponding, GG number and publication date. Extract ALL notices from 2024 through 2020. Do not include the Notice number.\",\n      z.object({\n        public_notices: z.array(\n          z.object({\n            notice_description: z\n              .string()\n              .describe(\n                \"the description of the notice. Do not include the Notice number\",\n              ),\n            gg_number: z\n              .string()\n              .describe(\"the GG number of the notice. For example, GG 12345\"),\n            publication_date: z\n              .string()\n              .describe(\n                \"the publication date of the notice. For example, 8 December 2021\",\n              ),\n          }),\n        ),\n      }),\n    );\n\n    const publicNotices = result.public_notices;\n    const expectedLength = 24;\n\n    const expectedFirstItem = {\n      notice_description:\n        \"Additional considerations in terms of section 80(2) in respect of which an application for a binding private ruling or a binding class ruling may be rejected\",\n      gg_number: \"GG 51526\",\n      publication_date: \"8 November 2024\",\n    };\n\n    const expectedLastItem = {\n      notice_description:\n        \"Notice in terms of section 25, read with section 66(1) of the Income Tax Act, 1962, for submission of 2020 income tax returns\",\n      gg_number: \"GG 43495\",\n      publication_date: \"3 July 2020\",\n    };\n\n    if (publicNotices.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of public notices extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: publicNotices.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of public notices extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n    const firstItemMatches =\n      compareStrings(\n        publicNotices[0].notice_description,\n        expectedFirstItem.notice_description,\n        0.9,\n      ) &&\n      compareStrings(\n        publicNotices[0].gg_number,\n        expectedFirstItem.gg_number,\n        0.9,\n      ) &&\n      compareStrings(\n        publicNotices[0].publication_date,\n        expectedFirstItem.publication_date,\n        0.9,\n      );\n\n    if (!firstItemMatches) {\n      logger.error({\n        message: \"First public notice extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedFirstItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(publicNotices[0]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"First public notice extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const lastItemMatches =\n      compareStrings(\n        publicNotices[publicNotices.length - 1].notice_description,\n        expectedLastItem.notice_description,\n        0.9,\n      ) &&\n      compareStrings(\n        publicNotices[publicNotices.length - 1].gg_number,\n        expectedLastItem.gg_number,\n        0.9,\n      ) &&\n      compareStrings(\n        publicNotices[publicNotices.length - 1].publication_date,\n        expectedLastItem.publication_date,\n        0.9,\n      );\n\n    if (!lastItemMatches) {\n      logger.error({\n        message: \"Last public notice extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedLastItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(publicNotices[publicNotices.length - 1]),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Last public notice extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_recipe.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_recipe: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/allrecipes-extract/\",\n      {\n        waitUntil: \"domcontentloaded\",\n      },\n    );\n\n    const selector = \"/html/body/main/article/div[3]/div[3]/div[4]\";\n    const recipeDetails = await v3.extract(\n      \"Extract the title of the number of tablespoons of olive oil needed for the steak, and the number of teaspoons of lemon juice needed for the mushroom pan sauce.\",\n      z.object({\n        tablespoons_olive_oil: z\n          .number()\n          .describe(\n            \"the number of tablespoons of olive oil needed for the steak\",\n          ),\n        teaspoons_lemon_juice: z\n          .number()\n          .describe(\n            \"the number of teaspoons of lemon juice needed for the mushroom pan sauce\",\n          ),\n      }),\n      { selector: selector },\n    );\n\n    const { tablespoons_olive_oil, teaspoons_lemon_juice } = recipeDetails;\n    const expectedTablespoons = 2;\n    const expectedTeaspoons = 2;\n\n    if (\n      tablespoons_olive_oil !== expectedTablespoons ||\n      teaspoons_lemon_juice !== expectedTeaspoons\n    ) {\n      const errors = [];\n      if (tablespoons_olive_oil !== expectedTablespoons) {\n        errors.push({\n          message:\n            \"Extracted tablespoons of olive oil do not match the extracted tablespoons of olive oil\",\n          expected: expectedTablespoons.toString(),\n          actual: tablespoons_olive_oil.toString(),\n        });\n      }\n      if (teaspoons_lemon_juice !== expectedTeaspoons) {\n        errors.push({\n          message:\n            \"Extracted teaspoons of lemon juice do not match the extracted teaspoons of lemon juice\",\n          expected: expectedTeaspoons.toString(),\n          actual: teaspoons_lemon_juice.toString(),\n        });\n      }\n\n      logger.error({\n        message: \"Failed to extract correct recipe details\",\n        level: 0,\n        auxiliary: {\n          errors: {\n            value: JSON.stringify(errors),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Recipe details extraction validation failed\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      recipeDetails: {\n        tablespoons_olive_oil: expectedTablespoons,\n        teaspoons_lemon_juice: expectedTeaspoons,\n      },\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_regulations_table.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_regulations_table: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/ncc-numbering-plan/\",\n    );\n\n    const xpath =\n      \"/html/body/div[3]/main/div[2]/div[2]/div/div/div[2]/article/div[2]/div[1]/div/table\";\n\n    const allottees = await v3.extract(\n      \"Extract ALL of the Allottees and their corresponding name, area, and area code.\",\n      z.object({\n        allottee_list: z.array(\n          z.object({\n            allottee_name: z.string(),\n            area: z.string(),\n            area_code: z.string(),\n            access_code: z.string(),\n          }),\n        ),\n      }),\n      { selector: xpath },\n    );\n\n    // Define the expected weather data\n    const allottees_expected_first = {\n      allottee_name: \"101 Communications Limited\",\n      area: \"Lagos\",\n      area_code: \"0201\",\n      access_code: \"249\",\n    };\n\n    const allottees_expected_last = {\n      allottee_name: \"Airtel Networks Limited\",\n      area: \"National\",\n      area_code: \"0708\",\n      access_code: \"708\",\n    };\n\n    const expected_length = 25;\n\n    const allotteeList = allottees.allottee_list;\n\n    // Check that the first entry, last entry, and total number match expectations\n    const isFirstCorrect =\n      JSON.stringify(allotteeList[0]) ===\n      JSON.stringify(allottees_expected_first);\n    const isLastCorrect =\n      JSON.stringify(allotteeList[allotteeList.length - 1]) ===\n      JSON.stringify(allottees_expected_last);\n    const isLengthCorrect = allotteeList.length === expected_length;\n\n    const isRegulationsCorrect =\n      isFirstCorrect && isLastCorrect && isLengthCorrect;\n\n    return {\n      _success: isRegulationsCorrect,\n      regulationsData: allottees,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_repo_name.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_repo_name: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://github.com/facebook/react\");\n\n    const { extraction } = await v3.extract(\n      \"extract the title of the Github repository. Do not include the owner of the repository.\",\n    );\n\n    logger.log({\n      message: \"Extracted repo title\",\n      level: 1,\n      auxiliary: {\n        repo_name: {\n          value: extraction,\n          type: \"object\",\n        },\n      },\n    });\n\n    return {\n      _success: extraction === \"react\",\n      extraction,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_resistor_info.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { normalizeString } from \"../utils.js\";\nimport { z } from \"zod\";\n\nexport const extract_resistor_info: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/resistor/\",\n    );\n\n    const result = await v3.extract(\n      \"Extract the manufacturer standard lead time, tolerance percentage, resistance, and operating temperature range of the resistor.\",\n      z.object({\n        manufacturer_standard_lead_time: z.string(),\n        tolerance_percentage: z.string(),\n        resistance: z.string(),\n        operating_temperature_range: z.string(),\n      }),\n    );\n\n    const {\n      manufacturer_standard_lead_time,\n      tolerance_percentage,\n      resistance,\n      operating_temperature_range,\n    } = result;\n\n    const expected = {\n      manufacturer_standard_lead_time: \"11 Weeks\",\n      tolerance_percentage: \"±5\",\n      resistance: \"330 ohms\",\n      operating_temperature_range: \"-55°C ~ 155°C\",\n    };\n\n    if (\n      normalizeString(manufacturer_standard_lead_time) !==\n      normalizeString(expected.manufacturer_standard_lead_time)\n    ) {\n      logger.error({\n        message:\n          \"manufacturer standard lead time extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.manufacturer_standard_lead_time),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(manufacturer_standard_lead_time),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error:\n          \"manufacturer standard lead time extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (\n      normalizeString(tolerance_percentage) !==\n      normalizeString(expected.tolerance_percentage)\n    ) {\n      logger.error({\n        message: \"Tolerance percentage extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.tolerance_percentage),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(tolerance_percentage),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Tolerance percentage extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (normalizeString(resistance) !== normalizeString(expected.resistance)) {\n      logger.error({\n        message: \"resistance extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.resistance),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(resistance),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"resistance extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    if (\n      normalizeString(operating_temperature_range) !==\n      normalizeString(expected.operating_temperature_range)\n    ) {\n      logger.error({\n        message:\n          \"Operating temperature range extracted does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: normalizeString(expected.operating_temperature_range),\n            type: \"string\",\n          },\n          actual: {\n            value: normalizeString(operating_temperature_range),\n            type: \"string\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Operating temperature range extracted does not match expected\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_rockauto.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_rockauto: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/rockauto/\",\n    );\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n    const result = await v3.extract(\n      \"Extract the part number of all the coolant and antifreeze products in the 'economy' category. \" +\n        \"Do not include the manufacturer name. Do not include products from the premium category.\",\n      z.object({\n        coolant_products: z.array(\n          z.object({\n            part_number: z.string(),\n          }),\n        ),\n      }),\n    );\n\n    const coolantProducts = result.coolant_products;\n    const expectedPartNumbers = [\n      \"GREEN5050GAL\",\n      \"719009\",\n      \"AF3300\",\n      \"AF3100\",\n      \"MV5050GAL\",\n    ];\n    const expectedLength = expectedPartNumbers.length;\n\n    if (coolantProducts.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of coolant products extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: coolantProducts.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of coolant products extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const missingParts = expectedPartNumbers.filter(\n      (expectedPart) =>\n        !coolantProducts.some((p) => p.part_number === expectedPart),\n    );\n\n    if (missingParts.length > 0) {\n      logger.error({\n        message: \"Missing expected part number(s)\",\n        level: 0,\n        auxiliary: {\n          missingParts: {\n            value: JSON.stringify(missingParts),\n            type: \"object\",\n          },\n          actualExtracted: {\n            value: JSON.stringify(coolantProducts),\n            type: \"object\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: `One or more expected part numbers were not found: ${missingParts.join(\", \")}`,\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_single_link.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const extract_single_link: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/geniusee/\",\n    );\n\n    const extraction = await v3.extract(\n      \"extract the link to the 'contact us' page\",\n      z.object({\n        link: z.string().url(),\n      }),\n    );\n    const extractedLink = extraction.link;\n    const expectedLink =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/geniusee/#contact\";\n\n    if (extractedLink === expectedLink) {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      reason: `Extracted link: ${extractedLink} does not match expected link: ${expectedLink}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_snowshoeing_destinations.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_snowshoeing_destinations: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://www.cbisland.com/blog/10-snowshoeing-adventures-on-cape-breton-island/\",\n    );\n\n    await v3.act(\"accept the cookies\");\n\n    const snowshoeing_regions = await v3.extract(\n      \"Extract all the snowshoeing regions and the names of the trails within each region.\",\n      z.object({\n        snowshoeing_regions: z.array(\n          z.object({\n            region_name: z\n              .string()\n              .describe(\"The name of the snowshoeing region\"),\n            trails: z\n              .array(\n                z.object({\n                  trail_name: z.string().describe(\"The name of the trail\"),\n                }),\n              )\n              .describe(\"The list of trails available in this region.\"),\n          }),\n        ),\n      }),\n    );\n\n    logger.log({\n      message: \"Extracted destinations and trails\",\n      level: 1,\n      auxiliary: {\n        destinations: {\n          value: JSON.stringify(snowshoeing_regions),\n          type: \"object\",\n        },\n      },\n    });\n\n    const _success = snowshoeing_regions.snowshoeing_regions.length === 10;\n\n    return {\n      _success,\n      snowshoeing_regions,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    logger.error({\n      message: \"Error in extract_snowshoeing_destinations function\",\n      level: 0,\n      auxiliary: {\n        error: {\n          value: error.message,\n          type: \"string\",\n        },\n        trace: {\n          value: error.stack,\n          type: \"string\",\n        },\n      },\n    });\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_staff_members.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_staff_members: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/panamcs/\",\n    );\n\n    const result = await v3.extract(\n      \"extract a list of ALL the staff members on this page, with their name and their job title\",\n      z.object({\n        staff_members: z.array(\n          z.object({\n            name: z.string(),\n            job_title: z.string(),\n          }),\n        ),\n      }),\n    );\n\n    const staff_members = result.staff_members;\n\n    const expectedLength = 50;\n\n    const expectedFirstItem = {\n      name: \"Louis Alvarez\",\n      job_title: \"School Resource Officer\",\n    };\n\n    const expectedLastItem = {\n      name: \"Jessica Zipin\",\n      job_title: \"School Based Therapist\",\n    };\n\n    if (staff_members.length !== expectedLength) {\n      logger.error({\n        message: \"Incorrect number of items extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: staff_members.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Incorrect number of staff members extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    // Check for the presence of the expected items\n    const firstItemExists = staff_members.some(\n      (member) =>\n        member.name === expectedFirstItem.name &&\n        member.job_title === expectedFirstItem.job_title,\n    );\n\n    if (!firstItemExists) {\n      logger.error({\n        message: \"Expected first staff member not found in extracted data\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedFirstItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(staff_members),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Expected first staff member not found in extracted data\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const lastItemExists = staff_members.some(\n      (member) =>\n        member.name === expectedLastItem.name &&\n        member.job_title === expectedLastItem.job_title,\n    );\n\n    if (!lastItemExists) {\n      logger.error({\n        message: \"Expected last staff member not found in extracted data\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedLastItem),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(staff_members),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Expected last staff member not found in extracted data\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/extract_zillow.ts",
    "content": "import { z } from \"zod\";\nimport { EvalFunction } from \"../types/evals.js\";\n\nexport const extract_zillow: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/zillow/\",\n    );\n\n    const real_estate_listings = await v3.extract(\n      \"Extract EACH AND EVERY HOME PRICE AND ADDRESS ON THE PAGE. DO NOT MISS ANY OF THEM.\",\n      z.object({\n        listings: z.array(\n          z.object({\n            price: z.string().describe(\"The price of the home\"),\n            trails: z.string().describe(\"The address of the home\"),\n          }),\n        ),\n      }),\n    );\n\n    await v3.close();\n    const listings = real_estate_listings.listings;\n    const expectedLength = 38;\n\n    if (listings.length < expectedLength) {\n      logger.error({\n        message: \"Incorrect number of listings extracted\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedLength.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: listings.length.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Incorrect number of listings extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/google_flights.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { Action } from \"@browserbasehq/stagehand\";\n\n/**\n * This eval attempts to click on an element that should not pass the playwright actionability check\n * which happens by default if you call locator.click (more information here:\n * https://playwright.dev/docs/actionability)\n *\n * If this eval passes, it means that we have correctly set {force: true} in performPlaywrightMethod,\n * and the click was successful even though the target element (found by the xpath) did not\n * pass the actionability check.\n */\n\nexport const google_flights: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google-flights/\",\n    );\n\n    const observeResult: Action = {\n      selector:\n        \"xpath=/html/body/c-wiz[2]/div/div[2]/c-wiz/div[1]/c-wiz/div[2]/div[2]/div[2]/div/div[2]/div[1]/ul/li[1]/div/div[1]\",\n      description: \"the first departing flight\",\n      method: \"click\",\n      arguments: [],\n    };\n    await v3.act(observeResult);\n\n    const expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google-flights/return-flight.html\";\n    const currentUrl = page.url();\n\n    await v3.close();\n\n    if (currentUrl === expectedUrl) {\n      return {\n        _success: true,\n        currentUrl,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      error: \"The current URL does not match expected.\",\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/heal_custom_dropdown.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const heal_custom_dropdown: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  /**\n   * This eval is meant to test whether we do not incorrectly attempt\n   * the selectOptionFromDropdown method (defined in actHandlerUtils.ts) on a\n   * 'dropdown' that is not a <select> element.\n   *\n   * This kind of dropdown must be clicked to be expanded before being interacted\n   * with.\n   */\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/expand-dropdown/\",\n    );\n\n    await v3.act({\n      description: \"The 'Select a country' dropdown\",\n      selector: \"/html/not-a-dropdown\",\n      arguments: [],\n      method: \"click\",\n    });\n\n    // we are expecting stagehand to click the dropdown to expand it,\n    // and therefore the available options should now be contained in the full\n    // a11y tree.\n\n    // to test, we'll grab the full a11y tree, and make sure it contains 'Canada'\n    const extraction = await v3.extract();\n    const fullTree = extraction.pageText;\n\n    if (fullTree.includes(\"Canada\")) {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: \"unable to expand the dropdown\",\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error attempting to select an option from the dropdown: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/heal_scroll_50.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const heal_scroll_50: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    await v3.act({\n      description: \"the element to scroll on\",\n      selector: \"/html/body/div/div/button\",\n      arguments: [\"50%\"],\n      method: \"scrollTo\",\n    });\n\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    // Get the current scroll position and total scroll height\n    const scrollInfo = await page.evaluate(() => {\n      return {\n        scrollTop: window.scrollY + window.innerHeight / 2,\n        scrollHeight: document.documentElement.scrollHeight,\n      };\n    });\n\n    const halfwayScroll = scrollInfo.scrollHeight / 2;\n    const halfwayReached =\n      Math.abs(scrollInfo.scrollTop - halfwayScroll) <= 200;\n    const evaluationResult = halfwayReached\n      ? {\n          _success: true,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n        }\n      : {\n          _success: false,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Scroll position (${scrollInfo.scrollTop}px) is not halfway down the page (${halfwayScroll}px).`,\n        };\n\n    return evaluationResult;\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/heal_simple_google_search.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const heal_simple_google_search: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google/\",\n    );\n\n    await v3.act({\n      description: \"The search bar\",\n      selector: \"/html/not-the-search-bar\",\n      arguments: [\"OpenAI\"],\n      method: \"fill\",\n    });\n\n    await v3.act(\"press enter\");\n\n    const expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google/openai.html\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl.startsWith(expectedUrl),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/hidden_input_dropdown.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const hidden_input_dropdown: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  /**\n   * This eval is meant to test whether we do not incorrectly attempt\n   * the selectOptionFromDropdown method (defined in actHandlerUtils.ts) on a\n   * hidden input 'dropdown'.\n   *\n   * This kind of dropdown must be clicked to be expanded before being interacted\n   * with.\n   */\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/hidden-input-dropdown/\",\n    );\n\n    await v3.act(\"click to expand the 'Favourite Colour' dropdown\");\n\n    // we are expecting stagehand to click the dropdown to expand it,\n    // and therefore the available options should now be contained in the full\n    // a11y tree.\n\n    // to test, we'll grab the full a11y tree, and make sure it contains 'Green'\n    const extraction = await v3.extract();\n    const fullTree = extraction.pageText;\n\n    if (fullTree.includes(\"Green\")) {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: \"unable to expand the dropdown\",\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error attempting click to expand the dropdown: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/history.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const history: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://docs.stagehand.dev\");\n    await v3.act(\"click on the 'Quickstart' tab\");\n    await v3.extract(\"Extract the title of the page\");\n    await v3.observe(\"Find all links on the page\");\n\n    const history = await v3.history;\n\n    const hasCorrectNumberOfEntries = history.length === 4;\n\n    const hasNavigateEntry = history[0].method === \"navigate\";\n    const hasActEntry = history[1].method === \"act\";\n    const hasExtractEntry = history[2].method === \"extract\";\n    const hasObserveEntry = history[3].method === \"observe\";\n\n    const allEntriesHaveTimestamps = history.every(\n      (entry) =>\n        typeof entry.timestamp === \"string\" && entry.timestamp.length > 0,\n    );\n    const allEntriesHaveResults = history.every(\n      (entry) => entry.result !== undefined,\n    );\n\n    const success =\n      hasCorrectNumberOfEntries &&\n      hasNavigateEntry &&\n      hasActEntry &&\n      hasExtractEntry &&\n      hasObserveEntry &&\n      allEntriesHaveTimestamps &&\n      allEntriesHaveResults;\n\n    return {\n      _success: success,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/homedepot.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const homedepot: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.homedepot.com/\");\n    await v3.act(\"enter 'gas grills' in the search bar\");\n    await v3.act(\"press enter\");\n    await v3.act(\"click on the best selling gas grill\");\n    await v3.act(\"click on the Product Details\");\n\n    const productSpecs = await v3.extract(\n      \"Extract the Primary exact Burner BTU of the product\",\n      z.object({\n        productSpecs: z.object({\n          burnerBTU: z.number().describe(\"Primary Burner BTU exact value\"),\n        }),\n      }),\n    );\n\n    logger.log({\n      message: `gas grill primary burner BTU`,\n      level: 1,\n      auxiliary: {\n        productSpecs: {\n          value: JSON.stringify(productSpecs),\n          type: \"object\",\n        },\n      },\n    });\n\n    if (!productSpecs || !productSpecs.productSpecs) {\n      return {\n        _success: false,\n        productSpecs,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const isLargerThan1000 = productSpecs.productSpecs.burnerBTU >= 10000;\n\n    return {\n      _success: isLargerThan1000,\n      productSpecs,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    logger.error({\n      message: \"error in homedepot function\",\n      level: 0,\n      auxiliary: {\n        error: {\n          value: error.message,\n          type: \"string\",\n        },\n        trace: {\n          value: error.stack,\n          type: \"string\",\n        },\n      },\n    });\n\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/iframe_form_filling.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const iframe_form_filling: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-form-filling/\",\n    );\n\n    await v3.act(\"type 'nunya' into the 'first name' field\");\n    await v3.act(\"type 'business' into the 'last name' field\");\n    await v3.act(\"type 'test@email.com' into the 'email' field\");\n    await v3.act(\"click 'phone' as the preferred contact method\");\n    await v3.act(\"type 'yooooooooooooooo' into the message box\");\n\n    const iframe = page.frameLocator(\"iframe\");\n\n    const firstNameValue: string = await iframe\n      .locator('input[placeholder=\"Jane\"]')\n      .inputValue();\n\n    const lastNameValue: string = await iframe\n      .locator('input[placeholder=\"Doe\"]')\n      .inputValue();\n\n    const emailValue: string = await iframe\n      .locator('input[placeholder=\"jane@example.com\"]')\n      .inputValue();\n\n    const contactValue: boolean = await iframe\n      .locator(\"xpath=/html/body/main/section[1]/form/fieldset/label[2]/input\")\n      .isChecked();\n\n    const messageValue: string = await iframe\n      .locator('textarea[placeholder=\"Say hello…\"]')\n      .inputValue();\n\n    const passed: boolean =\n      firstNameValue.toLowerCase().trim() === \"nunya\" &&\n      lastNameValue.toLowerCase().trim() === \"business\" &&\n      emailValue.toLowerCase() === \"test@email.com\" &&\n      messageValue.toLowerCase() === \"yooooooooooooooo\" &&\n      contactValue;\n\n    return {\n      _success: passed,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/iframe_hn.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const iframe_hn: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/\",\n    );\n\n    const result = await v3.extract(\n      \"extract the title of the first hackernews story\",\n      z.object({\n        story_title: z.string(),\n      }),\n    );\n\n    const title = result.story_title.toLowerCase();\n    const expectedTitleSubstring = \"overengineered anchor links\";\n\n    if (!title.includes(expectedTitleSubstring)) {\n      logger.error({\n        message: `Extracted title: ${title} does not contain expected substring: ${expectedTitleSubstring}`,\n        level: 0,\n      });\n      return {\n        _success: false,\n        error: `Extracted title: ${title} does not contain expected substring: ${expectedTitleSubstring}`,\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/iframe_same_proc.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const iframe_same_proc: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-same-proc/\",\n    );\n\n    await v3.act(\"type 'stagehand' into the 'your name' field\");\n\n    // overly specific prompting is okay here. we are just trying to evaluate whether\n    // we are properly traversing iframes\n    await v3.act(\n      \"select 'Green' from the favorite colour dropdown. Ensure the word 'Green' is capitalized. Choose the selectOption method.\",\n    );\n\n    const iframe = page.frameLocator(\"iframe\");\n\n    const nameValue: string = await iframe\n      .locator('input[placeholder=\"Alice\"]')\n      .inputValue();\n\n    const colorValue: string = await iframe.locator(\"select\").inputValue();\n\n    const passed: boolean =\n      nameValue.toLowerCase().trim() === \"stagehand\" &&\n      colorValue.toLowerCase().trim() === \"green\";\n\n    return {\n      _success: passed,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/iframe_scroll.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const iframe_scroll: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-same-proc-scroll/\",\n    );\n    await v3.act(\"scroll down 50% inside the iframe\");\n\n    const frames = page.frames();\n    const frame = frames[1];\n\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    // Get the current scroll position and total scroll height\n    const scrollInfo = await frame.evaluate(() => {\n      return {\n        scrollTop: window.scrollY + window.innerHeight / 2,\n        scrollHeight: document.documentElement.scrollHeight,\n      };\n    });\n\n    const halfwayScroll = scrollInfo.scrollHeight / 2;\n    const halfwayReached = Math.abs(scrollInfo.scrollTop - halfwayScroll) <= 1;\n    const evaluationResult = halfwayReached\n      ? {\n          _success: true,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n        }\n      : {\n          _success: false,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Scroll position (${scrollInfo.scrollTop}px) is not halfway down the page (${halfwayScroll}px).`,\n        };\n\n    return evaluationResult;\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/iframes_nested.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const iframes_nested: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/nested-iframes/\",\n    );\n\n    await v3.act(\"type 'stagehand' into the 'username' field\");\n\n    const inner = page\n      .frameLocator(\"iframe.lvl1\") // level 1\n      .frameLocator(\"iframe.lvl2\") // level 2\n      .frameLocator(\"iframe.lvl3\"); // level 3 – form lives here\n\n    const usernameText = await inner\n      .locator('input[name=\"username\"]')\n      .inputValue();\n\n    const passed: boolean = usernameText.toLowerCase().trim() === \"stagehand\";\n\n    return {\n      _success: passed,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/imdb_movie_details.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const imdb_movie_details: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.imdb.com/title/tt0111161/\", {\n      waitUntil: \"domcontentloaded\",\n    });\n    await v3.act(\"click on the movie ratings\");\n\n    const movieDetails = await v3.extract(\n      \"Extract the list of countries with the most ratings.\",\n      z.object({\n        countries: z\n          .array(z.string())\n          .describe(\"List of countries with the most ratings\"),\n      }),\n    );\n\n    const expectedCountries = [\n      \"United States\",\n      \"United Kingdom\",\n      \"Turkey\",\n      \"India\",\n      \"Germany\",\n    ];\n\n    if (!movieDetails.countries || movieDetails.countries.length !== 5) {\n      logger.error({\n        message: \"Failed to extract exactly five countries\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: JSON.stringify(expectedCountries),\n            type: \"object\",\n          },\n          actual: {\n            value: JSON.stringify(movieDetails.countries || []),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Incorrect number of countries extracted\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const missingCountries = expectedCountries.filter(\n      (country) => !movieDetails.countries.includes(country),\n    );\n\n    if (missingCountries.length > 0) {\n      logger.error({\n        message: \"Extracted countries do not match expected countries\",\n        level: 0,\n        auxiliary: {\n          missing: {\n            value: JSON.stringify(missingCountries),\n            type: \"object\",\n          },\n          extracted: {\n            value: JSON.stringify(movieDetails.countries),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Extracted countries do not match expected countries\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      countries: movieDetails.countries,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/instructions.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const instructions: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n\n    await page.goto(\"https://docs.browserbase.com/\");\n\n    await v3.act(\"secret12345\");\n\n    await page.waitForLoadState(\"domcontentloaded\");\n\n    const url = page.url();\n\n    const isCorrectUrl =\n      (await url) ===\n      \"https://docs.browserbase.com/introduction/what-is-browserbase\";\n\n    return {\n      _success: isCorrectUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/ionwave.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const ionwave: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/ionwave/\",\n    );\n\n    await v3.act('Click on \"Closed Bids\"');\n\n    const expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/ionwave/closed-bids.html\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl.startsWith(expectedUrl),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/ionwave_observe.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const ionwave_observe: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/ionwave/\",\n    );\n\n    const observations = await v3.observe();\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const expectedLocator = `#Form1 > div:nth-child(5) > div:nth-child(1) > a`;\n\n    const expectedResult = await page\n      .locator(expectedLocator)\n      .first()\n      .innerText();\n\n    let foundMatch = false;\n    for (const observation of observations) {\n      try {\n        const observationResult = await page\n          .locator(observation.selector)\n          .first()\n          .innerText();\n\n        if (observationResult === expectedResult) {\n          foundMatch = true;\n          break;\n        }\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error.message,\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      expected: expectedResult,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/login.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const login: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/login/\",\n    );\n\n    await v3.act(\"type %nunya% into the username field\", {\n      variables: { nunya: \"business\" },\n    });\n\n    const xpath = \"xpath=/html/body/main/form/div[1]/input\";\n    const actualValue = await page.locator(xpath).inputValue();\n\n    const expectedValue = \"business\";\n\n    return {\n      _success: actualValue === expectedValue,\n      expectedValue,\n      actualValue,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/multi_tab.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const multi_tab: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/\",\n    );\n\n    await v3.act(\"click the button to open the other page\");\n    await v3.act(\"click the button to open the other page\");\n    await v3.act(\"click the button to open the other page\");\n    await v3.act(\"click the button to open the other page\");\n    let activePage = await v3.context.awaitActivePage();\n\n    let currentPageUrl = await activePage.url();\n    let expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/page5.html\";\n\n    if (currentPageUrl !== expectedUrl) {\n      return {\n        _success: false,\n        message: \"expected URL does not match current URL\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    // try acting on the first page again\n    const pages = v3.context.pages();\n    const page1 = pages[0];\n    await v3.act(\"click the button to open the other page\", { page: page1 });\n\n    activePage = await v3.context.awaitActivePage();\n    currentPageUrl = await activePage.url();\n    expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/five-tab/page2.html\";\n    if (currentPageUrl !== expectedUrl) {\n      return {\n        _success: false,\n        message: \"expected URL does not match current URL\",\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const page2text = await v3.extract({ page: activePage });\n    const expectedPage2text = \"You've made it to page 2\";\n\n    if (page2text.pageText.includes(expectedPage2text)) {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `extracted page text: ${page2text.pageText} does not match expected page text: ${expectedPage2text}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/namespace_xpath.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const namespace_xpath: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/namespaced-xpath/\",\n    );\n\n    await v3.act(\"fill 'nunya' into the 'type here' form\");\n\n    const inputValue = await page.locator(\"#ns-text\").inputValue();\n    // confirm that the form was filled\n    const formHasBeenFilled = inputValue === \"nunya\";\n\n    return {\n      _success: formHasBeenFilled,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/nested_iframes_2.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const nested_iframes_2: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/nested-iframes-2/\",\n    );\n\n    await v3.act(\"click the button called 'click me (inner 2)'\");\n\n    const inner = page\n      .frameLocator('iframe[src=\"iframe2.html\"]')\n      .frameLocator('iframe[src=\"inner2.html\"]');\n\n    const messageText = await inner.locator(\"#msg\").textContent();\n\n    const passed: boolean =\n      messageText.toLowerCase().trim() ===\n      \"clicked the button in the second inner iframe\";\n\n    return {\n      _success: passed,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n      error,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/next_chunk.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const next_chunk: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.apartments.com/san-francisco-ca/\", {\n      waitUntil: \"domcontentloaded\",\n    });\n    await v3.act(\"click on the all filters button\");\n\n    const { initialScrollTop, chunkHeight } = await page.evaluate(() => {\n      const container = document.querySelector(\n        \"#advancedFilters > div\",\n      ) as HTMLElement;\n      if (!container) {\n        console.warn(\n          \"Could not find #advancedFilters > div. Returning 0 for measurements.\",\n        );\n        return { initialScrollTop: 0, chunkHeight: 0 };\n      }\n      return {\n        initialScrollTop: container.scrollTop,\n        chunkHeight: container.getBoundingClientRect().height,\n      };\n    });\n\n    await v3.act(\"scroll down one chunk on the filters modal\");\n\n    await new Promise((resolve) => setTimeout(resolve, 2000));\n\n    const newScrollTop = await page.evaluate(() => {\n      const container = document.querySelector(\n        \"#advancedFilters > div\",\n      ) as HTMLElement;\n      return container?.scrollTop ?? 0;\n    });\n\n    const actualDiff = newScrollTop - initialScrollTop;\n    const threshold = 20; // allowable difference in px\n    const scrolledOneChunk = Math.abs(actualDiff - chunkHeight) <= threshold;\n\n    const evaluationResult = scrolledOneChunk\n      ? {\n          _success: true,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Successfully scrolled ~one chunk: expected ~${chunkHeight}, got ${actualDiff}`,\n        }\n      : {\n          _success: false,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Scroll difference expected ~${chunkHeight} but only scrolled ${actualDiff}.`,\n        };\n\n    return evaluationResult;\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/no_js_click.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { Action } from \"@browserbasehq/stagehand\";\n\nexport const no_js_click: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  /**\n   * This eval is meant to test whether our `clickElement` function\n   * (inside actHandlerUtils.ts) is able to click elements even if\n   * the site blocks programmatic JS click events.\n   */\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/no-js-click/\",\n    );\n\n    const observeResult: Action = {\n      method: \"click\",\n      selector: \"xpath=/html/body/button\",\n      description: \"the button to click\",\n      arguments: [],\n    };\n    await v3.act(observeResult);\n\n    const text = await page.locator(\"#success-msg\").textContent();\n    if (text?.trim() === \"click succeeded\") {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: \"unable to click element on website that blocks JS click events\",\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error attempting to click the button: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/nonsense_action.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const nonsense_action: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.homedepot.com/\");\n\n    const result = await v3.act(\"what is the capital of the moon?\");\n\n    return {\n      _success: !result.success, // We expect this to fail\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_amazon_add_to_cart.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_amazon_add_to_cart: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/amazon/\",\n    );\n\n    const observations1 = await v3.observe(\n      \"Find and click the 'Add to Cart' button\",\n    );\n\n    // Example of using performPlaywrightMethod if you have the xpath\n    if (observations1.length > 0) {\n      const action1 = observations1[0];\n      await v3.act(action1);\n    }\n\n    const observations2 = await v3.observe(\n      \"Find and click the 'Proceed to checkout' button\",\n    );\n\n    // Example of using performPlaywrightMethod if you have the xpath\n    if (observations2.length > 0) {\n      const action2 = observations2[0];\n      await v3.act(action2);\n    }\n\n    const currentUrl = page.url();\n    const expectedUrlPrefix =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/amazon/sign-in.html\";\n\n    return {\n      _success: currentUrl.startsWith(expectedUrlPrefix),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_github.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_github: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/github/\",\n    );\n\n    const observations = await v3.observe(\n      \"find the scrollable element that holds the repos file tree.\",\n    );\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const possibleLocators = [\n      `#repo-content-pjax-container > react-app > div > div > div.prc-PageLayout-PageLayoutRoot-1zlEO > div > div > div.Box-sc-g0xbh4-0.gISSDQ`,\n      `#repo-content-pjax-container > react-app > div > div > div.prc-PageLayout-PageLayoutRoot-1zlEO > div > div > div.Box-sc-g0xbh4-0.gISSDQ > div`,\n      `#repo-content-pjax-container > react-app > div > div > div.prc-PageLayout-PageLayoutRoot-1zlEO > div > div > div.Box-sc-g0xbh4-0.gISSDQ > div > div.prc-PageLayout-Pane-Vl5LI`,\n      `#repo-content-pjax-container > react-app > div > div > div.prc-PageLayout-PageLayoutRoot-1zlEO > div > div > div.Box-sc-g0xbh4-0.gISSDQ > div > div.prc-PageLayout-Pane-Vl5LI > div`,\n      `#repos-file-tree > div.Box-sc-g0xbh4-0.ReposFileTreePane-module__Box_5--tQNH_`,\n      `#repos-file-tree > div.Box-sc-g0xbh4-0.ReposFileTreePane-module__Box_5--tQNH_ > div`,\n      `#repos-file-tree > div.Box-sc-g0xbh4-0.ReposFileTreePane-module__Box_5--tQNH_ > div > div`,\n      `#repos-file-tree > div.Box-sc-g0xbh4-0.ReposFileTreePane-module__Box_5--tQNH_ > div > div > div > nav`,\n      `#repos-file-tree > div.Box-sc-g0xbh4-0.ReposFileTreePane-module__Box_5--tQNH_ > div > div > div > nav > ul`,\n    ];\n\n    // Precompute candidate backendNodeIds\n    const candidateIds = new Map<string, number>();\n    for (const sel of possibleLocators) {\n      try {\n        const id = await page.locator(sel).backendNodeId();\n        candidateIds.set(sel, id);\n      } catch {\n        // ignore candidates that fail to resolve\n      }\n    }\n\n    let foundMatch = false;\n    let matchedLocator: string | null = null;\n\n    for (const observation of observations) {\n      try {\n        const obsId = await page.locator(observation.selector).backendNodeId();\n        for (const [candSel, candId] of candidateIds) {\n          if (candId === obsId) {\n            foundMatch = true;\n            matchedLocator = candSel;\n            break;\n          }\n        }\n        if (foundMatch) break;\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error?.message ?? String(error),\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      matchedLocator,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_iframes1.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_iframes1: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/\",\n    );\n\n    const observations = await v3.observe(\"find the main header of the page\");\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const possibleLocators = [\n      `body > main > section.iframe-wrapper > iframe`,\n      `body > header > h1`,\n    ];\n\n    // Precompute candidate backendNodeIds\n    const candidateIds = new Map<string, number>();\n    for (const sel of possibleLocators) {\n      try {\n        const id = await page.locator(sel).backendNodeId();\n        candidateIds.set(sel, id);\n      } catch {\n        // ignore candidates that fail to resolve\n      }\n    }\n\n    let foundMatch = false;\n    let matchedLocator: string | null = null;\n\n    for (const observation of observations) {\n      try {\n        const obsId = await page.locator(observation.selector).backendNodeId();\n        for (const [candSel, candId] of candidateIds) {\n          if (candId === obsId) {\n            foundMatch = true;\n            matchedLocator = candSel;\n            break;\n          }\n        }\n        if (foundMatch) break;\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error?.message ?? String(error),\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      matchedLocator,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_iframes2.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { Action } from \"@browserbasehq/stagehand\";\n\nexport const observe_iframes2: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://iframetester.com/?url=https://shopify.com\");\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    let observations: Action[];\n    try {\n      observations = await v3.observe(\"find the main header of the page\");\n    } catch (err) {\n      return {\n        _success: false,\n        message: err.message,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const possibleLocators = [`#iframe-window`, `body > header > h1`];\n\n    // Precompute candidate backendNodeIds\n    const candidateIds = new Map<string, number>();\n    for (const sel of possibleLocators) {\n      try {\n        const id = await page.locator(sel).backendNodeId();\n        candidateIds.set(sel, id);\n      } catch {\n        // ignore candidates that fail to resolve\n      }\n    }\n\n    let foundMatch = false;\n    let matchedLocator: string | null = null;\n\n    for (const observation of observations) {\n      try {\n        const obsId = await page.locator(observation.selector).backendNodeId();\n        for (const [candSel, candId] of candidateIds) {\n          if (candId === obsId) {\n            foundMatch = true;\n            matchedLocator = candSel;\n            break;\n          }\n        }\n        if (foundMatch) break;\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error?.message ?? String(error),\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      matchedLocator,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_simple_google_search.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_simple_google_search: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google/\",\n    );\n    const observation1 = await v3.observe(\n      \"Find the search bar and type 'OpenAI'\",\n    );\n\n    if (observation1.length > 0) {\n      const action1 = observation1[0];\n      await v3.act(action1);\n    }\n    const observation2 = await v3.observe(\"Press enter\");\n\n    if (observation2.length > 0) {\n      const action2 = observation2[0];\n      await v3.act(action2);\n    }\n    await new Promise((resolve) => setTimeout(resolve, 3000));\n\n    const expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google/openai.html\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl.startsWith(expectedUrl),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_taxes.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_taxes: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://file.1040.com/estimate/\");\n\n    const observations = await v3.observe(\n      \"Find all the form input elements under the 'Income' section\",\n    );\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    } else if (observations.length < 13) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const expectedLocator = `#tpWages`;\n\n    const expectedResult = await page\n      .locator(expectedLocator)\n      .first()\n      .innerText();\n\n    let foundMatch = false;\n    for (const observation of observations) {\n      try {\n        const observationResult = await page\n          .locator(observation.selector)\n          .first()\n          .innerText();\n\n        if (observationResult === expectedResult) {\n          foundMatch = true;\n          break;\n        }\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error.message,\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      expected: expectedResult,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_vantechjournal.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_vantechjournal: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://vantechjournal.com/archive\");\n\n    const observations = await v3.observe(\"Find the 'load more' link\");\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const expectedLocators = [\n      \"xpath=/html/body/div[2]/div/section/div/div/div[3]/a\",\n      \"xpath=/html/body/div[2]/div/section/div/div/div[3]/a/span\",\n    ];\n\n    const expectedIds: number[] = [];\n    for (const locator of expectedLocators) {\n      const node = page.locator(locator);\n      const id = await node.backendNodeId();\n      if (id !== undefined && id !== null) expectedIds.push(id);\n    }\n\n    const observedNode = page.locator(observations[0].selector);\n    const observedId = await observedNode.backendNodeId();\n\n    const foundMatch = expectedIds.includes(observedId);\n\n    return {\n      _success: foundMatch,\n      expected: expectedLocators,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error: unknown) {\n    return {\n      _success: false,\n      error: error instanceof Error ? error.message : String(error),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/observe_yc_startup.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const observe_yc_startup: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.ycombinator.com/companies\", {\n      waitUntil: \"networkidle\",\n    });\n\n    const observations = await v3.observe(\n      \"Click the container element that holds links to each of the startup companies. The companies each have a name, a description, and a link to their website.\",\n    );\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const possibleLocators = [\n      `div._rightCol_zhfs4_592`,\n      `div._section_zhfs4_163._results_zhfs4_343`,\n    ];\n\n    // Precompute candidate backendNodeIds\n    const candidateIds = new Map<string, number>();\n    for (const sel of possibleLocators) {\n      try {\n        const id = await page.locator(sel).backendNodeId();\n        candidateIds.set(sel, id);\n      } catch {\n        // ignore candidates that fail to resolve\n      }\n    }\n\n    let foundMatch = false;\n    let matchedLocator: string | null = null;\n\n    for (const observation of observations) {\n      try {\n        const obsId = await page.locator(observation.selector).backendNodeId();\n        for (const [candSel, candId] of candidateIds) {\n          if (candId === obsId) {\n            foundMatch = true;\n            matchedLocator = candSel;\n            break;\n          }\n        }\n        if (foundMatch) break;\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error?.message ?? String(error),\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      matchedLocator,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/oopif_in_csr.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const oopif_in_csr: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // fill a form inside a OOPIF (out of process iframe) that is inside an\n  // CSR (closed mode shadow) root\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-open-shadow-dom/\",\n    );\n    await v3.act(\"fill 'nunya' into the first name field\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"nunya\")) {\n      return {\n        _success: true,\n        message: `successfully filled the form`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to fill the form`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/oopif_in_osr.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const oopif_in_osr: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // fill a form inside a OOPIF (out of process iframe) that is inside an\n  // OSR (open mode shadow) root\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-open-shadow-dom/\",\n    );\n    await v3.act(\"fill 'nunya' into the first name field\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"nunya\")) {\n      return {\n        _success: true,\n        message: `successfully filled the form`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to fill the form`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/os_dropdown.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const os_dropdown: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  /**\n   * This eval is meant to test whether we can correctly select an element\n   * from an OS level dropdown\n   */\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/nested-dropdown/\",\n    );\n\n    await v3.act(\n      \"choose 'Smog Check Technician' from the 'License Type' dropdown\",\n    );\n    const selectedOption = await page\n      .locator(\"#licenseType >> option:checked\")\n      .textContent();\n\n    if (selectedOption === \"Smog Check Technician\") {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: \"incorrect option selected from the dropdown\",\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error attempting to select an option from the dropdown: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/osr_in_oopif.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const osr_in_oopif: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // click inside an OSR (open mode shadow) root that is inside an\n  // OOPIF (out of process iframe)\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/open-shadow-root-in-oopif/\",\n    );\n    await v3.act(\"click the button\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        message: `successfully clicked the button`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to click on the button`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/osr_in_spif.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const osr_in_spif: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // click inside an OSR (open mode shadow) root that is inside an\n  // SPIF (same process iframe)\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/open-shadow-root-in-spif/\",\n    );\n    await v3.act(\"click the button\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        message: `successfully clicked the button`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to click on the button`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/panamcs.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const panamcs: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/panamcs/\",\n    );\n\n    const observations = await v3.observe(\"click the 'about us' link\");\n\n    if (observations.length === 0) {\n      return {\n        _success: false,\n        observations,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    const expectedLocator = `#menu > li:nth-child(1) > a`;\n\n    const expectedResult = await page\n      .locator(expectedLocator)\n      .first()\n      .innerText();\n\n    let foundMatch = false;\n    for (const observation of observations) {\n      try {\n        const observationResult = await page\n          .locator(observation.selector)\n          .first()\n          .innerText();\n\n        if (observationResult === expectedResult) {\n          foundMatch = true;\n          break;\n        }\n      } catch (error) {\n        console.warn(\n          `Failed to check observation with selector ${observation.selector}:`,\n          error.message,\n        );\n        continue;\n      }\n    }\n\n    return {\n      _success: foundMatch,\n      expected: expectedResult,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/peeler_complex.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const peeler_complex: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(`https://chefstoys.com/`, { timeoutMs: 60000 });\n    await page.waitForLoadState(\"networkidle\");\n\n    await v3.act(\"find the button to close the popup\");\n    await v3.act(\"search for %search_query%\", {\n      variables: {\n        search_query: \"peeler\",\n      },\n    });\n\n    await v3.act('click on the first \"OXO\" brand peeler');\n\n    const { price } = await v3.extract(\n      \"get the price of the peeler\",\n      z.object({ price: z.number().nullable() }),\n    );\n\n    return {\n      _success: price === 11.99,\n      price,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    logger.error({\n      message: \"error in peeler_complex function\",\n      level: 0,\n      auxiliary: {\n        error: {\n          value: JSON.stringify(error, null, 2),\n          type: \"object\",\n        },\n        trace: {\n          value: error.stack,\n          type: \"string\",\n        },\n      },\n    });\n\n    return {\n      _success: false,\n      error: JSON.parse(JSON.stringify(error, null, 2)),\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/prev_chunk.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const prev_chunk: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    await new Promise((resolve) => setTimeout(resolve, 2000));\n    const { initialScrollTop, chunkHeight } = await page.evaluate(() => {\n      const halfPage = document.body.scrollHeight / 2;\n\n      window.scrollTo({\n        top: halfPage,\n        left: 0,\n        behavior: \"instant\",\n      });\n\n      const chunk = window.innerHeight;\n\n      return {\n        initialScrollTop: window.scrollY,\n        chunkHeight: chunk,\n      };\n    });\n    await new Promise((resolve) => setTimeout(resolve, 2000));\n    await v3.act(\"scroll up one chunk\");\n\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    const finalScrollTop = await page.evaluate(() => window.scrollY);\n\n    const actualDiff = initialScrollTop - finalScrollTop;\n    const threshold = 20; // px tolerance\n    const scrolledOneChunk = Math.abs(actualDiff - chunkHeight) <= threshold;\n\n    const evaluationResult = scrolledOneChunk\n      ? {\n          _success: true,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Successfully scrolled ~one chunk UP: expected ~${chunkHeight}, got ${actualDiff}.`,\n        }\n      : {\n          _success: false,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Scroll difference expected ~${chunkHeight} but only scrolled ${actualDiff}.`,\n        };\n\n    return evaluationResult;\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/radio_btn.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const radio_btn: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/paneer-pizza/\",\n    );\n\n    await v3.act(\"click the 'medium' option\");\n\n    // confirm that the Medium radio is now checked\n    const radioBtnClicked = await page\n      .locator('input[type=\"radio\"][name=\"Pizza\"][value=\"Medium\"]')\n      .isChecked();\n\n    return {\n      _success: radioBtnClicked,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/rakuten_jp.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const rakuten_jp: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.rakuten.co.jp/\");\n\n    await v3.act(\"type '香菜' into the search bar\");\n    await v3.act(\"press enter\");\n    const url = page.url();\n    const successUrl =\n      \"https://search.rakuten.co.jp/search/mall/%E9%A6%99%E8%8F%9C/\";\n\n    return {\n      _success: url === successUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/sciquest.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const sciquest: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://bids.sciquest.com/apps/Router/PublicEvent?tab=PHX_NAV_SourcingAllOpps&CustomerOrg=StateOfUtah\",\n    );\n\n    await v3.act('Click on the \"Closed\" tab');\n\n    const result = await v3.extract(\n      \"Extract the total number of results that the search produced. Not the number of results displayed on the page.\",\n      z.object({\n        total_results: z.string(),\n      }),\n    );\n\n    const { total_results } = result;\n\n    const expectedNumber = 12637;\n    const extractedNumber = parseInt(total_results.replace(/[^\\d]/g, \"\"), 10);\n\n    const isWithinRange =\n      extractedNumber >= expectedNumber - 1000 &&\n      extractedNumber <= expectedNumber + 1000;\n\n    if (!isWithinRange) {\n      logger.error({\n        message: \"Total number of results is not within the expected range\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: `${expectedNumber} ± 1000`,\n            type: \"string\",\n          },\n          actual: {\n            value: extractedNumber.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Total number of results is not within the expected range\",\n        extractedNumber,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      extractedNumber,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/scroll_50.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const scroll_50: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    await v3.act(\"Scroll 50% down the page\");\n\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    // Get the current scroll position and total scroll height\n    const scrollInfo = await page.evaluate(() => {\n      return {\n        scrollTop: window.scrollY + window.innerHeight / 2,\n        scrollHeight: document.documentElement.scrollHeight,\n      };\n    });\n\n    const halfwayScroll = scrollInfo.scrollHeight / 2;\n    const halfwayReached =\n      Math.abs(scrollInfo.scrollTop - halfwayScroll) <= 200;\n    const evaluationResult = halfwayReached\n      ? {\n          _success: true,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n        }\n      : {\n          _success: false,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Scroll position (${scrollInfo.scrollTop}px) is not halfway down the page (${halfwayScroll}px).`,\n        };\n\n    return evaluationResult;\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/scroll_75.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const scroll_75: EvalFunction = async ({\n  logger,\n  debugUrl,\n  sessionUrl,\n  v3,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/\",\n    );\n    await v3.act(\"Scroll 75% down the page\");\n\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    // Get the current scroll position and total scroll height\n    const scrollInfo = await page.evaluate(() => {\n      return {\n        scrollTop: window.scrollY + window.innerHeight * 0.75,\n        scrollHeight: document.documentElement.scrollHeight,\n      };\n    });\n\n    const threeQuartersScroll = scrollInfo.scrollHeight * 0.75;\n    const threeQuartersReached =\n      Math.abs(scrollInfo.scrollTop - threeQuartersScroll) <= 200;\n    const evaluationResult = threeQuartersReached\n      ? {\n          _success: true,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n        }\n      : {\n          _success: false,\n          logs: logger.getLogs(),\n          debugUrl,\n          sessionUrl,\n          message: `Scroll position (${scrollInfo.scrollTop}px) is not three quarters down the page (${threeQuartersScroll}px).`,\n        };\n\n    return evaluationResult;\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/shadow_dom.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const shadow_dom: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/shadow-dom/\",\n    );\n    await v3.act(\"click the button\");\n    const extraction = await v3.extract(\"extract the page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/simple_google_search.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const simple_google_search: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google/\",\n    );\n\n    await v3.act('type \"OpenAI\" into the search bar');\n\n    await v3.act(\"press enter\");\n\n    const expectedUrl =\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/google/openai.html\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl.startsWith(expectedUrl),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/spif_in_csr.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const spif_in_csr: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // click inside a SPIF (same process iframe) that is inside an\n  // CSR (closed mode shadow) root\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-closed-shadow-dom/\",\n    );\n    await v3.act(\"click the button\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        message: `successfully clicked the button`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to click on the button`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/spif_in_osr.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const spif_in_osr: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  // this eval is designed to test whether stagehand can successfully\n  // click inside a SPIF (same process iframe) that is inside an\n  // OSR (open mode shadow) root\n\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/spif-in-open-shadow-dom/\",\n    );\n    await v3.act(\"click the button\");\n\n    const extraction = await v3.extract(\"extract the entire page text\");\n\n    const pageText = extraction.extraction;\n\n    if (pageText.includes(\"button successfully clicked\")) {\n      return {\n        _success: true,\n        message: `successfully clicked the button`,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n    return {\n      _success: false,\n      message: `unable to click on the button`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: `error: ${error.message}`,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/stock_x.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const stock_x: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://stockx.com/air-jordan-3-retro-black-cement-2024\");\n\n    await v3.act(\"click on Jordan 3 Retro Crimson in the related products\");\n\n    const currentUrl = page.url();\n    const expectedUrlPrefix = \"https://stockx.com/jordan-3-retro-crimson\";\n\n    await v3.close();\n\n    return {\n      _success: currentUrl.startsWith(expectedUrlPrefix),\n      currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/tab_handling.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const tab_handling: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/new-tab/\",\n    );\n\n    await v3.act(\"click the button to open the other page\");\n\n    const pages = v3.context.pages();\n    const page1 = pages[0];\n    const page2 = pages[1];\n\n    // extract all the text from the first page\n    const extraction1 = await v3.extract({ page: page1 });\n    // extract all the text from the second page\n    const extraction2 = await v3.extract({ page: page2 });\n\n    const extraction1Success = extraction1.pageText.includes(\"Welcome!\");\n    const extraction2Success = extraction2.pageText.includes(\n      \"You’re on the other page\",\n    );\n\n    return {\n      _success: extraction1Success && extraction2Success,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      message: error.message,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/ted_talk.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { normalizeString } from \"../utils.js\";\nimport { z } from \"zod\";\n\nexport const ted_talk: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://www.ted.com/talks/sir_ken_robinson_do_schools_kill_creativity\",\n      {\n        waitUntil: \"domcontentloaded\",\n      },\n    );\n\n    await v3.act(\"scroll 10% down the page\");\n\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n\n    await v3.act(\n      \"Click the link that takes you to the page about the 'Culture' topic\",\n    );\n\n    const playlists = await v3.extract(\n      \"Extract the video playlist titles and the number of talks in each playlist. This info is in the Video Playlists about Culture section of the webpage.\",\n      z.object({\n        playlists: z\n          .array(\n            z.object({\n              title: z.string().describe(\"Title of the playlist\"),\n              num_talks: z.number().describe(\"Number of talks in the playlist\"),\n            }),\n          )\n          .describe(\"List of culture video playlists\"),\n      }),\n    );\n\n    const expectedPlaylists = [\n      {\n        title: \"Talks that celebrate the boundless creativity of an open mind\",\n        num_talks: 6,\n      },\n      {\n        title: \"Little-known big history\",\n        num_talks: 15,\n      },\n      {\n        title: \"Extraordinary, larger-than-life art\",\n        num_talks: 10,\n      },\n      {\n        title: \"How perfectionism fails us\",\n        num_talks: 4,\n      },\n    ];\n\n    if (!playlists.playlists || playlists.playlists.length === 0) {\n      logger.error({\n        message: \"Failed to extract playlists on culture\",\n        level: 0,\n      });\n\n      return {\n        _success: false,\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    const missingPlaylists = expectedPlaylists.filter((expected) =>\n      playlists.playlists.every(\n        (extracted) =>\n          normalizeString(extracted.title) !==\n            normalizeString(expected.title) ||\n          extracted.num_talks !== expected.num_talks,\n      ),\n    );\n\n    if (missingPlaylists.length > 0) {\n      logger.error({\n        message: \"Extracted playlists do not match expected playlists\",\n        level: 0,\n        auxiliary: {\n          missing: {\n            value: JSON.stringify(missingPlaylists),\n            type: \"object\",\n          },\n          extracted: {\n            value: JSON.stringify(playlists.playlists),\n            type: \"object\",\n          },\n        },\n      });\n\n      return {\n        _success: false,\n        error: \"Extracted playlists do not match expected playlists\",\n        logs: logger.getLogs(),\n        debugUrl,\n        sessionUrl,\n      };\n    }\n\n    return {\n      _success: true,\n      playlists: playlists.playlists,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      logs: logger.getLogs(),\n      debugUrl,\n      sessionUrl,\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/vanta_h.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const vanta_h: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://www.vanta.com/\");\n\n    const observations = await v3.observe(\n      \"click the buy now button if it is available\",\n    );\n\n    // we should have no saved observation since the element shouldn't exist\n    return {\n      _success: observations.length === 0,\n      observations,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/vantechjournal.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const vantechjournal: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\"https://vantechjournal.com\");\n\n    await v3.act(\"click on page 'recommendations'\");\n\n    const expectedUrl = \"https://vantechjournal.com/recommendations\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl === expectedUrl,\n      currentUrl,\n      expectedUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/wichita.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\nimport { z } from \"zod\";\n\nexport const wichita: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(\n      \"https://browserbase.github.io/stagehand-eval-sites/sites/wichita/\",\n    );\n\n    await v3.act('Click on \"Show Closed/Awarded/Cancelled bids\"');\n\n    const result = await v3.extract(\n      \"Extract the total number of bids that the search produced.\",\n      z.object({\n        total_results: z.number(),\n      }),\n    );\n\n    const { total_results } = result;\n\n    const expectedNumber = 430;\n\n    if (total_results !== expectedNumber) {\n      logger.error({\n        message: \"Total number of results does not match expected\",\n        level: 0,\n        auxiliary: {\n          expected: {\n            value: expectedNumber.toString(),\n            type: \"integer\",\n          },\n          actual: {\n            value: total_results.toString(),\n            type: \"integer\",\n          },\n        },\n      });\n      return {\n        _success: false,\n        error: \"Total number of results does not match expected\",\n        total_results,\n        debugUrl,\n        sessionUrl,\n        logs: logger.getLogs(),\n      };\n    }\n\n    return {\n      _success: true,\n      total_results,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tasks/wikipedia.ts",
    "content": "import { EvalFunction } from \"../types/evals.js\";\n\nexport const wikipedia: EvalFunction = async ({\n  debugUrl,\n  sessionUrl,\n  v3,\n  logger,\n}) => {\n  try {\n    const page = v3.context.pages()[0];\n    await page.goto(`https://en.wikipedia.org/wiki/Baseball`);\n    await v3.act('click the \"hit and run\" link in this article', {\n      timeout: 360_000,\n    });\n\n    const url = \"https://en.wikipedia.org/wiki/Hit_and_run_(baseball)\";\n    const currentUrl = page.url();\n\n    return {\n      _success: currentUrl === url,\n      expected: url,\n      actual: currentUrl,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } catch (error) {\n    return {\n      _success: false,\n      error: error,\n      debugUrl,\n      sessionUrl,\n      logs: logger.getLogs(),\n    };\n  } finally {\n    await v3.close();\n  }\n};\n"
  },
  {
    "path": "packages/evals/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"rootDir\": \".\",\n    \"outDir\": \"dist/esm\",\n    \"noEmit\": false,\n    \"paths\": {\n      \"@browserbasehq/stagehand\": [\"../core/dist/esm/index.d.ts\"]\n    }\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/evals/types/evals.ts",
    "content": "import { z } from \"zod\";\nimport type { AvailableModel } from \"@browserbasehq/stagehand\";\nimport type { LogLine } from \"@browserbasehq/stagehand\";\nimport type { AgentInstance } from \"@browserbasehq/stagehand\";\nimport type { EvalCase } from \"braintrust\";\nimport type { V3 } from \"@browserbasehq/stagehand\";\nimport { EvalLogger } from \"../logger.js\";\n\nexport type StagehandInitResult = {\n  v3?: V3;\n  v3Agent?: AgentInstance;\n  logger: EvalLogger;\n  debugUrl: string;\n  sessionUrl: string;\n  modelName: AvailableModel;\n  agent: AgentInstance;\n};\n\nexport type EvalFunction = (\n  taskInput: StagehandInitResult & { input: EvalInput },\n) => Promise<{\n  _success: boolean;\n  logs: LogLine[];\n  debugUrl: string;\n  sessionUrl: string;\n  error?: unknown;\n}>;\n\nexport const EvalCategorySchema = z.enum([\n  \"observe\",\n  \"act\",\n  \"combination\",\n  \"extract\",\n  \"experimental\",\n  \"targeted_extract\",\n  \"regression\",\n  \"regression_llm_providers\",\n  \"llm_clients\",\n  \"agent\",\n  \"external_agent_benchmarks\",\n]);\n\nexport type EvalCategory = z.infer<typeof EvalCategorySchema>;\nexport interface EvalInput {\n  name: string;\n  modelName: AvailableModel;\n  isCUA?: boolean;\n  // Optional per-test parameters, used by data-driven tasks\n  params?: Record<string, unknown>;\n}\n\nexport interface Testcase\n  extends EvalCase<\n    EvalInput,\n    unknown,\n    {\n      model: AvailableModel;\n      test: string;\n      categories?: string[];\n      category?: string;\n      dataset?: string;\n      task_id?: string;\n      website?: string;\n      difficulty?: string;\n    }\n  > {\n  input: EvalInput;\n  name: string;\n  tags: string[];\n  metadata: {\n    model: AvailableModel;\n    test: string;\n    categories?: string[];\n    category?: string;\n    dataset?: string;\n    task_id?: string;\n    website?: string;\n    difficulty?: string;\n    task_category?: string;\n  };\n  expected: unknown;\n}\n\nexport interface SummaryResult {\n  input: EvalInput;\n  output: { _success: boolean };\n  name: string;\n  score: number;\n}\n\nexport interface EvalArgs<TInput, TOutput, TExpected> {\n  input: TInput;\n  output: TOutput;\n  expected: TExpected;\n  metadata?: { model: AvailableModel; test: string };\n}\n\nexport interface EvalResult {\n  name: string;\n  score: number;\n}\n\nexport type LogLineEval = LogLine & {\n  parsedAuxiliary?: string | object;\n};\n\nexport type AgentModelEntry = {\n  modelName: string;\n  cua: boolean;\n};\n"
  },
  {
    "path": "packages/evals/types/screenshotCollector.ts",
    "content": "export interface ScreenshotCollectorOptions {\n  /**\n   * Interval in ms for polling-based screenshot capture.\n   * If provided, start() will begin polling at this interval.\n   * If omitted, use addScreenshot() via the V3 event bus for event-driven collection.\n   */\n  interval?: number;\n  maxScreenshots?: number;\n}\n\n// Minimal page-like interface: supports screenshot() and optional event hooks\nexport type ScreenshotCapablePage = {\n  screenshot: (...args: []) => Promise<Buffer | string>;\n  on?: (event: string, listener: (...args: []) => void) => void;\n  off?: (event: string, listener: (...args: []) => void) => void;\n};\n"
  },
  {
    "path": "packages/evals/utils/ScreenshotCollector.ts",
    "content": "import { V3 } from \"@browserbasehq/stagehand\";\nimport sharp from \"sharp\";\nimport { ScreenshotCollectorOptions } from \"../types/screenshotCollector.js\";\n\nexport class ScreenshotCollector {\n  private screenshots: Buffer[] = [];\n  private v3: V3;\n  private interval?: number;\n  private maxScreenshots: number;\n  private intervalId?: NodeJS.Timeout;\n  private isCapturing: boolean = false;\n  private lastScreenshot?: Buffer;\n  private ssimThreshold: number = 0.75;\n  private mseThreshold: number = 30;\n  private stopped: boolean = false;\n\n  constructor(v3: V3, options: ScreenshotCollectorOptions = {}) {\n    this.v3 = v3;\n    this.interval = options.interval; // undefined means event-driven mode\n    this.maxScreenshots = options.maxScreenshots || 10;\n  }\n\n  /**\n   * Start interval-based screenshot capture.\n   * Only activates if interval option was provided in constructor.\n   * For event-driven collection, use addScreenshot() directly via the V3 event bus.\n   */\n  start(): void {\n    // Only start interval if interval was provided\n    if (!this.interval) {\n      return;\n    }\n\n    if (this.intervalId) {\n      return;\n    }\n\n    // Set up time-based screenshot capture\n    this.intervalId = setInterval(() => {\n      this.captureScreenshot(\"interval\").catch((error) => {\n        console.error(\"Interval screenshot failed:\", error);\n      });\n    }, this.interval);\n\n    // Capture initial screenshot without blocking\n    this.captureScreenshot(\"initial\").catch((error) => {\n      console.error(\"Failed to capture initial screenshot:\", error);\n    });\n  }\n\n  async stop(): Promise<Buffer[]> {\n    // Mark as stopped first to prevent any new operations\n    this.stopped = true;\n\n    // Clear interval if running\n    if (this.intervalId) {\n      clearInterval(this.intervalId);\n      this.intervalId = undefined;\n    }\n\n    // Reset capturing flag to unblock any pending state\n    this.isCapturing = false;\n\n    // Try to capture final screenshot, but don't fail if CDP is disconnected\n    try {\n      await this.captureScreenshot(\"final\");\n    } catch {\n      // Ignore errors - CDP may be disconnected\n    }\n\n    // Return a copy and clear internal state to free memory\n    const result = [...this.screenshots];\n    this.screenshots = [];\n    this.lastScreenshot = undefined;\n\n    return result;\n  }\n\n  private async captureScreenshot(trigger: string): Promise<void> {\n    // Don't capture if stopped (unless it's the final capture) or already capturing\n    if ((this.stopped && trigger !== \"final\") || this.isCapturing) {\n      return;\n    }\n    this.isCapturing = true;\n\n    try {\n      const page = await this.v3.context.awaitActivePage();\n      const screenshot = await page.screenshot({ fullPage: false });\n\n      // If stopped while awaiting screenshot (and not final), don't process further\n      if (this.stopped && trigger !== \"final\") {\n        return;\n      }\n\n      // Check if we should keep this screenshot based on image diff\n      let shouldKeep = true;\n      if (this.lastScreenshot && trigger !== \"initial\" && trigger !== \"final\") {\n        try {\n          // First do a quick MSE check\n          const mse = await this.calculateMSE(this.lastScreenshot, screenshot);\n          if (mse < this.mseThreshold) {\n            // Very similar, skip\n            shouldKeep = false;\n          } else {\n            // Significant difference detected, verify with SSIM\n            const ssim = await this.calculateSSIM(\n              this.lastScreenshot,\n              screenshot,\n            );\n            shouldKeep = ssim < this.ssimThreshold;\n          }\n        } catch (error) {\n          // If comparison fails, keep the screenshot\n          console.error(\"Image comparison failed:\", error);\n          shouldKeep = true;\n        }\n      }\n\n      if (shouldKeep) {\n        this.screenshots.push(screenshot);\n        this.lastScreenshot = screenshot;\n\n        if (this.screenshots.length > this.maxScreenshots) {\n          this.screenshots.shift();\n        }\n      }\n    } catch (error) {\n      console.error(`Failed to capture screenshot (${trigger}):`, error);\n    } finally {\n      this.isCapturing = false;\n    }\n  }\n\n  getScreenshots(): Buffer[] {\n    return [...this.screenshots];\n  }\n\n  getScreenshotCount(): number {\n    return this.screenshots.length;\n  }\n\n  clear(): void {\n    this.screenshots = [];\n  }\n\n  /**\n   * Manually add a screenshot buffer to the collection.\n   * @param screenshot The screenshot buffer to add\n   */\n  async addScreenshot(screenshot: Buffer): Promise<void> {\n    // Don't add if stopped or already capturing\n    if (this.stopped || this.isCapturing) {\n      return;\n    }\n    this.isCapturing = true;\n\n    try {\n      // Apply MSE/SSIM logic to decide if we should keep this screenshot\n      let shouldKeep = true;\n      if (this.lastScreenshot) {\n        try {\n          // First do a quick MSE check\n          const mse = await this.calculateMSE(this.lastScreenshot, screenshot);\n          if (mse < this.mseThreshold) {\n            // Very similar, skip\n            shouldKeep = false;\n          } else {\n            // Significant difference detected, verify with SSIM\n            const ssim = await this.calculateSSIM(\n              this.lastScreenshot,\n              screenshot,\n            );\n            shouldKeep = ssim < this.ssimThreshold;\n          }\n        } catch (error) {\n          // If comparison fails, keep the screenshot\n          console.error(\"Image comparison failed:\", error);\n          shouldKeep = true;\n        }\n      }\n\n      if (shouldKeep) {\n        this.screenshots.push(screenshot);\n        this.lastScreenshot = screenshot;\n\n        if (this.screenshots.length > this.maxScreenshots) {\n          this.screenshots.shift();\n        }\n      }\n    } finally {\n      this.isCapturing = false;\n    }\n  }\n\n  private async calculateMSE(img1: Buffer, img2: Buffer): Promise<number> {\n    try {\n      // Resize images for faster comparison\n      const size = { width: 400, height: 300 };\n      const data1 = await sharp(img1).resize(size).raw().toBuffer();\n      const data2 = await sharp(img2).resize(size).raw().toBuffer();\n\n      if (data1.length !== data2.length) return Number.MAX_SAFE_INTEGER;\n\n      let sum = 0;\n      for (let i = 0; i < data1.length; i++) {\n        const diff = data1[i] - data2[i];\n        sum += diff * diff;\n      }\n\n      return sum / data1.length;\n    } catch {\n      // If sharp is not available, assume images are different\n      return Number.MAX_SAFE_INTEGER;\n    }\n  }\n\n  private async calculateSSIM(img1: Buffer, img2: Buffer): Promise<number> {\n    try {\n      // Resize and convert to grayscale for SSIM calculation\n      const size = { width: 400, height: 300 };\n      const gray1 = await sharp(img1).resize(size).grayscale().raw().toBuffer();\n      const gray2 = await sharp(img2).resize(size).grayscale().raw().toBuffer();\n\n      if (gray1.length !== gray2.length) return 0;\n\n      // Simplified SSIM calculation\n      const c1 = 0.01 * 0.01;\n      const c2 = 0.03 * 0.03;\n\n      let sum1 = 0,\n        sum2 = 0,\n        sum1_sq = 0,\n        sum2_sq = 0,\n        sum12 = 0;\n      const N = gray1.length;\n\n      for (let i = 0; i < N; i++) {\n        sum1 += gray1[i];\n        sum2 += gray2[i];\n        sum1_sq += gray1[i] * gray1[i];\n        sum2_sq += gray2[i] * gray2[i];\n        sum12 += gray1[i] * gray2[i];\n      }\n\n      const mean1 = sum1 / N;\n      const mean2 = sum2 / N;\n      const var1 = sum1_sq / N - mean1 * mean1;\n      const var2 = sum2_sq / N - mean2 * mean2;\n      const cov12 = sum12 / N - mean1 * mean2;\n\n      const numerator = (2 * mean1 * mean2 + c1) * (2 * cov12 + c2);\n      const denominator =\n        (mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2);\n\n      return numerator / denominator;\n    } catch {\n      // If sharp is not available, assume images are different\n      return 0;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/evals/utils/imageResize.ts",
    "content": "import sharp from \"sharp\";\n\nexport async function imageResize(\n  img: Buffer,\n  scaleFactor: number,\n): Promise<Buffer> {\n  const metadata = await sharp(img).metadata();\n\n  if (metadata.width && metadata.height) {\n    const width = Math.round(metadata.width * scaleFactor);\n    const height = Math.round(metadata.height * scaleFactor);\n    return await sharp(img)\n      .resize(width, height, { fit: \"inside\", kernel: sharp.kernel.lanczos3 })\n      .png({\n        compressionLevel: 9,\n        adaptiveFiltering: true,\n        palette: true,\n      })\n      .toBuffer();\n  }\n\n  return img;\n}\n"
  },
  {
    "path": "packages/evals/utils.ts",
    "content": "/**\n * This file provides utility functions and classes to assist with evaluation tasks.\n *\n * Key functionalities:\n * - String normalization and fuzzy comparison utility functions to compare output strings\n *   against expected results in a flexible and robust way.\n * - Generation of unique experiment names based on the current timestamp, environment,\n *   and eval name or category.\n */\nimport fs from \"fs\";\nimport { LogLine } from \"@browserbasehq/stagehand\";\nimport stringComparison from \"string-comparison\";\nconst { jaroWinkler } = stringComparison;\n\n/**\n * normalizeString:\n * Prepares a string for comparison by:\n * - Converting to lowercase\n * - Collapsing multiple spaces to a single space\n * - Removing punctuation and special characters that are not alphabetic or numeric\n * - Normalizing spacing around commas\n * - Trimming leading and trailing whitespace\n *\n * This helps create a stable string representation to compare against expected outputs,\n * even if the actual output contains minor formatting differences.\n */\nexport function normalizeString(str: string): string {\n  return str\n    .toLowerCase()\n    .replace(/\\s+/g, \" \")\n    .replace(/[;/#!$%^&*:{}=\\-_`~()]/g, \"\")\n    .replace(/\\s*,\\s*/g, \", \")\n    .trim();\n}\n\n/**\n * compareStrings:\n * Compares two strings (actual vs. expected) using a similarity metric (Jaro-Winkler).\n *\n * Arguments:\n * - actual: The actual output string to be checked.\n * - expected: The expected string we want to match against.\n * - similarityThreshold: A number between 0 and 1. Default is 0.85.\n *   If the computed similarity is greater than or equal to this threshold,\n *   we consider the strings sufficiently similar.\n *\n * Returns:\n * - similarity: A number indicating how similar the two strings are.\n * - meetsThreshold: A boolean indicating if the similarity meets or exceeds the threshold.\n *\n * This function is useful for tasks where exact string matching is too strict,\n * allowing for fuzzy matching that tolerates minor differences in formatting or spelling.\n */\nexport function compareStrings(\n  actual: string,\n  expected: string,\n  similarityThreshold: number = 0.85,\n): { similarity: number; meetsThreshold: boolean } {\n  const similarity = jaroWinkler.similarity(\n    normalizeString(actual),\n    normalizeString(expected),\n  );\n  return {\n    similarity,\n    meetsThreshold: similarity >= similarityThreshold,\n  };\n}\n\n/**\n * generateTimestamp:\n * Generates a timestamp string formatted as \"YYYYMMDDHHMMSS\".\n * Used to create unique experiment names, ensuring that results can be\n * distinguished by the time they were generated.\n */\nexport function generateTimestamp(): string {\n  const now = new Date();\n  return now\n    .toISOString()\n    .replace(/[-:TZ]/g, \"\")\n    .slice(0, 14);\n}\n\n/**\n * generateExperimentName:\n * Creates a unique name for the experiment based on optional evalName or category,\n * the environment (e.g., dev or CI), and the current timestamp.\n * This is used to label the output files and directories.\n */\nexport function generateExperimentName({\n  evalName,\n  category,\n  environment,\n}: {\n  evalName?: string;\n  category?: string;\n  environment: string;\n}): string {\n  const timestamp = generateTimestamp();\n  if (evalName) {\n    return `${evalName}_${environment.toLowerCase()}_${timestamp}`;\n  }\n  if (category) {\n    return `${category}_${environment.toLowerCase()}_${timestamp}`;\n  }\n  return `all_${environment.toLowerCase()}_${timestamp}`;\n}\n\nexport function logLineToString(logLine: LogLine): string {\n  try {\n    const timestamp = logLine.timestamp || new Date().toISOString();\n    if (logLine.auxiliary?.error) {\n      const errorValue = logLine.auxiliary.error?.value ?? \"\";\n      const traceValue = logLine.auxiliary.trace?.value ?? \"\";\n      const traceSuffix = traceValue ? `\\n ${traceValue}` : \"\";\n      return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message}\\n ${errorValue}${traceSuffix}`;\n    }\n    return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message} ${\n      logLine.auxiliary ? JSON.stringify(logLine.auxiliary) : \"\"\n    }`;\n  } catch (error) {\n    console.error(`Error logging line:`, error);\n    return \"error logging line\";\n  }\n}\n\nexport function dedent(\n  strings: TemplateStringsArray,\n  ...values: unknown[]\n): string {\n  // Interleave raw strings with substitution values\n  const raw = strings.raw;\n  let result = \"\";\n\n  for (let i = 0; i < raw.length; i++) {\n    result += raw[i]\n      // replace newline + any mix of spaces/tabs with “\\n”\n      .replace(/\\n[ \\t]+/g, \"\\n\")\n      .replace(/^\\n/, \"\"); // remove leading newline\n    if (i < values.length) result += values[i];\n  }\n\n  // trim trailing/leading blank lines\n  return result.trimEnd();\n}\n\n// Dataset helpers shared by suites\n\nexport function sampleUniform<T>(arr: T[], k: number): T[] {\n  const n = arr.length;\n  if (k >= n) return arr.slice();\n  const copy = arr.slice();\n  for (let i = n - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    const tmp = copy[i];\n    copy[i] = copy[j];\n    copy[j] = tmp;\n  }\n  return copy.slice(0, k);\n}\n\nexport function readJsonlFile(filePath: string): string[] {\n  let lines: string[];\n  try {\n    const content = fs.readFileSync(filePath, \"utf-8\");\n    lines = content.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n  } catch (e) {\n    console.warn(\n      `Could not read file at ${filePath}. Error: ${e instanceof Error ? e.message : String(e)}`,\n    );\n    lines = [];\n  }\n  return lines;\n}\n\nexport function parseJsonlRows<T>(\n  lines: string[],\n  validator: (parsed: unknown) => parsed is T,\n): T[] {\n  const candidates: T[] = [];\n  for (const line of lines) {\n    try {\n      const parsed = JSON.parse(line);\n      if (validator(parsed)) {\n        candidates.push(parsed);\n      }\n    } catch {\n      // skip invalid lines\n    }\n  }\n  return candidates;\n}\n\nexport function applySampling<T>(\n  candidates: T[],\n  sampleCount?: number,\n  maxCases: number = 25,\n): T[] {\n  if (sampleCount && sampleCount > 0) {\n    return sampleUniform(candidates, sampleCount);\n  } else {\n    const result: T[] = [];\n    for (const candidate of candidates) {\n      result.push(candidate);\n      if (result.length >= maxCases) break;\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/CHANGELOG.md",
    "content": "# @browserbasehq/stagehand-server-v3\n\n## 3.6.1\n\n### Patch Changes\n\n- [#1759](https://github.com/browserbase/stagehand/pull/1759) [`505e8c6`](https://github.com/browserbase/stagehand/commit/505e8c6736f3706328dbc8df670c49a018058388) Thanks [@shrey150](https://github.com/shrey150)! - Add bedrock to the provider enum in model configuration schemas and regenerate OpenAPI spec.\n\n- Updated dependencies [[`505e8c6`](https://github.com/browserbase/stagehand/commit/505e8c6736f3706328dbc8df670c49a018058388), [`2f43ffa`](https://github.com/browserbase/stagehand/commit/2f43ffac11778152d17e4c44405770cc32c3ec8c), [`63ee247`](https://github.com/browserbase/stagehand/commit/63ee247ac6bf2992046d4f6b2759f46b15643e36), [`7dc35f5`](https://github.com/browserbase/stagehand/commit/7dc35f5e25689e6518d68b25ef71536d2781c8aa), [`335cf47`](https://github.com/browserbase/stagehand/commit/335cf4730e73bce33e92331d04bda4b0fd42685d), [`6ba0a1d`](https://github.com/browserbase/stagehand/commit/6ba0a1db7fc2d5d5a2f8927b1417d8f1d15eda10), [`4ff3bb8`](https://github.com/browserbase/stagehand/commit/4ff3bb831a6ef6e2d57148e7afb68ea8d23e395d), [`c27054b`](https://github.com/browserbase/stagehand/commit/c27054bbd0508431ade91d655f89efc87bbf5867), [`2abf5b9`](https://github.com/browserbase/stagehand/commit/2abf5b90f1e2bb1442509ef3a686b6128c9cdcf6), [`7817fcc`](https://github.com/browserbase/stagehand/commit/7817fcc315eee4455ce04567cf56c9ec801caf0b), [`7390508`](https://github.com/browserbase/stagehand/commit/73905088c5ed5923d276da9cce2efd0a0a3a46eb), [`611f43a`](https://github.com/browserbase/stagehand/commit/611f43ac8d4c580216d55d2b217c14a9a9c11013), [`521a10e`](https://github.com/browserbase/stagehand/commit/521a10e3698fc5631e219947bc90dad0f8bddaa8), [`2402a3c`](https://github.com/browserbase/stagehand/commit/2402a3c4d50270391b3e6440f4385cdcf5e1eb64)]:\n  - @browserbasehq/stagehand@3.2.0\n\n## 3.6.0\n\n### Minor Changes\n\n- [#1611](https://github.com/browserbase/stagehand/pull/1611) [`8a3c066`](https://github.com/browserbase/stagehand/commit/8a3c06600a9ba98485db7e9ed5c3cc43ea180334) Thanks [@monadoid](https://github.com/monadoid)! - Using `mode` enum instead of old `cua` boolean in openapi spec\n\n### Patch Changes\n\n- [#1604](https://github.com/browserbase/stagehand/pull/1604) [`4753078`](https://github.com/browserbase/stagehand/commit/4753078cc9d37cbdb8d1a63dfdb53ccc4b4c2bd2) Thanks [@miguelg719](https://github.com/miguelg719)! - Enable bedrock\n\n- [#1636](https://github.com/browserbase/stagehand/pull/1636) [`ea33052`](https://github.com/browserbase/stagehand/commit/ea330520a325583b71b87d85beb740df4bdb9b2d) Thanks [@miguelg719](https://github.com/miguelg719)! - Include executionModel on the AgentConfigSchema\n\n- [#1602](https://github.com/browserbase/stagehand/pull/1602) [`22a0502`](https://github.com/browserbase/stagehand/commit/22a0502e8b042bef0cfafa32901984a8be9529d8) Thanks [@miguelg719](https://github.com/miguelg719)! - Include vertex as a supported provider\n\n- Updated dependencies [[`7584f3e`](https://github.com/browserbase/stagehand/commit/7584f3e92e60a557d2b3e0e0d2a2af04c3527523), [`1e1c9c1`](https://github.com/browserbase/stagehand/commit/1e1c9c15773e49d5c3cd36021dbc1d23495c1bce), [`6bef890`](https://github.com/browserbase/stagehand/commit/6bef89090ebd231e77d8092b2c32a0f06303d5a9), [`ffd4b33`](https://github.com/browserbase/stagehand/commit/ffd4b335a873d0f4dcd76ea22d44f47919bf8e49), [`677bff5`](https://github.com/browserbase/stagehand/commit/677bff5834c879a2d95f7dbff918b8e1510516b3), [`65ff464`](https://github.com/browserbase/stagehand/commit/65ff464bc13388eb109eba0a2cf533c1cc202854), [`101bcf2`](https://github.com/browserbase/stagehand/commit/101bcf2da8b527fd6ace6aa291ada5d0f2d90344), [`0a94301`](https://github.com/browserbase/stagehand/commit/0a94301caa991d1aa4cdade6e28a065b1aefb3e2), [`b27c04d`](https://github.com/browserbase/stagehand/commit/b27c04d278c290364347acd0c354a878ea9b7c2d), [`afbd08b`](https://github.com/browserbase/stagehand/commit/afbd08bb6367a9c9f65f67e453667987e4659918), [`e3db9aa`](https://github.com/browserbase/stagehand/commit/e3db9aa863f44270792215801fe6e3a02a1321aa), [`0e8d569`](https://github.com/browserbase/stagehand/commit/0e8d5695f662040f7384e64f46301152802e3c62), [`ff0f979`](https://github.com/browserbase/stagehand/commit/ff0f9795f3b2c1cf4f2610a80ebcb3341a24f987), [`2d89d2b`](https://github.com/browserbase/stagehand/commit/2d89d2b35ce812431956b28e0c8b52d32ddc7a27), [`aac9a19`](https://github.com/browserbase/stagehand/commit/aac9a19bdfbe62e4508631337ab0bfbcf8ae62b2), [`06de50f`](https://github.com/browserbase/stagehand/commit/06de50ff377fd31f1b0fcf79adb996d04562d2c0), [`aa4d981`](https://github.com/browserbase/stagehand/commit/aa4d981e440bdd0e3d3f42ccc310d5958aa25cc6), [`18b1e3b`](https://github.com/browserbase/stagehand/commit/18b1e3bd2b16b721845d52fcf1a45c6158e2403f), [`957d82b`](https://github.com/browserbase/stagehand/commit/957d82b9845b4413b123539e81a2e4a490e74a8a), [`b65756e`](https://github.com/browserbase/stagehand/commit/b65756e9e85643055446aa4a51956f7d6627c89f), [`22e371a`](https://github.com/browserbase/stagehand/commit/22e371ae4c25deb6350328fe02832bf2b2197b94), [`d29b91f`](https://github.com/browserbase/stagehand/commit/d29b91fa506636ca36f724fcf106320de54ec3f3), [`7b4f817`](https://github.com/browserbase/stagehand/commit/7b4f817cafb9829ac81c4b5890c318c7f9521fe4), [`176d420`](https://github.com/browserbase/stagehand/commit/176d42002cc0a2c7d13b4c0ffbbd56b70fdc49e8), [`3f9ca4d`](https://github.com/browserbase/stagehand/commit/3f9ca4d9acc109101357378d29cf969168991608), [`8a3c066`](https://github.com/browserbase/stagehand/commit/8a3c06600a9ba98485db7e9ed5c3cc43ea180334), [`49ead1e`](https://github.com/browserbase/stagehand/commit/49ead1e1e8678a8da0f87ad2042491dacc6b01d7), [`3673369`](https://github.com/browserbase/stagehand/commit/36733691f90c15386cf2a7b47d04ef429b7195ae), [`c465e87`](https://github.com/browserbase/stagehand/commit/c465e87ab41942435132c76338518fb3fa8e7896), [`ae533e4`](https://github.com/browserbase/stagehand/commit/ae533e40195181b53833f8055b1259fb360a927b), [`ea33052`](https://github.com/browserbase/stagehand/commit/ea330520a325583b71b87d85beb740df4bdb9b2d), [`5764ede`](https://github.com/browserbase/stagehand/commit/5764edee7aab00ef1aafafb68fc56eb26c0a70b2), [`f09b184`](https://github.com/browserbase/stagehand/commit/f09b184cc5e774736280ae8c94ba3f4f13adda80), [`a7d29de`](https://github.com/browserbase/stagehand/commit/a7d29decee0f7d12e2437267b9eef1795d3b4e3a), [`d334399`](https://github.com/browserbase/stagehand/commit/d3343990041bf9cd5613569840afb0c17131e33c), [`44416da`](https://github.com/browserbase/stagehand/commit/44416da7ff33301bb32d3811e6c3be8782a7d168), [`bdd8b4e`](https://github.com/browserbase/stagehand/commit/bdd8b4ee3c697a02728375510ab7fae764990576)]:\n  - @browserbasehq/stagehand@3.1.0\n\n## 3.5.0\n\n### Minor Changes\n\n- [#1578](https://github.com/browserbase/stagehand/pull/1578) [`a5074bd`](https://github.com/browserbase/stagehand/commit/a5074bdda0811140ba7847065c26ac72175cef98) Thanks [@monadoid](https://github.com/monadoid)! - /end endpoint no longer takes an empty object - instead, no request body is required.\n\n### Patch Changes\n\n- Updated dependencies [[`40ce5cc`](https://github.com/browserbase/stagehand/commit/40ce5cc83ec758f4e8c37132a7f4ac8eeea7ca34), [`5506f41`](https://github.com/browserbase/stagehand/commit/5506f416d2609d112b553263984e21d7a30e32b1), [`84c05ca`](https://github.com/browserbase/stagehand/commit/84c05ca8de4587181faf128e5c7464fd960caacc), [`692ffa0`](https://github.com/browserbase/stagehand/commit/692ffa0346ad3d121686aba503c0a22844293efa), [`1ef8901`](https://github.com/browserbase/stagehand/commit/1ef8901e1314e90f43b36be20192e652d3b5598f), [`72ac775`](https://github.com/browserbase/stagehand/commit/72ac775a831d6f0f376ceda4426525f93cc21452), [`3d5af07`](https://github.com/browserbase/stagehand/commit/3d5af07f66d6d26d1f5ac4bd9be7183c3381dd92), [`40e1d80`](https://github.com/browserbase/stagehand/commit/40e1d80776b9216422a25a81070ccb3105e56ec2), [`56c0d24`](https://github.com/browserbase/stagehand/commit/56c0d244f9b2431218bfa832ddfc0587930ae038), [`16d72fb`](https://github.com/browserbase/stagehand/commit/16d72fb4c4081dd33bf45605d75c27644ea4c00e), [`088c4cc`](https://github.com/browserbase/stagehand/commit/088c4cc31dc924bb232a9d5a09ab42cd961c2d36), [`4276f4a`](https://github.com/browserbase/stagehand/commit/4276f4abc8bbde215faac6c0321bf243484c376b), [`6005786`](https://github.com/browserbase/stagehand/commit/600578637e65f6fd18b0cdb322b9e0b857708b2f), [`6fbf5fc`](https://github.com/browserbase/stagehand/commit/6fbf5fc811e5e5d9d22f10c5309fbd336892263a), [`704cf18`](https://github.com/browserbase/stagehand/commit/704cf18cb2bdd187ba06c35f05ccb47317a7668c), [`091296e`](https://github.com/browserbase/stagehand/commit/091296e438bb2374c8bb10ef6c08283978145ebf), [`e56c6eb`](https://github.com/browserbase/stagehand/commit/e56c6eb139bf3aad37e98b16626fff13a6c671d0), [`2cb78d0`](https://github.com/browserbase/stagehand/commit/2cb78d0f5ddef9f7337a9a2fe3137f1421df700a), [`5dad639`](https://github.com/browserbase/stagehand/commit/5dad63938f08d968d434bb1ee2804f1e54fb836a), [`b7c2571`](https://github.com/browserbase/stagehand/commit/b7c2571ad4ac563f3ca0518e1f29a40da93e33bc), [`4c69117`](https://github.com/browserbase/stagehand/commit/4c6911748953199dc9aad3eabe98bcf325f871e4)]:\n  - @browserbasehq/stagehand@3.0.8\n\n## 3.2.0\n\n### Minor Changes\n\n- [#1459](https://github.com/browserbase/stagehand/pull/1459) [`abb3469`](https://github.com/browserbase/stagehand/commit/abb3469f51627b318a856fafe6047ff24e681666) Thanks [@monadoid](https://github.com/monadoid)! - Added building of binaries\n\n- [#1457](https://github.com/browserbase/stagehand/pull/1457) [`5fc1281`](https://github.com/browserbase/stagehand/commit/5fc12817a6529d4c59f2e32db92c916095a9a81e) Thanks [@monadoid](https://github.com/monadoid)! - First changeset for stagehand-server\n\n- [#1469](https://github.com/browserbase/stagehand/pull/1469) [`d634d45`](https://github.com/browserbase/stagehand/commit/d634d45a0dbc3a4c876413d94cf4aedace1f56d7) Thanks [@monadoid](https://github.com/monadoid)! - Bump to test binary builds\n\n### Patch Changes\n\n- Updated dependencies [[`0f3991e`](https://github.com/browserbase/stagehand/commit/0f3991eedc0aaff72ef718dda3ddb0839cf4a464), [`e0e22e0`](https://github.com/browserbase/stagehand/commit/e0e22e06bc752a8ffde30f3dbfa58d91e24e6c09), [`f261051`](https://github.com/browserbase/stagehand/commit/f2610517d74774374de9ee93191e663439ef55e5), [`e021674`](https://github.com/browserbase/stagehand/commit/e021674f9641c1c5f9d0c1817c3fdf599eea124d), [`6a5496f`](https://github.com/browserbase/stagehand/commit/6a5496f17dbb716be1ee1aaa4e5ba9d8c723b30b), [`fea1700`](https://github.com/browserbase/stagehand/commit/fea1700552af3319052f463685752501c8e71de3), [`5b288d9`](https://github.com/browserbase/stagehand/commit/5b288d9ac37406ff22460ac8050bea26b87a378e), [`e822f5a`](https://github.com/browserbase/stagehand/commit/e822f5a8898df9eb48ca32c321025f0c74b638f0), [`638efc7`](https://github.com/browserbase/stagehand/commit/638efc7fea401bc43dd05dceedf4c13a3495a728), [`a890f16`](https://github.com/browserbase/stagehand/commit/a890f16fa3a752f308f858e5ab9c9a0faf6b3b34), [`934f492`](https://github.com/browserbase/stagehand/commit/934f492ec587bef81f0ce75b45a35b44ab545712), [`bd2db92`](https://github.com/browserbase/stagehand/commit/bd2db925f66a826d61d58be1611d55646cbdb560), [`51e0170`](https://github.com/browserbase/stagehand/commit/51e01709ce1c947c1947b4e2cb0b1f4f97b77182), [`05f5580`](https://github.com/browserbase/stagehand/commit/05f5580937c3c157550e3c25ae6671f44f562211), [`f56a9c2`](https://github.com/browserbase/stagehand/commit/f56a9c296d4ddce25a405358c66837f8ce4d679f), [`b40ae11`](https://github.com/browserbase/stagehand/commit/b40ae11391af49c3581fce27faa1b7483fc4a169), [`0d2b398`](https://github.com/browserbase/stagehand/commit/0d2b398cd40b32a9ecaf28ede70853036b7c91bd), [`cd01f29`](https://github.com/browserbase/stagehand/commit/cd01f290578eac703521f801ba3712f5332918f3), [`a734fca`](https://github.com/browserbase/stagehand/commit/a734fca0b4573753767d3ebc48ec414baf4f23e1), [`b342acf`](https://github.com/browserbase/stagehand/commit/b342acfaae058127fb57664644c5fd965db02bf2), [`2987cd1`](https://github.com/browserbase/stagehand/commit/2987cd1e5ffabefa9411936609635d4a638faed5), [`dfab1d5`](https://github.com/browserbase/stagehand/commit/dfab1d566299c8c5a63f20565a6da07dc8f61ccd), [`4d71162`](https://github.com/browserbase/stagehand/commit/4d71162beb119635b69b17637564a2bbd0e373e7)]:\n  - @browserbasehq/stagehand@3.0.7\n\n## 3.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`605ed6b`](https://github.com/browserbase/stagehand/commit/605ed6b81a3ff8f25d4022f1e5fce6b42aecfc19), [`34e7e5b`](https://github.com/browserbase/stagehand/commit/34e7e5b292f5e6af6efc0da60118663310c5f718), [`943d2d7`](https://github.com/browserbase/stagehand/commit/943d2d79d0f289ac41c9164578f2f1dd876058f2), [`0e95cd2`](https://github.com/browserbase/stagehand/commit/0e95cd2f67672f64f0017024fd47d8b3aef59a95), [`d4237e4`](https://github.com/browserbase/stagehand/commit/d4237e40951ecd10abfdbe766672d498f8806484), [`86975e7`](https://github.com/browserbase/stagehand/commit/86975e795db7505804949a267b20509bd16b5256), [`d5e119b`](https://github.com/browserbase/stagehand/commit/d5e119be5eec84915a79f8d611b6ba0546f48c99), [`4e051b2`](https://github.com/browserbase/stagehand/commit/4e051b23add7ae276b0dbead38b4587838cfc1c1), [`6b5a3c9`](https://github.com/browserbase/stagehand/commit/6b5a3c9035654caaed2da375085b465edda97de4), [`bb85ad9`](https://github.com/browserbase/stagehand/commit/bb85ad912738623a7a866f0cb6e8d5807c6c2738), [`88d28cc`](https://github.com/browserbase/stagehand/commit/88d28cc6f31058d1cf6ec6dc948a4ae77a926b3c), [`45bcef0`](https://github.com/browserbase/stagehand/commit/45bcef0e5788b083f9e38dfd7c3bc63afcd4b6dd), [`6aa9d45`](https://github.com/browserbase/stagehand/commit/6aa9d455aa5836ec2ee8ab2e8b9df3fb218e5381), [`d382084`](https://github.com/browserbase/stagehand/commit/d382084745fff98c3e71413371466394a2625429), [`1df08cc`](https://github.com/browserbase/stagehand/commit/1df08ccb0a2cf73b5c37a91c129721114ff6371c), [`2b56600`](https://github.com/browserbase/stagehand/commit/2b566009606fcbba987260f21b075b318690ce99)]:\n  - @browserbasehq/stagehand@3.0.6\n"
  },
  {
    "path": "packages/server-v3/README.md",
    "content": "# Stagehand API\n\nThe Stagehand  is a powerful service that provides a RESTful interface for browser automation and session management using the Browserbase platform. It enables recording, playback, and manipulation of browser sessions with a focus on reliability and performance.\n\n## 📋 Prerequisites\n\nTo run the Stagehand API locally, ensure you have the following installed:\n\n- Node.js\n- pnpm\n\n## 🛠 Installation\n\n1. Clone the repository:\n\n```bash\ngit clone https://github.com/browserbase/stagehand/\ncd stagehand/packages/server-v3\n```\n\n2. Install dependencies:\n\n```bash\npnpm install\n```\n\n3. Set up environment variables:\n\n```bash\ncp .env.example .env\n```\n\n4. Configure your `.env` file with the environment variables required by `src/lib/env.ts` (BB environment, API base URLs, etc.).\n\n5. `pnpm dev`\n\n"
  },
  {
    "path": "packages/server-v3/SDK_RELEASE_WORKFLOW.md",
    "content": ""
  },
  {
    "path": "packages/server-v3/openapi.v3.yaml",
    "content": "openapi: \"3.1.0\"\ninfo:\n  title: Stagehand API\n  version: \"3.1.0\"\n  description: >-\n    Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\n\n    execute browser automation tasks remotely on the Browserbase cloud.\n\n    All endpoints except /sessions/start require an active session ID.\n\n    Responses are streamed using Server-Sent Events (SSE) when the\n\n    `x-stream-response: true` header is provided.\n\n\n    This SDK is currently ALPHA software and is not production ready!\n\n    Please try it and give us your feedback, stay tuned for upcoming release\n    announcements!\n  contact:\n    name: Browserbase\n    url: https://browserbase.com\ncomponents:\n  securitySchemes:\n    BrowserbaseApiKey:\n      type: apiKey\n      in: header\n      name: x-bb-api-key\n      description: Browserbase API key for authentication\n    BrowserbaseProjectId:\n      type: apiKey\n      in: header\n      name: x-bb-project-id\n      description: Browserbase project ID\n    ModelApiKey:\n      type: apiKey\n      in: header\n      name: x-model-api-key\n      description: API key for the AI model provider (OpenAI, Anthropic, etc.)\n  links:\n    SessionAct:\n      operationId: SessionAct\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Perform an action on the session\n    SessionExtract:\n      operationId: SessionExtract\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Extract data from the session\n    SessionObserve:\n      operationId: SessionObserve\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Observe available actions on the session\n    SessionNavigate:\n      operationId: SessionNavigate\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Navigate to a URL in the session\n    SessionAgentExecute:\n      operationId: SessionAgentExecute\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Execute an agent on the session\n    SessionReplay:\n      operationId: SessionReplay\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Replay session metrics\n    SessionEnd:\n      operationId: SessionEnd\n      parameters:\n        id: $response.body#/data/sessionId\n      description: End the session and release resources\n  schemas:\n    AgentCacheEntry:\n      type: object\n      properties:\n        cacheKey:\n          description: Opaque cache identifier computed from instruction, URL, options,\n            and config\n          type: string\n        entry:\n          description: Serialized cache entry that can be written to disk\n      required:\n        - cacheKey\n        - entry\n    BrowserbaseRegion:\n      type: string\n      enum:\n        - us-west-2\n        - us-east-1\n        - eu-central-1\n        - ap-southeast-1\n    LocalBrowserLaunchOptions:\n      type: object\n      properties:\n        args:\n          type: array\n          items:\n            type: string\n        executablePath:\n          type: string\n        port:\n          type: number\n        userDataDir:\n          type: string\n        preserveUserDataDir:\n          type: boolean\n        headless:\n          type: boolean\n        devtools:\n          type: boolean\n        chromiumSandbox:\n          type: boolean\n        ignoreDefaultArgs:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                type: string\n        proxy:\n          type: object\n          properties:\n            server:\n              type: string\n            bypass:\n              type: string\n            username:\n              type: string\n            password:\n              type: string\n          required:\n            - server\n        locale:\n          type: string\n        viewport:\n          type: object\n          properties:\n            width:\n              type: number\n            height:\n              type: number\n          required:\n            - width\n            - height\n        deviceScaleFactor:\n          type: number\n        hasTouch:\n          type: boolean\n        ignoreHTTPSErrors:\n          type: boolean\n        cdpUrl:\n          type: string\n        cdpHeaders:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties:\n            type: string\n        connectTimeoutMs:\n          type: number\n        downloadsPath:\n          type: string\n        acceptDownloads:\n          type: boolean\n      additionalProperties: false\n    ModelConfigObject:\n      type: object\n      properties:\n        provider:\n          description: AI provider for the model (or provide a baseURL endpoint instead)\n          example: openai\n          type: string\n          enum:\n            - openai\n            - anthropic\n            - google\n            - microsoft\n            - bedrock\n        modelName:\n          description: Model name string with provider prefix (e.g., 'openai/gpt-5-nano')\n          example: openai/gpt-5-nano\n          type: string\n        apiKey:\n          description: API key for the model provider\n          example: sk-some-openai-api-key\n          type: string\n        baseURL:\n          description: Base URL for the model provider\n          example: https://api.openai.com/v1\n          type: string\n          format: uri\n      required:\n        - modelName\n    ModelConfig:\n      $ref: \"#/components/schemas/ModelConfigObject\"\n    Action:\n      description: Action object returned by observe and used by act\n      type: object\n      properties:\n        selector:\n          description: CSS selector or XPath for the element\n          example: \"[data-testid='submit-button']\"\n          type: string\n        description:\n          description: Human-readable description of the action\n          example: Click the submit button\n          type: string\n        backendNodeId:\n          description: Backend node ID for the element\n          type: number\n        method:\n          description: The method to execute (click, fill, etc.)\n          example: click\n          type: string\n        arguments:\n          description: Arguments to pass to the method\n          example:\n            - Hello World\n          type: array\n          items:\n            type: string\n      required:\n        - selector\n        - description\n    BrowserConfig:\n      type: object\n      properties:\n        type:\n          description: Browser type to use\n          example: local\n          type: string\n          enum:\n            - local\n            - browserbase\n        cdpUrl:\n          description: Chrome DevTools Protocol URL for connecting to existing browser\n          example: ws://localhost:9222\n          type: string\n        launchOptions:\n          $ref: \"#/components/schemas/LocalBrowserLaunchOptions\"\n    BrowserbaseViewport:\n      type: object\n      properties:\n        width:\n          type: number\n        height:\n          type: number\n    BrowserbaseFingerprintScreen:\n      type: object\n      properties:\n        maxHeight:\n          type: number\n        maxWidth:\n          type: number\n        minHeight:\n          type: number\n        minWidth:\n          type: number\n    BrowserbaseFingerprint:\n      type: object\n      properties:\n        browsers:\n          type: array\n          items:\n            type: string\n            enum:\n              - chrome\n              - edge\n              - firefox\n              - safari\n        devices:\n          type: array\n          items:\n            type: string\n            enum:\n              - desktop\n              - mobile\n        httpVersion:\n          type: string\n          enum:\n            - \"1\"\n            - \"2\"\n        locales:\n          type: array\n          items:\n            type: string\n        operatingSystems:\n          type: array\n          items:\n            type: string\n            enum:\n              - android\n              - ios\n              - linux\n              - macos\n              - windows\n        screen:\n          $ref: \"#/components/schemas/BrowserbaseFingerprintScreen\"\n    BrowserbaseContext:\n      type: object\n      properties:\n        id:\n          type: string\n        persist:\n          type: boolean\n      required:\n        - id\n    BrowserbaseBrowserSettings:\n      type: object\n      properties:\n        advancedStealth:\n          type: boolean\n        blockAds:\n          type: boolean\n        context:\n          $ref: \"#/components/schemas/BrowserbaseContext\"\n        extensionId:\n          type: string\n        fingerprint:\n          $ref: \"#/components/schemas/BrowserbaseFingerprint\"\n        logSession:\n          type: boolean\n        recordSession:\n          type: boolean\n        solveCaptchas:\n          type: boolean\n        viewport:\n          $ref: \"#/components/schemas/BrowserbaseViewport\"\n    BrowserbaseProxyGeolocation:\n      type: object\n      properties:\n        country:\n          type: string\n        city:\n          type: string\n        state:\n          type: string\n      required:\n        - country\n    BrowserbaseProxyConfig:\n      type: object\n      properties:\n        type:\n          type: string\n          const: browserbase\n        domainPattern:\n          type: string\n        geolocation:\n          $ref: \"#/components/schemas/BrowserbaseProxyGeolocation\"\n      required:\n        - type\n    ExternalProxyConfig:\n      type: object\n      properties:\n        type:\n          type: string\n          const: external\n        server:\n          type: string\n        domainPattern:\n          type: string\n        username:\n          type: string\n        password:\n          type: string\n      required:\n        - type\n        - server\n    ProxyConfig:\n      oneOf:\n        - $ref: \"#/components/schemas/BrowserbaseProxyConfig\"\n        - $ref: \"#/components/schemas/ExternalProxyConfig\"\n      type: object\n      discriminator:\n        propertyName: type\n        mapping:\n          browserbase: \"#/components/schemas/BrowserbaseProxyConfig\"\n          external: \"#/components/schemas/ExternalProxyConfig\"\n    BrowserbaseSessionCreateParams:\n      type: object\n      properties:\n        projectId:\n          type: string\n        browserSettings:\n          $ref: \"#/components/schemas/BrowserbaseBrowserSettings\"\n        extensionId:\n          type: string\n        keepAlive:\n          type: boolean\n        proxies:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                $ref: \"#/components/schemas/ProxyConfig\"\n        region:\n          $ref: \"#/components/schemas/BrowserbaseRegion\"\n        timeout:\n          type: number\n        userMetadata:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n    SessionStartRequest:\n      type: object\n      properties:\n        modelName:\n          description: Model name to use for AI operations\n          example: openai/gpt-4o\n          type: string\n        domSettleTimeoutMs:\n          description: Timeout in ms to wait for DOM to settle\n          example: 5000\n          type: number\n        verbose:\n          description: Logging verbosity level (0=quiet, 1=normal, 2=debug)\n          example: 1\n          type: number\n          enum:\n            - 0\n            - 1\n            - 2\n        systemPrompt:\n          description: Custom system prompt for AI operations\n          type: string\n        browserbaseSessionCreateParams:\n          $ref: \"#/components/schemas/BrowserbaseSessionCreateParams\"\n        browser:\n          $ref: \"#/components/schemas/BrowserConfig\"\n        selfHeal:\n          description: Enable self-healing for failed actions\n          example: true\n          type: boolean\n        browserbaseSessionID:\n          description: Existing Browserbase session ID to resume\n          type: string\n        experimental:\n          type: boolean\n        waitForCaptchaSolves:\n          description: Wait for captcha solves (deprecated, v2 only)\n          type: boolean\n        actTimeoutMs:\n          description: Timeout in ms for act operations (deprecated, v2 only)\n          type: number\n      required:\n        - modelName\n    SessionStartResult:\n      type: object\n      properties:\n        sessionId:\n          description: Unique Browserbase session identifier\n          example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n          type: string\n        cdpUrl:\n          description: CDP WebSocket URL for connecting to the Browserbase cloud browser\n            (present when available)\n          example: wss://connect.browserbase.com/?signingKey=abc123\n          anyOf:\n            - type: string\n            - type: \"null\"\n        available:\n          type: boolean\n      required:\n        - sessionId\n        - available\n    ActOptions:\n      type: object\n      properties:\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfig\"\n            - type: string\n        variables:\n          description: Variables to substitute in the action instruction\n          example:\n            username: john_doe\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties:\n            type: string\n        timeout:\n          description: Timeout in ms for the action\n          example: 30000\n          type: number\n    ActRequest:\n      type: object\n      properties:\n        input:\n          description: Natural language instruction or Action object\n          example: Click the login button\n          anyOf:\n            - type: string\n            - $ref: \"#/components/schemas/Action\"\n        options:\n          $ref: \"#/components/schemas/ActOptions\"\n        frameId:\n          description: Target frame ID for the action\n          anyOf:\n            - type: string\n            - type: \"null\"\n        streamResponse:\n          description: Whether to stream the response via SSE\n          example: true\n          type: boolean\n      required:\n        - input\n    ActResultData:\n      type: object\n      properties:\n        success:\n          description: Whether the action completed successfully\n          example: true\n          type: boolean\n        message:\n          description: Human-readable result message\n          example: Successfully clicked the login button\n          type: string\n        actionDescription:\n          description: Description of the action that was performed\n          example: Clicked button with text 'Login'\n          type: string\n        actions:\n          description: List of actions that were executed\n          type: array\n          items:\n            $ref: \"#/components/schemas/Action\"\n      required:\n        - success\n        - message\n        - actionDescription\n        - actions\n    ActResult:\n      type: object\n      properties:\n        result:\n          $ref: \"#/components/schemas/ActResultData\"\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n    ExtractOptions:\n      type: object\n      properties:\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfig\"\n            - type: string\n        timeout:\n          description: Timeout in ms for the extraction\n          example: 30000\n          type: number\n        selector:\n          description: CSS selector to scope extraction to a specific element\n          example: \"#main-content\"\n          type: string\n    ExtractRequest:\n      type: object\n      properties:\n        instruction:\n          description: Natural language instruction for what to extract\n          example: Extract all product names and prices from the page\n          type: string\n        schema:\n          description: JSON Schema defining the structure of data to extract\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        options:\n          $ref: \"#/components/schemas/ExtractOptions\"\n        frameId:\n          description: Target frame ID for the extraction\n          anyOf:\n            - type: string\n            - type: \"null\"\n        streamResponse:\n          description: Whether to stream the response via SSE\n          example: true\n          type: boolean\n    ExtractResult:\n      type: object\n      properties:\n        result:\n          description: Extracted data matching the requested schema\n          x-stainless-any: true\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n    ObserveOptions:\n      type: object\n      properties:\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfig\"\n            - type: string\n        timeout:\n          description: Timeout in ms for the observation\n          example: 30000\n          type: number\n        selector:\n          description: CSS selector to scope observation to a specific element\n          example: nav\n          type: string\n    ObserveRequest:\n      type: object\n      properties:\n        instruction:\n          description: Natural language instruction for what actions to find\n          example: Find all clickable navigation links\n          type: string\n        options:\n          $ref: \"#/components/schemas/ObserveOptions\"\n        frameId:\n          description: Target frame ID for the observation\n          anyOf:\n            - type: string\n            - type: \"null\"\n        streamResponse:\n          description: Whether to stream the response via SSE\n          example: true\n          type: boolean\n    ObserveResult:\n      type: object\n      properties:\n        result:\n          type: array\n          items:\n            $ref: \"#/components/schemas/Action\"\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n    AgentConfig:\n      type: object\n      properties:\n        provider:\n          description: \"AI provider for the agent (legacy, use model: openai/gpt-5-nano\n            instead)\"\n          example: openai\n          type: string\n          enum:\n            - openai\n            - anthropic\n            - google\n            - microsoft\n            - bedrock\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfig\"\n            - type: string\n        systemPrompt:\n          description: Custom system prompt for the agent\n          type: string\n        cua:\n          description: \"Deprecated. Use mode: 'cua' instead. If both are provided, mode\n            takes precedence.\"\n          example: true\n          type: boolean\n        mode:\n          description: Tool mode for the agent (dom, hybrid, cua). If set, overrides cua.\n          example: cua\n          type: string\n          enum:\n            - dom\n            - hybrid\n            - cua\n        executionModel:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano') for tool execution (observe/act calls within\n            agent tools). If not specified, inherits from the main model\n            configuration.\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfig\"\n            - type: string\n    AgentAction:\n      type: object\n      properties:\n        type:\n          description: Type of action taken\n          example: click\n          type: string\n        reasoning:\n          description: Agent's reasoning for taking this action\n          type: string\n        taskCompleted:\n          type: boolean\n        action:\n          type: string\n        timeMs:\n          description: Time taken for this action in ms\n          type: number\n        pageText:\n          type: string\n        pageUrl:\n          type: string\n        instruction:\n          type: string\n      required:\n        - type\n      additionalProperties: {}\n    AgentUsage:\n      type: object\n      properties:\n        input_tokens:\n          example: 1500\n          type: number\n        output_tokens:\n          example: 250\n          type: number\n        reasoning_tokens:\n          type: number\n        cached_input_tokens:\n          type: number\n        inference_time_ms:\n          example: 2500\n          type: number\n      required:\n        - input_tokens\n        - output_tokens\n        - inference_time_ms\n    AgentResultData:\n      type: object\n      properties:\n        success:\n          description: Whether the agent completed successfully\n          example: true\n          type: boolean\n        message:\n          description: Summary of what the agent accomplished\n          example: Successfully logged in and navigated to dashboard\n          type: string\n        actions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/AgentAction\"\n        completed:\n          description: Whether the agent finished its task\n          example: true\n          type: boolean\n        metadata:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        usage:\n          $ref: \"#/components/schemas/AgentUsage\"\n      required:\n        - success\n        - message\n        - actions\n        - completed\n    AgentExecuteOptions:\n      type: object\n      properties:\n        instruction:\n          description: Natural language instruction for the agent\n          example: Log in with username 'demo' and password 'test123', then navigate to\n            settings\n          type: string\n        maxSteps:\n          description: Maximum number of steps the agent can take\n          example: 20\n          type: number\n        highlightCursor:\n          description: Whether to visually highlight the cursor during execution\n          example: true\n          type: boolean\n        useSearch:\n          description: Whether to enable the web search tool powered by Browserbase Search\n            API\n          example: true\n          type: boolean\n        toolTimeout:\n          description: Timeout in milliseconds for each agent tool call\n          example: 30000\n          type: number\n      required:\n        - instruction\n    AgentExecuteRequest:\n      type: object\n      properties:\n        agentConfig:\n          $ref: \"#/components/schemas/AgentConfig\"\n        executeOptions:\n          $ref: \"#/components/schemas/AgentExecuteOptions\"\n        frameId:\n          description: Target frame ID for the agent\n          anyOf:\n            - type: string\n            - type: \"null\"\n        streamResponse:\n          description: Whether to stream the response via SSE\n          example: true\n          type: boolean\n        shouldCache:\n          description: If true, the server captures a cache entry and returns it to the\n            client\n          type: boolean\n      required:\n        - agentConfig\n        - executeOptions\n    AgentExecuteResult:\n      type: object\n      properties:\n        result:\n          $ref: \"#/components/schemas/AgentResultData\"\n        cacheEntry:\n          $ref: \"#/components/schemas/AgentCacheEntry\"\n      required:\n        - result\n    NavigateOptions:\n      type: object\n      properties:\n        referer:\n          description: Referer header to send with the request\n          type: string\n        timeout:\n          description: Timeout in ms for the navigation\n          example: 30000\n          type: number\n        waitUntil:\n          description: When to consider navigation complete\n          example: networkidle\n          type: string\n          enum:\n            - load\n            - domcontentloaded\n            - networkidle\n    NavigateRequest:\n      type: object\n      properties:\n        url:\n          description: URL to navigate to\n          example: https://example.com\n          type: string\n        options:\n          $ref: \"#/components/schemas/NavigateOptions\"\n        frameId:\n          description: Target frame ID for the navigation\n          anyOf:\n            - type: string\n            - type: \"null\"\n        streamResponse:\n          description: Whether to stream the response via SSE\n          example: true\n          type: boolean\n      required:\n        - url\n    NavigateResult:\n      type: object\n      properties:\n        result:\n          description: Navigation response (Playwright Response object or null)\n          anyOf:\n            - {}\n            - type: \"null\"\n          x-stainless-any: true\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n    TokenUsage:\n      type: object\n      properties:\n        inputTokens:\n          type: number\n        outputTokens:\n          type: number\n        timeMs:\n          type: number\n        cost:\n          type: number\n    ReplayAction:\n      type: object\n      properties:\n        method:\n          type: string\n        parameters:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        result:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        timestamp:\n          type: number\n        endTime:\n          type: number\n        tokenUsage:\n          $ref: \"#/components/schemas/TokenUsage\"\n      required:\n        - method\n        - parameters\n        - result\n        - timestamp\n    ReplayPage:\n      type: object\n      properties:\n        url:\n          type: string\n        timestamp:\n          type: number\n        duration:\n          type: number\n        actions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/ReplayAction\"\n      required:\n        - url\n        - timestamp\n        - duration\n        - actions\n    ReplayResult:\n      type: object\n      properties:\n        pages:\n          type: array\n          items:\n            $ref: \"#/components/schemas/ReplayPage\"\n        clientLanguage:\n          type: string\n      required:\n        - pages\n    StreamEventStatus:\n      description: Current status of the streaming operation\n      type: string\n      enum:\n        - starting\n        - connected\n        - running\n        - finished\n        - error\n    StreamEventType:\n      description: Type of stream event - system events or log messages\n      type: string\n      enum:\n        - system\n        - log\n    StreamEventSystemData:\n      type: object\n      properties:\n        status:\n          $ref: \"#/components/schemas/StreamEventStatus\"\n        result:\n          description: Operation result (present when status is 'finished')\n          x-stainless-any: true\n        error:\n          description: Error message (present when status is 'error')\n          type: string\n      required:\n        - status\n    StreamEventLogData:\n      type: object\n      properties:\n        status:\n          type: string\n          const: running\n        message:\n          description: Log message from the operation\n          type: string\n      required:\n        - status\n        - message\n    SessionStartResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/SessionStartResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    SessionEndResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n      required:\n        - success\n      additionalProperties: false\n    ActResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/ActResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    ExtractResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/ExtractResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    ObserveResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/ObserveResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    AgentExecuteResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/AgentExecuteResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    NavigateResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/NavigateResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    ReplayResponse:\n      type: object\n      properties:\n        success:\n          description: Indicates whether the request was successful\n          type: boolean\n        data:\n          $ref: \"#/components/schemas/ReplayResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    AgentCacheEntryOutput:\n      type: object\n      properties:\n        cacheKey:\n          description: Opaque cache identifier computed from instruction, URL, options,\n            and config\n          type: string\n        entry:\n          description: Serialized cache entry that can be written to disk\n      required:\n        - cacheKey\n        - entry\n      additionalProperties: false\n    LocalBrowserLaunchOptionsOutput:\n      type: object\n      properties:\n        args:\n          type: array\n          items:\n            type: string\n        executablePath:\n          type: string\n        port:\n          type: number\n        userDataDir:\n          type: string\n        preserveUserDataDir:\n          type: boolean\n        headless:\n          type: boolean\n        devtools:\n          type: boolean\n        chromiumSandbox:\n          type: boolean\n        ignoreDefaultArgs:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                type: string\n        proxy:\n          type: object\n          properties:\n            server:\n              type: string\n            bypass:\n              type: string\n            username:\n              type: string\n            password:\n              type: string\n          required:\n            - server\n          additionalProperties: false\n        locale:\n          type: string\n        viewport:\n          type: object\n          properties:\n            width:\n              type: number\n            height:\n              type: number\n          required:\n            - width\n            - height\n          additionalProperties: false\n        deviceScaleFactor:\n          type: number\n        hasTouch:\n          type: boolean\n        ignoreHTTPSErrors:\n          type: boolean\n        cdpUrl:\n          type: string\n        cdpHeaders:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties:\n            type: string\n        connectTimeoutMs:\n          type: number\n        downloadsPath:\n          type: string\n        acceptDownloads:\n          type: boolean\n      additionalProperties: false\n    ModelConfigObjectOutput:\n      type: object\n      properties:\n        provider:\n          description: AI provider for the model (or provide a baseURL endpoint instead)\n          example: openai\n          type: string\n          enum:\n            - openai\n            - anthropic\n            - google\n            - microsoft\n            - bedrock\n        modelName:\n          description: Model name string with provider prefix (e.g., 'openai/gpt-5-nano')\n          example: openai/gpt-5-nano\n          type: string\n        apiKey:\n          description: API key for the model provider\n          example: sk-some-openai-api-key\n          type: string\n        baseURL:\n          description: Base URL for the model provider\n          example: https://api.openai.com/v1\n          type: string\n          format: uri\n      required:\n        - modelName\n      additionalProperties: false\n    ModelConfigOutput:\n      $ref: \"#/components/schemas/ModelConfigObjectOutput\"\n    ActionOutput:\n      description: Action object returned by observe and used by act\n      type: object\n      properties:\n        selector:\n          description: CSS selector or XPath for the element\n          example: \"[data-testid='submit-button']\"\n          type: string\n        description:\n          description: Human-readable description of the action\n          example: Click the submit button\n          type: string\n        backendNodeId:\n          description: Backend node ID for the element\n          type: number\n        method:\n          description: The method to execute (click, fill, etc.)\n          example: click\n          type: string\n        arguments:\n          description: Arguments to pass to the method\n          example:\n            - Hello World\n          type: array\n          items:\n            type: string\n      required:\n        - selector\n        - description\n      additionalProperties: false\n    BrowserConfigOutput:\n      type: object\n      properties:\n        type:\n          description: Browser type to use\n          example: local\n          type: string\n          enum:\n            - local\n            - browserbase\n        cdpUrl:\n          description: Chrome DevTools Protocol URL for connecting to existing browser\n          example: ws://localhost:9222\n          type: string\n        launchOptions:\n          $ref: \"#/components/schemas/LocalBrowserLaunchOptionsOutput\"\n      additionalProperties: false\n    BrowserbaseViewportOutput:\n      type: object\n      properties:\n        width:\n          type: number\n        height:\n          type: number\n      additionalProperties: false\n    BrowserbaseFingerprintScreenOutput:\n      type: object\n      properties:\n        maxHeight:\n          type: number\n        maxWidth:\n          type: number\n        minHeight:\n          type: number\n        minWidth:\n          type: number\n      additionalProperties: false\n    BrowserbaseFingerprintOutput:\n      type: object\n      properties:\n        browsers:\n          type: array\n          items:\n            type: string\n            enum:\n              - chrome\n              - edge\n              - firefox\n              - safari\n        devices:\n          type: array\n          items:\n            type: string\n            enum:\n              - desktop\n              - mobile\n        httpVersion:\n          type: string\n          enum:\n            - \"1\"\n            - \"2\"\n        locales:\n          type: array\n          items:\n            type: string\n        operatingSystems:\n          type: array\n          items:\n            type: string\n            enum:\n              - android\n              - ios\n              - linux\n              - macos\n              - windows\n        screen:\n          $ref: \"#/components/schemas/BrowserbaseFingerprintScreenOutput\"\n      additionalProperties: false\n    BrowserbaseContextOutput:\n      type: object\n      properties:\n        id:\n          type: string\n        persist:\n          type: boolean\n      required:\n        - id\n      additionalProperties: false\n    BrowserbaseBrowserSettingsOutput:\n      type: object\n      properties:\n        advancedStealth:\n          type: boolean\n        blockAds:\n          type: boolean\n        context:\n          $ref: \"#/components/schemas/BrowserbaseContextOutput\"\n        extensionId:\n          type: string\n        fingerprint:\n          $ref: \"#/components/schemas/BrowserbaseFingerprintOutput\"\n        logSession:\n          type: boolean\n        recordSession:\n          type: boolean\n        solveCaptchas:\n          type: boolean\n        viewport:\n          $ref: \"#/components/schemas/BrowserbaseViewportOutput\"\n      additionalProperties: false\n    BrowserbaseProxyGeolocationOutput:\n      type: object\n      properties:\n        country:\n          type: string\n        city:\n          type: string\n        state:\n          type: string\n      required:\n        - country\n      additionalProperties: false\n    BrowserbaseProxyConfigOutput:\n      type: object\n      properties:\n        type:\n          type: string\n          const: browserbase\n        domainPattern:\n          type: string\n        geolocation:\n          $ref: \"#/components/schemas/BrowserbaseProxyGeolocationOutput\"\n      required:\n        - type\n      additionalProperties: false\n    ExternalProxyConfigOutput:\n      type: object\n      properties:\n        type:\n          type: string\n          const: external\n        server:\n          type: string\n        domainPattern:\n          type: string\n        username:\n          type: string\n        password:\n          type: string\n      required:\n        - type\n        - server\n      additionalProperties: false\n    ProxyConfigOutput:\n      oneOf:\n        - $ref: \"#/components/schemas/BrowserbaseProxyConfigOutput\"\n        - $ref: \"#/components/schemas/ExternalProxyConfigOutput\"\n      type: object\n      discriminator:\n        propertyName: type\n        mapping:\n          browserbase: \"#/components/schemas/BrowserbaseProxyConfigOutput\"\n          external: \"#/components/schemas/ExternalProxyConfigOutput\"\n    BrowserbaseSessionCreateParamsOutput:\n      type: object\n      properties:\n        projectId:\n          type: string\n        browserSettings:\n          $ref: \"#/components/schemas/BrowserbaseBrowserSettingsOutput\"\n        extensionId:\n          type: string\n        keepAlive:\n          type: boolean\n        proxies:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                $ref: \"#/components/schemas/ProxyConfigOutput\"\n        region:\n          $ref: \"#/components/schemas/BrowserbaseRegion\"\n        timeout:\n          type: number\n        userMetadata:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n      additionalProperties: false\n    SessionStartResultOutput:\n      type: object\n      properties:\n        sessionId:\n          description: Unique Browserbase session identifier\n          example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n          type: string\n        cdpUrl:\n          description: CDP WebSocket URL for connecting to the Browserbase cloud browser\n            (present when available)\n          example: wss://connect.browserbase.com/?signingKey=abc123\n          anyOf:\n            - type: string\n            - type: \"null\"\n        available:\n          type: boolean\n      required:\n        - sessionId\n        - available\n      additionalProperties: false\n    ActOptionsOutput:\n      type: object\n      properties:\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfigOutput\"\n            - type: string\n        variables:\n          description: Variables to substitute in the action instruction\n          example:\n            username: john_doe\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties:\n            type: string\n        timeout:\n          description: Timeout in ms for the action\n          example: 30000\n          type: number\n      additionalProperties: false\n    ActResultDataOutput:\n      type: object\n      properties:\n        success:\n          description: Whether the action completed successfully\n          example: true\n          type: boolean\n        message:\n          description: Human-readable result message\n          example: Successfully clicked the login button\n          type: string\n        actionDescription:\n          description: Description of the action that was performed\n          example: Clicked button with text 'Login'\n          type: string\n        actions:\n          description: List of actions that were executed\n          type: array\n          items:\n            $ref: \"#/components/schemas/ActionOutput\"\n      required:\n        - success\n        - message\n        - actionDescription\n        - actions\n      additionalProperties: false\n    ActResultOutput:\n      type: object\n      properties:\n        result:\n          $ref: \"#/components/schemas/ActResultDataOutput\"\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n      additionalProperties: false\n    ExtractOptionsOutput:\n      type: object\n      properties:\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfigOutput\"\n            - type: string\n        timeout:\n          description: Timeout in ms for the extraction\n          example: 30000\n          type: number\n        selector:\n          description: CSS selector to scope extraction to a specific element\n          example: \"#main-content\"\n          type: string\n      additionalProperties: false\n    ExtractResultOutput:\n      type: object\n      properties:\n        result:\n          description: Extracted data matching the requested schema\n          x-stainless-any: true\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n      additionalProperties: false\n    ObserveOptionsOutput:\n      type: object\n      properties:\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfigOutput\"\n            - type: string\n        timeout:\n          description: Timeout in ms for the observation\n          example: 30000\n          type: number\n        selector:\n          description: CSS selector to scope observation to a specific element\n          example: nav\n          type: string\n      additionalProperties: false\n    ObserveResultOutput:\n      type: object\n      properties:\n        result:\n          type: array\n          items:\n            $ref: \"#/components/schemas/ActionOutput\"\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n      additionalProperties: false\n    AgentConfigOutput:\n      type: object\n      properties:\n        provider:\n          description: \"AI provider for the agent (legacy, use model: openai/gpt-5-nano\n            instead)\"\n          example: openai\n          type: string\n          enum:\n            - openai\n            - anthropic\n            - google\n            - microsoft\n            - bedrock\n        model:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano')\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfigOutput\"\n            - type: string\n        systemPrompt:\n          description: Custom system prompt for the agent\n          type: string\n        cua:\n          description: \"Deprecated. Use mode: 'cua' instead. If both are provided, mode\n            takes precedence.\"\n          example: true\n          type: boolean\n        mode:\n          description: Tool mode for the agent (dom, hybrid, cua). If set, overrides cua.\n          example: cua\n          type: string\n          enum:\n            - dom\n            - hybrid\n            - cua\n        executionModel:\n          description: Model configuration object or model name string (e.g.,\n            'openai/gpt-5-nano') for tool execution (observe/act calls within\n            agent tools). If not specified, inherits from the main model\n            configuration.\n          anyOf:\n            - $ref: \"#/components/schemas/ModelConfigOutput\"\n            - type: string\n      additionalProperties: false\n    AgentUsageOutput:\n      type: object\n      properties:\n        input_tokens:\n          example: 1500\n          type: number\n        output_tokens:\n          example: 250\n          type: number\n        reasoning_tokens:\n          type: number\n        cached_input_tokens:\n          type: number\n        inference_time_ms:\n          example: 2500\n          type: number\n      required:\n        - input_tokens\n        - output_tokens\n        - inference_time_ms\n      additionalProperties: false\n    AgentResultDataOutput:\n      type: object\n      properties:\n        success:\n          description: Whether the agent completed successfully\n          example: true\n          type: boolean\n        message:\n          description: Summary of what the agent accomplished\n          example: Successfully logged in and navigated to dashboard\n          type: string\n        actions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/AgentAction\"\n        completed:\n          description: Whether the agent finished its task\n          example: true\n          type: boolean\n        metadata:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        usage:\n          $ref: \"#/components/schemas/AgentUsageOutput\"\n      required:\n        - success\n        - message\n        - actions\n        - completed\n      additionalProperties: false\n    AgentExecuteOptionsOutput:\n      type: object\n      properties:\n        instruction:\n          description: Natural language instruction for the agent\n          example: Log in with username 'demo' and password 'test123', then navigate to\n            settings\n          type: string\n        maxSteps:\n          description: Maximum number of steps the agent can take\n          example: 20\n          type: number\n        highlightCursor:\n          description: Whether to visually highlight the cursor during execution\n          example: true\n          type: boolean\n        useSearch:\n          description: Whether to enable the web search tool powered by Browserbase Search\n            API\n          example: true\n          type: boolean\n        toolTimeout:\n          description: Timeout in milliseconds for each agent tool call\n          example: 30000\n          type: number\n      required:\n        - instruction\n      additionalProperties: false\n    AgentExecuteResultOutput:\n      type: object\n      properties:\n        result:\n          $ref: \"#/components/schemas/AgentResultDataOutput\"\n        cacheEntry:\n          $ref: \"#/components/schemas/AgentCacheEntryOutput\"\n      required:\n        - result\n      additionalProperties: false\n    NavigateOptionsOutput:\n      type: object\n      properties:\n        referer:\n          description: Referer header to send with the request\n          type: string\n        timeout:\n          description: Timeout in ms for the navigation\n          example: 30000\n          type: number\n        waitUntil:\n          description: When to consider navigation complete\n          example: networkidle\n          type: string\n          enum:\n            - load\n            - domcontentloaded\n            - networkidle\n      additionalProperties: false\n    NavigateResultOutput:\n      type: object\n      properties:\n        result:\n          description: Navigation response (Playwright Response object or null)\n          anyOf:\n            - {}\n            - type: \"null\"\n          x-stainless-any: true\n        actionId:\n          description: Action ID for tracking\n          type: string\n      required:\n        - result\n      additionalProperties: false\n    TokenUsageOutput:\n      type: object\n      properties:\n        inputTokens:\n          type: number\n        outputTokens:\n          type: number\n        timeMs:\n          type: number\n        cost:\n          type: number\n      additionalProperties: false\n    ReplayActionOutput:\n      type: object\n      properties:\n        method:\n          type: string\n        parameters:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        result:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n        timestamp:\n          type: number\n        endTime:\n          type: number\n        tokenUsage:\n          $ref: \"#/components/schemas/TokenUsageOutput\"\n      required:\n        - method\n        - parameters\n        - result\n        - timestamp\n      additionalProperties: false\n    ReplayPageOutput:\n      type: object\n      properties:\n        url:\n          type: string\n        timestamp:\n          type: number\n        duration:\n          type: number\n        actions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/ReplayActionOutput\"\n      required:\n        - url\n        - timestamp\n        - duration\n        - actions\n      additionalProperties: false\n    ReplayResultOutput:\n      type: object\n      properties:\n        pages:\n          type: array\n          items:\n            $ref: \"#/components/schemas/ReplayPageOutput\"\n        clientLanguage:\n          type: string\n      required:\n        - pages\n      additionalProperties: false\n    StreamEventSystemDataOutput:\n      type: object\n      properties:\n        status:\n          $ref: \"#/components/schemas/StreamEventStatus\"\n        result:\n          description: Operation result (present when status is 'finished')\n          x-stainless-any: true\n        error:\n          description: Error message (present when status is 'error')\n          type: string\n      required:\n        - status\n      additionalProperties: false\n    StreamEventLogDataOutput:\n      type: object\n      properties:\n        status:\n          type: string\n          const: running\n        message:\n          description: Log message from the operation\n          type: string\n      required:\n        - status\n        - message\n      additionalProperties: false\n    SessionIdParams:\n      type: object\n      properties:\n        id:\n          description: Unique session identifier\n          example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n          type: string\n      required:\n        - id\n      additionalProperties: false\n    SessionHeaders:\n      type: object\n      properties:\n        x-stream-response:\n          description: Whether to stream the response via SSE\n          example: \"true\"\n          type: string\n          enum:\n            - \"true\"\n            - \"false\"\n      additionalProperties: false\n    ErrorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: false\n        error:\n          type: string\n        code:\n          type: string\n      required:\n        - success\n        - error\n      additionalProperties: false\n    SessionEndResult:\n      type: object\n      properties: {}\n      additionalProperties: false\n    StreamEvent:\n      description: \"Server-Sent Event emitted during streaming responses. Events are\n        sent as `data: <JSON>\\\\n\\\\n`. Key order: data (with status first), type,\n        id.\"\n      type: object\n      properties:\n        data:\n          anyOf:\n            - $ref: \"#/components/schemas/StreamEventSystemDataOutput\"\n            - $ref: \"#/components/schemas/StreamEventLogDataOutput\"\n        type:\n          $ref: \"#/components/schemas/StreamEventType\"\n        id:\n          description: Unique identifier for this event\n          example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n          type: string\n          format: uuid\n          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$\n      required:\n        - data\n        - type\n        - id\n      additionalProperties: false\npaths:\n  /v1/sessions/{id}/act:\n    post:\n      operationId: SessionAct\n      summary: Perform an action\n      description: Executes a browser action using natural language instructions or a\n        predefined Action object.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/ActRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/ActResponse\"\n  /v1/sessions/{id}/end:\n    post:\n      operationId: SessionEnd\n      summary: End a browser session\n      description: Terminates the browser session and releases all associated resources.\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/SessionEndResponse\"\n  /v1/sessions/{id}/extract:\n    post:\n      operationId: SessionExtract\n      summary: Extract data from the page\n      description: Extracts structured data from the current page using AI-powered analysis.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/ExtractRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/ExtractResponse\"\n  /v1/sessions/{id}/navigate:\n    post:\n      operationId: SessionNavigate\n      summary: Navigate to a URL\n      description: Navigates the browser to the specified URL.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/NavigateRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/NavigateResponse\"\n  /v1/sessions/{id}/observe:\n    post:\n      operationId: SessionObserve\n      summary: Observe available actions\n      description: Identifies and returns available actions on the current page that\n        match the given instruction.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/ObserveRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/ObserveResponse\"\n  /v1/sessions/{id}/replay:\n    get:\n      operationId: SessionReplay\n      summary: Replay session metrics\n      description: Retrieves replay metrics for a session.\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/ReplayResponse\"\n  /v1/sessions/start:\n    post:\n      operationId: SessionStart\n      summary: Start a new browser session\n      description: Creates a new browser session with the specified configuration.\n        Returns a session ID used for all subsequent operations.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/SessionStartRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/SessionStartResponse\"\n  /v1/sessions/{id}/agentExecute:\n    post:\n      operationId: SessionAgentExecute\n      summary: Execute an AI agent\n      description: Runs an autonomous AI agent that can perform complex multi-step\n        browser tasks.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/AgentExecuteRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Unique session identifier\n            example: c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123\n            type: string\n          in: path\n          name: id\n          required: true\n          description: Unique session identifier\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/AgentExecuteResponse\"\nservers:\n  - url: https://api.stagehand.browserbase.com\nsecurity:\n  - BrowserbaseApiKey: []\n    BrowserbaseProjectId: []\n    ModelApiKey: []\n"
  },
  {
    "path": "packages/server-v3/package.json",
    "content": "{\n  \"name\": \"@browserbasehq/stagehand-server-v3\",\n  \"version\": \"3.6.1\",\n  \"description\": \"Stagehand API server v3\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"pnpm --filter @browserbasehq/stagehand-server-v3 run --parallel \\\"/^(build:esm-tests|build:server:dist|gen:openapi|build:sea:esm)$/\\\"\",\n    \"dev\": \"tsx watch src/server.ts\",\n    \"build:esm-tests\": \"pnpm -w --dir ../.. exec tsc -p packages/server-v3/tsconfig.tests.json\",\n    \"build:server:dist\": \"pnpm -w --dir ../.. exec tsc -p packages/server-v3/tsconfig.json && pnpm -w --dir ../.. exec tsc-alias -p packages/server-v3/tsconfig.json\",\n    \"build:sea:esm\": \"tsx scripts/build-sea.ts --mode=esm\",\n    \"build:sea:cjs\": \"tsx scripts/build-sea.ts --mode=cjs\",\n    \"lint\": \"cd ../.. && prettier --check packages/server-v3 && cd packages/server-v3 && eslint . && pnpm run typecheck\",\n    \"typecheck\": \"pnpm -w --dir ../.. exec tsc -p packages/server-v3/tsconfig.json --noEmit\",\n    \"test\": \"pnpm -w --dir ../.. exec turbo run test:server --filter=@browserbasehq/stagehand-server-v3 --\",\n    \"test:server\": \"tsx scripts/test-server.ts\",\n    \"test:integration\": \"pnpm run test:server -- packages/server-v3/dist/tests/integration\",\n    \"test:integration:local\": \"STAGEHAND_SERVER_TARGET=local pnpm run test:server -- packages/server-v3/dist/tests/integration\",\n    \"test:integration:sea\": \"STAGEHAND_SERVER_TARGET=sea pnpm run test:server -- packages/server-v3/dist/tests/integration\",\n    \"gen:openapi\": \"tsx scripts/gen-openapi.ts\"\n  },\n  \"dependencies\": {\n    \"@browserbasehq/sdk\": \"^2.7.0\",\n    \"@browserbasehq/stagehand\": \"workspace:*\",\n    \"@fastify/cors\": \"^11.0.1\",\n    \"@fastify/swagger\": \"^9.6.1\",\n    \"@fastify/swagger-ui\": \"^5.2.3\",\n    \"@t3-oss/env-core\": \"^0.13.8\",\n    \"fastify\": \"^5.3.2\",\n    \"fastify-metrics\": \"^12.1.0\",\n    \"fastify-plugin\": \"^4.5.1\",\n    \"fastify-zod-openapi\": \"^5.5.0\",\n    \"http-status-codes\": \"^2.3.0\",\n    \"pino\": \"^9.7.0\",\n    \"pino-pretty\": \"^11.3.0\",\n    \"playwright\": \"1.52.0\",\n    \"uuid\": \"^11.0.5\",\n    \"zod\": \"^4.2.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"22.13.1\",\n    \"eslint\": \"10.0.2\",\n    \"eslint-plugin-security\": \"^3.0.1\",\n    \"openai\": \"4.87.1\",\n    \"postject\": \"1.0.0-alpha.6\",\n    \"prettier\": \"^3.2.5\",\n    \"source-map\": \"^0.7.4\",\n    \"tsc-alias\": \"^1.8.10\",\n    \"tsx\": \"*\",\n    \"vitest\": \"^4.0.8\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/browserbase/stagehand.git\",\n    \"directory\": \"packages/server-v3\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/browserbase/stagehand/issues\"\n  },\n  \"homepage\": \"https://stagehand.dev\"\n}\n"
  },
  {
    "path": "packages/server-v3/scripts/build-sea.ts",
    "content": "#!/usr/bin/env node\n/**\n * Build SEA binary from ESM (test) or CJS (release) bundles.\n *\n * Prereqs:\n * - CJS mode: runs core CJS build via Turbo if dist is missing.\n * - ESM mode: core dist/esm available (pnpm run build:esm).\n * - postject installed; tar available for non-Windows downloads.\n *\n * Args: --mode=esm|cjs --target-platform=<platform> --target-arch=<arch> --binary-name=<name>\n * Env: SEA_BUILD_MODE, SEA_TARGET_PLATFORM, SEA_TARGET_ARCH, SEA_BINARY_NAME,\n *      SEA_INCLUDE_SOURCEMAPS.\n * Example: pnpm run build:sea:cjs -- --target-platform=linux --target-arch=arm64\n */\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport https from \"node:https\";\nimport { pathToFileURL } from \"node:url\";\nimport esbuild from \"esbuild\";\nimport { getRepoRootDir } from \"./runtimePaths.js\";\n\nconst repoDir = getRepoRootDir();\nconst seaFuse = \"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2\";\n\nconst argValue = (name: string) => {\n  const prefix = `--${name}=`;\n  for (let i = 0; i < process.argv.length; i++) {\n    const arg = process.argv[i];\n    if (arg === `--${name}` && process.argv[i + 1]) return process.argv[i + 1];\n    if (arg.startsWith(prefix)) return arg.slice(prefix.length);\n  }\n  return undefined;\n};\n\nconst mode = (\n  argValue(\"mode\") ??\n  process.env.SEA_BUILD_MODE ??\n  \"esm\"\n).toLowerCase();\nconst parseBoolean = (\n  value: string | undefined,\n  fallback: boolean,\n): boolean => {\n  if (value === undefined) return fallback;\n\n  const normalized = value.toLowerCase();\n  if (\n    normalized === \"1\" ||\n    normalized === \"true\" ||\n    normalized === \"yes\" ||\n    normalized === \"on\"\n  ) {\n    return true;\n  }\n  if (\n    normalized === \"0\" ||\n    normalized === \"false\" ||\n    normalized === \"no\" ||\n    normalized === \"off\"\n  ) {\n    return false;\n  }\n\n  throw new Error(\n    `Invalid boolean value \"${value}\" for --include-sourcemaps / SEA_INCLUDE_SOURCEMAPS`,\n  );\n};\nconst targetPlatform =\n  argValue(\"target-platform\") ??\n  argValue(\"platform\") ??\n  process.env.SEA_TARGET_PLATFORM ??\n  process.platform;\nconst targetArch =\n  argValue(\"target-arch\") ??\n  argValue(\"arch\") ??\n  process.env.SEA_TARGET_ARCH ??\n  process.arch;\nconst binaryName =\n  argValue(\"binary-name\") ??\n  process.env.SEA_BINARY_NAME ??\n  `stagehand-server-v3-${targetPlatform}-${targetArch}${targetPlatform === \"win32\" ? \".exe\" : \"\"}`;\nconst includeSourcemaps = parseBoolean(\n  argValue(\"include-sourcemaps\") ?? process.env.SEA_INCLUDE_SOURCEMAPS,\n  false,\n);\n\nconst run = (cmd: string, args: string[], opts: { cwd?: string } = {}) => {\n  const result = spawnSync(cmd, args, { stdio: \"inherit\", ...opts });\n  if (result.error) {\n    throw new Error(\n      `Command failed to start: ${cmd} ${args.join(\" \")}\\n${String(result.error)}`,\n    );\n  }\n  if (result.status !== 0) {\n    throw new Error(`Command failed: ${cmd} ${args.join(\" \")}`);\n  }\n};\n\nconst runNodeScript = (\n  scriptPath: string,\n  args: string[],\n  opts: { cwd?: string } = {},\n) => run(process.execPath, [scriptPath, ...args], opts);\n\nconst resolveFirstExisting = (paths: string[]): string => {\n  for (const candidate of paths) {\n    if (fs.existsSync(candidate)) return candidate;\n  }\n  throw new Error(`Missing tool script. Tried: ${paths.join(\", \")}`);\n};\n\nconst runOptional = (\n  cmd: string,\n  args: string[],\n  opts: { cwd?: string } = {},\n) => {\n  spawnSync(cmd, args, { stdio: \"ignore\", ...opts });\n};\n\nconst hasSeaFuse = (binaryPath: string): boolean => {\n  try {\n    return fs.readFileSync(binaryPath).includes(Buffer.from(seaFuse));\n  } catch {\n    return false;\n  }\n};\n\nconst download = (url: string, dest: string): Promise<void> =>\n  new Promise((resolve, reject) => {\n    https\n      .get(url, (res) => {\n        if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {\n          const location = res.headers.location;\n          if (!location) {\n            reject(new Error(`Redirect without location: ${url}`));\n            return;\n          }\n          res.resume();\n          download(location, dest).then(resolve, reject);\n          return;\n        }\n        if (res.statusCode !== 200) {\n          reject(new Error(`Download failed (${res.statusCode}) ${url}`));\n          res.resume();\n          return;\n        }\n\n        const file = fs.createWriteStream(dest);\n        const fail = (error: Error) => {\n          file.destroy();\n          reject(error);\n        };\n\n        res.on(\"error\", fail);\n        file.on(\"error\", fail);\n        file.on(\"finish\", () => {\n          file.close((closeError) => {\n            if (closeError) {\n              reject(closeError);\n              return;\n            }\n            resolve();\n          });\n        });\n        res.pipe(file);\n      })\n      .on(\"error\", reject);\n  });\n\nconst resolveNodeBinary = async (): Promise<string> => {\n  if (targetPlatform !== process.platform) {\n    throw new Error(\n      `Cross-platform builds are not supported. Host=${process.platform}, target=${targetPlatform}`,\n    );\n  }\n  if (targetArch === process.arch && hasSeaFuse(process.execPath)) {\n    return process.execPath;\n  }\n  if (targetArch === process.arch) {\n    console.warn(\n      `Current Node binary at ${process.execPath} does not include ${seaFuse}; falling back to the official ${process.version} distribution for SEA injection.`,\n    );\n  }\n\n  const version = process.version;\n  const distPlatform = targetPlatform === \"win32\" ? \"win\" : targetPlatform;\n  const archiveBase = `node-${version}-${distPlatform}-${targetArch}`;\n  const archiveExt = distPlatform === \"win\" ? \"zip\" : \"tar.xz\";\n  const tmpRoot = `${os.tmpdir()}/stagehand-server-v3-sea/${archiveBase}`;\n  const archivePath = `${tmpRoot}/${archiveBase}.${archiveExt}`;\n  const extractRoot = `${tmpRoot}/${archiveBase}`;\n  const binaryPath =\n    distPlatform === \"win\"\n      ? `${extractRoot}/node.exe`\n      : `${extractRoot}/bin/node`;\n\n  if (fs.existsSync(binaryPath)) {\n    if (!hasSeaFuse(binaryPath)) {\n      throw new Error(\n        `Node binary at ${binaryPath} does not include ${seaFuse}; unable to build SEA binary. Delete ${tmpRoot} and retry.`,\n      );\n    }\n    return binaryPath;\n  }\n\n  fs.mkdirSync(tmpRoot, { recursive: true });\n  if (!fs.existsSync(archivePath)) {\n    const url = `https://nodejs.org/dist/${version}/${archiveBase}.${archiveExt}`;\n    await download(url, archivePath);\n  }\n\n  if (archiveExt === \"zip\") {\n    if (process.platform !== \"win32\") {\n      throw new Error(\"Windows binaries must be built on Windows runners.\");\n    }\n    run(\"powershell\", [\n      \"-Command\",\n      `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpRoot}' -Force`,\n    ]);\n  } else {\n    run(\"tar\", [\"-xf\", archivePath, \"-C\", tmpRoot]);\n  }\n\n  if (!fs.existsSync(binaryPath)) {\n    throw new Error(`Missing Node binary at ${binaryPath}`);\n  }\n  if (!hasSeaFuse(binaryPath)) {\n    throw new Error(\n      `Node binary at ${binaryPath} does not include ${seaFuse}; unable to build SEA binary. Delete ${tmpRoot} and retry.`,\n    );\n  }\n  return binaryPath;\n};\n\nconst writeSeaConfig = (\n  mainPath: string,\n  outputPath: string,\n  execArgvExtension?: string,\n) => {\n  const configPath = `${repoDir}/packages/server-v3/dist/sea/sea-config-${mode}.json`;\n  const config = {\n    main: path\n      .relative(`${repoDir}/packages/server-v3`, mainPath)\n      .replaceAll(\"\\\\\", \"/\"),\n    output: path\n      .relative(`${repoDir}/packages/server-v3`, outputPath)\n      .replaceAll(\"\\\\\", \"/\"),\n    ...(execArgvExtension ? { execArgvExtension } : {}),\n  };\n  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));\n  return configPath;\n};\n\nconst buildCjsBundle = () => {\n  const turboBin = resolveFirstExisting([\n    `${repoDir}/node_modules/turbo/bin/turbo`,\n  ]);\n  runNodeScript(\n    turboBin,\n    [\"run\", \"build:cjs\", \"--filter\", \"@browserbasehq/stagehand\"],\n    {\n      cwd: repoDir,\n    },\n  );\n  fs.mkdirSync(`${repoDir}/packages/server-v3/dist/sea`, { recursive: true });\n  const bundlePath = `${repoDir}/packages/server-v3/dist/sea/bundle.cjs`;\n  esbuild.buildSync({\n    entryPoints: [\"packages/server-v3/src/sea-entry.ts\"],\n    bundle: true,\n    platform: \"node\",\n    format: \"cjs\",\n    outfile: bundlePath,\n    logLevel: \"warning\",\n    absWorkingDir: repoDir,\n  });\n  return bundlePath;\n};\n\nconst buildEsmBundle = () => {\n  if (!fs.existsSync(`${repoDir}/packages/core/dist/esm/index.js`)) {\n    throw new Error(\n      `Missing ${repoDir}/packages/core/dist/esm/index.js. Run pnpm run build:esm first.`,\n    );\n  }\n\n  fs.mkdirSync(`${repoDir}/packages/server-v3/dist/sea`, { recursive: true });\n  const appBundlePath = `${repoDir}/packages/server-v3/dist/app.mjs`;\n  esbuild.buildSync({\n    entryPoints: [\"packages/server-v3/src/sea-entry.ts\"],\n    bundle: true,\n    platform: \"node\",\n    format: \"esm\",\n    treeShaking: false,\n    outfile: appBundlePath,\n    alias: {\n      \"@browserbasehq/stagehand\": `${repoDir}/packages/core/dist/esm/index.js`,\n    },\n    sourcemap: includeSourcemaps ? \"inline\" : false,\n    sourcesContent: includeSourcemaps,\n    ...(includeSourcemaps ? { sourceRoot: repoDir } : {}),\n    banner: {\n      js: 'import { createRequire as __createRequire } from \"node:module\"; const require = __createRequire(import.meta.url);',\n    },\n    logLevel: \"warning\",\n    absWorkingDir: repoDir,\n  });\n\n  const appSource = fs.readFileSync(appBundlePath, \"utf8\");\n  let finalAppSource = appSource;\n\n  if (includeSourcemaps) {\n    const mapMatch = appSource.match(\n      /sourceMappingURL=data:application\\/json;base64,([A-Za-z0-9+/=]+)\\s*$/,\n    );\n    if (!mapMatch) {\n      throw new Error(\"Missing inline sourcemap in dist/app.mjs\");\n    }\n    const mapJson = Buffer.from(mapMatch[1], \"base64\").toString(\"utf8\");\n    const map = JSON.parse(mapJson) as {\n      sourceRoot?: string;\n      sources: string[];\n      sourcesContent?: string[];\n    };\n    const toPosix = (value: string) => value.replaceAll(\"\\\\\", \"/\");\n    const fileUrlToPathSafe = (value: string) => {\n      const parsed = new URL(value);\n      let pathname = decodeURIComponent(parsed.pathname);\n      if (/^\\/[A-Za-z]:/.test(pathname)) {\n        pathname = pathname.slice(1);\n      }\n      return pathname;\n    };\n    const toRepoRelative = (source: string) => {\n      let sourcePath = source;\n      if (source.startsWith(\"file://\")) {\n        sourcePath = fileUrlToPathSafe(source);\n      }\n\n      if (path.isAbsolute(sourcePath)) {\n        const normalizedSourcePath = toPosix(sourcePath);\n        if (normalizedSourcePath.startsWith(`${repoDir}/`)) {\n          return toPosix(path.relative(repoDir, normalizedSourcePath));\n        }\n        return normalizedSourcePath;\n      }\n\n      if (sourcePath.startsWith(\"../src/\")) {\n        const rel = sourcePath.slice(\"../src/\".length);\n        return `packages/server-v3/src/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../../core/\")) {\n        const rel = sourcePath.slice(\"../../core/\".length);\n        return `packages/core/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../../../node_modules/\")) {\n        const rel = sourcePath.slice(\"../../../node_modules/\".length);\n        return `node_modules/${rel}`;\n      }\n      if (sourcePath.startsWith(\"src/\")) {\n        const rel = sourcePath.slice(\"src/\".length);\n        return `packages/server-v3/src/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../node_modules/\")) {\n        const rel = sourcePath.slice(\"../node_modules/\".length);\n        return `node_modules/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../core/\")) {\n        const rel = sourcePath.slice(\"../core/\".length);\n        return `packages/core/${rel}`;\n      }\n      if (sourcePath.startsWith(\"core/\")) {\n        return `packages/core/${sourcePath.slice(\"core/\".length)}`;\n      }\n      if (\n        sourcePath.startsWith(\"packages/\") ||\n        sourcePath.startsWith(\"node_modules/\")\n      ) {\n        return toPosix(sourcePath);\n      }\n\n      const resolved = toPosix(\n        path.resolve(`${repoDir}/packages/server-v3`, sourcePath),\n      );\n      if (resolved.startsWith(`${repoDir}/`)) {\n        return toPosix(path.relative(repoDir, resolved));\n      }\n\n      return toPosix(sourcePath);\n    };\n\n    map.sourceRoot = pathToFileURL(`${repoDir}/`).href;\n    map.sources = map.sources.map(toRepoRelative);\n    const updatedMap = Buffer.from(JSON.stringify(map)).toString(\"base64\");\n    finalAppSource = appSource.replace(mapMatch[1], updatedMap);\n    fs.writeFileSync(appBundlePath, finalAppSource);\n  }\n\n  const appBytes = Buffer.from(finalAppSource);\n  const bundleHash = createHash(\"sha256\")\n    .update(appBytes)\n    .digest(\"hex\")\n    .slice(0, 12);\n  const bootstrapPath = `${repoDir}/packages/server-v3/dist/sea/sea-bootstrap.cjs`;\n  const bootstrap = `/* eslint-disable */\nconst fs = require(\"node:fs\");\nconst os = require(\"node:os\");\nconst { pathToFileURL } = require(\"node:url\");\n\nconst bundleBase64 = ${JSON.stringify(appBytes.toString(\"base64\"))};\nconst bundleLength = ${appBytes.length};\nconst bundleHash = ${JSON.stringify(bundleHash)};\n\nconst cacheRoot =\n  process.env.STAGEHAND_SEA_CACHE_DIR ||\n  \\`\\${os.tmpdir()}/stagehand-server-v3-sea\\`;\nconst cacheDir = \\`\\${cacheRoot}/\\${bundleHash}\\`;\nconst appPath = \\`\\${cacheDir}/app.mjs\\`;\n\nfs.mkdirSync(cacheDir, { recursive: true });\nlet needsWrite = true;\ntry {\n  const stat = fs.statSync(appPath);\n  needsWrite = stat.size !== bundleLength;\n} catch {}\n\nif (needsWrite) {\n  const tmpPath =\n    \\`\\${cacheDir}/app.mjs.tmp-\\${process.pid}-\\${Date.now().toString(16)}\\`;\n  fs.writeFileSync(tmpPath, Buffer.from(bundleBase64, \"base64\"));\n  try {\n    fs.renameSync(tmpPath, appPath);\n  } catch (err) {\n    if (!fs.existsSync(appPath)) throw err;\n  }\n  try {\n    fs.chmodSync(appPath, 0o500);\n  } catch {}\n}\n\n(async () => {\n  await import(pathToFileURL(appPath).href);\n})().catch((err) => {\n  console.error(err);\n  process.exitCode = 1;\n});\n`;\n  fs.writeFileSync(bootstrapPath, bootstrap);\n  return bootstrapPath;\n};\n\nconst main = async () => {\n  fs.mkdirSync(`${repoDir}/packages/server-v3/dist/sea`, { recursive: true });\n\n  let mainPath: string;\n  let execArgvExtension: string | undefined;\n\n  if (mode === \"cjs\") {\n    mainPath = buildCjsBundle();\n  } else if (mode === \"esm\") {\n    mainPath = buildEsmBundle();\n    execArgvExtension = \"cli\";\n  } else {\n    throw new Error(`Unknown SEA build mode: ${mode}`);\n  }\n\n  const seaConfigPath = writeSeaConfig(\n    mainPath,\n    `${repoDir}/packages/server-v3/dist/sea/sea-prep.blob`,\n    execArgvExtension,\n  );\n\n  run(\"node\", [\"--experimental-sea-config\", seaConfigPath], {\n    cwd: `${repoDir}/packages/server-v3`,\n  });\n  if (!fs.existsSync(`${repoDir}/packages/server-v3/dist/sea/sea-prep.blob`)) {\n    throw new Error(\n      `Missing ${repoDir}/packages/server-v3/dist/sea/sea-prep.blob; SEA blob generation failed.`,\n    );\n  }\n\n  const nodeBinary = await resolveNodeBinary();\n  const outPath = `${repoDir}/packages/server-v3/dist/sea/${binaryName}`;\n  fs.copyFileSync(nodeBinary, outPath);\n  if (targetPlatform !== \"win32\") {\n    fs.chmodSync(outPath, 0o755);\n  }\n\n  if (targetPlatform === \"darwin\") {\n    runOptional(\"codesign\", [\"--remove-signature\", outPath]);\n  }\n\n  const postjectCliPath = resolveFirstExisting([\n    `${repoDir}/packages/server-v3/node_modules/postject/dist/cli.js`,\n    `${repoDir}/node_modules/postject/dist/cli.js`,\n  ]);\n  const postjectArgs = [\n    outPath,\n    \"NODE_SEA_BLOB\",\n    `${repoDir}/packages/server-v3/dist/sea/sea-prep.blob`,\n    \"--sentinel-fuse\",\n    seaFuse,\n  ];\n  if (targetPlatform === \"darwin\") {\n    postjectArgs.push(\"--macho-segment-name\", \"NODE_SEA\");\n  }\n  runNodeScript(postjectCliPath, postjectArgs, {\n    cwd: `${repoDir}/packages/server-v3`,\n  });\n\n  if (targetPlatform === \"darwin\") {\n    runOptional(\"codesign\", [\"--sign\", \"-\", outPath]);\n  }\n};\n\nmain().catch((err) => {\n  console.error(err instanceof Error ? err.message : String(err));\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/server-v3/scripts/gen-openapi.ts",
    "content": "import { writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nimport fastify from \"fastify\";\nimport fastifySwagger from \"@fastify/swagger\";\nimport {\n  fastifyZodOpenApiPlugin,\n  fastifyZodOpenApiTransformers,\n  serializerCompiler,\n  validatorCompiler,\n  type FastifyZodOpenApiTypeProvider,\n} from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\n// Routes\nimport actRoute from \"../src/routes/v1/sessions/_id/act.js\";\nimport agentExecuteRoute from \"../src/routes/v1/sessions/_id/agentExecute.js\";\nimport endRoute from \"../src/routes/v1/sessions/_id/end.js\";\nimport extractRoute from \"../src/routes/v1/sessions/_id/extract.js\";\nimport navigateRoute from \"../src/routes/v1/sessions/_id/navigate.js\";\nimport observeRoute from \"../src/routes/v1/sessions/_id/observe.js\";\nimport replayRoute from \"../src/routes/v1/sessions/_id/replay.js\";\nimport startRoute from \"../src/routes/v1/sessions/start.js\";\nimport healthcheckRoute from \"../src/routes/healthcheck.js\";\nimport readinessRoute from \"../src/routes/readiness.js\";\n\nconst OUTPUT_PATH = path.resolve(getCurrentDirPath(), \"../openapi.v3.yaml\");\n\nasync function main() {\n  const app = fastify({\n    logger: false,\n  }).withTypeProvider<FastifyZodOpenApiTypeProvider>();\n\n  app.setValidatorCompiler(validatorCompiler);\n  app.setSerializerCompiler(serializerCompiler);\n\n  // Register all API schemas as components so fastify-zod-openapi can create $ref links\n  const components = {\n    schemas: {\n      // Region support\n      BrowserbaseRegion: Api.BrowserbaseRegionSchema,\n      // Shared components\n      LocalBrowserLaunchOptions: Api.LocalBrowserLaunchOptionsSchema,\n      ModelConfigObject: Api.ModelConfigObjectSchema,\n      ModelConfig: Api.ModelConfigSchema,\n      Action: Api.ActionSchema,\n      SessionIdParams: Api.SessionIdParamsSchema,\n      BrowserConfig: Api.BrowserConfigSchema,\n      SessionHeaders: Api.SessionHeadersSchema,\n      ErrorResponse: Api.ErrorResponseSchema,\n      // Browserbase schemas\n      BrowserbaseViewport: Api.BrowserbaseViewportSchema,\n      BrowserbaseFingerprintScreen: Api.BrowserbaseFingerprintScreenSchema,\n      BrowserbaseFingerprint: Api.BrowserbaseFingerprintSchema,\n      BrowserbaseContext: Api.BrowserbaseContextSchema,\n      BrowserbaseBrowserSettings: Api.BrowserbaseBrowserSettingsSchema,\n      BrowserbaseProxyGeolocation: Api.BrowserbaseProxyGeolocationSchema,\n      BrowserbaseProxyConfig: Api.BrowserbaseProxyConfigSchema,\n      ExternalProxyConfig: Api.ExternalProxyConfigSchema,\n      ProxyConfig: Api.ProxyConfigSchema,\n      BrowserbaseSessionCreateParams: Api.BrowserbaseSessionCreateParamsSchema,\n      // Session Start\n      SessionStartRequest: Api.SessionStartRequestSchema,\n      SessionStartResult: Api.SessionStartResultSchema,\n      SessionStartResponse: Api.SessionStartResponseSchema,\n      // Session End\n      SessionEndResult: Api.SessionEndResultSchema,\n      SessionEndResponse: Api.SessionEndResponseSchema,\n      // Act\n      ActOptions: Api.ActOptionsSchema,\n      ActRequest: Api.ActRequestSchema,\n      ActResultData: Api.ActResultDataSchema,\n      ActResult: Api.ActResultSchema,\n      ActResponse: Api.ActResponseSchema,\n      // Extract\n      ExtractOptions: Api.ExtractOptionsSchema,\n      ExtractRequest: Api.ExtractRequestSchema,\n      ExtractResult: Api.ExtractResultSchema,\n      ExtractResponse: Api.ExtractResponseSchema,\n      // Observe\n      ObserveOptions: Api.ObserveOptionsSchema,\n      ObserveRequest: Api.ObserveRequestSchema,\n      ObserveResult: Api.ObserveResultSchema,\n      ObserveResponse: Api.ObserveResponseSchema,\n      // Agent Execute\n      AgentConfig: Api.AgentConfigSchema,\n      AgentAction: Api.AgentActionSchema,\n      AgentUsage: Api.AgentUsageSchema,\n      AgentResultData: Api.AgentResultDataSchema,\n      AgentExecuteOptions: Api.AgentExecuteOptionsSchema,\n      AgentExecuteRequest: Api.AgentExecuteRequestSchema,\n      AgentExecuteResult: Api.AgentExecuteResultSchema,\n      AgentExecuteResponse: Api.AgentExecuteResponseSchema,\n      // Navigate\n      NavigateOptions: Api.NavigateOptionsSchema,\n      NavigateRequest: Api.NavigateRequestSchema,\n      NavigateResult: Api.NavigateResultSchema,\n      NavigateResponse: Api.NavigateResponseSchema,\n      // Replay\n      TokenUsage: Api.TokenUsageSchema,\n      ReplayAction: Api.ReplayActionSchema,\n      ReplayPage: Api.ReplayPageSchema,\n      ReplayResult: Api.ReplayResultSchema,\n      ReplayResponse: Api.ReplayResponseSchema,\n      // SSE Stream Events\n      StreamEventStatus: Api.StreamEventStatusSchema,\n      StreamEventType: Api.StreamEventTypeSchema,\n      StreamEventSystemData: Api.StreamEventSystemDataSchema,\n      StreamEventLogData: Api.StreamEventLogDataSchema,\n      StreamEvent: Api.StreamEventSchema,\n    },\n  };\n\n  await app.register(fastifyZodOpenApiPlugin, { components });\n\n  await app.register(fastifySwagger, {\n    openapi: {\n      info: {\n        title: \"Stagehand API\",\n        version: \"3.1.0\",\n        description: `Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\nexecute browser automation tasks remotely on the Browserbase cloud.\nAll endpoints except /sessions/start require an active session ID.\nResponses are streamed using Server-Sent Events (SSE) when the\n\\`x-stream-response: true\\` header is provided.\n\nThis SDK is currently ALPHA software and is not production ready!\nPlease try it and give us your feedback, stay tuned for upcoming release announcements!`,\n        contact: {\n          name: \"Browserbase\",\n          url: \"https://browserbase.com\",\n        },\n      },\n      openapi: \"3.1.0\",\n      servers: [\n        {\n          url: \"https://api.stagehand.browserbase.com\",\n        },\n      ],\n      components: {\n        securitySchemes: Api.openApiSecuritySchemes,\n        links: Api.openApiLinks,\n      },\n      security: [\n        { BrowserbaseApiKey: [], BrowserbaseProjectId: [], ModelApiKey: [] },\n      ],\n    },\n    ...fastifyZodOpenApiTransformers,\n  });\n\n  await app.register(\n    (instance, _opts, done) => {\n      instance.route(actRoute);\n      instance.route(endRoute);\n      instance.route(extractRoute);\n      instance.route(navigateRoute);\n      instance.route(observeRoute);\n      instance.route(replayRoute);\n      instance.route(startRoute);\n      instance.route(agentExecuteRoute);\n      done();\n    },\n    { prefix: \"/v1\" },\n  );\n\n  app.route(healthcheckRoute);\n  app.route(readinessRoute);\n\n  await app.ready();\n\n  const yaml = app.swagger({ yaml: true });\n  // Mintlify expects OpenAPI version fields to be strings, so quote them here.\n  const fixedYaml = yaml\n    .replace(/^openapi:\\s*(?!['\"])([^#\\s]+)\\s*$/m, 'openapi: \"$1\"')\n    .replace(/^ {2}version:\\s*(?!['\"])([^#\\s]+)\\s*$/m, '  version: \"$1\"');\n\n  await writeFile(OUTPUT_PATH, fixedYaml, \"utf8\");\n\n  await app.close();\n  console.log(`OpenAPI spec written to ${OUTPUT_PATH}`);\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/server-v3/scripts/runtimePaths.ts",
    "content": "/**\n * Keep this file in sync with:\n * - /packages/core/lib/v3/runtimePaths.ts\n * - /packages/server-v3/scripts/runtimePaths.ts\n * - /packages/evals/runtimePaths.ts\n * - /packages/docs/scripts/runtimePaths.js\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createRequire } from \"node:module\";\n\nconst PACKAGE_SEGMENT = \"/packages/server-v3/\";\nconst EVAL_FRAMES = new Set([\"[eval]\", \"[eval]-wrapper\"]);\nconst INTERNAL_FRAME_NAMES = new Set([\n  \"readCallsites\",\n  \"readCallsitePath\",\n  \"resolveCallerFilePath\",\n  \"getCurrentFilePath\",\n  \"getCurrentDirPath\",\n  \"getRepoRootDir\",\n  \"getPackageRootDir\",\n  \"createRequireFromCaller\",\n  \"isMainModule\",\n]);\n\nconst normalizePath = (value: string): string => {\n  const input = value.startsWith(\"file://\") ? fileURLToPath(value) : value;\n  return path.resolve(input).replaceAll(\"\\\\\", \"/\");\n};\n\nconst readCallsites = (): NodeJS.CallSite[] => {\n  const previousPrepare = Error.prepareStackTrace;\n  try {\n    Error.prepareStackTrace = (_, stack) => stack;\n    return (\n      (new Error().stack as unknown as NodeJS.CallSite[] | undefined) ?? []\n    );\n  } finally {\n    Error.prepareStackTrace = previousPrepare;\n  }\n};\n\ntype CallSiteWithScriptName = NodeJS.CallSite & {\n  getScriptNameOrSourceURL?: () => string | null;\n};\n\nconst readCallsitePath = (callsite: NodeJS.CallSite): string | null => {\n  const callsiteWithScript = callsite as CallSiteWithScriptName;\n  const rawPath =\n    callsite.getFileName() ?? callsiteWithScript.getScriptNameOrSourceURL?.();\n  if (!rawPath) return null;\n  if (rawPath.startsWith(\"node:\")) return null;\n  if (EVAL_FRAMES.has(rawPath)) return null;\n  return normalizePath(rawPath);\n};\n\nconst isInternalCallsite = (callsite: NodeJS.CallSite): boolean => {\n  const functionName = callsite.getFunctionName();\n  if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) return true;\n\n  const methodName = callsite.getMethodName();\n  if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) return true;\n\n  const callsiteString = callsite.toString();\n  for (const frameName of INTERNAL_FRAME_NAMES) {\n    if (callsiteString.includes(`${frameName} (`)) return true;\n    if (callsiteString.includes(`.${frameName} (`)) return true;\n  }\n  return false;\n};\n\nconst resolveCallerFilePath = (): string => {\n  const packageCandidates: string[] = [];\n  const fallbackCandidates: string[] = [];\n\n  for (const callsite of readCallsites()) {\n    const filePath = readCallsitePath(callsite);\n    if (!filePath) continue;\n    if (isInternalCallsite(callsite)) continue;\n    if (filePath.includes(PACKAGE_SEGMENT)) {\n      packageCandidates.push(filePath);\n      continue;\n    }\n    fallbackCandidates.push(filePath);\n  }\n\n  const packageCandidate = packageCandidates[0];\n  if (packageCandidate) return packageCandidate;\n\n  const fallbackCandidate = fallbackCandidates[0];\n  if (fallbackCandidate) return fallbackCandidate;\n\n  throw new Error(\"Unable to resolve caller file path.\");\n};\n\nexport const getCurrentFilePath = (): string => resolveCallerFilePath();\n\nexport const getCurrentDirPath = (): string =>\n  path.dirname(getCurrentFilePath());\n\nexport const getRepoRootDir = (): string => {\n  const currentFilePath = getCurrentFilePath();\n  const index = currentFilePath.lastIndexOf(PACKAGE_SEGMENT);\n  if (index === -1) {\n    throw new Error(\n      `Unable to determine repo root from ${currentFilePath} (missing ${PACKAGE_SEGMENT}).`,\n    );\n  }\n  return currentFilePath.slice(0, index);\n};\n\nexport const getPackageRootDir = (): string =>\n  `${getRepoRootDir()}${PACKAGE_SEGMENT.slice(0, -1)}`;\n\nexport const createRequireFromCaller = () =>\n  createRequire(getCurrentFilePath());\n\nexport const isMainModule = (): boolean => {\n  const entryScript = process.argv.at(1);\n  if (!entryScript) return false;\n  return normalizePath(entryScript) === getCurrentFilePath();\n};\n"
  },
  {
    "path": "packages/server-v3/scripts/test-server.ts",
    "content": "/**\n * Server unit + integration tests on dist/esm + SEA/local server targets.\n *\n * Prereqs:\n * - pnpm run build (packages/server-v3/dist/tests + packages/server-v3/dist/server.js).\n * - SEA integration still requires build:sea when STAGEHAND_SERVER_TARGET=sea.\n *\n * Args: [test paths...] -- [node --test args...] | --list (prints JSON matrix)\n * Env: STAGEHAND_SERVER_TARGET=sea|local|remote, STAGEHAND_BASE_URL, SEA_BINARY_NAME,\n *      NODE_TEST_CONSOLE_REPORTER, NODE_TEST_REPORTER, NODE_TEST_REPORTER_DESTINATION,\n *      NODE_V8_COVERAGE; writes CTRF to ctrf/node-test-*.xml by default.\n * Example: STAGEHAND_SERVER_TARGET=sea pnpm run test:server -- packages/server-v3/dist/tests/integration/v3/start.test.js\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawn, spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"./runtimePaths.js\";\n\nconst ensureParentDir = (filePath: string) => {\n  fs.mkdirSync(path.dirname(filePath), { recursive: true });\n};\n\nconst splitArgs = (args: string[]) => {\n  const tokens = [...args];\n  while (tokens[0] === \"--\") {\n    tokens.shift();\n  }\n\n  const leadingExtra: string[] = [];\n  while (tokens.length > 0 && tokens[0].startsWith(\"-\")) {\n    const arg = tokens.shift();\n    if (!arg) break;\n    if (arg === \"--\") break;\n    leadingExtra.push(arg);\n    if (\n      !arg.includes(\"=\") &&\n      tokens[0] &&\n      tokens[0] !== \"--\" &&\n      !tokens[0].startsWith(\"-\")\n    ) {\n      leadingExtra.push(tokens.shift() as string);\n    }\n  }\n\n  while (tokens[0] === \"--\") {\n    tokens.shift();\n  }\n\n  const separatorIndex = tokens.indexOf(\"--\");\n  return {\n    paths: separatorIndex === -1 ? tokens : tokens.slice(0, separatorIndex),\n    extra: [\n      ...leadingExtra,\n      ...(separatorIndex === -1 ? [] : tokens.slice(separatorIndex + 1)),\n    ],\n  };\n};\n\nconst toSafeName = (name: string) => name.replace(/[\\\\/]/g, \"-\");\n\nconst collectFiles = (dir: string, suffix: string) => {\n  const results: string[] = [];\n  const walk = (current: string) => {\n    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n      const full = `${current}/${entry.name}`;\n      if (entry.isDirectory()) {\n        walk(full);\n      } else if (entry.isFile() && entry.name.endsWith(suffix)) {\n        results.push(full);\n      }\n    }\n  };\n  if (fs.existsSync(dir)) walk(dir);\n  return results.sort();\n};\n\nconst repoRoot = getRepoRootDir();\n\nconst writeCtrfFromJunit = (junitPath: string, tool: string) => {\n  if (!fs.existsSync(junitPath)) return;\n  const stat = fs.statSync(junitPath);\n  if (stat.size === 0) return;\n  const ctrfPath = junitPath.match(/\\.xml$/i)\n    ? junitPath.replace(/\\.xml$/i, \".json\")\n    : `${junitPath}.json`;\n  const result = spawnSync(\n    \"pnpm\",\n    [\"exec\", \"junit-to-ctrf\", junitPath, \"-o\", ctrfPath, \"-t\", tool],\n    { stdio: \"inherit\", cwd: repoRoot },\n  );\n  if (result.status !== 0) {\n    console.warn(`CTRF conversion failed for ${junitPath}.`);\n  }\n};\n\nconst sourceTestsDir = `${repoRoot}/packages/server-v3/test`;\nconst sourceUnitDir = `${sourceTestsDir}/unit`;\nconst sourceIntegrationDir = `${sourceTestsDir}/integration`;\nconst unitDir = `${repoRoot}/packages/server-v3/dist/tests/unit`;\nconst integrationDir = `${repoRoot}/packages/server-v3/dist/tests/integration`;\nconst allTestsDir = `${repoRoot}/packages/server-v3/dist/tests`;\n\nconst resolveRepoRelative = (value: string) =>\n  path.isAbsolute(value) ? value : path.resolve(repoRoot, value);\n\nconst stripNodeReporterArgs = (argsList: string[]) => {\n  const filtered: string[] = [];\n  let removed = false;\n  for (let i = 0; i < argsList.length; i++) {\n    const arg = argsList[i];\n    if (\n      arg === \"--test-reporter\" ||\n      arg.startsWith(\"--test-reporter=\") ||\n      arg === \"--test-reporter-destination\" ||\n      arg.startsWith(\"--test-reporter-destination=\")\n    ) {\n      removed = true;\n      if (\n        (arg === \"--test-reporter\" || arg === \"--test-reporter-destination\") &&\n        argsList[i + 1]\n      ) {\n        i += 1;\n      }\n      continue;\n    }\n    filtered.push(arg);\n  }\n  return { filtered, removed };\n};\n\nconst toTestName = (testPath: string, root: string) => {\n  const abs = resolveRepoRelative(testPath);\n  const rel = path.relative(root, abs).replaceAll(\"\\\\\", \"/\");\n  if (!rel.startsWith(\"..\")) {\n    return rel.replace(/\\.test\\.js$/i, \"\");\n  }\n  return path.basename(abs).replace(/\\.test\\.js$/i, \"\");\n};\n\nconst rawArgs = process.argv.slice(2);\nconst listRequested = rawArgs.includes(\"--list\");\n\nif (listRequested) {\n  const unitTests = collectFiles(sourceUnitDir, \".test.ts\").map((file) => {\n    const relSource = path.relative(sourceTestsDir, file).replaceAll(\"\\\\\", \"/\");\n    const distPath = `${repoRoot}/packages/server-v3/dist/tests/${relSource.replace(/\\.test\\.ts$/, \".test.js\")}`;\n    const name = path.basename(file, \".test.ts\");\n    return {\n      path: path.relative(repoRoot, distPath).replaceAll(\"\\\\\", \"/\"),\n      name,\n      safe_name: toSafeName(name),\n    };\n  });\n  const integrationTests = collectFiles(sourceIntegrationDir, \".test.ts\").map(\n    (file) => {\n      const relSource = path\n        .relative(sourceTestsDir, file)\n        .replaceAll(\"\\\\\", \"/\");\n      const distPath = `${repoRoot}/packages/server-v3/dist/tests/${relSource.replace(/\\.test\\.ts$/, \".test.js\")}`;\n      const rel = path\n        .relative(sourceIntegrationDir, file)\n        .replaceAll(\"\\\\\", \"/\")\n        .replace(/\\.test\\.ts$/, \"\");\n      return {\n        path: path.relative(repoRoot, distPath).replaceAll(\"\\\\\", \"/\"),\n        name: rel,\n        safe_name: toSafeName(rel),\n      };\n    },\n  );\n  console.log(JSON.stringify([...unitTests, ...integrationTests]));\n  process.exit(0);\n}\n\nconst { paths, extra } = splitArgs(rawArgs);\nconst { filtered: extraArgs, removed: removedReporterOverride } =\n  stripNodeReporterArgs(extra);\nif (removedReporterOverride) {\n  console.warn(\n    \"Ignoring node --test reporter overrides to preserve console + JUnit output.\",\n  );\n}\n\nif (!fs.existsSync(allTestsDir)) {\n  console.error(\n    \"Missing packages/server-v3/dist/tests. Run pnpm run build first.\",\n  );\n  process.exit(1);\n}\n\nconst serverTarget = (\n  process.env.STAGEHAND_SERVER_TARGET ?? \"sea\"\n).toLowerCase();\nconst explicitBaseUrl = process.env.STAGEHAND_BASE_URL;\nconst baseUrl = explicitBaseUrl ?? \"http://stagehand-api.localhost:3106\"; // different than server-v4 to avoid clash\n\nif (serverTarget === \"remote\" && !explicitBaseUrl) {\n  console.error(\"Missing STAGEHAND_BASE_URL for remote server target.\");\n  process.exit(1);\n}\n\nif (\n  serverTarget === \"local\" &&\n  !fs.existsSync(`${repoRoot}/packages/server-v3/dist/server.js`)\n) {\n  console.error(\n    \"Missing packages/server-v3/dist/server.js. Run pnpm run build first.\",\n  );\n  process.exit(1);\n}\n\nconst parsedBaseUrl = new URL(baseUrl);\nconst port =\n  parsedBaseUrl.port || (parsedBaseUrl.protocol === \"https:\" ? \"443\" : \"80\");\n\nprocess.env.PORT = port;\nprocess.env.STAGEHAND_API_URL = baseUrl;\nprocess.env.BB_ENV = process.env.BB_ENV ?? \"local\";\n\nconst baseNodeOptions = \"--enable-source-maps\";\nconst nodeOptions = [process.env.NODE_OPTIONS, baseNodeOptions]\n  .filter(Boolean)\n  .join(\" \");\n\nconst allPaths =\n  paths.length > 0\n    ? paths.map(resolveRepoRelative)\n    : [\n        ...collectFiles(unitDir, \".test.js\"),\n        ...collectFiles(integrationDir, \".test.js\"),\n      ];\n\nconst unitPaths = allPaths.filter((p) =>\n  p.replaceAll(\"\\\\\", \"/\").includes(\"/packages/server-v3/dist/tests/unit/\"),\n);\nconst integrationPaths = allPaths.filter((p) =>\n  p\n    .replaceAll(\"\\\\\", \"/\")\n    .includes(\"/packages/server-v3/dist/tests/integration/\"),\n);\n\nconst singlePath = allPaths.length === 1 ? allPaths[0] : null;\nconst coverageSuffix =\n  singlePath &&\n  singlePath.startsWith(`${repoRoot}/packages/server-v3/dist/tests/unit/`)\n    ? `server-unit/${path.basename(singlePath).replace(/\\.test\\.js$/, \"\")}`\n    : singlePath &&\n        singlePath.startsWith(\n          `${repoRoot}/packages/server-v3/dist/tests/integration/`,\n        )\n      ? `server-integration/${path\n          .relative(integrationDir, singlePath)\n          .replace(/\\.test\\.js$/, \"\")\n          .replaceAll(\"\\\\\", \"/\")}`\n      : \"server\";\n\nconst coverageRoot = resolveRepoRelative(\n  process.env.NODE_V8_COVERAGE ?? `${repoRoot}/coverage/${coverageSuffix}`,\n);\nconst testsCoverage = `${coverageRoot}/tests`;\nconst serverCoverage = `${coverageRoot}/server`;\nfs.mkdirSync(testsCoverage, { recursive: true });\nfs.mkdirSync(serverCoverage, { recursive: true });\n\nconst consoleReporter = process.env.NODE_TEST_CONSOLE_REPORTER ?? \"spec\";\nconst defaultReporter = process.env.NODE_TEST_REPORTER ?? \"junit\";\nconst envDestination = process.env.NODE_TEST_REPORTER_DESTINATION\n  ? resolveRepoRelative(process.env.NODE_TEST_REPORTER_DESTINATION)\n  : null;\n\nconst reporterArgsFor = (kind: \"unit\" | \"integration\", testName?: string) => {\n  const destination =\n    envDestination ??\n    `${repoRoot}/ctrf/${kind === \"unit\" ? \"server-unit\" : \"server-integration\"}/${testName ? `${testName}.xml` : \"all.xml\"}`;\n  ensureParentDir(destination);\n  return {\n    args: [\n      `--test-reporter=${consoleReporter}`,\n      `--test-reporter=${defaultReporter}`,\n      \"--test-reporter-destination=stdout\",\n      `--test-reporter-destination=${destination}`,\n    ],\n    destination,\n  };\n};\n\nconst runNodeTests = (files: string[], reporterArgs: string[]) =>\n  spawnSync(\n    process.execPath,\n    [\"--test\", ...extraArgs, ...reporterArgs, ...files],\n    {\n      stdio: \"inherit\",\n      env: {\n        ...process.env,\n        NODE_OPTIONS: nodeOptions,\n        NODE_V8_COVERAGE: testsCoverage,\n      },\n    },\n  );\n\nconst waitForServer = async (url: string, timeoutMs = 30_000) => {\n  const start = Date.now();\n  while (Date.now() - start < timeoutMs) {\n    try {\n      const controller = new AbortController();\n      const timer = setTimeout(() => controller.abort(), 2_000);\n      const res = await fetch(url, { signal: controller.signal });\n      clearTimeout(timer);\n      if (res.ok) return true;\n    } catch {\n      // retry\n    }\n    await new Promise((resolve) => setTimeout(resolve, 1_000));\n  }\n  return false;\n};\n\nconst startServer = async () => {\n  if (serverTarget === \"remote\") return null;\n  if (serverTarget === \"local\") {\n    return spawn(\n      process.execPath,\n      [`${repoRoot}/packages/server-v3/dist/server.js`],\n      {\n        stdio: \"inherit\",\n        env: {\n          ...process.env,\n          NODE_ENV: \"development\",\n          NODE_OPTIONS: nodeOptions,\n          NODE_V8_COVERAGE: serverCoverage,\n        },\n      },\n    );\n  }\n\n  const defaultName = `stagehand-server-v3-${process.platform}-${process.arch}${process.platform === \"win32\" ? \".exe\" : \"\"}`;\n  const seaBinary = `${repoRoot}/packages/server-v3/dist/sea/${process.env.SEA_BINARY_NAME ?? defaultName}`;\n\n  if (!fs.existsSync(seaBinary)) {\n    console.error(`SEA binary not found at ${seaBinary}`);\n    process.exit(1);\n  }\n\n  return spawn(seaBinary, [\"--node-options=--no-lazy --enable-source-maps\"], {\n    stdio: \"inherit\",\n    env: {\n      ...process.env,\n      NODE_ENV: \"production\",\n      NODE_V8_COVERAGE: serverCoverage,\n      STAGEHAND_SEA_CACHE_DIR:\n        process.env.STAGEHAND_SEA_CACHE_DIR ?? `${repoRoot}/.stagehand-sea`,\n    },\n  });\n};\n\nlet serverProc: ReturnType<typeof spawn> | null = null;\nlet status = 0;\n\nif (unitPaths.length > 0) {\n  const unitName =\n    unitPaths.length === 1 ? toTestName(unitPaths[0], unitDir) : undefined;\n  const reporter = reporterArgsFor(\"unit\", unitName);\n  const result = runNodeTests(unitPaths, reporter.args);\n  status = result.status ?? 1;\n  writeCtrfFromJunit(reporter.destination, \"node-test\");\n}\n\nif (status === 0 && integrationPaths.length > 0) {\n  serverProc = await startServer();\n  const ready = await waitForServer(`${process.env.STAGEHAND_API_URL}/healthz`);\n  if (!ready) {\n    console.error(\"Server failed to start within 30 seconds.\");\n    status = 1;\n  } else {\n    const integrationName =\n      integrationPaths.length === 1\n        ? toTestName(integrationPaths[0], integrationDir)\n        : undefined;\n    const reporter = reporterArgsFor(\"integration\", integrationName);\n    const result = runNodeTests(integrationPaths, reporter.args);\n    status = result.status ?? 1;\n    writeCtrfFromJunit(reporter.destination, \"node-test\");\n  }\n}\n\nif (serverProc) {\n  serverProc.kill(\"SIGTERM\");\n  await new Promise<void>((resolve) => {\n    if (serverProc?.exitCode !== null) return resolve();\n    const timer = setTimeout(resolve, 10_000);\n    serverProc?.once(\"exit\", () => {\n      clearTimeout(timer);\n      resolve();\n    });\n  });\n  await new Promise((resolve) => setTimeout(resolve, 5_000));\n}\n\nprocess.exit(status);\n"
  },
  {
    "path": "packages/server-v3/src/lib/InMemorySessionStore.ts",
    "content": "import { randomUUID } from \"crypto\";\nimport type { V3Options, LogLine } from \"@browserbasehq/stagehand\";\nimport { V3 } from \"@browserbasehq/stagehand\";\nimport type {\n  SessionStore,\n  CreateSessionParams,\n  RequestContext,\n  SessionCacheConfig,\n  SessionStartResult,\n} from \"./SessionStore.js\";\n\nconst DEFAULT_MAX_CAPACITY = 100;\nconst DEFAULT_TTL_MS = 0; // 0 = infinite (no TTL-based eviction)\n\n/**\n * Internal node for LRU linked list\n */\ninterface LruNode {\n  sessionId: string;\n  params: CreateSessionParams;\n  stagehand: V3 | null;\n  loggerRef: { current?: (message: LogLine) => void };\n  expiry: number;\n  prev: LruNode | null;\n  next: LruNode | null;\n}\n\n/**\n * In-memory implementation of SessionStore with full caching support.\n *\n * Features:\n * - LRU eviction when at capacity\n * - TTL-based expiration\n * - Lazy V3 instance creation\n * - Dynamic logger updates for streaming\n * - Automatic cleanup of evicted sessions\n *\n * This is the default implementation used when no custom store is provided.\n * For stateless pod architectures, use a database-backed implementation.\n */\nexport class InMemorySessionStore implements SessionStore {\n  private first: LruNode | null = null;\n  private last: LruNode | null = null;\n  private items: Map<string, LruNode> = new Map();\n  private maxCapacity: number;\n  private ttlMs: number;\n  private cleanupInterval: NodeJS.Timeout | null = null;\n\n  constructor(config?: SessionCacheConfig) {\n    this.maxCapacity = config?.maxCapacity ?? DEFAULT_MAX_CAPACITY;\n    this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;\n    this.startCleanupInterval();\n  }\n\n  /**\n   * Start periodic cleanup of expired sessions\n   */\n  private startCleanupInterval(): void {\n    // Run cleanup every minute\n    this.cleanupInterval = setInterval(() => {\n      this.cleanupExpired();\n    }, 60_000);\n    // Allow process to exit gracefully even if this timer is still active\n    this.cleanupInterval.unref();\n  }\n\n  /**\n   * Cleanup expired sessions\n   */\n  private async cleanupExpired(): Promise<void> {\n    const now = Date.now();\n    const expiredIds: string[] = [];\n\n    for (const [sessionId, node] of this.items.entries()) {\n      if (this.ttlMs > 0 && node.expiry <= now) {\n        expiredIds.push(sessionId);\n      }\n    }\n\n    for (const sessionId of expiredIds) {\n      await this.deleteSession(sessionId);\n    }\n  }\n\n  /**\n   * Bump a node to the end of the LRU list (most recently used)\n   */\n  private bumpNode(node: LruNode): void {\n    // Update expiry\n    node.expiry = this.ttlMs > 0 ? Date.now() + this.ttlMs : Infinity;\n\n    if (this.last === node) {\n      return; // Already most recent\n    }\n\n    const { prev, next } = node;\n\n    // Unlink from current position\n    if (prev) prev.next = next;\n    if (next) next.prev = prev;\n    if (this.first === node) this.first = next;\n\n    // Link to end\n    node.prev = this.last;\n    node.next = null;\n    if (this.last) this.last.next = node;\n    this.last = node;\n\n    if (!this.first) this.first = node;\n  }\n\n  /**\n   * Evict the least recently used session\n   */\n  private async evictLru(): Promise<void> {\n    const lruNode = this.first;\n    if (!lruNode) return;\n\n    await this.deleteSession(lruNode.sessionId);\n  }\n\n  async startSession(params: CreateSessionParams): Promise<SessionStartResult> {\n    // Generate session ID or use provided browserbase session ID\n    const sessionId = params.browserbaseSessionID ?? randomUUID();\n\n    // Store the session\n    await this.createSession(sessionId, params);\n\n    return {\n      sessionId,\n      cdpUrl: params.connectUrl ?? \"\",\n      available: true,\n    };\n  }\n\n  async endSession(sessionId: string): Promise<void> {\n    await this.deleteSession(sessionId);\n  }\n\n  async hasSession(sessionId: string): Promise<boolean> {\n    const node = this.items.get(sessionId);\n    if (!node) return false;\n\n    // Check if expired\n    if (this.ttlMs > 0 && node.expiry <= Date.now()) {\n      await this.deleteSession(sessionId);\n      return false;\n    }\n\n    return true;\n  }\n\n  async getOrCreateStagehand(\n    sessionId: string,\n    ctx: RequestContext,\n  ): Promise<V3> {\n    const node = this.items.get(sessionId);\n\n    if (!node) {\n      throw new Error(`Session not found: ${sessionId}`);\n    }\n\n    // Check if expired\n    if (this.ttlMs > 0 && node.expiry <= Date.now()) {\n      await this.deleteSession(sessionId);\n      throw new Error(`Session expired: ${sessionId}`);\n    }\n\n    // Bump to most recently used\n    this.bumpNode(node);\n\n    // Update logger reference for this request\n    if (ctx.logger) {\n      node.loggerRef.current = ctx.logger;\n    }\n\n    // If V3 instance exists, return it\n    if (node.stagehand) {\n      return node.stagehand;\n    }\n\n    // Create V3 instance (lazy initialization)\n    const options = this.buildV3Options(node.params, ctx, node.loggerRef);\n    const stagehand = new V3(options);\n    try {\n      await stagehand.init();\n    } catch (error) {\n      try {\n        await stagehand.close();\n      } catch {\n        // best-effort cleanup for failed init attempts\n      }\n      throw error;\n    }\n    node.stagehand = stagehand;\n    return stagehand;\n  }\n\n  /**\n   * Build V3Options from stored params and request context\n   */\n  private buildV3Options(\n    params: CreateSessionParams,\n    ctx: RequestContext,\n    loggerRef: { current?: (message: LogLine) => void },\n  ): V3Options {\n    const isBrowserbase = params.browserType === \"browserbase\";\n\n    const options: V3Options = {\n      env: isBrowserbase ? \"BROWSERBASE\" : \"LOCAL\",\n      model: {\n        modelName: params.modelName,\n        apiKey: ctx.modelApiKey,\n      },\n      verbose: params.verbose,\n      systemPrompt: params.systemPrompt,\n      selfHeal: params.selfHeal,\n      domSettleTimeout: params.domSettleTimeoutMs,\n      experimental: params.experimental,\n      // Wrap logger to use the ref so it can be updated per-request\n      logger: (message: LogLine) => {\n        if (loggerRef.current) {\n          loggerRef.current(message);\n        }\n      },\n    };\n\n    if (isBrowserbase) {\n      options.apiKey = params.browserbaseApiKey;\n      options.projectId = params.browserbaseProjectId;\n\n      if (params.browserbaseSessionID) {\n        options.browserbaseSessionID = params.browserbaseSessionID;\n      }\n\n      if (params.browserbaseSessionCreateParams) {\n        options.browserbaseSessionCreateParams =\n          params.browserbaseSessionCreateParams;\n      }\n    } else if (params.localBrowserLaunchOptions) {\n      options.localBrowserLaunchOptions = params.localBrowserLaunchOptions;\n    }\n\n    return options;\n  }\n\n  async createSession(\n    sessionId: string,\n    params: CreateSessionParams,\n  ): Promise<void> {\n    // Check if already exists\n    if (this.items.has(sessionId)) {\n      throw new Error(`Session already exists: ${sessionId}`);\n    }\n\n    // Evict LRU if at capacity\n    if (this.maxCapacity > 0 && this.items.size >= this.maxCapacity) {\n      await this.evictLru();\n    }\n\n    // Create new node\n    const node: LruNode = {\n      sessionId,\n      params,\n      stagehand: null, // Lazy initialization\n      loggerRef: {},\n      expiry: this.ttlMs > 0 ? Date.now() + this.ttlMs : Infinity,\n      prev: this.last,\n      next: null,\n    };\n\n    this.items.set(sessionId, node);\n\n    // Link to end of list\n    if (this.last) this.last.next = node;\n    this.last = node;\n    if (!this.first) this.first = node;\n  }\n\n  async deleteSession(sessionId: string): Promise<void> {\n    const node = this.items.get(sessionId);\n    if (!node) return;\n\n    // Close V3 instance if it exists\n    if (node.stagehand) {\n      try {\n        await node.stagehand.close();\n      } catch (error) {\n        console.error(\n          `Error closing stagehand for session ${sessionId}:`,\n          error,\n        );\n      }\n    }\n\n    // Remove from map\n    this.items.delete(sessionId);\n\n    // Unlink from list\n    const { prev, next } = node;\n    if (prev) prev.next = next;\n    if (next) next.prev = prev;\n    if (this.first === node) this.first = next;\n    if (this.last === node) this.last = prev;\n  }\n\n  async getSessionConfig(sessionId: string): Promise<CreateSessionParams> {\n    const node = this.items.get(sessionId);\n\n    if (!node) {\n      throw new Error(`Session not found: ${sessionId}`);\n    }\n\n    // Return the stored params (contains browser metadata needed downstream)\n    return node.params;\n  }\n\n  updateCacheConfig(config: SessionCacheConfig): void {\n    if (config.maxCapacity !== undefined) {\n      if (config.maxCapacity <= 0) {\n        throw new Error(\"Max capacity must be greater than 0\");\n      }\n      const previousCapacity = this.maxCapacity;\n      this.maxCapacity = config.maxCapacity;\n\n      // Evict excess if new capacity is smaller\n      if (this.maxCapacity < previousCapacity) {\n        const excess = this.items.size - this.maxCapacity;\n        for (let i = 0; i < excess; i++) {\n          // Fire and forget - don't await to match cloud behavior\n          this.evictLru().catch(console.error);\n        }\n      }\n    }\n\n    if (config.ttlMs !== undefined) {\n      this.ttlMs = config.ttlMs;\n    }\n  }\n\n  getCacheConfig(): SessionCacheConfig {\n    return {\n      maxCapacity: this.maxCapacity,\n      ttlMs: this.ttlMs,\n    };\n  }\n\n  async destroy(): Promise<void> {\n    // Stop cleanup interval\n    if (this.cleanupInterval) {\n      clearInterval(this.cleanupInterval);\n      this.cleanupInterval = null;\n    }\n\n    // Close all V3 instances\n    const sessionIds = Array.from(this.items.keys());\n    await Promise.all(sessionIds.map((id) => this.deleteSession(id)));\n  }\n\n  /**\n   * Get the number of cached sessions\n   */\n  get size(): number {\n    return this.items.size;\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/SessionStore.ts",
    "content": "import type {\n  Api,\n  LocalBrowserLaunchOptions,\n  LogLine,\n  V3,\n} from \"@browserbasehq/stagehand\";\n\n/**\n * Result from SessionStore.startSession().\n */\nexport type SessionStartResult = Api.SessionStartResult;\n\n/**\n * Parameters for creating a new session.\n * This is what gets persisted - a subset of StartSessionParams\n * that excludes runtime-only values like modelApiKey.\n *\n * Includes cloud-specific fields that pass through to cloud implementations.\n * The library ignores fields it doesn't need, but they're available to SessionStore.\n */\nexport interface CreateSessionParams {\n  /** Browser choice for this session */\n  browserType: \"local\" | \"browserbase\";\n  /** Model name (e.g., \"openai/gpt-4o\") */\n  modelName: string;\n  /** Verbosity level */\n  verbose?: 0 | 1 | 2;\n  /** Custom system prompt */\n  systemPrompt?: string;\n  /** Enable self-healing for failed actions */\n  selfHeal?: boolean;\n  /** DOM settle timeout in milliseconds */\n  domSettleTimeoutMs?: number;\n  /** Enable experimental features */\n  experimental?: boolean;\n\n  // Browserbase-specific (used by cloud implementations)\n  /** Browserbase API key */\n  browserbaseApiKey?: string;\n  /** Browserbase project ID */\n  browserbaseProjectId?: string;\n  /** Existing Browserbase session ID to connect to */\n  browserbaseSessionID?: string;\n  /** Wait for captcha solves */\n  waitForCaptchaSolves?: boolean;\n  /** Browserbase session creation params */\n  browserbaseSessionCreateParams?: Record<string, unknown>;\n  /** Local browser launch overrides when browserType is local */\n  localBrowserLaunchOptions?: LocalBrowserLaunchOptions;\n\n  /** WebSocket URL for connecting to the browser (returned to client) */\n  connectUrl?: string;\n\n  // Cloud-specific metadata fields\n  /** Act timeout in milliseconds */\n  actTimeoutMs?: number;\n  /** Client language (typescript, python, playground) */\n  clientLanguage?: string;\n  /** SDK version */\n  sdkVersion?: string;\n}\n\n/**\n * Request-time context passed when resolving a session.\n * Contains values that come from request headers rather than storage.\n */\nexport interface RequestContext {\n  /** Model API key (from x-model-api-key header) */\n  modelApiKey?: string;\n  /** Logger function for this request */\n  logger?: (message: LogLine) => void;\n}\n\n/**\n * Configuration options for session cache behavior.\n */\nexport interface SessionCacheConfig {\n  /** Maximum number of sessions to cache. Default: 100 */\n  maxCapacity?: number;\n  /** TTL for cached sessions in milliseconds. Default: 300000 (5 minutes) */\n  ttlMs?: number;\n}\n\n/**\n * SessionStore interface for managing session lifecycle and V3 instances.\n *\n * The library provides an InMemorySessionStore as the default implementation\n * with full caching support (TTL, LRU eviction, etc.).\n *\n * Cloud environments can implement this interface to:\n * - Persist session config to a database\n * - Use custom caching strategies (e.g., LaunchDarkly-driven config)\n * - Add eviction hooks for cleanup\n * - Handle platform-specific session lifecycle (e.g., Browserbase)\n *\n * This enables stateless pod architectures where any pod can handle any request.\n */\nexport interface SessionStore {\n  /**\n   * Start a new session.\n   *\n   * This is the main entry point for session creation. Implementations can:\n   * - Create platform-specific resources (e.g., Browserbase session)\n   * - Persist session config to storage\n   * - Check feature flags for availability\n   *\n   * @param params - Session configuration\n   * @returns Session ID and availability status\n   */\n  startSession(params: CreateSessionParams): Promise<SessionStartResult>;\n\n  /**\n   * End a session and cleanup all resources.\n   *\n   * This is the main entry point for session cleanup. Implementations can:\n   * - Close platform-specific resources (e.g., Browserbase session)\n   * - Evict V3 instance from cache\n   * - Update session status in storage\n   *\n   * @param sessionId - The session identifier\n   */\n  endSession(sessionId: string): Promise<void>;\n\n  /**\n   * Check if a session exists.\n   * @param sessionId - The session identifier\n   * @returns true if the session exists\n   */\n  hasSession(sessionId: string): Promise<boolean>;\n\n  /**\n   * Get or create a V3 instance for a session.\n   *\n   * This method handles:\n   * - Checking the cache for an existing V3 instance\n   * - On cache miss: loading config, creating V3, caching it\n   * - Updating the logger reference for streaming\n   *\n   * @param sessionId - The session identifier\n   * @param ctx - Request-time context containing values from headers\n   * @returns The V3 instance ready for use\n   * @throws Error if session not found\n   */\n  getOrCreateStagehand(sessionId: string, ctx: RequestContext): Promise<V3>;\n\n  /**\n   * Create a new session with the given parameters.\n   * Lower-level than startSession - just stores the config.\n   * @param sessionId - The session identifier\n   * @param params - Session configuration to persist\n   */\n  createSession(sessionId: string, params: CreateSessionParams): Promise<void>;\n\n  /**\n   * Delete a session from cache and close V3 instance.\n   * Lower-level than endSession - just handles cache cleanup.\n   * @param sessionId - The session identifier\n   */\n  deleteSession(sessionId: string): Promise<void>;\n\n  /**\n   * Retrieve the stored session configuration for a given session.\n   * @param sessionId - The session identifier\n   */\n  getSessionConfig(sessionId: string): Promise<CreateSessionParams>;\n\n  /**\n   * Update cache configuration dynamically.\n   * @param config - New cache configuration values\n   */\n  updateCacheConfig?(config: SessionCacheConfig): void;\n\n  /**\n   * Get current cache configuration.\n   * @returns Current cache config\n   */\n  getCacheConfig?(): SessionCacheConfig;\n\n  /**\n   * Cleanup all resources (close all V3 instances, stop timers).\n   * Called when shutting down the server.\n   */\n  destroy(): Promise<void>;\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/auth.ts",
    "content": "import type { FastifyRequest } from \"fastify\";\n\nexport const authMiddleware = async (\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  request: FastifyRequest,\n): Promise<boolean> => {\n  // Authentication is currently disabled; we may re-enable when a real auth backend is wired up.\n  return await isAuthenticated();\n};\n\n// TODO: Temporarily disable auth until setup in supabase\nconst isAuthenticated = async (): Promise<boolean> => {\n  return true;\n};\n"
  },
  {
    "path": "packages/server-v3/src/lib/env.ts",
    "content": "import { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod/v4\";\n\n// Temporarily defining here until browserbase zod package is updated to 3.25.0+\nconst bbEnvSchema = z.enum([\"local\", \"dev\", \"prod\"]);\n\nexport const env = createEnv({\n  server: {\n    NODE_ENV: z.enum([\"development\", \"production\", \"staging\", \"test\"]),\n    BB_ENV: bbEnvSchema,\n  },\n  client: {},\n  clientPrefix: \"PUBLIC_\",\n  runtimeEnv: {\n    NODE_ENV: process.env.NODE_ENV ?? \"production\",\n    BB_ENV: process.env.BB_ENV ?? \"local\",\n  },\n});\n"
  },
  {
    "path": "packages/server-v3/src/lib/errorHandler.ts",
    "content": "import type {\n  FastifyReply,\n  FastifyRequest,\n  RouteGenericInterface,\n} from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\n\nimport { error } from \"./response.js\";\n\nexport class AppError extends Error {\n  statusCode: number;\n  isInternal: boolean;\n\n  constructor(\n    message: string,\n    statusCode = StatusCodes.BAD_REQUEST,\n    isInternal = false,\n  ) {\n    super(message);\n    this.statusCode = statusCode;\n    this.isInternal = isInternal;\n    this.name = this.constructor.name;\n    Error.captureStackTrace(this, this.constructor);\n  }\n\n  /**\n   * Get the message safe to send to clients.\n   * For internal errors (5xx), returns generic message.\n   * For client errors (4xx), returns actual message.\n   */\n  getClientMessage(): string {\n    if (this.isInternal) {\n      return this.statusCode >= StatusCodes.INTERNAL_SERVER_ERROR\n        ? \"An internal server error occurred\"\n        : \"An error occurred while processing your request\";\n    }\n    return this.message;\n  }\n}\n\n/**\n * Wraps a route handler with error handling\n * @param handler The route handler to wrap\n * @returns A wrapped route handler that catches errors\n */\nexport function withErrorHandling<\n  T extends RouteGenericInterface = RouteGenericInterface,\n  R = unknown,\n>(handler: (request: FastifyRequest<T>, reply: FastifyReply) => Promise<R>) {\n  return async (\n    request: FastifyRequest<T>,\n    reply: FastifyReply,\n  ): Promise<R | FastifyReply> => {\n    try {\n      return await handler(request, reply);\n    } catch (err) {\n      request.log.error(err);\n\n      if (err instanceof AppError) {\n        return error(reply, err.getClientMessage(), err.statusCode);\n      }\n\n      return error(\n        reply,\n        \"An internal server error occurred\",\n        StatusCodes.INTERNAL_SERVER_ERROR,\n      );\n    }\n  };\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/header.ts",
    "content": "import type { FastifyRequest } from \"fastify\";\n\nimport { MissingHeaderError } from \"../types/error.js\";\n\nexport const dangerouslyGetHeader = (\n  request: FastifyRequest,\n  header: string,\n): string => {\n  const headerValue = request.headers[header];\n\n  if (!headerValue) {\n    throw new MissingHeaderError(header);\n  }\n  if (Array.isArray(headerValue)) {\n    const [value] = headerValue;\n    if (!value) {\n      throw new MissingHeaderError(header);\n    }\n    return value;\n  }\n  return headerValue;\n};\n\nexport const getOptionalHeader = (\n  request: FastifyRequest,\n  header: string,\n): string | undefined => {\n  const headerValue = request.headers[header];\n  if (!headerValue) {\n    return undefined;\n  }\n  if (Array.isArray(headerValue)) {\n    const [value] = headerValue;\n    if (!value) {\n      return undefined;\n    }\n    return value;\n  }\n  return headerValue;\n};\n\n/**\n * Extracts model name from request body, supporting V3 structure.\n * - V3: body.options.model.modelName\n */\nexport function getModelName(request: FastifyRequest): string | undefined {\n  const body = request.body as Record<string, unknown> | undefined;\n  const options = body?.options as Record<string, unknown> | undefined;\n  const model = options?.model as Record<string, unknown> | undefined;\n\n  if (typeof model?.modelName === \"string\" && model.modelName) {\n    return model.modelName;\n  }\n\n  if (typeof body?.modelName === \"string\" && body.modelName) {\n    return body.modelName;\n  }\n\n  return undefined;\n}\n\n/**\n * Extracts the model API key with precedence:\n * 1. Per-request body apiKey (V3: body.options.model.apiKey)\n * 2. Per-request header x-model-api-key\n */\nexport function getModelApiKey(request: FastifyRequest): string | undefined {\n  const body = request.body as Record<string, unknown> | undefined;\n  const options = body?.options as Record<string, unknown> | undefined;\n  const model = options?.model as Record<string, unknown> | undefined;\n\n  if (typeof model?.apiKey === \"string\" && model.apiKey) {\n    return model.apiKey;\n  }\n\n  return getOptionalHeader(request, \"x-model-api-key\");\n}\n\n/**\n * Extracts the stream response value from either the request header or body.\n * Body parameter takes precedence over header.\n * Defaults to false (non-streaming) if neither is provided.\n */\nexport function shouldRespondWithSSE(request: FastifyRequest): boolean {\n  const body = request.body as Record<string, unknown> | undefined;\n  if (typeof body?.streamResponse === \"boolean\") {\n    return body.streamResponse;\n  }\n  if (typeof body?.streamResponse === \"string\") {\n    return body.streamResponse.toLowerCase() === \"true\";\n  }\n\n  const streamHeader = getOptionalHeader(request, \"x-stream-response\");\n  if (streamHeader) {\n    return streamHeader.toLowerCase() === \"true\";\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/logging/index.ts",
    "content": "import { FastifyInstance } from \"fastify\";\n\nimport { env } from \"../../lib/env.js\";\n\n// List of routes to ignore for request logging in local environments\nconst ignoredRoutes = [\"/healthz\", \"/readyz\"];\n\n// Helper function to determine if a request should be logged\nconst shouldLog = (url: string) => {\n  return env.BB_ENV !== \"local\" || !ignoredRoutes.includes(url);\n};\n\nconst logging = (app: FastifyInstance) => {\n  // Add request logging hooks\n  app.addHook(\"onRequest\", (req, _reply, done) => {\n    // Add request ID to response headers\n    if (shouldLog(req.url)) {\n      req.log.info(\n        {\n          req: {\n            host: req.hostname,\n            method: req.method,\n            remoteAddress: req.ip,\n            remotePort: req.socket.remotePort,\n            url: req.url,\n          },\n          reqId: req.id,\n        },\n        \"incoming request\",\n      );\n    }\n    done();\n  });\n\n  app.addHook(\"onResponse\", (req, reply, done) => {\n    if (shouldLog(req.url)) {\n      req.log.info(\n        {\n          reqId: req.id,\n          req: {\n            host: req.hostname,\n            method: req.method,\n            remoteAddress: req.ip,\n            remotePort: req.socket.remotePort,\n            url: req.url,\n          },\n          res: {\n            statusCode: reply.statusCode,\n          },\n          responseTime: reply.elapsedTime,\n        },\n        \"request completed\",\n      );\n    }\n    done();\n  });\n};\n\nexport { logging };\n"
  },
  {
    "path": "packages/server-v3/src/lib/response.ts",
    "content": "import type { FastifyReply } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\n\ninterface SuccessResponse<T> {\n  success: true;\n  data: T;\n}\n\ninterface ErrorResponse {\n  success: false;\n  message: string;\n}\n\ntype ApiResponse<T> = SuccessResponse<T> | ErrorResponse;\n\nexport function success<T>(\n  reply: FastifyReply,\n  data: T,\n  status = StatusCodes.OK,\n): FastifyReply {\n  return reply.status(status).send({\n    success: true,\n    data,\n  });\n}\n\nexport function error(\n  reply: FastifyReply,\n  message: string,\n  status = StatusCodes.BAD_REQUEST,\n): FastifyReply {\n  return reply.status(status).send({\n    success: false,\n    message,\n  });\n}\n\nexport function isSuccessResponse<T>(\n  response: ApiResponse<T>,\n): response is SuccessResponse<T> {\n  return response.success;\n}\n\nexport function isErrorResponse(\n  response: ApiResponse<unknown>,\n): response is ErrorResponse {\n  return !response.success;\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/sessionStoreManager.ts",
    "content": "import { InMemorySessionStore } from \"./InMemorySessionStore.js\";\nimport type { SessionCacheConfig, SessionStore } from \"./SessionStore.js\";\n\nlet sessionStore: SessionStore | null = null;\n\nexport function initializeSessionStore(\n  config?: SessionCacheConfig,\n): SessionStore {\n  if (!sessionStore) {\n    sessionStore = new InMemorySessionStore(config);\n  }\n  return sessionStore;\n}\n\nexport function getSessionStore(): SessionStore {\n  if (!sessionStore) {\n    throw new Error(\"Session store has not been initialized\");\n  }\n  return sessionStore;\n}\n\nexport async function destroySessionStore(): Promise<void> {\n  if (sessionStore) {\n    await sessionStore.destroy();\n    sessionStore = null;\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/stream.ts",
    "content": "import type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { Stagehand as V3Stagehand } from \"@browserbasehq/stagehand\";\nimport { v4 } from \"uuid\";\nimport { z } from \"zod/v4\";\n\nimport { AppError } from \"./errorHandler.js\";\nimport {\n  getModelApiKey,\n  getModelName,\n  getOptionalHeader,\n  shouldRespondWithSSE,\n} from \"./header.js\";\nimport { error, success } from \"./response.js\";\nimport { getSessionStore } from \"./sessionStoreManager.js\";\nimport type { RequestContext } from \"./SessionStore.js\";\n\ninterface StreamingResponseOptions<TV3> {\n  sessionId: string;\n  request: FastifyRequest;\n  reply: FastifyReply;\n  schema: z.ZodType<TV3>;\n  handler: (ctx: {\n    stagehand: V3Stagehand;\n    data: TV3;\n  }) => Promise<{ result: unknown; actionId?: string }>;\n  operation?: string;\n}\n\nexport async function createStreamingResponse<TV3>({\n  sessionId,\n  request,\n  reply,\n  schema,\n  handler,\n  operation,\n}: StreamingResponseOptions<TV3>) {\n  const shouldStreamResponse = shouldRespondWithSSE(request);\n  const modelApiKey = getModelApiKey(request);\n\n  const sessionStore = getSessionStore();\n  const sessionConfig = await sessionStore.getSessionConfig(sessionId);\n  const browserType = sessionConfig.browserType ?? \"local\";\n\n  let browserbaseApiKey = sessionConfig.browserbaseApiKey;\n  let browserbaseProjectId = sessionConfig.browserbaseProjectId;\n\n  if (browserType === \"browserbase\") {\n    browserbaseApiKey =\n      browserbaseApiKey ?? getOptionalHeader(request, \"x-bb-api-key\");\n    browserbaseProjectId =\n      browserbaseProjectId ?? getOptionalHeader(request, \"x-bb-project-id\");\n\n    if (!browserbaseApiKey || !browserbaseProjectId) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        error:\n          \"Browserbase API key and project ID are required for browserbase sessions\",\n      });\n    }\n  }\n\n  // Parse data using V3 schema\n  let parsedData: TV3;\n\n  try {\n    const json: unknown = request.body;\n    parsedData = await schema.parseAsync(json);\n  } catch (err) {\n    const parseError = err as Error | z.ZodError;\n\n    if (parseError instanceof z.ZodError) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        error: parseError.issues.map((issue) => ({\n          path: issue.path[0],\n          message: issue.message,\n        })),\n      });\n    }\n\n    return reply\n      .status(StatusCodes.BAD_REQUEST)\n      .send({ error: parseError.message });\n  }\n\n  if (shouldStreamResponse) {\n    try {\n      reply.raw.writeHead(StatusCodes.OK, {\n        \"Content-Type\": \"text/event-stream\",\n        \"Cache-Control\": \"no-cache, no-transform\",\n        Connection: \"keep-alive\",\n        \"Transfer-Encoding\": \"chunked\",\n        \"X-Accel-Buffering\": \"no\",\n        \"Access-Control-Allow-Origin\": \"*\",\n        \"Access-Control-Allow-Methods\": \"GET, POST, PUT, DELETE, OPTIONS\",\n        \"Access-Control-Allow-Headers\": \"*\",\n      });\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    } catch (_err) {\n      return error(\n        reply,\n        \"Failed to write head\",\n        StatusCodes.INTERNAL_SERVER_ERROR,\n      );\n    }\n  }\n\n  const sendData = (type: string, data: object) => {\n    if (!shouldStreamResponse) {\n      return;\n    }\n\n    reply.raw.write(`data: ${JSON.stringify({ data, type, id: v4() })}\\n\\n`);\n  };\n\n  const actionId = v4();\n\n  sendData(\"system\", { status: \"starting\" });\n\n  const requestContext: RequestContext = {\n    modelApiKey,\n    logger: shouldStreamResponse\n      ? (message) => {\n          sendData(\"log\", { status: \"running\", message });\n        }\n      : undefined,\n  };\n\n  let stagehand: V3Stagehand;\n  try {\n    stagehand = (await sessionStore.getOrCreateStagehand(\n      sessionId,\n      requestContext,\n    )) as V3Stagehand;\n  } catch (err) {\n    const loadError = err instanceof Error ? err : new Error(String(err));\n\n    sendData(\"system\", { status: \"error\", error: loadError.message });\n\n    if (shouldStreamResponse) {\n      reply.raw.end();\n      return reply;\n    }\n\n    return error(\n      reply,\n      loadError.message,\n      loadError instanceof AppError\n        ? loadError.statusCode\n        : StatusCodes.INTERNAL_SERVER_ERROR,\n    );\n  }\n\n  sendData(\"system\", { status: \"connected\" });\n\n  let result: Awaited<ReturnType<typeof handler>> | null = null;\n  let handlerError: Error | null = null;\n\n  try {\n    result = await handler({ stagehand, data: parsedData });\n  } catch (err) {\n    handlerError = err instanceof Error ? err : new Error(\"Unknown error\");\n    request.log.error(\n      {\n        err: handlerError,\n        operation: operation ?? \"operation\",\n        sessionId,\n        browserType,\n        modelName: getModelName(request),\n        hasModelApiKey: Boolean(modelApiKey),\n        hasBrowserbaseApiKey: Boolean(browserbaseApiKey),\n        hasBrowserbaseProjectId: Boolean(browserbaseProjectId),\n      },\n      \"operation handler failed\",\n    );\n  }\n\n  if (handlerError) {\n    const clientMessage =\n      handlerError instanceof AppError\n        ? handlerError.getClientMessage()\n        : `${operation ?? \"operation\"} failed`;\n\n    sendData(\"system\", { status: \"error\", error: clientMessage });\n\n    if (shouldStreamResponse) {\n      reply.raw.end();\n      return reply;\n    }\n\n    const statusCode =\n      handlerError instanceof AppError\n        ? handlerError.statusCode\n        : StatusCodes.INTERNAL_SERVER_ERROR;\n    return error(reply, clientMessage, statusCode);\n  }\n\n  sendData(\"system\", {\n    status: \"finished\",\n    result: result?.result,\n    actionId,\n  });\n\n  if (shouldStreamResponse) {\n    reply.raw.end();\n    return reply;\n  }\n\n  return success(reply, { result: result?.result, actionId });\n}\n"
  },
  {
    "path": "packages/server-v3/src/lib/utils.ts",
    "content": "import { StatusCodes } from \"http-status-codes\";\nimport { z } from \"zod/v3\";\nimport type { ZodTypeAny } from \"zod/v3\";\n\nimport { LegacyModel, LegacyProvider } from \"../types/model.js\";\nimport { AppError } from \"./errorHandler.js\";\n\ninterface JSONSchema {\n  type?: string | string[];\n  properties?: Record<string, JSONSchema>;\n  required?: string[];\n  description?: string;\n  items?: JSONSchema;\n  enum?: string[];\n  minimum?: number;\n  maximum?: number;\n  format?: \"uri\" | \"url\" | \"email\" | \"uuid\";\n  anyOf?: JSONSchema[];\n  oneOf?: JSONSchema[];\n  allOf?: JSONSchema[];\n}\n\n/**\n * Converts a JSON Schema object to a Zod schema.\n * @param schema The JSON Schema object to convert\n * @returns A Zod schema equivalent to the input JSON Schema\n */\nexport function jsonSchemaToZod(schema: JSONSchema): ZodTypeAny {\n  if (Array.isArray(schema.type)) {\n    const subSchemas = schema.type.map((singleType) => {\n      const sub = { ...schema, type: singleType };\n      return jsonSchemaToZod(sub);\n    });\n\n    if (subSchemas.length === 0) {\n      return z.any();\n    } else if (subSchemas.length === 1) {\n      const [subSchema] = subSchemas;\n      if (!subSchema) {\n        return z.any();\n      }\n      return subSchema;\n    }\n    return z.union(subSchemas as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]);\n  }\n\n  if (schema.anyOf && Array.isArray(schema.anyOf)) {\n    const subSchemas = schema.anyOf.map((sub) => jsonSchemaToZod(sub));\n    if (subSchemas.length === 0) {\n      return z.any();\n    } else if (subSchemas.length === 1) {\n      const [subSchema] = subSchemas;\n      if (!subSchema) {\n        return z.any();\n      }\n      return subSchema;\n    }\n    return z.union(subSchemas as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]);\n  }\n\n  if (schema.oneOf && Array.isArray(schema.oneOf)) {\n    const subSchemas = schema.oneOf.map((sub) => jsonSchemaToZod(sub));\n    if (subSchemas.length === 0) {\n      return z.any();\n    } else if (subSchemas.length === 1) {\n      const [subSchema] = subSchemas;\n      if (!subSchema) {\n        return z.any();\n      }\n      return subSchema;\n    }\n    return z.union(subSchemas as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]);\n  }\n\n  switch (schema.type) {\n    case \"object\":\n      if (schema.properties) {\n        const shape: Record<string, ZodTypeAny> = {};\n        for (const key in schema.properties) {\n          const subSchema = schema.properties[key];\n          if (!subSchema) {\n            throw new AppError(\n              `Property ${key} is not defined in the schema`,\n              StatusCodes.BAD_REQUEST,\n            );\n          }\n          shape[key] = jsonSchemaToZod(subSchema);\n        }\n        let zodObject = z.object(shape);\n\n        if (schema.required && Array.isArray(schema.required)) {\n          const requiredFields = schema.required.reduce<Record<string, true>>(\n            (acc, key) => {\n              acc[key] = true;\n              return acc;\n            },\n            {},\n          );\n          zodObject = zodObject.partial().required(requiredFields);\n        }\n\n        if (schema.description) {\n          zodObject = zodObject.describe(schema.description);\n        }\n        return zodObject;\n      }\n\n      return z.object({});\n\n    case \"array\":\n      if (schema.items) {\n        let zodArray = z.array(jsonSchemaToZod(schema.items));\n        if (schema.description) {\n          zodArray = zodArray.describe(schema.description);\n        }\n        return zodArray;\n      }\n      return z.array(z.any());\n\n    case \"string\": {\n      if (schema.enum) {\n        return z.string().refine((val) => schema.enum?.includes(val) ?? false);\n      }\n      let zodString = z.string();\n\n      switch (schema.format) {\n        case \"uri\":\n        case \"url\":\n          zodString = zodString.url();\n          break;\n        case \"email\":\n          zodString = zodString.email();\n          break;\n        case \"uuid\":\n          zodString = zodString.uuid();\n          break;\n        default:\n      }\n\n      if (schema.description) {\n        zodString = zodString.describe(schema.description);\n      }\n      return zodString;\n    }\n\n    case \"integer\": // integer is a subset of number\n    case \"number\": {\n      let zodNumber = z.number();\n      if (schema.minimum !== undefined) {\n        zodNumber = zodNumber.min(schema.minimum);\n      }\n      if (schema.maximum !== undefined) {\n        zodNumber = zodNumber.max(schema.maximum);\n      }\n      if (schema.description) {\n        zodNumber = zodNumber.describe(schema.description);\n      }\n      return zodNumber;\n    }\n\n    case \"boolean\": {\n      let zodBoolean = z.boolean();\n      if (schema.description) {\n        zodBoolean = zodBoolean.describe(schema.description);\n      }\n      return zodBoolean;\n    }\n\n    case \"null\": {\n      let zodNull = z.null();\n      if (schema.description) {\n        zodNull = zodNull.describe(schema.description);\n      }\n      return zodNull;\n    }\n\n    default:\n      // fallback if no recognized schema.type is present\n      return z.any();\n  }\n}\n\n// This function is legacy and will not be required after complete AISDK migration\nexport function mapModelToProvider(model: LegacyModel): LegacyProvider {\n  switch (model) {\n    case \"gpt-4o\":\n    case \"gpt-4o-mini\":\n    case \"gpt-4o-2024-08-06\":\n    case \"gpt-4o-2024-05-13\":\n    case \"o1-mini\":\n    case \"o1-preview\":\n    case \"gpt-4.5-preview\":\n    case \"o3-mini\":\n      return \"openai\";\n    case \"gemini-1.5-flash\":\n    case \"gemini-1.5-pro\":\n    case \"gemini-1.5-flash-8b\":\n    case \"gemini-2.0-flash-lite\":\n    case \"gemini-2.0-flash\":\n    case \"gemini-2.5-pro-preview-03-25\":\n    case \"gemini-2.5-flash-preview-04-17\":\n      return \"google\";\n    case \"cerebras-llama-3.3-70b\":\n    case \"cerebras-llama-3.1-8b\":\n      throw new AppError(\n        \"Cerebras models are not supported yet\",\n        StatusCodes.BAD_REQUEST,\n      );\n    case \"groq-llama-3.3-70b-specdec\":\n    case \"groq-llama-3.3-70b-versatile\":\n      throw new AppError(\n        \"Groq models are not supported yet\",\n        StatusCodes.BAD_REQUEST,\n      );\n    default: {\n      const errorMessage = `Unknown model: ${String(model)}`;\n      throw new AppError(errorMessage, StatusCodes.BAD_REQUEST);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/src/routes/healthcheck.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { z } from \"zod/v4\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport { withErrorHandling } from \"../lib/errorHandler.js\";\n\nconst healthcheckRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/healthz\",\n  logLevel: \"silent\",\n  schema: {\n    hide: true, // Hide from OpenAPI spec - utility endpoint\n    response: {\n      200: z\n        .object({\n          status: z.string(),\n          timestamp: z.string(),\n        })\n        .strict(),\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: withErrorHandling(async () => {\n    return {\n      status: \"ok\",\n      timestamp: new Date().toISOString(),\n    };\n  }),\n};\n\nexport default healthcheckRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/readiness.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { z } from \"zod/v4\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport { withErrorHandling } from \"../lib/errorHandler.js\";\n\n// Server readiness state management\nlet isReady = false;\n\n/**\n * Get the current readiness state of the server\n * @returns {boolean} Whether the server is ready to accept requests\n */\nexport const getIsReady = (): boolean => {\n  return isReady;\n};\n\n/**\n * Mark the server as ready to accept requests\n */\nexport const setReady = (): void => {\n  isReady = true;\n};\n\n/**\n * Mark the server as not ready to accept requests\n * Used during graceful shutdown to stop accepting new requests\n */\nexport const setUnready = (): void => {\n  isReady = false;\n};\n\nconst readinessRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/readyz\",\n  logLevel: \"silent\",\n  schema: {\n    hide: true, // Hide from OpenAPI spec - utility endpoint\n    response: {\n      200: z.string(),\n      503: z.string(),\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: withErrorHandling(async (_request, reply) => {\n    if (!isReady) {\n      return reply\n        .code(StatusCodes.SERVICE_UNAVAILABLE)\n        .send(\"Service Unavailable\");\n    }\n    return reply.code(StatusCodes.OK).send(\"Ready\");\n  }),\n};\n\nexport default readinessRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/act.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { ActResult, Action } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { AppError, withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { createStreamingResponse } from \"../../../../lib/stream.js\";\nimport { getSessionStore } from \"../../../../lib/sessionStoreManager.js\";\n\nconst actRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return reply\n        .status(StatusCodes.UNAUTHORIZED)\n        .send({ error: \"Unauthorized\" });\n    }\n\n    const { id } = request.params as Api.SessionIdParams;\n\n    if (!id.length) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        message: \"Missing session id\",\n      });\n    }\n\n    const sessionStore = getSessionStore();\n    const hasSession = await sessionStore.hasSession(id);\n    if (!hasSession) {\n      return reply.status(StatusCodes.NOT_FOUND).send({\n        message: \"Session not found\",\n      });\n    }\n\n    return createStreamingResponse<Api.ActRequest>({\n      sessionId: id,\n      request,\n      reply,\n      schema: Api.ActRequestSchema,\n      handler: async ({ stagehand, data }) => {\n        const { frameId } = data;\n        const page = frameId\n          ? stagehand.context.resolvePageByMainFrameId(frameId)\n          : await stagehand.context.awaitActivePage();\n\n        if (!page) {\n          throw new AppError(\n            \"Page not found\",\n            StatusCodes.INTERNAL_SERVER_ERROR,\n          );\n        }\n\n        const modelOpt = data.options?.model;\n        const normalizedModel =\n          typeof modelOpt === \"string\"\n            ? { modelName: modelOpt }\n            : modelOpt\n              ? { ...modelOpt, modelName: modelOpt.modelName ?? \"gpt-4o\" }\n              : undefined;\n\n        const safeOptions = {\n          ...data.options,\n          model: normalizedModel,\n          page,\n        };\n\n        let result: ActResult;\n        if (typeof data.input === \"string\") {\n          result = await stagehand.act(data.input, safeOptions);\n        } else {\n          result = await stagehand.act(data.input as Action, safeOptions);\n        }\n\n        return { result };\n      },\n      operation: \"act\",\n    });\n  },\n);\n\nconst actRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/:id/act\",\n  schema: {\n    ...Api.Operations.SessionAct,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    body: Api.ActRequestSchema,\n    response: {\n      200: Api.ActResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: actRouteHandler,\n};\n\nexport default actRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/agentExecute.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { AppError, withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { createStreamingResponse } from \"../../../../lib/stream.js\";\nimport { getSessionStore } from \"../../../../lib/sessionStoreManager.js\";\n\nconst agentExecuteRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return reply\n        .status(StatusCodes.UNAUTHORIZED)\n        .send({ error: \"Unauthorized\" });\n    }\n\n    const { id } = request.params as Api.SessionIdParams;\n\n    if (!id.length) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        message: \"Missing session id\",\n      });\n    }\n\n    const sessionStore = getSessionStore();\n    const hasSession = await sessionStore.hasSession(id);\n    if (!hasSession) {\n      return reply.status(StatusCodes.NOT_FOUND).send({\n        message: \"Session not found\",\n      });\n    }\n\n    return createStreamingResponse<Api.AgentExecuteRequest>({\n      sessionId: id,\n      request,\n      reply,\n      schema: Api.AgentExecuteRequestSchema,\n      handler: async ({ stagehand, data }) => {\n        const { agentConfig, executeOptions } = data;\n        const { frameId } = data;\n        const page = frameId\n          ? stagehand.context.resolvePageByMainFrameId(frameId)\n          : await stagehand.context.awaitActivePage();\n        if (!page) {\n          throw new AppError(\n            \"Page not found\",\n            StatusCodes.INTERNAL_SERVER_ERROR,\n          );\n        }\n        const normalizedAgentConfig = {\n          ...agentConfig,\n          model:\n            typeof agentConfig.model === \"string\"\n              ? { modelName: agentConfig.model }\n              : agentConfig.model\n                ? {\n                    ...agentConfig.model,\n                    modelName: agentConfig.model.modelName ?? \"gpt-4o\",\n                  }\n                : undefined,\n        };\n\n        const { instruction, ...restExecuteOptions } = executeOptions;\n        const fullExecuteOptions = {\n          instruction,\n          ...restExecuteOptions,\n          page,\n        };\n        let result;\n        try {\n          result = await stagehand\n            .agent(normalizedAgentConfig)\n            .execute(fullExecuteOptions);\n        } catch (err) {\n          const message = err instanceof Error ? err.message : String(err);\n          throw new AppError(message, StatusCodes.UNPROCESSABLE_ENTITY);\n        }\n\n        return { result };\n      },\n      operation: \"agentExecute\",\n    });\n  },\n);\n\nconst agentExecuteRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/:id/agentExecute\",\n  schema: {\n    ...Api.Operations.SessionAgentExecute,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    body: Api.AgentExecuteRequestSchema,\n    response: {\n      200: Api.AgentExecuteResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: agentExecuteRouteHandler,\n};\n\nexport default agentExecuteRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/end.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { error } from \"../../../../lib/response.js\";\nimport { getSessionStore } from \"../../../../lib/sessionStoreManager.js\";\n\nconst endRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return error(reply, \"Unauthorized\", StatusCodes.UNAUTHORIZED);\n    }\n\n    // This endpoint intentionally has no request body. Reject unexpected bodies to\n    // catch misconfigured clients, while still allowing empty JSON bodies.\n    const body = (request as { body?: unknown }).body;\n    if (body != null) {\n      if (typeof body !== \"object\" || Buffer.isBuffer(body)) {\n        return error(\n          reply,\n          \"Request body must be empty\",\n          StatusCodes.BAD_REQUEST,\n        );\n      }\n\n      if (Object.keys(body as Record<string, unknown>).length > 0) {\n        return error(\n          reply,\n          \"Request body must be empty\",\n          StatusCodes.BAD_REQUEST,\n        );\n      }\n    }\n\n    const { id: sessionId } = request.params as Api.SessionIdParams;\n    const sessionStore = getSessionStore();\n    const hasSession = await sessionStore.hasSession(sessionId);\n    if (!hasSession) {\n      return error(reply, \"Session not found\", StatusCodes.NOT_FOUND);\n    }\n    await sessionStore.endSession(sessionId);\n\n    return reply.status(StatusCodes.OK).send({ success: true });\n  },\n);\n\nconst endRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/:id/end\",\n  schema: {\n    ...Api.Operations.SessionEnd,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    response: {\n      200: Api.SessionEndResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: endRouteHandler,\n};\n\nexport default endRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/extract.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { ZodTypeAny } from \"zod/v3\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { AppError, withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { createStreamingResponse } from \"../../../../lib/stream.js\";\nimport { jsonSchemaToZod } from \"../../../../lib/utils.js\";\nimport { getSessionStore } from \"../../../../lib/sessionStoreManager.js\";\n\nconst extractRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return reply\n        .status(StatusCodes.UNAUTHORIZED)\n        .send({ error: \"Unauthorized\" });\n    }\n\n    const { id } = request.params as Api.SessionIdParams;\n\n    if (!id.length) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        message: \"Missing session id\",\n      });\n    }\n\n    const sessionStore = getSessionStore();\n    const hasSession = await sessionStore.hasSession(id);\n    if (!hasSession) {\n      return reply.status(StatusCodes.NOT_FOUND).send({\n        message: \"Session not found\",\n      });\n    }\n\n    return createStreamingResponse<Api.ExtractRequest>({\n      sessionId: id,\n      request,\n      reply,\n      schema: Api.ExtractRequestSchema,\n      handler: async ({ stagehand, data }) => {\n        const { frameId } = data;\n        const page = frameId\n          ? stagehand.context.resolvePageByMainFrameId(frameId)\n          : await stagehand.context.awaitActivePage();\n\n        if (!page) {\n          throw new AppError(\n            \"Page not found\",\n            StatusCodes.INTERNAL_SERVER_ERROR,\n          );\n        }\n\n        const modelOpt = data.options?.model;\n        const normalizedModel =\n          typeof modelOpt === \"string\"\n            ? { modelName: modelOpt }\n            : modelOpt\n              ? { ...modelOpt, modelName: modelOpt.modelName ?? \"gpt-4o\" }\n              : undefined;\n\n        const safeOptions = {\n          ...data.options,\n          model: normalizedModel,\n          page,\n        };\n\n        const extractFn = stagehand.extract.bind(stagehand);\n\n        let result: unknown;\n\n        if (data.instruction) {\n          if (data.schema) {\n            const zodSchema = jsonSchemaToZod(data.schema) as ZodTypeAny;\n            result = await extractFn(data.instruction, zodSchema, safeOptions);\n          } else {\n            result = await extractFn(data.instruction, safeOptions);\n          }\n        } else {\n          result = await extractFn(safeOptions);\n        }\n\n        return { result };\n      },\n      operation: \"extract\",\n    });\n  },\n);\n\nconst extractRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/:id/extract\",\n  schema: {\n    ...Api.Operations.SessionExtract,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    body: Api.ExtractRequestSchema,\n    response: {\n      200: Api.ExtractResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: extractRouteHandler,\n};\n\nexport default extractRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/navigate.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { AppError, withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { createStreamingResponse } from \"../../../../lib/stream.js\";\nimport { getSessionStore } from \"../../../../lib/sessionStoreManager.js\";\n\nconst navigateRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return reply\n        .status(StatusCodes.UNAUTHORIZED)\n        .send({ error: \"Unauthorized\" });\n    }\n\n    const { id } = request.params as Api.SessionIdParams;\n\n    if (!id.length) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        message: \"Missing session id\",\n      });\n    }\n\n    const sessionStore = getSessionStore();\n    const hasSession = await sessionStore.hasSession(id);\n    if (!hasSession) {\n      return reply.status(StatusCodes.NOT_FOUND).send({\n        message: \"Session not found\",\n      });\n    }\n\n    return createStreamingResponse<Api.NavigateRequest>({\n      sessionId: id,\n      request,\n      reply,\n      schema: Api.NavigateRequestSchema,\n      handler: async ({ stagehand, data }) => {\n        const page = data.frameId\n          ? stagehand.context.resolvePageByMainFrameId(data.frameId)\n          : await stagehand.context.awaitActivePage();\n\n        if (!page) {\n          throw new AppError(\"Page not found\", StatusCodes.NOT_FOUND);\n        }\n\n        const result = await page.goto(data.url, data.options);\n\n        return { result };\n      },\n      operation: \"navigate\",\n    });\n  },\n);\n\nconst navigateRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/:id/navigate\",\n  schema: {\n    ...Api.Operations.SessionNavigate,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    body: Api.NavigateRequestSchema,\n    response: {\n      200: Api.NavigateResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: navigateRouteHandler,\n};\n\nexport default navigateRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/observe.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { Action } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { AppError, withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { createStreamingResponse } from \"../../../../lib/stream.js\";\nimport { getSessionStore } from \"../../../../lib/sessionStoreManager.js\";\n\nconst observeRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return reply\n        .status(StatusCodes.UNAUTHORIZED)\n        .send({ error: \"Unauthorized\" });\n    }\n\n    const { id } = request.params as Api.SessionIdParams;\n\n    if (!id.length) {\n      return reply.status(StatusCodes.BAD_REQUEST).send({\n        message: \"Missing session id\",\n      });\n    }\n\n    const sessionStore = getSessionStore();\n    const hasSession = await sessionStore.hasSession(id);\n    if (!hasSession) {\n      return reply.status(StatusCodes.NOT_FOUND).send({\n        message: \"Session not found\",\n      });\n    }\n\n    return createStreamingResponse<Api.ObserveRequest>({\n      sessionId: id,\n      request,\n      reply,\n      schema: Api.ObserveRequestSchema,\n      handler: async ({ stagehand, data }) => {\n        const { frameId } = data;\n        const page = frameId\n          ? stagehand.context.resolvePageByMainFrameId(frameId)\n          : await stagehand.context.awaitActivePage();\n\n        if (!page) {\n          throw new AppError(\n            \"Page not found\",\n            StatusCodes.INTERNAL_SERVER_ERROR,\n          );\n        }\n\n        const safeOptions = {\n          ...data.options,\n          model:\n            typeof data.options?.model === \"string\"\n              ? { modelName: data.options.model }\n              : data.options?.model\n                ? {\n                    ...data.options.model,\n                    modelName: data.options.model.modelName ?? \"gpt-4o\",\n                  }\n                : undefined,\n          page,\n        };\n\n        let result: Action[];\n\n        if (data.instruction) {\n          result = await stagehand.observe(data.instruction, safeOptions);\n        } else {\n          result = await stagehand.observe(safeOptions);\n        }\n\n        return { result };\n      },\n      operation: \"observe\",\n    });\n  },\n);\n\nconst observeRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/:id/observe\",\n  schema: {\n    ...Api.Operations.SessionObserve,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    body: Api.ObserveRequestSchema,\n    response: {\n      200: Api.ObserveResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: observeRouteHandler,\n};\n\nexport default observeRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/_id/replay.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport { authMiddleware } from \"../../../../lib/auth.js\";\nimport { withErrorHandling } from \"../../../../lib/errorHandler.js\";\nimport { error, success } from \"../../../../lib/response.js\";\n\nconst replayRouteHandler: RouteHandlerMethod = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return error(reply, \"Unauthorized\", StatusCodes.UNAUTHORIZED);\n    }\n\n    reply.log.warn(\"Replay endpoint not implemented for local server.\");\n\n    const replay: Api.ReplayResult = {\n      pages: [],\n    };\n\n    return success(reply, replay);\n  },\n);\n\nconst replayRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/sessions/:id/replay\",\n  schema: {\n    ...Api.Operations.SessionReplay,\n    headers: Api.SessionHeadersSchema,\n    params: Api.SessionIdParamsSchema,\n    response: {\n      200: Api.ReplayResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: replayRouteHandler,\n};\n\nexport default replayRoute;\n"
  },
  {
    "path": "packages/server-v3/src/routes/v1/sessions/start.ts",
    "content": "import type { RouteHandler, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport Browserbase from \"@browserbasehq/sdk\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { SessionRetrieveResponse } from \"@browserbasehq/sdk/resources/sessions/sessions\";\nimport { type FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\nimport { z } from \"zod/v4\";\n\nimport { authMiddleware } from \"../../../lib/auth.js\";\nimport { withErrorHandling } from \"../../../lib/errorHandler.js\";\nimport { getModelApiKey, getOptionalHeader } from \"../../../lib/header.js\";\nimport { error, success } from \"../../../lib/response.js\";\nimport { getSessionStore } from \"../../../lib/sessionStoreManager.js\";\nimport { AISDK_PROVIDERS } from \"../../../types/model.js\";\n\n// Extended schema with custom refinement for local browser validation\nconst startBodySchema = z\n  .preprocess((value) => {\n    if (!value || typeof value !== \"object\") {\n      return value;\n    }\n    const record = value as Record<string, unknown>;\n    if (\n      typeof record.verbose === \"string\" &&\n      [\"0\", \"1\", \"2\"].includes(record.verbose)\n    ) {\n      return { ...record, verbose: Number(record.verbose) };\n    }\n    return value;\n  }, Api.SessionStartRequestSchema)\n  .superRefine((value, ctx) => {\n    if (value.browser?.type === \"local\") {\n      const hasConnect = Boolean(value.browser.cdpUrl);\n      const hasLaunch = Boolean(value.browser.launchOptions);\n      if (!hasConnect && !hasLaunch) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          path: [\"browser\"],\n          message:\n            \"When browser.type is 'local', provide either browser.cdpUrl or browser.launchOptions.\",\n        });\n      }\n    }\n  });\n\nconst startRouteHandler: RouteHandler = withErrorHandling(\n  async (request, reply) => {\n    if (!(await authMiddleware(request))) {\n      return error(reply, \"Unauthorized\", StatusCodes.UNAUTHORIZED);\n    }\n\n    const sdkVersion = getOptionalHeader(request, \"x-sdk-version\");\n\n    const clientLanguage = request.headers[\"x-language\"] as string | undefined;\n    if (\n      clientLanguage &&\n      ![\"typescript\", \"python\", \"playground\"].includes(clientLanguage)\n    ) {\n      return error(\n        reply,\n        \"Invalid client language header\",\n        StatusCodes.BAD_REQUEST,\n      );\n    }\n\n    // Use the validated request body directly - fields come from Api.SessionStartRequestSchema\n    const body = request.body as Api.SessionStartRequest;\n    const {\n      modelName,\n      domSettleTimeoutMs,\n      verbose,\n      systemPrompt,\n      browserbaseSessionCreateParams,\n      selfHeal,\n      waitForCaptchaSolves,\n      browserbaseSessionID,\n      experimental,\n      browser,\n    } = body;\n    if (!modelName) {\n      return error(reply, \"Missing required model name\");\n    }\n\n    // TODO: Remove this after complete AISDK migration. Validation should be done stagehand-side\n    if (modelName.includes(\"/\")) {\n      const [providerName] = modelName.split(\"/\", 1);\n      if (!providerName) {\n        return error(\n          reply,\n          `Invalid model: ${modelName}`,\n          StatusCodes.BAD_REQUEST,\n        );\n      }\n      if (!(AISDK_PROVIDERS as readonly string[]).includes(providerName)) {\n        return error(\n          reply,\n          `Invalid provider: ${providerName}`,\n          StatusCodes.BAD_REQUEST,\n        );\n      }\n    }\n\n    const browserType = browser?.type ?? \"browserbase\";\n\n    let bbApiKey: string | undefined;\n    let bbProjectId: string | undefined;\n    let browserbaseSessionId: string | undefined;\n    let connectUrl: string | undefined;\n\n    if (browserType === \"browserbase\") {\n      bbApiKey = getOptionalHeader(request, \"x-bb-api-key\");\n      bbProjectId = getOptionalHeader(request, \"x-bb-project-id\");\n\n      if (!bbApiKey) {\n        return error(\n          reply,\n          \"Missing required headers for browserbase sessions\",\n        );\n      }\n\n      const bb = new Browserbase({ apiKey: bbApiKey });\n\n      if (browserbaseSessionID) {\n        const existing = await bb.sessions.retrieve(browserbaseSessionID);\n        browserbaseSessionId = existing?.id;\n        connectUrl = existing?.connectUrl;\n        if (!browserbaseSessionId) {\n          return error(reply, \"Failed to retrieve browserbase session\");\n        }\n        if (!connectUrl) {\n          return error(reply, \"Browserbase session missing connectUrl\");\n        }\n      } else {\n        const resolvedProjectId =\n          browserbaseSessionCreateParams?.projectId ?? bbProjectId;\n        const createPayload = {\n          ...(resolvedProjectId ? { projectId: resolvedProjectId } : {}),\n          ...browserbaseSessionCreateParams,\n          browserSettings: {\n            ...(browserbaseSessionCreateParams?.browserSettings ?? {}),\n            viewport: browserbaseSessionCreateParams?.browserSettings\n              ?.viewport ?? {\n              width: 1288,\n              height: 711,\n            },\n          },\n          userMetadata: {\n            ...(browserbaseSessionCreateParams?.userMetadata ?? {}),\n            stagehand: \"true\",\n          },\n        } satisfies Browserbase.Sessions.SessionCreateParams;\n\n        const created = (await bb.sessions.create(\n          createPayload,\n        )) as SessionRetrieveResponse;\n\n        browserbaseSessionId = created?.id;\n        connectUrl = created?.connectUrl;\n        if (!browserbaseSessionId) {\n          return error(reply, \"Failed to create browserbase session\");\n        }\n        if (!connectUrl) {\n          return error(reply, \"Browserbase session missing connectUrl\");\n        }\n      }\n    }\n\n    const sessionStore = getSessionStore();\n\n    // For local browsers without a connectUrl, get it from browser.connectUrl\n    if (browserType === \"local\") {\n      connectUrl = browser?.cdpUrl;\n    }\n\n    const session = await sessionStore.startSession({\n      browserType,\n      connectUrl,\n      browserbaseSessionID:\n        browserType === \"browserbase\"\n          ? (browserbaseSessionId ?? browserbaseSessionID)\n          : undefined,\n      browserbaseApiKey: bbApiKey,\n      browserbaseProjectId: bbProjectId,\n      modelName,\n      domSettleTimeoutMs,\n      verbose,\n      systemPrompt,\n      browserbaseSessionCreateParams,\n      selfHeal,\n      waitForCaptchaSolves,\n      clientLanguage,\n      sdkVersion,\n      experimental,\n      localBrowserLaunchOptions:\n        browserType === \"local\" && (browser?.launchOptions || browser?.cdpUrl)\n          ? {\n              cdpUrl: browser?.cdpUrl,\n              ...(browser?.launchOptions ?? {}),\n            }\n          : undefined,\n    });\n\n    // For local browsers with launchOptions (no explicit cdpUrl), eagerly\n    // initialize the browser so we can return the actual CDP URL\n    let finalCdpUrl = connectUrl ?? session.cdpUrl ?? \"\";\n    if (browserType === \"local\" && browser?.launchOptions && !browser?.cdpUrl) {\n      const modelApiKey = getModelApiKey(request);\n      try {\n        const stagehand = await sessionStore.getOrCreateStagehand(\n          session.sessionId,\n          { modelApiKey },\n        );\n        finalCdpUrl = stagehand.connectURL();\n      } catch (err) {\n        request.log.error(\n          {\n            err,\n            sessionId: session.sessionId,\n            browserType,\n            chromePathEnv: process.env.CHROME_PATH,\n            launchOptions: {\n              executablePath: browser.launchOptions.executablePath,\n              argsCount: browser.launchOptions.args?.length ?? 0,\n              headless: browser.launchOptions.headless,\n              hasUserDataDir: Boolean(browser.launchOptions.userDataDir),\n              port: browser.launchOptions.port,\n              connectTimeoutMs: browser.launchOptions.connectTimeoutMs,\n            },\n          },\n          \"Failed to initialize local browser session in /v1/sessions/start\",\n        );\n        throw err;\n      }\n    }\n\n    return success(reply, {\n      sessionId: session.sessionId,\n      available: session.available,\n      cdpUrl: finalCdpUrl,\n    });\n  },\n);\n\nconst startRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/sessions/start\",\n  schema: {\n    ...Api.Operations.SessionStart,\n    headers: Api.SessionHeadersSchema,\n    body: startBodySchema,\n    response: {\n      200: Api.SessionStartResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: startRouteHandler,\n};\n\nexport default startRoute;\n"
  },
  {
    "path": "packages/server-v3/src/sea-entry.ts",
    "content": "import { __internalMaybeRunShutdownSupervisorFromArgv } from \"@browserbasehq/stagehand\";\n\n// if SEA binary is launched with --supervisor, it will run the shutdown supervisor only\nconst argv = process.argv.slice(1);\nconst normalizedArgv = argv[0]?.startsWith(\"--\") ? argv : argv.slice(1);\n\n// otherwise, start the stagehand/server\nif (!__internalMaybeRunShutdownSupervisorFromArgv(normalizedArgv)) {\n  void import(\"./server.js\").catch((err) => {\n    console.error(\"Failed to start server:\", err);\n    process.exit(1);\n  });\n}\n"
  },
  {
    "path": "packages/server-v3/src/server.ts",
    "content": "import { randomUUID } from \"crypto\";\n\nimport cors from \"@fastify/cors\";\nimport fastify from \"fastify\";\nimport metricsPlugin from \"fastify-metrics\";\nimport fastifySwagger from \"@fastify/swagger\";\nimport fastifySwaggerUI from \"@fastify/swagger-ui\";\nimport {\n  fastifyZodOpenApiPlugin,\n  fastifyZodOpenApiTransformers,\n  serializerCompiler,\n  validatorCompiler,\n  type FastifyZodOpenApiTypeProvider,\n  RequestValidationError,\n  ResponseSerializationError,\n} from \"fastify-zod-openapi\";\nimport { StatusCodes } from \"http-status-codes\";\n\nimport { logging } from \"./lib/logging/index.js\";\nimport {\n  destroySessionStore,\n  initializeSessionStore,\n} from \"./lib/sessionStoreManager.js\";\nimport healthcheckRoute from \"./routes/healthcheck.js\";\nimport readinessRoute, { setReady, setUnready } from \"./routes/readiness.js\";\nimport actRoute from \"./routes/v1/sessions/_id/act.js\";\nimport agentExecuteRoute from \"./routes/v1/sessions/_id/agentExecute.js\";\nimport endRoute from \"./routes/v1/sessions/_id/end.js\";\nimport extractRoute from \"./routes/v1/sessions/_id/extract.js\";\nimport navigateRoute from \"./routes/v1/sessions/_id/navigate.js\";\nimport observeRoute from \"./routes/v1/sessions/_id/observe.js\";\nimport replayRoute from \"./routes/v1/sessions/_id/replay.js\";\nimport startRoute from \"./routes/v1/sessions/start.js\";\n\n// Constants for graceful shutdown\nconst READY_WAIT_PERIOD = 10_000; // 10 seconds\nconst GRACEFUL_SHUTDOWN_PERIOD = 30_000; // 30 seconds\n\nconst usePrettyLogs = process.env.NODE_ENV === \"development\" && !process.env.CI;\n\nconst app = fastify({\n  disableRequestLogging: true,\n\n  genReqId: () => {\n    return randomUUID();\n  },\n\n  logger: {\n    formatters: {\n      level(label: string) {\n        return { level: label };\n      },\n    },\n\n    level: process.env.NODE_ENV === \"production\" ? \"info\" : \"trace\",\n\n    ...(usePrettyLogs && {\n      transport: {\n        options: {\n          colorize: true,\n          ignore: \"pid,hostname\",\n        },\n        target: \"pino-pretty\",\n      },\n    }),\n  },\n\n  return503OnClosing: false,\n});\n\nexport const logger = app.log;\n\n// Allow requests with `Content-Type: application/json` and an empty body (0 bytes).\n// Some clients always send the header even when there is no request body (e.g. /end).\nconst defaultJsonParser = app.getDefaultJsonParser(\"error\", \"error\");\napp.addContentTypeParser<string>(\n  \"application/json\",\n  { parseAs: \"string\" },\n  (request, body, done) => {\n    if (body === \"\" || (Buffer.isBuffer(body) && body.length === 0)) {\n      done(null, {});\n      return;\n    }\n\n    void defaultJsonParser(request, body, done);\n  },\n);\n\nprocess.on(\"uncaughtException\", (error) => {\n  app.log.error(error, \"Uncaught Exception:\");\n});\n\nprocess.on(\"unhandledRejection\", (reason, promise) => {\n  app.log.error(\n    reason instanceof Error ? reason : new Error(String(reason)),\n    \"Unhandled Rejection at:\",\n    promise,\n    \"reason:\",\n    reason,\n  );\n});\n\n// Graceful shutdown handler\nconst gracefulShutdown = async () => {\n  app.log.info(\"gracefulShutdown\");\n\n  setUnready();\n\n  await new Promise((resolve) => setTimeout(resolve, READY_WAIT_PERIOD));\n\n  const timeout = setTimeout(() => {\n    app.log.warn(\"forcefully shutting down after 30 seconds\");\n    process.exit(1);\n  }, GRACEFUL_SHUTDOWN_PERIOD);\n\n  timeout.unref();\n\n  await app.close();\n  await destroySessionStore();\n  clearTimeout(timeout);\n\n  app.log.info(\"gracefulShutdown complete\");\n  process.exit(0);\n};\n\n// Handle termination signals\nprocess.on(\"SIGTERM\", () => {\n  gracefulShutdown().catch((err: unknown) => {\n    app.log.error(err, \"error gracefully shutting down\");\n  });\n});\n\nprocess.on(\"SIGINT\", () => {\n  gracefulShutdown().catch((err: unknown) => {\n    app.log.error(err, \"error gracefully shutting down\");\n  });\n});\n\nconst start = async () => {\n  try {\n    if (process.env.NODE_ENV === \"development\") {\n      await app.register(cors, {\n        origin: [\"http://localhost:3000\"],\n        methods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"],\n        allowedHeaders: [\"*\"],\n        credentials: true,\n      });\n    }\n\n    app.setValidatorCompiler(validatorCompiler);\n    app.setSerializerCompiler(serializerCompiler);\n\n    await app.register(fastifyZodOpenApiPlugin);\n\n    await app.register(fastifySwagger, {\n      openapi: {\n        info: {\n          title: \"Stagehand API\",\n          version: \"3.0.5\",\n        },\n        openapi: \"3.1.0\",\n      },\n      ...fastifyZodOpenApiTransformers,\n    });\n\n    // Only register Swagger UI in development - SEA binaries can't load static files\n    if (process.env.NODE_ENV === \"development\") {\n      await app.register(fastifySwaggerUI, {\n        routePrefix: \"/documentation\",\n      });\n    }\n\n    app.setSchemaErrorFormatter(function (errors, dataVar) {\n      const zodIssues = errors\n        .filter((err) => err instanceof RequestValidationError)\n        .map((err) => err.params.issue);\n      this.log.warn({ dataVar, zodIssues }, \"request validation failed\");\n      return new Error(`${dataVar} validation failed`);\n    });\n\n    app.setErrorHandler((error, request, reply) => {\n      if ((error as { validation?: unknown }).validation) {\n        const zodIssues = (error as { validation: unknown[] }).validation\n          .filter((err) => err instanceof RequestValidationError)\n          .map((err) => (err as RequestValidationError).params.issue);\n\n        request.log.warn({ zodIssues }, \"request validation failed\");\n        return reply.status(StatusCodes.BAD_REQUEST).send({\n          error: \"Request validation failed\",\n          issues: zodIssues,\n        });\n      }\n\n      if (error instanceof ResponseSerializationError) {\n        request.log.error({ err: error }, \"response serialization failed\");\n        return reply\n          .status(StatusCodes.INTERNAL_SERVER_ERROR)\n          .send({ error: \"Response validation failed\" });\n      }\n\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      request.log.error(`Server error: ${errorMessage}`);\n\n      const statusCode =\n        (error as { statusCode?: number }).statusCode ??\n        StatusCodes.INTERNAL_SERVER_ERROR;\n\n      reply.status(statusCode).send({\n        error:\n          statusCode === Number(StatusCodes.INTERNAL_SERVER_ERROR)\n            ? \"Internal Server Error\"\n            : errorMessage,\n        statusCode,\n      });\n    });\n\n    await app.register(metricsPlugin, {\n      defaultMetrics: {\n        enabled: true,\n        prefix: \"stagehand_api_\",\n      },\n      routeMetrics: {\n        overrides: {\n          histogram: {\n            name: \"stagehand_api_http_request_duration_seconds\",\n          },\n          summary: {\n            name: \"stagehand_api_http_request_summary_seconds\",\n          },\n        },\n      },\n    });\n\n    initializeSessionStore();\n\n    const appWithTypes = app.withTypeProvider<FastifyZodOpenApiTypeProvider>();\n\n    await appWithTypes.register(\n      (instance, _opts, done) => {\n        instance.route(actRoute);\n        instance.route(endRoute);\n        instance.route(extractRoute);\n        instance.route(navigateRoute);\n        instance.route(observeRoute);\n        instance.route(replayRoute);\n        instance.route(startRoute);\n        instance.route(agentExecuteRoute);\n        done();\n      },\n      { prefix: \"/v1\" },\n    );\n\n    logging(app);\n\n    // Register health and readiness routes at the root level\n    appWithTypes.route(healthcheckRoute);\n    appWithTypes.route(readinessRoute);\n    await app.ready();\n\n    await app.listen({\n      host: \"0.0.0.0\",\n      port: parseInt(process.env.PORT ?? \"3000\", 10),\n    });\n    console.log(\"Routes registered:\", app.printRoutes());\n\n    // Mark the server as ready after it's started\n    setReady();\n  } catch (err) {\n    console.error(\"Failed to start server:\", err);\n    process.exit(1);\n  }\n};\n\nstart().catch((err: unknown) => {\n  console.error(\"Failed to start server:\", err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/server-v3/src/types/error.ts",
    "content": "import { StatusCodes } from \"http-status-codes\";\n\nimport { AppError } from \"../lib/errorHandler.js\";\n\nexport class UnknownModelError extends AppError {\n  constructor(model: string) {\n    super(`Unknown model: ${model}`, StatusCodes.BAD_REQUEST);\n  }\n}\nexport class InvalidProviderError extends AppError {\n  constructor(provider: string) {\n    super(`Invalid provider: ${provider}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class InvalidModelError extends AppError {\n  constructor(model: string) {\n    super(`Invalid model: ${model}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class UnauthorizedError extends AppError {\n  constructor() {\n    super(\"Unauthorized\", StatusCodes.UNAUTHORIZED);\n  }\n}\n\nexport class MissingHeaderError extends AppError {\n  constructor(header: string) {\n    super(`Missing required header: ${header}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class InvalidAPIKeyError extends AppError {\n  constructor(provider: string) {\n    super(`Invalid API key for provider: ${provider}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class AttemptedCloseOnNonActiveSessionError extends AppError {\n  constructor() {\n    super(\n      \"Attempted to close session that is not currently active\",\n      StatusCodes.CONFLICT,\n    );\n  }\n}\n\ninterface BrowserbaseError {\n  status?: number;\n  statusCode?: number;\n  message?: string;\n  response?: {\n    status?: number;\n    data?: {\n      message?: string;\n    };\n  };\n}\n\nexport class BrowserbaseSDKError extends AppError {\n  constructor(error: unknown, defaultMessage: string) {\n    const browserbaseError = error as BrowserbaseError;\n    const {\n      message: errMessage,\n      status,\n      statusCode: errStatusCode,\n      response,\n    } = browserbaseError;\n\n    let message = defaultMessage;\n    let finalStatusCode = StatusCodes.BAD_REQUEST;\n\n    // Extract message from error\n    if (errMessage) {\n      message = errMessage;\n    } else if (response?.data?.message) {\n      ({ message } = response.data);\n    }\n\n    // Extract status code from error\n    if (status && typeof status === \"number\") {\n      finalStatusCode = status as StatusCodes;\n    } else if (errStatusCode && typeof errStatusCode === \"number\") {\n      finalStatusCode = errStatusCode as StatusCodes;\n    } else if (response?.status && typeof response.status === \"number\") {\n      finalStatusCode = response.status as StatusCodes;\n    }\n\n    // Check for specific session error\n    if (message.includes(\"is not running\")) {\n      throw new AttemptedCloseOnNonActiveSessionError();\n    }\n\n    // Mark 5xx errors as internal to sanitize sensitive details\n    const isInternal =\n      Number(finalStatusCode) >= Number(StatusCodes.INTERNAL_SERVER_ERROR);\n\n    super(message, finalStatusCode, isInternal);\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/src/types/fastify.d.ts",
    "content": "import \"fastify\";\n\ndeclare module \"fastify\" {\n  interface FastifyRequest {\n    metrics: {\n      startTime: number;\n    };\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/src/types/model.ts",
    "content": "export const AISDK_PROVIDERS = [\n  \"openai\",\n  \"anthropic\",\n  \"google\",\n  \"xai\",\n  \"azure\",\n  \"groq\",\n  \"cerebras\",\n  \"togetherai\",\n  \"mistral\",\n  \"deepseek\",\n  \"perplexity\",\n  \"ollama\",\n  \"vertex\",\n  \"bedrock\",\n] as const;\nexport type AISDKProvider = (typeof AISDK_PROVIDERS)[number];\n\nexport type LegacyModel =\n  | \"gpt-4o\"\n  | \"gpt-4o-mini\"\n  | \"gpt-4o-2024-08-06\"\n  | \"gpt-4o-2024-05-13\"\n  | \"cerebras-llama-3.3-70b\"\n  | \"cerebras-llama-3.1-8b\"\n  | \"o1-mini\"\n  | \"o1-preview\"\n  | \"o3-mini\"\n  | \"gpt-4.5-preview\"\n  | \"groq-llama-3.3-70b-specdec\"\n  | \"groq-llama-3.3-70b-versatile\"\n  | \"gemini-1.5-flash\"\n  | \"gemini-1.5-pro\"\n  | \"gemini-1.5-flash-8b\"\n  | \"gemini-2.0-flash-lite\"\n  | \"gemini-2.0-flash\"\n  | \"gemini-2.5-pro-preview-03-25\"\n  | \"gemini-2.5-flash-preview-04-17\";\n\nexport type LegacyProvider = \"openai\" | \"anthropic\" | \"google\";\n"
  },
  {
    "path": "packages/server-v3/src/types/rrweb.ts",
    "content": "export interface Node {\n  type: string;\n  tagName?: string;\n  attributes?: Record<string, string>;\n  childNodes?: Node[];\n  textContent?: string;\n  id: number;\n}\n\nexport interface Event {\n  type: number;\n  /*\n  The data object is different for each event type\n  but we're only accessing it when the data follows\n  this structure, so we can just type this way.\n  */\n  data: { node: Node };\n  sessionId?: string;\n  timestamp: Date;\n  actionId: string;\n}\n"
  },
  {
    "path": "packages/server-v3/test/integration/api-server-cache.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { after, before, describe, it } from \"node:test\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  createSession,\n  endSession,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  HTTP_OK,\n  navigateSession,\n} from \"./utils.js\";\n\n// Shared read-only session — extract is safe to re-use across tests.\nlet sessionId: string;\n\nbefore(async () => {\n  sessionId = await createSession(getHeaders(\"3.0.0\"));\n  const nav = await navigateSession(\n    sessionId,\n    \"https://example.com\",\n    getHeaders(\"3.0.0\"),\n  );\n  assert.equal(nav.status, HTTP_OK, \"Navigate should succeed\");\n});\n\nafter(async () => {\n  await endSession(sessionId, getHeaders(\"3.0.0\"));\n});\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction extractUrl() {\n  return `${getBaseUrl()}/v1/sessions/${sessionId}/extract`;\n}\n\nfunction extractBody(instruction = \"extract the page title\") {\n  return JSON.stringify({ instruction });\n}\n\n// ---------------------------------------------------------------------------\n// browserbase-cache-bypass request header\n// ---------------------------------------------------------------------------\n\ndescribe(\"browserbase-cache-bypass request header\", () => {\n  it(\"request with bypass header does not return cache HIT\", async () => {\n    const ctx = await fetchWithContext(extractUrl(), {\n      method: \"POST\",\n      headers: {\n        ...getHeaders(\"3.0.0\"),\n        \"browserbase-cache-bypass\": \"true\",\n      },\n      body: extractBody(),\n    });\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract with bypass should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n\n    const cacheStatus = ctx.headers.get(\"browserbase-cache-status\");\n    assert.notEqual(\n      cacheStatus,\n      \"HIT\",\n      \"A bypassed request must not return a cache HIT\",\n    );\n  });\n});\n\n// ---------------------------------------------------------------------------\n// browserbase-cache-status response header\n// ---------------------------------------------------------------------------\n\ndescribe(\"browserbase-cache-status response header\", () => {\n  it(\"returns HIT or MISS when the header is present\", async () => {\n    const ctx = await fetchWithContext(extractUrl(), {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: extractBody(),\n    });\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract should succeed\");\n\n    const cacheStatus = ctx.headers.get(\"browserbase-cache-status\");\n    if (cacheStatus !== null) {\n      assert.ok(\n        cacheStatus === \"HIT\" || cacheStatus === \"MISS\",\n        `browserbase-cache-status must be HIT or MISS, got: ${cacheStatus}`,\n      );\n    }\n  });\n\n  it(\"returns HIT on a repeated identical request when caching is active\", async () => {\n    const body = extractBody(\"count the number of links\");\n\n    // First call — warms the cache.\n    const first = await fetchWithContext(extractUrl(), {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body,\n    });\n    assertFetchStatus(first, HTTP_OK, \"First extract should succeed\");\n\n    // Second call — should be a HIT if server-side caching is enabled.\n    const second = await fetchWithContext(extractUrl(), {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body,\n    });\n    assertFetchStatus(second, HTTP_OK, \"Second extract should succeed\");\n\n    const cacheStatus = second.headers.get(\"browserbase-cache-status\");\n    if (cacheStatus !== null) {\n      assert.equal(\n        cacheStatus,\n        \"HIT\",\n        \"Repeated identical request should be a cache HIT\",\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/utils.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { chromium } from \"playwright\";\n\n// =============================================================================\n// HTTP Status Codes\n// =============================================================================\n\nexport const HTTP_OK = 200;\nexport const HTTP_BAD_REQUEST = 400;\nexport const HTTP_NOT_FOUND = 404;\nexport const HTTP_GONE = 410;\nexport const HTTP_UNPROCESSABLE_ENTITY = 422;\nexport const HTTP_INTERNAL_SERVER_ERROR = 500;\n\n// =============================================================================\n// Timing Constants\n// =============================================================================\n\nexport const SESSION_CLOSE_WAIT_MS = 2000;\n\n// =============================================================================\n// Environment Variables\n// =============================================================================\n\nexport const {\n  STAGEHAND_API_URL,\n  OPENAI_API_KEY,\n  GEMINI_API_KEY,\n  ANTHROPIC_API_KEY,\n} = process.env;\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\nexport function requireEnv(name: string, value: string | undefined): string {\n  if (!value) {\n    throw new Error(`Missing required environment variable: ${name}`);\n  }\n  return value;\n}\n\nexport function getBaseUrl(): string {\n  return STAGEHAND_API_URL ?? \"http://127.0.0.1:3107\";\n}\n\n// =============================================================================\n// Header Generators\n// =============================================================================\n\nexport function getHeaders(\n  sdkVersion: string,\n  language: string = \"typescript\",\n): Record<string, string> {\n  return {\n    \"Content-Type\": \"application/json\",\n    \"x-model-api-key\": requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY),\n    \"x-language\": language,\n    \"x-sdk-version\": sdkVersion,\n  };\n}\n\n// =============================================================================\n// Session Management\n// =============================================================================\n\nexport interface StartSessionResponse {\n  success: boolean;\n  message?: string;\n  data?: {\n    sessionId: string;\n    cdpUrl: string;\n    available: boolean;\n  };\n}\n\nconst SESSION_READY_DELAY_MS = 250;\nconst LOCAL_CONNECT_TIMEOUT_MS = (() => {\n  const parsed = Number(process.env.STAGEHAND_TEST_LOCAL_CONNECT_TIMEOUT_MS);\n  return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;\n})();\n\nexport interface SessionInfo {\n  sessionId: string;\n  cdpUrl: string;\n}\n\nfunction createLocalBrowserBody() {\n  const resolveChromePath = (): string => {\n    const explicit = process.env.CHROME_PATH;\n    if (explicit && fs.existsSync(explicit)) {\n      return explicit;\n    }\n    if (explicit) {\n      throw new Error(`CHROME_PATH does not exist: ${explicit}`);\n    }\n\n    const playwrightPath = chromium.executablePath();\n    if (playwrightPath && fs.existsSync(playwrightPath)) {\n      return playwrightPath;\n    }\n\n    throw new Error(\n      \"Unable to locate a Chrome executable. Set CHROME_PATH in the test environment.\",\n    );\n  };\n\n  return {\n    browser: {\n      type: \"local\",\n      launchOptions: {\n        headless: true,\n        executablePath: resolveChromePath(),\n        args: process.env.CI ? [\"--no-sandbox\"] : undefined,\n        connectTimeoutMs: LOCAL_CONNECT_TIMEOUT_MS,\n      },\n    },\n  };\n}\n\nexport const LOCAL_BROWSER_BODY = createLocalBrowserBody();\n\nfunction readLaunchDiagnostics(launchOptions?: {\n  executablePath?: string;\n  args?: string[];\n  headless?: boolean;\n  userDataDir?: string;\n  port?: number;\n  connectTimeoutMs?: number;\n}): string {\n  const diagnostics: string[] = [];\n  const userDataDir = launchOptions?.userDataDir;\n  diagnostics.push(\"--- launch diagnostics ---\");\n  diagnostics.push(`CHROME_PATH env: ${process.env.CHROME_PATH ?? \"<unset>\"}`);\n  diagnostics.push(`CI env: ${process.env.CI ?? \"<unset>\"}`);\n  diagnostics.push(`userDataDir: ${userDataDir ?? \"<auto>\"}`);\n  if (!userDataDir) {\n    diagnostics.push(\n      \"chrome stdout/stderr logs unavailable (profile dir auto-managed by server launch)\",\n    );\n  } else {\n    diagnostics.push(`userDataDir exists: ${fs.existsSync(userDataDir)}`);\n    if (fs.existsSync(userDataDir)) {\n      const outPath = path.join(userDataDir, \"chrome-out.log\");\n      const errPath = path.join(userDataDir, \"chrome-err.log\");\n      if (fs.existsSync(outPath)) {\n        diagnostics.push(\n          `--- chrome stdout ---\\n${fs.readFileSync(outPath, \"utf8\")}`,\n        );\n      }\n      if (fs.existsSync(errPath)) {\n        diagnostics.push(\n          `--- chrome stderr ---\\n${fs.readFileSync(errPath, \"utf8\")}`,\n        );\n      }\n    }\n  }\n  if (launchOptions) {\n    diagnostics.push(\n      `launch.executablePath: ${launchOptions.executablePath ?? \"<unset>\"}`,\n    );\n    diagnostics.push(\n      `launch.executablePath exists: ${\n        launchOptions.executablePath\n          ? fs.existsSync(launchOptions.executablePath)\n          : false\n      }`,\n    );\n    diagnostics.push(`launch.headless: ${String(launchOptions.headless)}`);\n    diagnostics.push(\n      `launch.args: ${JSON.stringify(launchOptions.args ?? [])}`,\n    );\n    diagnostics.push(`launch.port: ${launchOptions.port ?? \"<auto>\"}`);\n    diagnostics.push(\n      `launch.connectTimeoutMs: ${launchOptions.connectTimeoutMs ?? \"<default>\"}`,\n    );\n  }\n  return diagnostics.join(\"\\n\");\n}\n\nexport async function createSession(\n  headers: Record<string, string>,\n): Promise<string> {\n  const info = await createSessionWithCdp(headers);\n  return info.sessionId;\n}\n\nexport async function createSessionWithCdp(\n  headers: Record<string, string>,\n): Promise<SessionInfo> {\n  const url = getBaseUrl();\n  const startPayload = {\n    modelName: \"gpt-4.1-nano\",\n    ...createLocalBrowserBody(),\n  };\n\n  const response = await fetch(`${url}/v1/sessions/start`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify(startPayload),\n  });\n\n  const responseText = await response.text();\n  let parsedBody: unknown;\n  try {\n    parsedBody = responseText ? JSON.parse(responseText) : null;\n  } catch {\n    parsedBody = responseText;\n  }\n  const body = parsedBody as StartSessionResponse;\n\n  if (!response.ok || !body?.success) {\n    const launchDiagnostics = readLaunchDiagnostics(\n      startPayload.browser?.launchOptions,\n    );\n    throw new Error(\n      `Failed to create session (status=${response.status}): ${JSON.stringify(\n        parsedBody,\n      )}\\n${launchDiagnostics}`,\n    );\n  }\n  if (!body.data?.available) {\n    throw new Error(`Session not available`);\n  }\n  if (!body.data.sessionId) {\n    throw new Error(\"No sessionId returned\");\n  }\n  if (!body.data.cdpUrl) {\n    throw new Error(\"No cdpUrl returned\");\n  }\n\n  // Wait for session to be fully ready before returning\n  await new Promise((resolve) => setTimeout(resolve, SESSION_READY_DELAY_MS));\n\n  return {\n    sessionId: body.data.sessionId,\n    cdpUrl: body.data.cdpUrl,\n  };\n}\n\nexport async function endSession(\n  sessionId: string,\n  headers: Record<string, string>,\n): Promise<void> {\n  const url = getBaseUrl();\n\n  await fetch(`${url}/v1/sessions/${sessionId}/end`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify({}),\n  });\n}\n\n// =============================================================================\n// Navigation Helper\n// =============================================================================\n\nexport async function navigateSession(\n  sessionId: string,\n  targetUrl: string,\n  headers: Record<string, string>,\n): Promise<Response> {\n  const url = getBaseUrl();\n\n  return fetch(`${url}/v1/sessions/${sessionId}/navigate`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify({ url: targetUrl, frameId: \"\" }),\n  });\n}\n\n/**\n * Gets the main frame ID from a CDP session\n */\nexport async function getMainFrameId(cdpUrl: string): Promise<string> {\n  const browser = await chromium.connectOverCDP(cdpUrl);\n  try {\n    const contexts = browser.contexts();\n    if (contexts.length === 0) {\n      throw new Error(\"No browser contexts found\");\n    }\n    const pages = contexts[0]!.pages();\n    if (pages.length === 0) {\n      throw new Error(\"No pages found\");\n    }\n    const page = pages[0]!;\n\n    // Use CDP to get the frame tree and extract the main frame ID\n    const cdpSession = await page.context().newCDPSession(page);\n    const { frameTree } = await cdpSession.send(\"Page.getFrameTree\");\n    await cdpSession.detach();\n\n    return frameTree.frame.id;\n  } finally {\n    await browser.close();\n  }\n}\n\n// =============================================================================\n// SSE Stream Reader\n// =============================================================================\n\n// Legacy SSE event interface (generic)\nexport interface SSEEvent {\n  event?: string;\n  data?: string;\n  parsed?: unknown;\n}\n\nexport async function readSSEStream(response: Response): Promise<SSEEvent[]> {\n  const reader = response.body?.getReader() as\n    | ReadableStreamDefaultReader<Uint8Array>\n    | undefined;\n  if (!reader) {\n    throw new Error(\"No response body reader available\");\n  }\n\n  const decoder = new TextDecoder();\n  let fullResponse = \"\";\n\n  for (;;) {\n    const result = await reader.read();\n    if (result.done) break;\n    fullResponse += decoder.decode(result.value, { stream: true });\n  }\n\n  // Parse SSE events\n  const events: SSEEvent[] = [];\n  const rawEvents = fullResponse.split(\"\\n\\n\").filter((e) => e.trim());\n\n  for (const rawEvent of rawEvents) {\n    const event: SSEEvent = {};\n    const lines = rawEvent.split(\"\\n\");\n\n    for (const line of lines) {\n      if (line.startsWith(\"event:\")) {\n        event.event = line.slice(6).trim();\n      } else if (line.startsWith(\"data:\")) {\n        event.data = line.slice(5).trim();\n        try {\n          event.parsed = JSON.parse(event.data);\n        } catch {\n          // Keep as string if not valid JSON\n        }\n      }\n    }\n\n    if (event.data || event.event) {\n      events.push(event);\n    }\n  }\n\n  return events;\n}\n\n// =============================================================================\n// Typed SSE Event Helpers (for stagehand-api backend format)\n// =============================================================================\n\n// Actual SSE event format from backend (see stream.ts):\n// { data: { status: \"starting\" | \"connected\" | \"finished\", result?: ... }, type: \"system\" | \"log\", id: \"<uuid>\" }\nexport interface TypedSSEEvent<TResult = unknown> {\n  data: {\n    status: string;\n    result?: TResult;\n    message?: string;\n    error?: string;\n  };\n  type: string;\n  id: string;\n}\n\n/**\n * Read SSE stream from response and return raw string\n */\nexport async function readSSEStreamRaw(response: Response): Promise<string> {\n  const reader = response.body?.getReader() as\n    | ReadableStreamDefaultReader<Uint8Array>\n    | undefined;\n  if (!reader) throw new Error(\"No response body reader\");\n\n  const decoder = new TextDecoder();\n  let fullResponse = \"\";\n\n  for (;;) {\n    const result = await reader.read();\n    if (result.done) break;\n    fullResponse += decoder.decode(result.value, { stream: true });\n  }\n\n  return fullResponse;\n}\n\n/**\n * Parse raw SSE response string into typed events\n */\nexport function parseTypedSSEEvents<TResult = unknown>(\n  rawResponse: string,\n): TypedSSEEvent<TResult>[] {\n  const events = rawResponse.split(\"\\n\\n\").filter((e) => e.trim());\n  return events\n    .map((event) => {\n      const dataMatch = event.match(/data: (.+)/);\n      if (dataMatch?.[1]) {\n        return JSON.parse(dataMatch[1]) as TypedSSEEvent<TResult>;\n      }\n      return null;\n    })\n    .filter((e): e is TypedSSEEvent<TResult> => e !== null);\n}\n\n/**\n * Result of reading an SSE stream with full context for debugging\n */\nexport interface SSEStreamResult<TResult = unknown> {\n  /** HTTP status code */\n  status: number;\n  /** HTTP status text */\n  statusText: string;\n  /** Raw response body */\n  raw: string;\n  /** Parsed SSE events */\n  events: TypedSSEEvent<TResult>[];\n  /** Get debug summary for error messages */\n  debugSummary(): string;\n}\n\n/**\n * Read SSE stream and parse into typed events (legacy - no debug context)\n */\nexport async function readTypedSSEStream<TResult = unknown>(\n  response: Response,\n): Promise<TypedSSEEvent<TResult>[]> {\n  const raw = await readSSEStreamRaw(response);\n  return parseTypedSSEEvents<TResult>(raw);\n}\n\n/**\n * Read SSE stream with full context for debugging test failures.\n * Use this instead of readTypedSSEStream when you need better error messages.\n */\nexport async function readTypedSSEStreamWithContext<TResult = unknown>(\n  response: Response,\n): Promise<SSEStreamResult<TResult>> {\n  const status = response.status;\n  const statusText = response.statusText;\n  const raw = await readSSEStreamRaw(response);\n  const events = parseTypedSSEEvents<TResult>(raw);\n\n  return {\n    status,\n    statusText,\n    raw,\n    events,\n    debugSummary() {\n      const eventStatuses = events.map((e) => e.data.status).join(\" → \");\n      const errorEvents = events.filter((e) => e.data.status === \"error\");\n      const errorMessages = errorEvents\n        .map((e) => e.data.error ?? \"unknown error\")\n        .join(\", \");\n\n      let summary = `HTTP ${status} ${statusText}`;\n      if (events.length === 0) {\n        summary += `\\n  No SSE events received`;\n        summary += `\\n  Raw response: ${raw.slice(0, 500)}${raw.length > 500 ? \"...\" : \"\"}`;\n      } else {\n        summary += `\\n  Events (${events.length}): ${eventStatuses}`;\n        if (errorMessages) {\n          summary += `\\n  Errors: ${errorMessages}`;\n        }\n      }\n      return summary;\n    },\n  };\n}\n\n/**\n * Assert with debug context - includes SSE stream info on failure\n */\nexport function assertWithContext(\n  condition: boolean,\n  message: string,\n  context: SSEStreamResult<unknown>,\n): asserts condition {\n  if (!condition) {\n    throw new Error(`${message}\\n\\nDebug context:\\n${context.debugSummary()}`);\n  }\n}\n\n/**\n * Assert SSE event exists with debug context on failure, returns the found event\n */\nexport function assertEventExists<TResult>(\n  events: TypedSSEEvent<TResult>[],\n  status: string,\n  context: SSEStreamResult<TResult>,\n): TypedSSEEvent<TResult> {\n  const found = events.find((e) => e.data.status === status);\n  assertWithContext(\n    found !== undefined,\n    `Should have a \"${status}\" event`,\n    context,\n  );\n  return found;\n}\n\n/**\n * Assert HTTP status with debug context on failure\n */\nexport function assertHttpStatus(\n  context: SSEStreamResult<unknown>,\n  expectedStatus: number,\n  message?: string,\n): void {\n  assertWithContext(\n    context.status === expectedStatus,\n    message ?? `Expected HTTP ${expectedStatus}, got ${context.status}`,\n    context,\n  );\n}\n\n// =============================================================================\n// JSON Response Debug Utilities (for non-SSE tests)\n// =============================================================================\n\n/**\n * Result of a fetch request with full context for debugging\n */\nexport interface FetchResult<T = unknown> {\n  /** HTTP status code */\n  status: number;\n  /** HTTP status text */\n  statusText: string;\n  /** Parsed JSON body (if parseable) */\n  body: T | null;\n  /** Raw response text */\n  raw: string;\n  /** Request duration in ms */\n  durationMs: number;\n  /** Response headers */\n  headers: Headers;\n  /** Get debug summary for error messages */\n  debugSummary(): string;\n}\n\n/**\n * Fetch with full context for debugging test failures.\n * Captures timing, status, and response body.\n */\nexport async function fetchWithContext<T = unknown>(\n  url: string,\n  options: RequestInit,\n): Promise<FetchResult<T>> {\n  const startTime = Date.now();\n  let response: Response;\n\n  try {\n    response = await fetch(url, options);\n  } catch (err) {\n    const durationMs = Date.now() - startTime;\n    const errorMsg = err instanceof Error ? err.message : String(err);\n    return {\n      status: 0,\n      statusText: \"FETCH_ERROR\",\n      body: null,\n      raw: errorMsg,\n      durationMs,\n      headers: new Headers(),\n      debugSummary() {\n        return `Fetch failed after ${durationMs}ms: ${errorMsg}`;\n      },\n    };\n  }\n\n  const durationMs = Date.now() - startTime;\n  const status = response.status;\n  const statusText = response.statusText;\n  const headers = response.headers;\n  const raw = await response.text();\n\n  let body: T | null = null;\n  try {\n    body = JSON.parse(raw) as T;\n  } catch {\n    // Keep body as null if not valid JSON\n  }\n\n  return {\n    status,\n    statusText,\n    body,\n    raw,\n    durationMs,\n    headers,\n    debugSummary() {\n      const seconds = (durationMs / 1000).toFixed(1);\n      let summary = `HTTP ${status} ${statusText} (${seconds}s)`;\n\n      if (body && typeof body === \"object\") {\n        const b = body as Record<string, unknown>;\n        if (b.success === false && typeof b.message === \"string\") {\n          summary += `\\n  Error: ${b.message}`;\n        }\n        if (typeof b.error === \"string\") {\n          summary += `\\n  Error: ${b.error}`;\n        }\n      }\n\n      // Show raw response if it's an error or unexpected\n      if (status >= 400 || !body) {\n        const truncated = raw.slice(0, 500);\n        summary += `\\n  Response: ${truncated}${raw.length > 500 ? \"...\" : \"\"}`;\n      }\n\n      return summary;\n    },\n  };\n}\n\n/**\n * Assert with fetch context - includes response info on failure\n */\nexport function assertFetchOk<T>(\n  condition: boolean,\n  message: string,\n  context: FetchResult<T>,\n): asserts condition {\n  if (!condition) {\n    throw new Error(`${message}\\n\\nDebug context:\\n${context.debugSummary()}`);\n  }\n}\n\n/**\n * Assert fetch succeeded with expected status\n */\nexport function assertFetchStatus<T>(\n  context: FetchResult<T>,\n  expectedStatus: number,\n  message?: string,\n): void {\n  assertFetchOk(\n    context.status === expectedStatus,\n    message ?? `Expected HTTP ${expectedStatus}, got ${context.status}`,\n    context,\n  );\n}\n\n// =============================================================================\n// Test Context Manager\n// =============================================================================\n\nexport class TestSession {\n  public sessionId: string | null = null;\n  private headers: Record<string, string>;\n\n  constructor(headers: Record<string, string>) {\n    this.headers = headers;\n  }\n\n  async start(): Promise<string> {\n    this.sessionId = await createSession(this.headers);\n    return this.sessionId;\n  }\n\n  async navigate(targetUrl: string): Promise<Response> {\n    if (!this.sessionId) {\n      throw new Error(\"Session not started\");\n    }\n    return navigateSession(this.sessionId, targetUrl, this.headers);\n  }\n\n  async end(): Promise<void> {\n    if (this.sessionId) {\n      try {\n        await endSession(this.sessionId, this.headers);\n      } catch {\n        // Ignore errors when ending session\n      }\n      this.sessionId = null;\n    }\n  }\n\n  getSessionId(): string {\n    if (!this.sessionId) {\n      throw new Error(\"Session not started\");\n    }\n    return this.sessionId;\n  }\n}\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/act.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { after, before, beforeEach, describe, it } from \"node:test\";\n\nimport { chromium } from \"playwright\";\n\nimport {\n  assertEventExists,\n  assertFetchOk,\n  assertFetchStatus,\n  assertWithContext,\n  createSessionWithCdp,\n  endSession,\n  fetchWithContext,\n  GEMINI_API_KEY,\n  getBaseUrl,\n  getHeaders,\n  getMainFrameId,\n  HTTP_OK,\n  navigateSession,\n  OPENAI_API_KEY,\n  readTypedSSEStreamWithContext,\n  requireEnv,\n} from \"../utils.js\";\n\ninterface ActResponse {\n  success: boolean;\n  data?: {\n    result: { success: boolean; message?: string };\n    actionId?: string;\n  };\n}\n\n/** Result type for act SSE events */\ninterface ActResult {\n  success: boolean;\n  message?: string;\n  action?: string;\n}\n\n// Module-level session variable shared across all describe blocks\nlet sessionId: string;\nlet cdpUrl: string;\n\n// Single session creation for all tests\nbefore(async () => {\n  ({ sessionId, cdpUrl } = await createSessionWithCdp(getHeaders(\"3.0.0\")));\n});\n\n// Navigate back to example.com before each test since act() may navigate away\nbeforeEach(async () => {\n  const navResponse = await navigateSession(\n    sessionId,\n    \"https://example.com\",\n    getHeaders(\"3.0.0\"),\n  );\n  assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n});\n\n// Single session cleanup after all tests\nafter(async () => {\n  await endSession(sessionId, getHeaders(\"3.0.0\"));\n});\n\n// =============================================================================\n// POST /v1/sessions/:id/act (V3 Format)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/act (V3)\", () => {\n  // ===========================================================================\n  // V3 Format Tests\n  // ===========================================================================\n\n  it(\"should perform an action using string input format\", async () => {\n    const url = getBaseUrl();\n    const frameId = await getMainFrameId(cdpUrl);\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n        },\n        body: JSON.stringify({\n          input: \"click the Learn more link\",\n          frameId,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"act should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n\n    // Verify navigation via CDP\n    const browser = await chromium.connectOverCDP(cdpUrl);\n    const contexts = browser.contexts();\n    assert.ok(contexts.length > 0, \"Should have at least one browser context\");\n    const pages = contexts[0]!.pages();\n    assert.ok(pages.length > 0, \"Should have at least one page\");\n    const pageUrl = pages[0]!.url();\n    assert.ok(\n      pageUrl.includes(\"iana.org/help/example-domains\"),\n      `Page URL should be iana.org/help/example-domains, got: ${pageUrl}`,\n    );\n    await browser.close();\n  });\n\n  it(\"should perform an action using object input format\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n        },\n        body: JSON.stringify({\n          input: {\n            selector: \"a\",\n            description: \"Click a link on the page\",\n            method: \"click\",\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"act with object input should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n  });\n\n  it(\"should accept options with string input\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n        },\n        body: JSON.stringify({\n          input: \"click the Learn more link\",\n          options: {\n            timeout: 30000,\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"act with options should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n  });\n\n  // ===========================================================================\n  // V3 Inline Model Configuration Tests\n  // ===========================================================================\n\n  it(\"should perform action with inline model config (modelName + apiKey)\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n          \"x-model-api-key\": \"\", // Clear the header to ensure body config is used\n        },\n        body: JSON.stringify({\n          input: \"click the Learn more link\",\n          options: {\n            model: {\n              modelName: \"openai/gpt-4.1-nano\",\n              apiKey: openaiApiKey,\n            },\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"act with inline model config should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n  });\n\n  it(\"should perform action with inline model config and options\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n          \"x-model-api-key\": \"\", // Clear the header to ensure body config is used\n        },\n        body: JSON.stringify({\n          input: \"click the Learn more link\",\n          options: {\n            model: {\n              modelName: \"openai/gpt-4.1-nano\",\n              apiKey: openaiApiKey,\n            },\n            timeout: 30000,\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"act with inline model config and options should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n  });\n\n  it(\"should perform action with object input and inline model config\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n          \"x-model-api-key\": \"\", // Clear the header to ensure body config is used\n        },\n        body: JSON.stringify({\n          input: {\n            selector: \"a\",\n            description: \"Click a link on the page\",\n            method: \"click\",\n          },\n          options: {\n            model: {\n              modelName: \"openai/gpt-4.1-nano\",\n              apiKey: openaiApiKey,\n            },\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"act with object input and inline model config should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n  });\n\n  it(\"should perform action with google/gemini-2.5-flash-lite model\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<ActResponse>(\n      `${url}/v1/sessions/${sessionId}/act`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n          \"x-model-api-key\": \"\", // Clear the header to ensure body config is used\n        },\n        body: JSON.stringify({\n          input: \"click the Learn more link\",\n          options: {\n            model: {\n              modelName: \"google/gemini-2.5-flash-lite\",\n              apiKey: geminiApiKey,\n            },\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"act with google/gemini-2.5-flash-lite model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result.success,\n      \"boolean\",\n      \"Result should have success boolean\",\n    );\n  });\n});\n\n// =============================================================================\n// SSE Streaming Tests (V3)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/act with SSE streaming (V3)\", () => {\n  it(\"should stream valid SSE events with correct structure\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/act`, {\n      method: \"POST\",\n      headers: {\n        ...getHeaders(\"3.0.0\"),\n      },\n      body: JSON.stringify({\n        input: \"click the Learn more link\",\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ActResult>(response);\n    const { events } = ctx;\n\n    assertWithContext(\n      events.length >= 2,\n      \"Should have at least starting and finished events\",\n      ctx,\n    );\n\n    // Verify starting event\n    const startingEvent = assertEventExists(events, \"starting\", ctx);\n    assert.equal(\n      startingEvent.type,\n      \"system\",\n      \"Starting event should be system type\",\n    );\n\n    // Verify finished event with result\n    const finishedEvent = assertEventExists(events, \"finished\", ctx);\n    assert.equal(\n      finishedEvent.type,\n      \"system\",\n      \"Finished event should be system type\",\n    );\n    assertWithContext(\n      !!finishedEvent.data.result,\n      \"Finished event must have result\",\n      ctx,\n    );\n    assert.equal(\n      typeof finishedEvent.data.result.success,\n      \"boolean\",\n      \"Result.success must be a boolean\",\n    );\n  });\n\n  it(\"should stream SSE events with inline model config\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/act`, {\n      method: \"POST\",\n      headers: {\n        ...getHeaders(\"3.0.0\"),\n        \"x-model-api-key\": \"\", // Clear the header to ensure body config is used\n      },\n      body: JSON.stringify({\n        input: \"click the Learn more link\",\n        options: {\n          model: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n        },\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ActResult>(response);\n    const { events } = ctx;\n\n    assertWithContext(\n      events.length >= 2,\n      \"Should have at least starting and finished events\",\n      ctx,\n    );\n\n    // Verify starting event\n    const startingEvent = assertEventExists(events, \"starting\", ctx);\n    assert.equal(\n      startingEvent.type,\n      \"system\",\n      \"Starting event should be system type\",\n    );\n\n    // Verify finished event with result\n    const finishedEvent = assertEventExists(events, \"finished\", ctx);\n    assert.equal(\n      finishedEvent.type,\n      \"system\",\n      \"Finished event should be system type\",\n    );\n    assertWithContext(\n      !!finishedEvent.data.result,\n      \"Finished event must have result\",\n      ctx,\n    );\n    assert.equal(\n      typeof finishedEvent.data.result.success,\n      \"boolean\",\n      \"Result.success must be a boolean\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/agentExecute.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { after, before, beforeEach, describe, it } from \"node:test\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  assertWithContext,\n  createSession,\n  createSessionWithCdp,\n  endSession,\n  fetchWithContext,\n  GEMINI_API_KEY,\n  getBaseUrl,\n  getHeaders,\n  getMainFrameId,\n  HTTP_BAD_REQUEST,\n  HTTP_OK,\n  HTTP_UNPROCESSABLE_ENTITY,\n  navigateSession,\n  OPENAI_API_KEY,\n  readTypedSSEStreamWithContext,\n  requireEnv,\n} from \"../utils.js\";\n\n// =============================================================================\n// POST /v1/sessions/:id/agentExecute (V3 Format)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute (V3) - Basic Config\", () => {\n  let sessionId: string;\n  let cdpUrl: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    ({ sessionId, cdpUrl } = await createSessionWithCdp(headers));\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should execute agent with basic config (empty agentConfig)\", async () => {\n    const url = getBaseUrl();\n    const frameId = await getMainFrameId(cdpUrl);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {},\n        executeOptions: {\n          instruction: \"Describe the main heading on this page\",\n          frameId,\n        },\n      }),\n    });\n\n    assertFetchStatus(ctx, HTTP_OK, \"V3 agent execute should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with string agentConfig.model\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: \"gpt-4.1-nano\",\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with string model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with object model config (provider + modelName)\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            provider: \"openai\",\n            modelName: \"gpt-4.1-nano\",\n          },\n        },\n        executeOptions: {\n          instruction: \"Describe the page content\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with object model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with systemPrompt and maxSteps\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          systemPrompt: \"You are a helpful web browsing assistant.\",\n        },\n        executeOptions: {\n          instruction: \"Find and describe the main content\",\n          maxSteps: 3,\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with systemPrompt and maxSteps should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n});\n\n// ===========================================================================\n// V3 Format Tests with model: {modelName, apiKey} format - Google Gemini\n// ===========================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute (V3) - Google Gemini with API key\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should execute agent with Google model object containing modelName and apiKey\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n            apiKey: geminiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with Google model object should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with Google model object, systemPrompt, and maxSteps\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n            apiKey: geminiApiKey,\n          },\n          systemPrompt: \"You are a helpful web browsing assistant.\",\n        },\n        executeOptions: {\n          instruction: \"Find and read the main heading\",\n          maxSteps: 3,\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with Google model, systemPrompt and maxSteps should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n});\n\n// ===========================================================================\n// V3 Format Tests with OpenAI model: {modelName, apiKey} format\n// ===========================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute (V3) - OpenAI with API key\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should execute agent with OpenAI model object containing modelName and apiKey\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with OpenAI model object should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n});\n\n// ===========================================================================\n// V3 CUA Mode Tests - Testing explicit cua flag with model compatibility\n// ===========================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute (V3) - CUA flag compatibility\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should execute agent with cua: true and CUA model (valid combination)\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          cua: true,\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n            apiKey: geminiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with cua: true and CUA model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with cua: false and non-CUA model (valid combination)\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          cua: false,\n          model: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with cua: false and non-CUA model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with cua: false and CUA model (works in non-CUA mode)\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      message?: string;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          cua: false,\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n            apiKey: geminiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with cua: false and Google CUA model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should fail with cua: true and non-CUA model (invalid combination)\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      message?: string;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          cua: true,\n          model: {\n            modelName: \"google/gemini-2.5-flash-lite\",\n            apiKey: geminiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_UNPROCESSABLE_ENTITY,\n      \"V3 agent execute with cua: true and non-CUA model should fail\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(!ctx.body.success, \"Response should indicate failure\", ctx);\n  });\n\n  it(\"should prefer mode over cua when both are provided\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          cua: true,\n          mode: \"dom\",\n          model: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with mode: dom and cua: true should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n});\n\n// =============================================================================\n// V3 executionModel Tests - Testing agentConfig.executionModel serialization\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute (V3) - executionModel serialization\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should execute agent with string executionModel\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          executionModel: \"gpt-4.1-nano\",\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with string executionModel should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with object executionModel (modelName and apiKey)\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          executionModel: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"Describe the main content of this page\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with object executionModel should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with both model and executionModel\", async () => {\n    const url = getBaseUrl();\n    const openaiApiKey = requireEnv(\"OPENAI_API_KEY\", OPENAI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n          executionModel: {\n            modelName: \"openai/gpt-4.1-nano\",\n            apiKey: openaiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with both model and executionModel should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n});\n\n// =============================================================================\n// V3 Mode Tests - Testing agentConfig.mode field (dom, hybrid, cua)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute - agentConfig.mode (V3)\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should execute agent with mode: dom\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          mode: \"dom\",\n        },\n        executeOptions: {\n          instruction: \"What is the title of this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with mode: dom should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with mode: hybrid\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          mode: \"hybrid\",\n          model: {\n            provider: \"google\", // bonus: test split provider/modelName format\n            modelName: \"gemini-2.5-flash-preview-04-17\",\n            apiKey: geminiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"Describe the main heading on this page\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with mode: hybrid should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n\n  it(\"should execute agent with mode: cua and CUA model\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          mode: \"cua\",\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n            apiKey: geminiApiKey,\n          },\n        },\n        executeOptions: {\n          instruction: \"What is visible on this page?\",\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"V3 agent execute with mode: cua and CUA model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.result !== undefined,\n      \"Response should have result\",\n      ctx,\n    );\n  });\n});\n\n// =============================================================================\n// SSE Streaming Tests (V3)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute with SSE streaming (V3)\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should stream SSE events with valid structure, sequence, and UUIDs\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(\n      `${url}/v1/sessions/${sessionId}/agentExecute`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          agentConfig: {},\n          executeOptions: {\n            instruction: \"Describe the main content on the page\",\n          },\n          streamResponse: true,\n        }),\n      },\n    );\n\n    const ctx = await readTypedSSEStreamWithContext(response);\n\n    // Verify event count\n    assertWithContext(\n      ctx.events.length >= 2,\n      \"Should have at least starting and finished events\",\n      ctx,\n    );\n\n    // Verify event sequence\n    const startingIndex = ctx.events.findIndex(\n      (e) => e.data.status === \"starting\",\n    );\n    const connectedIndex = ctx.events.findIndex(\n      (e) => e.data.status === \"connected\",\n    );\n    const finishedIndex = ctx.events.findIndex(\n      (e) => e.data.status === \"finished\",\n    );\n\n    assertWithContext(\n      startingIndex !== -1,\n      \"Should have a starting event\",\n      ctx,\n    );\n    assertWithContext(\n      connectedIndex !== -1,\n      \"Should have a connected event\",\n      ctx,\n    );\n    assertWithContext(\n      finishedIndex !== -1,\n      \"Should have a finished event\",\n      ctx,\n    );\n    assertWithContext(\n      startingIndex < connectedIndex,\n      \"Starting event must come before connected event\",\n      ctx,\n    );\n    assertWithContext(\n      connectedIndex < finishedIndex,\n      \"Connected event must come before finished event\",\n      ctx,\n    );\n\n    // Verify event types\n    const startingEvent = ctx.events[startingIndex];\n    const finishedEvent = ctx.events[finishedIndex];\n\n    assertWithContext(\n      startingEvent !== undefined,\n      \"Starting event should exist\",\n      ctx,\n    );\n    assertWithContext(\n      finishedEvent !== undefined,\n      \"Finished event should exist\",\n      ctx,\n    );\n    assertWithContext(\n      startingEvent.type === \"system\",\n      \"Starting event should be system type\",\n      ctx,\n    );\n    assertWithContext(\n      finishedEvent.type === \"system\",\n      \"Finished event should be system type\",\n      ctx,\n    );\n    assertWithContext(\n      finishedEvent.data.result !== undefined,\n      \"Finished event must have result\",\n      ctx,\n    );\n\n    // Verify UUID format\n    const uuidRegex =\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n    for (const event of ctx.events) {\n      assertWithContext(\n        uuidRegex.test(event.id),\n        `Event id should be a valid UUID format, got: ${event.id}`,\n        ctx,\n      );\n    }\n  });\n});\n\n// =============================================================================\n// Validation Error Tests (V3)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute - validation errors (V3)\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  beforeEach(async () => {\n    // Navigate to example.com before each test (including first)\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should return 400 when agentConfig is missing\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success?: boolean;\n      error?: string;\n      message?: string;\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        executeOptions: {\n          instruction: \"Do something\",\n        },\n      }),\n    });\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(\n      !ctx.body?.success || ctx.body.error !== undefined,\n      \"Response should indicate failure\",\n      ctx,\n    );\n  });\n\n  it(\"should return 400 when executeOptions is missing\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success?: boolean;\n      error?: string;\n      message?: string;\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n          },\n        },\n      }),\n    });\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(\n      !ctx.body?.success || ctx.body.error !== undefined,\n      \"Response should indicate failure\",\n      ctx,\n    );\n  });\n\n  it(\"should return 400 when instruction is missing\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success?: boolean;\n      error?: string;\n      message?: string;\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          model: {\n            modelName: \"google/gemini-2.5-computer-use-preview-10-2025\",\n          },\n        },\n        executeOptions: {\n          maxSteps: 5,\n        },\n      }),\n    });\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(\n      !ctx.body?.success || ctx.body.error !== undefined,\n      \"Response should indicate failure\",\n      ctx,\n    );\n  });\n\n  it(\"should return 400 for invalid agentConfig.mode\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success?: boolean;\n      error?: string;\n      message?: string;\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {\n          mode: \"invalid-mode\",\n        },\n        executeOptions: {\n          instruction: \"Do something\",\n        },\n      }),\n    });\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(\n      !ctx.body?.success || ctx.body.error !== undefined,\n      \"Response should indicate failure\",\n      ctx,\n    );\n  });\n});\n\n// =============================================================================\n// V3 Format Tests - executeOptions.useSearch and executeOptions.toolTimeout\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/agentExecute (V3) - useSearch & toolTimeout\", () => {\n  let sessionId: string;\n  const headers = getHeaders(\"3.0.0\");\n\n  before(async () => {\n    ({ sessionId } = await createSessionWithCdp(headers));\n  });\n\n  beforeEach(async () => {\n    const navResponse = await navigateSession(\n      sessionId,\n      \"https://example.com\",\n      headers,\n    );\n    assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n  });\n\n  after(async () => {\n    if (sessionId) {\n      await endSession(sessionId, headers);\n      sessionId = \"\";\n    }\n  });\n\n  it(\"should accept executeOptions.useSearch as boolean\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {},\n        executeOptions: {\n          instruction: \"Describe the main heading on this page\",\n          useSearch: true,\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Agent execute with useSearch should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n  });\n\n  it(\"should accept executeOptions.toolTimeout as number\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {},\n        executeOptions: {\n          instruction: \"Describe the main heading on this page\",\n          toolTimeout: 30000,\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Agent execute with toolTimeout should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n  });\n\n  it(\"should accept both useSearch and toolTimeout together\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/agentExecute`, {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        agentConfig: {},\n        executeOptions: {\n          instruction: \"Describe the main heading on this page\",\n          useSearch: false,\n          toolTimeout: 60000,\n        },\n      }),\n    });\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Agent execute with both options should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/end.test.ts",
    "content": "import { after, before, describe, it } from \"node:test\";\n\nimport assert from \"node:assert/strict\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  createSession,\n  endSession,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  HTTP_BAD_REQUEST,\n  HTTP_NOT_FOUND,\n  HTTP_INTERNAL_SERVER_ERROR,\n  HTTP_OK,\n} from \"../utils.js\";\n\n// =============================================================================\n// POST /v1/sessions/:id/end (V3 Format)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/end (V3)\", () => {\n  const headers = getHeaders(\"3.0.0\");\n  let sessionId: string;\n\n  before(async () => {\n    sessionId = await createSession(headers);\n  });\n\n  after(async () => {\n    // Try to clean up in case test didn't end the session\n    try {\n      await endSession(sessionId, headers);\n    } catch {\n      // Ignore - session may already be ended\n    }\n  });\n\n  it(\"should return 200 if JSON content-type has an empty body\", async () => {\n    const url = getBaseUrl();\n    // Create a fresh session for this test since we need to test error cases\n    const testSessionId = await createSession(headers);\n\n    const response = await fetch(`${url}/v1/sessions/${testSessionId}/end`, {\n      method: \"POST\",\n      headers: {\n        ...headers,\n        \"Content-Type\": \"application/json\",\n      },\n      body: \"\",\n    });\n\n    // Empty body should be accepted\n    assertFetchStatus(\n      {\n        status: response.status,\n        statusText: response.statusText,\n        body: null,\n        raw: \"\",\n        durationMs: 0,\n        headers: response.headers,\n        debugSummary: () => `HTTP ${response.status}`,\n      },\n      HTTP_OK,\n      \"Should return 200 for empty body with JSON content-type\",\n    );\n\n    // Clean up\n    await endSession(testSessionId, headers);\n  });\n\n  it(\"should return 400 if body contains extra keys\", async () => {\n    const url = getBaseUrl();\n    const testSessionId = await createSession(headers);\n\n    const ctx = await fetchWithContext<{ success?: boolean; message?: string }>(\n      `${url}/v1/sessions/${testSessionId}/end`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({ unexpected: true }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_BAD_REQUEST,\n      \"Should return 400 for extra keys\",\n    );\n\n    // Clean up\n    await endSession(testSessionId, headers);\n  });\n\n  it(\"should return 200 when body is {}\", async () => {\n    const url = getBaseUrl();\n    const testSessionId = await createSession(headers);\n\n    const ctx = await fetchWithContext<{ success: boolean }>(\n      `${url}/v1/sessions/${testSessionId}/end`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({}),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Should return 200 for empty object body\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(ctx.body.success === true, \"Should indicate success\", ctx);\n  });\n\n  it(\"should return 200 when body is 0 bytes (no body)\", async () => {\n    const url = getBaseUrl();\n    const testSessionId = await createSession(headers);\n\n    // Send request with no body at all\n    const response = await fetch(`${url}/v1/sessions/${testSessionId}/end`, {\n      method: \"POST\",\n      headers: {\n        ...headers,\n        // Don't set Content-Type to application/json when there's no body\n      },\n    });\n\n    // Should succeed with 200 for no body\n    assert.equal(\n      response.status,\n      HTTP_OK,\n      `Should return 200 for 0-byte body, got ${response.status}`,\n    );\n\n    const body = await response.json();\n    assert.equal(body.success, true, \"Should indicate success\");\n  });\n\n  it(\"should end session successfully\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{ success: boolean }>(\n      `${url}/v1/sessions/${sessionId}/end`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({}),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"End session should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(ctx.body.success === true, \"Should indicate success\", ctx);\n  });\n\n  it(\"should return error for non-existent session\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{ success?: boolean; message?: string }>(\n      `${url}/v1/sessions/non-existent-session-id/end`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({}),\n      },\n    );\n\n    // Server returns 404 or 500 for non-existent sessions\n    assert.ok(\n      [HTTP_NOT_FOUND, HTTP_INTERNAL_SERVER_ERROR].includes(ctx.status),\n      `Expected 404 or 500, got ${ctx.status}`,\n    );\n\n    if (ctx.status === HTTP_INTERNAL_SERVER_ERROR) {\n      assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n      assertFetchOk(\n        ctx.body.message === \"An internal server error occurred\",\n        \"500 responses should return a generic internal error message\",\n        ctx,\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/extract.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { after, before, describe, it } from \"node:test\";\n\nimport {\n  assertEventExists,\n  assertFetchOk,\n  assertFetchStatus,\n  assertWithContext,\n  createSessionWithCdp,\n  endSession,\n  fetchWithContext,\n  GEMINI_API_KEY,\n  getBaseUrl,\n  getHeaders,\n  getMainFrameId,\n  HTTP_OK,\n  navigateSession,\n  readTypedSSEStreamWithContext,\n  requireEnv,\n} from \"../utils.js\";\n\n/** Result type for extract SSE events */\ntype ExtractResult = Record<string, unknown>;\n\n// Shared session for all extract tests (extract is read-only, safe to share)\nlet sessionId: string;\nlet cdpUrl: string;\n\nbefore(async () => {\n  ({ sessionId, cdpUrl } = await createSessionWithCdp(getHeaders(\"3.0.0\")));\n  const navResponse = await navigateSession(\n    sessionId,\n    \"https://example.com\",\n    getHeaders(\"3.0.0\"),\n  );\n  assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n});\n\nafter(async () => {\n  await endSession(sessionId, getHeaders(\"3.0.0\"));\n});\n\n// =============================================================================\n// POST /v1/sessions/:id/extract - V3 Format Tests\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/extract (V3)\", () => {\n  it(\"should extract data with instruction and schema\", async () => {\n    const url = getBaseUrl();\n    const frameId = await getMainFrameId(cdpUrl);\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"extract the page title\",\n          schema: {\n            type: \"object\",\n            properties: {\n              title: { type: \"string\" },\n            },\n            required: [\"title\"],\n          },\n          frameId,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n    assertFetchOk(\n      \"title\" in ctx.body.data.result,\n      \"Result should have title property\",\n      ctx,\n    );\n  });\n\n  it(\"should extract with instruction and options\", async () => {\n    const url = getBaseUrl();\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"extract the page title\",\n          schema: {\n            type: \"object\",\n            properties: {\n              title: { type: \"string\" },\n            },\n          },\n          options: {\n            timeout: 30000,\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract with options should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n  });\n\n  it(\"should extract with CSS selector in options\", async () => {\n    const url = getBaseUrl();\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"extract the link information\",\n          schema: {\n            type: \"object\",\n            properties: {\n              href: { type: \"string\" },\n              text: { type: \"string\" },\n            },\n          },\n          options: {\n            selector: \"a\", // CSS selector\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract with CSS selector should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n  });\n\n  it(\"should extract with XPath selector in options\", async () => {\n    const url = getBaseUrl();\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"extract the link information\",\n          schema: {\n            type: \"object\",\n            properties: {\n              href: { type: \"string\" },\n              text: { type: \"string\" },\n            },\n          },\n          options: {\n            selector: \"//a\", // XPath selector\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Extract with XPath selector should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n  });\n\n  it(\"should extract with instruction only (no schema)\", async () => {\n    const url = getBaseUrl();\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"extract the main content from the page\",\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract without schema should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n  });\n\n  it(\"should extract without instruction (extract all)\", async () => {\n    const url = getBaseUrl();\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          options: {\n            timeout: 30000,\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Extract without instruction should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n  });\n\n  it(\"should extract with google/gemini-2.5-flash-lite model\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    interface ExtractResponse {\n      success: boolean;\n      data?: { result: Record<string, unknown>; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ExtractResponse>(\n      `${url}/v1/sessions/${sessionId}/extract`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"extract the page title\",\n          schema: {\n            type: \"object\",\n            properties: {\n              title: { type: \"string\" },\n            },\n            required: [\"title\"],\n          },\n          options: {\n            model: {\n              modelName: \"google/gemini-2.5-flash-lite\",\n              apiKey: geminiApiKey,\n            },\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Extract with Gemini model should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(!!ctx.body.data, \"Response should have data\", ctx);\n    assertFetchOk(!!ctx.body.data.result, \"Response should have result\", ctx);\n    assert.equal(\n      typeof ctx.body.data.result,\n      \"object\",\n      \"Result should be an object\",\n    );\n    assertFetchOk(\n      \"title\" in ctx.body.data.result,\n      \"Result should have title property\",\n      ctx,\n    );\n  });\n});\n\n// =============================================================================\n// SSE Streaming Tests - V3\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/extract with SSE streaming (V3)\", () => {\n  it(\"should stream valid SSE events with correct structure\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/extract`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"extract the page title\",\n        schema: {\n          type: \"object\",\n          properties: {\n            title: { type: \"string\" },\n          },\n          required: [\"title\"],\n        },\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ExtractResult>(response);\n    const { events } = ctx;\n\n    assertWithContext(\n      events.length >= 2,\n      \"Should have at least starting and finished events\",\n      ctx,\n    );\n\n    // Verify starting event\n    const startingEvent = assertEventExists(events, \"starting\", ctx);\n    assert.equal(\n      startingEvent.type,\n      \"system\",\n      \"Starting event should be system type\",\n    );\n\n    // Verify finished event with result\n    const finishedEvent = assertEventExists(events, \"finished\", ctx);\n    assert.equal(\n      finishedEvent.type,\n      \"system\",\n      \"Finished event should be system type\",\n    );\n    assertWithContext(\n      !!finishedEvent.data.result,\n      \"Finished event must have result\",\n      ctx,\n    );\n    assert.equal(\n      typeof finishedEvent.data.result,\n      \"object\",\n      \"Result must be an object\",\n    );\n    assertWithContext(\n      \"title\" in finishedEvent.data.result,\n      \"Result should have title property\",\n      ctx,\n    );\n  });\n\n  it(\"should have correct event sequence: starting -> connected -> finished\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/extract`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"extract the page title\",\n        schema: {\n          type: \"object\",\n          properties: {\n            title: { type: \"string\" },\n          },\n        },\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ExtractResult>(response);\n    const { events } = ctx;\n\n    assertEventExists(events, \"starting\", ctx);\n    assertEventExists(events, \"connected\", ctx);\n    assertEventExists(events, \"finished\", ctx);\n\n    const startingIndex = events.findIndex((e) => e.data.status === \"starting\");\n    const connectedIndex = events.findIndex(\n      (e) => e.data.status === \"connected\",\n    );\n    const finishedIndex = events.findIndex((e) => e.data.status === \"finished\");\n\n    assertWithContext(\n      startingIndex < connectedIndex,\n      \"Starting event must come before connected event\",\n      ctx,\n    );\n    assertWithContext(\n      connectedIndex < finishedIndex,\n      \"Connected event must come before finished event\",\n      ctx,\n    );\n  });\n\n  it(\"should have valid UUID for each event id\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/extract`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"extract the page title\",\n        schema: {\n          type: \"object\",\n          properties: {\n            title: { type: \"string\" },\n          },\n        },\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ExtractResult>(response);\n    const { events } = ctx;\n\n    const uuidRegex =\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n    for (const event of events) {\n      assertWithContext(\n        uuidRegex.test(event.id),\n        `Event id should be a valid UUID format, got: ${event.id}`,\n        ctx,\n      );\n    }\n  });\n\n  it(\"should extract data matching the provided schema\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/extract`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"extract the page title\",\n        schema: {\n          type: \"object\",\n          properties: {\n            title: { type: \"string\" },\n          },\n          required: [\"title\"],\n        },\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ExtractResult>(response);\n    const { events } = ctx;\n\n    const finishedEvent = assertEventExists(events, \"finished\", ctx);\n    assertWithContext(!!finishedEvent.data.result, \"Should have result\", ctx);\n\n    // Verify the extracted data has the expected shape\n    assert.equal(\n      typeof finishedEvent.data.result.title,\n      \"string\",\n      \"Extracted title should be a string\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/multiRegion.test.ts",
    "content": "import { describe, it } from \"node:test\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  endSession,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  HTTP_OK,\n  LOCAL_BROWSER_BODY,\n} from \"../utils.js\";\n\n// =============================================================================\n// Response Type Definitions\n// =============================================================================\n\ninterface StartSuccessResponse {\n  success: true;\n  data: {\n    sessionId: string;\n    cdpUrl: string;\n    available: boolean;\n  };\n}\n\ninterface StartErrorResponse {\n  success: false;\n  message: string;\n}\n\ntype StartResponse = StartSuccessResponse | StartErrorResponse;\n\nfunction isSuccessResponse(\n  response: StartResponse,\n): response is StartSuccessResponse {\n  return response.success && response.data.sessionId !== null;\n}\n\n// =============================================================================\n// Multi-Region Integration Tests\n// =============================================================================\n// These tests verify that the API client correctly handles multi-region\n// configuration. Prior to the multi-region feature, non-us-west-2 regions\n// would be rejected with { available: false }. Now all supported regions\n// are accepted.\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/start - Multi-region support\", () => {\n  const headers = getHeaders(\"3.0.0\");\n  const localBrowser = LOCAL_BROWSER_BODY;\n\n  it(\"should start session with us-west-2 region (default)\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"us-west-2\",\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    try {\n      assertFetchOk(\n        ctx.body.data.available,\n        \"Session should be available\",\n        ctx,\n      );\n      assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    } finally {\n      await endSession(ctx.body.data.sessionId, headers);\n    }\n  });\n\n  it(\"should start session with us-east-1 region\", async () => {\n    const url = getBaseUrl();\n\n    // This test verifies that non-us-west-2 regions are now accepted.\n    // Previously, this would have returned { available: false }.\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"us-east-1\",\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    try {\n      assertFetchOk(\n        ctx.body.data.available,\n        \"Session should be available\",\n        ctx,\n      );\n      assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    } finally {\n      await endSession(ctx.body.data.sessionId, headers);\n    }\n  });\n\n  it(\"should start session with eu-central-1 region\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"eu-central-1\",\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    try {\n      assertFetchOk(\n        ctx.body.data.available,\n        \"Session should be available\",\n        ctx,\n      );\n      assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    } finally {\n      await endSession(ctx.body.data.sessionId, headers);\n    }\n  });\n\n  it(\"should start session with ap-southeast-1 region\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"ap-southeast-1\",\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    try {\n      assertFetchOk(\n        ctx.body.data.available,\n        \"Session should be available\",\n        ctx,\n      );\n      assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    } finally {\n      await endSession(ctx.body.data.sessionId, headers);\n    }\n  });\n\n  it(\"should start session without region (defaults to us-west-2)\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {},\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    try {\n      assertFetchOk(\n        ctx.body.data.available,\n        \"Session should be available\",\n        ctx,\n      );\n      assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    } finally {\n      await endSession(ctx.body.data.sessionId, headers);\n    }\n  });\n\n  it(\"should start session without browserbaseSessionCreateParams\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    try {\n      assertFetchOk(\n        ctx.body.data.available,\n        \"Session should be available\",\n        ctx,\n      );\n      assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    } finally {\n      await endSession(ctx.body.data.sessionId, headers);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/navigate.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { after, before, describe, it } from \"node:test\";\n\nimport { chromium } from \"playwright\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  createSessionWithCdp,\n  endSession,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  HTTP_BAD_REQUEST,\n  HTTP_OK,\n} from \"../utils.js\";\n\n// =============================================================================\n// POST /v1/sessions/:id/navigate (V3 Format)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/navigate (V3)\", () => {\n  let sessionId: string;\n  let cdpUrl: string;\n\n  before(async () => {\n    ({ sessionId, cdpUrl } = await createSessionWithCdp(getHeaders(\"3.0.0\")));\n  });\n\n  after(async () => {\n    await endSession(sessionId, getHeaders(\"3.0.0\"));\n  });\n\n  it(\"should navigate to a URL successfully and verify via CDP\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/navigate`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({ url: \"https://example.com\", frameId: \"\" }),\n    });\n\n    assertFetchStatus(ctx, HTTP_OK, \"Navigate should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      typeof ctx.body.data.actionId === \"string\",\n      \"Response should have actionId\",\n      ctx,\n    );\n\n    // Verify navigation via CDP\n    const browser = await chromium.connectOverCDP(cdpUrl);\n    const contexts = browser.contexts();\n    assert.ok(contexts.length > 0, \"Should have at least one browser context\");\n    const pages = contexts[0]!.pages();\n    assert.ok(pages.length > 0, \"Should have at least one page\");\n    const page = pages[0]!;\n    await page\n      .waitForLoadState(\"domcontentloaded\", { timeout: 15_000 })\n      .catch(() => {});\n    await page\n      .waitForURL(\"**example.com**\", { timeout: 15_000 })\n      .catch(() => {});\n    const pageUrl = page.url();\n    assert.ok(\n      pageUrl.includes(\"example.com\"),\n      `Page URL should be example.com, got: ${pageUrl}`,\n    );\n    await browser.close();\n  });\n\n  it(\"should navigate with options\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success: boolean;\n      data?: { result: unknown; actionId?: string };\n    }>(`${url}/v1/sessions/${sessionId}/navigate`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        url: \"https://example.com\",\n        frameId: \"\",\n        options: {\n          waitUntil: \"networkidle\",\n          timeout: 30000,\n        },\n      }),\n    });\n\n    assertFetchStatus(ctx, HTTP_OK, \"Navigate with options should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      typeof ctx.body.data.actionId === \"string\",\n      \"Response should have actionId\",\n      ctx,\n    );\n  });\n\n  // ===========================================================================\n  // Validation Tests\n  // ===========================================================================\n\n  it(\"should return 400 when url is missing\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<{\n      success?: boolean;\n      message?: string;\n      error?: string;\n    }>(`${url}/v1/sessions/${sessionId}/navigate`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({ frameId: \"\" }), // Missing url\n    });\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    // Fastify validation errors may have different format than our custom errors\n    assertFetchOk(\n      !ctx.body.success ||\n        ctx.body.error !== undefined ||\n        ctx.body.message !== undefined,\n      \"Response should indicate failure\",\n      ctx,\n    );\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/observe.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { after, before, describe, it } from \"node:test\";\n\nimport {\n  assertEventExists,\n  assertFetchOk,\n  assertFetchStatus,\n  assertWithContext,\n  createSessionWithCdp,\n  endSession,\n  fetchWithContext,\n  GEMINI_API_KEY,\n  getBaseUrl,\n  getHeaders,\n  getMainFrameId,\n  HTTP_OK,\n  navigateSession,\n  readTypedSSEStreamWithContext,\n  requireEnv,\n} from \"../utils.js\";\n\n/** Result type for observe SSE events */\ntype ObserveResult = unknown[];\n\n// Shared session for all observe tests (observe is read-only, safe to share)\nlet sessionId: string;\nlet cdpUrl: string;\n\nbefore(async () => {\n  ({ sessionId, cdpUrl } = await createSessionWithCdp(getHeaders(\"3.0.0\")));\n  // Navigate to a page first\n  const navResponse = await navigateSession(\n    sessionId,\n    \"https://example.com\",\n    getHeaders(\"3.0.0\"),\n  );\n  assert.equal(navResponse.status, HTTP_OK, \"Navigate should succeed\");\n});\n\nafter(async () => {\n  await endSession(sessionId, getHeaders(\"3.0.0\"));\n});\n\n// =============================================================================\n// POST /v1/sessions/:id/observe - V3 Format Tests\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/observe (V3)\", () => {\n  it(\"should observe elements with instruction\", async () => {\n    const url = getBaseUrl();\n    const frameId = await getMainFrameId(cdpUrl);\n\n    interface ObserveResponse {\n      success: boolean;\n      data?: { result: unknown[]; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ObserveResponse>(\n      `${url}/v1/sessions/${sessionId}/observe`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"Find any link on the page\",\n          frameId,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Observe should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      Array.isArray(ctx.body.data.result),\n      \"Result should be an array of observed elements\",\n      ctx,\n    );\n  });\n\n  it(\"should observe with instruction and options\", async () => {\n    const url = getBaseUrl();\n\n    interface ObserveResponse {\n      success: boolean;\n      data?: { result: unknown[]; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ObserveResponse>(\n      `${url}/v1/sessions/${sessionId}/observe`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"Find any link on the page\",\n          options: {\n            timeout: 30000,\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Observe with options should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      Array.isArray(ctx.body.data.result),\n      \"Result should be an array of observed elements\",\n      ctx,\n    );\n  });\n\n  it(\"should observe with selector option\", async () => {\n    const url = getBaseUrl();\n\n    interface ObserveResponse {\n      success: boolean;\n      data?: { result: unknown[]; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ObserveResponse>(\n      `${url}/v1/sessions/${sessionId}/observe`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          instruction: \"Find any link on the page\",\n          options: {\n            selector: \"a\",\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Observe with selector should succeed\");\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      Array.isArray(ctx.body.data.result),\n      \"Result should be an array of observed elements\",\n      ctx,\n    );\n  });\n\n  it(\"should observe without instruction (observe all)\", async () => {\n    const url = getBaseUrl();\n\n    interface ObserveResponse {\n      success: boolean;\n      data?: { result: unknown[]; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ObserveResponse>(\n      `${url}/v1/sessions/${sessionId}/observe`,\n      {\n        method: \"POST\",\n        headers: getHeaders(\"3.0.0\"),\n        body: JSON.stringify({\n          options: {\n            timeout: 30000,\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Observe without instruction should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      Array.isArray(ctx.body.data.result),\n      \"Result should be an array of observed elements\",\n      ctx,\n    );\n  });\n\n  it(\"should observe with google/gemini-2.5-flash-lite model\", async () => {\n    const url = getBaseUrl();\n    const geminiApiKey = requireEnv(\"GEMINI_API_KEY\", GEMINI_API_KEY);\n\n    interface ObserveResponse {\n      success: boolean;\n      data?: { result: unknown[]; actionId?: string };\n    }\n\n    const ctx = await fetchWithContext<ObserveResponse>(\n      `${url}/v1/sessions/${sessionId}/observe`,\n      {\n        method: \"POST\",\n        headers: {\n          ...getHeaders(\"3.0.0\"),\n          \"x-model-api-key\": \"\", // Clear the header to ensure body config is used\n        },\n        body: JSON.stringify({\n          instruction: \"Find any link on the page\",\n          options: {\n            model: {\n              modelName: \"google/gemini-2.5-flash-lite\",\n              apiKey: geminiApiKey,\n            },\n          },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Observe with google/gemini-2.5-flash-lite model should succeed\",\n    );\n    assertFetchOk(ctx.body !== null, \"Response body should be parseable\", ctx);\n    assertFetchOk(ctx.body.success, \"Response should indicate success\", ctx);\n    assertFetchOk(\n      ctx.body.data !== undefined,\n      \"Response should have data\",\n      ctx,\n    );\n    assertFetchOk(\n      Array.isArray(ctx.body.data.result),\n      \"Result should be an array of observed elements\",\n      ctx,\n    );\n  });\n});\n\n// =============================================================================\n// SSE Streaming Tests - V3\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/:id/observe with SSE streaming (V3)\", () => {\n  it(\"should stream valid SSE events with correct structure\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/observe`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"Find any link on the page\",\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ObserveResult>(response);\n    const { events } = ctx;\n\n    assertWithContext(\n      events.length >= 2,\n      \"Should have at least starting and finished events\",\n      ctx,\n    );\n\n    // Verify starting event\n    const startingEvent = assertEventExists(events, \"starting\", ctx);\n    assertWithContext(\n      startingEvent.type === \"system\",\n      \"Starting event should be system type\",\n      ctx,\n    );\n\n    // Verify finished event with result\n    const finishedEvent = assertEventExists(events, \"finished\", ctx);\n    assertWithContext(\n      finishedEvent.type === \"system\",\n      \"Finished event should be system type\",\n      ctx,\n    );\n    assertWithContext(\n      !!finishedEvent.data.result,\n      \"Finished event must have result\",\n      ctx,\n    );\n    assertWithContext(\n      Array.isArray(finishedEvent.data.result),\n      \"Result must be an array of observed elements\",\n      ctx,\n    );\n  });\n\n  it(\"should have correct event sequence: starting -> connected -> finished\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/observe`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"Find any link on the page\",\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ObserveResult>(response);\n    const { events } = ctx;\n\n    assertEventExists(events, \"starting\", ctx);\n    assertEventExists(events, \"connected\", ctx);\n    assertEventExists(events, \"finished\", ctx);\n\n    const startingIndex = events.findIndex((e) => e.data.status === \"starting\");\n    const connectedIndex = events.findIndex(\n      (e) => e.data.status === \"connected\",\n    );\n    const finishedIndex = events.findIndex((e) => e.data.status === \"finished\");\n\n    assertWithContext(\n      startingIndex < connectedIndex,\n      \"Starting event must come before connected event\",\n      ctx,\n    );\n    assertWithContext(\n      connectedIndex < finishedIndex,\n      \"Connected event must come before finished event\",\n      ctx,\n    );\n  });\n\n  it(\"should have valid UUID for each event id\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/observe`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"Find any link on the page\",\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ObserveResult>(response);\n    const { events } = ctx;\n\n    const uuidRegex =\n      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n    for (const event of events) {\n      assertWithContext(\n        uuidRegex.test(event.id),\n        `Event id should be a valid UUID format, got: ${event.id}`,\n        ctx,\n      );\n    }\n  });\n\n  it(\"should return observed elements with expected properties\", async () => {\n    const url = getBaseUrl();\n\n    const response = await fetch(`${url}/v1/sessions/${sessionId}/observe`, {\n      method: \"POST\",\n      headers: getHeaders(\"3.0.0\"),\n      body: JSON.stringify({\n        instruction: \"Find any link on the page\",\n        streamResponse: true,\n      }),\n    });\n\n    const ctx = await readTypedSSEStreamWithContext<ObserveResult>(response);\n    const { events } = ctx;\n\n    const finishedEvent = assertEventExists(events, \"finished\", ctx);\n    assertWithContext(!!finishedEvent.data.result, \"Should have result\", ctx);\n    assertWithContext(\n      Array.isArray(finishedEvent.data.result),\n      \"Result should be an array\",\n      ctx,\n    );\n\n    // If there are observed elements, verify they have expected structure\n    if (finishedEvent.data.result.length > 0) {\n      const firstElement = finishedEvent.data.result[0] as Record<\n        string,\n        unknown\n      >;\n      assertWithContext(\n        typeof firstElement === \"object\",\n        \"Each observed element should be an object\",\n        ctx,\n      );\n      // Observed elements typically have selector and description\n      assertWithContext(\n        \"selector\" in firstElement || \"description\" in firstElement,\n        \"Observed element should have selector or description\",\n        ctx,\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/replay.test.ts",
    "content": "import { describe, it } from \"node:test\";\nimport { Api } from \"@browserbasehq/stagehand\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  HTTP_OK,\n} from \"../utils.js\";\n\ndescribe(\"GET /v1/sessions/:id/replay (V3)\", () => {\n  it(\"should return an empty replay result for local server\", async () => {\n    const url = getBaseUrl();\n    const headers = getHeaders(\"3.0.0\");\n\n    const ctx = await fetchWithContext<unknown>(\n      `${url}/v1/sessions/test-session-id/replay`,\n      {\n        method: \"GET\",\n        headers,\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Replay should return 200\");\n    assertFetchOk(ctx.body !== null, \"Response should have body\", ctx);\n    const parsedBody = Api.ReplayResponseSchema.safeParse(ctx.body);\n    assertFetchOk(\n      parsedBody.success,\n      \"Replay response should match schema\",\n      ctx,\n    );\n    if (!parsedBody.success) {\n      return;\n    }\n\n    assertFetchOk(\n      parsedBody.data.success,\n      \"Response should indicate success\",\n      ctx,\n    );\n    assertFetchOk(\n      parsedBody.data.data !== undefined,\n      \"Response should include data\",\n      ctx,\n    );\n    assertFetchOk(\n      Array.isArray(parsedBody.data.data.pages),\n      \"Replay pages should be an array\",\n      ctx,\n    );\n    assertFetchOk(\n      parsedBody.data.data.pages.length === 0,\n      \"Replay pages should be empty on local server\",\n      ctx,\n    );\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/test/integration/v3/start.test.ts",
    "content": "import { spawn } from \"node:child_process\";\nimport { execFileSync } from \"node:child_process\";\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport net from \"node:net\";\nimport { fileURLToPath } from \"node:url\";\nimport assert from \"node:assert/strict\";\nimport { afterEach, describe, it } from \"node:test\";\nimport Browserbase from \"@browserbasehq/sdk\";\nimport { chromium } from \"playwright\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  endSession,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  LOCAL_BROWSER_BODY,\n  HTTP_BAD_REQUEST,\n  HTTP_OK,\n} from \"../utils.js\";\nimport type { BrowserbaseRegion } from \"@browserbasehq/stagehand\";\n\n// =============================================================================\n// Response Type Definitions\n// =============================================================================\n\ninterface StartSuccessResponse {\n  success: true;\n  data: {\n    sessionId: string;\n    cdpUrl: string;\n    available: boolean;\n  };\n}\n\ninterface StartUnavailableResponse {\n  success: true;\n  data: {\n    sessionId: null;\n    available: false;\n  };\n}\n\ninterface StartErrorResponse {\n  success: false;\n  message: string;\n}\n\ntype StartResponse =\n  | StartSuccessResponse\n  | StartUnavailableResponse\n  | StartErrorResponse;\n\nfunction isSuccessResponse(\n  response: StartResponse,\n): response is StartSuccessResponse {\n  return response.success && response.data.sessionId !== null;\n}\n\ntype SeaHandle = {\n  proc: ChildProcessWithoutNullStreams;\n  baseUrl: string;\n  logs: string[];\n};\n\ntype SupervisorInfo = {\n  pid: number;\n  args: string;\n  chromePid?: number;\n};\n\nconst repoRoot = (() => {\n  const value = fileURLToPath(import.meta.url).replaceAll(\"\\\\\", \"/\");\n  const root = value.split(\"/packages/server-v3/\")[0];\n  if (root === value) {\n    throw new Error(`Unable to determine repo root from ${value}`);\n  }\n  return root;\n})();\n\nconst defaultSeaBinaryName = `stagehand-server-v3-${process.platform}-${process.arch}${process.platform === \"win32\" ? \".exe\" : \"\"}`;\nconst seaBinaryPath = `${repoRoot}/packages/server-v3/dist/sea/${process.env.SEA_BINARY_NAME ?? defaultSeaBinaryName}`;\nconst bbApiKey = process.env.BROWSERBASE_API_KEY;\nconst bbProjectId = process.env.BROWSERBASE_PROJECT_ID;\nconst activeSea = new Set<SeaHandle>();\n\nafterEach(async () => {\n  await Promise.all(\n    [...activeSea].map(async (handle) => {\n      await stopSeaServer(handle);\n      activeSea.delete(handle);\n    }),\n  );\n});\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function getFreePort(): Promise<number> {\n  return await new Promise((resolve, reject) => {\n    const server = net.createServer();\n    server.listen(0, \"127.0.0.1\", () => {\n      const addr = server.address();\n      if (!addr || typeof addr === \"string\") {\n        reject(new Error(\"Failed to allocate an ephemeral port\"));\n        return;\n      }\n      const { port } = addr;\n      server.close((error) => {\n        if (error) {\n          reject(error);\n          return;\n        }\n        resolve(port);\n      });\n    });\n    server.on(\"error\", reject);\n  });\n}\n\nfunction listProcesses(): Array<{ pid: number; args: string }> {\n  const output = execFileSync(\"ps\", [\"-axo\", \"pid=,args=\"], {\n    encoding: \"utf8\",\n  });\n  return output\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter(Boolean)\n    .map((line) => {\n      const firstSpace = line.indexOf(\" \");\n      if (firstSpace === -1) {\n        return { pid: Number(line), args: \"\" };\n      }\n      return {\n        pid: Number(line.slice(0, firstSpace)),\n        args: line.slice(firstSpace + 1),\n      };\n    })\n    .filter((entry) => Number.isFinite(entry.pid) && entry.pid > 0);\n}\n\nfunction parseSupervisorConfigArg(args: string): {\n  kind?: string;\n  pid?: number;\n  parentPid?: number;\n} | null {\n  const prefix = \"--supervisor-config=\";\n  const index = args.indexOf(prefix);\n  if (index === -1) return null;\n  const raw = args.slice(index + prefix.length).trim();\n  if (!raw) return null;\n  try {\n    return JSON.parse(raw) as {\n      kind?: string;\n      pid?: number;\n      parentPid?: number;\n    };\n  } catch {\n    return null;\n  }\n}\n\nfunction findLocalSupervisorByParentPid(\n  parentPid: number,\n): SupervisorInfo | null {\n  const candidates = listProcesses()\n    .map((entry) => ({\n      ...entry,\n      config: parseSupervisorConfigArg(entry.args),\n    }))\n    .filter(\n      (entry) =>\n        entry.config?.kind === \"LOCAL\" && entry.config.parentPid === parentPid,\n    )\n    .sort((a, b) => b.pid - a.pid);\n\n  const entry = candidates[0];\n  if (!entry) return null;\n\n  return {\n    pid: entry.pid,\n    args: entry.args,\n    chromePid:\n      typeof entry.config?.pid === \"number\" && Number.isFinite(entry.config.pid)\n        ? entry.config.pid\n        : undefined,\n  };\n}\n\nfunction isPidAlive(pid: number): boolean {\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch (error) {\n    const err = error as NodeJS.ErrnoException;\n    if (err.code === \"ESRCH\") return false;\n    return true;\n  }\n}\n\nasync function waitForValue<T>(\n  read: () => T | null,\n  timeoutMs: number,\n  intervalMs = 200,\n): Promise<T> {\n  const startedAt = Date.now();\n  while (Date.now() - startedAt < timeoutMs) {\n    const value = read();\n    if (value !== null) return value;\n    await sleep(intervalMs);\n  }\n  throw new Error(`Timed out after ${timeoutMs}ms`);\n}\n\nasync function waitForPidState(\n  pid: number,\n  shouldBeAlive: boolean,\n  timeoutMs: number,\n): Promise<void> {\n  const startedAt = Date.now();\n  while (Date.now() - startedAt < timeoutMs) {\n    if (isPidAlive(pid) === shouldBeAlive) return;\n    await sleep(200);\n  }\n  const entry = listProcesses().find((candidate) => candidate.pid === pid);\n  const details = entry ? ` args=${entry.args}` : \"\";\n  throw new Error(\n    `PID ${pid} did not become ${shouldBeAlive ? \"alive\" : \"dead\"} within ${timeoutMs}ms${details}`,\n  );\n}\n\nasync function waitForServerReady(baseUrl: string, timeoutMs = 30_000) {\n  const startedAt = Date.now();\n  while (Date.now() - startedAt < timeoutMs) {\n    try {\n      const response = await fetch(`${baseUrl}/healthz`);\n      if (response.ok) return;\n    } catch {\n      // retry\n    }\n    await sleep(500);\n  }\n  throw new Error(\n    `Server did not become ready at ${baseUrl} within ${timeoutMs}ms`,\n  );\n}\n\nasync function waitForProcessExit(\n  proc: ChildProcessWithoutNullStreams,\n  timeoutMs: number,\n): Promise<boolean> {\n  if (proc.exitCode !== null) {\n    return true;\n  }\n  return await new Promise((resolve) => {\n    const timer = setTimeout(() => resolve(false), timeoutMs);\n    proc.once(\"exit\", () => {\n      clearTimeout(timer);\n      resolve(true);\n    });\n  });\n}\n\nasync function startSeaServer(\n  envOverrides: Record<string, string> = {},\n): Promise<SeaHandle> {\n  const port = await getFreePort();\n  const baseUrl = `http://127.0.0.1:${port}`;\n  const logs: string[] = [];\n  const proc = spawn(\n    seaBinaryPath,\n    [\"--node-options=--no-lazy --enable-source-maps\"],\n    {\n      env: {\n        ...process.env,\n        ...envOverrides,\n        NODE_ENV: \"production\",\n        PORT: String(port),\n        STAGEHAND_SEA_CACHE_DIR:\n          process.env.STAGEHAND_SEA_CACHE_DIR ?? `${repoRoot}/.stagehand-sea`,\n      },\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n    },\n  );\n\n  proc.stdout.on(\"data\", (chunk: Buffer) => {\n    const value = chunk.toString().trim();\n    if (value) logs.push(value);\n  });\n  proc.stderr.on(\"data\", (chunk: Buffer) => {\n    const value = chunk.toString().trim();\n    if (value) logs.push(value);\n  });\n\n  if (!proc.pid) {\n    throw new Error(\"SEA process did not provide a PID\");\n  }\n\n  const handle: SeaHandle = { proc, baseUrl, logs };\n  activeSea.add(handle);\n\n  try {\n    await waitForServerReady(baseUrl);\n    return handle;\n  } catch (error) {\n    await stopSeaServer(handle);\n    const tail = logs.slice(-30).join(\"\\n\");\n    throw new Error(\n      `Failed to start SEA server at ${baseUrl}: ${(error as Error).message}\\n${tail}`,\n      {\n        cause: error,\n      },\n    );\n  }\n}\n\nasync function stopSeaServer(handle: SeaHandle): Promise<void> {\n  const { proc } = handle;\n  if (proc.exitCode !== null) return;\n  try {\n    proc.kill(\"SIGTERM\");\n  } catch {\n    // ignore\n  }\n  const exited = await waitForProcessExit(proc, 5_000);\n  if (!exited) {\n    try {\n      proc.kill(\"SIGKILL\");\n    } catch {\n      // ignore\n    }\n    await waitForProcessExit(proc, 5_000);\n  }\n}\n\nasync function forceKillSeaServer(handle: SeaHandle): Promise<void> {\n  const { proc } = handle;\n  if (proc.exitCode !== null) return;\n  try {\n    proc.kill(\"SIGKILL\");\n  } catch {\n    // ignore\n  }\n  await waitForProcessExit(proc, 5_000);\n}\n\nasync function startKeepAliveFalseLocalSession(baseUrl: string): Promise<{\n  sessionId: string;\n  cdpUrl: string;\n}> {\n  const headers = getHeaders(\"3.0.0\");\n  const ctx = await fetchWithContext<StartResponse>(\n    `${baseUrl}/v1/sessions/start`,\n    {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        modelName: \"gpt-4.1-nano\",\n        keepAlive: false,\n        ...LOCAL_BROWSER_BODY,\n      }),\n    },\n  );\n\n  assert.equal(\n    ctx.status,\n    HTTP_OK,\n    `Expected local /start to succeed, got ${ctx.status}\\n${ctx.debugSummary()}`,\n  );\n  assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n  assertFetchOk(\n    isSuccessResponse(ctx.body),\n    \"Should return a successful start response\",\n    ctx,\n  );\n  return {\n    sessionId: ctx.body.data.sessionId,\n    cdpUrl: ctx.body.data.cdpUrl,\n  };\n}\n\nasync function startKeepAliveFalseBrowserbaseSession(\n  baseUrl: string,\n): Promise<string> {\n  assert.ok(bbApiKey, \"BROWSERBASE_API_KEY must be set\");\n  assert.ok(bbProjectId, \"BROWSERBASE_PROJECT_ID must be set\");\n  const headers = {\n    ...getHeaders(\"3.0.0\"),\n    \"x-bb-api-key\": bbApiKey,\n    \"x-bb-project-id\": bbProjectId,\n  };\n  const ctx = await fetchWithContext<StartResponse>(\n    `${baseUrl}/v1/sessions/start`,\n    {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        modelName: \"gpt-4.1-nano\",\n        keepAlive: false,\n        experimental: true,\n        browser: { type: \"browserbase\" },\n      }),\n    },\n  );\n\n  assert.equal(\n    ctx.status,\n    HTTP_OK,\n    `Expected browserbase /start to succeed, got ${ctx.status}\\n${ctx.debugSummary()}`,\n  );\n  assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n  assertFetchOk(\n    isSuccessResponse(ctx.body),\n    \"Should return a successful start response\",\n    ctx,\n  );\n  const sessionId = ctx.body.data.sessionId;\n\n  // Browserbase Stagehand init is lazy; navigate once to ensure supervisor is running.\n  const navigateCtx = await fetchWithContext<{ success?: boolean }>(\n    `${baseUrl}/v1/sessions/${sessionId}/navigate`,\n    {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({ url: \"https://example.com\", frameId: \"\" }),\n    },\n  );\n  assert.equal(\n    navigateCtx.status,\n    HTTP_OK,\n    `Expected browserbase /navigate to succeed, got ${navigateCtx.status}\\n${navigateCtx.debugSummary()}`,\n  );\n\n  return sessionId;\n}\n\nasync function closeLocalBrowserViaCdp(cdpUrl: string): Promise<void> {\n  const browser = await chromium.connectOverCDP(cdpUrl);\n  try {\n    const context = browser.contexts()[0];\n    if (!context) return;\n    const page = context.pages()[0] ?? (await context.newPage());\n    const cdp = await context.newCDPSession(page);\n    await cdp.send(\"Browser.close\");\n  } finally {\n    await browser.close().catch(() => {\n      // best-effort close of Playwright transport\n    });\n  }\n}\n\nasync function waitForBrowserbaseNotRunning(\n  sessionId: string,\n  timeoutMs: number,\n): Promise<string> {\n  assert.ok(bbApiKey, \"BROWSERBASE_API_KEY must be set\");\n  const bb = new Browserbase({ apiKey: bbApiKey });\n\n  let lastStatus = \"UNKNOWN\";\n  const startedAt = Date.now();\n  while (Date.now() - startedAt < timeoutMs) {\n    try {\n      const snapshot = (await bb.sessions.retrieve(sessionId)) as {\n        status?: string;\n      };\n      lastStatus = snapshot.status ?? \"UNKNOWN\";\n      if (lastStatus !== \"RUNNING\") {\n        return lastStatus;\n      }\n    } catch {\n      return \"RETRIEVE_FAILED\";\n    }\n    await sleep(1000);\n  }\n  throw new Error(\n    `Browserbase session ${sessionId} stayed RUNNING for ${timeoutMs}ms (last status=${lastStatus})`,\n  );\n}\n\nasync function requestBrowserbaseReleaseBestEffort(sessionId: string) {\n  if (!bbApiKey || !bbProjectId) return;\n  const bb = new Browserbase({ apiKey: bbApiKey });\n  try {\n    await bb.sessions.update(sessionId, {\n      status: \"REQUEST_RELEASE\",\n      projectId: bbProjectId,\n    });\n  } catch {\n    // best-effort cleanup\n  }\n}\n\n// =============================================================================\n// V3 Format Tests (x-sdk-version: 3.x.x header)\n// =============================================================================\n\ndescribe(\"POST /v1/sessions/start - V3 format\", () => {\n  const headers = getHeaders(\"3.0.0\");\n  const localBrowser = LOCAL_BROWSER_BODY;\n\n  it(\"should start session with modelName string and V3 header\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({ modelName: \"gpt-4.1-nano\", ...localBrowser }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    assertFetchOk(ctx.body.data.available, \"Session should be available\", ctx);\n    assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n    assertFetchOk(!!ctx.body.data.cdpUrl, \"Should have cdpUrl\", ctx);\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should start session with experimental flag\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          experimental: true,\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should accept x-language header for python V3\", async () => {\n    const url = getBaseUrl();\n    const pythonHeaders = getHeaders(\"1.0.0\", \"python\");\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers: pythonHeaders,\n        body: JSON.stringify({ modelName: \"gpt-4.1-nano\", ...localBrowser }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n\n    await endSession(ctx.body.data.sessionId, pythonHeaders);\n  });\n\n  it(\"should start session with extended options (timeouts, verbose)\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          actTimeoutMs: 30000,\n          domSettleTimeoutMs: 5000,\n          verbose: \"2\",\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    assertFetchOk(ctx.body.data.available, \"Session should be available\", ctx);\n    assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should return cdpUrl as a valid WebSocket URL for local browser\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({ modelName: \"gpt-4.1-nano\", ...localBrowser }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    // cdpUrl should not be empty since we eagerly launch the browser\n    assertFetchOk(\n      ctx.body.data.cdpUrl !== \"\",\n      \"cdpUrl should not be empty\",\n      ctx,\n    );\n    // cdpUrl should be a valid WebSocket URL\n    assertFetchOk(\n      ctx.body.data.cdpUrl.startsWith(\"ws://\"),\n      \"cdpUrl should be a WebSocket URL\",\n      ctx,\n    );\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should return provided cdpUrl when explicit cdpUrl is passed\", async () => {\n    const url = getBaseUrl();\n    const providedCdpUrl = \"ws://localhost:9222/devtools/browser/test\";\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browser: { type: \"local\", cdpUrl: providedCdpUrl },\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.cdpUrl === providedCdpUrl,\n      \"cdpUrl should match provided value\",\n      ctx,\n    );\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should return error for browserbase requests without API key\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browser: { type: \"browserbase\" },\n        }),\n      },\n    );\n\n    // Should fail because browserbase requires x-bb-api-key header\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST, \"Request should fail with 400\");\n  });\n\n  it(\"should start browserbase session with API key but no project ID\", async () => {\n    if (!bbApiKey) return; // skip when credentials unavailable\n\n    const url = getBaseUrl();\n    const bbHeaders = {\n      ...getHeaders(\"3.0.0\"),\n      \"x-bb-api-key\": bbApiKey,\n      // intentionally omitting x-bb-project-id\n    };\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers: bbHeaders,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browser: { type: \"browserbase\" },\n        }),\n      },\n    );\n\n    assertFetchStatus(\n      ctx,\n      HTTP_OK,\n      \"Request should succeed without project ID\",\n    );\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should return a successful start response\",\n      ctx,\n    );\n\n    await endSession(ctx.body.data.sessionId, bbHeaders);\n  });\n\n  // =============================================================================\n  // Multi-Region Support Tests\n  // =============================================================================\n\n  it(\"should accept non-default region in browserbaseSessionCreateParams\", async () => {\n    const url = getBaseUrl();\n\n    // Test with us-east-1 region - server should accept this request\n    // Note: Local browser sessions don't actually use the region, but the server\n    // should still accept the parameter without returning { available: false }\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"us-east-1\" as BrowserbaseRegion,\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    // The key assertion: non-default regions should NOT return available: false\n    assertFetchOk(\n      ctx.body.data.available === true,\n      \"Session should be available for non-default regions\",\n      ctx,\n    );\n    assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should accept eu-central-1 region in browserbaseSessionCreateParams\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"eu-central-1\" as BrowserbaseRegion,\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.available === true,\n      \"Session should be available for eu-central-1 region\",\n      ctx,\n    );\n    assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n\n  it(\"should accept ap-southeast-1 region in browserbaseSessionCreateParams\", async () => {\n    const url = getBaseUrl();\n\n    const ctx = await fetchWithContext<StartResponse>(\n      `${url}/v1/sessions/start`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          browserbaseSessionCreateParams: {\n            region: \"ap-southeast-1\" as BrowserbaseRegion,\n          },\n          ...localBrowser,\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_OK, \"Request should succeed\");\n    assertFetchOk(ctx.body !== null, \"Should have response body\", ctx);\n    assertFetchOk(\n      isSuccessResponse(ctx.body),\n      \"Should be a success response\",\n      ctx,\n    );\n    assertFetchOk(\n      ctx.body.data.available === true,\n      \"Session should be available for ap-southeast-1 region\",\n      ctx,\n    );\n    assertFetchOk(!!ctx.body.data.sessionId, \"Should have sessionId\", ctx);\n\n    await endSession(ctx.body.data.sessionId, headers);\n  });\n});\n\ndescribe(\"POST /v1/sessions/start - keepAlive=false supervision in SEA\", () => {\n  it(\"spawns a supervisor and exits it when chrome dies\", async () => {\n    const handle = await startSeaServer();\n    const seaPid = handle.proc.pid;\n    assert.ok(seaPid, \"SEA server must have a PID\");\n\n    const { cdpUrl } = await startKeepAliveFalseLocalSession(handle.baseUrl);\n    const supervisor = await waitForValue(\n      () => findLocalSupervisorByParentPid(seaPid),\n      10_000,\n    );\n\n    assert.ok(\n      supervisor.chromePid,\n      `Expected local supervisor to include --chrome-pid. args=${supervisor.args}`,\n    );\n    assert.ok(\n      isPidAlive(supervisor.pid),\n      `Supervisor PID ${supervisor.pid} should be alive`,\n    );\n    assert.ok(\n      isPidAlive(supervisor.chromePid),\n      `Chrome PID ${supervisor.chromePid} should be alive`,\n    );\n\n    await closeLocalBrowserViaCdp(cdpUrl);\n\n    await waitForPidState(supervisor.chromePid, false, 10_000);\n    await waitForPidState(supervisor.pid, false, 10_000);\n    assert.ok(\n      isPidAlive(seaPid),\n      \"SEA process should stay alive after chrome dies\",\n    );\n  });\n\n  it(\"force-killing SEA kills local chrome and exits supervisor within 10s\", async () => {\n    const handle = await startSeaServer();\n    const seaPid = handle.proc.pid;\n    assert.ok(seaPid, \"SEA server must have a PID\");\n\n    await startKeepAliveFalseLocalSession(handle.baseUrl);\n    const supervisor = await waitForValue(\n      () => findLocalSupervisorByParentPid(seaPid),\n      10_000,\n    );\n\n    assert.ok(\n      supervisor.chromePid,\n      `Expected local supervisor to include --chrome-pid. args=${supervisor.args}`,\n    );\n    assert.ok(\n      isPidAlive(supervisor.pid),\n      `Supervisor PID ${supervisor.pid} should be alive`,\n    );\n    assert.ok(\n      isPidAlive(supervisor.chromePid),\n      `Chrome PID ${supervisor.chromePid} should be alive`,\n    );\n\n    await forceKillSeaServer(handle);\n\n    await waitForPidState(supervisor.pid, false, 10_000);\n    await waitForPidState(supervisor.chromePid, false, 10_000);\n  });\n\n  it(\"force-killing SEA ends Browserbase session when keepAlive=false\", async () => {\n    const handle = await startSeaServer({ BB_ENV: \"prod\" });\n    const sessionId = await startKeepAliveFalseBrowserbaseSession(\n      handle.baseUrl,\n    );\n\n    try {\n      await forceKillSeaServer(handle);\n      const finalStatus = await waitForBrowserbaseNotRunning(sessionId, 30_000);\n      assert.notEqual(\n        finalStatus,\n        \"RUNNING\",\n        \"Browserbase session should not remain RUNNING after SEA kill\",\n      );\n    } finally {\n      await requestBrowserbaseReleaseBestEffort(sessionId);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/server-v3/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"verbatimModuleSyntax\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/server-v3/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"test\",\n    \"outDir\": \"dist/tests\",\n    \"declaration\": false,\n    \"noEmit\": false\n  },\n  \"include\": [\"test/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/server-v3/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: \"node\",\n    include: [\"test/**/*.test.ts\"],\n  },\n});\n"
  },
  {
    "path": "packages/server-v4/CHANGELOG.md",
    "content": "# @browserbasehq/stagehand-server-v4\n\n## 3.6.1\n\n### Patch Changes\n\n- Updated dependencies [[`505e8c6`](https://github.com/browserbase/stagehand/commit/505e8c6736f3706328dbc8df670c49a018058388), [`2f43ffa`](https://github.com/browserbase/stagehand/commit/2f43ffac11778152d17e4c44405770cc32c3ec8c), [`63ee247`](https://github.com/browserbase/stagehand/commit/63ee247ac6bf2992046d4f6b2759f46b15643e36), [`7dc35f5`](https://github.com/browserbase/stagehand/commit/7dc35f5e25689e6518d68b25ef71536d2781c8aa), [`335cf47`](https://github.com/browserbase/stagehand/commit/335cf4730e73bce33e92331d04bda4b0fd42685d), [`6ba0a1d`](https://github.com/browserbase/stagehand/commit/6ba0a1db7fc2d5d5a2f8927b1417d8f1d15eda10), [`4ff3bb8`](https://github.com/browserbase/stagehand/commit/4ff3bb831a6ef6e2d57148e7afb68ea8d23e395d), [`c27054b`](https://github.com/browserbase/stagehand/commit/c27054bbd0508431ade91d655f89efc87bbf5867), [`2abf5b9`](https://github.com/browserbase/stagehand/commit/2abf5b90f1e2bb1442509ef3a686b6128c9cdcf6), [`7817fcc`](https://github.com/browserbase/stagehand/commit/7817fcc315eee4455ce04567cf56c9ec801caf0b), [`7390508`](https://github.com/browserbase/stagehand/commit/73905088c5ed5923d276da9cce2efd0a0a3a46eb), [`611f43a`](https://github.com/browserbase/stagehand/commit/611f43ac8d4c580216d55d2b217c14a9a9c11013), [`521a10e`](https://github.com/browserbase/stagehand/commit/521a10e3698fc5631e219947bc90dad0f8bddaa8), [`2402a3c`](https://github.com/browserbase/stagehand/commit/2402a3c4d50270391b3e6440f4385cdcf5e1eb64)]:\n  - @browserbasehq/stagehand@3.2.0\n"
  },
  {
    "path": "packages/server-v4/README.md",
    "content": "# Stagehand API\n\nThe Stagehand  is a powerful service that provides a RESTful interface for browser automation and session management using the Browserbase platform. It enables recording, playback, and manipulation of browser sessions with a focus on reliability and performance.\n\n## 📋 Prerequisites\n\nTo run the Stagehand API locally, ensure you have the following installed:\n\n- Node.js\n- pnpm\n\n## 🛠 Installation\n\n1. Clone the repository:\n\n```bash\ngit clone https://github.com/browserbase/stagehand/\ncd stagehand/packages/server-v4\n```\n\n2. Install dependencies:\n\n```bash\npnpm install\n```\n\n3. Set up environment variables:\n\n```bash\ncp .env.example .env\n```\n\n4. Configure your `.env` file with the environment variables required by `src/lib/env.ts` (BB environment, API base URLs, etc.).\n\n5. `pnpm dev`\n\n"
  },
  {
    "path": "packages/server-v4/openapi.v4.yaml",
    "content": "openapi: \"3.1.0\"\ninfo:\n  title: Stagehand API v4\n  version: \"4.0.0\"\n  description: >-\n    Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\n\n    execute browser automation tasks remotely on the Browserbase cloud.\n\n    Create a browser session with /browsersession, then use that id with page\n    routes.\n\n    Responses are streamed using Server-Sent Events (SSE) when the\n\n    `x-stream-response: true` header is provided.\n\n\n    This SDK is currently ALPHA software and is not production ready!\n\n    Please try it and give us your feedback, stay tuned for upcoming release\n    announcements!\n  contact:\n    name: Browserbase\n    url: https://browserbase.com\ncomponents:\n  securitySchemes:\n    BrowserbaseApiKey:\n      type: apiKey\n      in: header\n      name: x-bb-api-key\n      description: Browserbase API key for authentication\n    BrowserbaseProjectId:\n      type: apiKey\n      in: header\n      name: x-bb-project-id\n      description: Browserbase project ID\n    ModelApiKey:\n      type: apiKey\n      in: header\n      name: x-model-api-key\n      description: API key for the AI model provider (OpenAI, Anthropic, etc.)\n  links:\n    SessionAct:\n      operationId: SessionAct\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Perform an action on the session\n    SessionExtract:\n      operationId: SessionExtract\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Extract data from the session\n    SessionObserve:\n      operationId: SessionObserve\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Observe available actions on the session\n    SessionNavigate:\n      operationId: SessionNavigate\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Navigate to a URL in the session\n    SessionAgentExecute:\n      operationId: SessionAgentExecute\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Execute an agent on the session\n    SessionReplay:\n      operationId: SessionReplay\n      parameters:\n        id: $response.body#/data/sessionId\n      description: Replay session metrics\n    SessionEnd:\n      operationId: SessionEnd\n      parameters:\n        id: $response.body#/data/sessionId\n      description: End the session and release resources\n  schemas:\n    BrowserSessionLocalCreateRequest:\n      type: object\n      properties:\n        modelName:\n          description: Model name to use for AI operations\n          example: openai/gpt-4.1-nano\n          type: string\n        domSettleTimeoutMs:\n          type: number\n        verbose:\n          anyOf:\n            - type: number\n              const: 0\n            - type: number\n              const: 1\n            - type: number\n              const: 2\n        systemPrompt:\n          type: string\n        selfHeal:\n          type: boolean\n        waitForCaptchaSolves:\n          type: boolean\n        experimental:\n          type: boolean\n        actTimeoutMs:\n          type: number\n        env:\n          type: string\n          const: LOCAL\n        cdpUrl:\n          type: string\n        localBrowserLaunchOptions:\n          $ref: \"#/components/schemas/LocalBrowserLaunchOptions\"\n      required:\n        - modelName\n        - env\n      additionalProperties: false\n    BrowserSessionBrowserbaseCreateRequest:\n      type: object\n      properties:\n        modelName:\n          description: Model name to use for AI operations\n          example: openai/gpt-4.1-nano\n          type: string\n        domSettleTimeoutMs:\n          type: number\n        verbose:\n          anyOf:\n            - type: number\n              const: 0\n            - type: number\n              const: 1\n            - type: number\n              const: 2\n        systemPrompt:\n          type: string\n        selfHeal:\n          type: boolean\n        waitForCaptchaSolves:\n          type: boolean\n        experimental:\n          type: boolean\n        actTimeoutMs:\n          type: number\n        env:\n          type: string\n          const: BROWSERBASE\n        browserbaseSessionId:\n          type: string\n        browserbaseSessionCreateParams:\n          $ref: \"#/components/schemas/BrowserbaseSessionCreateParams\"\n      required:\n        - modelName\n        - env\n      additionalProperties: false\n    BrowserbaseBrowserSettings:\n      type: object\n      properties:\n        advancedStealth:\n          type: boolean\n        blockAds:\n          type: boolean\n        context:\n          $ref: \"#/components/schemas/BrowserbaseContext\"\n        extensionId:\n          type: string\n        fingerprint:\n          $ref: \"#/components/schemas/BrowserbaseFingerprint\"\n        logSession:\n          type: boolean\n        recordSession:\n          type: boolean\n        solveCaptchas:\n          type: boolean\n        viewport:\n          $ref: \"#/components/schemas/BrowserbaseViewport\"\n    BrowserbaseContext:\n      type: object\n      properties:\n        id:\n          type: string\n        persist:\n          type: boolean\n      required:\n        - id\n    BrowserbaseFingerprint:\n      type: object\n      properties:\n        browsers:\n          type: array\n          items:\n            type: string\n            enum:\n              - chrome\n              - edge\n              - firefox\n              - safari\n        devices:\n          type: array\n          items:\n            type: string\n            enum:\n              - desktop\n              - mobile\n        httpVersion:\n          type: string\n          enum:\n            - \"1\"\n            - \"2\"\n        locales:\n          type: array\n          items:\n            type: string\n        operatingSystems:\n          type: array\n          items:\n            type: string\n            enum:\n              - android\n              - ios\n              - linux\n              - macos\n              - windows\n        screen:\n          $ref: \"#/components/schemas/BrowserbaseFingerprintScreen\"\n    BrowserbaseFingerprintScreen:\n      type: object\n      properties:\n        maxHeight:\n          type: number\n        maxWidth:\n          type: number\n        minHeight:\n          type: number\n        minWidth:\n          type: number\n    BrowserbaseViewport:\n      type: object\n      properties:\n        width:\n          type: number\n        height:\n          type: number\n    ProxyConfig:\n      oneOf:\n        - $ref: \"#/components/schemas/BrowserbaseProxyConfig\"\n        - $ref: \"#/components/schemas/ExternalProxyConfig\"\n      type: object\n      discriminator:\n        propertyName: type\n        mapping:\n          browserbase: \"#/components/schemas/BrowserbaseProxyConfig\"\n          external: \"#/components/schemas/ExternalProxyConfig\"\n    BrowserbaseProxyConfig:\n      type: object\n      properties:\n        type:\n          type: string\n          const: browserbase\n        domainPattern:\n          type: string\n        geolocation:\n          $ref: \"#/components/schemas/BrowserbaseProxyGeolocation\"\n      required:\n        - type\n    BrowserbaseProxyGeolocation:\n      type: object\n      properties:\n        country:\n          type: string\n        city:\n          type: string\n        state:\n          type: string\n      required:\n        - country\n    ExternalProxyConfig:\n      type: object\n      properties:\n        type:\n          type: string\n          const: external\n        server:\n          type: string\n        domainPattern:\n          type: string\n        username:\n          type: string\n        password:\n          type: string\n      required:\n        - type\n        - server\n    BrowserbaseRegion:\n      type: string\n      enum:\n        - us-west-2\n        - us-east-1\n        - eu-central-1\n        - ap-southeast-1\n    SessionHeaders:\n      type: object\n      properties:\n        x-stream-response:\n          description: Whether to stream the response via SSE\n          example: \"true\"\n          type: string\n          enum:\n            - \"true\"\n            - \"false\"\n    BrowserSessionAddInitScriptResult:\n      type: object\n      properties:\n        added:\n          type: boolean\n      required:\n        - added\n      additionalProperties: false\n    BrowserSessionSetExtraHTTPHeadersResult:\n      type: object\n      properties:\n        headers:\n          $ref: \"#/components/schemas/PageHeaders\"\n      required:\n        - headers\n      additionalProperties: false\n    BrowserSessionPagesResult:\n      type: object\n      properties:\n        pages:\n          type: array\n          items:\n            $ref: \"#/components/schemas/BrowserSessionPage\"\n      required:\n        - pages\n      additionalProperties: false\n    BrowserSessionOptionalPageResult:\n      type: object\n      properties:\n        page:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionPage\"\n            - type: \"null\"\n      required:\n        - page\n      additionalProperties: false\n    BrowserSessionPageResult:\n      type: object\n      properties:\n        page:\n          $ref: \"#/components/schemas/BrowserSessionPage\"\n      required:\n        - page\n      additionalProperties: false\n    BrowserSessionFrameTreeResult:\n      type: object\n      properties:\n        frameTree: {}\n      required:\n        - frameTree\n      additionalProperties: false\n    BrowserSessionCookiesResult:\n      type: object\n      properties:\n        cookies:\n          type: array\n          items:\n            $ref: \"#/components/schemas/BrowserSessionCookie\"\n      required:\n        - cookies\n      additionalProperties: false\n    BrowserSessionAddCookiesResult:\n      type: object\n      properties:\n        added:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - added\n      additionalProperties: false\n    BrowserSessionClearCookiesResult:\n      type: object\n      properties:\n        cleared:\n          type: boolean\n      required:\n        - cleared\n      additionalProperties: false\n    BrowserSessionConnectURLResult:\n      type: object\n      properties:\n        connectURL:\n          type: string\n      required:\n        - connectURL\n      additionalProperties: false\n    BrowserSessionConfiguredViewportResult:\n      $ref: \"#/components/schemas/BrowserSessionViewport\"\n    BrowserSessionBrowserbaseSessionIDResult:\n      type: object\n      properties:\n        browserbaseSessionID:\n          anyOf:\n            - type: string\n            - type: \"null\"\n      required:\n        - browserbaseSessionID\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionURLResult:\n      type: object\n      properties:\n        browserbaseSessionURL:\n          anyOf:\n            - type: string\n            - type: \"null\"\n      required:\n        - browserbaseSessionURL\n      additionalProperties: false\n    BrowserSessionBrowserbaseDebugURLResult:\n      type: object\n      properties:\n        browserbaseDebugURL:\n          anyOf:\n            - type: string\n            - type: \"null\"\n      required:\n        - browserbaseDebugURL\n      additionalProperties: false\n    BrowserSessionIsBrowserbaseResult:\n      type: object\n      properties:\n        isBrowserbase:\n          type: boolean\n      required:\n        - isBrowserbase\n      additionalProperties: false\n    BrowserSessionIsAdvancedStealthResult:\n      type: object\n      properties:\n        isAdvancedStealth:\n          type: boolean\n      required:\n        - isAdvancedStealth\n      additionalProperties: false\n    BrowserSessionSetViewportSizeResult:\n      $ref: \"#/components/schemas/BrowserSessionViewport\"\n    BrowserSessionCloseResult:\n      type: object\n      properties:\n        closed:\n          type: boolean\n      required:\n        - closed\n      additionalProperties: false\n    PageXPathResult:\n      type: object\n      properties:\n        xpath:\n          type: string\n      additionalProperties: false\n    PageDragAndDropResult:\n      type: object\n      properties:\n        fromXpath:\n          type: string\n        toXpath:\n          type: string\n      additionalProperties: false\n    PageTypeResult:\n      type: object\n      properties:\n        text:\n          type: string\n      required:\n        - text\n      additionalProperties: false\n    PageKeyPressResult:\n      type: object\n      properties:\n        key:\n          type: string\n      required:\n        - key\n      additionalProperties: false\n    PageEnableCursorOverlayResult:\n      type: object\n      properties:\n        enabled:\n          type: boolean\n      required:\n        - enabled\n      additionalProperties: false\n    PageAddInitScriptResult:\n      type: object\n      properties:\n        added:\n          type: boolean\n      required:\n        - added\n      additionalProperties: false\n    PageNavigationResult:\n      type: object\n      properties:\n        url:\n          type: string\n        response:\n          anyOf:\n            - type: object\n              properties:\n                url:\n                  type: string\n                status:\n                  type: integer\n                  minimum: -9007199254740991\n                  maximum: 9007199254740991\n                statusText:\n                  type: string\n                ok:\n                  type: boolean\n                headers:\n                  $ref: \"#/components/schemas/PageHeaders\"\n              required:\n                - url\n                - status\n                - statusText\n                - ok\n                - headers\n              additionalProperties: false\n            - type: \"null\"\n      required:\n        - url\n        - response\n      additionalProperties: false\n    PageTargetIdResult:\n      type: object\n      properties:\n        targetId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - targetId\n      additionalProperties: false\n    PageMainFrameIdResult:\n      type: object\n      properties:\n        mainFrameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - mainFrameId\n      additionalProperties: false\n    PageMainFrameResult:\n      type: object\n      properties:\n        frame:\n          $ref: \"#/components/schemas/PageFrame\"\n      required:\n        - frame\n      additionalProperties: false\n    PageFrame:\n      type: object\n      properties:\n        frameId:\n          $ref: \"#/components/schemas/FrameId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        sessionId:\n          anyOf:\n            - $ref: \"#/components/schemas/CDPSessionId\"\n            - type: \"null\"\n        isBrowserRemote:\n          type: boolean\n      required:\n        - frameId\n        - pageId\n        - sessionId\n        - isBrowserRemote\n      additionalProperties: false\n    PageFrameTreeResult:\n      type: object\n      properties:\n        frameTree: {}\n      required:\n        - frameTree\n      additionalProperties: false\n    PageListAllFrameIdsResult:\n      type: object\n      properties:\n        frameIds:\n          type: array\n          items:\n            $ref: \"#/components/schemas/FrameId\"\n      required:\n        - frameIds\n      additionalProperties: false\n    PageGetOrdinalResult:\n      type: object\n      properties:\n        frameId:\n          $ref: \"#/components/schemas/FrameId\"\n        ordinal:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - frameId\n        - ordinal\n      additionalProperties: false\n    PageTitleResult:\n      type: object\n      properties:\n        title:\n          type: string\n      required:\n        - title\n      additionalProperties: false\n    PageUrlResult:\n      type: object\n      properties:\n        url:\n          type: string\n      required:\n        - url\n      additionalProperties: false\n    PageScreenshotResult:\n      type: object\n      properties:\n        base64:\n          type: string\n        mimeType:\n          $ref: \"#/components/schemas/ScreenshotMimeType\"\n      required:\n        - base64\n        - mimeType\n      additionalProperties: false\n    PageSnapshotResult:\n      type: object\n      properties:\n        formattedTree:\n          type: string\n        xpathMap:\n          type: object\n          properties: {}\n          additionalProperties:\n            type: string\n        urlMap:\n          type: object\n          properties: {}\n          additionalProperties:\n            type: string\n      required:\n        - formattedTree\n        - xpathMap\n        - urlMap\n      additionalProperties: false\n    PageFramesResult:\n      type: object\n      properties:\n        frames:\n          type: array\n          items:\n            $ref: \"#/components/schemas/PageFrame\"\n      required:\n        - frames\n      additionalProperties: false\n    PageSetViewportSizeResult:\n      type: object\n      properties:\n        width:\n          type: number\n          exclusiveMinimum: 0\n        height:\n          type: number\n          exclusiveMinimum: 0\n        deviceScaleFactor:\n          type: number\n          exclusiveMinimum: 0\n      required:\n        - width\n        - height\n      additionalProperties: false\n    PageSetExtraHTTPHeadersResult:\n      type: object\n      properties:\n        headers:\n          $ref: \"#/components/schemas/PageHeaders\"\n      required:\n        - headers\n      additionalProperties: false\n    PageWaitForLoadStateResult:\n      type: object\n      properties:\n        state:\n          $ref: \"#/components/schemas/LoadState\"\n      required:\n        - state\n      additionalProperties: false\n    PageWaitForSelectorResult:\n      type: object\n      properties:\n        selector:\n          $ref: \"#/components/schemas/ElementSelector\"\n        matched:\n          type: boolean\n      required:\n        - selector\n        - matched\n      additionalProperties: false\n    PageWaitForTimeoutResult:\n      type: object\n      properties:\n        ms:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - ms\n      additionalProperties: false\n    PageEvaluateResult:\n      type: object\n      properties:\n        value: {}\n      required:\n        - value\n      additionalProperties: false\n    PageSendCDPResult:\n      type: object\n      properties:\n        value: {}\n      required:\n        - value\n      additionalProperties: false\n    PageCloseResult:\n      type: object\n      properties:\n        closed:\n          type: boolean\n      required:\n        - closed\n      additionalProperties: false\n    LocalBrowserLaunchOptions:\n      type: object\n      properties:\n        args:\n          type: array\n          items:\n            type: string\n        executablePath:\n          type: string\n        port:\n          type: number\n        userDataDir:\n          type: string\n        preserveUserDataDir:\n          type: boolean\n        headless:\n          type: boolean\n        devtools:\n          type: boolean\n        chromiumSandbox:\n          type: boolean\n        ignoreDefaultArgs:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                type: string\n        proxy:\n          type: object\n          properties:\n            server:\n              type: string\n            bypass:\n              type: string\n            username:\n              type: string\n            password:\n              type: string\n          required:\n            - server\n        locale:\n          type: string\n        viewport:\n          type: object\n          properties:\n            width:\n              type: number\n            height:\n              type: number\n          required:\n            - width\n            - height\n        deviceScaleFactor:\n          type: number\n        hasTouch:\n          type: boolean\n        ignoreHTTPSErrors:\n          type: boolean\n        cdpUrl:\n          type: string\n        cdpHeaders:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties:\n            type: string\n        connectTimeoutMs:\n          type: number\n        downloadsPath:\n          type: string\n        acceptDownloads:\n          type: boolean\n      additionalProperties: false\n    BrowserbaseSessionCreateParams:\n      type: object\n      properties:\n        projectId:\n          type: string\n        browserSettings:\n          $ref: \"#/components/schemas/BrowserbaseBrowserSettings\"\n        extensionId:\n          type: string\n        keepAlive:\n          type: boolean\n        proxies:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                $ref: \"#/components/schemas/ProxyConfig\"\n        region:\n          $ref: \"#/components/schemas/BrowserbaseRegion\"\n        timeout:\n          type: number\n        userMetadata:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n    BrowserSessionId:\n      example: session_01JXAMPLE\n      type: string\n      minLength: 1\n    BrowserSessionEnv:\n      type: string\n      enum:\n        - LOCAL\n        - BROWSERBASE\n    BrowserSessionStatus:\n      type: string\n      enum:\n        - running\n        - ended\n    BrowserSessionCreateRequest:\n      oneOf:\n        - $ref: \"#/components/schemas/BrowserSessionLocalCreateRequest\"\n        - $ref: \"#/components/schemas/BrowserSessionBrowserbaseCreateRequest\"\n      type: object\n      discriminator:\n        propertyName: env\n        mapping:\n          LOCAL: \"#/components/schemas/BrowserSessionLocalCreateRequest\"\n          BROWSERBASE: \"#/components/schemas/BrowserSessionBrowserbaseCreateRequest\"\n    BrowserSessionEndRequest:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSession:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        env:\n          $ref: \"#/components/schemas/BrowserSessionEnv\"\n        status:\n          $ref: \"#/components/schemas/BrowserSessionStatus\"\n        modelName:\n          type: string\n        cdpUrl:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        available:\n          type: boolean\n        browserbaseSessionId:\n          type: string\n        browserbaseSessionCreateParams:\n          $ref: \"#/components/schemas/BrowserbaseSessionCreateParams\"\n        localBrowserLaunchOptions:\n          $ref: \"#/components/schemas/LocalBrowserLaunchOptions\"\n        domSettleTimeoutMs:\n          type: number\n        verbose:\n          anyOf:\n            - type: number\n              const: 0\n            - type: number\n              const: 1\n            - type: number\n              const: 2\n        systemPrompt:\n          type: string\n        selfHeal:\n          type: boolean\n        waitForCaptchaSolves:\n          type: boolean\n        experimental:\n          type: boolean\n        actTimeoutMs:\n          type: number\n      required:\n        - id\n        - env\n        - status\n        - modelName\n        - available\n      additionalProperties: false\n    BrowserSessionResult:\n      type: object\n      properties:\n        browserSession:\n          $ref: \"#/components/schemas/BrowserSession\"\n      required:\n        - browserSession\n      additionalProperties: false\n    BrowserSessionActionMethod:\n      type: string\n      enum:\n        - addInitScript\n        - setExtraHTTPHeaders\n        - pages\n        - activePage\n        - awaitActivePage\n        - resolvePageByMainFrameId\n        - getFullFrameTreeByMainFrameId\n        - newPage\n        - cookies\n        - addCookies\n        - clearCookies\n        - connectURL\n        - configuredViewport\n        - browserbaseSessionID\n        - browserbaseSessionURL\n        - browserbaseDebugURL\n        - isBrowserbase\n        - isAdvancedStealth\n        - setViewportSize\n        - close\n    BrowserSessionActionStatus:\n      type: string\n      enum:\n        - queued\n        - running\n        - completed\n        - failed\n        - canceled\n    BrowserSessionPage:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        targetId:\n          $ref: \"#/components/schemas/PageId\"\n        mainFrameId:\n          $ref: \"#/components/schemas/FrameId\"\n        url:\n          type: string\n      required:\n        - pageId\n        - targetId\n        - mainFrameId\n        - url\n      additionalProperties: false\n    BrowserSessionCookie:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        domain:\n          type: string\n        path:\n          type: string\n        expires:\n          type: number\n        httpOnly:\n          type: boolean\n        secure:\n          type: boolean\n        sameSite:\n          type: string\n          enum:\n            - Strict\n            - Lax\n            - None\n      required:\n        - name\n        - value\n        - domain\n        - path\n        - expires\n        - httpOnly\n        - secure\n        - sameSite\n      additionalProperties: false\n    BrowserSessionCookieParam:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        url:\n          type: string\n        domain:\n          type: string\n        path:\n          type: string\n        expires:\n          type: number\n        httpOnly:\n          type: boolean\n        secure:\n          type: boolean\n        sameSite:\n          type: string\n          enum:\n            - Strict\n            - Lax\n            - None\n      required:\n        - name\n        - value\n      additionalProperties: false\n    BrowserSessionRegex:\n      type: object\n      properties:\n        source:\n          type: string\n        flags:\n          type: string\n      required:\n        - source\n      additionalProperties: false\n    BrowserSessionStringPattern:\n      anyOf:\n        - type: string\n        - $ref: \"#/components/schemas/BrowserSessionRegex\"\n    BrowserSessionClearCookiesOptions:\n      type: object\n      properties:\n        name:\n          $ref: \"#/components/schemas/BrowserSessionStringPattern\"\n        domain:\n          $ref: \"#/components/schemas/BrowserSessionStringPattern\"\n        path:\n          $ref: \"#/components/schemas/BrowserSessionStringPattern\"\n      additionalProperties: false\n    BrowserSessionViewport:\n      type: object\n      properties:\n        width:\n          type: number\n          exclusiveMinimum: 0\n        height:\n          type: number\n          exclusiveMinimum: 0\n        deviceScaleFactor:\n          type: number\n          exclusiveMinimum: 0\n      required:\n        - width\n        - height\n      additionalProperties: false\n    BrowserSessionAddInitScriptParams:\n      type: object\n      properties:\n        script:\n          $ref: \"#/components/schemas/PageInitScript\"\n      required:\n        - script\n      additionalProperties: false\n    BrowserSessionSetExtraHTTPHeadersParams:\n      type: object\n      properties:\n        headers:\n          $ref: \"#/components/schemas/PageHeaders\"\n      required:\n        - headers\n      additionalProperties: false\n    BrowserSessionPagesParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionActivePageParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionAwaitActivePageParams:\n      type: object\n      properties:\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      additionalProperties: false\n    BrowserSessionResolvePageByMainFrameIdParams:\n      type: object\n      properties:\n        mainFrameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - mainFrameId\n      additionalProperties: false\n    BrowserSessionGetFullFrameTreeByMainFrameIdParams:\n      type: object\n      properties:\n        mainFrameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - mainFrameId\n      additionalProperties: false\n    BrowserSessionNewPageParams:\n      type: object\n      properties:\n        url:\n          type: string\n      additionalProperties: false\n    BrowserSessionCookiesParams:\n      type: object\n      properties:\n        urls:\n          anyOf:\n            - type: string\n            - type: array\n              items:\n                type: string\n      additionalProperties: false\n    BrowserSessionAddCookiesParams:\n      type: object\n      properties:\n        cookies:\n          type: array\n          items:\n            $ref: \"#/components/schemas/BrowserSessionCookieParam\"\n      required:\n        - cookies\n      additionalProperties: false\n    BrowserSessionClearCookiesParams:\n      $ref: \"#/components/schemas/BrowserSessionClearCookiesOptions\"\n    BrowserSessionConnectURLParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionConfiguredViewportParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionIDParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionURLParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionBrowserbaseDebugURLParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionIsBrowserbaseParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionIsAdvancedStealthParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionSetViewportSizeParams:\n      $ref: \"#/components/schemas/BrowserSessionViewport\"\n    BrowserSessionCloseParams:\n      type: object\n      properties: {}\n      additionalProperties: false\n    BrowserSessionAddInitScriptRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionAddInitScriptParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionSetExtraHTTPHeadersRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionPagesRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionPagesParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionActivePageRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionActivePageParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionAwaitActivePageRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionAwaitActivePageParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionResolvePageByMainFrameIdRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionResolvePageByMainFrameIdParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionGetFullFrameTreeByMainFrameIdRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionGetFullFrameTreeByMainFrameIdParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionNewPageRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionNewPageParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionCookiesRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionCookiesParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionAddCookiesRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionAddCookiesParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionClearCookiesRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionClearCookiesParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionConnectURLRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionConnectURLParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionConfiguredViewportRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionConfiguredViewportParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionIDRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionURLRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionBrowserbaseDebugURLRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionAddInitScriptAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: addInitScript\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionAddInitScriptParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionAddInitScriptResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionSetExtraHTTPHeadersAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: setExtraHTTPHeaders\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionPagesAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: pages\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionPagesParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionPagesResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionActivePageAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: activePage\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionActivePageParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionOptionalPageResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionAwaitActivePageAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: awaitActivePage\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionAwaitActivePageParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionPageResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionResolvePageByMainFrameIdAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: resolvePageByMainFrameId\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionResolvePageByMainFrameIdParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionOptionalPageResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionGetFullFrameTreeByMainFrameIdAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: getFullFrameTreeByMainFrameId\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionGetFullFrameTreeByMainFrameIdParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionFrameTreeResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionNewPageAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: newPage\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionNewPageParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionPageResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionCookiesAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: cookies\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionCookiesParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionCookiesResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionAddCookiesAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: addCookies\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionAddCookiesParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionAddCookiesResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionClearCookiesAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: clearCookies\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionClearCookiesParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionClearCookiesResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionConnectURLAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: connectURL\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionConnectURLParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionConnectURLResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionConfiguredViewportAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: configuredViewport\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionConfiguredViewportParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionConfiguredViewportResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionIDAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: browserbaseSessionID\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionURLAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: browserbaseSessionURL\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionBrowserbaseDebugURLAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: browserbaseDebugURL\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionIsBrowserbaseAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: isBrowserbase\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionIsBrowserbaseParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionIsBrowserbaseResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionIsAdvancedStealthAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: isAdvancedStealth\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionIsAdvancedStealthParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionIsAdvancedStealthResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionSetViewportSizeAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: setViewportSize\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionSetViewportSizeParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionSetViewportSizeResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionCloseAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: close\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionCloseParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/BrowserSessionCloseResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    BrowserSessionAction:\n      anyOf:\n        - $ref: \"#/components/schemas/BrowserSessionAddInitScriptAction\"\n        - $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersAction\"\n        - $ref: \"#/components/schemas/BrowserSessionPagesAction\"\n        - $ref: \"#/components/schemas/BrowserSessionActivePageAction\"\n        - $ref: \"#/components/schemas/BrowserSessionAwaitActivePageAction\"\n        - $ref: \"#/components/schemas/BrowserSessionResolvePageByMainFrameIdAction\"\n        - $ref: \"#/components/schemas/BrowserSessionGetFullFrameTreeByMainFrameIdAction\"\n        - $ref: \"#/components/schemas/BrowserSessionNewPageAction\"\n        - $ref: \"#/components/schemas/BrowserSessionCookiesAction\"\n        - $ref: \"#/components/schemas/BrowserSessionAddCookiesAction\"\n        - $ref: \"#/components/schemas/BrowserSessionClearCookiesAction\"\n        - $ref: \"#/components/schemas/BrowserSessionConnectURLAction\"\n        - $ref: \"#/components/schemas/BrowserSessionConfiguredViewportAction\"\n        - $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDAction\"\n        - $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLAction\"\n        - $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLAction\"\n        - $ref: \"#/components/schemas/BrowserSessionIsBrowserbaseAction\"\n        - $ref: \"#/components/schemas/BrowserSessionIsAdvancedStealthAction\"\n        - $ref: \"#/components/schemas/BrowserSessionSetViewportSizeAction\"\n        - $ref: \"#/components/schemas/BrowserSessionCloseAction\"\n    RequestId:\n      example: req_01JXAMPLE\n      type: string\n      minLength: 1\n    SessionId:\n      example: session_01JXAMPLE\n      type: string\n      minLength: 1\n    PageId:\n      example: target_01JXAMPLE\n      type: string\n      minLength: 1\n    FrameId:\n      example: frame_01JXAMPLE\n      type: string\n      minLength: 1\n    ActionId:\n      example: action_01JXAMPLE\n      type: string\n      minLength: 1\n    CDPSessionId:\n      example: cdp-session_01JXAMPLE\n      type: string\n      minLength: 1\n    Timestamp:\n      example: 2026-02-03T12:00:00.000Z\n      type: string\n      format: date-time\n      pattern: ^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$\n    MouseButton:\n      type: string\n      enum:\n        - left\n        - right\n        - middle\n    LoadState:\n      type: string\n      enum:\n        - load\n        - domcontentloaded\n        - networkidle\n    WaitForSelectorState:\n      type: string\n      enum:\n        - attached\n        - detached\n        - visible\n        - hidden\n    ScreenshotType:\n      type: string\n      enum:\n        - png\n        - jpeg\n    ScreenshotMimeType:\n      type: string\n      enum:\n        - image/png\n        - image/jpeg\n    ScreenshotScale:\n      type: string\n      enum:\n        - css\n        - device\n    ScreenshotAnimations:\n      type: string\n      enum:\n        - allow\n        - disabled\n    ScreenshotCaret:\n      type: string\n      enum:\n        - hide\n        - initial\n    PageActionMethod:\n      type: string\n      enum:\n        - click\n        - hover\n        - scroll\n        - dragAndDrop\n        - type\n        - keyPress\n        - enableCursorOverlay\n        - addInitScript\n        - goto\n        - reload\n        - goBack\n        - goForward\n        - targetId\n        - mainFrameId\n        - mainFrame\n        - getFullFrameTree\n        - asProtocolFrameTree\n        - listAllFrameIds\n        - getOrdinal\n        - title\n        - url\n        - screenshot\n        - snapshot\n        - frames\n        - setViewportSize\n        - setExtraHTTPHeaders\n        - waitForLoadState\n        - waitForMainLoadState\n        - waitForSelector\n        - waitForTimeout\n        - evaluate\n        - sendCDP\n        - close\n    PageActionStatus:\n      type: string\n      enum:\n        - queued\n        - running\n        - completed\n        - failed\n        - canceled\n    XPathSelector:\n      type: object\n      properties:\n        xpath:\n          example: //button[text()='Submit']\n          type: string\n          minLength: 1\n        idx:\n          example: 0\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - xpath\n      additionalProperties: false\n    CssSelector:\n      type: object\n      properties:\n        css:\n          example: .btn-submit\n          type: string\n          minLength: 1\n        idx:\n          example: 0\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - css\n      additionalProperties: false\n    TextSelector:\n      type: object\n      properties:\n        text:\n          example: Submit\n          type: string\n          minLength: 1\n        idx:\n          example: 0\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - text\n      additionalProperties: false\n    CoordinateSelector:\n      type: object\n      properties:\n        x:\n          type: number\n        y:\n          type: number\n      required:\n        - x\n        - y\n      additionalProperties: false\n    Selector:\n      anyOf:\n        - $ref: \"#/components/schemas/XPathSelector\"\n        - $ref: \"#/components/schemas/CssSelector\"\n        - $ref: \"#/components/schemas/TextSelector\"\n        - $ref: \"#/components/schemas/CoordinateSelector\"\n    ElementSelector:\n      anyOf:\n        - $ref: \"#/components/schemas/XPathSelector\"\n        - $ref: \"#/components/schemas/CssSelector\"\n        - $ref: \"#/components/schemas/TextSelector\"\n    PageHeaders:\n      type: object\n      properties: {}\n      additionalProperties:\n        type: string\n    PageInitScript:\n      anyOf:\n        - type: string\n          minLength: 1\n        - type: object\n          properties:\n            path:\n              type: string\n              minLength: 1\n            content:\n              type: string\n              minLength: 1\n          additionalProperties: false\n    PageClip:\n      type: object\n      properties:\n        x:\n          type: number\n        y:\n          type: number\n        width:\n          type: integer\n          exclusiveMinimum: 0\n          maximum: 9007199254740991\n        height:\n          type: integer\n          exclusiveMinimum: 0\n          maximum: 9007199254740991\n      required:\n        - x\n        - y\n        - width\n        - height\n      additionalProperties: false\n    PageError:\n      type: string\n      minLength: 1\n    PageClickParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        selector:\n          $ref: \"#/components/schemas/Selector\"\n        button:\n          $ref: \"#/components/schemas/MouseButton\"\n        clickCount:\n          type: integer\n          minimum: 1\n          maximum: 9007199254740991\n      required:\n        - selector\n      additionalProperties: false\n    PageHoverParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        selector:\n          $ref: \"#/components/schemas/Selector\"\n      required:\n        - selector\n      additionalProperties: false\n    PageScrollElementParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        selector:\n          $ref: \"#/components/schemas/ElementSelector\"\n        percentage:\n          type: number\n          minimum: 0\n          maximum: 100\n      required:\n        - selector\n        - percentage\n      additionalProperties: false\n    PageScrollCoordinateParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        selector:\n          $ref: \"#/components/schemas/CoordinateSelector\"\n        deltaX:\n          type: number\n        deltaY:\n          type: number\n      required:\n        - selector\n        - deltaY\n      additionalProperties: false\n    PageScrollParams:\n      anyOf:\n        - $ref: \"#/components/schemas/PageScrollElementParams\"\n        - $ref: \"#/components/schemas/PageScrollCoordinateParams\"\n    PageDragAndDropParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        from:\n          $ref: \"#/components/schemas/Selector\"\n        to:\n          $ref: \"#/components/schemas/Selector\"\n        button:\n          $ref: \"#/components/schemas/MouseButton\"\n        steps:\n          type: integer\n          exclusiveMinimum: 0\n          maximum: 9007199254740991\n        delay:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - from\n        - to\n      additionalProperties: false\n    PageTypeParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        text:\n          type: string\n        delay:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n        withMistakes:\n          type: boolean\n      required:\n        - text\n      additionalProperties: false\n    PageKeyPressParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        key:\n          type: string\n          minLength: 1\n        delay:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - key\n      additionalProperties: false\n    PageEnableCursorOverlayParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageAddInitScriptParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        script:\n          $ref: \"#/components/schemas/PageInitScript\"\n      required:\n        - script\n      additionalProperties: false\n    PageGotoParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        url:\n          type: string\n          format: uri\n        waitUntil:\n          $ref: \"#/components/schemas/LoadState\"\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - url\n      additionalProperties: false\n    PageReloadParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        waitUntil:\n          $ref: \"#/components/schemas/LoadState\"\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n        ignoreCache:\n          type: boolean\n      additionalProperties: false\n    PageGoBackParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        waitUntil:\n          $ref: \"#/components/schemas/LoadState\"\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      additionalProperties: false\n    PageGoForwardParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        waitUntil:\n          $ref: \"#/components/schemas/LoadState\"\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      additionalProperties: false\n    PageTargetIdParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageMainFrameIdParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageMainFrameParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageGetFullFrameTreeParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageAsProtocolFrameTreeParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        rootMainFrameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - rootMainFrameId\n      additionalProperties: false\n    PageListAllFrameIdsParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageGetOrdinalParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        frameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - frameId\n      additionalProperties: false\n    PageTitleParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageUrlParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageScreenshotParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        fullPage:\n          type: boolean\n        clip:\n          $ref: \"#/components/schemas/PageClip\"\n        type:\n          $ref: \"#/components/schemas/ScreenshotType\"\n        quality:\n          type: integer\n          minimum: 0\n          maximum: 100\n        scale:\n          $ref: \"#/components/schemas/ScreenshotScale\"\n        animations:\n          $ref: \"#/components/schemas/ScreenshotAnimations\"\n        caret:\n          $ref: \"#/components/schemas/ScreenshotCaret\"\n        style:\n          type: string\n        omitBackground:\n          type: boolean\n        timeout:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      additionalProperties: false\n    PageSnapshotParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        includeIframes:\n          type: boolean\n      additionalProperties: false\n    PageFramesParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageSetViewportSizeParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        width:\n          type: number\n          exclusiveMinimum: 0\n        height:\n          type: number\n          exclusiveMinimum: 0\n        deviceScaleFactor:\n          type: number\n          exclusiveMinimum: 0\n      required:\n        - width\n        - height\n      additionalProperties: false\n    PageSetExtraHTTPHeadersParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        headers:\n          $ref: \"#/components/schemas/PageHeaders\"\n      required:\n        - headers\n      additionalProperties: false\n    PageWaitForLoadStateParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        state:\n          $ref: \"#/components/schemas/LoadState\"\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - state\n      additionalProperties: false\n    PageWaitForMainLoadStateParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        state:\n          $ref: \"#/components/schemas/LoadState\"\n        timeoutMs:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - state\n      additionalProperties: false\n    PageWaitForSelectorParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        selector:\n          $ref: \"#/components/schemas/ElementSelector\"\n        state:\n          $ref: \"#/components/schemas/WaitForSelectorState\"\n        timeout:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n        pierceShadow:\n          type: boolean\n      required:\n        - selector\n      additionalProperties: false\n    PageWaitForTimeoutParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        ms:\n          type: integer\n          minimum: 0\n          maximum: 9007199254740991\n      required:\n        - ms\n      additionalProperties: false\n    PageEvaluateParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        expression:\n          type: string\n          minLength: 1\n        arg: {}\n      required:\n        - expression\n      additionalProperties: false\n    PageSendCDPParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        method:\n          type: string\n          minLength: 1\n        params: {}\n      required:\n        - method\n      additionalProperties: false\n    PageCloseParams:\n      type: object\n      properties:\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      additionalProperties: false\n    PageClickRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageClickParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageHoverRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageHoverParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageScrollRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageScrollParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageDragAndDropRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageDragAndDropParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageTypeRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageTypeParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageKeyPressRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageKeyPressParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageEnableCursorOverlayRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageEnableCursorOverlayParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageAddInitScriptRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageAddInitScriptParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageGotoRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageGotoParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageReloadRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageReloadParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageGoBackRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageGoBackParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageGoForwardRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageGoForwardParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageScreenshotRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageScreenshotParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageSnapshotRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageSnapshotParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageSetViewportSizeRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageSetViewportSizeParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageSetExtraHTTPHeadersRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageSetExtraHTTPHeadersParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageWaitForLoadStateRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForLoadStateParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageWaitForMainLoadStateRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForMainLoadStateParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageWaitForSelectorRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForSelectorParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageWaitForTimeoutRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForTimeoutParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageEvaluateRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageEvaluateParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageSendCDPRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageSendCDPParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageCloseRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        params:\n          $ref: \"#/components/schemas/PageCloseParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    PageClickAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: click\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageClickParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageXPathResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageHoverAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: hover\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageHoverParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageXPathResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageScrollAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: scroll\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageScrollParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageXPathResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageDragAndDropAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: dragAndDrop\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageDragAndDropParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageDragAndDropResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageTypeAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: type\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageTypeParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageTypeResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageKeyPressAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: keyPress\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageKeyPressParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageKeyPressResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageEnableCursorOverlayAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: enableCursorOverlay\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageEnableCursorOverlayParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageEnableCursorOverlayResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageAddInitScriptAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: addInitScript\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageAddInitScriptParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageAddInitScriptResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageGotoAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: goto\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageGotoParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageNavigationResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageReloadAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: reload\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageReloadParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageNavigationResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageGoBackAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: goBack\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageGoBackParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageNavigationResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageGoForwardAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: goForward\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageGoForwardParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageNavigationResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageTargetIdAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: targetId\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageTargetIdParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageTargetIdResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageMainFrameIdAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: mainFrameId\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageMainFrameIdParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageMainFrameIdResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageMainFrameAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: mainFrame\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageMainFrameParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageMainFrameResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageGetFullFrameTreeAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: getFullFrameTree\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageGetFullFrameTreeParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageFrameTreeResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageAsProtocolFrameTreeAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: asProtocolFrameTree\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageAsProtocolFrameTreeParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageFrameTreeResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageListAllFrameIdsAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: listAllFrameIds\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageListAllFrameIdsParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageListAllFrameIdsResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageGetOrdinalAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: getOrdinal\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageGetOrdinalParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageGetOrdinalResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageTitleAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: title\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageTitleParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageTitleResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageUrlAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: url\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageUrlParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageUrlResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageScreenshotAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: screenshot\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageScreenshotParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageScreenshotResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageSnapshotAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: snapshot\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageSnapshotParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageSnapshotResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageFramesAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: frames\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageFramesParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageFramesResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageSetViewportSizeAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: setViewportSize\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageSetViewportSizeParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageSetViewportSizeResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageSetExtraHTTPHeadersAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: setExtraHTTPHeaders\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageSetExtraHTTPHeadersParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageSetExtraHTTPHeadersResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageWaitForLoadStateAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: waitForLoadState\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForLoadStateParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageWaitForLoadStateResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageWaitForMainLoadStateAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: waitForMainLoadState\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForMainLoadStateParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageWaitForLoadStateResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageWaitForSelectorAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: waitForSelector\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForSelectorParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageWaitForSelectorResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageWaitForTimeoutAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: waitForTimeout\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageWaitForTimeoutParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageWaitForTimeoutResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageEvaluateAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: evaluate\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageEvaluateParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageEvaluateResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageSendCDPAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: sendCDP\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageSendCDPParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageSendCDPResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageCloseAction:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          type: string\n          const: close\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n        params:\n          $ref: \"#/components/schemas/PageCloseParams\"\n        result:\n          anyOf:\n            - $ref: \"#/components/schemas/PageCloseResult\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n        - params\n        - result\n      additionalProperties: false\n    PageAction:\n      anyOf:\n        - $ref: \"#/components/schemas/PageClickAction\"\n        - $ref: \"#/components/schemas/PageHoverAction\"\n        - $ref: \"#/components/schemas/PageScrollAction\"\n        - $ref: \"#/components/schemas/PageDragAndDropAction\"\n        - $ref: \"#/components/schemas/PageTypeAction\"\n        - $ref: \"#/components/schemas/PageKeyPressAction\"\n        - $ref: \"#/components/schemas/PageEnableCursorOverlayAction\"\n        - $ref: \"#/components/schemas/PageAddInitScriptAction\"\n        - $ref: \"#/components/schemas/PageGotoAction\"\n        - $ref: \"#/components/schemas/PageReloadAction\"\n        - $ref: \"#/components/schemas/PageGoBackAction\"\n        - $ref: \"#/components/schemas/PageGoForwardAction\"\n        - $ref: \"#/components/schemas/PageTargetIdAction\"\n        - $ref: \"#/components/schemas/PageMainFrameIdAction\"\n        - $ref: \"#/components/schemas/PageMainFrameAction\"\n        - $ref: \"#/components/schemas/PageGetFullFrameTreeAction\"\n        - $ref: \"#/components/schemas/PageAsProtocolFrameTreeAction\"\n        - $ref: \"#/components/schemas/PageListAllFrameIdsAction\"\n        - $ref: \"#/components/schemas/PageGetOrdinalAction\"\n        - $ref: \"#/components/schemas/PageTitleAction\"\n        - $ref: \"#/components/schemas/PageUrlAction\"\n        - $ref: \"#/components/schemas/PageScreenshotAction\"\n        - $ref: \"#/components/schemas/PageSnapshotAction\"\n        - $ref: \"#/components/schemas/PageFramesAction\"\n        - $ref: \"#/components/schemas/PageSetViewportSizeAction\"\n        - $ref: \"#/components/schemas/PageSetExtraHTTPHeadersAction\"\n        - $ref: \"#/components/schemas/PageWaitForLoadStateAction\"\n        - $ref: \"#/components/schemas/PageWaitForMainLoadStateAction\"\n        - $ref: \"#/components/schemas/PageWaitForSelectorAction\"\n        - $ref: \"#/components/schemas/PageWaitForTimeoutAction\"\n        - $ref: \"#/components/schemas/PageEvaluateAction\"\n        - $ref: \"#/components/schemas/PageSendCDPAction\"\n        - $ref: \"#/components/schemas/PageCloseAction\"\n    BrowserSessionResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        data:\n          $ref: \"#/components/schemas/BrowserSessionResultOutput\"\n      required:\n        - success\n        - data\n      additionalProperties: false\n    BrowserSessionErrorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: false\n        message:\n          type: string\n      required:\n        - success\n        - message\n      additionalProperties: false\n    BrowserSessionV4ErrorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: false\n        error:\n          type: string\n        statusCode:\n          type: integer\n          minimum: -9007199254740991\n          maximum: 9007199254740991\n        stack:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionAction\"\n      required:\n        - success\n        - error\n        - statusCode\n        - stack\n      additionalProperties: false\n    BrowserSessionAddInitScriptResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionAddInitScriptAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionSetExtraHTTPHeadersResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionPagesResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionPagesAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionActivePageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionActivePageAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionAwaitActivePageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionAwaitActivePageAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionResolvePageByMainFrameIdResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionResolvePageByMainFrameIdAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionGetFullFrameTreeByMainFrameIdResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionGetFullFrameTreeByMainFrameIdAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionNewPageResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionNewPageAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionCookiesResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionCookiesAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionAddCookiesResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionAddCookiesAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionClearCookiesResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionClearCookiesAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionConnectURLResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionConnectURLAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionConfiguredViewportResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionConfiguredViewportAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionIDResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionBrowserbaseSessionURLResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionBrowserbaseDebugURLResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionActionDetailsResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionActionListResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        actions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/BrowserSessionAction\"\n      required:\n        - success\n        - error\n        - actions\n      additionalProperties: false\n    V4ErrorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: false\n        error:\n          $ref: \"#/components/schemas/PageError\"\n        statusCode:\n          type: integer\n          minimum: -9007199254740991\n          maximum: 9007199254740991\n        stack:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageAction\"\n      required:\n        - success\n        - error\n        - statusCode\n        - stack\n      additionalProperties: false\n    PageClickResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageClickAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageHoverResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageHoverAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageScrollResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageScrollAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageDragAndDropResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageDragAndDropAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageTypeResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageTypeAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageKeyPressResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageKeyPressAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageEnableCursorOverlayResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageEnableCursorOverlayAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageAddInitScriptResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageAddInitScriptAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageGotoResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageGotoAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageReloadResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageReloadAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageGoBackResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageGoBackAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageGoForwardResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageGoForwardAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageTargetIdResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageTargetIdAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageMainFrameIdResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageMainFrameIdAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageMainFrameResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageMainFrameAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageGetFullFrameTreeResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageGetFullFrameTreeAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageAsProtocolFrameTreeResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageAsProtocolFrameTreeAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageListAllFrameIdsResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageListAllFrameIdsAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageGetOrdinalResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageGetOrdinalAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageTitleResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageTitleAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageUrlResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageUrlAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageScreenshotResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageScreenshotAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageSnapshotResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageSnapshotAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageFramesResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageFramesAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageSetViewportSizeResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageSetViewportSizeAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageSetExtraHTTPHeadersResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageSetExtraHTTPHeadersAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageWaitForLoadStateResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageWaitForLoadStateAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageWaitForMainLoadStateResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageWaitForMainLoadStateAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageWaitForSelectorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageWaitForSelectorAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageWaitForTimeoutResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageWaitForTimeoutAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageEvaluateResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageEvaluateAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageSendCDPResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageSendCDPAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageCloseResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageCloseAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageActionDetailsResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    PageActionListResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        actions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/PageAction\"\n      required:\n        - success\n        - error\n        - actions\n      additionalProperties: false\n    BrowserbaseBrowserSettingsOutput:\n      type: object\n      properties:\n        advancedStealth:\n          type: boolean\n        blockAds:\n          type: boolean\n        context:\n          $ref: \"#/components/schemas/BrowserbaseContextOutput\"\n        extensionId:\n          type: string\n        fingerprint:\n          $ref: \"#/components/schemas/BrowserbaseFingerprintOutput\"\n        logSession:\n          type: boolean\n        recordSession:\n          type: boolean\n        solveCaptchas:\n          type: boolean\n        viewport:\n          $ref: \"#/components/schemas/BrowserbaseViewportOutput\"\n      additionalProperties: false\n    BrowserbaseContextOutput:\n      type: object\n      properties:\n        id:\n          type: string\n        persist:\n          type: boolean\n      required:\n        - id\n      additionalProperties: false\n    BrowserbaseFingerprintOutput:\n      type: object\n      properties:\n        browsers:\n          type: array\n          items:\n            type: string\n            enum:\n              - chrome\n              - edge\n              - firefox\n              - safari\n        devices:\n          type: array\n          items:\n            type: string\n            enum:\n              - desktop\n              - mobile\n        httpVersion:\n          type: string\n          enum:\n            - \"1\"\n            - \"2\"\n        locales:\n          type: array\n          items:\n            type: string\n        operatingSystems:\n          type: array\n          items:\n            type: string\n            enum:\n              - android\n              - ios\n              - linux\n              - macos\n              - windows\n        screen:\n          $ref: \"#/components/schemas/BrowserbaseFingerprintScreenOutput\"\n      additionalProperties: false\n    BrowserbaseFingerprintScreenOutput:\n      type: object\n      properties:\n        maxHeight:\n          type: number\n        maxWidth:\n          type: number\n        minHeight:\n          type: number\n        minWidth:\n          type: number\n      additionalProperties: false\n    BrowserbaseViewportOutput:\n      type: object\n      properties:\n        width:\n          type: number\n        height:\n          type: number\n      additionalProperties: false\n    ProxyConfigOutput:\n      oneOf:\n        - $ref: \"#/components/schemas/BrowserbaseProxyConfigOutput\"\n        - $ref: \"#/components/schemas/ExternalProxyConfigOutput\"\n      type: object\n      discriminator:\n        propertyName: type\n        mapping:\n          browserbase: \"#/components/schemas/BrowserbaseProxyConfigOutput\"\n          external: \"#/components/schemas/ExternalProxyConfigOutput\"\n    BrowserbaseProxyConfigOutput:\n      type: object\n      properties:\n        type:\n          type: string\n          const: browserbase\n        domainPattern:\n          type: string\n        geolocation:\n          $ref: \"#/components/schemas/BrowserbaseProxyGeolocationOutput\"\n      required:\n        - type\n      additionalProperties: false\n    BrowserbaseProxyGeolocationOutput:\n      type: object\n      properties:\n        country:\n          type: string\n        city:\n          type: string\n        state:\n          type: string\n      required:\n        - country\n      additionalProperties: false\n    ExternalProxyConfigOutput:\n      type: object\n      properties:\n        type:\n          type: string\n          const: external\n        server:\n          type: string\n        domainPattern:\n          type: string\n        username:\n          type: string\n        password:\n          type: string\n      required:\n        - type\n        - server\n      additionalProperties: false\n    SessionHeadersOutput:\n      type: object\n      properties:\n        x-stream-response:\n          description: Whether to stream the response via SSE\n          example: \"true\"\n          type: string\n          enum:\n            - \"true\"\n            - \"false\"\n      additionalProperties: false\n    BrowserSessionLocalCreateRequestOutput:\n      type: object\n      properties:\n        modelName:\n          description: Model name to use for AI operations\n          example: openai/gpt-4.1-nano\n          type: string\n        domSettleTimeoutMs:\n          type: number\n        verbose:\n          anyOf:\n            - type: number\n              const: 0\n            - type: number\n              const: 1\n            - type: number\n              const: 2\n        systemPrompt:\n          type: string\n        selfHeal:\n          type: boolean\n        waitForCaptchaSolves:\n          type: boolean\n        experimental:\n          type: boolean\n        actTimeoutMs:\n          type: number\n        env:\n          type: string\n          const: LOCAL\n        cdpUrl:\n          type: string\n        localBrowserLaunchOptions:\n          $ref: \"#/components/schemas/LocalBrowserLaunchOptionsOutput\"\n      required:\n        - modelName\n        - env\n      additionalProperties: false\n    BrowserSessionBrowserbaseCreateRequestOutput:\n      type: object\n      properties:\n        modelName:\n          description: Model name to use for AI operations\n          example: openai/gpt-4.1-nano\n          type: string\n        domSettleTimeoutMs:\n          type: number\n        verbose:\n          anyOf:\n            - type: number\n              const: 0\n            - type: number\n              const: 1\n            - type: number\n              const: 2\n        systemPrompt:\n          type: string\n        selfHeal:\n          type: boolean\n        waitForCaptchaSolves:\n          type: boolean\n        experimental:\n          type: boolean\n        actTimeoutMs:\n          type: number\n        env:\n          type: string\n          const: BROWSERBASE\n        browserbaseSessionId:\n          type: string\n        browserbaseSessionCreateParams:\n          $ref: \"#/components/schemas/BrowserbaseSessionCreateParamsOutput\"\n      required:\n        - modelName\n        - env\n      additionalProperties: false\n    LocalBrowserLaunchOptionsOutput:\n      type: object\n      properties:\n        args:\n          type: array\n          items:\n            type: string\n        executablePath:\n          type: string\n        port:\n          type: number\n        userDataDir:\n          type: string\n        preserveUserDataDir:\n          type: boolean\n        headless:\n          type: boolean\n        devtools:\n          type: boolean\n        chromiumSandbox:\n          type: boolean\n        ignoreDefaultArgs:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                type: string\n        proxy:\n          type: object\n          properties:\n            server:\n              type: string\n            bypass:\n              type: string\n            username:\n              type: string\n            password:\n              type: string\n          required:\n            - server\n          additionalProperties: false\n        locale:\n          type: string\n        viewport:\n          type: object\n          properties:\n            width:\n              type: number\n            height:\n              type: number\n          required:\n            - width\n            - height\n          additionalProperties: false\n        deviceScaleFactor:\n          type: number\n        hasTouch:\n          type: boolean\n        ignoreHTTPSErrors:\n          type: boolean\n        cdpUrl:\n          type: string\n        cdpHeaders:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties:\n            type: string\n        connectTimeoutMs:\n          type: number\n        downloadsPath:\n          type: string\n        acceptDownloads:\n          type: boolean\n      additionalProperties: false\n    BrowserbaseSessionCreateParamsOutput:\n      type: object\n      properties:\n        projectId:\n          type: string\n        browserSettings:\n          $ref: \"#/components/schemas/BrowserbaseBrowserSettingsOutput\"\n        extensionId:\n          type: string\n        keepAlive:\n          type: boolean\n        proxies:\n          anyOf:\n            - type: boolean\n            - type: array\n              items:\n                $ref: \"#/components/schemas/ProxyConfigOutput\"\n        region:\n          $ref: \"#/components/schemas/BrowserbaseRegion\"\n        timeout:\n          type: number\n        userMetadata:\n          type: object\n          propertyNames:\n            type: string\n          additionalProperties: {}\n      additionalProperties: false\n    BrowserSessionOutput:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        env:\n          $ref: \"#/components/schemas/BrowserSessionEnv\"\n        status:\n          $ref: \"#/components/schemas/BrowserSessionStatus\"\n        modelName:\n          type: string\n        cdpUrl:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        available:\n          type: boolean\n        browserbaseSessionId:\n          type: string\n        browserbaseSessionCreateParams:\n          $ref: \"#/components/schemas/BrowserbaseSessionCreateParamsOutput\"\n        localBrowserLaunchOptions:\n          $ref: \"#/components/schemas/LocalBrowserLaunchOptionsOutput\"\n        domSettleTimeoutMs:\n          type: number\n        verbose:\n          anyOf:\n            - type: number\n              const: 0\n            - type: number\n              const: 1\n            - type: number\n              const: 2\n        systemPrompt:\n          type: string\n        selfHeal:\n          type: boolean\n        waitForCaptchaSolves:\n          type: boolean\n        experimental:\n          type: boolean\n        actTimeoutMs:\n          type: number\n      required:\n        - id\n        - env\n        - status\n        - modelName\n        - available\n      additionalProperties: false\n    BrowserSessionResultOutput:\n      type: object\n      properties:\n        browserSession:\n          $ref: \"#/components/schemas/BrowserSessionOutput\"\n      required:\n        - browserSession\n      additionalProperties: false\n    BrowserSessionHeaders:\n      $ref: \"#/components/schemas/SessionHeadersOutput\"\n    BrowserSessionIdParams:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n      required:\n        - id\n      additionalProperties: false\n    BrowserSessionActionBase:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          $ref: \"#/components/schemas/BrowserSessionActionMethod\"\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - type: string\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n      additionalProperties: false\n    BrowserSessionIsBrowserbaseRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionIsBrowserbaseParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionIsAdvancedStealthRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionIsAdvancedStealthParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionSetViewportSizeRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionSetViewportSizeParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionCloseRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        params:\n          $ref: \"#/components/schemas/BrowserSessionCloseParams\"\n      required:\n        - sessionId\n        - params\n      additionalProperties: false\n    BrowserSessionIsBrowserbaseResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionIsBrowserbaseAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionIsAdvancedStealthResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionIsAdvancedStealthAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionSetViewportSizeResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionSetViewportSizeAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionCloseResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: true\n        error:\n          type: \"null\"\n        action:\n          $ref: \"#/components/schemas/BrowserSessionCloseAction\"\n      required:\n        - success\n        - error\n        - action\n      additionalProperties: false\n    BrowserSessionActionIdParams:\n      type: object\n      properties:\n        actionId:\n          $ref: \"#/components/schemas/ActionId\"\n      required:\n        - actionId\n      additionalProperties: false\n    BrowserSessionActionDetailsQuery:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    BrowserSessionActionListQuery:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/BrowserSessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        method:\n          $ref: \"#/components/schemas/BrowserSessionActionMethod\"\n        status:\n          $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n        limit:\n          type: integer\n          exclusiveMinimum: 0\n          maximum: 500\n      required:\n        - sessionId\n      additionalProperties: false\n    ValidationErrorResponse:\n      type: object\n      properties:\n        success:\n          type: boolean\n          const: false\n        error:\n          $ref: \"#/components/schemas/PageError\"\n        statusCode:\n          type: integer\n          minimum: -9007199254740991\n          maximum: 9007199254740991\n        stack:\n          anyOf:\n            - type: string\n            - type: \"null\"\n        action:\n          $ref: \"#/components/schemas/PageAction\"\n      required:\n        - success\n        - error\n        - statusCode\n        - stack\n      additionalProperties: false\n    PageActionBase:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/ActionId\"\n        method:\n          $ref: \"#/components/schemas/PageActionMethod\"\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        createdAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        updatedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        completedAt:\n          $ref: \"#/components/schemas/Timestamp\"\n        error:\n          anyOf:\n            - $ref: \"#/components/schemas/PageError\"\n            - type: \"null\"\n      required:\n        - id\n        - method\n        - status\n        - sessionId\n        - createdAt\n        - updatedAt\n        - error\n      additionalProperties: false\n    PageTargetIdRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageMainFrameIdRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageMainFrameRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageGetFullFrameTreeRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageAsProtocolFrameTreeRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        rootMainFrameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - sessionId\n        - rootMainFrameId\n      additionalProperties: false\n    PageListAllFrameIdsRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageGetOrdinalRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        frameId:\n          $ref: \"#/components/schemas/FrameId\"\n      required:\n        - sessionId\n        - frameId\n      additionalProperties: false\n    PageTitleRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageUrlRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageFramesRequest:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageActionIdParams:\n      type: object\n      properties:\n        actionId:\n          $ref: \"#/components/schemas/ActionId\"\n      required:\n        - actionId\n      additionalProperties: false\n    PageActionDetailsQuery:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n      required:\n        - sessionId\n      additionalProperties: false\n    PageActionListQuery:\n      type: object\n      properties:\n        id:\n          $ref: \"#/components/schemas/RequestId\"\n        sessionId:\n          $ref: \"#/components/schemas/SessionId\"\n        pageId:\n          $ref: \"#/components/schemas/PageId\"\n        method:\n          $ref: \"#/components/schemas/PageActionMethod\"\n        status:\n          $ref: \"#/components/schemas/PageActionStatus\"\n        limit:\n          type: integer\n          exclusiveMinimum: 0\n          maximum: 500\n      required:\n        - sessionId\n      additionalProperties: false\npaths:\n  /v4/browsersession:\n    post:\n      operationId: BrowserSessionCreate\n      summary: Create a browser session\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionCreateRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionErrorResponse\"\n  /v4/browsersession/{id}:\n    get:\n      operationId: BrowserSessionStatus\n      summary: Get browser session status\n      tags:\n        - browserSession\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/BrowserSessionId\"\n          in: path\n          name: id\n          required: true\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionResponse\"\n  /v4/browsersession/{id}/end:\n    post:\n      operationId: BrowserSessionEnd\n      summary: End a browser session\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              x-fastify-zod-openapi-optional: true\n              $ref: \"#/components/schemas/BrowserSessionEndRequest\"\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/BrowserSessionId\"\n          in: path\n          name: id\n          required: true\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionResponse\"\n  /v4/browsersession/addInitScript:\n    post:\n      operationId: BrowserSessionAddInitScript\n      summary: browserSession.addInitScript\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionAddInitScriptRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionAddInitScriptResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/setExtraHTTPHeaders:\n    post:\n      operationId: BrowserSessionSetExtraHTTPHeaders\n      summary: browserSession.setExtraHTTPHeaders\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionSetExtraHTTPHeadersResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/pages:\n    post:\n      operationId: BrowserSessionPages\n      summary: browserSession.pages\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionPagesRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionPagesResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/activePage:\n    post:\n      operationId: BrowserSessionActivePage\n      summary: browserSession.activePage\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionActivePageRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionActivePageResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/awaitActivePage:\n    post:\n      operationId: BrowserSessionAwaitActivePage\n      summary: browserSession.awaitActivePage\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionAwaitActivePageRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionAwaitActivePageResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/resolvePageByMainFrameId:\n    post:\n      operationId: BrowserSessionResolvePageByMainFrameId\n      summary: browserSession.resolvePageByMainFrameId\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionResolvePageByMainFrameIdRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionResolvePageByMainFrameIdResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/getFullFrameTreeByMainFrameId:\n    post:\n      operationId: BrowserSessionGetFullFrameTreeByMainFrameId\n      summary: browserSession.getFullFrameTreeByMainFrameId\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionGetFullFrameTreeByMainFrameIdRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionGetFullFrameTreeByMainFrameIdResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/newPage:\n    post:\n      operationId: BrowserSessionNewPage\n      summary: browserSession.newPage\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionNewPageRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionNewPageResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/cookies:\n    post:\n      operationId: BrowserSessionCookies\n      summary: browserSession.cookies\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionCookiesRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionCookiesResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/addCookies:\n    post:\n      operationId: BrowserSessionAddCookies\n      summary: browserSession.addCookies\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionAddCookiesRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionAddCookiesResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/clearCookies:\n    post:\n      operationId: BrowserSessionClearCookies\n      summary: browserSession.clearCookies\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionClearCookiesRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionClearCookiesResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/connectURL:\n    post:\n      operationId: BrowserSessionConnectURL\n      summary: browserSession.connectURL\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionConnectURLRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionConnectURLResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/configuredViewport:\n    post:\n      operationId: BrowserSessionConfiguredViewport\n      summary: browserSession.configuredViewport\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionConfiguredViewportRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionConfiguredViewportResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/browserbaseSessionID:\n    post:\n      operationId: BrowserSessionBrowserbaseSessionID\n      summary: browserSession.browserbaseSessionID\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionIDResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/browserbaseSessionURL:\n    post:\n      operationId: BrowserSessionBrowserbaseSessionURL\n      summary: browserSession.browserbaseSessionURL\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionBrowserbaseSessionURLResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/browserbaseDebugURL:\n    post:\n      operationId: BrowserSessionBrowserbaseDebugURL\n      summary: browserSession.browserbaseDebugURL\n      tags:\n        - browserSession\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionBrowserbaseDebugURLResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/action:\n    get:\n      operationId: BrowserSessionActionList\n      summary: browserSession.actions\n      tags:\n        - browserSession\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/BrowserSessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            $ref: \"#/components/schemas/BrowserSessionActionMethod\"\n          in: query\n          name: method\n        - schema:\n            $ref: \"#/components/schemas/BrowserSessionActionStatus\"\n          in: query\n          name: status\n        - schema:\n            type: integer\n            exclusiveMinimum: 0\n            maximum: 500\n          in: query\n          name: limit\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionActionListResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/browsersession/action/{actionId}:\n    get:\n      operationId: BrowserSessionActionDetails\n      summary: browserSession.action\n      tags:\n        - browserSession\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/BrowserSessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/ActionId\"\n          in: path\n          name: actionId\n          required: true\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionActionDetailsResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/BrowserSessionV4ErrorResponse\"\n  /v4/page/click:\n    post:\n      operationId: PageClick\n      summary: page.click\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageClickRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageClickResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/hover:\n    post:\n      operationId: PageHover\n      summary: page.hover\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageHoverRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageHoverResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/scroll:\n    post:\n      operationId: PageScroll\n      summary: page.scroll\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageScrollRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageScrollResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/dragAndDrop:\n    post:\n      operationId: PageDragAndDrop\n      summary: page.dragAndDrop\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageDragAndDropRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageDragAndDropResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/type:\n    post:\n      operationId: PageType\n      summary: page.type\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageTypeRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageTypeResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/keyPress:\n    post:\n      operationId: PageKeyPress\n      summary: page.keyPress\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageKeyPressRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageKeyPressResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/goto:\n    post:\n      operationId: PageGoto\n      summary: page.goto\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageGotoRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageGotoResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/reload:\n    post:\n      operationId: PageReload\n      summary: page.reload\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageReloadRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageReloadResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/goBack:\n    post:\n      operationId: PageGoBack\n      summary: page.goBack\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageGoBackRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageGoBackResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/goForward:\n    post:\n      operationId: PageGoForward\n      summary: page.goForward\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageGoForwardRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageGoForwardResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/close:\n    post:\n      operationId: PageClose\n      summary: page.close\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageCloseRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageCloseResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/enableCursorOverlay:\n    post:\n      operationId: PageEnableCursorOverlay\n      summary: page.enableCursorOverlay\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageEnableCursorOverlayRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageEnableCursorOverlayResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/addInitScript:\n    post:\n      operationId: PageAddInitScript\n      summary: page.addInitScript\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageAddInitScriptRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageAddInitScriptResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/targetId:\n    get:\n      operationId: PageTargetId\n      summary: page.targetId\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageTargetIdResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/mainFrameId:\n    get:\n      operationId: PageMainFrameId\n      summary: page.mainFrameId\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageMainFrameIdResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/mainFrame:\n    get:\n      operationId: PageMainFrame\n      summary: page.mainFrame\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageMainFrameResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/getFullFrameTree:\n    get:\n      operationId: PageGetFullFrameTree\n      summary: page.getFullFrameTree\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageGetFullFrameTreeResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/asProtocolFrameTree:\n    get:\n      operationId: PageAsProtocolFrameTree\n      summary: page.asProtocolFrameTree\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            $ref: \"#/components/schemas/FrameId\"\n          in: query\n          name: rootMainFrameId\n          required: true\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageAsProtocolFrameTreeResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/listAllFrameIds:\n    get:\n      operationId: PageListAllFrameIds\n      summary: page.listAllFrameIds\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageListAllFrameIdsResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/getOrdinal:\n    get:\n      operationId: PageGetOrdinal\n      summary: page.getOrdinal\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            $ref: \"#/components/schemas/FrameId\"\n          in: query\n          name: frameId\n          required: true\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageGetOrdinalResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/title:\n    get:\n      operationId: PageTitle\n      summary: page.title\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageTitleResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/url:\n    get:\n      operationId: PageUrl\n      summary: page.url\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageUrlResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/frames:\n    get:\n      operationId: PageFrames\n      summary: page.frames\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageFramesResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/setExtraHTTPHeaders:\n    post:\n      operationId: PageSetExtraHTTPHeaders\n      summary: page.setExtraHTTPHeaders\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageSetExtraHTTPHeadersRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageSetExtraHTTPHeadersResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/waitForMainLoadState:\n    post:\n      operationId: PageWaitForMainLoadState\n      summary: page.waitForMainLoadState\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageWaitForMainLoadStateRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageWaitForMainLoadStateResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/screenshot:\n    post:\n      operationId: PageScreenshot\n      summary: page.screenshot\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageScreenshotRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageScreenshotResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/snapshot:\n    post:\n      operationId: PageSnapshot\n      summary: page.snapshot\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageSnapshotRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageSnapshotResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/setViewportSize:\n    post:\n      operationId: PageSetViewportSize\n      summary: page.setViewportSize\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageSetViewportSizeRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageSetViewportSizeResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/waitForLoadState:\n    post:\n      operationId: PageWaitForLoadState\n      summary: page.waitForLoadState\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageWaitForLoadStateRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageWaitForLoadStateResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/waitForSelector:\n    post:\n      operationId: PageWaitForSelector\n      summary: page.waitForSelector\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageWaitForSelectorRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageWaitForSelectorResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/waitForTimeout:\n    post:\n      operationId: PageWaitForTimeout\n      summary: page.waitForTimeout\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageWaitForTimeoutRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageWaitForTimeoutResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/evaluate:\n    post:\n      operationId: PageEvaluate\n      summary: page.evaluate\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageEvaluateRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageEvaluateResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/sendCDP:\n    post:\n      operationId: PageSendCDP\n      summary: page.sendCDP\n      tags:\n        - page\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/PageSendCDPRequest\"\n        required: true\n      parameters:\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageSendCDPResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/action:\n    get:\n      operationId: PageActionList\n      summary: page.action\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/PageId\"\n          in: query\n          name: pageId\n        - schema:\n            $ref: \"#/components/schemas/PageActionMethod\"\n          in: query\n          name: method\n        - schema:\n            $ref: \"#/components/schemas/PageActionStatus\"\n          in: query\n          name: status\n        - schema:\n            type: integer\n            exclusiveMinimum: 0\n            maximum: 500\n          in: query\n          name: limit\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageActionListResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n  /v4/page/action/{actionId}:\n    get:\n      operationId: PageActionDetails\n      summary: page.actionById\n      tags:\n        - page\n      parameters:\n        - schema:\n            $ref: \"#/components/schemas/RequestId\"\n          in: query\n          name: id\n        - schema:\n            $ref: \"#/components/schemas/SessionId\"\n          in: query\n          name: sessionId\n          required: true\n        - schema:\n            $ref: \"#/components/schemas/ActionId\"\n          in: path\n          name: actionId\n          required: true\n        - schema:\n            description: Whether to stream the response via SSE\n            example: \"true\"\n            type: string\n            enum:\n              - \"true\"\n              - \"false\"\n          in: header\n          name: x-stream-response\n          description: Whether to stream the response via SSE\n      responses:\n        \"200\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/PageActionDetailsResponse\"\n        \"400\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"401\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"404\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"408\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"422\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\n        \"500\":\n          description: Default Response\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/V4ErrorResponse\"\nservers:\n  - url: https://api.stagehand.browserbase.com\nsecurity:\n  - BrowserbaseApiKey: []\n    BrowserbaseProjectId: []\n    ModelApiKey: []\n"
  },
  {
    "path": "packages/server-v4/package.json",
    "content": "{\n  \"name\": \"@browserbasehq/stagehand-server-v4\",\n  \"version\": \"3.6.1\",\n  \"description\": \"Stagehand API server v4\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"pnpm --filter @browserbasehq/stagehand-server-v4 run --parallel \\\"/^(build:esm-tests|build:server:dist|gen:openapi|build:sea:esm)$/\\\"\",\n    \"dev\": \"tsx watch src/server.ts\",\n    \"build:esm-tests\": \"pnpm -w --dir ../.. exec tsc -p packages/server-v4/tsconfig.tests.json\",\n    \"build:server:dist\": \"pnpm -w --dir ../.. exec tsc -p packages/server-v4/tsconfig.json && pnpm -w --dir ../.. exec tsc-alias -p packages/server-v4/tsconfig.json\",\n    \"build:sea:esm\": \"tsx scripts/build-sea.ts --mode=esm\",\n    \"build:sea:cjs\": \"tsx scripts/build-sea.ts --mode=cjs\",\n    \"lint\": \"cd ../.. && prettier --check packages/server-v4 && cd packages/server-v4 && eslint . && pnpm run typecheck\",\n    \"typecheck\": \"pnpm -w --dir ../.. exec tsc -p packages/server-v4/tsconfig.json --noEmit\",\n    \"test\": \"pnpm -w --dir ../.. exec turbo run test:server --filter=@browserbasehq/stagehand-server-v4 --\",\n    \"test:server\": \"tsx scripts/test-server.ts\",\n    \"test:integration\": \"pnpm run test:server -- packages/server-v4/dist/tests/integration\",\n    \"test:integration:local\": \"STAGEHAND_SERVER_TARGET=local pnpm run test:server -- packages/server-v4/dist/tests/integration\",\n    \"test:integration:sea\": \"STAGEHAND_SERVER_TARGET=sea pnpm run test:server -- packages/server-v4/dist/tests/integration\",\n    \"gen:openapi\": \"tsx scripts/gen-openapi.ts\"\n  },\n  \"dependencies\": {\n    \"@browserbasehq/sdk\": \"^2.7.0\",\n    \"@browserbasehq/stagehand\": \"workspace:*\",\n    \"@fastify/cors\": \"^11.0.1\",\n    \"@fastify/swagger\": \"^9.6.1\",\n    \"@fastify/swagger-ui\": \"^5.2.3\",\n    \"@t3-oss/env-core\": \"^0.13.8\",\n    \"fastify\": \"^5.3.2\",\n    \"fastify-metrics\": \"^12.1.0\",\n    \"fastify-plugin\": \"^4.5.1\",\n    \"fastify-zod-openapi\": \"^5.5.0\",\n    \"http-status-codes\": \"^2.3.0\",\n    \"pino\": \"^9.7.0\",\n    \"pino-pretty\": \"^11.3.0\",\n    \"playwright\": \"1.52.0\",\n    \"uuid\": \"^11.0.5\",\n    \"zod\": \"^4.2.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"22.13.1\",\n    \"eslint\": \"10.0.2\",\n    \"eslint-plugin-security\": \"^3.0.1\",\n    \"openai\": \"4.87.1\",\n    \"postject\": \"1.0.0-alpha.6\",\n    \"prettier\": \"^3.2.5\",\n    \"source-map\": \"^0.7.4\",\n    \"tsc-alias\": \"^1.8.10\",\n    \"tsx\": \"*\",\n    \"vitest\": \"^4.0.8\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/browserbase/stagehand.git\",\n    \"directory\": \"packages/server-v4\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/browserbase/stagehand/issues\"\n  },\n  \"homepage\": \"https://stagehand.dev\"\n}\n"
  },
  {
    "path": "packages/server-v4/scripts/build-sea.ts",
    "content": "#!/usr/bin/env node\n/**\n * Build SEA binary from ESM (test) or CJS (release) bundles.\n *\n * Prereqs:\n * - CJS mode: runs core CJS build via Turbo if dist is missing.\n * - ESM mode: core dist/esm available (pnpm run build:esm).\n * - postject installed; tar available for non-Windows downloads.\n *\n * Args: --mode=esm|cjs --target-platform=<platform> --target-arch=<arch> --binary-name=<name>\n * Env: SEA_BUILD_MODE, SEA_TARGET_PLATFORM, SEA_TARGET_ARCH, SEA_BINARY_NAME,\n *      SEA_INCLUDE_SOURCEMAPS.\n * Example: pnpm run build:sea:cjs -- --target-platform=linux --target-arch=arm64\n */\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport https from \"node:https\";\nimport { pathToFileURL } from \"node:url\";\nimport esbuild from \"esbuild\";\nimport { getRepoRootDir } from \"./runtimePaths.js\";\n\nconst repoDir = getRepoRootDir();\n\nconst argValue = (name: string) => {\n  const prefix = `--${name}=`;\n  for (let i = 0; i < process.argv.length; i++) {\n    const arg = process.argv[i];\n    if (arg === `--${name}` && process.argv[i + 1]) return process.argv[i + 1];\n    if (arg.startsWith(prefix)) return arg.slice(prefix.length);\n  }\n  return undefined;\n};\n\nconst mode = (\n  argValue(\"mode\") ??\n  process.env.SEA_BUILD_MODE ??\n  \"esm\"\n).toLowerCase();\nconst parseBoolean = (\n  value: string | undefined,\n  fallback: boolean,\n): boolean => {\n  if (value === undefined) return fallback;\n\n  const normalized = value.toLowerCase();\n  if (\n    normalized === \"1\" ||\n    normalized === \"true\" ||\n    normalized === \"yes\" ||\n    normalized === \"on\"\n  ) {\n    return true;\n  }\n  if (\n    normalized === \"0\" ||\n    normalized === \"false\" ||\n    normalized === \"no\" ||\n    normalized === \"off\"\n  ) {\n    return false;\n  }\n\n  throw new Error(\n    `Invalid boolean value \"${value}\" for --include-sourcemaps / SEA_INCLUDE_SOURCEMAPS`,\n  );\n};\nconst targetPlatform =\n  argValue(\"target-platform\") ??\n  argValue(\"platform\") ??\n  process.env.SEA_TARGET_PLATFORM ??\n  process.platform;\nconst targetArch =\n  argValue(\"target-arch\") ??\n  argValue(\"arch\") ??\n  process.env.SEA_TARGET_ARCH ??\n  process.arch;\nconst binaryName =\n  argValue(\"binary-name\") ??\n  process.env.SEA_BINARY_NAME ??\n  `stagehand-server-v4-${targetPlatform}-${targetArch}${targetPlatform === \"win32\" ? \".exe\" : \"\"}`;\nconst includeSourcemaps = parseBoolean(\n  argValue(\"include-sourcemaps\") ?? process.env.SEA_INCLUDE_SOURCEMAPS,\n  false,\n);\n\nconst run = (cmd: string, args: string[], opts: { cwd?: string } = {}) => {\n  const result = spawnSync(cmd, args, { stdio: \"inherit\", ...opts });\n  if (result.error) {\n    throw new Error(\n      `Command failed to start: ${cmd} ${args.join(\" \")}\\n${String(result.error)}`,\n    );\n  }\n  if (result.status !== 0) {\n    throw new Error(`Command failed: ${cmd} ${args.join(\" \")}`);\n  }\n};\n\nconst runNodeScript = (\n  scriptPath: string,\n  args: string[],\n  opts: { cwd?: string } = {},\n) => run(process.execPath, [scriptPath, ...args], opts);\n\nconst resolveFirstExisting = (paths: string[]): string => {\n  for (const candidate of paths) {\n    if (fs.existsSync(candidate)) return candidate;\n  }\n  throw new Error(`Missing tool script. Tried: ${paths.join(\", \")}`);\n};\n\nconst runOptional = (\n  cmd: string,\n  args: string[],\n  opts: { cwd?: string } = {},\n) => {\n  spawnSync(cmd, args, { stdio: \"ignore\", ...opts });\n};\n\nconst download = (url: string, dest: string): Promise<void> =>\n  new Promise((resolve, reject) => {\n    https\n      .get(url, (res) => {\n        if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {\n          const location = res.headers.location;\n          if (!location) {\n            reject(new Error(`Redirect without location: ${url}`));\n            return;\n          }\n          res.resume();\n          download(location, dest).then(resolve, reject);\n          return;\n        }\n        if (res.statusCode !== 200) {\n          reject(new Error(`Download failed (${res.statusCode}) ${url}`));\n          res.resume();\n          return;\n        }\n\n        const file = fs.createWriteStream(dest);\n        const fail = (error: Error) => {\n          file.destroy();\n          reject(error);\n        };\n\n        res.on(\"error\", fail);\n        file.on(\"error\", fail);\n        file.on(\"finish\", () => {\n          file.close((closeError) => {\n            if (closeError) {\n              reject(closeError);\n              return;\n            }\n            resolve();\n          });\n        });\n        res.pipe(file);\n      })\n      .on(\"error\", reject);\n  });\n\nconst resolveNodeBinary = async (): Promise<string> => {\n  if (targetPlatform !== process.platform) {\n    throw new Error(\n      `Cross-platform builds are not supported. Host=${process.platform}, target=${targetPlatform}`,\n    );\n  }\n  if (targetArch === process.arch) {\n    return process.execPath;\n  }\n\n  const version = process.version;\n  const distPlatform = targetPlatform === \"win32\" ? \"win\" : targetPlatform;\n  const archiveBase = `node-${version}-${distPlatform}-${targetArch}`;\n  const archiveExt = distPlatform === \"win\" ? \"zip\" : \"tar.xz\";\n  const tmpRoot = `${os.tmpdir()}/stagehand-sea/${archiveBase}`;\n  const archivePath = `${tmpRoot}/${archiveBase}.${archiveExt}`;\n  const extractRoot = `${tmpRoot}/${archiveBase}`;\n  const binaryPath =\n    distPlatform === \"win\"\n      ? `${extractRoot}/node.exe`\n      : `${extractRoot}/bin/node`;\n\n  if (fs.existsSync(binaryPath)) {\n    return binaryPath;\n  }\n\n  fs.mkdirSync(tmpRoot, { recursive: true });\n  if (!fs.existsSync(archivePath)) {\n    const url = `https://nodejs.org/dist/${version}/${archiveBase}.${archiveExt}`;\n    await download(url, archivePath);\n  }\n\n  if (archiveExt === \"zip\") {\n    if (process.platform !== \"win32\") {\n      throw new Error(\"Windows binaries must be built on Windows runners.\");\n    }\n    run(\"powershell\", [\n      \"-Command\",\n      `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpRoot}' -Force`,\n    ]);\n  } else {\n    run(\"tar\", [\"-xf\", archivePath, \"-C\", tmpRoot]);\n  }\n\n  if (!fs.existsSync(binaryPath)) {\n    throw new Error(`Missing Node binary at ${binaryPath}`);\n  }\n  return binaryPath;\n};\n\nconst writeSeaConfig = (\n  mainPath: string,\n  outputPath: string,\n  execArgvExtension?: string,\n) => {\n  const configPath = `${repoDir}/packages/server-v4/dist/sea/sea-config-${mode}.json`;\n  const config = {\n    main: path\n      .relative(`${repoDir}/packages/server-v4`, mainPath)\n      .replaceAll(\"\\\\\", \"/\"),\n    output: path\n      .relative(`${repoDir}/packages/server-v4`, outputPath)\n      .replaceAll(\"\\\\\", \"/\"),\n    ...(execArgvExtension ? { execArgvExtension } : {}),\n  };\n  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));\n  return configPath;\n};\n\nconst buildCjsBundle = () => {\n  const turboBin = resolveFirstExisting([\n    `${repoDir}/node_modules/turbo/bin/turbo`,\n  ]);\n  runNodeScript(\n    turboBin,\n    [\"run\", \"build:cjs\", \"--filter\", \"@browserbasehq/stagehand\"],\n    {\n      cwd: repoDir,\n    },\n  );\n  fs.mkdirSync(`${repoDir}/packages/server-v4/dist/sea`, { recursive: true });\n  const bundlePath = `${repoDir}/packages/server-v4/dist/sea/bundle.cjs`;\n  esbuild.buildSync({\n    entryPoints: [\"packages/server-v4/src/sea-entry.ts\"],\n    bundle: true,\n    platform: \"node\",\n    format: \"cjs\",\n    outfile: bundlePath,\n    logLevel: \"warning\",\n    absWorkingDir: repoDir,\n  });\n  return bundlePath;\n};\n\nconst buildEsmBundle = () => {\n  if (!fs.existsSync(`${repoDir}/packages/core/dist/esm/index.js`)) {\n    throw new Error(\n      `Missing ${repoDir}/packages/core/dist/esm/index.js. Run pnpm run build:esm first.`,\n    );\n  }\n\n  fs.mkdirSync(`${repoDir}/packages/server-v4/dist/sea`, { recursive: true });\n  const appBundlePath = `${repoDir}/packages/server-v4/dist/app.mjs`;\n  esbuild.buildSync({\n    entryPoints: [\"packages/server-v4/src/sea-entry.ts\"],\n    bundle: true,\n    platform: \"node\",\n    format: \"esm\",\n    treeShaking: false,\n    outfile: appBundlePath,\n    alias: {\n      \"@browserbasehq/stagehand\": `${repoDir}/packages/core/dist/esm/index.js`,\n    },\n    sourcemap: includeSourcemaps ? \"inline\" : false,\n    sourcesContent: includeSourcemaps,\n    ...(includeSourcemaps ? { sourceRoot: repoDir } : {}),\n    banner: {\n      js: 'import { createRequire as __createRequire } from \"node:module\"; const require = __createRequire(import.meta.url);',\n    },\n    logLevel: \"warning\",\n    absWorkingDir: repoDir,\n  });\n\n  const appSource = fs.readFileSync(appBundlePath, \"utf8\");\n  let finalAppSource = appSource;\n\n  if (includeSourcemaps) {\n    const mapMatch = appSource.match(\n      /sourceMappingURL=data:application\\/json;base64,([A-Za-z0-9+/=]+)\\s*$/,\n    );\n    if (!mapMatch) {\n      throw new Error(\"Missing inline sourcemap in dist/app.mjs\");\n    }\n    const mapJson = Buffer.from(mapMatch[1], \"base64\").toString(\"utf8\");\n    const map = JSON.parse(mapJson) as {\n      sourceRoot?: string;\n      sources: string[];\n      sourcesContent?: string[];\n    };\n    const toPosix = (value: string) => value.replaceAll(\"\\\\\", \"/\");\n    const fileUrlToPathSafe = (value: string) => {\n      const parsed = new URL(value);\n      let pathname = decodeURIComponent(parsed.pathname);\n      if (/^\\/[A-Za-z]:/.test(pathname)) {\n        pathname = pathname.slice(1);\n      }\n      return pathname;\n    };\n    const toRepoRelative = (source: string) => {\n      let sourcePath = source;\n      if (source.startsWith(\"file://\")) {\n        sourcePath = fileUrlToPathSafe(source);\n      }\n\n      if (path.isAbsolute(sourcePath)) {\n        const normalizedSourcePath = toPosix(sourcePath);\n        if (normalizedSourcePath.startsWith(`${repoDir}/`)) {\n          return toPosix(path.relative(repoDir, normalizedSourcePath));\n        }\n        return normalizedSourcePath;\n      }\n\n      if (sourcePath.startsWith(\"../src/\")) {\n        const rel = sourcePath.slice(\"../src/\".length);\n        return `packages/server-v4/src/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../../core/\")) {\n        const rel = sourcePath.slice(\"../../core/\".length);\n        return `packages/core/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../../../node_modules/\")) {\n        const rel = sourcePath.slice(\"../../../node_modules/\".length);\n        return `node_modules/${rel}`;\n      }\n      if (sourcePath.startsWith(\"src/\")) {\n        const rel = sourcePath.slice(\"src/\".length);\n        return `packages/server-v4/src/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../node_modules/\")) {\n        const rel = sourcePath.slice(\"../node_modules/\".length);\n        return `node_modules/${rel}`;\n      }\n      if (sourcePath.startsWith(\"../core/\")) {\n        const rel = sourcePath.slice(\"../core/\".length);\n        return `packages/core/${rel}`;\n      }\n      if (sourcePath.startsWith(\"core/\")) {\n        return `packages/core/${sourcePath.slice(\"core/\".length)}`;\n      }\n      if (\n        sourcePath.startsWith(\"packages/\") ||\n        sourcePath.startsWith(\"node_modules/\")\n      ) {\n        return toPosix(sourcePath);\n      }\n\n      const resolved = toPosix(\n        path.resolve(`${repoDir}/packages/server-v4`, sourcePath),\n      );\n      if (resolved.startsWith(`${repoDir}/`)) {\n        return toPosix(path.relative(repoDir, resolved));\n      }\n\n      return toPosix(sourcePath);\n    };\n\n    map.sourceRoot = pathToFileURL(`${repoDir}/`).href;\n    map.sources = map.sources.map(toRepoRelative);\n    const updatedMap = Buffer.from(JSON.stringify(map)).toString(\"base64\");\n    finalAppSource = appSource.replace(mapMatch[1], updatedMap);\n    fs.writeFileSync(appBundlePath, finalAppSource);\n  }\n\n  const appBytes = Buffer.from(finalAppSource);\n  const bundleHash = createHash(\"sha256\")\n    .update(appBytes)\n    .digest(\"hex\")\n    .slice(0, 12);\n  const bootstrapPath = `${repoDir}/packages/server-v4/dist/sea/sea-bootstrap.cjs`;\n  const bootstrap = `/* eslint-disable */\nconst fs = require(\"node:fs\");\nconst os = require(\"node:os\");\nconst { pathToFileURL } = require(\"node:url\");\n\nconst bundleBase64 = ${JSON.stringify(appBytes.toString(\"base64\"))};\nconst bundleLength = ${appBytes.length};\nconst bundleHash = ${JSON.stringify(bundleHash)};\n\nconst cacheRoot =\n  process.env.STAGEHAND_SEA_CACHE_DIR ||\n  \\`\\${os.tmpdir()}/stagehand-server-v4-sea\\`;\nconst cacheDir = \\`\\${cacheRoot}/\\${bundleHash}\\`;\nconst appPath = \\`\\${cacheDir}/app.mjs\\`;\n\nfs.mkdirSync(cacheDir, { recursive: true });\nlet needsWrite = true;\ntry {\n  const stat = fs.statSync(appPath);\n  needsWrite = stat.size !== bundleLength;\n} catch {}\n\nif (needsWrite) {\n  const tmpPath =\n    \\`\\${cacheDir}/app.mjs.tmp-\\${process.pid}-\\${Date.now().toString(16)}\\`;\n  fs.writeFileSync(tmpPath, Buffer.from(bundleBase64, \"base64\"));\n  try {\n    fs.renameSync(tmpPath, appPath);\n  } catch (err) {\n    if (!fs.existsSync(appPath)) throw err;\n  }\n  try {\n    fs.chmodSync(appPath, 0o500);\n  } catch {}\n}\n\n(async () => {\n  await import(pathToFileURL(appPath).href);\n})().catch((err) => {\n  console.error(err);\n  process.exitCode = 1;\n});\n`;\n  fs.writeFileSync(bootstrapPath, bootstrap);\n  return bootstrapPath;\n};\n\nconst main = async () => {\n  fs.mkdirSync(`${repoDir}/packages/server-v4/dist/sea`, { recursive: true });\n\n  let mainPath: string;\n  let execArgvExtension: string | undefined;\n\n  if (mode === \"cjs\") {\n    mainPath = buildCjsBundle();\n  } else if (mode === \"esm\") {\n    mainPath = buildEsmBundle();\n    execArgvExtension = \"cli\";\n  } else {\n    throw new Error(`Unknown SEA build mode: ${mode}`);\n  }\n\n  const seaConfigPath = writeSeaConfig(\n    mainPath,\n    `${repoDir}/packages/server-v4/dist/sea/sea-prep.blob`,\n    execArgvExtension,\n  );\n\n  run(\"node\", [\"--experimental-sea-config\", seaConfigPath], {\n    cwd: `${repoDir}/packages/server-v4`,\n  });\n  if (!fs.existsSync(`${repoDir}/packages/server-v4/dist/sea/sea-prep.blob`)) {\n    throw new Error(\n      `Missing ${repoDir}/packages/server-v4/dist/sea/sea-prep.blob; SEA blob generation failed.`,\n    );\n  }\n\n  const nodeBinary = await resolveNodeBinary();\n  const outPath = `${repoDir}/packages/server-v4/dist/sea/${binaryName}`;\n  fs.copyFileSync(nodeBinary, outPath);\n  if (targetPlatform !== \"win32\") {\n    fs.chmodSync(outPath, 0o755);\n  }\n\n  if (targetPlatform === \"darwin\") {\n    runOptional(\"codesign\", [\"--remove-signature\", outPath]);\n  }\n\n  const postjectCliPath = resolveFirstExisting([\n    `${repoDir}/packages/server-v4/node_modules/postject/dist/cli.js`,\n    `${repoDir}/node_modules/postject/dist/cli.js`,\n  ]);\n  const postjectArgs = [\n    outPath,\n    \"NODE_SEA_BLOB\",\n    `${repoDir}/packages/server-v4/dist/sea/sea-prep.blob`,\n    \"--sentinel-fuse\",\n    \"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2\",\n  ];\n  if (targetPlatform === \"darwin\") {\n    postjectArgs.push(\"--macho-segment-name\", \"NODE_SEA\");\n  }\n  runNodeScript(postjectCliPath, postjectArgs, {\n    cwd: `${repoDir}/packages/server-v4`,\n  });\n\n  if (targetPlatform === \"darwin\") {\n    runOptional(\"codesign\", [\"--sign\", \"-\", outPath]);\n  }\n};\n\nmain().catch((err) => {\n  console.error(err instanceof Error ? err.message : String(err));\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/server-v4/scripts/gen-openapi.ts",
    "content": "import { writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getCurrentDirPath } from \"./runtimePaths.js\";\n\nimport fastify from \"fastify\";\nimport fastifySwagger from \"@fastify/swagger\";\nimport {\n  fastifyZodOpenApiPlugin,\n  fastifyZodOpenApiTransformers,\n  serializerCompiler,\n  validatorCompiler,\n  type FastifyZodOpenApiTypeProvider,\n} from \"fastify-zod-openapi\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport { browserSessionOpenApiComponents } from \"../src/schemas/v4/browserSession.js\";\nimport { pageOpenApiComponents } from \"../src/schemas/v4/page.js\";\nimport { browserSessionRoutes } from \"../src/routes/v4/browsersession/routes.js\";\nimport { pageRoutes } from \"../src/routes/v4/page/routes.js\";\n\n// Routes\nimport healthcheckRoute from \"../src/routes/healthcheck.js\";\nimport readinessRoute from \"../src/routes/readiness.js\";\n\nconst OUTPUT_PATH = path.resolve(getCurrentDirPath(), \"../openapi.v4.yaml\");\n\nasync function main() {\n  const app = fastify({\n    logger: false,\n  }).withTypeProvider<FastifyZodOpenApiTypeProvider>();\n\n  app.setValidatorCompiler(validatorCompiler);\n  app.setSerializerCompiler(serializerCompiler);\n\n  // Register all API schemas as components so fastify-zod-openapi can create $ref links\n  const components = {\n    schemas: {\n      ...browserSessionOpenApiComponents.schemas,\n      ...pageOpenApiComponents.schemas,\n    },\n  };\n\n  await app.register(fastifyZodOpenApiPlugin, { components });\n\n  await app.register(fastifySwagger, {\n    openapi: {\n      info: {\n        title: \"Stagehand API v4\",\n        version: \"4.0.0\",\n        description: `Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\nexecute browser automation tasks remotely on the Browserbase cloud.\nCreate a browser session with /browsersession, then use that id with page routes.\nResponses are streamed using Server-Sent Events (SSE) when the\n\\`x-stream-response: true\\` header is provided.\n\nThis SDK is currently ALPHA software and is not production ready!\nPlease try it and give us your feedback, stay tuned for upcoming release announcements!`,\n        contact: {\n          name: \"Browserbase\",\n          url: \"https://browserbase.com\",\n        },\n      },\n      openapi: \"3.1.0\",\n      servers: [\n        {\n          url: \"https://api.stagehand.browserbase.com\",\n        },\n      ],\n      components: {\n        securitySchemes: Api.openApiSecuritySchemes,\n        links: Api.openApiLinks,\n      },\n      security: [\n        { BrowserbaseApiKey: [], BrowserbaseProjectId: [], ModelApiKey: [] },\n      ],\n    },\n    ...fastifyZodOpenApiTransformers,\n  });\n\n  await app.register(\n    (instance, _opts, done) => {\n      for (const route of browserSessionRoutes) {\n        instance.route(route);\n      }\n      for (const route of pageRoutes) {\n        instance.route(route);\n      }\n      done();\n    },\n    { prefix: \"/v4\" },\n  );\n\n  app.route(healthcheckRoute);\n  app.route(readinessRoute);\n\n  await app.ready();\n\n  const yaml = app.swagger({ yaml: true });\n  // Mintlify expects OpenAPI version fields to be strings, so quote them here.\n  const fixedYaml = yaml\n    .replace(/^openapi:\\s*(?!['\"])([^#\\s]+)\\s*$/m, 'openapi: \"$1\"')\n    .replace(/^ {2}version:\\s*(?!['\"])([^#\\s]+)\\s*$/m, '  version: \"$1\"')\n    .replace(\n      \"description: Wait for captcha solves (deprecated, v2 only)\",\n      \"description: Wait for captcha solves\",\n    )\n    .replace(\n      \"description: Timeout in ms for act operations (deprecated, v2 only)\",\n      \"description: Timeout in ms for act operations\",\n    );\n\n  await writeFile(OUTPUT_PATH, fixedYaml, \"utf8\");\n\n  await app.close();\n  console.log(`OpenAPI spec written to ${OUTPUT_PATH}`);\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/server-v4/scripts/runtimePaths.ts",
    "content": "/**\n * Keep this file in sync with:\n * - /packages/core/lib/v3/runtimePaths.ts\n * - /packages/server-v4/scripts/runtimePaths.ts\n * - /packages/evals/runtimePaths.ts\n * - /packages/docs/scripts/runtimePaths.js\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createRequire } from \"node:module\";\n\nconst PACKAGE_SEGMENT = \"/packages/server-v4/\";\nconst EVAL_FRAMES = new Set([\"[eval]\", \"[eval]-wrapper\"]);\nconst INTERNAL_FRAME_NAMES = new Set([\n  \"readCallsites\",\n  \"readCallsitePath\",\n  \"resolveCallerFilePath\",\n  \"getCurrentFilePath\",\n  \"getCurrentDirPath\",\n  \"getRepoRootDir\",\n  \"getPackageRootDir\",\n  \"createRequireFromCaller\",\n  \"isMainModule\",\n]);\n\nconst normalizePath = (value: string): string => {\n  const input = value.startsWith(\"file://\") ? fileURLToPath(value) : value;\n  return path.resolve(input).replaceAll(\"\\\\\", \"/\");\n};\n\nconst readCallsites = (): NodeJS.CallSite[] => {\n  const previousPrepare = Error.prepareStackTrace;\n  try {\n    Error.prepareStackTrace = (_, stack) => stack;\n    return (\n      (new Error().stack as unknown as NodeJS.CallSite[] | undefined) ?? []\n    );\n  } finally {\n    Error.prepareStackTrace = previousPrepare;\n  }\n};\n\ntype CallSiteWithScriptName = NodeJS.CallSite & {\n  getScriptNameOrSourceURL?: () => string | null;\n};\n\nconst readCallsitePath = (callsite: NodeJS.CallSite): string | null => {\n  const callsiteWithScript = callsite as CallSiteWithScriptName;\n  const rawPath =\n    callsite.getFileName() ?? callsiteWithScript.getScriptNameOrSourceURL?.();\n  if (!rawPath) return null;\n  if (rawPath.startsWith(\"node:\")) return null;\n  if (EVAL_FRAMES.has(rawPath)) return null;\n  return normalizePath(rawPath);\n};\n\nconst isInternalCallsite = (callsite: NodeJS.CallSite): boolean => {\n  const functionName = callsite.getFunctionName();\n  if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) return true;\n\n  const methodName = callsite.getMethodName();\n  if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) return true;\n\n  const callsiteString = callsite.toString();\n  for (const frameName of INTERNAL_FRAME_NAMES) {\n    if (callsiteString.includes(`${frameName} (`)) return true;\n    if (callsiteString.includes(`.${frameName} (`)) return true;\n  }\n  return false;\n};\n\nconst resolveCallerFilePath = (): string => {\n  const packageCandidates: string[] = [];\n  const fallbackCandidates: string[] = [];\n\n  for (const callsite of readCallsites()) {\n    const filePath = readCallsitePath(callsite);\n    if (!filePath) continue;\n    if (isInternalCallsite(callsite)) continue;\n    if (filePath.includes(PACKAGE_SEGMENT)) {\n      packageCandidates.push(filePath);\n      continue;\n    }\n    fallbackCandidates.push(filePath);\n  }\n\n  const packageCandidate = packageCandidates[0];\n  if (packageCandidate) return packageCandidate;\n\n  const fallbackCandidate = fallbackCandidates[0];\n  if (fallbackCandidate) return fallbackCandidate;\n\n  throw new Error(\"Unable to resolve caller file path.\");\n};\n\nexport const getCurrentFilePath = (): string => resolveCallerFilePath();\n\nexport const getCurrentDirPath = (): string =>\n  path.dirname(getCurrentFilePath());\n\nexport const getRepoRootDir = (): string => {\n  const currentFilePath = getCurrentFilePath();\n  const index = currentFilePath.lastIndexOf(PACKAGE_SEGMENT);\n  if (index === -1) {\n    throw new Error(\n      `Unable to determine repo root from ${currentFilePath} (missing ${PACKAGE_SEGMENT}).`,\n    );\n  }\n  return currentFilePath.slice(0, index);\n};\n\nexport const getPackageRootDir = (): string =>\n  `${getRepoRootDir()}${PACKAGE_SEGMENT.slice(0, -1)}`;\n\nexport const createRequireFromCaller = () =>\n  createRequire(getCurrentFilePath());\n\nexport const isMainModule = (): boolean => {\n  const entryScript = process.argv.at(1);\n  if (!entryScript) return false;\n  return normalizePath(entryScript) === getCurrentFilePath();\n};\n"
  },
  {
    "path": "packages/server-v4/scripts/test-server.ts",
    "content": "/**\n * Server unit + integration tests on dist/esm + SEA/local server targets.\n *\n * Prereqs:\n * - pnpm run build (packages/server-v4/dist/tests + packages/server-v4/dist/server.js).\n * - SEA integration still requires build:sea when STAGEHAND_SERVER_TARGET=sea.\n *\n * Args: [test paths...] -- [node --test args...] | --list (prints JSON matrix)\n * Env: STAGEHAND_SERVER_TARGET=sea|local|remote, STAGEHAND_BASE_URL, SEA_BINARY_NAME,\n *      NODE_TEST_CONSOLE_REPORTER, NODE_TEST_REPORTER, NODE_TEST_REPORTER_DESTINATION,\n *      NODE_V8_COVERAGE; writes CTRF to ctrf/node-test-*.xml by default.\n * Example: STAGEHAND_SERVER_TARGET=sea pnpm run test:server -- packages/server-v4/dist/tests/integration/v4/start.test.js\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawn, spawnSync } from \"node:child_process\";\nimport { getRepoRootDir } from \"./runtimePaths.js\";\n\nconst ensureParentDir = (filePath: string) => {\n  fs.mkdirSync(path.dirname(filePath), { recursive: true });\n};\n\nconst splitArgs = (args: string[]) => {\n  const tokens = [...args];\n  while (tokens[0] === \"--\") {\n    tokens.shift();\n  }\n\n  const leadingExtra: string[] = [];\n  while (tokens.length > 0 && tokens[0].startsWith(\"-\")) {\n    const arg = tokens.shift();\n    if (!arg) break;\n    if (arg === \"--\") break;\n    leadingExtra.push(arg);\n    if (\n      !arg.includes(\"=\") &&\n      tokens[0] &&\n      tokens[0] !== \"--\" &&\n      !tokens[0].startsWith(\"-\")\n    ) {\n      leadingExtra.push(tokens.shift() as string);\n    }\n  }\n\n  while (tokens[0] === \"--\") {\n    tokens.shift();\n  }\n\n  const separatorIndex = tokens.indexOf(\"--\");\n  return {\n    paths: separatorIndex === -1 ? tokens : tokens.slice(0, separatorIndex),\n    extra: [\n      ...leadingExtra,\n      ...(separatorIndex === -1 ? [] : tokens.slice(separatorIndex + 1)),\n    ],\n  };\n};\n\nconst toSafeName = (name: string) => name.replace(/[\\\\/]/g, \"-\");\n\nconst collectFiles = (dir: string, suffix: string) => {\n  const results: string[] = [];\n  const walk = (current: string) => {\n    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n      const full = `${current}/${entry.name}`;\n      if (entry.isDirectory()) {\n        walk(full);\n      } else if (entry.isFile() && entry.name.endsWith(suffix)) {\n        results.push(full);\n      }\n    }\n  };\n  if (fs.existsSync(dir)) walk(dir);\n  return results.sort();\n};\n\nconst repoRoot = getRepoRootDir();\n\nconst writeCtrfFromJunit = (junitPath: string, tool: string) => {\n  if (!fs.existsSync(junitPath)) return;\n  const stat = fs.statSync(junitPath);\n  if (stat.size === 0) return;\n  const ctrfPath = junitPath.match(/\\.xml$/i)\n    ? junitPath.replace(/\\.xml$/i, \".json\")\n    : `${junitPath}.json`;\n  const result = spawnSync(\n    \"pnpm\",\n    [\"exec\", \"junit-to-ctrf\", junitPath, \"-o\", ctrfPath, \"-t\", tool],\n    { stdio: \"inherit\", cwd: repoRoot },\n  );\n  if (result.status !== 0) {\n    console.warn(`CTRF conversion failed for ${junitPath}.`);\n  }\n};\n\nconst sourceTestsDir = `${repoRoot}/packages/server-v4/test`;\nconst sourceUnitDir = `${sourceTestsDir}/unit`;\nconst sourceIntegrationDir = `${sourceTestsDir}/integration`;\nconst unitDir = `${repoRoot}/packages/server-v4/dist/tests/unit`;\nconst integrationDir = `${repoRoot}/packages/server-v4/dist/tests/integration`;\nconst allTestsDir = `${repoRoot}/packages/server-v4/dist/tests`;\n\nconst resolveRepoRelative = (value: string) =>\n  path.isAbsolute(value) ? value : path.resolve(repoRoot, value);\n\nconst stripNodeReporterArgs = (argsList: string[]) => {\n  const filtered: string[] = [];\n  let removed = false;\n  for (let i = 0; i < argsList.length; i++) {\n    const arg = argsList[i];\n    if (\n      arg === \"--test-reporter\" ||\n      arg.startsWith(\"--test-reporter=\") ||\n      arg === \"--test-reporter-destination\" ||\n      arg.startsWith(\"--test-reporter-destination=\")\n    ) {\n      removed = true;\n      if (\n        (arg === \"--test-reporter\" || arg === \"--test-reporter-destination\") &&\n        argsList[i + 1]\n      ) {\n        i += 1;\n      }\n      continue;\n    }\n    filtered.push(arg);\n  }\n  return { filtered, removed };\n};\n\nconst toTestName = (testPath: string, root: string) => {\n  const abs = resolveRepoRelative(testPath);\n  const rel = path.relative(root, abs).replaceAll(\"\\\\\", \"/\");\n  if (!rel.startsWith(\"..\")) {\n    return rel.replace(/\\.test\\.js$/i, \"\");\n  }\n  return path.basename(abs).replace(/\\.test\\.js$/i, \"\");\n};\n\nconst rawArgs = process.argv.slice(2);\nconst listRequested = rawArgs.includes(\"--list\");\n\nif (listRequested) {\n  const unitTests = collectFiles(sourceUnitDir, \".test.ts\").map((file) => {\n    const relSource = path.relative(sourceTestsDir, file).replaceAll(\"\\\\\", \"/\");\n    const distPath = `${repoRoot}/packages/server-v4/dist/tests/${relSource.replace(/\\.test\\.ts$/, \".test.js\")}`;\n    const name = path.basename(file, \".test.ts\");\n    return {\n      path: path.relative(repoRoot, distPath).replaceAll(\"\\\\\", \"/\"),\n      name,\n      safe_name: toSafeName(name),\n    };\n  });\n  const integrationTests = collectFiles(sourceIntegrationDir, \".test.ts\").map(\n    (file) => {\n      const relSource = path\n        .relative(sourceTestsDir, file)\n        .replaceAll(\"\\\\\", \"/\");\n      const distPath = `${repoRoot}/packages/server-v4/dist/tests/${relSource.replace(/\\.test\\.ts$/, \".test.js\")}`;\n      const rel = path\n        .relative(sourceIntegrationDir, file)\n        .replaceAll(\"\\\\\", \"/\")\n        .replace(/\\.test\\.ts$/, \"\");\n      return {\n        path: path.relative(repoRoot, distPath).replaceAll(\"\\\\\", \"/\"),\n        name: rel,\n        safe_name: toSafeName(rel),\n      };\n    },\n  );\n  console.log(JSON.stringify([...unitTests, ...integrationTests]));\n  process.exit(0);\n}\n\nconst { paths, extra } = splitArgs(rawArgs);\nconst { filtered: extraArgs, removed: removedReporterOverride } =\n  stripNodeReporterArgs(extra);\nif (removedReporterOverride) {\n  console.warn(\n    \"Ignoring node --test reporter overrides to preserve console + JUnit output.\",\n  );\n}\n\nif (!fs.existsSync(allTestsDir)) {\n  console.error(\n    \"Missing packages/server-v4/dist/tests. Run pnpm run build first.\",\n  );\n  process.exit(1);\n}\n\nconst serverTarget = (\n  process.env.STAGEHAND_SERVER_TARGET ?? \"sea\"\n).toLowerCase();\nconst explicitBaseUrl = process.env.STAGEHAND_BASE_URL;\nconst baseUrl = explicitBaseUrl ?? \"http://stagehand-api.localhost:3107\";\n\nif (serverTarget === \"remote\" && !explicitBaseUrl) {\n  console.error(\"Missing STAGEHAND_BASE_URL for remote server target.\");\n  process.exit(1);\n}\n\nif (\n  serverTarget === \"local\" &&\n  !fs.existsSync(`${repoRoot}/packages/server-v4/dist/server.js`)\n) {\n  console.error(\n    \"Missing packages/server-v4/dist/server.js. Run pnpm run build first.\",\n  );\n  process.exit(1);\n}\n\nconst parsedBaseUrl = new URL(baseUrl);\nconst port =\n  parsedBaseUrl.port || (parsedBaseUrl.protocol === \"https:\" ? \"443\" : \"80\");\n\nprocess.env.PORT = port;\nprocess.env.STAGEHAND_API_URL = baseUrl;\nprocess.env.BB_ENV = process.env.BB_ENV ?? \"local\";\n\nconst baseNodeOptions = \"--enable-source-maps\";\nconst nodeOptions = [process.env.NODE_OPTIONS, baseNodeOptions]\n  .filter(Boolean)\n  .join(\" \");\n\nconst allPaths =\n  paths.length > 0\n    ? paths.map(resolveRepoRelative)\n    : [\n        ...collectFiles(unitDir, \".test.js\"),\n        ...collectFiles(integrationDir, \".test.js\"),\n      ];\n\nconst unitPaths = allPaths.filter((p) =>\n  p.replaceAll(\"\\\\\", \"/\").includes(\"/packages/server-v4/dist/tests/unit/\"),\n);\nconst integrationPaths = allPaths.filter((p) =>\n  p\n    .replaceAll(\"\\\\\", \"/\")\n    .includes(\"/packages/server-v4/dist/tests/integration/\"),\n);\n\nconst singlePath = allPaths.length === 1 ? allPaths[0] : null;\nconst coverageSuffix =\n  singlePath &&\n  singlePath.startsWith(`${repoRoot}/packages/server-v4/dist/tests/unit/`)\n    ? `server-unit/${path.basename(singlePath).replace(/\\.test\\.js$/, \"\")}`\n    : singlePath &&\n        singlePath.startsWith(\n          `${repoRoot}/packages/server-v4/dist/tests/integration/`,\n        )\n      ? `server-integration/${path\n          .relative(integrationDir, singlePath)\n          .replace(/\\.test\\.js$/, \"\")\n          .replaceAll(\"\\\\\", \"/\")}`\n      : \"server\";\n\nconst coverageRoot = resolveRepoRelative(\n  process.env.NODE_V8_COVERAGE ?? `${repoRoot}/coverage/${coverageSuffix}`,\n);\nconst testsCoverage = `${coverageRoot}/tests`;\nconst serverCoverage = `${coverageRoot}/server`;\nfs.mkdirSync(testsCoverage, { recursive: true });\nfs.mkdirSync(serverCoverage, { recursive: true });\n\nconst consoleReporter = process.env.NODE_TEST_CONSOLE_REPORTER ?? \"spec\";\nconst defaultReporter = process.env.NODE_TEST_REPORTER ?? \"junit\";\nconst envDestination = process.env.NODE_TEST_REPORTER_DESTINATION\n  ? resolveRepoRelative(process.env.NODE_TEST_REPORTER_DESTINATION)\n  : null;\n\nconst reporterArgsFor = (kind: \"unit\" | \"integration\", testName?: string) => {\n  const destination =\n    envDestination ??\n    `${repoRoot}/ctrf/${kind === \"unit\" ? \"server-unit\" : \"server-integration\"}/${testName ? `${testName}.xml` : \"all.xml\"}`;\n  ensureParentDir(destination);\n  return {\n    args: [\n      `--test-reporter=${consoleReporter}`,\n      `--test-reporter=${defaultReporter}`,\n      \"--test-reporter-destination=stdout\",\n      `--test-reporter-destination=${destination}`,\n    ],\n    destination,\n  };\n};\n\nconst runNodeTests = (files: string[], reporterArgs: string[]) =>\n  spawnSync(\n    process.execPath,\n    [\"--test\", ...extraArgs, ...reporterArgs, ...files],\n    {\n      stdio: \"inherit\",\n      env: {\n        ...process.env,\n        NODE_OPTIONS: nodeOptions,\n        NODE_V8_COVERAGE: testsCoverage,\n      },\n    },\n  );\n\nconst waitForServer = async (url: string, timeoutMs = 30_000) => {\n  const start = Date.now();\n  while (Date.now() - start < timeoutMs) {\n    try {\n      const controller = new AbortController();\n      const timer = setTimeout(() => controller.abort(), 2_000);\n      const res = await fetch(url, { signal: controller.signal });\n      clearTimeout(timer);\n      if (res.ok) return true;\n    } catch {\n      // retry\n    }\n    await new Promise((resolve) => setTimeout(resolve, 1_000));\n  }\n  return false;\n};\n\nconst startServer = async () => {\n  if (serverTarget === \"remote\") return null;\n  if (serverTarget === \"local\") {\n    return spawn(\n      process.execPath,\n      [`${repoRoot}/packages/server-v4/dist/server.js`],\n      {\n        stdio: \"inherit\",\n        env: {\n          ...process.env,\n          NODE_ENV: \"development\",\n          NODE_OPTIONS: nodeOptions,\n          NODE_V8_COVERAGE: serverCoverage,\n        },\n      },\n    );\n  }\n\n  const defaultName = `stagehand-server-v4-${process.platform}-${process.arch}${process.platform === \"win32\" ? \".exe\" : \"\"}`;\n  const seaBinary = `${repoRoot}/packages/server-v4/dist/sea/${process.env.SEA_BINARY_NAME ?? defaultName}`;\n\n  if (!fs.existsSync(seaBinary)) {\n    console.error(`SEA binary not found at ${seaBinary}`);\n    process.exit(1);\n  }\n\n  return spawn(seaBinary, [\"--node-options=--no-lazy --enable-source-maps\"], {\n    stdio: \"inherit\",\n    env: {\n      ...process.env,\n      NODE_ENV: \"production\",\n      NODE_V8_COVERAGE: serverCoverage,\n      STAGEHAND_SEA_CACHE_DIR:\n        process.env.STAGEHAND_SEA_CACHE_DIR ?? `${repoRoot}/.stagehand-sea`,\n    },\n  });\n};\n\nlet serverProc: ReturnType<typeof spawn> | null = null;\nlet status = 0;\n\nif (unitPaths.length > 0) {\n  const unitName =\n    unitPaths.length === 1 ? toTestName(unitPaths[0], unitDir) : undefined;\n  const reporter = reporterArgsFor(\"unit\", unitName);\n  const result = runNodeTests(unitPaths, reporter.args);\n  status = result.status ?? 1;\n  writeCtrfFromJunit(reporter.destination, \"node-test\");\n}\n\nif (status === 0 && integrationPaths.length > 0) {\n  serverProc = await startServer();\n  const ready = await waitForServer(`${process.env.STAGEHAND_API_URL}/healthz`);\n  if (!ready) {\n    console.error(\"Server failed to start within 30 seconds.\");\n    status = 1;\n  } else {\n    const integrationName =\n      integrationPaths.length === 1\n        ? toTestName(integrationPaths[0], integrationDir)\n        : undefined;\n    const reporter = reporterArgsFor(\"integration\", integrationName);\n    const result = runNodeTests(integrationPaths, reporter.args);\n    status = result.status ?? 1;\n    writeCtrfFromJunit(reporter.destination, \"node-test\");\n  }\n}\n\nif (serverProc) {\n  serverProc.kill(\"SIGTERM\");\n  await new Promise<void>((resolve) => {\n    if (serverProc?.exitCode !== null) return resolve();\n    const timer = setTimeout(resolve, 10_000);\n    serverProc?.once(\"exit\", () => {\n      clearTimeout(timer);\n      resolve();\n    });\n  });\n  await new Promise((resolve) => setTimeout(resolve, 5_000));\n}\n\nprocess.exit(status);\n"
  },
  {
    "path": "packages/server-v4/src/routes/healthcheck.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { z } from \"zod/v4\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nconst healthcheckRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/healthz\",\n  logLevel: \"silent\",\n  schema: {\n    hide: true, // Hide from OpenAPI spec - utility endpoint\n    response: {\n      200: z\n        .object({\n          status: z.string(),\n          timestamp: z.string(),\n        })\n        .strict(),\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: async () => ({\n    status: \"ok\",\n    timestamp: new Date().toISOString(),\n  }),\n};\n\nexport default healthcheckRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/readiness.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { z } from \"zod/v4\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\n// Server readiness state management\nlet isReady = false;\n\n/**\n * Get the current readiness state of the server\n * @returns {boolean} Whether the server is ready to accept requests\n */\nexport const getIsReady = (): boolean => {\n  return isReady;\n};\n\n/**\n * Mark the server as ready to accept requests\n */\nexport const setReady = (): void => {\n  isReady = true;\n};\n\n/**\n * Mark the server as not ready to accept requests\n * Used during graceful shutdown to stop accepting new requests\n */\nexport const setUnready = (): void => {\n  isReady = false;\n};\n\nconst readinessRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/readyz\",\n  logLevel: \"silent\",\n  schema: {\n    hide: true, // Hide from OpenAPI spec - utility endpoint\n    response: {\n      200: z.string(),\n      503: z.string(),\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: async (_request, reply) => {\n    if (!isReady) {\n      return reply\n        .code(StatusCodes.SERVICE_UNAVAILABLE)\n        .send(\"Service Unavailable\");\n    }\n    return reply.code(StatusCodes.OK).send(\"Ready\");\n  },\n};\n\nexport default readinessRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/_id/end.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionEndRequestSchema,\n  BrowserSessionHeadersSchema,\n  BrowserSessionIdParamsSchema,\n  BrowserSessionResponseSchema,\n  type BrowserSessionIdParams,\n} from \"../../../../schemas/v4/browserSession.js\";\nimport { buildBrowserSession } from \"../shared.js\";\n\nconst endBrowserSessionHandler: RouteHandlerMethod = async (request, reply) => {\n  const { id } = request.params as BrowserSessionIdParams;\n\n  return reply.status(StatusCodes.OK).send(\n    BrowserSessionResponseSchema.parse({\n      success: true,\n      data: {\n        browserSession: buildBrowserSession({\n          id,\n          env: \"LOCAL\",\n          status: \"ended\",\n          modelName: \"stub/model\",\n          cdpUrl: \"ws://stub.invalid/devtools/browser/stub\",\n          available: false,\n        }),\n      },\n    }),\n  );\n};\n\nconst endBrowserSessionRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/:id/end\",\n  schema: {\n    operationId: \"BrowserSessionEnd\",\n    summary: \"End a browser session\",\n    headers: BrowserSessionHeadersSchema,\n    params: BrowserSessionIdParamsSchema,\n    body: BrowserSessionEndRequestSchema,\n    response: {\n      200: BrowserSessionResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: endBrowserSessionHandler,\n};\n\nexport default endBrowserSessionRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/_id/index.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionHeadersSchema,\n  BrowserSessionIdParamsSchema,\n  BrowserSessionResponseSchema,\n  type BrowserSessionIdParams,\n} from \"../../../../schemas/v4/browserSession.js\";\nimport { buildBrowserSession } from \"../shared.js\";\n\nconst getBrowserSessionHandler: RouteHandlerMethod = async (request, reply) => {\n  const { id } = request.params as BrowserSessionIdParams;\n\n  return reply.status(StatusCodes.OK).send(\n    BrowserSessionResponseSchema.parse({\n      success: true,\n      data: {\n        browserSession: buildBrowserSession({\n          id,\n          env: \"LOCAL\",\n          status: \"running\",\n          modelName: \"stub/model\",\n          cdpUrl: \"ws://stub.invalid/devtools/browser/stub\",\n          available: false,\n        }),\n      },\n    }),\n  );\n};\n\nconst getBrowserSessionRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/browsersession/:id\",\n  schema: {\n    operationId: \"BrowserSessionStatus\",\n    summary: \"Get browser session status\",\n    headers: BrowserSessionHeadersSchema,\n    params: BrowserSessionIdParamsSchema,\n    response: {\n      200: BrowserSessionResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: getBrowserSessionHandler,\n};\n\nexport default getBrowserSessionRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/action/_actionId.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionActionDetailsQuerySchema,\n  BrowserSessionActionDetailsResponseSchema,\n  BrowserSessionActionIdParamsSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionDetailsHandler,\n  browserSessionActionErrorResponses,\n} from \"../shared.js\";\n\nconst browserSessionActionDetailsRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/browsersession/action/:actionId\",\n  schema: {\n    operationId: \"BrowserSessionActionDetails\",\n    summary: \"browserSession.action\",\n    headers: BrowserSessionHeadersSchema,\n    params: BrowserSessionActionIdParamsSchema,\n    querystring: BrowserSessionActionDetailsQuerySchema,\n    response: {\n      200: BrowserSessionActionDetailsResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: browserSessionActionDetailsHandler,\n};\n\nexport default browserSessionActionDetailsRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/action/index.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionActionListQuerySchema,\n  BrowserSessionActionListResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  browserSessionActionListHandler,\n} from \"../shared.js\";\n\nconst browserSessionActionListRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/browsersession/action\",\n  schema: {\n    operationId: \"BrowserSessionActionList\",\n    summary: \"browserSession.actions\",\n    headers: BrowserSessionHeadersSchema,\n    querystring: BrowserSessionActionListQuerySchema,\n    response: {\n      200: BrowserSessionActionListResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: browserSessionActionListHandler,\n};\n\nexport default browserSessionActionListRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/activePage.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionActivePageActionSchema,\n  BrowserSessionOptionalPageResultSchema,\n  BrowserSessionActivePageRequestSchema,\n  BrowserSessionActivePageResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  buildStubBrowserSessionPage,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst activePageRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/activePage\",\n  schema: {\n    operationId: \"BrowserSessionActivePage\",\n    summary: \"browserSession.activePage\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionActivePageRequestSchema,\n    response: {\n      200: BrowserSessionActivePageResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"activePage\",\n    actionSchema: BrowserSessionActivePageActionSchema,\n    execute: async ({ sessionId }) => {\n      const page = buildStubBrowserSessionPage(sessionId);\n      return {\n        pageId: page.pageId,\n        result: BrowserSessionOptionalPageResultSchema.parse({ page }),\n      };\n    },\n  }),\n};\n\nexport default activePageRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/addCookies.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionAddCookiesActionSchema,\n  BrowserSessionAddCookiesResultSchema,\n  BrowserSessionAddCookiesRequestSchema,\n  BrowserSessionAddCookiesResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst addCookiesRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/addCookies\",\n  schema: {\n    operationId: \"BrowserSessionAddCookies\",\n    summary: \"browserSession.addCookies\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionAddCookiesRequestSchema,\n    response: {\n      200: BrowserSessionAddCookiesResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"addCookies\",\n    actionSchema: BrowserSessionAddCookiesActionSchema,\n    execute: async ({ params }) => {\n      return {\n        result: BrowserSessionAddCookiesResultSchema.parse({\n          added: params.cookies.length,\n        }),\n      };\n    },\n  }),\n};\n\nexport default addCookiesRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/addInitScript.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionAddInitScriptActionSchema,\n  BrowserSessionAddInitScriptResultSchema,\n  BrowserSessionAddInitScriptRequestSchema,\n  BrowserSessionAddInitScriptResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst addInitScriptRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/addInitScript\",\n  schema: {\n    operationId: \"BrowserSessionAddInitScript\",\n    summary: \"browserSession.addInitScript\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionAddInitScriptRequestSchema,\n    response: {\n      200: BrowserSessionAddInitScriptResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"addInitScript\",\n    actionSchema: BrowserSessionAddInitScriptActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionAddInitScriptResultSchema.parse({ added: true }),\n      };\n    },\n  }),\n};\n\nexport default addInitScriptRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/awaitActivePage.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionAwaitActivePageActionSchema,\n  BrowserSessionPageResultSchema,\n  BrowserSessionAwaitActivePageRequestSchema,\n  BrowserSessionAwaitActivePageResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  buildStubBrowserSessionPage,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst awaitActivePageRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/awaitActivePage\",\n  schema: {\n    operationId: \"BrowserSessionAwaitActivePage\",\n    summary: \"browserSession.awaitActivePage\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionAwaitActivePageRequestSchema,\n    response: {\n      200: BrowserSessionAwaitActivePageResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"awaitActivePage\",\n    actionSchema: BrowserSessionAwaitActivePageActionSchema,\n    execute: async ({ sessionId }) => {\n      const page = buildStubBrowserSessionPage(sessionId);\n      return {\n        pageId: page.pageId,\n        result: BrowserSessionPageResultSchema.parse({ page }),\n      };\n    },\n  }),\n};\n\nexport default awaitActivePageRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/browserbaseDebugURL.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionBrowserbaseDebugURLActionSchema,\n  BrowserSessionBrowserbaseDebugURLResultSchema,\n  BrowserSessionBrowserbaseDebugURLRequestSchema,\n  BrowserSessionBrowserbaseDebugURLResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst browserbaseDebugURLRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/browserbaseDebugURL\",\n  schema: {\n    operationId: \"BrowserSessionBrowserbaseDebugURL\",\n    summary: \"browserSession.browserbaseDebugURL\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionBrowserbaseDebugURLRequestSchema,\n    response: {\n      200: BrowserSessionBrowserbaseDebugURLResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"browserbaseDebugURL\",\n    actionSchema: BrowserSessionBrowserbaseDebugURLActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionBrowserbaseDebugURLResultSchema.parse({\n          browserbaseDebugURL: \"https://stub.invalid/debug\",\n        }),\n      };\n    },\n  }),\n};\n\nexport default browserbaseDebugURLRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/browserbaseSessionID.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionBrowserbaseSessionIDActionSchema,\n  BrowserSessionBrowserbaseSessionIDResultSchema,\n  BrowserSessionBrowserbaseSessionIDRequestSchema,\n  BrowserSessionBrowserbaseSessionIDResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst browserbaseSessionIDRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/browserbaseSessionID\",\n  schema: {\n    operationId: \"BrowserSessionBrowserbaseSessionID\",\n    summary: \"browserSession.browserbaseSessionID\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionBrowserbaseSessionIDRequestSchema,\n    response: {\n      200: BrowserSessionBrowserbaseSessionIDResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"browserbaseSessionID\",\n    actionSchema: BrowserSessionBrowserbaseSessionIDActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionBrowserbaseSessionIDResultSchema.parse({\n          browserbaseSessionID: \"bb_session_stub\",\n        }),\n      };\n    },\n  }),\n};\n\nexport default browserbaseSessionIDRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/browserbaseSessionURL.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionBrowserbaseSessionURLActionSchema,\n  BrowserSessionBrowserbaseSessionURLResultSchema,\n  BrowserSessionBrowserbaseSessionURLRequestSchema,\n  BrowserSessionBrowserbaseSessionURLResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst browserbaseSessionURLRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/browserbaseSessionURL\",\n  schema: {\n    operationId: \"BrowserSessionBrowserbaseSessionURL\",\n    summary: \"browserSession.browserbaseSessionURL\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionBrowserbaseSessionURLRequestSchema,\n    response: {\n      200: BrowserSessionBrowserbaseSessionURLResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"browserbaseSessionURL\",\n    actionSchema: BrowserSessionBrowserbaseSessionURLActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionBrowserbaseSessionURLResultSchema.parse({\n          browserbaseSessionURL: \"https://browserbase.com/sessions/stub\",\n        }),\n      };\n    },\n  }),\n};\n\nexport default browserbaseSessionURLRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/clearCookies.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionClearCookiesActionSchema,\n  BrowserSessionClearCookiesResultSchema,\n  BrowserSessionClearCookiesRequestSchema,\n  BrowserSessionClearCookiesResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst clearCookiesRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/clearCookies\",\n  schema: {\n    operationId: \"BrowserSessionClearCookies\",\n    summary: \"browserSession.clearCookies\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionClearCookiesRequestSchema,\n    response: {\n      200: BrowserSessionClearCookiesResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"clearCookies\",\n    actionSchema: BrowserSessionClearCookiesActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionClearCookiesResultSchema.parse({ cleared: true }),\n      };\n    },\n  }),\n};\n\nexport default clearCookiesRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/configuredViewport.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionConfiguredViewportActionSchema,\n  BrowserSessionConfiguredViewportResultSchema,\n  BrowserSessionConfiguredViewportRequestSchema,\n  BrowserSessionConfiguredViewportResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  buildStubViewport,\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst configuredViewportRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/configuredViewport\",\n  schema: {\n    operationId: \"BrowserSessionConfiguredViewport\",\n    summary: \"browserSession.configuredViewport\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionConfiguredViewportRequestSchema,\n    response: {\n      200: BrowserSessionConfiguredViewportResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"configuredViewport\",\n    actionSchema: BrowserSessionConfiguredViewportActionSchema,\n    execute: async () => {\n      return {\n        result:\n          BrowserSessionConfiguredViewportResultSchema.parse(\n            buildStubViewport(),\n          ),\n      };\n    },\n  }),\n};\n\nexport default configuredViewportRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/connectURL.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionConnectURLActionSchema,\n  BrowserSessionConnectURLResultSchema,\n  BrowserSessionConnectURLRequestSchema,\n  BrowserSessionConnectURLResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst connectURLRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/connectURL\",\n  schema: {\n    operationId: \"BrowserSessionConnectURL\",\n    summary: \"browserSession.connectURL\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionConnectURLRequestSchema,\n    response: {\n      200: BrowserSessionConnectURLResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"connectURL\",\n    actionSchema: BrowserSessionConnectURLActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionConnectURLResultSchema.parse({\n          connectURL: \"ws://stub.invalid/connect\",\n        }),\n      };\n    },\n  }),\n};\n\nexport default connectURLRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/cookies.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionCookiesActionSchema,\n  BrowserSessionCookiesResultSchema,\n  BrowserSessionCookiesRequestSchema,\n  BrowserSessionCookiesResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  buildStubBrowserSessionCookie,\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst cookiesRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/cookies\",\n  schema: {\n    operationId: \"BrowserSessionCookies\",\n    summary: \"browserSession.cookies\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionCookiesRequestSchema,\n    response: {\n      200: BrowserSessionCookiesResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"cookies\",\n    actionSchema: BrowserSessionCookiesActionSchema,\n    execute: async () => {\n      return {\n        result: BrowserSessionCookiesResultSchema.parse({\n          cookies: [buildStubBrowserSessionCookie()],\n        }),\n      };\n    },\n  }),\n};\n\nexport default cookiesRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/getFullFrameTreeByMainFrameId.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionGetFullFrameTreeByMainFrameIdActionSchema,\n  BrowserSessionFrameTreeResultSchema,\n  BrowserSessionGetFullFrameTreeByMainFrameIdRequestSchema,\n  BrowserSessionGetFullFrameTreeByMainFrameIdResponseSchema,\n  BrowserSessionHeadersSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst getFullFrameTreeByMainFrameIdRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/getFullFrameTreeByMainFrameId\",\n  schema: {\n    operationId: \"BrowserSessionGetFullFrameTreeByMainFrameId\",\n    summary: \"browserSession.getFullFrameTreeByMainFrameId\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionGetFullFrameTreeByMainFrameIdRequestSchema,\n    response: {\n      200: BrowserSessionGetFullFrameTreeByMainFrameIdResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"getFullFrameTreeByMainFrameId\",\n    actionSchema: BrowserSessionGetFullFrameTreeByMainFrameIdActionSchema,\n    execute: async ({ params }) => {\n      return {\n        pageId: \"page_stub\",\n        result: BrowserSessionFrameTreeResultSchema.parse({\n          frameTree: { mainFrameId: params.mainFrameId, children: [] },\n        }),\n      };\n    },\n  }),\n};\n\nexport default getFullFrameTreeByMainFrameIdRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/index.ts",
    "content": "import type { RouteHandlerMethod, RouteOptions } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { type FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionCreateRequestSchema,\n  BrowserSessionErrorResponseSchema,\n  BrowserSessionHeadersSchema,\n  BrowserSessionResponseSchema,\n  type BrowserSessionCreateRequest,\n} from \"../../../schemas/v4/browserSession.js\";\nimport { buildBrowserSession } from \"./shared.js\";\n\nconst createBrowserSessionHandler: RouteHandlerMethod = async (\n  request,\n  reply,\n) => {\n  const body = request.body as BrowserSessionCreateRequest;\n  const env = body.env === \"BROWSERBASE\" ? \"BROWSERBASE\" : \"LOCAL\";\n  const cdpUrl = \"cdpUrl\" in body ? body.cdpUrl : undefined;\n  const browserbaseSessionId =\n    \"browserbaseSessionId\" in body ? body.browserbaseSessionId : undefined;\n  const browserbaseSessionCreateParams =\n    \"browserbaseSessionCreateParams\" in body\n      ? body.browserbaseSessionCreateParams\n      : undefined;\n  const localBrowserLaunchOptions =\n    \"localBrowserLaunchOptions\" in body\n      ? body.localBrowserLaunchOptions\n      : undefined;\n\n  return reply.status(StatusCodes.OK).send(\n    BrowserSessionResponseSchema.parse({\n      success: true,\n      data: {\n        browserSession: buildBrowserSession({\n          id: \"session_stub\",\n          env,\n          status: \"running\",\n          modelName: body.modelName,\n          cdpUrl:\n            env === \"LOCAL\"\n              ? (cdpUrl ?? \"ws://stub.invalid/devtools/browser/stub\")\n              : \"ws://stub.invalid/devtools/browser/stub\",\n          available: false,\n          browserbaseSessionId,\n          browserbaseSessionCreateParams,\n          localBrowserLaunchOptions,\n          domSettleTimeoutMs: body.domSettleTimeoutMs,\n          verbose: body.verbose,\n          systemPrompt: body.systemPrompt,\n          selfHeal: body.selfHeal,\n          waitForCaptchaSolves: body.waitForCaptchaSolves,\n          experimental: body.experimental,\n          actTimeoutMs: body.actTimeoutMs,\n        }),\n      },\n    }),\n  );\n};\n\nconst createBrowserSessionRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession\",\n  schema: {\n    operationId: \"BrowserSessionCreate\",\n    summary: \"Create a browser session\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionCreateRequestSchema,\n    response: {\n      200: BrowserSessionResponseSchema,\n      400: BrowserSessionErrorResponseSchema,\n      401: BrowserSessionErrorResponseSchema,\n      500: BrowserSessionErrorResponseSchema,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionHandler,\n};\n\nexport default createBrowserSessionRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/newPage.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionHeadersSchema,\n  BrowserSessionNewPageActionSchema,\n  BrowserSessionPageResultSchema,\n  BrowserSessionNewPageRequestSchema,\n  BrowserSessionNewPageResponseSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  buildStubBrowserSessionPage,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst newPageRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/newPage\",\n  schema: {\n    operationId: \"BrowserSessionNewPage\",\n    summary: \"browserSession.newPage\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionNewPageRequestSchema,\n    response: {\n      200: BrowserSessionNewPageResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"newPage\",\n    actionSchema: BrowserSessionNewPageActionSchema,\n    execute: async ({ sessionId, params }) => {\n      const page = buildStubBrowserSessionPage(sessionId, { url: params.url });\n      return {\n        pageId: page.pageId,\n        result: BrowserSessionPageResultSchema.parse({ page }),\n      };\n    },\n  }),\n};\n\nexport default newPageRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/pages.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionHeadersSchema,\n  BrowserSessionPagesActionSchema,\n  BrowserSessionPagesResultSchema,\n  BrowserSessionPagesRequestSchema,\n  BrowserSessionPagesResponseSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  buildStubBrowserSessionPage,\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst pagesRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/pages\",\n  schema: {\n    operationId: \"BrowserSessionPages\",\n    summary: \"browserSession.pages\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionPagesRequestSchema,\n    response: {\n      200: BrowserSessionPagesResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"pages\",\n    actionSchema: BrowserSessionPagesActionSchema,\n    execute: async ({ sessionId }) => {\n      return {\n        result: BrowserSessionPagesResultSchema.parse({\n          pages: [buildStubBrowserSessionPage(sessionId)],\n        }),\n      };\n    },\n  }),\n};\n\nexport default pagesRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/resolvePageByMainFrameId.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionHeadersSchema,\n  BrowserSessionResolvePageByMainFrameIdActionSchema,\n  BrowserSessionOptionalPageResultSchema,\n  BrowserSessionResolvePageByMainFrameIdRequestSchema,\n  BrowserSessionResolvePageByMainFrameIdResponseSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  buildStubBrowserSessionPage,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst resolvePageByMainFrameIdRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/resolvePageByMainFrameId\",\n  schema: {\n    operationId: \"BrowserSessionResolvePageByMainFrameId\",\n    summary: \"browserSession.resolvePageByMainFrameId\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionResolvePageByMainFrameIdRequestSchema,\n    response: {\n      200: BrowserSessionResolvePageByMainFrameIdResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"resolvePageByMainFrameId\",\n    actionSchema: BrowserSessionResolvePageByMainFrameIdActionSchema,\n    execute: async ({ sessionId }) => {\n      const page = buildStubBrowserSessionPage(sessionId);\n      return {\n        pageId: page.pageId,\n        result: BrowserSessionOptionalPageResultSchema.parse({ page }),\n      };\n    },\n  }),\n};\n\nexport default resolvePageByMainFrameIdRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/routes.ts",
    "content": "import type { FastifyPluginCallback, RouteOptions } from \"fastify\";\n\nimport browserSessionActionDetailsRoute from \"./action/_actionId.js\";\nimport browserSessionActionListRoute from \"./action/index.js\";\nimport activePageRoute from \"./activePage.js\";\nimport addCookiesRoute from \"./addCookies.js\";\nimport addInitScriptRoute from \"./addInitScript.js\";\nimport awaitActivePageRoute from \"./awaitActivePage.js\";\nimport browserbaseDebugURLRoute from \"./browserbaseDebugURL.js\";\nimport browserbaseSessionIDRoute from \"./browserbaseSessionID.js\";\nimport browserbaseSessionURLRoute from \"./browserbaseSessionURL.js\";\nimport clearCookiesRoute from \"./clearCookies.js\";\nimport configuredViewportRoute from \"./configuredViewport.js\";\nimport connectURLRoute from \"./connectURL.js\";\nimport cookiesRoute from \"./cookies.js\";\nimport endBrowserSessionRoute from \"./_id/end.js\";\nimport getBrowserSessionRoute from \"./_id/index.js\";\nimport getFullFrameTreeByMainFrameIdRoute from \"./getFullFrameTreeByMainFrameId.js\";\nimport createBrowserSessionRoute from \"./index.js\";\nimport newPageRoute from \"./newPage.js\";\nimport pagesRoute from \"./pages.js\";\nimport resolvePageByMainFrameIdRoute from \"./resolvePageByMainFrameId.js\";\nimport setExtraHTTPHeadersRoute from \"./setExtraHTTPHeaders.js\";\nimport { buildBrowserSessionErrorResponse } from \"../../../schemas/v4/browserSession.js\";\nimport { normalizePluginError, withTag } from \"../pluginUtils.js\";\n\nconst rawBrowserSessionRoutes: RouteOptions[] = [\n  createBrowserSessionRoute,\n  getBrowserSessionRoute,\n  endBrowserSessionRoute,\n  addInitScriptRoute,\n  setExtraHTTPHeadersRoute,\n  pagesRoute,\n  activePageRoute,\n  awaitActivePageRoute,\n  resolvePageByMainFrameIdRoute,\n  getFullFrameTreeByMainFrameIdRoute,\n  newPageRoute,\n  cookiesRoute,\n  addCookiesRoute,\n  clearCookiesRoute,\n  connectURLRoute,\n  configuredViewportRoute,\n  browserbaseSessionIDRoute,\n  browserbaseSessionURLRoute,\n  browserbaseDebugURLRoute,\n  browserSessionActionListRoute,\n  browserSessionActionDetailsRoute,\n];\n\nexport const browserSessionRoutes: RouteOptions[] = rawBrowserSessionRoutes.map(\n  (route) => withTag(route, \"browserSession\"),\n);\n\nexport const browserSessionRoutesPlugin: FastifyPluginCallback = (\n  instance,\n  _opts,\n  done,\n) => {\n  instance.setErrorHandler((error, _request, reply) => {\n    const { errorMessage, stack, statusCode } = normalizePluginError(error);\n\n    return reply.status(statusCode).send(\n      buildBrowserSessionErrorResponse({\n        error: errorMessage,\n        statusCode,\n        stack,\n      }),\n    );\n  });\n\n  for (const route of browserSessionRoutes) {\n    instance.route(route);\n  }\n\n  done();\n};\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/setExtraHTTPHeaders.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  BrowserSessionHeadersSchema,\n  BrowserSessionSetExtraHTTPHeadersActionSchema,\n  BrowserSessionSetExtraHTTPHeadersResultSchema,\n  BrowserSessionSetExtraHTTPHeadersRequestSchema,\n  BrowserSessionSetExtraHTTPHeadersResponseSchema,\n} from \"../../../schemas/v4/browserSession.js\";\nimport {\n  browserSessionActionErrorResponses,\n  createBrowserSessionActionHandler,\n} from \"./shared.js\";\n\nconst setExtraHTTPHeadersRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/browsersession/setExtraHTTPHeaders\",\n  schema: {\n    operationId: \"BrowserSessionSetExtraHTTPHeaders\",\n    summary: \"browserSession.setExtraHTTPHeaders\",\n    headers: BrowserSessionHeadersSchema,\n    body: BrowserSessionSetExtraHTTPHeadersRequestSchema,\n    response: {\n      200: BrowserSessionSetExtraHTTPHeadersResponseSchema,\n      ...browserSessionActionErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createBrowserSessionActionHandler({\n    method: \"setExtraHTTPHeaders\",\n    actionSchema: BrowserSessionSetExtraHTTPHeadersActionSchema,\n    execute: async ({ params }) => {\n      return {\n        result: BrowserSessionSetExtraHTTPHeadersResultSchema.parse({\n          headers: params.headers,\n        }),\n      };\n    },\n  }),\n};\n\nexport default setExtraHTTPHeadersRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/browsersession/shared.ts",
    "content": "import { randomUUID } from \"node:crypto\";\n\nimport type { RouteHandlerMethod } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { z } from \"zod/v4\";\n\nimport {\n  type BrowserSession,\n  type BrowserSessionAction,\n  BrowserSessionPagesActionSchema,\n  type BrowserSessionActionDetailsQuery,\n  type BrowserSessionActionMethod,\n  type BrowserSessionPage,\n  BrowserSessionSchema,\n  BrowserSessionV4ErrorResponseSchema,\n} from \"../../../schemas/v4/browserSession.js\";\n\nexport function buildBrowserSession(input: {\n  id: string;\n  env: BrowserSession[\"env\"];\n  status: \"running\" | \"ended\";\n  available: boolean;\n  modelName: string;\n  cdpUrl?: string | null;\n  browserbaseSessionId?: string;\n  browserbaseSessionCreateParams?: BrowserSession[\"browserbaseSessionCreateParams\"];\n  localBrowserLaunchOptions?: BrowserSession[\"localBrowserLaunchOptions\"];\n  domSettleTimeoutMs?: number;\n  verbose?: BrowserSession[\"verbose\"];\n  systemPrompt?: string;\n  selfHeal?: boolean;\n  waitForCaptchaSolves?: boolean;\n  experimental?: boolean;\n  actTimeoutMs?: number;\n}): BrowserSession {\n  return BrowserSessionSchema.parse({\n    id: input.id,\n    env: input.env,\n    status: input.status,\n    modelName: input.modelName,\n    cdpUrl: input.cdpUrl ?? \"ws://stub.invalid/devtools/browser/stub\",\n    available: input.available,\n    browserbaseSessionId: input.browserbaseSessionId,\n    browserbaseSessionCreateParams: input.browserbaseSessionCreateParams,\n    localBrowserLaunchOptions: input.localBrowserLaunchOptions,\n    domSettleTimeoutMs: input.domSettleTimeoutMs,\n    verbose: input.verbose,\n    systemPrompt: input.systemPrompt,\n    selfHeal: input.selfHeal,\n    waitForCaptchaSolves: input.waitForCaptchaSolves,\n    experimental: input.experimental,\n    actTimeoutMs: input.actTimeoutMs,\n  });\n}\n\nexport const browserSessionActionErrorResponses = {\n  400: BrowserSessionV4ErrorResponseSchema,\n  401: BrowserSessionV4ErrorResponseSchema,\n  404: BrowserSessionV4ErrorResponseSchema,\n  408: BrowserSessionV4ErrorResponseSchema,\n  422: BrowserSessionV4ErrorResponseSchema,\n  500: BrowserSessionV4ErrorResponseSchema,\n};\n\ntype BrowserSessionRequestBody<TAction extends BrowserSessionAction> = {\n  sessionId: string;\n  params: TAction[\"params\"];\n};\n\ntype BrowserSessionActionHandlerContext<TAction extends BrowserSessionAction> =\n  {\n    params: TAction[\"params\"];\n    request: Parameters<RouteHandlerMethod>[0];\n    sessionId: string;\n    sessionStore: unknown;\n  };\n\ntype BrowserSessionActionExecutionResult<TAction extends BrowserSessionAction> =\n  {\n    result: TAction[\"result\"];\n    pageId?: string;\n  };\n\nexport function buildBrowserSessionPage(page: {\n  mainFrameId(): string;\n  targetId(): string;\n  url(): string;\n}): BrowserSessionPage {\n  const targetId = page.targetId();\n  return {\n    pageId: targetId,\n    targetId,\n    mainFrameId: page.mainFrameId(),\n    url: page.url(),\n  };\n}\n\nexport function buildStubBrowserSessionPage(\n  sessionId: string,\n  input?: { pageId?: string; url?: string },\n): BrowserSessionPage {\n  const pageId = input?.pageId ?? \"page_stub\";\n\n  return {\n    pageId,\n    targetId: pageId,\n    mainFrameId: \"frame_stub\",\n    url: input?.url ?? `https://stub.invalid/${sessionId}`,\n  };\n}\n\nexport function buildStubBrowserSessionCookie() {\n  return {\n    name: \"stub_cookie\",\n    value: \"stub_value\",\n    domain: \"stub.invalid\",\n    path: \"/\",\n    expires: 0,\n    httpOnly: false,\n    secure: true,\n    sameSite: \"Lax\" as const,\n  };\n}\n\nexport function buildStubViewport() {\n  return {\n    width: 1280,\n    height: 720,\n    deviceScaleFactor: 1,\n  };\n}\n\nfunction getInitialPageId(params: unknown): string | undefined {\n  if (\n    typeof params === \"object\" &&\n    params !== null &&\n    \"pageId\" in params &&\n    typeof (params as { pageId?: unknown }).pageId === \"string\"\n  ) {\n    return (params as { pageId: string }).pageId;\n  }\n\n  return undefined;\n}\n\nexport function toStringOrRegExp(\n  value?:\n    | string\n    | {\n        source: string;\n        flags?: string;\n      },\n): string | RegExp | undefined {\n  if (!value) {\n    return undefined;\n  }\n\n  if (typeof value === \"string\") {\n    return value;\n  }\n\n  return new RegExp(value.source, value.flags);\n}\n\nexport function createBrowserSessionActionHandler<\n  TAction extends BrowserSessionAction,\n>(options: {\n  actionSchema: z.ZodType<TAction>;\n  execute: (\n    ctx: BrowserSessionActionHandlerContext<TAction>,\n  ) => Promise<BrowserSessionActionExecutionResult<TAction>>;\n  method: BrowserSessionActionMethod;\n}): RouteHandlerMethod {\n  const { actionSchema, method } = options;\n\n  return async (request, reply) => {\n    const { params, sessionId } =\n      request.body as BrowserSessionRequestBody<TAction>;\n    const execution = await options.execute({\n      params,\n      request,\n      sessionId,\n      sessionStore: undefined,\n    });\n    const createdAt = new Date().toISOString();\n    const action = actionSchema.parse({\n      id: randomUUID(),\n      method,\n      status: \"completed\",\n      sessionId,\n      pageId: execution.pageId ?? getInitialPageId(params),\n      createdAt,\n      updatedAt: createdAt,\n      completedAt: createdAt,\n      error: null,\n      params,\n      result: execution.result,\n    });\n\n    return reply.status(StatusCodes.OK).send({\n      success: true,\n      error: null,\n      action,\n    });\n  };\n}\n\nexport const browserSessionActionDetailsHandler: RouteHandlerMethod = async (\n  request,\n  reply,\n) => {\n  const { actionId } = request.params as { actionId: string };\n  const { sessionId } = request.query as BrowserSessionActionDetailsQuery;\n  const createdAt = new Date().toISOString();\n  const action = BrowserSessionPagesActionSchema.parse({\n    id: actionId,\n    method: \"pages\",\n    status: \"completed\",\n    sessionId,\n    createdAt,\n    updatedAt: createdAt,\n    completedAt: createdAt,\n    error: null,\n    params: {},\n    result: {\n      pages: [buildStubBrowserSessionPage(sessionId)],\n    },\n  });\n\n  return reply.status(StatusCodes.OK).send({\n    success: true,\n    error: null,\n    action,\n  });\n};\n\nexport const browserSessionActionListHandler: RouteHandlerMethod = async (\n  request,\n  reply,\n) => {\n  const { sessionId } = request.query as BrowserSessionActionDetailsQuery;\n  const createdAt = new Date().toISOString();\n  return reply.status(StatusCodes.OK).send({\n    success: true,\n    error: null,\n    actions: [\n      BrowserSessionPagesActionSchema.parse({\n        id: randomUUID(),\n        method: \"pages\",\n        status: \"completed\",\n        sessionId,\n        createdAt,\n        updatedAt: createdAt,\n        completedAt: createdAt,\n        error: null,\n        params: {},\n        result: {\n          pages: [buildStubBrowserSessionPage(sessionId)],\n        },\n      }),\n    ],\n  });\n};\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/action/_actionId.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageActionDetailsQuerySchema,\n  PageActionDetailsResponseSchema,\n  PageActionIdParamsSchema,\n} from \"../../../../schemas/v4/page.js\";\nimport { pageActionDetailsHandler, pageErrorResponses } from \"../shared.js\";\n\nconst pageActionDetailsRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/action/:actionId\",\n  schema: {\n    operationId: \"PageActionDetails\",\n    summary: \"page.actionById\",\n    headers: Api.SessionHeadersSchema,\n    params: PageActionIdParamsSchema,\n    querystring: PageActionDetailsQuerySchema,\n    response: {\n      200: PageActionDetailsResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: pageActionDetailsHandler,\n};\n\nexport default pageActionDetailsRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/action/index.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageActionListQuerySchema,\n  PageActionListResponseSchema,\n} from \"../../../../schemas/v4/page.js\";\nimport { pageActionListHandler, pageErrorResponses } from \"../shared.js\";\n\nconst pageActionListRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/action\",\n  schema: {\n    operationId: \"PageActionList\",\n    summary: \"page.action\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageActionListQuerySchema,\n    response: {\n      200: PageActionListResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: pageActionListHandler,\n};\n\nexport default pageActionListRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/addInitScript.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageAddInitScriptActionSchema,\n  PageAddInitScriptResultSchema,\n  PageAddInitScriptRequestSchema,\n  PageAddInitScriptResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst addInitScriptRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/addInitScript\",\n  schema: {\n    operationId: \"PageAddInitScript\",\n    summary: \"page.addInitScript\",\n    headers: Api.SessionHeadersSchema,\n    body: PageAddInitScriptRequestSchema,\n    response: {\n      200: PageAddInitScriptResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"addInitScript\",\n    actionSchema: PageAddInitScriptActionSchema,\n    execute: async () => {\n      return PageAddInitScriptResultSchema.parse({ added: true });\n    },\n  }),\n};\n\nexport default addInitScriptRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/asProtocolFrameTree.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageAsProtocolFrameTreeActionSchema,\n  PageFrameTreeResultSchema,\n  PageAsProtocolFrameTreeRequestSchema,\n  PageAsProtocolFrameTreeResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst asProtocolFrameTreeRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/asProtocolFrameTree\",\n  schema: {\n    operationId: \"PageAsProtocolFrameTree\",\n    summary: \"page.asProtocolFrameTree\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageAsProtocolFrameTreeRequestSchema,\n    response: {\n      200: PageAsProtocolFrameTreeResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"asProtocolFrameTree\",\n    actionSchema: PageAsProtocolFrameTreeActionSchema,\n    execute: async ({ params }) => {\n      return PageFrameTreeResultSchema.parse({\n        frameTree: {\n          rootMainFrameId: params.rootMainFrameId,\n          children: [],\n        },\n      });\n    },\n  }),\n};\n\nexport default asProtocolFrameTreeRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/click.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageClickActionSchema,\n  PageClickRequestSchema,\n  PageClickResponseSchema,\n  PageXPathResultSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst clickRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/click\",\n  schema: {\n    operationId: \"PageClick\",\n    summary: \"page.click\",\n    headers: Api.SessionHeadersSchema,\n    body: PageClickRequestSchema,\n    response: {\n      200: PageClickResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"click\",\n    actionSchema: PageClickActionSchema,\n    execute: async ({ params }) => {\n      const sel = params.selector;\n      return PageXPathResultSchema.parse({\n        xpath: \"xpath\" in sel ? sel.xpath : \"xpath=//stub-click\",\n      });\n    },\n  }),\n};\n\nexport default clickRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/close.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageCloseActionSchema,\n  PageCloseResultSchema,\n  PageCloseRequestSchema,\n  PageCloseResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst closeRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/close\",\n  schema: {\n    operationId: \"PageClose\",\n    summary: \"page.close\",\n    headers: Api.SessionHeadersSchema,\n    body: PageCloseRequestSchema,\n    response: {\n      200: PageCloseResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"close\",\n    actionSchema: PageCloseActionSchema,\n    execute: async () => {\n      return PageCloseResultSchema.parse({ closed: true });\n    },\n  }),\n};\n\nexport default closeRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/dragAndDrop.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageDragAndDropActionSchema,\n  PageDragAndDropResultSchema,\n  PageDragAndDropRequestSchema,\n  PageDragAndDropResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst dragAndDropRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/dragAndDrop\",\n  schema: {\n    operationId: \"PageDragAndDrop\",\n    summary: \"page.dragAndDrop\",\n    headers: Api.SessionHeadersSchema,\n    body: PageDragAndDropRequestSchema,\n    response: {\n      200: PageDragAndDropResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"dragAndDrop\",\n    actionSchema: PageDragAndDropActionSchema,\n    execute: async ({ params }) => {\n      return PageDragAndDropResultSchema.parse({\n        fromXpath:\n          \"xpath\" in params.from ? params.from.xpath : \"xpath=//stub-from\",\n        toXpath: \"xpath\" in params.to ? params.to.xpath : \"xpath=//stub-to\",\n      });\n    },\n  }),\n};\n\nexport default dragAndDropRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/enableCursorOverlay.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageEnableCursorOverlayActionSchema,\n  PageEnableCursorOverlayResultSchema,\n  PageEnableCursorOverlayRequestSchema,\n  PageEnableCursorOverlayResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst enableCursorOverlayRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/enableCursorOverlay\",\n  schema: {\n    operationId: \"PageEnableCursorOverlay\",\n    summary: \"page.enableCursorOverlay\",\n    headers: Api.SessionHeadersSchema,\n    body: PageEnableCursorOverlayRequestSchema,\n    response: {\n      200: PageEnableCursorOverlayResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"enableCursorOverlay\",\n    actionSchema: PageEnableCursorOverlayActionSchema,\n    execute: async () => {\n      return PageEnableCursorOverlayResultSchema.parse({ enabled: true });\n    },\n  }),\n};\n\nexport default enableCursorOverlayRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/evaluate.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageEvaluateActionSchema,\n  PageEvaluateResultSchema,\n  PageEvaluateRequestSchema,\n  PageEvaluateResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst evaluateRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/evaluate\",\n  schema: {\n    operationId: \"PageEvaluate\",\n    summary: \"page.evaluate\",\n    headers: Api.SessionHeadersSchema,\n    body: PageEvaluateRequestSchema,\n    response: {\n      200: PageEvaluateResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"evaluate\",\n    actionSchema: PageEvaluateActionSchema,\n    execute: async ({ params }) => {\n      return PageEvaluateResultSchema.parse({\n        value: {\n          expression: params.expression,\n          arg: params.arg ?? null,\n        },\n      });\n    },\n  }),\n};\n\nexport default evaluateRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/frames.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageFramesActionSchema,\n  PageFramesResultSchema,\n  PageFramesRequestSchema,\n  PageFramesResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  buildStubPageFrame,\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst framesRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/frames\",\n  schema: {\n    operationId: \"PageFrames\",\n    summary: \"page.frames\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageFramesRequestSchema,\n    response: {\n      200: PageFramesResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"frames\",\n    actionSchema: PageFramesActionSchema,\n    execute: async ({ params }) => {\n      return PageFramesResultSchema.parse({\n        frames: [buildStubPageFrame(getPageId(params))],\n      });\n    },\n  }),\n};\n\nexport default framesRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/getFullFrameTree.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageGetFullFrameTreeActionSchema,\n  PageFrameTreeResultSchema,\n  PageGetFullFrameTreeRequestSchema,\n  PageGetFullFrameTreeResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst getFullFrameTreeRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/getFullFrameTree\",\n  schema: {\n    operationId: \"PageGetFullFrameTree\",\n    summary: \"page.getFullFrameTree\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageGetFullFrameTreeRequestSchema,\n    response: {\n      200: PageGetFullFrameTreeResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"getFullFrameTree\",\n    actionSchema: PageGetFullFrameTreeActionSchema,\n    execute: async ({ params }) => {\n      return PageFrameTreeResultSchema.parse({\n        frameTree: {\n          pageId: getPageId(params),\n          children: [],\n        },\n      });\n    },\n  }),\n};\n\nexport default getFullFrameTreeRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/getOrdinal.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageGetOrdinalActionSchema,\n  PageGetOrdinalResultSchema,\n  PageGetOrdinalRequestSchema,\n  PageGetOrdinalResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst getOrdinalRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/getOrdinal\",\n  schema: {\n    operationId: \"PageGetOrdinal\",\n    summary: \"page.getOrdinal\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageGetOrdinalRequestSchema,\n    response: {\n      200: PageGetOrdinalResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"getOrdinal\",\n    actionSchema: PageGetOrdinalActionSchema,\n    execute: async ({ params }) => {\n      return PageGetOrdinalResultSchema.parse({\n        frameId: params.frameId,\n        ordinal: 0,\n      });\n    },\n  }),\n};\n\nexport default getOrdinalRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/goBack.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageGoBackActionSchema,\n  PageNavigationResultSchema,\n  PageGoBackRequestSchema,\n  PageGoBackResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  buildStubNavigationResult,\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst goBackRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/goBack\",\n  schema: {\n    operationId: \"PageGoBack\",\n    summary: \"page.goBack\",\n    headers: Api.SessionHeadersSchema,\n    body: PageGoBackRequestSchema,\n    response: {\n      200: PageGoBackResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"goBack\",\n    actionSchema: PageGoBackActionSchema,\n    execute: async ({ params }) => {\n      return PageNavigationResultSchema.parse(\n        buildStubNavigationResult(`https://stub.invalid/${getPageId(params)}`),\n      );\n    },\n  }),\n};\n\nexport default goBackRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/goForward.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageGoForwardActionSchema,\n  PageNavigationResultSchema,\n  PageGoForwardRequestSchema,\n  PageGoForwardResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  buildStubNavigationResult,\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst goForwardRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/goForward\",\n  schema: {\n    operationId: \"PageGoForward\",\n    summary: \"page.goForward\",\n    headers: Api.SessionHeadersSchema,\n    body: PageGoForwardRequestSchema,\n    response: {\n      200: PageGoForwardResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"goForward\",\n    actionSchema: PageGoForwardActionSchema,\n    execute: async ({ params }) => {\n      return PageNavigationResultSchema.parse(\n        buildStubNavigationResult(`https://stub.invalid/${getPageId(params)}`),\n      );\n    },\n  }),\n};\n\nexport default goForwardRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/goto.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageGotoActionSchema,\n  PageNavigationResultSchema,\n  PageGotoRequestSchema,\n  PageGotoResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  buildStubNavigationResult,\n  createPageActionHandler,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst gotoRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/goto\",\n  schema: {\n    operationId: \"PageGoto\",\n    summary: \"page.goto\",\n    headers: Api.SessionHeadersSchema,\n    body: PageGotoRequestSchema,\n    response: {\n      200: PageGotoResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"goto\",\n    actionSchema: PageGotoActionSchema,\n    execute: async ({ params }) => {\n      return PageNavigationResultSchema.parse(\n        buildStubNavigationResult(params.url),\n      );\n    },\n  }),\n};\n\nexport default gotoRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/hover.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageHoverActionSchema,\n  PageHoverRequestSchema,\n  PageHoverResponseSchema,\n  PageXPathResultSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst hoverRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/hover\",\n  schema: {\n    operationId: \"PageHover\",\n    summary: \"page.hover\",\n    headers: Api.SessionHeadersSchema,\n    body: PageHoverRequestSchema,\n    response: {\n      200: PageHoverResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"hover\",\n    actionSchema: PageHoverActionSchema,\n    execute: async ({ params }) => {\n      const sel = params.selector;\n      return PageXPathResultSchema.parse({\n        xpath: \"xpath\" in sel ? sel.xpath : \"xpath=//stub-hover\",\n      });\n    },\n  }),\n};\n\nexport default hoverRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/keyPress.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageKeyPressActionSchema,\n  PageKeyPressResultSchema,\n  PageKeyPressRequestSchema,\n  PageKeyPressResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst keyPressRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/keyPress\",\n  schema: {\n    operationId: \"PageKeyPress\",\n    summary: \"page.keyPress\",\n    headers: Api.SessionHeadersSchema,\n    body: PageKeyPressRequestSchema,\n    response: {\n      200: PageKeyPressResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"keyPress\",\n    actionSchema: PageKeyPressActionSchema,\n    execute: async ({ params }) => {\n      return PageKeyPressResultSchema.parse({ key: params.key });\n    },\n  }),\n};\n\nexport default keyPressRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/listAllFrameIds.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageListAllFrameIdsActionSchema,\n  PageListAllFrameIdsResultSchema,\n  PageListAllFrameIdsRequestSchema,\n  PageListAllFrameIdsResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst listAllFrameIdsRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/listAllFrameIds\",\n  schema: {\n    operationId: \"PageListAllFrameIds\",\n    summary: \"page.listAllFrameIds\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageListAllFrameIdsRequestSchema,\n    response: {\n      200: PageListAllFrameIdsResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"listAllFrameIds\",\n    actionSchema: PageListAllFrameIdsActionSchema,\n    execute: async () => {\n      return PageListAllFrameIdsResultSchema.parse({\n        frameIds: [\"frame_stub\"],\n      });\n    },\n  }),\n};\n\nexport default listAllFrameIdsRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/mainFrame.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageMainFrameActionSchema,\n  PageMainFrameResultSchema,\n  PageMainFrameRequestSchema,\n  PageMainFrameResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  buildStubPageFrame,\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst mainFrameRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/mainFrame\",\n  schema: {\n    operationId: \"PageMainFrame\",\n    summary: \"page.mainFrame\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageMainFrameRequestSchema,\n    response: {\n      200: PageMainFrameResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"mainFrame\",\n    actionSchema: PageMainFrameActionSchema,\n    execute: async ({ params }) => {\n      return PageMainFrameResultSchema.parse({\n        frame: buildStubPageFrame(getPageId(params)),\n      });\n    },\n  }),\n};\n\nexport default mainFrameRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/mainFrameId.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageMainFrameIdActionSchema,\n  PageMainFrameIdResultSchema,\n  PageMainFrameIdRequestSchema,\n  PageMainFrameIdResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst mainFrameIdRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/mainFrameId\",\n  schema: {\n    operationId: \"PageMainFrameId\",\n    summary: \"page.mainFrameId\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageMainFrameIdRequestSchema,\n    response: {\n      200: PageMainFrameIdResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"mainFrameId\",\n    actionSchema: PageMainFrameIdActionSchema,\n    execute: async () => {\n      return PageMainFrameIdResultSchema.parse({ mainFrameId: \"frame_stub\" });\n    },\n  }),\n};\n\nexport default mainFrameIdRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/reload.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageReloadActionSchema,\n  PageNavigationResultSchema,\n  PageReloadRequestSchema,\n  PageReloadResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  buildStubNavigationResult,\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst reloadRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/reload\",\n  schema: {\n    operationId: \"PageReload\",\n    summary: \"page.reload\",\n    headers: Api.SessionHeadersSchema,\n    body: PageReloadRequestSchema,\n    response: {\n      200: PageReloadResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"reload\",\n    actionSchema: PageReloadActionSchema,\n    execute: async ({ params }) => {\n      return PageNavigationResultSchema.parse(\n        buildStubNavigationResult(`https://stub.invalid/${getPageId(params)}`),\n      );\n    },\n  }),\n};\n\nexport default reloadRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/routes.ts",
    "content": "import type { FastifyPluginCallback, RouteOptions } from \"fastify\";\n\nimport addInitScriptRoute from \"./addInitScript.js\";\nimport asProtocolFrameTreeRoute from \"./asProtocolFrameTree.js\";\nimport pageActionDetailsRoute from \"./action/_actionId.js\";\nimport pageActionListRoute from \"./action/index.js\";\nimport clickRoute from \"./click.js\";\nimport closeRoute from \"./close.js\";\nimport dragAndDropRoute from \"./dragAndDrop.js\";\nimport enableCursorOverlayRoute from \"./enableCursorOverlay.js\";\nimport evaluateRoute from \"./evaluate.js\";\nimport framesRoute from \"./frames.js\";\nimport getFullFrameTreeRoute from \"./getFullFrameTree.js\";\nimport getOrdinalRoute from \"./getOrdinal.js\";\nimport goBackRoute from \"./goBack.js\";\nimport goForwardRoute from \"./goForward.js\";\nimport gotoRoute from \"./goto.js\";\nimport hoverRoute from \"./hover.js\";\nimport keyPressRoute from \"./keyPress.js\";\nimport listAllFrameIdsRoute from \"./listAllFrameIds.js\";\nimport mainFrameRoute from \"./mainFrame.js\";\nimport mainFrameIdRoute from \"./mainFrameId.js\";\nimport screenshotRoute from \"./screenshot.js\";\nimport scrollRoute from \"./scroll.js\";\nimport sendCDPRoute from \"./sendCDP.js\";\nimport setExtraHTTPHeadersRoute from \"./setExtraHTTPHeaders.js\";\nimport setViewportSizeRoute from \"./setViewportSize.js\";\nimport snapshotRoute from \"./snapshot.js\";\nimport targetIdRoute from \"./targetId.js\";\nimport titleRoute from \"./title.js\";\nimport typeRoute from \"./type.js\";\nimport urlRoute from \"./url.js\";\nimport waitForLoadStateRoute from \"./waitForLoadState.js\";\nimport waitForMainLoadStateRoute from \"./waitForMainLoadState.js\";\nimport waitForSelectorRoute from \"./waitForSelector.js\";\nimport waitForTimeoutRoute from \"./waitForTimeout.js\";\nimport reloadRoute from \"./reload.js\";\nimport { buildErrorResponse } from \"../../../schemas/v4/page.js\";\nimport { normalizePluginError, withTag } from \"../pluginUtils.js\";\n\nconst rawPageRoutes: RouteOptions[] = [\n  clickRoute,\n  hoverRoute,\n  scrollRoute,\n  dragAndDropRoute,\n  typeRoute,\n  keyPressRoute,\n  gotoRoute,\n  reloadRoute,\n  goBackRoute,\n  goForwardRoute,\n  closeRoute,\n  enableCursorOverlayRoute,\n  addInitScriptRoute,\n  targetIdRoute,\n  mainFrameIdRoute,\n  mainFrameRoute,\n  getFullFrameTreeRoute,\n  asProtocolFrameTreeRoute,\n  listAllFrameIdsRoute,\n  getOrdinalRoute,\n  titleRoute,\n  urlRoute,\n  framesRoute,\n  setExtraHTTPHeadersRoute,\n  waitForMainLoadStateRoute,\n  screenshotRoute,\n  snapshotRoute,\n  setViewportSizeRoute,\n  waitForLoadStateRoute,\n  waitForSelectorRoute,\n  waitForTimeoutRoute,\n  evaluateRoute,\n  sendCDPRoute,\n  pageActionListRoute,\n  pageActionDetailsRoute,\n];\n\nexport const pageRoutes: RouteOptions[] = rawPageRoutes.map((route) =>\n  withTag(route, \"page\"),\n);\n\nexport const pageRoutesPlugin: FastifyPluginCallback = (\n  instance,\n  _opts,\n  done,\n) => {\n  instance.setErrorHandler((error, _request, reply) => {\n    const { errorMessage, stack, statusCode } = normalizePluginError(error);\n\n    return reply.status(statusCode).send(\n      buildErrorResponse({\n        error: errorMessage,\n        statusCode,\n        stack,\n      }),\n    );\n  });\n\n  for (const route of pageRoutes) {\n    instance.route(route);\n  }\n\n  done();\n};\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/screenshot.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageScreenshotActionSchema,\n  PageScreenshotResultSchema,\n  PageScreenshotRequestSchema,\n  PageScreenshotResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst screenshotRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/screenshot\",\n  schema: {\n    operationId: \"PageScreenshot\",\n    summary: \"page.screenshot\",\n    headers: Api.SessionHeadersSchema,\n    body: PageScreenshotRequestSchema,\n    response: {\n      200: PageScreenshotResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"screenshot\",\n    actionSchema: PageScreenshotActionSchema,\n    execute: async ({ params }) => {\n      return PageScreenshotResultSchema.parse({\n        base64: \"c3R1Yg==\",\n        mimeType: params.type === \"jpeg\" ? \"image/jpeg\" : \"image/png\",\n      });\n    },\n  }),\n};\n\nexport default screenshotRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/scroll.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageScrollActionSchema,\n  PageScrollRequestSchema,\n  PageScrollResponseSchema,\n  PageXPathResultSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst scrollRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/scroll\",\n  schema: {\n    operationId: \"PageScroll\",\n    summary: \"page.scroll\",\n    headers: Api.SessionHeadersSchema,\n    body: PageScrollRequestSchema,\n    response: {\n      200: PageScrollResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"scroll\",\n    actionSchema: PageScrollActionSchema,\n    execute: async ({ params }) => {\n      const sel = params.selector;\n      return PageXPathResultSchema.parse({\n        xpath: \"xpath\" in sel ? sel.xpath : \"xpath=//stub-scroll\",\n      });\n    },\n  }),\n};\n\nexport default scrollRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/sendCDP.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageSendCDPActionSchema,\n  PageSendCDPResultSchema,\n  PageSendCDPRequestSchema,\n  PageSendCDPResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst sendCDPRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/sendCDP\",\n  schema: {\n    operationId: \"PageSendCDP\",\n    summary: \"page.sendCDP\",\n    headers: Api.SessionHeadersSchema,\n    body: PageSendCDPRequestSchema,\n    response: {\n      200: PageSendCDPResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"sendCDP\",\n    actionSchema: PageSendCDPActionSchema,\n    execute: async ({ params }) => {\n      return PageSendCDPResultSchema.parse({\n        value: {\n          method: params.method,\n          params: params.params ?? null,\n        },\n      });\n    },\n  }),\n};\n\nexport default sendCDPRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/setExtraHTTPHeaders.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageSetExtraHTTPHeadersActionSchema,\n  PageSetExtraHTTPHeadersResultSchema,\n  PageSetExtraHTTPHeadersRequestSchema,\n  PageSetExtraHTTPHeadersResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst setExtraHTTPHeadersRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/setExtraHTTPHeaders\",\n  schema: {\n    operationId: \"PageSetExtraHTTPHeaders\",\n    summary: \"page.setExtraHTTPHeaders\",\n    headers: Api.SessionHeadersSchema,\n    body: PageSetExtraHTTPHeadersRequestSchema,\n    response: {\n      200: PageSetExtraHTTPHeadersResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"setExtraHTTPHeaders\",\n    actionSchema: PageSetExtraHTTPHeadersActionSchema,\n    execute: async ({ params }) => {\n      return PageSetExtraHTTPHeadersResultSchema.parse({\n        headers: params.headers,\n      });\n    },\n  }),\n};\n\nexport default setExtraHTTPHeadersRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/setViewportSize.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageSetViewportSizeActionSchema,\n  PageSetViewportSizeResultSchema,\n  PageSetViewportSizeRequestSchema,\n  PageSetViewportSizeResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst setViewportSizeRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/setViewportSize\",\n  schema: {\n    operationId: \"PageSetViewportSize\",\n    summary: \"page.setViewportSize\",\n    headers: Api.SessionHeadersSchema,\n    body: PageSetViewportSizeRequestSchema,\n    response: {\n      200: PageSetViewportSizeResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"setViewportSize\",\n    actionSchema: PageSetViewportSizeActionSchema,\n    execute: async ({ params }) => {\n      return PageSetViewportSizeResultSchema.parse({\n        width: params.width,\n        height: params.height,\n        deviceScaleFactor: params.deviceScaleFactor,\n      });\n    },\n  }),\n};\n\nexport default setViewportSizeRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/shared.ts",
    "content": "import { randomUUID } from \"node:crypto\";\n\nimport type { RouteHandlerMethod } from \"fastify\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { z } from \"zod/v4\";\n\nimport {\n  type PageAction,\n  PageTitleActionSchema,\n  type PageActionDetailsQuery,\n  type PageActionMethod,\n  V4ErrorResponseSchema,\n} from \"../../../schemas/v4/page.js\";\n\nexport const pageErrorResponses = {\n  400: V4ErrorResponseSchema,\n  401: V4ErrorResponseSchema,\n  404: V4ErrorResponseSchema,\n  408: V4ErrorResponseSchema,\n  422: V4ErrorResponseSchema,\n  500: V4ErrorResponseSchema,\n};\n\ntype PageRequestBody<TAction extends PageAction> = {\n  sessionId: string;\n  params: TAction[\"params\"];\n};\n\ntype PageRequestQuery<TAction extends PageAction> = {\n  id?: string;\n  sessionId: string;\n} & TAction[\"params\"];\n\ntype PageActionHandlerContext<TAction extends PageAction> = {\n  params: TAction[\"params\"];\n  request: Parameters<RouteHandlerMethod>[0];\n  sessionId: string;\n};\n\n// Selector is a discriminated union of xpath, css, text, or coordinate types.\n// Only xpath is fully resolved today; other types fall back to a stub xpath.\nfunction normalizeXPath(xpath: string): string {\n  return xpath.startsWith(\"xpath=\") || xpath.startsWith(\"/\")\n    ? xpath\n    : `xpath=${xpath}`;\n}\n\nexport function getPageId(params: unknown): string | undefined {\n  if (\n    typeof params === \"object\" &&\n    params !== null &&\n    \"pageId\" in params &&\n    typeof (params as { pageId?: unknown }).pageId === \"string\"\n  ) {\n    return (params as { pageId: string }).pageId;\n  }\n\n  return \"page_stub\";\n}\n\nexport function buildStubPageFrame(pageId = \"page_stub\") {\n  return {\n    frameId: \"frame_stub\",\n    pageId,\n    sessionId: \"cdp-session_stub\",\n    isBrowserRemote: false,\n  };\n}\n\nexport function buildStubNavigationResult(url = \"https://stub.invalid\") {\n  return {\n    url,\n    response: {\n      url,\n      status: 200,\n      statusText: \"OK\",\n      ok: true,\n      headers: {},\n    },\n  };\n}\n\nfunction extractPageParams<TAction extends PageAction>(\n  input: PageRequestBody<TAction> | PageRequestQuery<TAction>,\n): TAction[\"params\"] {\n  if (\"params\" in input) {\n    return input.params;\n  }\n\n  const params = { ...input };\n  delete (params as { id?: string }).id;\n  delete (params as { sessionId?: string }).sessionId;\n  return params as TAction[\"params\"];\n}\n\nexport function createPageActionHandler<TAction extends PageAction>(options: {\n  actionSchema: z.ZodType<TAction>;\n  execute: (\n    ctx: PageActionHandlerContext<TAction>,\n  ) => Promise<TAction[\"result\"]>;\n  method: PageActionMethod;\n}): RouteHandlerMethod {\n  const { actionSchema, method } = options;\n\n  return async (request, reply) => {\n    const input = (request.body ?? request.query) as\n      | PageRequestBody<TAction>\n      | PageRequestQuery<TAction>;\n    const sessionId = input.sessionId ?? \"session_stub\";\n    const params = extractPageParams(input);\n    const result = await options.execute({\n      params,\n      request,\n      sessionId,\n    });\n    const createdAt = new Date().toISOString();\n    const action = actionSchema.parse({\n      id: \"id\" in input ? (input.id ?? randomUUID()) : randomUUID(),\n      method,\n      status: \"completed\",\n      sessionId,\n      pageId: getPageId(params),\n      createdAt,\n      updatedAt: createdAt,\n      completedAt: createdAt,\n      error: null,\n      params,\n      result,\n    });\n\n    return reply.status(StatusCodes.OK).send({\n      success: true,\n      error: null,\n      action,\n    });\n  };\n}\n\nexport const pageActionDetailsHandler: RouteHandlerMethod = async (\n  request,\n  reply,\n) => {\n  const { actionId } = request.params as { actionId: string };\n  const { sessionId } = request.query as PageActionDetailsQuery;\n  const createdAt = new Date().toISOString();\n  const action = PageTitleActionSchema.parse({\n    id: actionId,\n    method: \"title\",\n    status: \"completed\",\n    sessionId,\n    pageId: \"page_stub\",\n    createdAt,\n    updatedAt: createdAt,\n    completedAt: createdAt,\n    error: null,\n    params: {},\n    result: { title: \"Stub title\" },\n  });\n\n  return reply.status(StatusCodes.OK).send({\n    success: true,\n    error: null,\n    action,\n  });\n};\n\nexport const pageActionListHandler: RouteHandlerMethod = async (\n  request,\n  reply,\n) => {\n  const { sessionId } = request.query as PageActionDetailsQuery;\n  const createdAt = new Date().toISOString();\n  return reply.status(StatusCodes.OK).send({\n    success: true,\n    error: null,\n    actions: [\n      PageTitleActionSchema.parse({\n        id: randomUUID(),\n        method: \"title\",\n        status: \"completed\",\n        sessionId,\n        pageId: \"page_stub\",\n        createdAt,\n        updatedAt: createdAt,\n        completedAt: createdAt,\n        error: null,\n        params: {},\n        result: { title: \"Stub title\" },\n      }),\n    ] as PageAction[],\n  });\n};\n\nexport { normalizeXPath };\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/snapshot.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageSnapshotActionSchema,\n  PageSnapshotResultSchema,\n  PageSnapshotRequestSchema,\n  PageSnapshotResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst snapshotRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/snapshot\",\n  schema: {\n    operationId: \"PageSnapshot\",\n    summary: \"page.snapshot\",\n    headers: Api.SessionHeadersSchema,\n    body: PageSnapshotRequestSchema,\n    response: {\n      200: PageSnapshotResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"snapshot\",\n    actionSchema: PageSnapshotActionSchema,\n    execute: async () => {\n      return PageSnapshotResultSchema.parse({\n        formattedTree: \"stub tree\",\n        xpathMap: { stub: \"//html\" },\n        urlMap: { stub: \"https://stub.invalid\" },\n      });\n    },\n  }),\n};\n\nexport default snapshotRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/targetId.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageTargetIdActionSchema,\n  PageTargetIdResultSchema,\n  PageTargetIdRequestSchema,\n  PageTargetIdResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst targetIdRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/targetId\",\n  schema: {\n    operationId: \"PageTargetId\",\n    summary: \"page.targetId\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageTargetIdRequestSchema,\n    response: {\n      200: PageTargetIdResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"targetId\",\n    actionSchema: PageTargetIdActionSchema,\n    execute: async ({ params }) => {\n      return PageTargetIdResultSchema.parse({\n        targetId: getPageId(params),\n      });\n    },\n  }),\n};\n\nexport default targetIdRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/title.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageTitleActionSchema,\n  PageTitleResultSchema,\n  PageTitleRequestSchema,\n  PageTitleResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst titleRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/title\",\n  schema: {\n    operationId: \"PageTitle\",\n    summary: \"page.title\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageTitleRequestSchema,\n    response: {\n      200: PageTitleResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"title\",\n    actionSchema: PageTitleActionSchema,\n    execute: async () => {\n      return PageTitleResultSchema.parse({ title: \"Stub title\" });\n    },\n  }),\n};\n\nexport default titleRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/type.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageTypeActionSchema,\n  PageTypeResultSchema,\n  PageTypeRequestSchema,\n  PageTypeResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst typeRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/type\",\n  schema: {\n    operationId: \"PageType\",\n    summary: \"page.type\",\n    headers: Api.SessionHeadersSchema,\n    body: PageTypeRequestSchema,\n    response: {\n      200: PageTypeResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"type\",\n    actionSchema: PageTypeActionSchema,\n    execute: async ({ params }) => {\n      return PageTypeResultSchema.parse({ text: params.text });\n    },\n  }),\n};\n\nexport default typeRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/url.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageUrlActionSchema,\n  PageUrlResultSchema,\n  PageUrlRequestSchema,\n  PageUrlResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport {\n  createPageActionHandler,\n  getPageId,\n  pageErrorResponses,\n} from \"./shared.js\";\n\nconst urlRoute: RouteOptions = {\n  method: \"GET\",\n  url: \"/page/url\",\n  schema: {\n    operationId: \"PageUrl\",\n    summary: \"page.url\",\n    headers: Api.SessionHeadersSchema,\n    querystring: PageUrlRequestSchema,\n    response: {\n      200: PageUrlResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"url\",\n    actionSchema: PageUrlActionSchema,\n    execute: async ({ params }) => {\n      return PageUrlResultSchema.parse({\n        url: `https://stub.invalid/${getPageId(params)}`,\n      });\n    },\n  }),\n};\n\nexport default urlRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/waitForLoadState.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageWaitForLoadStateActionSchema,\n  PageWaitForLoadStateResultSchema,\n  PageWaitForLoadStateRequestSchema,\n  PageWaitForLoadStateResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst waitForLoadStateRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/waitForLoadState\",\n  schema: {\n    operationId: \"PageWaitForLoadState\",\n    summary: \"page.waitForLoadState\",\n    headers: Api.SessionHeadersSchema,\n    body: PageWaitForLoadStateRequestSchema,\n    response: {\n      200: PageWaitForLoadStateResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"waitForLoadState\",\n    actionSchema: PageWaitForLoadStateActionSchema,\n    execute: async ({ params }) => {\n      return PageWaitForLoadStateResultSchema.parse({\n        state: params.state,\n      });\n    },\n  }),\n};\n\nexport default waitForLoadStateRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/waitForMainLoadState.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageWaitForMainLoadStateActionSchema,\n  PageWaitForLoadStateResultSchema,\n  PageWaitForMainLoadStateRequestSchema,\n  PageWaitForMainLoadStateResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst waitForMainLoadStateRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/waitForMainLoadState\",\n  schema: {\n    operationId: \"PageWaitForMainLoadState\",\n    summary: \"page.waitForMainLoadState\",\n    headers: Api.SessionHeadersSchema,\n    body: PageWaitForMainLoadStateRequestSchema,\n    response: {\n      200: PageWaitForMainLoadStateResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"waitForMainLoadState\",\n    actionSchema: PageWaitForMainLoadStateActionSchema,\n    execute: async ({ params }) => {\n      return PageWaitForLoadStateResultSchema.parse({\n        state: params.state,\n      });\n    },\n  }),\n};\n\nexport default waitForMainLoadStateRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/waitForSelector.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageWaitForSelectorActionSchema,\n  PageWaitForSelectorResultSchema,\n  PageWaitForSelectorRequestSchema,\n  PageWaitForSelectorResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst waitForSelectorRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/waitForSelector\",\n  schema: {\n    operationId: \"PageWaitForSelector\",\n    summary: \"page.waitForSelector\",\n    headers: Api.SessionHeadersSchema,\n    body: PageWaitForSelectorRequestSchema,\n    response: {\n      200: PageWaitForSelectorResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"waitForSelector\",\n    actionSchema: PageWaitForSelectorActionSchema,\n    execute: async ({ params }) => {\n      return PageWaitForSelectorResultSchema.parse({\n        selector: params.selector,\n        matched: true,\n      });\n    },\n  }),\n};\n\nexport default waitForSelectorRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/page/waitForTimeout.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport type { FastifyZodOpenApiSchema } from \"fastify-zod-openapi\";\n\nimport {\n  PageWaitForTimeoutActionSchema,\n  PageWaitForTimeoutResultSchema,\n  PageWaitForTimeoutRequestSchema,\n  PageWaitForTimeoutResponseSchema,\n} from \"../../../schemas/v4/page.js\";\nimport { createPageActionHandler, pageErrorResponses } from \"./shared.js\";\n\nconst waitForTimeoutRoute: RouteOptions = {\n  method: \"POST\",\n  url: \"/page/waitForTimeout\",\n  schema: {\n    operationId: \"PageWaitForTimeout\",\n    summary: \"page.waitForTimeout\",\n    headers: Api.SessionHeadersSchema,\n    body: PageWaitForTimeoutRequestSchema,\n    response: {\n      200: PageWaitForTimeoutResponseSchema,\n      ...pageErrorResponses,\n    },\n  } satisfies FastifyZodOpenApiSchema,\n  handler: createPageActionHandler({\n    method: \"waitForTimeout\",\n    actionSchema: PageWaitForTimeoutActionSchema,\n    execute: async ({ params }) => {\n      return PageWaitForTimeoutResultSchema.parse({ ms: params.ms });\n    },\n  }),\n};\n\nexport default waitForTimeoutRoute;\n"
  },
  {
    "path": "packages/server-v4/src/routes/v4/pluginUtils.ts",
    "content": "import type { RouteOptions } from \"fastify\";\nimport { ResponseSerializationError } from \"fastify-zod-openapi\";\nimport { StatusCodes } from \"http-status-codes\";\n\ntype TaggedRouteSchema = NonNullable<RouteOptions[\"schema\"]> & {\n  tags?: string[];\n};\n\ntype ValidationLikeError = {\n  validation: unknown[];\n};\n\nfunction isValidationLikeError(error: unknown): error is ValidationLikeError {\n  return (\n    typeof error === \"object\" &&\n    error !== null &&\n    \"validation\" in error &&\n    Array.isArray((error as { validation?: unknown }).validation)\n  );\n}\n\nfunction getErrorStatusCode(error: unknown): number {\n  if (\n    typeof error === \"object\" &&\n    error !== null &&\n    \"statusCode\" in error &&\n    typeof (error as { statusCode?: unknown }).statusCode === \"number\"\n  ) {\n    return (error as { statusCode: number }).statusCode;\n  }\n\n  return StatusCodes.INTERNAL_SERVER_ERROR;\n}\n\nexport function withTag(route: RouteOptions, tag: string): RouteOptions {\n  if (!route.schema) {\n    return route;\n  }\n\n  const schema = route.schema as TaggedRouteSchema;\n  const tags = schema.tags ?? [];\n\n  return {\n    ...route,\n    schema: {\n      ...schema,\n      tags: tags.includes(tag) ? tags : [...tags, tag],\n    },\n  };\n}\n\nexport function normalizePluginError(error: unknown): {\n  errorMessage: string;\n  stack: string | null;\n  statusCode: number;\n} {\n  if (isValidationLikeError(error)) {\n    return {\n      errorMessage: \"Request validation failed\",\n      stack: null,\n      statusCode: StatusCodes.BAD_REQUEST,\n    };\n  }\n\n  if (error instanceof ResponseSerializationError) {\n    return {\n      errorMessage: \"Response validation failed\",\n      stack: error.stack ?? null,\n      statusCode: StatusCodes.INTERNAL_SERVER_ERROR,\n    };\n  }\n\n  const normalizedError =\n    error instanceof Error ? error : new Error(String(error));\n\n  return {\n    errorMessage: normalizedError.message,\n    stack: normalizedError.stack ?? null,\n    statusCode: getErrorStatusCode(error),\n  };\n}\n"
  },
  {
    "path": "packages/server-v4/src/schemas/v4/browserSession.ts",
    "content": "import { z } from \"zod/v4\";\nimport { Api } from \"@browserbasehq/stagehand\";\nimport {\n  ActionIdSchema,\n  FrameIdSchema,\n  PageHeadersSchema,\n  PageIdSchema,\n  PageInitScriptSchema,\n  RequestIdSchema,\n  TimestampSchema,\n} from \"./page.js\";\n\nexport const BrowserSessionIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"BrowserSessionId\", example: \"session_01JXAMPLE\" });\n\nexport const BrowserSessionEnvSchema = z\n  .enum([\"LOCAL\", \"BROWSERBASE\"])\n  .meta({ id: \"BrowserSessionEnv\" });\n\nexport const BrowserSessionStatusSchema = z\n  .enum([\"running\", \"ended\"])\n  .meta({ id: \"BrowserSessionStatus\" });\n\nexport const BrowserSessionHeadersSchema = Api.SessionHeadersSchema.meta({\n  id: \"BrowserSessionHeaders\",\n});\n\nexport const BrowserSessionErrorResponseSchema = z\n  .object({\n    success: z.literal(false),\n    message: z.string(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionErrorResponse\" });\n\nconst BrowserSessionCommonSchema = z\n  .object({\n    modelName: z.string().meta({\n      description: \"Model name to use for AI operations\",\n      example: \"openai/gpt-4.1-nano\",\n    }),\n    domSettleTimeoutMs: z.number().optional(),\n    verbose: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),\n    systemPrompt: z.string().optional(),\n    selfHeal: z.boolean().optional(),\n    waitForCaptchaSolves: z.boolean().optional(),\n    experimental: z.boolean().optional(),\n    actTimeoutMs: z.number().optional(),\n  })\n  .strict();\n\nconst BrowserSessionLocalCreateSchema = BrowserSessionCommonSchema.extend({\n  env: z.literal(\"LOCAL\"),\n  cdpUrl: z.string().optional(),\n  localBrowserLaunchOptions: Api.LocalBrowserLaunchOptionsSchema.optional(),\n})\n  .strict()\n  .superRefine((value, ctx) => {\n    if (!value.cdpUrl && !value.localBrowserLaunchOptions) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"localBrowserLaunchOptions\"],\n        message:\n          \"When env is LOCAL, provide either cdpUrl or localBrowserLaunchOptions.\",\n      });\n    }\n  })\n  .meta({ id: \"BrowserSessionLocalCreateRequest\" });\n\nconst BrowserSessionBrowserbaseCreateSchema = BrowserSessionCommonSchema.extend(\n  {\n    env: z.literal(\"BROWSERBASE\"),\n    browserbaseSessionId: z.string().optional(),\n    browserbaseSessionCreateParams:\n      Api.BrowserbaseSessionCreateParamsSchema.optional(),\n  },\n)\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseCreateRequest\" });\n\nexport const BrowserSessionCreateRequestSchema = z\n  .discriminatedUnion(\"env\", [\n    BrowserSessionLocalCreateSchema,\n    BrowserSessionBrowserbaseCreateSchema,\n  ])\n  .meta({ id: \"BrowserSessionCreateRequest\" });\n\nexport const BrowserSessionIdParamsSchema = z\n  .object({\n    id: BrowserSessionIdSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionIdParams\" });\n\nexport const BrowserSessionEndRequestSchema = z\n  .object({})\n  .strict()\n  .optional()\n  .meta({ id: \"BrowserSessionEndRequest\" });\n\nexport const BrowserSessionSchema = z\n  .object({\n    id: BrowserSessionIdSchema,\n    env: BrowserSessionEnvSchema,\n    status: BrowserSessionStatusSchema,\n    modelName: z.string(),\n    cdpUrl: z.string().nullish(),\n    available: z.boolean(),\n    browserbaseSessionId: z.string().optional(),\n    browserbaseSessionCreateParams:\n      Api.BrowserbaseSessionCreateParamsSchema.optional(),\n    localBrowserLaunchOptions: Api.LocalBrowserLaunchOptionsSchema.optional(),\n    domSettleTimeoutMs: z.number().optional(),\n    verbose: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),\n    systemPrompt: z.string().optional(),\n    selfHeal: z.boolean().optional(),\n    waitForCaptchaSolves: z.boolean().optional(),\n    experimental: z.boolean().optional(),\n    actTimeoutMs: z.number().optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSession\" });\n\nexport const BrowserSessionResultSchema = z\n  .object({\n    browserSession: BrowserSessionSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionResult\" });\n\nexport const BrowserSessionResponseSchema = z\n  .object({\n    success: z.literal(true),\n    data: BrowserSessionResultSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionResponse\" });\n\nexport const BrowserSessionActionMethodSchema = z\n  .enum([\n    \"addInitScript\",\n    \"setExtraHTTPHeaders\",\n    \"pages\",\n    \"activePage\",\n    \"awaitActivePage\",\n    \"resolvePageByMainFrameId\",\n    \"getFullFrameTreeByMainFrameId\",\n    \"newPage\",\n    \"cookies\",\n    \"addCookies\",\n    \"clearCookies\",\n    \"connectURL\",\n    \"configuredViewport\",\n    \"browserbaseSessionID\",\n    \"browserbaseSessionURL\",\n    \"browserbaseDebugURL\",\n    \"isBrowserbase\",\n    \"isAdvancedStealth\",\n    \"setViewportSize\",\n    \"close\",\n  ])\n  .meta({ id: \"BrowserSessionActionMethod\" });\n\nexport const BrowserSessionActionStatusSchema = z\n  .enum([\"queued\", \"running\", \"completed\", \"failed\", \"canceled\"])\n  .meta({ id: \"BrowserSessionActionStatus\" });\n\nexport const BrowserSessionPageSchema = z\n  .object({\n    pageId: PageIdSchema,\n    targetId: PageIdSchema,\n    mainFrameId: FrameIdSchema,\n    url: z.string(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionPage\" });\n\nexport const BrowserSessionCookieSchema = z\n  .object({\n    name: z.string(),\n    value: z.string(),\n    domain: z.string(),\n    path: z.string(),\n    expires: z.number(),\n    httpOnly: z.boolean(),\n    secure: z.boolean(),\n    sameSite: z.enum([\"Strict\", \"Lax\", \"None\"]),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionCookie\" });\n\nexport const BrowserSessionCookieParamSchema = z\n  .object({\n    name: z.string(),\n    value: z.string(),\n    url: z.string().optional(),\n    domain: z.string().optional(),\n    path: z.string().optional(),\n    expires: z.number().optional(),\n    httpOnly: z.boolean().optional(),\n    secure: z.boolean().optional(),\n    sameSite: z.enum([\"Strict\", \"Lax\", \"None\"]).optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionCookieParam\" });\n\nexport const BrowserSessionRegexSchema = z\n  .object({\n    source: z.string(),\n    flags: z.string().optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionRegex\" });\n\nexport const BrowserSessionStringPatternSchema = z\n  .union([z.string(), BrowserSessionRegexSchema])\n  .meta({ id: \"BrowserSessionStringPattern\" });\n\nexport const BrowserSessionClearCookiesOptionsSchema = z\n  .object({\n    name: BrowserSessionStringPatternSchema.optional(),\n    domain: BrowserSessionStringPatternSchema.optional(),\n    path: BrowserSessionStringPatternSchema.optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionClearCookiesOptions\" });\n\nexport const BrowserSessionViewportSchema = z\n  .object({\n    width: z.number().positive(),\n    height: z.number().positive(),\n    deviceScaleFactor: z.number().positive().optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionViewport\" });\n\nconst BrowserSessionBodySchema = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: BrowserSessionIdSchema,\n  })\n  .strict();\n\nconst BrowserSessionActionBaseSchema = z\n  .object({\n    id: ActionIdSchema,\n    method: BrowserSessionActionMethodSchema,\n    status: BrowserSessionActionStatusSchema,\n    sessionId: BrowserSessionIdSchema,\n    pageId: PageIdSchema.optional(),\n    createdAt: TimestampSchema,\n    updatedAt: TimestampSchema,\n    completedAt: TimestampSchema.optional(),\n    error: z.string().nullable(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionActionBase\" });\n\nfunction createBrowserSessionRequestSchema<T extends z.ZodTypeAny>(\n  id: string,\n  params: T,\n) {\n  return BrowserSessionBodySchema.extend({ params }).meta({ id });\n}\n\nfunction createBrowserSessionActionSchema<\n  TMethod extends BrowserSessionActionMethod,\n  TParams extends z.ZodTypeAny,\n  TResult extends z.ZodTypeAny,\n>(id: string, method: TMethod, params: TParams, result: TResult) {\n  return BrowserSessionActionBaseSchema.extend({\n    method: z.literal(method),\n    params,\n    result: result.nullable(),\n  }).meta({ id });\n}\n\nfunction createBrowserSessionResponseSchema<T extends z.ZodTypeAny>(\n  id: string,\n  action: T,\n) {\n  return z\n    .object({\n      success: z.literal(true),\n      error: z.null(),\n      action,\n    })\n    .strict()\n    .meta({ id });\n}\n\nexport const BrowserSessionAddInitScriptParamsSchema = z\n  .object({\n    script: PageInitScriptSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionAddInitScriptParams\" });\n\nexport const BrowserSessionSetExtraHTTPHeadersParamsSchema = z\n  .object({\n    headers: PageHeadersSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionSetExtraHTTPHeadersParams\" });\n\nexport const BrowserSessionPagesParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionPagesParams\" });\n\nexport const BrowserSessionActivePageParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionActivePageParams\" });\n\nexport const BrowserSessionAwaitActivePageParamsSchema = z\n  .object({\n    timeoutMs: z.number().int().nonnegative().optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionAwaitActivePageParams\" });\n\nexport const BrowserSessionResolvePageByMainFrameIdParamsSchema = z\n  .object({\n    mainFrameId: FrameIdSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionResolvePageByMainFrameIdParams\" });\n\nexport const BrowserSessionGetFullFrameTreeByMainFrameIdParamsSchema = z\n  .object({\n    mainFrameId: FrameIdSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionGetFullFrameTreeByMainFrameIdParams\" });\n\nexport const BrowserSessionNewPageParamsSchema = z\n  .object({\n    url: z.string().optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionNewPageParams\" });\n\nexport const BrowserSessionCookiesParamsSchema = z\n  .object({\n    urls: z.union([z.string(), z.array(z.string())]).optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionCookiesParams\" });\n\nexport const BrowserSessionAddCookiesParamsSchema = z\n  .object({\n    cookies: z.array(BrowserSessionCookieParamSchema),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionAddCookiesParams\" });\n\nexport const BrowserSessionClearCookiesParamsSchema =\n  BrowserSessionClearCookiesOptionsSchema.meta({\n    id: \"BrowserSessionClearCookiesParams\",\n  });\n\nexport const BrowserSessionConnectURLParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionConnectURLParams\" });\n\nexport const BrowserSessionConfiguredViewportParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionConfiguredViewportParams\" });\n\nexport const BrowserSessionBrowserbaseSessionIDParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseSessionIDParams\" });\n\nexport const BrowserSessionBrowserbaseSessionURLParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseSessionURLParams\" });\n\nexport const BrowserSessionBrowserbaseDebugURLParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseDebugURLParams\" });\n\nexport const BrowserSessionIsBrowserbaseParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionIsBrowserbaseParams\" });\n\nexport const BrowserSessionIsAdvancedStealthParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionIsAdvancedStealthParams\" });\n\nexport const BrowserSessionSetViewportSizeParamsSchema =\n  BrowserSessionViewportSchema.meta({\n    id: \"BrowserSessionSetViewportSizeParams\",\n  });\n\nexport const BrowserSessionCloseParamsSchema = z\n  .object({})\n  .strict()\n  .meta({ id: \"BrowserSessionCloseParams\" });\n\nexport const BrowserSessionAddInitScriptRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionAddInitScriptRequest\",\n    BrowserSessionAddInitScriptParamsSchema,\n  );\n\nexport const BrowserSessionSetExtraHTTPHeadersRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionSetExtraHTTPHeadersRequest\",\n    BrowserSessionSetExtraHTTPHeadersParamsSchema,\n  );\n\nexport const BrowserSessionPagesRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionPagesRequest\",\n    BrowserSessionPagesParamsSchema,\n  );\n\nexport const BrowserSessionActivePageRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionActivePageRequest\",\n    BrowserSessionActivePageParamsSchema,\n  );\n\nexport const BrowserSessionAwaitActivePageRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionAwaitActivePageRequest\",\n    BrowserSessionAwaitActivePageParamsSchema,\n  );\n\nexport const BrowserSessionResolvePageByMainFrameIdRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionResolvePageByMainFrameIdRequest\",\n    BrowserSessionResolvePageByMainFrameIdParamsSchema,\n  );\n\nexport const BrowserSessionGetFullFrameTreeByMainFrameIdRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionGetFullFrameTreeByMainFrameIdRequest\",\n    BrowserSessionGetFullFrameTreeByMainFrameIdParamsSchema,\n  );\n\nexport const BrowserSessionNewPageRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionNewPageRequest\",\n    BrowserSessionNewPageParamsSchema,\n  );\n\nexport const BrowserSessionCookiesRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionCookiesRequest\",\n    BrowserSessionCookiesParamsSchema,\n  );\n\nexport const BrowserSessionAddCookiesRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionAddCookiesRequest\",\n    BrowserSessionAddCookiesParamsSchema,\n  );\n\nexport const BrowserSessionClearCookiesRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionClearCookiesRequest\",\n    BrowserSessionClearCookiesParamsSchema,\n  );\n\nexport const BrowserSessionConnectURLRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionConnectURLRequest\",\n    BrowserSessionConnectURLParamsSchema,\n  );\n\nexport const BrowserSessionConfiguredViewportRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionConfiguredViewportRequest\",\n    BrowserSessionConfiguredViewportParamsSchema,\n  );\n\nexport const BrowserSessionBrowserbaseSessionIDRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionBrowserbaseSessionIDRequest\",\n    BrowserSessionBrowserbaseSessionIDParamsSchema,\n  );\n\nexport const BrowserSessionBrowserbaseSessionURLRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionBrowserbaseSessionURLRequest\",\n    BrowserSessionBrowserbaseSessionURLParamsSchema,\n  );\n\nexport const BrowserSessionBrowserbaseDebugURLRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionBrowserbaseDebugURLRequest\",\n    BrowserSessionBrowserbaseDebugURLParamsSchema,\n  );\n\nexport const BrowserSessionIsBrowserbaseRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionIsBrowserbaseRequest\",\n    BrowserSessionIsBrowserbaseParamsSchema,\n  );\n\nexport const BrowserSessionIsAdvancedStealthRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionIsAdvancedStealthRequest\",\n    BrowserSessionIsAdvancedStealthParamsSchema,\n  );\n\nexport const BrowserSessionSetViewportSizeRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionSetViewportSizeRequest\",\n    BrowserSessionSetViewportSizeParamsSchema,\n  );\n\nexport const BrowserSessionCloseRequestSchema =\n  createBrowserSessionRequestSchema(\n    \"BrowserSessionCloseRequest\",\n    BrowserSessionCloseParamsSchema,\n  );\n\nexport const BrowserSessionAddInitScriptResultSchema = z\n  .object({\n    added: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionAddInitScriptResult\" });\n\nexport const BrowserSessionSetExtraHTTPHeadersResultSchema = z\n  .object({\n    headers: PageHeadersSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionSetExtraHTTPHeadersResult\" });\n\nexport const BrowserSessionPagesResultSchema = z\n  .object({\n    pages: z.array(BrowserSessionPageSchema),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionPagesResult\" });\n\nexport const BrowserSessionOptionalPageResultSchema = z\n  .object({\n    page: BrowserSessionPageSchema.nullable(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionOptionalPageResult\" });\n\nexport const BrowserSessionPageResultSchema = z\n  .object({\n    page: BrowserSessionPageSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionPageResult\" });\n\nexport const BrowserSessionFrameTreeResultSchema = z\n  .object({\n    frameTree: z.unknown(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionFrameTreeResult\" });\n\nexport const BrowserSessionCookiesResultSchema = z\n  .object({\n    cookies: z.array(BrowserSessionCookieSchema),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionCookiesResult\" });\n\nexport const BrowserSessionAddCookiesResultSchema = z\n  .object({\n    added: z.number().int().nonnegative(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionAddCookiesResult\" });\n\nexport const BrowserSessionClearCookiesResultSchema = z\n  .object({\n    cleared: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionClearCookiesResult\" });\n\nexport const BrowserSessionConnectURLResultSchema = z\n  .object({\n    connectURL: z.string(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionConnectURLResult\" });\n\nexport const BrowserSessionConfiguredViewportResultSchema =\n  BrowserSessionViewportSchema.meta({\n    id: \"BrowserSessionConfiguredViewportResult\",\n  });\n\nexport const BrowserSessionBrowserbaseSessionIDResultSchema = z\n  .object({\n    browserbaseSessionID: z.string().nullable(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseSessionIDResult\" });\n\nexport const BrowserSessionBrowserbaseSessionURLResultSchema = z\n  .object({\n    browserbaseSessionURL: z.string().nullable(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseSessionURLResult\" });\n\nexport const BrowserSessionBrowserbaseDebugURLResultSchema = z\n  .object({\n    browserbaseDebugURL: z.string().nullable(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionBrowserbaseDebugURLResult\" });\n\nexport const BrowserSessionIsBrowserbaseResultSchema = z\n  .object({\n    isBrowserbase: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionIsBrowserbaseResult\" });\n\nexport const BrowserSessionIsAdvancedStealthResultSchema = z\n  .object({\n    isAdvancedStealth: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionIsAdvancedStealthResult\" });\n\nexport const BrowserSessionSetViewportSizeResultSchema =\n  BrowserSessionViewportSchema.meta({\n    id: \"BrowserSessionSetViewportSizeResult\",\n  });\n\nexport const BrowserSessionCloseResultSchema = z\n  .object({\n    closed: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionCloseResult\" });\n\nexport const BrowserSessionAddInitScriptActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionAddInitScriptAction\",\n    \"addInitScript\",\n    BrowserSessionAddInitScriptParamsSchema,\n    BrowserSessionAddInitScriptResultSchema,\n  );\n\nexport const BrowserSessionSetExtraHTTPHeadersActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionSetExtraHTTPHeadersAction\",\n    \"setExtraHTTPHeaders\",\n    BrowserSessionSetExtraHTTPHeadersParamsSchema,\n    BrowserSessionSetExtraHTTPHeadersResultSchema,\n  );\n\nexport const BrowserSessionPagesActionSchema = createBrowserSessionActionSchema(\n  \"BrowserSessionPagesAction\",\n  \"pages\",\n  BrowserSessionPagesParamsSchema,\n  BrowserSessionPagesResultSchema,\n);\n\nexport const BrowserSessionActivePageActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionActivePageAction\",\n    \"activePage\",\n    BrowserSessionActivePageParamsSchema,\n    BrowserSessionOptionalPageResultSchema,\n  );\n\nexport const BrowserSessionAwaitActivePageActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionAwaitActivePageAction\",\n    \"awaitActivePage\",\n    BrowserSessionAwaitActivePageParamsSchema,\n    BrowserSessionPageResultSchema,\n  );\n\nexport const BrowserSessionResolvePageByMainFrameIdActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionResolvePageByMainFrameIdAction\",\n    \"resolvePageByMainFrameId\",\n    BrowserSessionResolvePageByMainFrameIdParamsSchema,\n    BrowserSessionOptionalPageResultSchema,\n  );\n\nexport const BrowserSessionGetFullFrameTreeByMainFrameIdActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionGetFullFrameTreeByMainFrameIdAction\",\n    \"getFullFrameTreeByMainFrameId\",\n    BrowserSessionGetFullFrameTreeByMainFrameIdParamsSchema,\n    BrowserSessionFrameTreeResultSchema,\n  );\n\nexport const BrowserSessionNewPageActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionNewPageAction\",\n    \"newPage\",\n    BrowserSessionNewPageParamsSchema,\n    BrowserSessionPageResultSchema,\n  );\n\nexport const BrowserSessionCookiesActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionCookiesAction\",\n    \"cookies\",\n    BrowserSessionCookiesParamsSchema,\n    BrowserSessionCookiesResultSchema,\n  );\n\nexport const BrowserSessionAddCookiesActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionAddCookiesAction\",\n    \"addCookies\",\n    BrowserSessionAddCookiesParamsSchema,\n    BrowserSessionAddCookiesResultSchema,\n  );\n\nexport const BrowserSessionClearCookiesActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionClearCookiesAction\",\n    \"clearCookies\",\n    BrowserSessionClearCookiesParamsSchema,\n    BrowserSessionClearCookiesResultSchema,\n  );\n\nexport const BrowserSessionConnectURLActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionConnectURLAction\",\n    \"connectURL\",\n    BrowserSessionConnectURLParamsSchema,\n    BrowserSessionConnectURLResultSchema,\n  );\n\nexport const BrowserSessionConfiguredViewportActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionConfiguredViewportAction\",\n    \"configuredViewport\",\n    BrowserSessionConfiguredViewportParamsSchema,\n    BrowserSessionConfiguredViewportResultSchema,\n  );\n\nexport const BrowserSessionBrowserbaseSessionIDActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionBrowserbaseSessionIDAction\",\n    \"browserbaseSessionID\",\n    BrowserSessionBrowserbaseSessionIDParamsSchema,\n    BrowserSessionBrowserbaseSessionIDResultSchema,\n  );\n\nexport const BrowserSessionBrowserbaseSessionURLActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionBrowserbaseSessionURLAction\",\n    \"browserbaseSessionURL\",\n    BrowserSessionBrowserbaseSessionURLParamsSchema,\n    BrowserSessionBrowserbaseSessionURLResultSchema,\n  );\n\nexport const BrowserSessionBrowserbaseDebugURLActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionBrowserbaseDebugURLAction\",\n    \"browserbaseDebugURL\",\n    BrowserSessionBrowserbaseDebugURLParamsSchema,\n    BrowserSessionBrowserbaseDebugURLResultSchema,\n  );\n\nexport const BrowserSessionIsBrowserbaseActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionIsBrowserbaseAction\",\n    \"isBrowserbase\",\n    BrowserSessionIsBrowserbaseParamsSchema,\n    BrowserSessionIsBrowserbaseResultSchema,\n  );\n\nexport const BrowserSessionIsAdvancedStealthActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionIsAdvancedStealthAction\",\n    \"isAdvancedStealth\",\n    BrowserSessionIsAdvancedStealthParamsSchema,\n    BrowserSessionIsAdvancedStealthResultSchema,\n  );\n\nexport const BrowserSessionSetViewportSizeActionSchema =\n  createBrowserSessionActionSchema(\n    \"BrowserSessionSetViewportSizeAction\",\n    \"setViewportSize\",\n    BrowserSessionSetViewportSizeParamsSchema,\n    BrowserSessionSetViewportSizeResultSchema,\n  );\n\nexport const BrowserSessionCloseActionSchema = createBrowserSessionActionSchema(\n  \"BrowserSessionCloseAction\",\n  \"close\",\n  BrowserSessionCloseParamsSchema,\n  BrowserSessionCloseResultSchema,\n);\n\nexport const BrowserSessionActionSchema = z\n  .union([\n    BrowserSessionAddInitScriptActionSchema,\n    BrowserSessionSetExtraHTTPHeadersActionSchema,\n    BrowserSessionPagesActionSchema,\n    BrowserSessionActivePageActionSchema,\n    BrowserSessionAwaitActivePageActionSchema,\n    BrowserSessionResolvePageByMainFrameIdActionSchema,\n    BrowserSessionGetFullFrameTreeByMainFrameIdActionSchema,\n    BrowserSessionNewPageActionSchema,\n    BrowserSessionCookiesActionSchema,\n    BrowserSessionAddCookiesActionSchema,\n    BrowserSessionClearCookiesActionSchema,\n    BrowserSessionConnectURLActionSchema,\n    BrowserSessionConfiguredViewportActionSchema,\n    BrowserSessionBrowserbaseSessionIDActionSchema,\n    BrowserSessionBrowserbaseSessionURLActionSchema,\n    BrowserSessionBrowserbaseDebugURLActionSchema,\n    BrowserSessionIsBrowserbaseActionSchema,\n    BrowserSessionIsAdvancedStealthActionSchema,\n    BrowserSessionSetViewportSizeActionSchema,\n    BrowserSessionCloseActionSchema,\n  ])\n  .meta({ id: \"BrowserSessionAction\" });\n\nexport const BrowserSessionV4ErrorResponseSchema = z\n  .object({\n    success: z.literal(false),\n    error: z.string(),\n    statusCode: z.number().int(),\n    stack: z.string().nullable(),\n    action: BrowserSessionActionSchema.optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionV4ErrorResponse\" });\n\nexport const BrowserSessionAddInitScriptResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionAddInitScriptResponse\",\n    BrowserSessionAddInitScriptActionSchema,\n  );\n\nexport const BrowserSessionSetExtraHTTPHeadersResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionSetExtraHTTPHeadersResponse\",\n    BrowserSessionSetExtraHTTPHeadersActionSchema,\n  );\n\nexport const BrowserSessionPagesResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionPagesResponse\",\n    BrowserSessionPagesActionSchema,\n  );\n\nexport const BrowserSessionActivePageResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionActivePageResponse\",\n    BrowserSessionActivePageActionSchema,\n  );\n\nexport const BrowserSessionAwaitActivePageResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionAwaitActivePageResponse\",\n    BrowserSessionAwaitActivePageActionSchema,\n  );\n\nexport const BrowserSessionResolvePageByMainFrameIdResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionResolvePageByMainFrameIdResponse\",\n    BrowserSessionResolvePageByMainFrameIdActionSchema,\n  );\n\nexport const BrowserSessionGetFullFrameTreeByMainFrameIdResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionGetFullFrameTreeByMainFrameIdResponse\",\n    BrowserSessionGetFullFrameTreeByMainFrameIdActionSchema,\n  );\n\nexport const BrowserSessionNewPageResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionNewPageResponse\",\n    BrowserSessionNewPageActionSchema,\n  );\n\nexport const BrowserSessionCookiesResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionCookiesResponse\",\n    BrowserSessionCookiesActionSchema,\n  );\n\nexport const BrowserSessionAddCookiesResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionAddCookiesResponse\",\n    BrowserSessionAddCookiesActionSchema,\n  );\n\nexport const BrowserSessionClearCookiesResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionClearCookiesResponse\",\n    BrowserSessionClearCookiesActionSchema,\n  );\n\nexport const BrowserSessionConnectURLResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionConnectURLResponse\",\n    BrowserSessionConnectURLActionSchema,\n  );\n\nexport const BrowserSessionConfiguredViewportResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionConfiguredViewportResponse\",\n    BrowserSessionConfiguredViewportActionSchema,\n  );\n\nexport const BrowserSessionBrowserbaseSessionIDResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionBrowserbaseSessionIDResponse\",\n    BrowserSessionBrowserbaseSessionIDActionSchema,\n  );\n\nexport const BrowserSessionBrowserbaseSessionURLResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionBrowserbaseSessionURLResponse\",\n    BrowserSessionBrowserbaseSessionURLActionSchema,\n  );\n\nexport const BrowserSessionBrowserbaseDebugURLResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionBrowserbaseDebugURLResponse\",\n    BrowserSessionBrowserbaseDebugURLActionSchema,\n  );\n\nexport const BrowserSessionIsBrowserbaseResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionIsBrowserbaseResponse\",\n    BrowserSessionIsBrowserbaseActionSchema,\n  );\n\nexport const BrowserSessionIsAdvancedStealthResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionIsAdvancedStealthResponse\",\n    BrowserSessionIsAdvancedStealthActionSchema,\n  );\n\nexport const BrowserSessionSetViewportSizeResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionSetViewportSizeResponse\",\n    BrowserSessionSetViewportSizeActionSchema,\n  );\n\nexport const BrowserSessionCloseResponseSchema =\n  createBrowserSessionResponseSchema(\n    \"BrowserSessionCloseResponse\",\n    BrowserSessionCloseActionSchema,\n  );\n\nexport const BrowserSessionActionIdParamsSchema = z\n  .object({\n    actionId: ActionIdSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionActionIdParams\" });\n\nexport const BrowserSessionActionDetailsQuerySchema = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: BrowserSessionIdSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionActionDetailsQuery\" });\n\nexport const BrowserSessionActionListQuerySchema = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: BrowserSessionIdSchema,\n    pageId: PageIdSchema.optional(),\n    method: BrowserSessionActionMethodSchema.optional(),\n    status: BrowserSessionActionStatusSchema.optional(),\n    limit: z.coerce.number().int().positive().max(500).optional(),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionActionListQuery\" });\n\nexport const BrowserSessionActionDetailsResponseSchema = z\n  .object({\n    success: z.literal(true),\n    error: z.null(),\n    action: BrowserSessionActionSchema,\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionActionDetailsResponse\" });\n\nexport const BrowserSessionActionListResponseSchema = z\n  .object({\n    success: z.literal(true),\n    error: z.null(),\n    actions: z.array(BrowserSessionActionSchema),\n  })\n  .strict()\n  .meta({ id: \"BrowserSessionActionListResponse\" });\n\nexport const browserSessionOpenApiComponents = {\n  schemas: {\n    LocalBrowserLaunchOptions: Api.LocalBrowserLaunchOptionsSchema,\n    BrowserbaseSessionCreateParams: Api.BrowserbaseSessionCreateParamsSchema,\n    BrowserSessionHeaders: BrowserSessionHeadersSchema,\n    BrowserSessionId: BrowserSessionIdSchema,\n    BrowserSessionEnv: BrowserSessionEnvSchema,\n    BrowserSessionStatus: BrowserSessionStatusSchema,\n    BrowserSessionCreateRequest: BrowserSessionCreateRequestSchema,\n    BrowserSessionIdParams: BrowserSessionIdParamsSchema,\n    BrowserSessionEndRequest: BrowserSessionEndRequestSchema,\n    BrowserSession: BrowserSessionSchema,\n    BrowserSessionResult: BrowserSessionResultSchema,\n    BrowserSessionResponse: BrowserSessionResponseSchema,\n    BrowserSessionErrorResponse: BrowserSessionErrorResponseSchema,\n    BrowserSessionActionMethod: BrowserSessionActionMethodSchema,\n    BrowserSessionActionStatus: BrowserSessionActionStatusSchema,\n    BrowserSessionPage: BrowserSessionPageSchema,\n    BrowserSessionCookie: BrowserSessionCookieSchema,\n    BrowserSessionCookieParam: BrowserSessionCookieParamSchema,\n    BrowserSessionRegex: BrowserSessionRegexSchema,\n    BrowserSessionStringPattern: BrowserSessionStringPatternSchema,\n    BrowserSessionClearCookiesOptions: BrowserSessionClearCookiesOptionsSchema,\n    BrowserSessionViewport: BrowserSessionViewportSchema,\n    BrowserSessionActionBase: BrowserSessionActionBaseSchema,\n    BrowserSessionAddInitScriptParams: BrowserSessionAddInitScriptParamsSchema,\n    BrowserSessionSetExtraHTTPHeadersParams:\n      BrowserSessionSetExtraHTTPHeadersParamsSchema,\n    BrowserSessionPagesParams: BrowserSessionPagesParamsSchema,\n    BrowserSessionActivePageParams: BrowserSessionActivePageParamsSchema,\n    BrowserSessionAwaitActivePageParams:\n      BrowserSessionAwaitActivePageParamsSchema,\n    BrowserSessionResolvePageByMainFrameIdParams:\n      BrowserSessionResolvePageByMainFrameIdParamsSchema,\n    BrowserSessionGetFullFrameTreeByMainFrameIdParams:\n      BrowserSessionGetFullFrameTreeByMainFrameIdParamsSchema,\n    BrowserSessionNewPageParams: BrowserSessionNewPageParamsSchema,\n    BrowserSessionCookiesParams: BrowserSessionCookiesParamsSchema,\n    BrowserSessionAddCookiesParams: BrowserSessionAddCookiesParamsSchema,\n    BrowserSessionClearCookiesParams: BrowserSessionClearCookiesParamsSchema,\n    BrowserSessionConnectURLParams: BrowserSessionConnectURLParamsSchema,\n    BrowserSessionConfiguredViewportParams:\n      BrowserSessionConfiguredViewportParamsSchema,\n    BrowserSessionBrowserbaseSessionIDParams:\n      BrowserSessionBrowserbaseSessionIDParamsSchema,\n    BrowserSessionBrowserbaseSessionURLParams:\n      BrowserSessionBrowserbaseSessionURLParamsSchema,\n    BrowserSessionBrowserbaseDebugURLParams:\n      BrowserSessionBrowserbaseDebugURLParamsSchema,\n    BrowserSessionIsBrowserbaseParams: BrowserSessionIsBrowserbaseParamsSchema,\n    BrowserSessionIsAdvancedStealthParams:\n      BrowserSessionIsAdvancedStealthParamsSchema,\n    BrowserSessionSetViewportSizeParams:\n      BrowserSessionSetViewportSizeParamsSchema,\n    BrowserSessionCloseParams: BrowserSessionCloseParamsSchema,\n    BrowserSessionAddInitScriptRequest:\n      BrowserSessionAddInitScriptRequestSchema,\n    BrowserSessionSetExtraHTTPHeadersRequest:\n      BrowserSessionSetExtraHTTPHeadersRequestSchema,\n    BrowserSessionPagesRequest: BrowserSessionPagesRequestSchema,\n    BrowserSessionActivePageRequest: BrowserSessionActivePageRequestSchema,\n    BrowserSessionAwaitActivePageRequest:\n      BrowserSessionAwaitActivePageRequestSchema,\n    BrowserSessionResolvePageByMainFrameIdRequest:\n      BrowserSessionResolvePageByMainFrameIdRequestSchema,\n    BrowserSessionGetFullFrameTreeByMainFrameIdRequest:\n      BrowserSessionGetFullFrameTreeByMainFrameIdRequestSchema,\n    BrowserSessionNewPageRequest: BrowserSessionNewPageRequestSchema,\n    BrowserSessionCookiesRequest: BrowserSessionCookiesRequestSchema,\n    BrowserSessionAddCookiesRequest: BrowserSessionAddCookiesRequestSchema,\n    BrowserSessionClearCookiesRequest: BrowserSessionClearCookiesRequestSchema,\n    BrowserSessionConnectURLRequest: BrowserSessionConnectURLRequestSchema,\n    BrowserSessionConfiguredViewportRequest:\n      BrowserSessionConfiguredViewportRequestSchema,\n    BrowserSessionBrowserbaseSessionIDRequest:\n      BrowserSessionBrowserbaseSessionIDRequestSchema,\n    BrowserSessionBrowserbaseSessionURLRequest:\n      BrowserSessionBrowserbaseSessionURLRequestSchema,\n    BrowserSessionBrowserbaseDebugURLRequest:\n      BrowserSessionBrowserbaseDebugURLRequestSchema,\n    BrowserSessionIsBrowserbaseRequest:\n      BrowserSessionIsBrowserbaseRequestSchema,\n    BrowserSessionIsAdvancedStealthRequest:\n      BrowserSessionIsAdvancedStealthRequestSchema,\n    BrowserSessionSetViewportSizeRequest:\n      BrowserSessionSetViewportSizeRequestSchema,\n    BrowserSessionCloseRequest: BrowserSessionCloseRequestSchema,\n    BrowserSessionAddInitScriptAction: BrowserSessionAddInitScriptActionSchema,\n    BrowserSessionSetExtraHTTPHeadersAction:\n      BrowserSessionSetExtraHTTPHeadersActionSchema,\n    BrowserSessionPagesAction: BrowserSessionPagesActionSchema,\n    BrowserSessionActivePageAction: BrowserSessionActivePageActionSchema,\n    BrowserSessionAwaitActivePageAction:\n      BrowserSessionAwaitActivePageActionSchema,\n    BrowserSessionResolvePageByMainFrameIdAction:\n      BrowserSessionResolvePageByMainFrameIdActionSchema,\n    BrowserSessionGetFullFrameTreeByMainFrameIdAction:\n      BrowserSessionGetFullFrameTreeByMainFrameIdActionSchema,\n    BrowserSessionNewPageAction: BrowserSessionNewPageActionSchema,\n    BrowserSessionCookiesAction: BrowserSessionCookiesActionSchema,\n    BrowserSessionAddCookiesAction: BrowserSessionAddCookiesActionSchema,\n    BrowserSessionClearCookiesAction: BrowserSessionClearCookiesActionSchema,\n    BrowserSessionConnectURLAction: BrowserSessionConnectURLActionSchema,\n    BrowserSessionConfiguredViewportAction:\n      BrowserSessionConfiguredViewportActionSchema,\n    BrowserSessionBrowserbaseSessionIDAction:\n      BrowserSessionBrowserbaseSessionIDActionSchema,\n    BrowserSessionBrowserbaseSessionURLAction:\n      BrowserSessionBrowserbaseSessionURLActionSchema,\n    BrowserSessionBrowserbaseDebugURLAction:\n      BrowserSessionBrowserbaseDebugURLActionSchema,\n    BrowserSessionIsBrowserbaseAction: BrowserSessionIsBrowserbaseActionSchema,\n    BrowserSessionIsAdvancedStealthAction:\n      BrowserSessionIsAdvancedStealthActionSchema,\n    BrowserSessionSetViewportSizeAction:\n      BrowserSessionSetViewportSizeActionSchema,\n    BrowserSessionCloseAction: BrowserSessionCloseActionSchema,\n    BrowserSessionAction: BrowserSessionActionSchema,\n    BrowserSessionV4ErrorResponse: BrowserSessionV4ErrorResponseSchema,\n    BrowserSessionAddInitScriptResponse:\n      BrowserSessionAddInitScriptResponseSchema,\n    BrowserSessionSetExtraHTTPHeadersResponse:\n      BrowserSessionSetExtraHTTPHeadersResponseSchema,\n    BrowserSessionPagesResponse: BrowserSessionPagesResponseSchema,\n    BrowserSessionActivePageResponse: BrowserSessionActivePageResponseSchema,\n    BrowserSessionAwaitActivePageResponse:\n      BrowserSessionAwaitActivePageResponseSchema,\n    BrowserSessionResolvePageByMainFrameIdResponse:\n      BrowserSessionResolvePageByMainFrameIdResponseSchema,\n    BrowserSessionGetFullFrameTreeByMainFrameIdResponse:\n      BrowserSessionGetFullFrameTreeByMainFrameIdResponseSchema,\n    BrowserSessionNewPageResponse: BrowserSessionNewPageResponseSchema,\n    BrowserSessionCookiesResponse: BrowserSessionCookiesResponseSchema,\n    BrowserSessionAddCookiesResponse: BrowserSessionAddCookiesResponseSchema,\n    BrowserSessionClearCookiesResponse:\n      BrowserSessionClearCookiesResponseSchema,\n    BrowserSessionConnectURLResponse: BrowserSessionConnectURLResponseSchema,\n    BrowserSessionConfiguredViewportResponse:\n      BrowserSessionConfiguredViewportResponseSchema,\n    BrowserSessionBrowserbaseSessionIDResponse:\n      BrowserSessionBrowserbaseSessionIDResponseSchema,\n    BrowserSessionBrowserbaseSessionURLResponse:\n      BrowserSessionBrowserbaseSessionURLResponseSchema,\n    BrowserSessionBrowserbaseDebugURLResponse:\n      BrowserSessionBrowserbaseDebugURLResponseSchema,\n    BrowserSessionIsBrowserbaseResponse:\n      BrowserSessionIsBrowserbaseResponseSchema,\n    BrowserSessionIsAdvancedStealthResponse:\n      BrowserSessionIsAdvancedStealthResponseSchema,\n    BrowserSessionSetViewportSizeResponse:\n      BrowserSessionSetViewportSizeResponseSchema,\n    BrowserSessionCloseResponse: BrowserSessionCloseResponseSchema,\n    BrowserSessionActionIdParams: BrowserSessionActionIdParamsSchema,\n    BrowserSessionActionDetailsQuery: BrowserSessionActionDetailsQuerySchema,\n    BrowserSessionActionListQuery: BrowserSessionActionListQuerySchema,\n    BrowserSessionActionDetailsResponse:\n      BrowserSessionActionDetailsResponseSchema,\n    BrowserSessionActionListResponse: BrowserSessionActionListResponseSchema,\n  },\n};\n\nexport type BrowserSessionCreateRequest = z.infer<\n  typeof BrowserSessionCreateRequestSchema\n>;\nexport type BrowserSessionIdParams = z.infer<\n  typeof BrowserSessionIdParamsSchema\n>;\nexport type BrowserSession = z.infer<typeof BrowserSessionSchema>;\nexport type BrowserSessionActionMethod = z.infer<\n  typeof BrowserSessionActionMethodSchema\n>;\nexport type BrowserSessionAction = z.infer<typeof BrowserSessionActionSchema>;\nexport type BrowserSessionActionDetailsQuery = z.infer<\n  typeof BrowserSessionActionDetailsQuerySchema\n>;\nexport type BrowserSessionActionListQuery = z.infer<\n  typeof BrowserSessionActionListQuerySchema\n>;\nexport type BrowserSessionPage = z.infer<typeof BrowserSessionPageSchema>;\n\nexport function buildBrowserSessionErrorResponse(input: {\n  error: string;\n  statusCode: number;\n  stack?: string | null;\n  action?: z.input<typeof BrowserSessionActionSchema>;\n}) {\n  return BrowserSessionV4ErrorResponseSchema.parse({\n    success: false,\n    error: input.error,\n    statusCode: input.statusCode,\n    stack: input.stack ?? null,\n    ...(input.action ? { action: input.action } : {}),\n  });\n}\n"
  },
  {
    "path": "packages/server-v4/src/schemas/v4/page.ts",
    "content": "import { z } from \"zod/v4\";\n\nexport const RequestIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"RequestId\", example: \"req_01JXAMPLE\" });\n\nexport const SessionIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"SessionId\", example: \"session_01JXAMPLE\" });\n\nexport const PageIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"PageId\", example: \"target_01JXAMPLE\" });\n\nexport const FrameIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"FrameId\", example: \"frame_01JXAMPLE\" });\n\nexport const ActionIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"ActionId\", example: \"action_01JXAMPLE\" });\n\nexport const CDPSessionIdSchema = z\n  .string()\n  .min(1)\n  .meta({ id: \"CDPSessionId\", example: \"cdp-session_01JXAMPLE\" });\n\nexport const TimestampSchema = z\n  .string()\n  .datetime()\n  .meta({ id: \"Timestamp\", example: \"2026-02-03T12:00:00.000Z\" });\n\nexport const MouseButtonSchema = z\n  .enum([\"left\", \"right\", \"middle\"])\n  .meta({ id: \"MouseButton\" });\n\nexport const LoadStateSchema = z\n  .enum([\"load\", \"domcontentloaded\", \"networkidle\"])\n  .meta({ id: \"LoadState\" });\n\nexport const WaitForSelectorStateSchema = z\n  .enum([\"attached\", \"detached\", \"visible\", \"hidden\"])\n  .meta({ id: \"WaitForSelectorState\" });\n\nexport const ScreenshotTypeSchema = z\n  .enum([\"png\", \"jpeg\"])\n  .meta({ id: \"ScreenshotType\" });\n\nexport const ScreenshotMimeTypeSchema = z\n  .enum([\"image/png\", \"image/jpeg\"])\n  .meta({ id: \"ScreenshotMimeType\" });\n\nexport const ScreenshotScaleSchema = z\n  .enum([\"css\", \"device\"])\n  .meta({ id: \"ScreenshotScale\" });\n\nexport const ScreenshotAnimationsSchema = z\n  .enum([\"allow\", \"disabled\"])\n  .meta({ id: \"ScreenshotAnimations\" });\n\nexport const ScreenshotCaretSchema = z\n  .enum([\"hide\", \"initial\"])\n  .meta({ id: \"ScreenshotCaret\" });\n\nexport const PageActionMethodSchema = z\n  .enum([\n    \"click\",\n    \"hover\",\n    \"scroll\",\n    \"dragAndDrop\",\n    \"type\",\n    \"keyPress\",\n    \"enableCursorOverlay\",\n    \"addInitScript\",\n    \"goto\",\n    \"reload\",\n    \"goBack\",\n    \"goForward\",\n    \"targetId\",\n    \"mainFrameId\",\n    \"mainFrame\",\n    \"getFullFrameTree\",\n    \"asProtocolFrameTree\",\n    \"listAllFrameIds\",\n    \"getOrdinal\",\n    \"title\",\n    \"url\",\n    \"screenshot\",\n    \"snapshot\",\n    \"frames\",\n    \"setViewportSize\",\n    \"setExtraHTTPHeaders\",\n    \"waitForLoadState\",\n    \"waitForMainLoadState\",\n    \"waitForSelector\",\n    \"waitForTimeout\",\n    \"evaluate\",\n    \"sendCDP\",\n    \"close\",\n  ])\n  .meta({ id: \"PageActionMethod\" });\n\nexport const PageActionStatusSchema = z\n  .enum([\"queued\", \"running\", \"completed\", \"failed\", \"canceled\"])\n  .meta({ id: \"PageActionStatus\" });\n\nexport const XPathSelectorSchema = z\n  .object({\n    xpath: z.string().min(1).meta({ example: \"//button[text()='Submit']\" }),\n    idx: z.number().int().nonnegative().optional().meta({ example: 0 }),\n  })\n  .strict()\n  .meta({ id: \"XPathSelector\" });\n\nexport const CssSelectorSchema = z\n  .object({\n    css: z.string().min(1).meta({ example: \".btn-submit\" }),\n    idx: z.number().int().nonnegative().optional().meta({ example: 0 }),\n  })\n  .strict()\n  .meta({ id: \"CssSelector\" });\n\nexport const TextSelectorSchema = z\n  .object({\n    text: z.string().min(1).meta({ example: \"Submit\" }),\n    idx: z.number().int().nonnegative().optional().meta({ example: 0 }),\n  })\n  .strict()\n  .meta({ id: \"TextSelector\" });\n\nexport const CoordinateSelectorSchema = z\n  .object({\n    x: z.number(),\n    y: z.number(),\n  })\n  .strict()\n  .meta({ id: \"CoordinateSelector\" });\n\n// Full union (all 4 types)\nexport const SelectorSchema = z\n  .union([\n    XPathSelectorSchema,\n    CssSelectorSchema,\n    TextSelectorSchema,\n    CoordinateSelectorSchema,\n  ])\n  .meta({ id: \"Selector\" });\n\n// Element-only (no coordinates) — for waitForSelector\nexport const ElementSelectorSchema = z\n  .union([XPathSelectorSchema, CssSelectorSchema, TextSelectorSchema])\n  .meta({ id: \"ElementSelector\" });\n\nexport const PageHeadersSchema = z\n  .object({})\n  .catchall(z.string())\n  .meta({ id: \"PageHeaders\" });\n\nexport const PageInitScriptSchema = z\n  .union([\n    z.string().min(1),\n    z\n      .object({\n        path: z.string().min(1).optional(),\n        content: z.string().min(1).optional(),\n      })\n      .strict()\n      .refine(\n        (value) => value.path !== undefined || value.content !== undefined,\n        {\n          message: \"script must include path or content\",\n        },\n      ),\n  ])\n  .meta({ id: \"PageInitScript\" });\n\nexport const PageClipSchema = z\n  .object({\n    x: z.number(),\n    y: z.number(),\n    width: z.number().int().positive(),\n    height: z.number().int().positive(),\n  })\n  .strict()\n  .meta({ id: \"PageClip\" });\n\nexport const PageErrorSchema = z.string().min(1).meta({ id: \"PageError\" });\n\nexport const ValidationErrorResponseSchema = z\n  .object({\n    success: z.literal(false),\n    error: PageErrorSchema,\n    statusCode: z.number().int(),\n    stack: z.string().nullable(),\n    action: z.lazy(() => PageActionSchema).optional(),\n  })\n  .strict()\n  .meta({ id: \"ValidationErrorResponse\" });\n\nconst PageBodySchema = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: SessionIdSchema,\n  })\n  .strict();\n\nconst PageQuerySchemaBase = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: SessionIdSchema,\n  })\n  .strict();\n\nconst PageWithPageIdSchema = z\n  .object({\n    pageId: PageIdSchema.optional(),\n  })\n  .strict();\n\nconst PageActionBaseSchema = z\n  .object({\n    id: ActionIdSchema,\n    method: PageActionMethodSchema,\n    status: PageActionStatusSchema,\n    sessionId: SessionIdSchema,\n    pageId: PageIdSchema.optional(),\n    createdAt: TimestampSchema,\n    updatedAt: TimestampSchema,\n    completedAt: TimestampSchema.optional(),\n    error: PageErrorSchema.nullable(),\n  })\n  .strict()\n  .meta({ id: \"PageActionBase\" });\n\nfunction createPageRequestSchema<T extends z.ZodTypeAny>(\n  id: string,\n  params: T,\n) {\n  return PageBodySchema.extend({ params }).meta({ id });\n}\n\nfunction createPageActionSchema<\n  TMethod extends PageActionMethod,\n  TParams extends z.ZodTypeAny,\n  TResult extends z.ZodTypeAny,\n>(id: string, method: TMethod, params: TParams, result: TResult) {\n  return PageActionBaseSchema.extend({\n    method: z.literal(method),\n    params,\n    result: result.nullable(),\n  }).meta({ id });\n}\n\nfunction createPageResponseSchema<T extends z.ZodTypeAny>(\n  id: string,\n  action: T,\n) {\n  return z\n    .object({\n      success: z.literal(true),\n      error: z.null(),\n      action,\n    })\n    .strict()\n    .meta({ id });\n}\n\nexport const PageClickParamsSchema = PageWithPageIdSchema.extend({\n  selector: SelectorSchema,\n  button: MouseButtonSchema.optional(),\n  clickCount: z.number().int().min(1).optional(),\n})\n  .strict()\n  .meta({ id: \"PageClickParams\" });\n\nexport const PageHoverParamsSchema = PageWithPageIdSchema.extend({\n  selector: SelectorSchema,\n})\n  .strict()\n  .meta({ id: \"PageHoverParams\" });\n\nexport const PageScrollElementParamsSchema = PageWithPageIdSchema.extend({\n  selector: ElementSelectorSchema,\n  percentage: z.number().min(0).max(100),\n})\n  .strict()\n  .meta({ id: \"PageScrollElementParams\" });\n\nexport const PageScrollCoordinateParamsSchema = PageWithPageIdSchema.extend({\n  selector: CoordinateSelectorSchema,\n  deltaX: z.number().optional(),\n  deltaY: z.number(),\n})\n  .strict()\n  .meta({ id: \"PageScrollCoordinateParams\" });\n\nexport const PageScrollParamsSchema = z\n  .union([PageScrollElementParamsSchema, PageScrollCoordinateParamsSchema])\n  .meta({ id: \"PageScrollParams\" });\n\nexport const PageDragAndDropParamsSchema = PageWithPageIdSchema.extend({\n  from: SelectorSchema,\n  to: SelectorSchema,\n  button: MouseButtonSchema.optional(),\n  steps: z.number().int().positive().optional(),\n  delay: z.number().int().min(0).optional(),\n})\n  .strict()\n  .meta({ id: \"PageDragAndDropParams\" });\n\nexport const PageTypeParamsSchema = PageWithPageIdSchema.extend({\n  text: z.string(),\n  delay: z.number().int().min(0).optional(),\n  withMistakes: z.boolean().optional(),\n})\n  .strict()\n  .meta({ id: \"PageTypeParams\" });\n\nexport const PageKeyPressParamsSchema = PageWithPageIdSchema.extend({\n  key: z.string().min(1),\n  delay: z.number().int().min(0).optional(),\n})\n  .strict()\n  .meta({ id: \"PageKeyPressParams\" });\n\nexport const PageGotoParamsSchema = PageWithPageIdSchema.extend({\n  url: z.string().url(),\n  waitUntil: LoadStateSchema.optional(),\n  timeoutMs: z.number().int().nonnegative().optional(),\n})\n  .strict()\n  .meta({ id: \"PageGotoParams\" });\n\nexport const PageReloadParamsSchema = PageWithPageIdSchema.extend({\n  waitUntil: LoadStateSchema.optional(),\n  timeoutMs: z.number().int().nonnegative().optional(),\n  ignoreCache: z.boolean().optional(),\n})\n  .strict()\n  .meta({ id: \"PageReloadParams\" });\n\nexport const PageGoBackParamsSchema = PageWithPageIdSchema.extend({\n  waitUntil: LoadStateSchema.optional(),\n  timeoutMs: z.number().int().nonnegative().optional(),\n})\n  .strict()\n  .meta({ id: \"PageGoBackParams\" });\n\nexport const PageGoForwardParamsSchema = PageWithPageIdSchema.extend({\n  waitUntil: LoadStateSchema.optional(),\n  timeoutMs: z.number().int().nonnegative().optional(),\n})\n  .strict()\n  .meta({ id: \"PageGoForwardParams\" });\n\nexport const PageEnableCursorOverlayParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageEnableCursorOverlayParams\",\n});\n\nexport const PageAddInitScriptParamsSchema = PageWithPageIdSchema.extend({\n  script: PageInitScriptSchema,\n})\n  .strict()\n  .meta({ id: \"PageAddInitScriptParams\" });\n\nexport const PageTargetIdParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageTargetIdParams\",\n});\n\nexport const PageMainFrameIdParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageMainFrameIdParams\",\n});\n\nexport const PageMainFrameParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageMainFrameParams\",\n});\n\nexport const PageGetFullFrameTreeParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageGetFullFrameTreeParams\",\n});\n\nexport const PageAsProtocolFrameTreeParamsSchema = PageWithPageIdSchema.extend({\n  rootMainFrameId: FrameIdSchema,\n})\n  .strict()\n  .meta({ id: \"PageAsProtocolFrameTreeParams\" });\n\nexport const PageListAllFrameIdsParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageListAllFrameIdsParams\",\n});\n\nexport const PageGetOrdinalParamsSchema = PageWithPageIdSchema.extend({\n  frameId: FrameIdSchema,\n})\n  .strict()\n  .meta({ id: \"PageGetOrdinalParams\" });\n\nexport const PageTitleParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageTitleParams\",\n});\n\nexport const PageUrlParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageUrlParams\",\n});\n\nexport const PageScreenshotParamsSchema = PageWithPageIdSchema.extend({\n  fullPage: z.boolean().optional(),\n  clip: PageClipSchema.optional(),\n  type: ScreenshotTypeSchema.optional(),\n  quality: z.number().int().min(0).max(100).optional(),\n  scale: ScreenshotScaleSchema.optional(),\n  animations: ScreenshotAnimationsSchema.optional(),\n  caret: ScreenshotCaretSchema.optional(),\n  style: z.string().optional(),\n  omitBackground: z.boolean().optional(),\n  timeout: z.number().int().nonnegative().optional(),\n})\n  .strict()\n  .superRefine((value, ctx) => {\n    if (value.quality !== undefined && value.type !== \"jpeg\") {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"quality\"],\n        message: \"quality is only supported when type is 'jpeg'\",\n      });\n    }\n\n    if (value.clip && value.fullPage) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        path: [\"clip\"],\n        message: \"clip cannot be used together with fullPage\",\n      });\n    }\n  })\n  .meta({ id: \"PageScreenshotParams\" });\n\nexport const PageSnapshotParamsSchema = PageWithPageIdSchema.extend({\n  includeIframes: z.boolean().optional(),\n})\n  .strict()\n  .meta({ id: \"PageSnapshotParams\" });\n\nexport const PageFramesParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageFramesParams\",\n});\n\nexport const PageSetViewportSizeParamsSchema = PageWithPageIdSchema.extend({\n  width: z.number().positive(),\n  height: z.number().positive(),\n  deviceScaleFactor: z.number().positive().optional(),\n})\n  .strict()\n  .meta({ id: \"PageSetViewportSizeParams\" });\n\nexport const PageSetExtraHTTPHeadersParamsSchema = PageWithPageIdSchema.extend({\n  headers: PageHeadersSchema,\n})\n  .strict()\n  .meta({ id: \"PageSetExtraHTTPHeadersParams\" });\n\nexport const PageWaitForLoadStateParamsSchema = PageWithPageIdSchema.extend({\n  state: LoadStateSchema,\n  timeoutMs: z.number().int().nonnegative().optional(),\n})\n  .strict()\n  .meta({ id: \"PageWaitForLoadStateParams\" });\n\nexport const PageWaitForMainLoadStateParamsSchema = PageWithPageIdSchema.extend(\n  {\n    state: LoadStateSchema,\n    timeoutMs: z.number().int().nonnegative().optional(),\n  },\n)\n  .strict()\n  .meta({ id: \"PageWaitForMainLoadStateParams\" });\n\nexport const PageWaitForSelectorParamsSchema = PageWithPageIdSchema.extend({\n  selector: ElementSelectorSchema,\n  state: WaitForSelectorStateSchema.optional(),\n  timeout: z.number().int().nonnegative().optional(),\n  pierceShadow: z.boolean().optional(),\n})\n  .strict()\n  .meta({ id: \"PageWaitForSelectorParams\" });\n\nexport const PageWaitForTimeoutParamsSchema = PageWithPageIdSchema.extend({\n  ms: z.number().int().nonnegative(),\n})\n  .strict()\n  .meta({ id: \"PageWaitForTimeoutParams\" });\n\nexport const PageEvaluateParamsSchema = PageWithPageIdSchema.extend({\n  expression: z.string().min(1),\n  arg: z.unknown().optional(),\n})\n  .strict()\n  .meta({ id: \"PageEvaluateParams\" });\n\nexport const PageSendCDPParamsSchema = PageWithPageIdSchema.extend({\n  method: z.string().min(1),\n  params: z.unknown().optional(),\n})\n  .strict()\n  .meta({ id: \"PageSendCDPParams\" });\n\nexport const PageCloseParamsSchema = PageWithPageIdSchema.meta({\n  id: \"PageCloseParams\",\n});\n\nexport const PageClickRequestSchema = createPageRequestSchema(\n  \"PageClickRequest\",\n  PageClickParamsSchema,\n);\n\nexport const PageHoverRequestSchema = createPageRequestSchema(\n  \"PageHoverRequest\",\n  PageHoverParamsSchema,\n);\n\nexport const PageScrollRequestSchema = createPageRequestSchema(\n  \"PageScrollRequest\",\n  PageScrollParamsSchema,\n);\n\nexport const PageDragAndDropRequestSchema = createPageRequestSchema(\n  \"PageDragAndDropRequest\",\n  PageDragAndDropParamsSchema,\n);\n\nexport const PageTypeRequestSchema = createPageRequestSchema(\n  \"PageTypeRequest\",\n  PageTypeParamsSchema,\n);\n\nexport const PageKeyPressRequestSchema = createPageRequestSchema(\n  \"PageKeyPressRequest\",\n  PageKeyPressParamsSchema,\n);\n\nexport const PageGotoRequestSchema = createPageRequestSchema(\n  \"PageGotoRequest\",\n  PageGotoParamsSchema,\n);\n\nexport const PageReloadRequestSchema = createPageRequestSchema(\n  \"PageReloadRequest\",\n  PageReloadParamsSchema,\n);\n\nexport const PageGoBackRequestSchema = createPageRequestSchema(\n  \"PageGoBackRequest\",\n  PageGoBackParamsSchema,\n);\n\nexport const PageGoForwardRequestSchema = createPageRequestSchema(\n  \"PageGoForwardRequest\",\n  PageGoForwardParamsSchema,\n);\n\nexport const PageEnableCursorOverlayRequestSchema = createPageRequestSchema(\n  \"PageEnableCursorOverlayRequest\",\n  PageEnableCursorOverlayParamsSchema,\n);\n\nexport const PageAddInitScriptRequestSchema = createPageRequestSchema(\n  \"PageAddInitScriptRequest\",\n  PageAddInitScriptParamsSchema,\n);\n\nexport const PageTargetIdRequestSchema = PageQuerySchemaBase.extend(\n  PageTargetIdParamsSchema.shape,\n).meta({ id: \"PageTargetIdRequest\" });\n\nexport const PageMainFrameIdRequestSchema = PageQuerySchemaBase.extend(\n  PageMainFrameIdParamsSchema.shape,\n).meta({ id: \"PageMainFrameIdRequest\" });\n\nexport const PageMainFrameRequestSchema = PageQuerySchemaBase.extend(\n  PageMainFrameParamsSchema.shape,\n).meta({ id: \"PageMainFrameRequest\" });\n\nexport const PageGetFullFrameTreeRequestSchema = PageQuerySchemaBase.extend(\n  PageGetFullFrameTreeParamsSchema.shape,\n).meta({ id: \"PageGetFullFrameTreeRequest\" });\n\nexport const PageAsProtocolFrameTreeRequestSchema = PageQuerySchemaBase.extend(\n  PageAsProtocolFrameTreeParamsSchema.shape,\n).meta({ id: \"PageAsProtocolFrameTreeRequest\" });\n\nexport const PageListAllFrameIdsRequestSchema = PageQuerySchemaBase.extend(\n  PageListAllFrameIdsParamsSchema.shape,\n).meta({ id: \"PageListAllFrameIdsRequest\" });\n\nexport const PageGetOrdinalRequestSchema = PageQuerySchemaBase.extend(\n  PageGetOrdinalParamsSchema.shape,\n).meta({ id: \"PageGetOrdinalRequest\" });\n\nexport const PageTitleRequestSchema = PageQuerySchemaBase.extend(\n  PageTitleParamsSchema.shape,\n).meta({ id: \"PageTitleRequest\" });\n\nexport const PageUrlRequestSchema = PageQuerySchemaBase.extend(\n  PageUrlParamsSchema.shape,\n).meta({ id: \"PageUrlRequest\" });\n\nexport const PageScreenshotRequestSchema = createPageRequestSchema(\n  \"PageScreenshotRequest\",\n  PageScreenshotParamsSchema,\n);\n\nexport const PageSnapshotRequestSchema = createPageRequestSchema(\n  \"PageSnapshotRequest\",\n  PageSnapshotParamsSchema,\n);\n\nexport const PageFramesRequestSchema = PageQuerySchemaBase.extend(\n  PageFramesParamsSchema.shape,\n).meta({ id: \"PageFramesRequest\" });\n\nexport const PageSetViewportSizeRequestSchema = createPageRequestSchema(\n  \"PageSetViewportSizeRequest\",\n  PageSetViewportSizeParamsSchema,\n);\n\nexport const PageSetExtraHTTPHeadersRequestSchema = createPageRequestSchema(\n  \"PageSetExtraHTTPHeadersRequest\",\n  PageSetExtraHTTPHeadersParamsSchema,\n);\n\nexport const PageWaitForLoadStateRequestSchema = createPageRequestSchema(\n  \"PageWaitForLoadStateRequest\",\n  PageWaitForLoadStateParamsSchema,\n);\n\nexport const PageWaitForMainLoadStateRequestSchema = createPageRequestSchema(\n  \"PageWaitForMainLoadStateRequest\",\n  PageWaitForMainLoadStateParamsSchema,\n);\n\nexport const PageWaitForSelectorRequestSchema = createPageRequestSchema(\n  \"PageWaitForSelectorRequest\",\n  PageWaitForSelectorParamsSchema,\n);\n\nexport const PageWaitForTimeoutRequestSchema = createPageRequestSchema(\n  \"PageWaitForTimeoutRequest\",\n  PageWaitForTimeoutParamsSchema,\n);\n\nexport const PageEvaluateRequestSchema = createPageRequestSchema(\n  \"PageEvaluateRequest\",\n  PageEvaluateParamsSchema,\n);\n\nexport const PageSendCDPRequestSchema = createPageRequestSchema(\n  \"PageSendCDPRequest\",\n  PageSendCDPParamsSchema,\n);\n\nexport const PageCloseRequestSchema = createPageRequestSchema(\n  \"PageCloseRequest\",\n  PageCloseParamsSchema,\n);\n\nexport const PageXPathResultSchema = z\n  .object({\n    xpath: z.string().optional(),\n  })\n  .strict()\n  .meta({ id: \"PageXPathResult\" });\n\nexport const PageDragAndDropResultSchema = z\n  .object({\n    fromXpath: z.string().optional(),\n    toXpath: z.string().optional(),\n  })\n  .strict()\n  .meta({ id: \"PageDragAndDropResult\" });\n\nexport const PageTypeResultSchema = z\n  .object({\n    text: z.string(),\n  })\n  .strict()\n  .meta({ id: \"PageTypeResult\" });\n\nexport const PageKeyPressResultSchema = z\n  .object({\n    key: z.string(),\n  })\n  .strict()\n  .meta({ id: \"PageKeyPressResult\" });\n\nexport const PageEnableCursorOverlayResultSchema = z\n  .object({\n    enabled: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"PageEnableCursorOverlayResult\" });\n\nexport const PageAddInitScriptResultSchema = z\n  .object({\n    added: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"PageAddInitScriptResult\" });\n\nexport const PageNavigationResultSchema = z\n  .object({\n    url: z.string(),\n    response: z\n      .object({\n        url: z.string(),\n        status: z.number().int(),\n        statusText: z.string(),\n        ok: z.boolean(),\n        headers: PageHeadersSchema,\n      })\n      .strict()\n      .nullable(),\n  })\n  .strict()\n  .meta({ id: \"PageNavigationResult\" });\n\nexport const PageTargetIdResultSchema = z\n  .object({\n    targetId: PageIdSchema,\n  })\n  .strict()\n  .meta({ id: \"PageTargetIdResult\" });\n\nexport const PageMainFrameIdResultSchema = z\n  .object({\n    mainFrameId: FrameIdSchema,\n  })\n  .strict()\n  .meta({ id: \"PageMainFrameIdResult\" });\n\nexport const PageFrameSchema = z\n  .object({\n    frameId: FrameIdSchema,\n    pageId: PageIdSchema,\n    sessionId: CDPSessionIdSchema.nullable(),\n    isBrowserRemote: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"PageFrame\" });\n\nexport const PageMainFrameResultSchema = z\n  .object({\n    frame: PageFrameSchema,\n  })\n  .strict()\n  .meta({ id: \"PageMainFrameResult\" });\n\nexport const PageFrameTreeResultSchema = z\n  .object({\n    frameTree: z.unknown(),\n  })\n  .strict()\n  .meta({ id: \"PageFrameTreeResult\" });\n\nexport const PageListAllFrameIdsResultSchema = z\n  .object({\n    frameIds: z.array(FrameIdSchema),\n  })\n  .strict()\n  .meta({ id: \"PageListAllFrameIdsResult\" });\n\nexport const PageGetOrdinalResultSchema = z\n  .object({\n    frameId: FrameIdSchema,\n    ordinal: z.number().int().nonnegative(),\n  })\n  .strict()\n  .meta({ id: \"PageGetOrdinalResult\" });\n\nexport const PageTitleResultSchema = z\n  .object({\n    title: z.string(),\n  })\n  .strict()\n  .meta({ id: \"PageTitleResult\" });\n\nexport const PageUrlResultSchema = z\n  .object({\n    url: z.string(),\n  })\n  .strict()\n  .meta({ id: \"PageUrlResult\" });\n\nexport const PageScreenshotResultSchema = z\n  .object({\n    base64: z.string(),\n    mimeType: ScreenshotMimeTypeSchema,\n  })\n  .strict()\n  .meta({ id: \"PageScreenshotResult\" });\n\nexport const PageSnapshotResultSchema = z\n  .object({\n    formattedTree: z.string(),\n    xpathMap: z.object({}).catchall(z.string()),\n    urlMap: z.object({}).catchall(z.string()),\n  })\n  .strict()\n  .meta({ id: \"PageSnapshotResult\" });\n\nexport const PageSetViewportSizeResultSchema = z\n  .object({\n    width: z.number().positive(),\n    height: z.number().positive(),\n    deviceScaleFactor: z.number().positive().optional(),\n  })\n  .strict()\n  .meta({ id: \"PageSetViewportSizeResult\" });\n\nexport const PageFramesResultSchema = z\n  .object({\n    frames: z.array(PageFrameSchema),\n  })\n  .strict()\n  .meta({ id: \"PageFramesResult\" });\n\nexport const PageSetExtraHTTPHeadersResultSchema = z\n  .object({\n    headers: PageHeadersSchema,\n  })\n  .strict()\n  .meta({ id: \"PageSetExtraHTTPHeadersResult\" });\n\nexport const PageWaitForLoadStateResultSchema = z\n  .object({\n    state: LoadStateSchema,\n  })\n  .strict()\n  .meta({ id: \"PageWaitForLoadStateResult\" });\n\nexport const PageWaitForSelectorResultSchema = z\n  .object({\n    selector: ElementSelectorSchema,\n    matched: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"PageWaitForSelectorResult\" });\n\nexport const PageWaitForTimeoutResultSchema = z\n  .object({\n    ms: z.number().int().nonnegative(),\n  })\n  .strict()\n  .meta({ id: \"PageWaitForTimeoutResult\" });\n\nexport const PageEvaluateResultSchema = z\n  .object({\n    value: z.unknown(),\n  })\n  .strict()\n  .meta({ id: \"PageEvaluateResult\" });\n\nexport const PageSendCDPResultSchema = z\n  .object({\n    value: z.unknown(),\n  })\n  .strict()\n  .meta({ id: \"PageSendCDPResult\" });\n\nexport const PageCloseResultSchema = z\n  .object({\n    closed: z.boolean(),\n  })\n  .strict()\n  .meta({ id: \"PageCloseResult\" });\n\nexport const PageClickActionSchema = createPageActionSchema(\n  \"PageClickAction\",\n  \"click\",\n  PageClickParamsSchema,\n  PageXPathResultSchema,\n);\n\nexport const PageHoverActionSchema = createPageActionSchema(\n  \"PageHoverAction\",\n  \"hover\",\n  PageHoverParamsSchema,\n  PageXPathResultSchema,\n);\n\nexport const PageScrollActionSchema = createPageActionSchema(\n  \"PageScrollAction\",\n  \"scroll\",\n  PageScrollParamsSchema,\n  PageXPathResultSchema,\n);\n\nexport const PageDragAndDropActionSchema = createPageActionSchema(\n  \"PageDragAndDropAction\",\n  \"dragAndDrop\",\n  PageDragAndDropParamsSchema,\n  PageDragAndDropResultSchema,\n);\n\nexport const PageTypeActionSchema = createPageActionSchema(\n  \"PageTypeAction\",\n  \"type\",\n  PageTypeParamsSchema,\n  PageTypeResultSchema,\n);\n\nexport const PageKeyPressActionSchema = createPageActionSchema(\n  \"PageKeyPressAction\",\n  \"keyPress\",\n  PageKeyPressParamsSchema,\n  PageKeyPressResultSchema,\n);\n\nexport const PageEnableCursorOverlayActionSchema = createPageActionSchema(\n  \"PageEnableCursorOverlayAction\",\n  \"enableCursorOverlay\",\n  PageEnableCursorOverlayParamsSchema,\n  PageEnableCursorOverlayResultSchema,\n);\n\nexport const PageAddInitScriptActionSchema = createPageActionSchema(\n  \"PageAddInitScriptAction\",\n  \"addInitScript\",\n  PageAddInitScriptParamsSchema,\n  PageAddInitScriptResultSchema,\n);\n\nexport const PageGotoActionSchema = createPageActionSchema(\n  \"PageGotoAction\",\n  \"goto\",\n  PageGotoParamsSchema,\n  PageNavigationResultSchema,\n);\n\nexport const PageReloadActionSchema = createPageActionSchema(\n  \"PageReloadAction\",\n  \"reload\",\n  PageReloadParamsSchema,\n  PageNavigationResultSchema,\n);\n\nexport const PageGoBackActionSchema = createPageActionSchema(\n  \"PageGoBackAction\",\n  \"goBack\",\n  PageGoBackParamsSchema,\n  PageNavigationResultSchema,\n);\n\nexport const PageGoForwardActionSchema = createPageActionSchema(\n  \"PageGoForwardAction\",\n  \"goForward\",\n  PageGoForwardParamsSchema,\n  PageNavigationResultSchema,\n);\n\nexport const PageTargetIdActionSchema = createPageActionSchema(\n  \"PageTargetIdAction\",\n  \"targetId\",\n  PageTargetIdParamsSchema,\n  PageTargetIdResultSchema,\n);\n\nexport const PageMainFrameIdActionSchema = createPageActionSchema(\n  \"PageMainFrameIdAction\",\n  \"mainFrameId\",\n  PageMainFrameIdParamsSchema,\n  PageMainFrameIdResultSchema,\n);\n\nexport const PageMainFrameActionSchema = createPageActionSchema(\n  \"PageMainFrameAction\",\n  \"mainFrame\",\n  PageMainFrameParamsSchema,\n  PageMainFrameResultSchema,\n);\n\nexport const PageGetFullFrameTreeActionSchema = createPageActionSchema(\n  \"PageGetFullFrameTreeAction\",\n  \"getFullFrameTree\",\n  PageGetFullFrameTreeParamsSchema,\n  PageFrameTreeResultSchema,\n);\n\nexport const PageAsProtocolFrameTreeActionSchema = createPageActionSchema(\n  \"PageAsProtocolFrameTreeAction\",\n  \"asProtocolFrameTree\",\n  PageAsProtocolFrameTreeParamsSchema,\n  PageFrameTreeResultSchema,\n);\n\nexport const PageListAllFrameIdsActionSchema = createPageActionSchema(\n  \"PageListAllFrameIdsAction\",\n  \"listAllFrameIds\",\n  PageListAllFrameIdsParamsSchema,\n  PageListAllFrameIdsResultSchema,\n);\n\nexport const PageGetOrdinalActionSchema = createPageActionSchema(\n  \"PageGetOrdinalAction\",\n  \"getOrdinal\",\n  PageGetOrdinalParamsSchema,\n  PageGetOrdinalResultSchema,\n);\n\nexport const PageTitleActionSchema = createPageActionSchema(\n  \"PageTitleAction\",\n  \"title\",\n  PageTitleParamsSchema,\n  PageTitleResultSchema,\n);\n\nexport const PageUrlActionSchema = createPageActionSchema(\n  \"PageUrlAction\",\n  \"url\",\n  PageUrlParamsSchema,\n  PageUrlResultSchema,\n);\n\nexport const PageScreenshotActionSchema = createPageActionSchema(\n  \"PageScreenshotAction\",\n  \"screenshot\",\n  PageScreenshotParamsSchema,\n  PageScreenshotResultSchema,\n);\n\nexport const PageSnapshotActionSchema = createPageActionSchema(\n  \"PageSnapshotAction\",\n  \"snapshot\",\n  PageSnapshotParamsSchema,\n  PageSnapshotResultSchema,\n);\n\nexport const PageFramesActionSchema = createPageActionSchema(\n  \"PageFramesAction\",\n  \"frames\",\n  PageFramesParamsSchema,\n  PageFramesResultSchema,\n);\n\nexport const PageSetViewportSizeActionSchema = createPageActionSchema(\n  \"PageSetViewportSizeAction\",\n  \"setViewportSize\",\n  PageSetViewportSizeParamsSchema,\n  PageSetViewportSizeResultSchema,\n);\n\nexport const PageSetExtraHTTPHeadersActionSchema = createPageActionSchema(\n  \"PageSetExtraHTTPHeadersAction\",\n  \"setExtraHTTPHeaders\",\n  PageSetExtraHTTPHeadersParamsSchema,\n  PageSetExtraHTTPHeadersResultSchema,\n);\n\nexport const PageWaitForLoadStateActionSchema = createPageActionSchema(\n  \"PageWaitForLoadStateAction\",\n  \"waitForLoadState\",\n  PageWaitForLoadStateParamsSchema,\n  PageWaitForLoadStateResultSchema,\n);\n\nexport const PageWaitForMainLoadStateActionSchema = createPageActionSchema(\n  \"PageWaitForMainLoadStateAction\",\n  \"waitForMainLoadState\",\n  PageWaitForMainLoadStateParamsSchema,\n  PageWaitForLoadStateResultSchema,\n);\n\nexport const PageWaitForSelectorActionSchema = createPageActionSchema(\n  \"PageWaitForSelectorAction\",\n  \"waitForSelector\",\n  PageWaitForSelectorParamsSchema,\n  PageWaitForSelectorResultSchema,\n);\n\nexport const PageWaitForTimeoutActionSchema = createPageActionSchema(\n  \"PageWaitForTimeoutAction\",\n  \"waitForTimeout\",\n  PageWaitForTimeoutParamsSchema,\n  PageWaitForTimeoutResultSchema,\n);\n\nexport const PageEvaluateActionSchema = createPageActionSchema(\n  \"PageEvaluateAction\",\n  \"evaluate\",\n  PageEvaluateParamsSchema,\n  PageEvaluateResultSchema,\n);\n\nexport const PageSendCDPActionSchema = createPageActionSchema(\n  \"PageSendCDPAction\",\n  \"sendCDP\",\n  PageSendCDPParamsSchema,\n  PageSendCDPResultSchema,\n);\n\nexport const PageCloseActionSchema = createPageActionSchema(\n  \"PageCloseAction\",\n  \"close\",\n  PageCloseParamsSchema,\n  PageCloseResultSchema,\n);\n\nexport const PageActionSchema = z\n  .union([\n    PageClickActionSchema,\n    PageHoverActionSchema,\n    PageScrollActionSchema,\n    PageDragAndDropActionSchema,\n    PageTypeActionSchema,\n    PageKeyPressActionSchema,\n    PageEnableCursorOverlayActionSchema,\n    PageAddInitScriptActionSchema,\n    PageGotoActionSchema,\n    PageReloadActionSchema,\n    PageGoBackActionSchema,\n    PageGoForwardActionSchema,\n    PageTargetIdActionSchema,\n    PageMainFrameIdActionSchema,\n    PageMainFrameActionSchema,\n    PageGetFullFrameTreeActionSchema,\n    PageAsProtocolFrameTreeActionSchema,\n    PageListAllFrameIdsActionSchema,\n    PageGetOrdinalActionSchema,\n    PageTitleActionSchema,\n    PageUrlActionSchema,\n    PageScreenshotActionSchema,\n    PageSnapshotActionSchema,\n    PageFramesActionSchema,\n    PageSetViewportSizeActionSchema,\n    PageSetExtraHTTPHeadersActionSchema,\n    PageWaitForLoadStateActionSchema,\n    PageWaitForMainLoadStateActionSchema,\n    PageWaitForSelectorActionSchema,\n    PageWaitForTimeoutActionSchema,\n    PageEvaluateActionSchema,\n    PageSendCDPActionSchema,\n    PageCloseActionSchema,\n  ])\n  .meta({ id: \"PageAction\" });\n\nexport const V4ErrorResponseSchema = z\n  .object({\n    success: z.literal(false),\n    error: PageErrorSchema,\n    statusCode: z.number().int(),\n    stack: z.string().nullable(),\n    action: PageActionSchema.optional(),\n  })\n  .strict()\n  .meta({ id: \"V4ErrorResponse\" });\n\nexport const PageClickResponseSchema = createPageResponseSchema(\n  \"PageClickResponse\",\n  PageClickActionSchema,\n);\n\nexport const PageHoverResponseSchema = createPageResponseSchema(\n  \"PageHoverResponse\",\n  PageHoverActionSchema,\n);\n\nexport const PageScrollResponseSchema = createPageResponseSchema(\n  \"PageScrollResponse\",\n  PageScrollActionSchema,\n);\n\nexport const PageDragAndDropResponseSchema = createPageResponseSchema(\n  \"PageDragAndDropResponse\",\n  PageDragAndDropActionSchema,\n);\n\nexport const PageTypeResponseSchema = createPageResponseSchema(\n  \"PageTypeResponse\",\n  PageTypeActionSchema,\n);\n\nexport const PageKeyPressResponseSchema = createPageResponseSchema(\n  \"PageKeyPressResponse\",\n  PageKeyPressActionSchema,\n);\n\nexport const PageEnableCursorOverlayResponseSchema = createPageResponseSchema(\n  \"PageEnableCursorOverlayResponse\",\n  PageEnableCursorOverlayActionSchema,\n);\n\nexport const PageAddInitScriptResponseSchema = createPageResponseSchema(\n  \"PageAddInitScriptResponse\",\n  PageAddInitScriptActionSchema,\n);\n\nexport const PageGotoResponseSchema = createPageResponseSchema(\n  \"PageGotoResponse\",\n  PageGotoActionSchema,\n);\n\nexport const PageReloadResponseSchema = createPageResponseSchema(\n  \"PageReloadResponse\",\n  PageReloadActionSchema,\n);\n\nexport const PageGoBackResponseSchema = createPageResponseSchema(\n  \"PageGoBackResponse\",\n  PageGoBackActionSchema,\n);\n\nexport const PageGoForwardResponseSchema = createPageResponseSchema(\n  \"PageGoForwardResponse\",\n  PageGoForwardActionSchema,\n);\n\nexport const PageTargetIdResponseSchema = createPageResponseSchema(\n  \"PageTargetIdResponse\",\n  PageTargetIdActionSchema,\n);\n\nexport const PageMainFrameIdResponseSchema = createPageResponseSchema(\n  \"PageMainFrameIdResponse\",\n  PageMainFrameIdActionSchema,\n);\n\nexport const PageMainFrameResponseSchema = createPageResponseSchema(\n  \"PageMainFrameResponse\",\n  PageMainFrameActionSchema,\n);\n\nexport const PageGetFullFrameTreeResponseSchema = createPageResponseSchema(\n  \"PageGetFullFrameTreeResponse\",\n  PageGetFullFrameTreeActionSchema,\n);\n\nexport const PageAsProtocolFrameTreeResponseSchema = createPageResponseSchema(\n  \"PageAsProtocolFrameTreeResponse\",\n  PageAsProtocolFrameTreeActionSchema,\n);\n\nexport const PageListAllFrameIdsResponseSchema = createPageResponseSchema(\n  \"PageListAllFrameIdsResponse\",\n  PageListAllFrameIdsActionSchema,\n);\n\nexport const PageGetOrdinalResponseSchema = createPageResponseSchema(\n  \"PageGetOrdinalResponse\",\n  PageGetOrdinalActionSchema,\n);\n\nexport const PageTitleResponseSchema = createPageResponseSchema(\n  \"PageTitleResponse\",\n  PageTitleActionSchema,\n);\n\nexport const PageUrlResponseSchema = createPageResponseSchema(\n  \"PageUrlResponse\",\n  PageUrlActionSchema,\n);\n\nexport const PageScreenshotResponseSchema = createPageResponseSchema(\n  \"PageScreenshotResponse\",\n  PageScreenshotActionSchema,\n);\n\nexport const PageSnapshotResponseSchema = createPageResponseSchema(\n  \"PageSnapshotResponse\",\n  PageSnapshotActionSchema,\n);\n\nexport const PageFramesResponseSchema = createPageResponseSchema(\n  \"PageFramesResponse\",\n  PageFramesActionSchema,\n);\n\nexport const PageSetViewportSizeResponseSchema = createPageResponseSchema(\n  \"PageSetViewportSizeResponse\",\n  PageSetViewportSizeActionSchema,\n);\n\nexport const PageSetExtraHTTPHeadersResponseSchema = createPageResponseSchema(\n  \"PageSetExtraHTTPHeadersResponse\",\n  PageSetExtraHTTPHeadersActionSchema,\n);\n\nexport const PageWaitForLoadStateResponseSchema = createPageResponseSchema(\n  \"PageWaitForLoadStateResponse\",\n  PageWaitForLoadStateActionSchema,\n);\n\nexport const PageWaitForMainLoadStateResponseSchema = createPageResponseSchema(\n  \"PageWaitForMainLoadStateResponse\",\n  PageWaitForMainLoadStateActionSchema,\n);\n\nexport const PageWaitForSelectorResponseSchema = createPageResponseSchema(\n  \"PageWaitForSelectorResponse\",\n  PageWaitForSelectorActionSchema,\n);\n\nexport const PageWaitForTimeoutResponseSchema = createPageResponseSchema(\n  \"PageWaitForTimeoutResponse\",\n  PageWaitForTimeoutActionSchema,\n);\n\nexport const PageEvaluateResponseSchema = createPageResponseSchema(\n  \"PageEvaluateResponse\",\n  PageEvaluateActionSchema,\n);\n\nexport const PageSendCDPResponseSchema = createPageResponseSchema(\n  \"PageSendCDPResponse\",\n  PageSendCDPActionSchema,\n);\n\nexport const PageCloseResponseSchema = createPageResponseSchema(\n  \"PageCloseResponse\",\n  PageCloseActionSchema,\n);\n\nexport const PageActionIdParamsSchema = z\n  .object({\n    actionId: ActionIdSchema,\n  })\n  .strict()\n  .meta({ id: \"PageActionIdParams\" });\n\nexport const PageActionDetailsQuerySchema = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: SessionIdSchema,\n  })\n  .strict()\n  .meta({ id: \"PageActionDetailsQuery\" });\n\nexport const PageActionListQuerySchema = z\n  .object({\n    id: RequestIdSchema.optional(),\n    sessionId: SessionIdSchema,\n    pageId: PageIdSchema.optional(),\n    method: PageActionMethodSchema.optional(),\n    status: PageActionStatusSchema.optional(),\n    limit: z.coerce.number().int().positive().max(500).optional(),\n  })\n  .strict()\n  .meta({ id: \"PageActionListQuery\" });\n\nexport const PageActionDetailsResponseSchema = z\n  .object({\n    success: z.literal(true),\n    error: z.null(),\n    action: PageActionSchema,\n  })\n  .strict()\n  .meta({ id: \"PageActionDetailsResponse\" });\n\nexport const PageActionListResponseSchema = z\n  .object({\n    success: z.literal(true),\n    error: z.null(),\n    actions: z.array(PageActionSchema),\n  })\n  .strict()\n  .meta({ id: \"PageActionListResponse\" });\n\nexport const pageOpenApiComponents = {\n  schemas: {\n    RequestId: RequestIdSchema,\n    SessionId: SessionIdSchema,\n    PageId: PageIdSchema,\n    FrameId: FrameIdSchema,\n    ActionId: ActionIdSchema,\n    CDPSessionId: CDPSessionIdSchema,\n    Timestamp: TimestampSchema,\n    MouseButton: MouseButtonSchema,\n    LoadState: LoadStateSchema,\n    WaitForSelectorState: WaitForSelectorStateSchema,\n    ScreenshotType: ScreenshotTypeSchema,\n    ScreenshotMimeType: ScreenshotMimeTypeSchema,\n    ScreenshotScale: ScreenshotScaleSchema,\n    ScreenshotAnimations: ScreenshotAnimationsSchema,\n    ScreenshotCaret: ScreenshotCaretSchema,\n    PageActionMethod: PageActionMethodSchema,\n    PageActionStatus: PageActionStatusSchema,\n    XPathSelector: XPathSelectorSchema,\n    CssSelector: CssSelectorSchema,\n    TextSelector: TextSelectorSchema,\n    CoordinateSelector: CoordinateSelectorSchema,\n    Selector: SelectorSchema,\n    ElementSelector: ElementSelectorSchema,\n    PageHeaders: PageHeadersSchema,\n    PageInitScript: PageInitScriptSchema,\n    PageClip: PageClipSchema,\n    PageError: PageErrorSchema,\n    ValidationErrorResponse: ValidationErrorResponseSchema,\n    V4ErrorResponse: V4ErrorResponseSchema,\n    PageActionBase: PageActionBaseSchema,\n    PageClickParams: PageClickParamsSchema,\n    PageHoverParams: PageHoverParamsSchema,\n    PageScrollElementParams: PageScrollElementParamsSchema,\n    PageScrollCoordinateParams: PageScrollCoordinateParamsSchema,\n    PageScrollParams: PageScrollParamsSchema,\n    PageDragAndDropParams: PageDragAndDropParamsSchema,\n    PageTypeParams: PageTypeParamsSchema,\n    PageKeyPressParams: PageKeyPressParamsSchema,\n    PageEnableCursorOverlayParams: PageEnableCursorOverlayParamsSchema,\n    PageAddInitScriptParams: PageAddInitScriptParamsSchema,\n    PageGotoParams: PageGotoParamsSchema,\n    PageReloadParams: PageReloadParamsSchema,\n    PageGoBackParams: PageGoBackParamsSchema,\n    PageGoForwardParams: PageGoForwardParamsSchema,\n    PageTargetIdParams: PageTargetIdParamsSchema,\n    PageMainFrameIdParams: PageMainFrameIdParamsSchema,\n    PageMainFrameParams: PageMainFrameParamsSchema,\n    PageGetFullFrameTreeParams: PageGetFullFrameTreeParamsSchema,\n    PageAsProtocolFrameTreeParams: PageAsProtocolFrameTreeParamsSchema,\n    PageListAllFrameIdsParams: PageListAllFrameIdsParamsSchema,\n    PageGetOrdinalParams: PageGetOrdinalParamsSchema,\n    PageTitleParams: PageTitleParamsSchema,\n    PageUrlParams: PageUrlParamsSchema,\n    PageScreenshotParams: PageScreenshotParamsSchema,\n    PageSnapshotParams: PageSnapshotParamsSchema,\n    PageFramesParams: PageFramesParamsSchema,\n    PageSetViewportSizeParams: PageSetViewportSizeParamsSchema,\n    PageSetExtraHTTPHeadersParams: PageSetExtraHTTPHeadersParamsSchema,\n    PageWaitForLoadStateParams: PageWaitForLoadStateParamsSchema,\n    PageWaitForMainLoadStateParams: PageWaitForMainLoadStateParamsSchema,\n    PageWaitForSelectorParams: PageWaitForSelectorParamsSchema,\n    PageWaitForTimeoutParams: PageWaitForTimeoutParamsSchema,\n    PageEvaluateParams: PageEvaluateParamsSchema,\n    PageSendCDPParams: PageSendCDPParamsSchema,\n    PageCloseParams: PageCloseParamsSchema,\n    PageClickRequest: PageClickRequestSchema,\n    PageHoverRequest: PageHoverRequestSchema,\n    PageScrollRequest: PageScrollRequestSchema,\n    PageDragAndDropRequest: PageDragAndDropRequestSchema,\n    PageTypeRequest: PageTypeRequestSchema,\n    PageKeyPressRequest: PageKeyPressRequestSchema,\n    PageEnableCursorOverlayRequest: PageEnableCursorOverlayRequestSchema,\n    PageAddInitScriptRequest: PageAddInitScriptRequestSchema,\n    PageGotoRequest: PageGotoRequestSchema,\n    PageReloadRequest: PageReloadRequestSchema,\n    PageGoBackRequest: PageGoBackRequestSchema,\n    PageGoForwardRequest: PageGoForwardRequestSchema,\n    PageTargetIdRequest: PageTargetIdRequestSchema,\n    PageMainFrameIdRequest: PageMainFrameIdRequestSchema,\n    PageMainFrameRequest: PageMainFrameRequestSchema,\n    PageGetFullFrameTreeRequest: PageGetFullFrameTreeRequestSchema,\n    PageAsProtocolFrameTreeRequest: PageAsProtocolFrameTreeRequestSchema,\n    PageListAllFrameIdsRequest: PageListAllFrameIdsRequestSchema,\n    PageGetOrdinalRequest: PageGetOrdinalRequestSchema,\n    PageTitleRequest: PageTitleRequestSchema,\n    PageUrlRequest: PageUrlRequestSchema,\n    PageScreenshotRequest: PageScreenshotRequestSchema,\n    PageSnapshotRequest: PageSnapshotRequestSchema,\n    PageFramesRequest: PageFramesRequestSchema,\n    PageSetViewportSizeRequest: PageSetViewportSizeRequestSchema,\n    PageSetExtraHTTPHeadersRequest: PageSetExtraHTTPHeadersRequestSchema,\n    PageWaitForLoadStateRequest: PageWaitForLoadStateRequestSchema,\n    PageWaitForMainLoadStateRequest: PageWaitForMainLoadStateRequestSchema,\n    PageWaitForSelectorRequest: PageWaitForSelectorRequestSchema,\n    PageWaitForTimeoutRequest: PageWaitForTimeoutRequestSchema,\n    PageEvaluateRequest: PageEvaluateRequestSchema,\n    PageSendCDPRequest: PageSendCDPRequestSchema,\n    PageCloseRequest: PageCloseRequestSchema,\n    PageClickAction: PageClickActionSchema,\n    PageHoverAction: PageHoverActionSchema,\n    PageScrollAction: PageScrollActionSchema,\n    PageDragAndDropAction: PageDragAndDropActionSchema,\n    PageTypeAction: PageTypeActionSchema,\n    PageKeyPressAction: PageKeyPressActionSchema,\n    PageEnableCursorOverlayAction: PageEnableCursorOverlayActionSchema,\n    PageAddInitScriptAction: PageAddInitScriptActionSchema,\n    PageGotoAction: PageGotoActionSchema,\n    PageReloadAction: PageReloadActionSchema,\n    PageGoBackAction: PageGoBackActionSchema,\n    PageGoForwardAction: PageGoForwardActionSchema,\n    PageTargetIdAction: PageTargetIdActionSchema,\n    PageMainFrameIdAction: PageMainFrameIdActionSchema,\n    PageMainFrameAction: PageMainFrameActionSchema,\n    PageGetFullFrameTreeAction: PageGetFullFrameTreeActionSchema,\n    PageAsProtocolFrameTreeAction: PageAsProtocolFrameTreeActionSchema,\n    PageListAllFrameIdsAction: PageListAllFrameIdsActionSchema,\n    PageGetOrdinalAction: PageGetOrdinalActionSchema,\n    PageTitleAction: PageTitleActionSchema,\n    PageUrlAction: PageUrlActionSchema,\n    PageScreenshotAction: PageScreenshotActionSchema,\n    PageSnapshotAction: PageSnapshotActionSchema,\n    PageFramesAction: PageFramesActionSchema,\n    PageSetViewportSizeAction: PageSetViewportSizeActionSchema,\n    PageSetExtraHTTPHeadersAction: PageSetExtraHTTPHeadersActionSchema,\n    PageWaitForLoadStateAction: PageWaitForLoadStateActionSchema,\n    PageWaitForMainLoadStateAction: PageWaitForMainLoadStateActionSchema,\n    PageWaitForSelectorAction: PageWaitForSelectorActionSchema,\n    PageWaitForTimeoutAction: PageWaitForTimeoutActionSchema,\n    PageEvaluateAction: PageEvaluateActionSchema,\n    PageSendCDPAction: PageSendCDPActionSchema,\n    PageCloseAction: PageCloseActionSchema,\n    PageAction: PageActionSchema,\n    PageClickResponse: PageClickResponseSchema,\n    PageHoverResponse: PageHoverResponseSchema,\n    PageScrollResponse: PageScrollResponseSchema,\n    PageDragAndDropResponse: PageDragAndDropResponseSchema,\n    PageTypeResponse: PageTypeResponseSchema,\n    PageKeyPressResponse: PageKeyPressResponseSchema,\n    PageEnableCursorOverlayResponse: PageEnableCursorOverlayResponseSchema,\n    PageAddInitScriptResponse: PageAddInitScriptResponseSchema,\n    PageGotoResponse: PageGotoResponseSchema,\n    PageReloadResponse: PageReloadResponseSchema,\n    PageGoBackResponse: PageGoBackResponseSchema,\n    PageGoForwardResponse: PageGoForwardResponseSchema,\n    PageTargetIdResponse: PageTargetIdResponseSchema,\n    PageMainFrameIdResponse: PageMainFrameIdResponseSchema,\n    PageMainFrameResponse: PageMainFrameResponseSchema,\n    PageGetFullFrameTreeResponse: PageGetFullFrameTreeResponseSchema,\n    PageAsProtocolFrameTreeResponse: PageAsProtocolFrameTreeResponseSchema,\n    PageListAllFrameIdsResponse: PageListAllFrameIdsResponseSchema,\n    PageGetOrdinalResponse: PageGetOrdinalResponseSchema,\n    PageTitleResponse: PageTitleResponseSchema,\n    PageUrlResponse: PageUrlResponseSchema,\n    PageScreenshotResponse: PageScreenshotResponseSchema,\n    PageSnapshotResponse: PageSnapshotResponseSchema,\n    PageFramesResponse: PageFramesResponseSchema,\n    PageSetViewportSizeResponse: PageSetViewportSizeResponseSchema,\n    PageSetExtraHTTPHeadersResponse: PageSetExtraHTTPHeadersResponseSchema,\n    PageWaitForLoadStateResponse: PageWaitForLoadStateResponseSchema,\n    PageWaitForMainLoadStateResponse: PageWaitForMainLoadStateResponseSchema,\n    PageWaitForSelectorResponse: PageWaitForSelectorResponseSchema,\n    PageWaitForTimeoutResponse: PageWaitForTimeoutResponseSchema,\n    PageEvaluateResponse: PageEvaluateResponseSchema,\n    PageSendCDPResponse: PageSendCDPResponseSchema,\n    PageCloseResponse: PageCloseResponseSchema,\n    PageActionIdParams: PageActionIdParamsSchema,\n    PageActionDetailsQuery: PageActionDetailsQuerySchema,\n    PageActionListQuery: PageActionListQuerySchema,\n    PageActionDetailsResponse: PageActionDetailsResponseSchema,\n    PageActionListResponse: PageActionListResponseSchema,\n  },\n};\n\nexport type PageActionMethod = z.infer<typeof PageActionMethodSchema>;\nexport type PageActionStatus = z.infer<typeof PageActionStatusSchema>;\nexport type PageAction = z.infer<typeof PageActionSchema>;\nexport type PageActionDetailsQuery = z.infer<\n  typeof PageActionDetailsQuerySchema\n>;\nexport type PageActionListQuery = z.infer<typeof PageActionListQuerySchema>;\n\nexport function buildErrorResponse(input: {\n  error: z.input<typeof PageErrorSchema>;\n  statusCode: number;\n  stack?: string | null;\n  action?: z.input<typeof PageActionSchema>;\n}) {\n  return V4ErrorResponseSchema.parse({\n    success: false,\n    error: input.error,\n    statusCode: input.statusCode,\n    stack: input.stack ?? null,\n    ...(input.action ? { action: input.action } : {}),\n  });\n}\n"
  },
  {
    "path": "packages/server-v4/src/sea-entry.ts",
    "content": "import { __internalMaybeRunShutdownSupervisorFromArgv } from \"@browserbasehq/stagehand\";\n\n// if SEA binary is launched with --supervisor, it will run the shutdown supervisor only\nconst argv = process.argv.slice(1);\nconst normalizedArgv = argv[0]?.startsWith(\"--\") ? argv : argv.slice(1);\n\n// otherwise, start the stagehand/server\nif (!__internalMaybeRunShutdownSupervisorFromArgv(normalizedArgv)) {\n  void import(\"./server.js\").catch((err) => {\n    console.error(\"Failed to start server:\", err);\n    process.exit(1);\n  });\n}\n"
  },
  {
    "path": "packages/server-v4/src/server.ts",
    "content": "import fastify from \"fastify\";\nimport fastifySwagger from \"@fastify/swagger\";\nimport fastifySwaggerUI from \"@fastify/swagger-ui\";\nimport {\n  fastifyZodOpenApiPlugin,\n  fastifyZodOpenApiTransformers,\n  serializerCompiler,\n  validatorCompiler,\n  type FastifyZodOpenApiTypeProvider,\n} from \"fastify-zod-openapi\";\nimport { StatusCodes } from \"http-status-codes\";\n\nimport { browserSessionOpenApiComponents } from \"./schemas/v4/browserSession.js\";\nimport { pageOpenApiComponents } from \"./schemas/v4/page.js\";\nimport healthcheckRoute from \"./routes/healthcheck.js\";\nimport readinessRoute, { setReady, setUnready } from \"./routes/readiness.js\";\nimport { browserSessionRoutesPlugin } from \"./routes/v4/browsersession/routes.js\";\nimport { pageRoutesPlugin } from \"./routes/v4/page/routes.js\";\n\nconst app = fastify({\n  logger: false,\n  return503OnClosing: false,\n});\n\n// Allow requests with `Content-Type: application/json` and an empty body (0 bytes).\n// Some clients always send the header even when there is no request body (e.g. /end).\nconst defaultJsonParser = app.getDefaultJsonParser(\"error\", \"error\");\napp.addContentTypeParser<string>(\n  \"application/json\",\n  { parseAs: \"string\" },\n  (request, body, done) => {\n    if (body === \"\" || (Buffer.isBuffer(body) && body.length === 0)) {\n      done(null, {});\n      return;\n    }\n\n    void defaultJsonParser(request, body, done);\n  },\n);\n\nconst start = async () => {\n  try {\n    app.setValidatorCompiler(validatorCompiler);\n    app.setSerializerCompiler(serializerCompiler);\n\n    await app.register(fastifyZodOpenApiPlugin, {\n      components: {\n        schemas: {\n          ...browserSessionOpenApiComponents.schemas,\n          ...pageOpenApiComponents.schemas,\n        },\n      },\n    });\n\n    await app.register(fastifySwagger, {\n      openapi: {\n        info: {\n          title: \"Stagehand API\",\n          version: \"3.0.5\",\n        },\n        openapi: \"3.1.0\",\n        tags: [\n          {\n            name: \"browserSession\",\n            description: \"Browser session lifecycle and browser-scoped actions\",\n          },\n          {\n            name: \"page\",\n            description: \"Page-scoped actions and action history endpoints\",\n          },\n        ],\n      },\n      ...fastifyZodOpenApiTransformers,\n    });\n\n    // Only register Swagger UI in development - SEA binaries can't load static files\n    if (process.env.NODE_ENV === \"development\") {\n      await app.register(fastifySwaggerUI, {\n        routePrefix: \"/documentation\",\n      });\n    }\n\n    app.setErrorHandler((error, _request, reply) => {\n      const statusCode = (error as { validation?: unknown[] }).validation\n        ? StatusCodes.BAD_REQUEST\n        : ((error as { statusCode?: number }).statusCode ??\n          StatusCodes.INTERNAL_SERVER_ERROR);\n      const errorMessage = (error as { validation?: unknown[] }).validation\n        ? \"Request validation failed\"\n        : error instanceof Error\n          ? error.message\n          : String(error);\n\n      reply.status(statusCode).send({\n        error:\n          statusCode === Number(StatusCodes.INTERNAL_SERVER_ERROR)\n            ? \"Internal Server Error\"\n            : errorMessage,\n        statusCode,\n      });\n    });\n\n    const appWithTypes = app.withTypeProvider<FastifyZodOpenApiTypeProvider>();\n\n    await appWithTypes.register(browserSessionRoutesPlugin, { prefix: \"/v4\" });\n    await appWithTypes.register(pageRoutesPlugin, { prefix: \"/v4\" });\n\n    // Register health and readiness routes at the root level\n    appWithTypes.route(healthcheckRoute);\n    appWithTypes.route(readinessRoute);\n    await app.ready();\n\n    await app.listen({\n      host: \"0.0.0.0\",\n      port: parseInt(process.env.PORT ?? \"3000\", 10),\n    });\n    setReady();\n  } catch (err) {\n    console.error(\"Failed to start server:\", err);\n    process.exit(1);\n  }\n};\n\nconst shutdown = async () => {\n  setUnready();\n  await app.close();\n  process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => {\n  shutdown().catch((err: unknown) => {\n    console.error(\"Failed to shut down cleanly:\", err);\n    process.exit(1);\n  });\n});\n\nprocess.on(\"SIGINT\", () => {\n  shutdown().catch((err: unknown) => {\n    console.error(\"Failed to shut down cleanly:\", err);\n    process.exit(1);\n  });\n});\n\nstart().catch((err: unknown) => {\n  console.error(\"Failed to start server:\", err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/server-v4/src/types/error.ts",
    "content": "import { StatusCodes } from \"http-status-codes\";\n\nexport class AppError extends Error {\n  public readonly statusCode: number;\n  public readonly isInternal: boolean;\n\n  constructor(\n    message: string,\n    statusCode: number = StatusCodes.INTERNAL_SERVER_ERROR,\n    isInternal = false,\n  ) {\n    super(message);\n    this.name = new.target.name;\n    this.statusCode = statusCode;\n    this.isInternal = isInternal;\n  }\n}\n\nexport class UnknownModelError extends AppError {\n  constructor(model: string) {\n    super(`Unknown model: ${model}`, StatusCodes.BAD_REQUEST);\n  }\n}\nexport class InvalidProviderError extends AppError {\n  constructor(provider: string) {\n    super(`Invalid provider: ${provider}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class InvalidModelError extends AppError {\n  constructor(model: string) {\n    super(`Invalid model: ${model}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class UnauthorizedError extends AppError {\n  constructor() {\n    super(\"Unauthorized\", StatusCodes.UNAUTHORIZED);\n  }\n}\n\nexport class MissingHeaderError extends AppError {\n  constructor(header: string) {\n    super(`Missing required header: ${header}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class InvalidAPIKeyError extends AppError {\n  constructor(provider: string) {\n    super(`Invalid API key for provider: ${provider}`, StatusCodes.BAD_REQUEST);\n  }\n}\n\nexport class AttemptedCloseOnNonActiveSessionError extends AppError {\n  constructor() {\n    super(\n      \"Attempted to close session that is not currently active\",\n      StatusCodes.CONFLICT,\n    );\n  }\n}\n\ninterface BrowserbaseError {\n  status?: number;\n  statusCode?: number;\n  message?: string;\n  response?: {\n    status?: number;\n    data?: {\n      message?: string;\n    };\n  };\n}\n\nexport class BrowserbaseSDKError extends AppError {\n  constructor(error: unknown, defaultMessage: string) {\n    const browserbaseError = error as BrowserbaseError;\n    const {\n      message: errMessage,\n      status,\n      statusCode: errStatusCode,\n      response,\n    } = browserbaseError;\n\n    let message = defaultMessage;\n    let finalStatusCode = StatusCodes.BAD_REQUEST;\n\n    // Extract message from error\n    if (errMessage) {\n      message = errMessage;\n    } else if (response?.data?.message) {\n      ({ message } = response.data);\n    }\n\n    // Extract status code from error\n    if (status && typeof status === \"number\") {\n      finalStatusCode = status as StatusCodes;\n    } else if (errStatusCode && typeof errStatusCode === \"number\") {\n      finalStatusCode = errStatusCode as StatusCodes;\n    } else if (response?.status && typeof response.status === \"number\") {\n      finalStatusCode = response.status as StatusCodes;\n    }\n\n    // Check for specific session error\n    if (message.includes(\"is not running\")) {\n      throw new AttemptedCloseOnNonActiveSessionError();\n    }\n\n    // Mark 5xx errors as internal to sanitize sensitive details\n    const isInternal =\n      Number(finalStatusCode) >= Number(StatusCodes.INTERNAL_SERVER_ERROR);\n\n    super(message, finalStatusCode, isInternal);\n  }\n}\n"
  },
  {
    "path": "packages/server-v4/src/types/fastify.d.ts",
    "content": "import \"fastify\";\n\ndeclare module \"fastify\" {\n  interface FastifyRequest {\n    metrics: {\n      startTime: number;\n    };\n  }\n}\n"
  },
  {
    "path": "packages/server-v4/src/types/model.ts",
    "content": "export const AISDK_PROVIDERS = [\n  \"openai\",\n  \"anthropic\",\n  \"google\",\n  \"xai\",\n  \"azure\",\n  \"groq\",\n  \"cerebras\",\n  \"togetherai\",\n  \"mistral\",\n  \"deepseek\",\n  \"perplexity\",\n  \"ollama\",\n  \"vertex\",\n  \"bedrock\",\n] as const;\nexport type AISDKProvider = (typeof AISDK_PROVIDERS)[number];\n\nexport type LegacyModel =\n  | \"gpt-4o\"\n  | \"gpt-4o-mini\"\n  | \"gpt-4o-2024-08-06\"\n  | \"gpt-4o-2024-05-13\"\n  | \"cerebras-llama-3.3-70b\"\n  | \"cerebras-llama-3.1-8b\"\n  | \"o1-mini\"\n  | \"o1-preview\"\n  | \"o3-mini\"\n  | \"gpt-4.5-preview\"\n  | \"groq-llama-3.3-70b-specdec\"\n  | \"groq-llama-3.3-70b-versatile\"\n  | \"gemini-1.5-flash\"\n  | \"gemini-1.5-pro\"\n  | \"gemini-1.5-flash-8b\"\n  | \"gemini-2.0-flash-lite\"\n  | \"gemini-2.0-flash\"\n  | \"gemini-2.5-pro-preview-03-25\"\n  | \"gemini-2.5-flash-preview-04-17\";\n\nexport type LegacyProvider = \"openai\" | \"anthropic\" | \"google\";\n"
  },
  {
    "path": "packages/server-v4/src/types/rrweb.ts",
    "content": "export interface Node {\n  type: string;\n  tagName?: string;\n  attributes?: Record<string, string>;\n  childNodes?: Node[];\n  textContent?: string;\n  id: number;\n}\n\nexport interface Event {\n  type: number;\n  /*\n  The data object is different for each event type\n  but we're only accessing it when the data follows\n  this structure, so we can just type this way.\n  */\n  data: { node: Node };\n  sessionId?: string;\n  timestamp: Date;\n  actionId: string;\n}\n"
  },
  {
    "path": "packages/server-v4/test/integration/utils.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { chromium } from \"playwright\";\n\n// =============================================================================\n// HTTP Status Codes\n// =============================================================================\n\nexport const HTTP_OK = 200;\nexport const HTTP_BAD_REQUEST = 400;\nexport const HTTP_NOT_FOUND = 404;\nexport const HTTP_GONE = 410;\nexport const HTTP_UNPROCESSABLE_ENTITY = 422;\nexport const HTTP_INTERNAL_SERVER_ERROR = 500;\n\n// =============================================================================\n// Timing Constants\n// =============================================================================\n\nexport const SESSION_CLOSE_WAIT_MS = 2000;\n\n// =============================================================================\n// Environment Variables\n// =============================================================================\n\nexport const {\n  STAGEHAND_API_URL,\n  OPENAI_API_KEY,\n  GEMINI_API_KEY,\n  ANTHROPIC_API_KEY,\n} = process.env;\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\nexport function requireEnv(name: string, value: string | undefined): string {\n  if (!value) {\n    throw new Error(`Missing required environment variable: ${name}`);\n  }\n  return value;\n}\n\nexport function getBaseUrl(): string {\n  return STAGEHAND_API_URL ?? \"http://127.0.0.1:3107\";\n}\n\n// =============================================================================\n// Header Generators\n// =============================================================================\n\nexport function getHeaders(\n  sdkVersion: string,\n  language: string = \"typescript\",\n): Record<string, string> {\n  return {\n    \"Content-Type\": \"application/json\",\n    \"x-model-api-key\": OPENAI_API_KEY ?? \"test-model-api-key\",\n    \"x-language\": language,\n    \"x-sdk-version\": sdkVersion,\n  };\n}\n\n// =============================================================================\n// Session Management\n// =============================================================================\n\nexport interface StartSessionResponse {\n  success: boolean;\n  message?: string;\n  data?: {\n    browserSession: {\n      id: string;\n      cdpUrl: string;\n      available: boolean;\n    };\n  };\n}\n\nconst SESSION_READY_DELAY_MS = 250;\nconst LOCAL_CONNECT_TIMEOUT_MS = (() => {\n  const parsed = Number(process.env.STAGEHAND_TEST_LOCAL_CONNECT_TIMEOUT_MS);\n  return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;\n})();\n\nexport interface SessionInfo {\n  sessionId: string;\n  cdpUrl: string;\n}\n\nfunction createLocalBrowserBody() {\n  const resolveChromePath = (): string => {\n    const explicit = process.env.CHROME_PATH;\n    if (explicit && fs.existsSync(explicit)) {\n      return explicit;\n    }\n    if (explicit) {\n      throw new Error(`CHROME_PATH does not exist: ${explicit}`);\n    }\n\n    const playwrightPath = chromium.executablePath();\n    if (playwrightPath && fs.existsSync(playwrightPath)) {\n      return playwrightPath;\n    }\n\n    throw new Error(\n      \"Unable to locate a Chrome executable. Set CHROME_PATH in the test environment.\",\n    );\n  };\n\n  return {\n    env: \"LOCAL\",\n    localBrowserLaunchOptions: {\n      headless: true,\n      executablePath: resolveChromePath(),\n      args: process.env.CI ? [\"--no-sandbox\"] : undefined,\n      connectTimeoutMs: LOCAL_CONNECT_TIMEOUT_MS,\n    },\n  };\n}\n\nexport const LOCAL_BROWSER_BODY = createLocalBrowserBody();\n\nfunction readLaunchDiagnostics(launchOptions?: {\n  executablePath?: string;\n  args?: string[];\n  headless?: boolean;\n  userDataDir?: string;\n  port?: number;\n  connectTimeoutMs?: number;\n}): string {\n  const diagnostics: string[] = [];\n  const userDataDir = launchOptions?.userDataDir;\n  diagnostics.push(\"--- launch diagnostics ---\");\n  diagnostics.push(`CHROME_PATH env: ${process.env.CHROME_PATH ?? \"<unset>\"}`);\n  diagnostics.push(`CI env: ${process.env.CI ?? \"<unset>\"}`);\n  diagnostics.push(`userDataDir: ${userDataDir ?? \"<auto>\"}`);\n  if (!userDataDir) {\n    diagnostics.push(\n      \"chrome stdout/stderr logs unavailable (profile dir auto-managed by server launch)\",\n    );\n  } else {\n    diagnostics.push(`userDataDir exists: ${fs.existsSync(userDataDir)}`);\n    if (fs.existsSync(userDataDir)) {\n      const outPath = path.join(userDataDir, \"chrome-out.log\");\n      const errPath = path.join(userDataDir, \"chrome-err.log\");\n      if (fs.existsSync(outPath)) {\n        diagnostics.push(\n          `--- chrome stdout ---\\n${fs.readFileSync(outPath, \"utf8\")}`,\n        );\n      }\n      if (fs.existsSync(errPath)) {\n        diagnostics.push(\n          `--- chrome stderr ---\\n${fs.readFileSync(errPath, \"utf8\")}`,\n        );\n      }\n    }\n  }\n  if (launchOptions) {\n    diagnostics.push(\n      `launch.executablePath: ${launchOptions.executablePath ?? \"<unset>\"}`,\n    );\n    diagnostics.push(\n      `launch.executablePath exists: ${\n        launchOptions.executablePath\n          ? fs.existsSync(launchOptions.executablePath)\n          : false\n      }`,\n    );\n    diagnostics.push(`launch.headless: ${String(launchOptions.headless)}`);\n    diagnostics.push(\n      `launch.args: ${JSON.stringify(launchOptions.args ?? [])}`,\n    );\n    diagnostics.push(`launch.port: ${launchOptions.port ?? \"<auto>\"}`);\n    diagnostics.push(\n      `launch.connectTimeoutMs: ${launchOptions.connectTimeoutMs ?? \"<default>\"}`,\n    );\n  }\n  return diagnostics.join(\"\\n\");\n}\n\nexport async function createSession(\n  headers: Record<string, string>,\n): Promise<string> {\n  const info = await createSessionWithCdp(headers);\n  return info.sessionId;\n}\n\nexport async function createSessionWithCdp(\n  headers: Record<string, string>,\n): Promise<SessionInfo> {\n  const url = getBaseUrl();\n  const startPayload = {\n    modelName: \"gpt-4.1-nano\",\n    ...createLocalBrowserBody(),\n  };\n\n  const response = await fetch(`${url}/v4/browsersession`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify(startPayload),\n  });\n\n  const responseText = await response.text();\n  let parsedBody: unknown;\n  try {\n    parsedBody = responseText ? JSON.parse(responseText) : null;\n  } catch {\n    parsedBody = responseText;\n  }\n  const body = parsedBody as StartSessionResponse;\n\n  if (!response.ok || !body?.success) {\n    const launchDiagnostics = readLaunchDiagnostics(\n      startPayload.localBrowserLaunchOptions,\n    );\n    throw new Error(\n      `Failed to create session (status=${response.status}): ${JSON.stringify(\n        parsedBody,\n      )}\\n${launchDiagnostics}`,\n    );\n  }\n  if (!body.data?.browserSession.available) {\n    throw new Error(`Session not available`);\n  }\n  if (!body.data.browserSession.id) {\n    throw new Error(\"No browserSession id returned\");\n  }\n  if (!body.data.browserSession.cdpUrl) {\n    throw new Error(\"No cdpUrl returned\");\n  }\n\n  // Wait for session to be fully ready before returning\n  await new Promise((resolve) => setTimeout(resolve, SESSION_READY_DELAY_MS));\n\n  return {\n    sessionId: body.data.browserSession.id,\n    cdpUrl: body.data.browserSession.cdpUrl,\n  };\n}\n\nexport async function endSession(\n  sessionId: string,\n  headers: Record<string, string>,\n): Promise<void> {\n  const url = getBaseUrl();\n\n  await fetch(`${url}/v4/browsersession/${sessionId}/end`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify({}),\n  });\n}\n\n/**\n * Gets the main frame ID from a CDP session\n */\nexport async function getMainFrameId(cdpUrl: string): Promise<string> {\n  const browser = await chromium.connectOverCDP(cdpUrl);\n  try {\n    const contexts = browser.contexts();\n    if (contexts.length === 0) {\n      throw new Error(\"No browser contexts found\");\n    }\n    const pages = contexts[0]!.pages();\n    if (pages.length === 0) {\n      throw new Error(\"No pages found\");\n    }\n    const page = pages[0]!;\n\n    // Use CDP to get the frame tree and extract the main frame ID\n    const cdpSession = await page.context().newCDPSession(page);\n    const { frameTree } = await cdpSession.send(\"Page.getFrameTree\");\n    await cdpSession.detach();\n\n    return frameTree.frame.id;\n  } finally {\n    await browser.close();\n  }\n}\n\n// =============================================================================\n// SSE Stream Reader\n// =============================================================================\n\n// Legacy SSE event interface (generic)\nexport interface SSEEvent {\n  event?: string;\n  data?: string;\n  parsed?: unknown;\n}\n\nexport async function readSSEStream(response: Response): Promise<SSEEvent[]> {\n  const reader = response.body?.getReader() as\n    | ReadableStreamDefaultReader<Uint8Array>\n    | undefined;\n  if (!reader) {\n    throw new Error(\"No response body reader available\");\n  }\n\n  const decoder = new TextDecoder();\n  let fullResponse = \"\";\n\n  for (;;) {\n    const result = await reader.read();\n    if (result.done) break;\n    fullResponse += decoder.decode(result.value, { stream: true });\n  }\n\n  // Parse SSE events\n  const events: SSEEvent[] = [];\n  const rawEvents = fullResponse.split(\"\\n\\n\").filter((e) => e.trim());\n\n  for (const rawEvent of rawEvents) {\n    const event: SSEEvent = {};\n    const lines = rawEvent.split(\"\\n\");\n\n    for (const line of lines) {\n      if (line.startsWith(\"event:\")) {\n        event.event = line.slice(6).trim();\n      } else if (line.startsWith(\"data:\")) {\n        event.data = line.slice(5).trim();\n        try {\n          event.parsed = JSON.parse(event.data);\n        } catch {\n          // Keep as string if not valid JSON\n        }\n      }\n    }\n\n    if (event.data || event.event) {\n      events.push(event);\n    }\n  }\n\n  return events;\n}\n\n// =============================================================================\n// Typed SSE Event Helpers (for stagehand-api backend format)\n// =============================================================================\n\n// Actual SSE event format from backend (see stream.ts):\n// { data: { status: \"starting\" | \"connected\" | \"finished\", result?: ... }, type: \"system\" | \"log\", id: \"<uuid>\" }\nexport interface TypedSSEEvent<TResult = unknown> {\n  data: {\n    status: string;\n    result?: TResult;\n    message?: string;\n    error?: string;\n  };\n  type: string;\n  id: string;\n}\n\n/**\n * Read SSE stream from response and return raw string\n */\nexport async function readSSEStreamRaw(response: Response): Promise<string> {\n  const reader = response.body?.getReader() as\n    | ReadableStreamDefaultReader<Uint8Array>\n    | undefined;\n  if (!reader) throw new Error(\"No response body reader\");\n\n  const decoder = new TextDecoder();\n  let fullResponse = \"\";\n\n  for (;;) {\n    const result = await reader.read();\n    if (result.done) break;\n    fullResponse += decoder.decode(result.value, { stream: true });\n  }\n\n  return fullResponse;\n}\n\n/**\n * Parse raw SSE response string into typed events\n */\nexport function parseTypedSSEEvents<TResult = unknown>(\n  rawResponse: string,\n): TypedSSEEvent<TResult>[] {\n  const events = rawResponse.split(\"\\n\\n\").filter((e) => e.trim());\n  return events\n    .map((event) => {\n      const dataMatch = event.match(/data: (.+)/);\n      if (dataMatch?.[1]) {\n        return JSON.parse(dataMatch[1]) as TypedSSEEvent<TResult>;\n      }\n      return null;\n    })\n    .filter((e): e is TypedSSEEvent<TResult> => e !== null);\n}\n\n/**\n * Result of reading an SSE stream with full context for debugging\n */\nexport interface SSEStreamResult<TResult = unknown> {\n  /** HTTP status code */\n  status: number;\n  /** HTTP status text */\n  statusText: string;\n  /** Raw response body */\n  raw: string;\n  /** Parsed SSE events */\n  events: TypedSSEEvent<TResult>[];\n  /** Get debug summary for error messages */\n  debugSummary(): string;\n}\n\n/**\n * Read SSE stream and parse into typed events (legacy - no debug context)\n */\nexport async function readTypedSSEStream<TResult = unknown>(\n  response: Response,\n): Promise<TypedSSEEvent<TResult>[]> {\n  const raw = await readSSEStreamRaw(response);\n  return parseTypedSSEEvents<TResult>(raw);\n}\n\n/**\n * Read SSE stream with full context for debugging test failures.\n * Use this instead of readTypedSSEStream when you need better error messages.\n */\nexport async function readTypedSSEStreamWithContext<TResult = unknown>(\n  response: Response,\n): Promise<SSEStreamResult<TResult>> {\n  const status = response.status;\n  const statusText = response.statusText;\n  const raw = await readSSEStreamRaw(response);\n  const events = parseTypedSSEEvents<TResult>(raw);\n\n  return {\n    status,\n    statusText,\n    raw,\n    events,\n    debugSummary() {\n      const eventStatuses = events.map((e) => e.data.status).join(\" → \");\n      const errorEvents = events.filter((e) => e.data.status === \"error\");\n      const errorMessages = errorEvents\n        .map((e) => e.data.error ?? \"unknown error\")\n        .join(\", \");\n\n      let summary = `HTTP ${status} ${statusText}`;\n      if (events.length === 0) {\n        summary += `\\n  No SSE events received`;\n        summary += `\\n  Raw response: ${raw.slice(0, 500)}${raw.length > 500 ? \"...\" : \"\"}`;\n      } else {\n        summary += `\\n  Events (${events.length}): ${eventStatuses}`;\n        if (errorMessages) {\n          summary += `\\n  Errors: ${errorMessages}`;\n        }\n      }\n      return summary;\n    },\n  };\n}\n\n/**\n * Assert with debug context - includes SSE stream info on failure\n */\nexport function assertWithContext(\n  condition: boolean,\n  message: string,\n  context: SSEStreamResult<unknown>,\n): asserts condition {\n  if (!condition) {\n    throw new Error(`${message}\\n\\nDebug context:\\n${context.debugSummary()}`);\n  }\n}\n\n/**\n * Assert SSE event exists with debug context on failure, returns the found event\n */\nexport function assertEventExists<TResult>(\n  events: TypedSSEEvent<TResult>[],\n  status: string,\n  context: SSEStreamResult<TResult>,\n): TypedSSEEvent<TResult> {\n  const found = events.find((e) => e.data.status === status);\n  assertWithContext(\n    found !== undefined,\n    `Should have a \"${status}\" event`,\n    context,\n  );\n  return found;\n}\n\n/**\n * Assert HTTP status with debug context on failure\n */\nexport function assertHttpStatus(\n  context: SSEStreamResult<unknown>,\n  expectedStatus: number,\n  message?: string,\n): void {\n  assertWithContext(\n    context.status === expectedStatus,\n    message ?? `Expected HTTP ${expectedStatus}, got ${context.status}`,\n    context,\n  );\n}\n\n// =============================================================================\n// JSON Response Debug Utilities (for non-SSE tests)\n// =============================================================================\n\n/**\n * Result of a fetch request with full context for debugging\n */\nexport interface FetchResult<T = unknown> {\n  /** HTTP status code */\n  status: number;\n  /** HTTP status text */\n  statusText: string;\n  /** Parsed JSON body (if parseable) */\n  body: T | null;\n  /** Raw response text */\n  raw: string;\n  /** Request duration in ms */\n  durationMs: number;\n  /** Response headers */\n  headers: Headers;\n  /** Get debug summary for error messages */\n  debugSummary(): string;\n}\n\n/**\n * Fetch with full context for debugging test failures.\n * Captures timing, status, and response body.\n */\nexport async function fetchWithContext<T = unknown>(\n  url: string,\n  options: RequestInit,\n): Promise<FetchResult<T>> {\n  const startTime = Date.now();\n  let response: Response;\n\n  try {\n    response = await fetch(url, options);\n  } catch (err) {\n    const durationMs = Date.now() - startTime;\n    const errorMsg = err instanceof Error ? err.message : String(err);\n    return {\n      status: 0,\n      statusText: \"FETCH_ERROR\",\n      body: null,\n      raw: errorMsg,\n      durationMs,\n      headers: new Headers(),\n      debugSummary() {\n        return `Fetch failed after ${durationMs}ms: ${errorMsg}`;\n      },\n    };\n  }\n\n  const durationMs = Date.now() - startTime;\n  const status = response.status;\n  const statusText = response.statusText;\n  const headers = response.headers;\n  const raw = await response.text();\n\n  let body: T | null = null;\n  try {\n    body = JSON.parse(raw) as T;\n  } catch {\n    // Keep body as null if not valid JSON\n  }\n\n  return {\n    status,\n    statusText,\n    body,\n    raw,\n    durationMs,\n    headers,\n    debugSummary() {\n      const seconds = (durationMs / 1000).toFixed(1);\n      let summary = `HTTP ${status} ${statusText} (${seconds}s)`;\n\n      if (body && typeof body === \"object\") {\n        const b = body as Record<string, unknown>;\n        if (b.success === false && typeof b.message === \"string\") {\n          summary += `\\n  Error: ${b.message}`;\n        }\n        if (typeof b.error === \"string\") {\n          summary += `\\n  Error: ${b.error}`;\n        }\n      }\n\n      // Show raw response if it's an error or unexpected\n      if (status >= 400 || !body) {\n        const truncated = raw.slice(0, 500);\n        summary += `\\n  Response: ${truncated}${raw.length > 500 ? \"...\" : \"\"}`;\n      }\n\n      return summary;\n    },\n  };\n}\n\n/**\n * Assert with fetch context - includes response info on failure\n */\nexport function assertFetchOk<T>(\n  condition: boolean,\n  message: string,\n  context: FetchResult<T>,\n): asserts condition {\n  if (!condition) {\n    throw new Error(`${message}\\n\\nDebug context:\\n${context.debugSummary()}`);\n  }\n}\n\n/**\n * Assert fetch succeeded with expected status\n */\nexport function assertFetchStatus<T>(\n  context: FetchResult<T>,\n  expectedStatus: number,\n  message?: string,\n): void {\n  assertFetchOk(\n    context.status === expectedStatus,\n    message ?? `Expected HTTP ${expectedStatus}, got ${context.status}`,\n    context,\n  );\n}\n"
  },
  {
    "path": "packages/server-v4/test/integration/v4/browsersession.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { describe, it } from \"node:test\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  fetchWithContext,\n  getBaseUrl,\n  getHeaders,\n  HTTP_BAD_REQUEST,\n  HTTP_NOT_FOUND,\n  HTTP_OK,\n  LOCAL_BROWSER_BODY,\n} from \"../utils.js\";\n\ninterface BrowserSessionRecord {\n  id: string;\n  env: \"LOCAL\" | \"BROWSERBASE\";\n  status: \"running\" | \"ended\";\n  modelName: string;\n  cdpUrl: string;\n  available: boolean;\n}\n\ninterface BrowserSessionResponse {\n  success: boolean;\n  message?: string;\n  data?: {\n    browserSession: BrowserSessionRecord;\n  };\n}\n\nconst headers = getHeaders(\"4.0.0\");\n\ndescribe(\"v4 browsersession routes\", { concurrency: false }, () => {\n  it(\"POST /v4/browsersession creates a local browser session and GET/POST end work\", async () => {\n    const createCtx = await fetchWithContext<BrowserSessionResponse>(\n      `${getBaseUrl()}/v4/browsersession`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          modelName: \"gpt-4.1-nano\",\n          ...LOCAL_BROWSER_BODY,\n        }),\n      },\n    );\n\n    assertFetchStatus(createCtx, HTTP_OK);\n    assertFetchOk(\n      createCtx.body !== null,\n      \"Expected a JSON response body\",\n      createCtx,\n    );\n    assert.equal(createCtx.body.success, true);\n    assertFetchOk(\n      createCtx.body.data?.browserSession !== undefined,\n      \"Expected a browserSession payload\",\n      createCtx,\n    );\n\n    const browserSession = createCtx.body.data!.browserSession;\n    assert.equal(browserSession.env, \"LOCAL\");\n    assert.equal(browserSession.status, \"running\");\n    assert.equal(browserSession.modelName, \"gpt-4.1-nano\");\n    assert.equal(browserSession.available, true);\n    assert.ok(browserSession.cdpUrl.length > 0);\n\n    const statusCtx = await fetchWithContext<BrowserSessionResponse>(\n      `${getBaseUrl()}/v4/browsersession/${browserSession.id}`,\n      {\n        method: \"GET\",\n        headers,\n      },\n    );\n\n    assertFetchStatus(statusCtx, HTTP_OK);\n    assertFetchOk(\n      statusCtx.body !== null,\n      \"Expected a JSON response body\",\n      statusCtx,\n    );\n    assert.equal(statusCtx.body.data?.browserSession.id, browserSession.id);\n    assert.equal(statusCtx.body.data?.browserSession.status, \"running\");\n\n    const endCtx = await fetchWithContext<BrowserSessionResponse>(\n      `${getBaseUrl()}/v4/browsersession/${browserSession.id}/end`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({}),\n      },\n    );\n\n    assertFetchStatus(endCtx, HTTP_OK);\n    assertFetchOk(\n      endCtx.body !== null,\n      \"Expected a JSON response body\",\n      endCtx,\n    );\n    assert.equal(endCtx.body.data?.browserSession.id, browserSession.id);\n    assert.equal(endCtx.body.data?.browserSession.status, \"ended\");\n    assert.equal(endCtx.body.data?.browserSession.available, false);\n\n    const missingCtx = await fetchWithContext<BrowserSessionResponse>(\n      `${getBaseUrl()}/v4/browsersession/${browserSession.id}`,\n      {\n        method: \"GET\",\n        headers,\n      },\n    );\n\n    assertFetchStatus(missingCtx, HTTP_NOT_FOUND);\n    assertFetchOk(\n      missingCtx.body !== null,\n      \"Expected a JSON response body\",\n      missingCtx,\n    );\n    assert.equal(missingCtx.body.success, false);\n  });\n\n  it(\"POST /v4/browsersession rejects LOCAL requests without cdpUrl or localBrowserLaunchOptions\", async () => {\n    const ctx = await fetchWithContext<BrowserSessionResponse>(\n      `${getBaseUrl()}/v4/browsersession`,\n      {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify({\n          env: \"LOCAL\",\n          modelName: \"gpt-4.1-nano\",\n        }),\n      },\n    );\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(ctx.body !== null, \"Expected a JSON response body\", ctx);\n    assert.equal(ctx.body.success, false);\n    assert.ok(ctx.body.message);\n  });\n});\n"
  },
  {
    "path": "packages/server-v4/test/integration/v4/page.test.ts",
    "content": "import assert from \"node:assert/strict\";\nimport { createServer } from \"node:http\";\nimport { after, before, describe, it } from \"node:test\";\n\nimport type { Page } from \"playwright\";\nimport { chromium } from \"playwright\";\n\nimport {\n  assertFetchOk,\n  assertFetchStatus,\n  createSessionWithCdp,\n  endSession,\n  fetchWithContext,\n  getBaseUrl,\n  getMainFrameId,\n  getHeaders,\n  HTTP_BAD_REQUEST,\n  HTTP_OK,\n} from \"../utils.js\";\n\ninterface PageActionRecord {\n  id: string;\n  method: string;\n  status: string;\n  sessionId: string;\n  pageId?: string;\n  createdAt?: string;\n  updatedAt?: string;\n  completedAt?: string;\n  error?: string | null;\n  [key: string]: unknown;\n}\n\ninterface PageActionResponse {\n  success: boolean;\n  error: string | null;\n  statusCode?: number;\n  stack?: string | null;\n  action?: PageActionRecord;\n  actions?: PageActionRecord[];\n}\n\nconst headers = getHeaders(\"3.0.0\");\n\nconst GOTO_TEST_URL = `data:text/html;charset=utf-8,${encodeURIComponent(`\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>V4 goto route</title>\n  </head>\n  <body>\n    <main id=\"message\">goto-ok</main>\n  </body>\n</html>\n`)}`;\n\nconst CLICK_TEST_URL = `data:text/html;charset=utf-8,${encodeURIComponent(`\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>V4 click route</title>\n  </head>\n  <body data-clicked=\"no\">\n    <button\n      id=\"click-target\"\n      onclick=\"document.body.dataset.clicked='yes';document.getElementById('status').textContent='clicked';\"\n    >\n      Submit\n    </button>\n    <div id=\"status\">idle</div>\n  </body>\n</html>\n`)}`;\n\nconst METHODS_TEST_URL = `data:text/html;charset=utf-8,${encodeURIComponent(`\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>V4 methods route</title>\n    <style>\n      body { font-family: sans-serif; }\n      #scroll-box {\n        border: 1px solid #333;\n        height: 80px;\n        overflow: auto;\n        width: 200px;\n      }\n      #scroll-inner {\n        height: 400px;\n      }\n      #drag-source, #drag-target {\n        align-items: center;\n        border: 1px solid #333;\n        display: flex;\n        height: 40px;\n        justify-content: center;\n        margin-top: 8px;\n        width: 120px;\n      }\n    </style>\n  </head>\n  <body data-hovered=\"no\" data-dropped=\"no\">\n    <main id=\"message\">methods-ok</main>\n    <input id=\"text-input\" value=\"\" />\n    <button\n      id=\"hover-target\"\n      onmouseover=\"document.body.dataset.hovered='yes';\"\n    >\n      Hover me\n    </button>\n    <div id=\"scroll-box\">\n      <div id=\"scroll-inner\">scroll target</div>\n    </div>\n    <div\n      id=\"drag-source\"\n      onmousedown=\"window.__dragStart = true;\"\n    >\n      Drag source\n    </div>\n    <div\n      id=\"drag-target\"\n      onmouseup=\"if (window.__dragStart) { document.body.dataset.dropped='yes'; }\"\n    >\n      Drop target\n    </div>\n    <script>\n      setTimeout(() => {\n        const lateItem = document.createElement(\"div\");\n        lateItem.id = \"late-item\";\n        lateItem.textContent = \"ready\";\n        document.body.appendChild(lateItem);\n      }, 150);\n    </script>\n  </body>\n</html>\n`)}`;\n\nasync function withSessionPage<T>(\n  cdpUrl: string,\n  fn: (page: Page) => Promise<T>,\n): Promise<T> {\n  const browser = await chromium.connectOverCDP(cdpUrl);\n\n  try {\n    const contexts = browser.contexts();\n    assert.ok(contexts.length > 0, \"Expected at least one browser context\");\n\n    const pages = contexts[0]!.pages();\n    assert.ok(pages.length > 0, \"Expected at least one browser page\");\n\n    return await fn(pages[0]!);\n  } finally {\n    await browser.close();\n  }\n}\n\nasync function postPageRoute(\n  path: string,\n  sessionId: string,\n  params: Record<string, unknown>,\n) {\n  return fetchWithContext<PageActionResponse>(\n    `${getBaseUrl()}/v4/page/${path}`,\n    {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify({\n        sessionId,\n        params,\n      }),\n    },\n  );\n}\n\nasync function getPageRoute(\n  path: string,\n  sessionId: string,\n  params: Record<string, unknown>,\n) {\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"sessionId\", sessionId);\n\n  for (const [key, value] of Object.entries(params)) {\n    searchParams.set(key, String(value));\n  }\n\n  return fetchWithContext<PageActionResponse>(\n    `${getBaseUrl()}/v4/page/${path}?${searchParams.toString()}`,\n    {\n      method: \"GET\",\n      headers,\n    },\n  );\n}\n\nfunction assertSuccessAction(\n  ctx: Awaited<ReturnType<typeof fetchWithContext<PageActionResponse>>>,\n  expectedType: string,\n): PageActionRecord {\n  assertFetchStatus(ctx, HTTP_OK);\n  assertFetchOk(ctx.body !== null, \"Expected a JSON response body\", ctx);\n  assert.equal(ctx.body.success, true);\n  assert.equal(ctx.body.error, null);\n  assertFetchOk(\n    ctx.body.action !== undefined,\n    \"Expected an action payload\",\n    ctx,\n  );\n\n  const action = ctx.body.action;\n  assert.equal(typeof action.id, \"string\");\n  assert.notEqual(action.id.length, 0);\n  assert.equal(action.method, expectedType);\n  assert.equal(action.status, \"completed\");\n\n  return action;\n}\n\nfunction assertSuccessActionList(\n  ctx: Awaited<ReturnType<typeof fetchWithContext<PageActionResponse>>>,\n) {\n  assertFetchStatus(ctx, HTTP_OK);\n  assertFetchOk(ctx.body !== null, \"Expected a JSON response body\", ctx);\n  assert.equal(ctx.body.success, true);\n  assert.equal(ctx.body.error, null);\n  assertFetchOk(\n    Array.isArray(ctx.body.actions),\n    \"Expected an actions array payload\",\n    ctx,\n  );\n\n  return ctx.body.actions;\n}\n\ndescribe(\"v4 page routes\", { concurrency: false }, () => {\n  let sessionId: string;\n  let cdpUrl: string;\n\n  before(async () => {\n    ({ sessionId, cdpUrl } = await createSessionWithCdp(headers));\n  });\n\n  after(async () => {\n    await endSession(sessionId, headers);\n  });\n\n  it(\"POST /v4/page/goto returns the new envelope and navigates a real local session\", async () => {\n    const ctx = await postPageRoute(\"goto\", sessionId, {\n      url: GOTO_TEST_URL,\n      waitUntil: \"load\",\n    });\n\n    const action = assertSuccessAction(ctx, \"goto\");\n    assert.equal(action.sessionId, sessionId);\n    assert.equal(\n      (action.result as { response: unknown | null; url: string }).url,\n      GOTO_TEST_URL,\n    );\n    assert.equal(\n      (action.result as { response: unknown | null }).response,\n      null,\n    );\n\n    await withSessionPage(cdpUrl, async (page) => {\n      await page.waitForLoadState(\"load\", { timeout: 15_000 }).catch(() => {});\n      assert.equal(await page.title(), \"V4 goto route\");\n      assert.equal(await page.textContent(\"#message\"), \"goto-ok\");\n    });\n  });\n\n  it(\"POST /v4/page/click returns the new envelope and clicks a real page element\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: CLICK_TEST_URL,\n      waitUntil: \"load\",\n    });\n    const gotoAction = assertSuccessAction(gotoCtx, \"goto\");\n\n    const clickCtx = await postPageRoute(\"click\", sessionId, {\n      pageId: gotoAction.pageId,\n      selector: {\n        xpath: \"//button[@id='click-target']\",\n      },\n    });\n\n    const action = assertSuccessAction(clickCtx, \"click\");\n    assert.equal(action.sessionId, sessionId);\n\n    await withSessionPage(cdpUrl, async (page) => {\n      await page.waitForFunction(\n        () => document.body.dataset.clicked === \"yes\",\n        undefined,\n        {\n          timeout: 15_000,\n        },\n      );\n      assert.equal(await page.locator(\"#status\").textContent(), \"clicked\");\n    });\n  });\n\n  it(\"POST /v4/page methods route through the underlying understudy implementation\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: METHODS_TEST_URL,\n      waitUntil: \"load\",\n    });\n    assertSuccessAction(gotoCtx, \"goto\");\n\n    const hoverCtx = await postPageRoute(\"hover\", sessionId, {\n      selector: {\n        xpath: \"//button[@id='hover-target']\",\n      },\n    });\n    assertSuccessAction(hoverCtx, \"hover\");\n\n    const scrollCtx = await postPageRoute(\"scroll\", sessionId, {\n      selector: {\n        xpath: \"//div[@id='scroll-box']\",\n      },\n      percentage: 100,\n    });\n    assertSuccessAction(scrollCtx, \"scroll\");\n\n    const dragCtx = await postPageRoute(\"dragAndDrop\", sessionId, {\n      from: {\n        xpath: \"//div[@id='drag-source']\",\n      },\n      to: {\n        xpath: \"//div[@id='drag-target']\",\n      },\n    });\n    assertSuccessAction(dragCtx, \"dragAndDrop\");\n\n    const focusInputCtx = await postPageRoute(\"click\", sessionId, {\n      selector: {\n        xpath: \"//input[@id='text-input']\",\n      },\n    });\n    assertSuccessAction(focusInputCtx, \"click\");\n\n    const typeCtx = await postPageRoute(\"type\", sessionId, {\n      text: \"hello\",\n    });\n    assertSuccessAction(typeCtx, \"type\");\n\n    const keyPressCtx = await postPageRoute(\"keyPress\", sessionId, {\n      key: \"Backspace\",\n    });\n    assertSuccessAction(keyPressCtx, \"keyPress\");\n\n    const waitForSelectorCtx = await postPageRoute(\n      \"waitForSelector\",\n      sessionId,\n      {\n        selector: {\n          xpath: \"//div[@id='late-item']\",\n        },\n        state: \"visible\",\n        timeout: 5_000,\n      },\n    );\n    const waitForSelectorAction = assertSuccessAction(\n      waitForSelectorCtx,\n      \"waitForSelector\",\n    );\n    assert.equal(\n      (waitForSelectorAction.result as { matched: boolean }).matched,\n      true,\n    );\n\n    const waitForLoadStateCtx = await postPageRoute(\n      \"waitForLoadState\",\n      sessionId,\n      {\n        state: \"load\",\n      },\n    );\n    assertSuccessAction(waitForLoadStateCtx, \"waitForLoadState\");\n\n    const titleCtx = await getPageRoute(\"title\", sessionId, {});\n    const titleAction = assertSuccessAction(titleCtx, \"title\");\n    assert.equal(\n      (titleAction.result as { title: string }).title,\n      \"V4 methods route\",\n    );\n\n    const urlCtx = await getPageRoute(\"url\", sessionId, {});\n    const urlAction = assertSuccessAction(urlCtx, \"url\");\n    assert.equal((urlAction.result as { url: string }).url, METHODS_TEST_URL);\n\n    const evaluateCtx = await postPageRoute(\"evaluate\", sessionId, {\n      expression: \"arg.value * 2\",\n      arg: {\n        value: 21,\n      },\n    });\n    const evaluateAction = assertSuccessAction(evaluateCtx, \"evaluate\");\n    assert.equal((evaluateAction.result as { value: number }).value, 42);\n\n    const sendCDPCtx = await postPageRoute(\"sendCDP\", sessionId, {\n      method: \"Runtime.evaluate\",\n      params: {\n        expression: \"6 * 7\",\n        returnByValue: true,\n      },\n    });\n    const sendCDPAction = assertSuccessAction(sendCDPCtx, \"sendCDP\");\n    assert.equal(\n      (\n        sendCDPAction.result as {\n          value: { result?: { value?: number } };\n        }\n      ).value.result?.value,\n      42,\n    );\n\n    await withSessionPage(cdpUrl, async (page) => {\n      await page.waitForFunction(\n        () => document.body.dataset.hovered === \"yes\",\n        undefined,\n        { timeout: 5_000 },\n      );\n      await page.waitForFunction(\n        () => document.body.dataset.dropped === \"yes\",\n        undefined,\n        { timeout: 5_000 },\n      );\n      assert.equal(await page.locator(\"#text-input\").inputValue(), \"hell\");\n      assert.ok(\n        await page\n          .locator(\"#scroll-box\")\n          .evaluate((node) => (node as HTMLDivElement).scrollTop > 0),\n      );\n    });\n  });\n\n  it(\"POST /v4/page navigation helpers, screenshot, snapshot, viewport, timeout, and close work on a live session\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: METHODS_TEST_URL,\n      waitUntil: \"load\",\n    });\n    const gotoAction = assertSuccessAction(gotoCtx, \"goto\");\n    assert.equal(\n      (gotoAction.result as { response: unknown | null; url: string }).url,\n      METHODS_TEST_URL,\n    );\n    assert.equal(\n      (gotoAction.result as { response: unknown | null }).response,\n      null,\n    );\n\n    const setViewportSizeCtx = await postPageRoute(\n      \"setViewportSize\",\n      sessionId,\n      {\n        width: 900,\n        height: 700,\n        deviceScaleFactor: 1,\n      },\n    );\n    assertSuccessAction(setViewportSizeCtx, \"setViewportSize\");\n\n    const screenshotCtx = await postPageRoute(\"screenshot\", sessionId, {\n      type: \"jpeg\",\n      quality: 70,\n    });\n    const screenshotAction = assertSuccessAction(screenshotCtx, \"screenshot\");\n    const screenshotResult = screenshotAction.result as {\n      base64: string;\n      mimeType: string;\n    };\n    assert.equal(screenshotResult.mimeType, \"image/jpeg\");\n    assert.ok(screenshotResult.base64.length > 0);\n\n    const snapshotCtx = await postPageRoute(\"snapshot\", sessionId, {\n      includeIframes: true,\n    });\n    const snapshotAction = assertSuccessAction(snapshotCtx, \"snapshot\");\n    assert.match(\n      (snapshotAction.result as { formattedTree: string }).formattedTree,\n      /methods-ok/i,\n    );\n\n    const waitStart = Date.now();\n    const waitCtx = await postPageRoute(\"waitForTimeout\", sessionId, {\n      ms: 75,\n    });\n    assertSuccessAction(waitCtx, \"waitForTimeout\");\n    assert.ok(Date.now() - waitStart >= 50);\n\n    const zeroWaitCtx = await postPageRoute(\"waitForTimeout\", sessionId, {\n      ms: 0,\n    });\n    assertSuccessAction(zeroWaitCtx, \"waitForTimeout\");\n\n    const reloadCtx = await postPageRoute(\"reload\", sessionId, {\n      waitUntil: \"load\",\n    });\n    const reloadAction = assertSuccessAction(reloadCtx, \"reload\");\n    assert.equal(\n      (reloadAction.result as { response: unknown | null; url: string }).url,\n      METHODS_TEST_URL,\n    );\n    assert.equal(\n      (reloadAction.result as { response: unknown | null }).response,\n      null,\n    );\n\n    await withSessionPage(cdpUrl, async (page) => {\n      const viewport = await page.evaluate(() => ({\n        width: window.innerWidth,\n        height: window.innerHeight,\n      }));\n      assert.equal(viewport.width, 900);\n      assert.equal(viewport.height, 700);\n      assert.equal(\n        await page.evaluate(\n          () =>\n            performance.getEntriesByType(\"navigation\")[0]?.toJSON().type ?? \"\",\n        ),\n        \"reload\",\n      );\n    });\n\n    const gotoBackTargetCtx = await postPageRoute(\"goto\", sessionId, {\n      url: GOTO_TEST_URL,\n      waitUntil: \"load\",\n    });\n    assertSuccessAction(gotoBackTargetCtx, \"goto\");\n\n    const goBackCtx = await postPageRoute(\"goBack\", sessionId, {\n      waitUntil: \"load\",\n    });\n    const goBackAction = assertSuccessAction(goBackCtx, \"goBack\");\n    assert.equal(\n      (goBackAction.result as { response: unknown | null; url: string }).url,\n      METHODS_TEST_URL,\n    );\n    assert.equal(\n      (goBackAction.result as { response: unknown | null }).response,\n      null,\n    );\n\n    await withSessionPage(cdpUrl, async (page) => {\n      assert.equal(await page.title(), \"V4 methods route\");\n    });\n\n    const goForwardCtx = await postPageRoute(\"goForward\", sessionId, {\n      waitUntil: \"load\",\n    });\n    const goForwardAction = assertSuccessAction(goForwardCtx, \"goForward\");\n    assert.equal(\n      (goForwardAction.result as { response: unknown | null; url: string }).url,\n      GOTO_TEST_URL,\n    );\n    assert.equal(\n      (goForwardAction.result as { response: unknown | null }).response,\n      null,\n    );\n\n    await withSessionPage(cdpUrl, async (page) => {\n      assert.equal(await page.title(), \"V4 goto route\");\n    });\n\n    const temp = await createSessionWithCdp(headers);\n    try {\n      const closeGotoCtx = await postPageRoute(\"goto\", temp.sessionId, {\n        url: GOTO_TEST_URL,\n        waitUntil: \"load\",\n      });\n      assertSuccessAction(closeGotoCtx, \"goto\");\n\n      const closeCtx = await fetchWithContext<PageActionResponse>(\n        `${getBaseUrl()}/v4/page/close`,\n        {\n          method: \"POST\",\n          headers,\n          body: JSON.stringify({\n            sessionId: temp.sessionId,\n            params: {},\n          }),\n        },\n      );\n      assertSuccessAction(closeCtx, \"close\");\n\n      const browser = await chromium.connectOverCDP(temp.cdpUrl);\n      try {\n        const contexts = browser.contexts();\n        const pages = contexts.flatMap((context) => context.pages());\n        assert.equal(pages.length, 0);\n      } finally {\n        await browser.close();\n      }\n    } finally {\n      await endSession(temp.sessionId, headers);\n    }\n  });\n\n  it(\"GET page getters and POST page config methods expose the underlying understudy interface\", async () => {\n    const temp = await createSessionWithCdp(headers);\n    let requestHeaders: Record<string, string | string[] | undefined> | null =\n      null;\n    const server = createServer((req, res) => {\n      if (req.url === \"/\") {\n        requestHeaders = req.headers;\n      }\n\n      res.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" });\n      res.end(`<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>V4 header route</title>\n  </head>\n  <body>\n    <main id=\"message\">header-ok</main>\n  </body>\n</html>`);\n    });\n\n    await new Promise<void>((resolve) => {\n      server.listen(0, \"127.0.0.1\", () => resolve());\n    });\n\n    const address = server.address();\n    assert.ok(address && typeof address === \"object\");\n    const url = `http://127.0.0.1:${address.port}/`;\n\n    try {\n      const enableCursorOverlayCtx = await postPageRoute(\n        \"enableCursorOverlay\",\n        temp.sessionId,\n        {},\n      );\n      const enableCursorOverlayAction = assertSuccessAction(\n        enableCursorOverlayCtx,\n        \"enableCursorOverlay\",\n      );\n      assert.equal(\n        (enableCursorOverlayAction.result as { enabled: boolean }).enabled,\n        true,\n      );\n\n      const addInitScriptCtx = await postPageRoute(\n        \"addInitScript\",\n        temp.sessionId,\n        {\n          script: \"window.__v4InitValue = 'present';\",\n        },\n      );\n      const addInitScriptAction = assertSuccessAction(\n        addInitScriptCtx,\n        \"addInitScript\",\n      );\n      assert.equal(\n        (addInitScriptAction.result as { added: boolean }).added,\n        true,\n      );\n\n      const setHeadersCtx = await postPageRoute(\n        \"setExtraHTTPHeaders\",\n        temp.sessionId,\n        {\n          headers: {\n            \"x-stagehand-test\": \"present\",\n          },\n        },\n      );\n      const setHeadersAction = assertSuccessAction(\n        setHeadersCtx,\n        \"setExtraHTTPHeaders\",\n      );\n      assert.equal(\n        (\n          setHeadersAction.result as {\n            headers: Record<string, string>;\n          }\n        ).headers[\"x-stagehand-test\"],\n        \"present\",\n      );\n\n      const gotoCtx = await postPageRoute(\"goto\", temp.sessionId, {\n        url,\n        waitUntil: \"load\",\n      });\n      const gotoAction = assertSuccessAction(gotoCtx, \"goto\");\n      assert.equal(requestHeaders?.[\"x-stagehand-test\"], \"present\");\n\n      const targetIdCtx = await getPageRoute(\"targetId\", temp.sessionId, {});\n      const targetIdAction = assertSuccessAction(targetIdCtx, \"targetId\");\n      assert.equal(\n        (targetIdAction.result as { targetId: string }).targetId,\n        gotoAction.pageId,\n      );\n\n      const mainFrameIdCtx = await getPageRoute(\n        \"mainFrameId\",\n        temp.sessionId,\n        {},\n      );\n      const mainFrameIdAction = assertSuccessAction(\n        mainFrameIdCtx,\n        \"mainFrameId\",\n      );\n      const mainFrameId = (mainFrameIdAction.result as { mainFrameId: string })\n        .mainFrameId;\n      assert.equal(mainFrameId, await getMainFrameId(temp.cdpUrl));\n\n      const mainFrameCtx = await getPageRoute(\"mainFrame\", temp.sessionId, {});\n      const mainFrameAction = assertSuccessAction(mainFrameCtx, \"mainFrame\");\n      assert.equal(\n        (\n          mainFrameAction.result as {\n            frame: { frameId: string };\n          }\n        ).frame.frameId,\n        mainFrameId,\n      );\n\n      const framesCtx = await getPageRoute(\"frames\", temp.sessionId, {});\n      const framesAction = assertSuccessAction(framesCtx, \"frames\");\n      const frames = (\n        framesAction.result as {\n          frames: Array<{ frameId: string }>;\n        }\n      ).frames;\n      assert.ok(frames.some((frame) => frame.frameId === mainFrameId));\n\n      const fullFrameTreeCtx = await getPageRoute(\n        \"getFullFrameTree\",\n        temp.sessionId,\n        {},\n      );\n      const fullFrameTreeAction = assertSuccessAction(\n        fullFrameTreeCtx,\n        \"getFullFrameTree\",\n      );\n      assert.equal(\n        (\n          fullFrameTreeAction.result as {\n            frameTree: { frame: { id: string } };\n          }\n        ).frameTree.frame.id,\n        mainFrameId,\n      );\n\n      const protocolFrameTreeCtx = await getPageRoute(\n        \"asProtocolFrameTree\",\n        temp.sessionId,\n        { rootMainFrameId: mainFrameId },\n      );\n      const protocolFrameTreeAction = assertSuccessAction(\n        protocolFrameTreeCtx,\n        \"asProtocolFrameTree\",\n      );\n      assert.equal(\n        (\n          protocolFrameTreeAction.result as {\n            frameTree: { frame: { id: string } };\n          }\n        ).frameTree.frame.id,\n        mainFrameId,\n      );\n\n      const listAllFrameIdsCtx = await getPageRoute(\n        \"listAllFrameIds\",\n        temp.sessionId,\n        {},\n      );\n      const listAllFrameIdsAction = assertSuccessAction(\n        listAllFrameIdsCtx,\n        \"listAllFrameIds\",\n      );\n      const frameIds = (listAllFrameIdsAction.result as { frameIds: string[] })\n        .frameIds;\n      assert.ok(frameIds.includes(mainFrameId));\n      assert.deepEqual(\n        [...frameIds].sort(),\n        [...frames.map((frame) => frame.frameId)].sort(),\n      );\n\n      const getOrdinalCtx = await getPageRoute(\"getOrdinal\", temp.sessionId, {\n        frameId: mainFrameId,\n      });\n      const getOrdinalAction = assertSuccessAction(getOrdinalCtx, \"getOrdinal\");\n      assert.equal(\n        (getOrdinalAction.result as { frameId: string }).frameId,\n        mainFrameId,\n      );\n      assert.ok((getOrdinalAction.result as { ordinal: number }).ordinal >= 0);\n\n      const waitForMainLoadStateCtx = await postPageRoute(\n        \"waitForMainLoadState\",\n        temp.sessionId,\n        {\n          state: \"load\",\n          timeoutMs: 15_000,\n        },\n      );\n      assertSuccessAction(waitForMainLoadStateCtx, \"waitForMainLoadState\");\n\n      const evaluateCtx = await postPageRoute(\"evaluate\", temp.sessionId, {\n        expression: `({\n          title: document.title,\n          cursorOverlay: !!document.getElementById(\"__v3_cursor_overlay__\"),\n          initValue: globalThis.__v4InitValue ?? null\n        })`,\n      });\n      const evaluateAction = assertSuccessAction(evaluateCtx, \"evaluate\");\n      assert.deepEqual(evaluateAction.result, {\n        value: {\n          title: \"V4 header route\",\n          cursorOverlay: true,\n          initValue: \"present\",\n        },\n      });\n    } finally {\n      await new Promise<void>((resolve, reject) => {\n        server.close((error) => (error ? reject(error) : resolve()));\n      });\n      await endSession(temp.sessionId, headers);\n    }\n  });\n\n  it(\"GET /v4/page/action/:actionId returns the new envelope for a stored action\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: GOTO_TEST_URL,\n      waitUntil: \"load\",\n    });\n    const createdAction = assertSuccessAction(gotoCtx, \"goto\");\n\n    const detailCtx = await fetchWithContext<PageActionResponse>(\n      `${getBaseUrl()}/v4/page/action/${createdAction.id}?sessionId=${sessionId}`,\n      {\n        method: \"GET\",\n        headers,\n      },\n    );\n\n    assertFetchStatus(detailCtx, HTTP_OK);\n    assertFetchOk(\n      detailCtx.body !== null,\n      \"Expected a JSON response body\",\n      detailCtx,\n    );\n    assert.equal(detailCtx.body.success, true);\n    assert.equal(detailCtx.body.error, null);\n    assertFetchOk(\n      detailCtx.body.action !== undefined,\n      \"Expected an action payload\",\n      detailCtx,\n    );\n    assert.equal(detailCtx.body.action.id, createdAction.id);\n    assert.equal(detailCtx.body.action.method, \"goto\");\n    assert.equal(detailCtx.body.action.sessionId, sessionId);\n  });\n\n  it(\"GET /v4/page/action returns the new envelope with action history\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: CLICK_TEST_URL,\n      waitUntil: \"load\",\n    });\n    const gotoAction = assertSuccessAction(gotoCtx, \"goto\");\n\n    const clickCtx = await postPageRoute(\"click\", sessionId, {\n      selector: {\n        xpath: \"//button[@id='click-target']\",\n      },\n    });\n    const clickAction = assertSuccessAction(clickCtx, \"click\");\n\n    const listCtx = await fetchWithContext<PageActionResponse>(\n      `${getBaseUrl()}/v4/page/action?sessionId=${sessionId}`,\n      {\n        method: \"GET\",\n        headers,\n      },\n    );\n\n    const actions = assertSuccessActionList(listCtx);\n    const actionIds = new Set(actions.map((action) => action.id));\n\n    assert.ok(actionIds.has(gotoAction.id), \"Expected goto action in history\");\n    assert.ok(\n      actionIds.has(clickAction.id),\n      \"Expected click action in history\",\n    );\n\n    const listedClickAction = actions.find(\n      (action) => action.id === clickAction.id,\n    );\n    assert.ok(listedClickAction, \"Expected click action details in history\");\n    assert.equal(listedClickAction.method, \"click\");\n    assert.equal(listedClickAction.sessionId, sessionId);\n  });\n\n  it(\"GET /v4/page/action still returns stored actions after the session ends\", async () => {\n    const temp = await createSessionWithCdp(headers);\n    try {\n      const gotoCtx = await postPageRoute(\"goto\", temp.sessionId, {\n        url: GOTO_TEST_URL,\n        waitUntil: \"load\",\n      });\n      const action = assertSuccessAction(gotoCtx, \"goto\");\n\n      await endSession(temp.sessionId, headers);\n\n      const detailCtx = await fetchWithContext<PageActionResponse>(\n        `${getBaseUrl()}/v4/page/action/${action.id}?sessionId=${temp.sessionId}`,\n        {\n          method: \"GET\",\n          headers,\n        },\n      );\n      const fetchedAction = assertSuccessAction(detailCtx, \"goto\");\n      assert.equal(fetchedAction.id, action.id);\n\n      const listCtx = await fetchWithContext<PageActionResponse>(\n        `${getBaseUrl()}/v4/page/action?sessionId=${temp.sessionId}`,\n        {\n          method: \"GET\",\n          headers,\n        },\n      );\n      const actions = assertSuccessActionList(listCtx);\n      assert.ok(actions.some((candidate) => candidate.id === action.id));\n    } finally {\n      await endSession(temp.sessionId, headers);\n    }\n  });\n\n  it(\"POST /v4/page/click accepts css, text, and coordinate selector types\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: CLICK_TEST_URL,\n      waitUntil: \"load\",\n    });\n    assertSuccessAction(gotoCtx, \"goto\");\n\n    const cssSelectorCtx = await postPageRoute(\"click\", sessionId, {\n      selector: { css: \"#click-target\" },\n    });\n    assertSuccessAction(cssSelectorCtx, \"click\");\n\n    const cssWithIndexCtx = await postPageRoute(\"click\", sessionId, {\n      selector: { css: \"button\", idx: 0 },\n    });\n    assertSuccessAction(cssWithIndexCtx, \"click\");\n\n    const xpathWithIndexCtx = await postPageRoute(\"click\", sessionId, {\n      selector: { xpath: \"//button\", idx: 0 },\n    });\n    assertSuccessAction(xpathWithIndexCtx, \"click\");\n\n    const textWithIndexCtx = await postPageRoute(\"click\", sessionId, {\n      selector: { text: \"Submit\", idx: 0 },\n    });\n    assertSuccessAction(textWithIndexCtx, \"click\");\n\n    const textSelectorCtx = await postPageRoute(\"click\", sessionId, {\n      selector: { text: \"Submit\" },\n    });\n    assertSuccessAction(textSelectorCtx, \"click\");\n\n    const coordSelectorCtx = await postPageRoute(\"click\", sessionId, {\n      selector: { x: 100, y: 200 },\n    });\n    assertSuccessAction(coordSelectorCtx, \"click\");\n  });\n\n  it(\"POST /v4/page/dragAndDrop accepts mixed selector types (xpath from, coordinates to)\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: METHODS_TEST_URL,\n      waitUntil: \"load\",\n    });\n    assertSuccessAction(gotoCtx, \"goto\");\n\n    const dragCtx = await postPageRoute(\"dragAndDrop\", sessionId, {\n      from: { xpath: \"//div[@id='drag-source']\" },\n      to: { x: 200, y: 300 },\n    });\n    assertSuccessAction(dragCtx, \"dragAndDrop\");\n  });\n\n  it(\"POST /v4/page/click returns the new top-level failure shape for validation errors\", async () => {\n    const ctx = await postPageRoute(\"click\", sessionId, {});\n\n    assertFetchStatus(ctx, HTTP_BAD_REQUEST);\n    assertFetchOk(ctx.body !== null, \"Expected a JSON response body\", ctx);\n    assert.equal(ctx.body.success, false);\n    assert.equal(ctx.body.statusCode, HTTP_BAD_REQUEST);\n    assert.equal(typeof ctx.body.error, \"string\");\n    assert.ok(ctx.body.error);\n    assert.ok(\n      ctx.body.stack === null || typeof ctx.body.stack === \"string\",\n      \"Expected stack to be null or a string\",\n    );\n    assert.equal(ctx.body.action, undefined);\n    assert.equal(ctx.body.actions, undefined);\n  });\n\n  it(\"POST /v4/page routes return the underlying error message and stack for route failures\", async () => {\n    const gotoCtx = await postPageRoute(\"goto\", sessionId, {\n      url: CLICK_TEST_URL,\n      waitUntil: \"load\",\n    });\n    assertSuccessAction(gotoCtx, \"goto\");\n\n    const ctx = await postPageRoute(\"click\", sessionId, {\n      selector: {\n        xpath: \"//button[@id='missing-target']\",\n      },\n    });\n\n    assertFetchStatus(ctx, 404);\n    assertFetchOk(ctx.body !== null, \"Expected a JSON response body\", ctx);\n    assert.equal(ctx.body.success, false);\n    assert.equal(ctx.body.statusCode, 404);\n    assert.equal(typeof ctx.body.error, \"string\");\n    assert.ok(ctx.body.error);\n    assert.equal(typeof ctx.body.stack, \"string\");\n    assert.ok(ctx.body.stack);\n    assertFetchOk(\n      ctx.body.action !== undefined,\n      \"Expected a failed action payload\",\n      ctx,\n    );\n    assert.equal(ctx.body.action.status, \"failed\");\n  });\n});\n"
  },
  {
    "path": "packages/server-v4/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"verbatimModuleSyntax\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/server-v4/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"test\",\n    \"outDir\": \"dist/tests\",\n    \"declaration\": false,\n    \"noEmit\": false\n  },\n  \"include\": [\"test/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/server-v4/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: \"node\",\n    include: [\"test/**/*.test.ts\"],\n  },\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/core\"\n  - \"packages/cli\"\n  - \"packages/evals\"\n  - \"packages/docs\"\n  - \"packages/server-v3\"\n  - \"packages/server-v4\"\n"
  },
  {
    "path": "stainless.yml",
    "content": "# yaml-language-server: $schema=https://app.stainless.com/config-internal.schema.json\n\n##########################################################################\n############ DO NOT EDIT THIS FILE IN THE STAINLESS STUDIO UI ############\n############ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ############\n############ ONLY EDIT IN browserbase/stagehand/stainless.yml ############\n##########################################################################\n\nedition: 2025-10-10\n\norganization:\n  name: stagehand\n  docs: https://docs.stagehand.dev\n  contact: \"\"\n\ntargets:\n  python:\n    edition: python.2025-11-20\n    package_name: stagehand\n    project_name: stagehand\n    production_repo: browserbase/stagehand-python\n    publish:\n      pypi: true\n  go:\n    edition: go.2025-10-08\n    package_name: stagehand\n    production_repo: browserbase/stagehand-go\n    options:\n      enable_v2: true\n  java:\n    edition: java.2025-10-08\n    reverse_domain: com.browserbase.api\n    package_name: stagehand\n    production_repo: browserbase/stagehand-java\n    publish:\n      maven:\n        sonatype_platform: portal\n  kotlin:\n    edition: kotlin.2025-10-08\n    reverse_domain: com.browserbase.api\n    package_name: stagehand\n    production_repo: browserbase/stagehand-kotlin\n    publish:\n      maven:\n        sonatype_platform: portal\n  ruby:\n    edition: ruby.2025-10-08\n    gem_name: stagehand\n    production_repo: browserbase/stagehand-ruby\n    publish:\n      rubygems: false\n  typescript:\n    edition: typescript.2025-10-10\n    package_name: stagehand-sdk\n    production_repo: null\n    publish:\n      npm: false\n    options:\n      mcp_server: false\n  php:\n    edition: php.2025-10-08\n    package_name: stagehand\n    production_repo: browserbase/stagehand-php\n    composer_package_name: browserbase/stagehand\n    publish:\n      packagist: true\n  csharp:\n    edition: csharp.2025-10-08\n    package_name: stagehand\n    production_repo: browserbase/stagehand-net\n    publish:\n      nuget: true\n  # cli:\n  #   edition: cli.2025-10-08\n  #   binary_name: stagehand\n  #   production_repo: browserbase/stagehand-cli\n\n# `environments` are a map of the name of the environment (e.g. \"sandbox\",\n# \"production\") to the corresponding url to use.\nenvironments:\n  production: https://api.stagehand.browserbase.com\n  # dev: https://api.stagehand.dev.browserbase.com\n  # local: http://stagehand-api.localhost\n\n# OpenAPI transforms applied by Stainless during SDK generation.\n# This keeps the generated `packages/server-v3/openapi.v3.yaml` faithful to the Fastify+Zod source,\n# while still producing a Stainless-compatible spec for codegen.\nopenapi:\n  code_samples: mintlify\n  transforms:\n    # Stainless doesn't support `propertyNames` (emitted by some JSON Schema generators).\n    - command: remove\n      reason: Remove unsupported JSON Schema keyword\n      args:\n        target: \"$..propertyNames\"\n\n    # Empty-schema `additionalProperties: {}` is equivalent to `true`, and avoids Stainless issues.\n    - command: update\n      reason: Treat record value schema as any\n      args:\n        target: \"$.components.schemas.BrowserbaseSessionCreateParams.properties.userMetadata.additionalProperties\"\n        value: true\n    - command: update\n      reason: Treat record value schema as any\n      args:\n        target: \"$.components.schemas.BrowserbaseSessionCreateParamsOutput.properties.userMetadata.additionalProperties\"\n        value: true\n    - command: update\n      reason: Treat record value schema as any\n      args:\n        target: \"$.components.schemas.ExtractRequest.properties.schema.additionalProperties\"\n        value: true\n    - command: update\n      reason: Treat record value schema as any\n      args:\n        target: \"$.components.schemas.AgentResultData.properties.metadata.additionalProperties\"\n        value: true\n    - command: update\n      reason: Treat record value schema as any\n      args:\n        target: \"$.components.schemas.AgentResultDataOutput.properties.metadata.additionalProperties\"\n        value: true\n    - command: update\n      reason: Treat passthrough schema as any\n      args:\n        target: \"$.components.schemas.AgentAction.additionalProperties\"\n        value: true\n\n    # Add a stable title to help Stainless infer a consistent name for this anonymous array schema.\n    - command: merge\n      reason: Improve name inference for anonymous arrays\n      args:\n        target: '$.components.schemas.BrowserbaseSessionCreateParams.properties.proxies.anyOf[?(@.type == \"array\")]'\n        value:\n          title: ProxyConfigList\n    - command: merge\n      reason: Improve name inference for anonymous arrays\n      args:\n        target: '$.components.schemas.BrowserbaseSessionCreateParamsOutput.properties.proxies.anyOf[?(@.type == \"array\")]'\n        value:\n          title: ProxyConfigList\n\n    # `result` is intentionally untyped and should be treated as `any` in Stainless.\n    - command: merge\n      reason: Treat StreamEventSystemData.result as any\n      args:\n        target: \"$.components.schemas.StreamEventSystemData.properties.result\"\n        value:\n          x-stainless-any: true\n    - command: merge\n      reason: Treat StreamEventSystemDataOutput.result as any\n      args:\n        target: \"$.components.schemas.StreamEventSystemDataOutput.properties.result\"\n        value:\n          x-stainless-any: true\n\n# `resources` define the structure and organization for your API, such as how\n# methods and models are grouped together and accessed. See the [configuration\n# guide] for more information.\n#\n# [configuration guide]: https://www.stainless.com/docs/guides/configure#resources\nresources:\n  sessions:\n    models:\n      action: \"#/components/schemas/Action\"\n      model_config: \"#/components/schemas/ModelConfig\"\n      stream_event: \"#/components/schemas/StreamEvent\"\n    methods:\n      start: post /v1/sessions/start\n      act:\n        endpoint: post /v1/sessions/{id}/act\n        type: http\n        streaming:\n          param_discriminator: streamResponse\n          stream_event_model: sessions.stream_event\n          params_type_name: streamResponse\n      extract:\n        endpoint: post /v1/sessions/{id}/extract\n        type: http\n        streaming:\n          param_discriminator: streamResponse\n          stream_event_model: sessions.stream_event\n          params_type_name: streamResponse\n      observe:\n        endpoint: post /v1/sessions/{id}/observe\n        type: http\n        streaming:\n          param_discriminator: streamResponse\n          stream_event_model: sessions.stream_event\n          params_type_name: streamResponse\n      execute:\n        endpoint: post /v1/sessions/{id}/agentExecute\n        type: http\n        streaming:\n          param_discriminator: streamResponse\n          stream_event_model: sessions.stream_event\n          params_type_name: streamResponse\n      navigate: post /v1/sessions/{id}/navigate\n      replay: get /v1/sessions/{id}/replay\n      end: post /v1/sessions/{id}/end\n\nstreaming:\n  on_event:\n    - data_starts_with: '{\"data\":{\"status\":\"finished\"'\n      handle: done\n    - data_starts_with: error\n      handle: error\n    - event_type: null\n      handle: yield\n\nsettings:\n  # All generated integration tests that hit the prism mock http server are marked\n  # as skipped. Removing this setting or setting it to false enables tests, but\n  # doing so may result in test failures due to bugs in the test server.\n  #\n  # [prism mock http server]: https://stoplight.io/open-source/prism\n  disable_mock_tests: true\n  license: MIT\n\n# `client_settings` define settings for the API client, such as extra constructor\n# arguments (used for authentication), retry behavior, idempotency, etc.\nclient_settings:\n  opts:\n    BROWSERBASE_API_KEY:\n      type: string\n      read_env: BROWSERBASE_API_KEY\n      description: Your [Browserbase API Key](https://www.browserbase.com/settings)\n      nullable: false\n      auth:\n        security_scheme: BBApiKeyAuth\n    BROWSERBASE_PROJECT_ID:\n      type: string\n      read_env: BROWSERBASE_PROJECT_ID\n      description: Your [Browserbase Project ID](https://www.browserbase.com/settings)\n      nullable: false\n      auth:\n        security_scheme: BBProjectIdAuth\n    MODEL_API_KEY:\n      type: string\n      read_env: MODEL_API_KEY\n      description: Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)\n      nullable: false\n      auth:\n        security_scheme: LLMModelApiKeyAuth\n\nsecurity_schemes:\n  BBApiKeyAuth:\n    type: apiKey\n    in: header\n    name: x-bb-api-key\n  BBProjectIdAuth:\n    type: apiKey\n    in: header\n    name: x-bb-project-id\n  LLMModelApiKeyAuth:\n    type: apiKey\n    in: header\n    name: x-model-api-key\n\nsecurity:\n  - BBApiKeyAuth: []\n    BBProjectIdAuth: []\n    LLMModelApiKeyAuth: []\n\n# `readme` is used to configure the code snippets that will be rendered in the\n# README.md of various SDKs.\nreadme:\n  example_requests:\n    default:\n      type: request\n      endpoint: post /v1/sessions/start\n      params:\n        modelName: \"openai/gpt-5-nano\"\n    headline:\n      type: request\n      endpoint: post /v1/sessions/{id}/act\n      params:\n        input: \"click the first link on the page\"\n        id: \"00000000-your-session-id-000000000000\"\n\ndiagnostics:\n  ignored:\n    Ruby/NameNotAllowed: true\n    Ruby/NameShadowedBuiltin: true\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"noImplicitAny\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"node\",\n    \"sourceMap\": true,\n    \"inlineSources\": true,\n    \"declaration\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"*\": [\"node_modules/*\", \"packages/core/lib/types/*\"],\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"dist\", \".eslintrc.cjs\"]\n}\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://v2-8-10.turborepo.dev/schema.json\",\n  \"globalEnv\": [\n    \"CI\"\n  ],\n  \"globalDependencies\": [\n    \".github/workflows/ci.yml\",\n    \"package.json\",\n    \"packages/*/package.json\",\n    \"tsconfig.json\",\n    \"tsconfig.base.json\",\n    \"eslint.config.mjs\",\n    \".prettierrc\",\n    \".prettierignore\"\n  ],\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\n        \"dist/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/**\"\n      ]\n    },\n    \"@browserbasehq/stagehand#build\": {\n      \"dependsOn\": [\n        \"^build\",\n        \"gen-version\",\n        \"build-dom-scripts:dom\",\n        \"build-dom-scripts:locator\",\n        \"build-dom-scripts:screenshot\",\n        \"build-dom-scripts:a11y\"\n      ],\n      \"outputs\": [\n        \"dist/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!lib/version.ts\",\n        \"!lib/dom/build/**\",\n        \"!lib/v3/dom/build/**\",\n        \"!dist/**\"\n      ]\n    },\n    \"@browserbasehq/stagehand-server-v3#build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\n        \"dist/**\",\n        \"openapi.v3.yaml\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/**\",\n        \"!openapi.v3.yaml\"\n      ]\n    },\n    \"@browserbasehq/stagehand-server-v4#build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\n        \"dist/**\",\n        \"openapi.v4.yaml\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/**\",\n        \"!openapi.v4.yaml\"\n      ]\n    },\n    \"@browserbasehq/browse-cli#build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\n        \"dist/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsup.config.ts\",\n        \"!dist/**\"\n      ]\n    },\n    \"@browserbasehq/stagehand-server-v4#gen:openapi\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\"openapi.v4.yaml\"],\n      \"inputs\": [\n        \"src/**/*.ts\",\n        \"scripts/gen-openapi.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!openapi.v4.yaml\"\n      ]\n    },\n    \"@browserbasehq/stagehand#build:esm\": {\n      \"dependsOn\": [\n        \"^build:esm\",\n        \"gen-version\",\n        \"build-dom-scripts:dom\",\n        \"build-dom-scripts:locator\",\n        \"build-dom-scripts:screenshot\",\n        \"build-dom-scripts:a11y\"\n      ],\n      \"outputs\": [\n        \"dist/esm/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!lib/version.ts\",\n        \"!lib/dom/build/**\",\n        \"!lib/v3/dom/build/**\",\n        \"!dist/**\"\n      ]\n    },\n    \"@browserbasehq/stagehand#build:cjs\": {\n      \"dependsOn\": [\n        \"^build:cjs\",\n        \"gen-version\",\n        \"build-dom-scripts:dom\",\n        \"build-dom-scripts:locator\",\n        \"build-dom-scripts:screenshot\",\n        \"build-dom-scripts:a11y\"\n      ],\n      \"outputs\": [\n        \"dist/cjs/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!lib/version.ts\",\n        \"!lib/dom/build/**\",\n        \"!lib/v3/dom/build/**\",\n        \"!dist/**\"\n      ]\n    },\n    \"gen-version\": {\n      \"outputs\": [\"lib/version.ts\"],\n      \"inputs\": [\n        \"scripts/gen-version.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!lib/version.ts\"\n      ]\n    },\n    \"build-dom-scripts:dom\": {\n      \"outputs\": [\n        \"lib/v3/dom/build/v3-index.js\",\n        \"lib/v3/dom/build/scriptV3Content.ts\",\n        \"lib/v3/dom/build/rerender-index.js\",\n        \"lib/v3/dom/build/reRenderScriptContent.ts\"\n      ],\n      \"inputs\": [\n        \"lib/v3/dom/genDomScripts.ts\",\n        \"lib/v3/dom/**/*.ts\",\n        \"!lib/v3/dom/build/**\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"build-dom-scripts:locator\": {\n      \"outputs\": [\n        \"lib/v3/dom/build/locatorScripts.generated.ts\"\n      ],\n      \"inputs\": [\n        \"lib/v3/dom/genLocatorScripts.ts\",\n        \"lib/v3/dom/locatorScripts/**/*.ts\",\n        \"!lib/v3/dom/build/**\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"build-dom-scripts:screenshot\": {\n      \"outputs\": [\n        \"lib/v3/dom/build/screenshotScripts.generated.ts\"\n      ],\n      \"inputs\": [\n        \"lib/v3/dom/genScreenshotScripts.ts\",\n        \"lib/v3/dom/screenshotScripts/**/*.ts\",\n        \"!lib/v3/dom/build/**\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"build-dom-scripts:a11y\": {\n      \"outputs\": [\n        \"lib/v3/dom/build/a11yScripts.generated.ts\"\n      ],\n      \"inputs\": [\n        \"lib/v3/dom/genA11yScripts.ts\",\n        \"lib/v3/dom/a11yScripts/**/*.ts\",\n        \"!lib/v3/dom/build/**\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"build:esm\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\n        \"dist/esm/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/esm/**\"\n      ]\n    },\n    \"build:esm-tests\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\"dist/tests/**\"],\n      \"inputs\": [\n        \"test/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/tests/**\"\n      ]\n    },\n    \"build:server:dist\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\n        \"dist/lib/**\",\n        \"dist/routes/**\",\n        \"dist/types/**\",\n        \"dist/*.js\",\n        \"dist/*.js.map\",\n        \"dist/*.d.ts\"\n      ],\n      \"inputs\": [\n        \"src/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/lib/**\",\n        \"!dist/routes/**\",\n        \"!dist/types/**\",\n        \"!dist/*.js\",\n        \"!dist/*.js.map\",\n        \"!dist/*.d.ts\"\n      ]\n    },\n    \"gen:openapi\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\"openapi.v3.yaml\"],\n      \"inputs\": [\n        \"src/**/*.ts\",\n        \"scripts/gen-openapi.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!openapi.v3.yaml\"\n      ]\n    },\n    \"build:sea:esm\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\n        \"dist/sea/**\",\n        \"dist/app.mjs\"\n      ],\n      \"env\": [\n        \"SEA_BUILD_MODE\",\n        \"SEA_TARGET_PLATFORM\",\n        \"SEA_TARGET_ARCH\",\n        \"SEA_BINARY_NAME\"\n      ],\n      \"inputs\": [\n        \"src/**/*.ts\",\n        \"scripts/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/sea/**\",\n        \"!dist/app.mjs\"\n      ]\n    },\n    \"build:sea:cjs\": {\n      \"dependsOn\": [\"^build:cjs\"],\n      \"outputs\": [\"dist/sea/**\"],\n      \"env\": [\n        \"SEA_BUILD_MODE\",\n        \"SEA_TARGET_PLATFORM\",\n        \"SEA_TARGET_ARCH\",\n        \"SEA_BINARY_NAME\"\n      ],\n      \"inputs\": [\n        \"src/**/*.ts\",\n        \"scripts/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/sea/**\"\n      ]\n    },\n    \"build:cjs\": {\n      \"dependsOn\": [\"^build:cjs\"],\n      \"outputs\": [\n        \"dist/cjs/**\"\n      ],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.js\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/cjs/**\"\n      ]\n    },\n    \"build:cli\": {\n      \"dependsOn\": [\"^build:esm\"],\n      \"outputs\": [\"dist/cli/**\"],\n      \"inputs\": [\n        \"cli.ts\",\n        \"evals.config.json\",\n        \"scripts/**\",\n        \"!dist/cli/**\"\n      ]\n    },\n    \"lint\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [],\n      \"inputs\": [\n        \"**/*.ts\",\n        \"**/*.tsx\",\n        \"**/*.js\",\n        \"**/*.jsx\",\n        \"**/*.mjs\",\n        \"**/*.cjs\",\n        \"**/*.mts\",\n        \"**/*.cts\",\n        \"**/*.md\",\n        \"**/*.mdx\",\n        \"**/*.yml\",\n        \"**/*.yaml\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!dist/**\",\n        \"!node_modules/**\",\n        \"!.turbo/**\"\n      ]\n    },\n    \"format\": {\n      \"outputs\": [],\n      \"cache\": false\n    },\n    \"test\": {\n      \"dependsOn\": [],\n      \"outputs\": [],\n      \"inputs\": [\n        \"**/*.spec.ts\",\n        \"**/*.test.ts\"\n      ]\n    },\n    \"test:core\": {\n      \"dependsOn\": [\"build:esm\"],\n      \"outputs\": [],\n      \"cache\": false,\n      \"env\": [\n        \"BROWSERBASE_FLOW_LOGS\",\n        \"VITEST_CONSOLE_REPORTER\"\n      ],\n      \"inputs\": [\n        \"tests/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"test:e2e\": {\n      \"dependsOn\": [\"build:esm\"],\n      \"outputs\": [\n        \"playwright-report/**\",\n        \"test-results/**\"\n      ],\n      \"cache\": false,\n      \"env\": [\n        \"BB_API_KEY\",\n        \"BB_PROJECT_ID\",\n        \"BROWSERBASE_REGION\",\n        \"BROWSERBASE_SESSION_LIMIT_PER_E2E_TEST\",\n        \"IFRAME_CHILD_FRAME_TIMEOUT_MS\",\n        \"IFRAME_DEBUG\",\n        \"IFRAME_POPUP_TIMEOUT_MS\",\n        \"IFRAME_POPUP_URL_TIMEOUT_MS\",\n        \"KEEP_ALIVE_ACTION_EXIT_TIMEOUT_MS\",\n        \"KEEP_ALIVE_BB_INFO_TIMEOUT_MS\",\n        \"KEEP_ALIVE_BB_TIMEOUT_MS\",\n        \"KEEP_ALIVE_DEBUG\",\n        \"KEEP_ALIVE_LOCAL_INFO_TIMEOUT_MS\",\n        \"KEEP_ALIVE_LOCAL_TIMEOUT_MS\",\n        \"KEEP_ALIVE_STAY_OPEN_MS\",\n        \"KEEP_ALIVE_VIEW_MS\",\n        \"LOCAL_SESSION_LIMIT_PER_E2E_TEST\",\n        \"PLAYWRIGHT_CONSOLE_REPORTER\",\n        \"STAGEHAND_BROWSER_TARGET\",\n        \"STAGEHAND_API_URL\"\n      ],\n      \"passThroughEnv\": [\n        \"ANTHROPIC_API_KEY\",\n        \"BROWSERBASE_API_KEY\",\n        \"BROWSERBASE_CDP_CONNECT_MAX_MS\",\n        \"BROWSERBASE_PROJECT_ID\",\n        \"BROWSERBASE_SESSION_CREATE_MAX_MS\",\n        \"CHROME_PATH\",\n        \"CTRF_JUNIT_PATH\",\n        \"GEMINI_API_KEY\",\n        \"GOOGLE_GENERATIVE_AI_API_KEY\",\n        \"HEADLESS\",\n        \"LLM_MAX_MS\",\n        \"NODE_OPTIONS\",\n        \"NODE_V8_COVERAGE\",\n        \"OPENAI_API_KEY\",\n        \"STAGEHAND_SERVER_TARGET\"\n      ],\n      \"inputs\": [\n        \"lib/v3/tests/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\",\n        \"!playwright-report/**\",\n        \"!test-results/**\"\n      ]\n    },\n    \"test:evals\": {\n      \"dependsOn\": [\"build:esm\", \"build:cli\"],\n      \"outputs\": [],\n      \"cache\": false,\n      \"env\": [\n        \"AGENT_EVAL_MAX_STEPS\",\n        \"BRAINTRUST_API_KEY\",\n        \"EVAL_AGENT_MODELS\",\n        \"EVAL_AGENT_MODELS_CUA\",\n        \"EVAL_CATEGORIES\",\n        \"EVAL_DATASET\",\n        \"EVAL_ENV\",\n        \"EVAL_GAIA_FILE\",\n        \"EVAL_GAIA_LEVEL\",\n        \"EVAL_GAIA_LIMIT\",\n        \"EVAL_GAIA_SAMPLE\",\n        \"EVAL_MAX_CONCURRENCY\",\n        \"EVAL_MAX_K\",\n        \"EVAL_MODELS\",\n        \"EVAL_ONLINEMIND2WEB_LIMIT\",\n        \"EVAL_ONLINEMIND2WEB_SAMPLE\",\n        \"EVAL_PROVIDER\",\n        \"EVAL_TRIAL_COUNT\",\n        \"EVAL_WEBVOYAGER_LIMIT\",\n        \"EVAL_WEBVOYAGER_SAMPLE\",\n        \"OP_AUTO_ENV_DISABLE\",\n        \"OP_ENV_FILE\",\n        \"STAGEHAND_BROWSER_TARGET\",\n        \"USE_API\"\n      ],\n      \"passThroughEnv\": [\n        \"ANTHROPIC_API_KEY\",\n        \"BROWSERBASE_API_KEY\",\n        \"BROWSERBASE_CDP_CONNECT_MAX_MS\",\n        \"BROWSERBASE_PROJECT_ID\",\n        \"BROWSERBASE_SESSION_CREATE_MAX_MS\",\n        \"CHROME_PATH\",\n        \"CTRF_JUNIT_PATH\",\n        \"GEMINI_API_KEY\",\n        \"GOOGLE_GENERATIVE_AI_API_KEY\",\n        \"HEADLESS\",\n        \"LLM_MAX_MS\",\n        \"NODE_OPTIONS\",\n        \"NODE_V8_COVERAGE\",\n        \"OPENAI_API_KEY\",\n        \"STAGEHAND_SERVER_TARGET\"\n      ],\n      \"inputs\": [\n        \"scripts/**\",\n        \"cli.ts\",\n        \"evals.config.json\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"test:server\": {\n      \"dependsOn\": [\"build:sea:esm\", \"build:esm-tests\"],\n      \"outputs\": [],\n      \"cache\": false,\n      \"env\": [\n        \"BB_ENV\",\n        \"NODE_ENV\",\n        \"NODE_TEST_CONSOLE_REPORTER\",\n        \"NODE_TEST_REPORTER\",\n        \"NODE_TEST_REPORTER_DESTINATION\",\n        \"SEA_BINARY_NAME\",\n        \"STAGEHAND_BASE_URL\",\n        \"STAGEHAND_SEA_CACHE_DIR\",\n        \"STAGEHAND_TEST_LOCAL_CONNECT_TIMEOUT_MS\"\n      ],\n      \"passThroughEnv\": [\n        \"ANTHROPIC_API_KEY\",\n        \"BROWSERBASE_API_KEY\",\n        \"BROWSERBASE_CDP_CONNECT_MAX_MS\",\n        \"BROWSERBASE_PROJECT_ID\",\n        \"BROWSERBASE_SESSION_CREATE_MAX_MS\",\n        \"CHROME_PATH\",\n        \"CTRF_JUNIT_PATH\",\n        \"GEMINI_API_KEY\",\n        \"GOOGLE_GENERATIVE_AI_API_KEY\",\n        \"HEADLESS\",\n        \"LLM_MAX_MS\",\n        \"NODE_OPTIONS\",\n        \"NODE_V8_COVERAGE\",\n        \"OPENAI_API_KEY\",\n        \"STAGEHAND_BROWSER_TARGET\",\n        \"STAGEHAND_SERVER_TARGET\"\n      ],\n      \"inputs\": [\n        \"test/**/*.ts\",\n        \"scripts/**\",\n        \"src/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"tsconfig.*.json\"\n      ]\n    },\n    \"test:cli\": {\n      \"dependsOn\": [\"build\"],\n      \"outputs\": [],\n      \"cache\": false,\n      \"inputs\": [\n        \"tests/**/*.ts\",\n        \"src/**/*.ts\",\n        \"package.json\",\n        \"tsconfig.json\",\n        \"vitest.config.ts\"\n      ]\n    },\n    \"docs\": {\n      \"persistent\": true,\n      \"cache\": false\n    },\n    \"dev\": {\n      \"persistent\": true,\n      \"cache\": false\n    }\n  }\n}\n"
  }
]